From 01984e2ba89bf3a6123877063fd6e6bba181c9a4 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Tue, 26 Jul 2022 21:32:10 +0200 Subject: [PATCH 01/18] Update models.py, __init__.py, and office365.py --- django_mailbox/models.py | 5 +- django_mailbox/transports/__init__.py | 1 + django_mailbox/transports/office365.py | 129 +++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 django_mailbox/transports/office365.py diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 0f0ac9d2..9ef29ac2 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -31,7 +31,7 @@ from django_mailbox.signals import message_received from django_mailbox.transports import Pop3Transport, ImapTransport, \ MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ - MMDFTransport, GmailImapTransport + MMDFTransport, GmailImapTransport, Office365Transport logger = logging.getLogger(__name__) @@ -223,6 +223,9 @@ def get_connection(self): ssl=self.use_ssl ) conn.connect(self.username, self.password) + elif self.type == 'office365': + conn = Office365Transport(self.location) + conn.connect(self.username, self.password) elif self.type == 'maildir': conn = MaildirTransport(self.location) elif self.type == 'mbox': diff --git a/django_mailbox/transports/__init__.py b/django_mailbox/transports/__init__.py index 7aeef95e..9f733e34 100644 --- a/django_mailbox/transports/__init__.py +++ b/django_mailbox/transports/__init__.py @@ -8,3 +8,4 @@ from django_mailbox.transports.mh import MHTransport from django_mailbox.transports.mmdf import MMDFTransport from django_mailbox.transports.gmail import GmailImapTransport +from django_mailbox.transports.office365 import Office365Transport diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py new file mode 100644 index 00000000..8ce5554e --- /dev/null +++ b/django_mailbox/transports/office365.py @@ -0,0 +1,129 @@ +import O365 +import logging + +from django.conf import settings + +from .base import EmailTransport, MessageParseError + +logger = logging.getLogger(__name__) + + +class Office365Transport(EmailTransport): + def __init__( + self, hostname, port=None, ssl=False, tls=False, + archive='', folder=None, + ): + self.max_message_size = getattr( + settings, + 'DJANGO_MAILBOX_MAX_MESSAGE_SIZE', + False + ) + self.integration_testing_subject = getattr( + settings, + 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', + None + ) + # self.hostname = hostname + # self.port = port + # self.archive = archive + # self.folder = folder + # self.tls = tls + # if ssl: + # # self.transport = imaplib.IMAP4_SSL + # if not self.port: + # self.port = 993 + # else: + # self.transport = imaplib.IMAP4 + # if not self.port: + # self.port = 143 + + def connect(self, username, password): + # self.server = self.transport(self.hostname, self.port) + # if self.tls: + # self.server.starttls() + # typ, msg = self.server.login(username, password) + # + # if self.folder: + # self.server.select(self.folder) + # else: + # self.server.select() + + def _get_all_message_ids(self): + # # Fetch all the message uids + # response, message_ids = self.server.uid('search', None, 'ALL') + # message_id_string = message_ids[0].strip() + # # Usually `message_id_string` will be a list of space-separated + # # ids; we must make sure that it isn't an empty string before + # # splitting into individual UIDs. + # if message_id_string: + # return message_id_string.decode().split(' ') + # return [] + + def _get_small_message_ids(self, message_ids): + # Using existing message uids, get the sizes and + # return only those that are under the size + # limit + safe_message_ids = [] + + status, data = self.server.uid( + 'fetch', + ','.join(message_ids), + '(RFC822.SIZE)' + ) + + for each_msg in data: + each_msg = each_msg.decode() + try: + uid = each_msg.split(' ')[2] + size = each_msg.split(' ')[4].rstrip(')') + if int(size) <= int(self.max_message_size): + safe_message_ids.append(uid) + except ValueError as e: + logger.warning( + "ValueError: {} working on {}".format(e, each_msg[0]) + ) + pass + return safe_message_ids + + def get_message(self, condition=None): + message_ids = self._get_all_message_ids() + + if not message_ids: + return + + # Limit the uids to the small ones if we care about that + if self.max_message_size: + message_ids = self._get_small_message_ids(message_ids) + + if self.archive: + typ, folders = self.server.list(pattern=self.archive) + if folders[0] is None: + # If the archive folder does not exist, create it + self.server.create(self.archive) + + for uid in message_ids: + try: + typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') + if not msg_contents: + continue + try: + message = self.get_email_from_bytes(msg_contents[0][1]) + except TypeError: + # This happens if another thread/process deletes the + # message between our generating the ID list and our + # processing it here. + continue + + if condition and not condition(message): + continue + + yield message + except MessageParseError: + continue + + if self.archive: + self.server.uid('copy', uid, self.archive) + + self.server.uid('store', uid, "+FLAGS", "(\\Deleted)") + self.server.expunge() + return From b37fa6e08d0ac8f7b71b7dfd7871292b8ef0455d Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Tue, 26 Jul 2022 21:45:27 +0200 Subject: [PATCH 02/18] Update setup.py --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2460dc0f..a6ade7d1 100755 --- a/setup.py +++ b/setup.py @@ -13,6 +13,10 @@ 'python-social-auth', ] +office365_oauth2_require = [ + 'O365', +] + setup( name='django-mailbox', version=version_string, @@ -25,7 +29,8 @@ author='Adam Coddington', author_email='me@adamcoddington.net', extras_require={ - 'gmail-oauth2': gmail_oauth2_require + 'gmail-oauth2': gmail_oauth2_require, + 'office365-oauth2': office365_oauth2_require }, python_requires=">=3", classifiers=[ From f44eb3036f7b2a7277a98f2bdb379184da70e008 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Wed, 27 Jul 2022 10:21:02 +0200 Subject: [PATCH 03/18] Office365Transport properties --- .idea/.gitignore | 3 ++ .idea/django-mailbox.iml | 14 ++++++ .idea/inspectionProfiles/Project_Default.xml | 41 ++++++++++++++++ .../inspectionProfiles/profiles_settings.xml | 6 +++ .idea/misc.xml | 7 +++ .idea/modules.xml | 8 ++++ .idea/vcs.xml | 6 +++ django_mailbox/models.py | 33 ++++++++++++- django_mailbox/transports/office365.py | 48 ++++++++++--------- 9 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/django-mailbox.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/django-mailbox.iml b/.idea/django-mailbox.iml new file mode 100644 index 00000000..8e5446ac --- /dev/null +++ b/.idea/django-mailbox.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..2d827cd2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..921287b9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..9b00bcc0 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 9ef29ac2..52e4c1e7 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -189,6 +189,30 @@ def folder(self): return None return folder[0] + @property + def client_id(self): + """Returns (if specified) the client id for Office365.""" + client_id = self._query_string.get('client_id', None) + if not client_id: + return None + return client_id[0] + + @property + def client_secret(self): + """Returns (if specified) the client secret for Office365.""" + client_secret = self._query_string.get('client_secret', None) + if not client_secret: + return None + return client_secret[0] + + @property + def tenant_id(self): + """Returns (if specified) the tenant id for Office365.""" + tenant_id = self._query_string.get('tentant_id', None) + if not tenant_id: + return None + return tenant_id[0] + def get_connection(self): """Returns the transport instance for this mailbox. @@ -224,7 +248,14 @@ def get_connection(self): ) conn.connect(self.username, self.password) elif self.type == 'office365': - conn = Office365Transport(self.location) + conn = Office365Transport( + port=self.port if self.port else None, + ssl=True, + archive=self.archive, + client_id=self.client_id, + client_secret=self.client_secret, + tenant_id=self.tenant_id + ) conn.connect(self.username, self.password) elif self.type == 'maildir': conn = MaildirTransport(self.location) diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index 8ce5554e..f98f8c14 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -11,7 +11,7 @@ class Office365Transport(EmailTransport): def __init__( self, hostname, port=None, ssl=False, tls=False, - archive='', folder=None, + archive='', folder=None, client_id=None, client_secret=None, tenant_id=None ): self.max_message_size = getattr( settings, @@ -23,30 +23,32 @@ def __init__( 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', None ) - # self.hostname = hostname - # self.port = port - # self.archive = archive - # self.folder = folder - # self.tls = tls - # if ssl: - # # self.transport = imaplib.IMAP4_SSL - # if not self.port: - # self.port = 993 - # else: - # self.transport = imaplib.IMAP4 - # if not self.port: - # self.port = 143 + self.hostname = hostname + self.port = port + self.archive = archive + self.folder = folder + self.tls = tls + if ssl: + #self.transport = imaplib.IMAP4_SSL + if not self.port: + self.port = 993 + else: + #self.transport = imaplib.IMAP4 + if not self.port: + self.port = 143 + self.client_id = client_id + self.client_secret = client_secret + self.tenant_id = tenant_id def connect(self, username, password): - # self.server = self.transport(self.hostname, self.port) - # if self.tls: - # self.server.starttls() - # typ, msg = self.server.login(username, password) - # - # if self.folder: - # self.server.select(self.folder) - # else: - # self.server.select() + credentials = (self.client_id, self.client_secret) + + # the default protocol will be Microsoft Graph + # the default authentication method will be "on behalf of a user" + + self.server = O365.Account(credentials, auth_flow_type='credentials', tenant_id=self.tenant_id) + if self.server.authenticate(scopes=['mailbox']): + print('Authenticated!') def _get_all_message_ids(self): # # Fetch all the message uids From a52f20dabe9c8dc2be40b729ea79626de7537972 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Wed, 27 Jul 2022 10:22:52 +0200 Subject: [PATCH 04/18] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 804fef47..94fab772 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dummy_project/* .tox/ messages *.sqlite3 +.idea/ From 50ee86d1d920bb204ec976f5da889d1500ee5aa1 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Wed, 27 Jul 2022 10:31:01 +0200 Subject: [PATCH 05/18] wrong folder stage --- .idea/.gitignore | 3 - .idea/django-mailbox.iml | 14 - .idea/inspectionProfiles/Project_Default.xml | 41 - .../inspectionProfiles/profiles_settings.xml | 6 - .idea/misc.xml | 7 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - build/lib/django_mailbox/__init__.py | 3 + build/lib/django_mailbox/admin.py | 112 +++ build/lib/django_mailbox/apps.py | 7 + build/lib/django_mailbox/google_utils.py | 111 +++ .../locale/ru_RU/LC_MESSAGES/django.mo | Bin 0 -> 5329 bytes .../locale/ru_RU/LC_MESSAGES/django.po | 193 ++++ .../lib/django_mailbox/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/getmail.py | 30 + .../commands/processincomingmessage.py | 52 ++ .../commands/rebuildmessageattachments.py | 71 ++ .../django_mailbox/migrations/0001_initial.py | 56 ++ .../migrations/0002_add_eml_to_message.py | 17 + .../migrations/0003_auto_20150409_0316.py | 16 + .../migrations/0004_bytestring_to_unicode.py | 21 + .../migrations/0005_auto_20160523_2240.py | 17 + .../migrations/0006_mailbox_last_polling.py | 18 + .../migrations/0007_auto_20180421_0026.py | 25 + .../migrations/0008_auto_20190219_1553.py | 23 + .../lib/django_mailbox/migrations/__init__.py | 0 build/lib/django_mailbox/models.py | 855 ++++++++++++++++++ build/lib/django_mailbox/signals.py | 3 + .../south_migrations/0001_initial.py | 58 ++ .../0002_auto__chg_field_mailbox_uri.py | 38 + .../0003_auto__add_field_mailbox_active.py | 41 + .../0004_auto__add_field_message_outgoing.py | 42 + .../south_migrations/0005_rename_fields.py | 38 + ...006_auto__add_field_message_in_reply_to.py | 55 ++ ...__add_field_message_from_header__add_fi.py | 59 ++ .../0008_populate_from_to_fields.py | 46 + .../0009_remove_references_table.py | 47 + ...0010_auto__add_field_mailbox_from_email.py | 45 + .../0011_auto__add_field_message_read.py | 46 + .../0012_auto__add_messageattachment.py | 66 ++ ...to__add_field_messageattachment_message.py | 53 ++ .../0014_migrate_message_attachments.py | 54 ++ ...to__add_field_messageattachment_headers.py | 64 ++ .../0016_auto__add_field_message_encoded.py | 54 ++ .../0017_auto__add_field_message_eml.py | 55 ++ .../south_migrations/__init__.py | 0 build/lib/django_mailbox/tests/__init__.py | 0 build/lib/django_mailbox/tests/base.py | 191 ++++ build/lib/django_mailbox/tests/settings.py | 12 + .../tests/test_integration_imap.py | 70 ++ .../lib/django_mailbox/tests/test_mailbox.py | 46 + .../tests/test_message_flattening.py | 116 +++ .../tests/test_process_email.py | 432 +++++++++ .../tests/test_processincomingmessage.py | 65 ++ .../django_mailbox/tests/test_transports.py | 167 ++++ .../lib/django_mailbox/transports/__init__.py | 11 + build/lib/django_mailbox/transports/babyl.py | 6 + build/lib/django_mailbox/transports/base.py | 11 + .../lib/django_mailbox/transports/generic.py | 26 + build/lib/django_mailbox/transports/gmail.py | 57 ++ build/lib/django_mailbox/transports/imap.py | 137 +++ .../lib/django_mailbox/transports/maildir.py | 9 + build/lib/django_mailbox/transports/mbox.py | 6 + build/lib/django_mailbox/transports/mh.py | 6 + build/lib/django_mailbox/transports/mmdf.py | 6 + .../django_mailbox/transports/office365.py | 131 +++ build/lib/django_mailbox/transports/pop3.py | 44 + build/lib/django_mailbox/utils.py | 151 ++++ 69 files changed, 4191 insertions(+), 85 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/django-mailbox.iml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 build/lib/django_mailbox/__init__.py create mode 100644 build/lib/django_mailbox/admin.py create mode 100644 build/lib/django_mailbox/apps.py create mode 100644 build/lib/django_mailbox/google_utils.py create mode 100644 build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.mo create mode 100644 build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po create mode 100644 build/lib/django_mailbox/management/__init__.py create mode 100644 build/lib/django_mailbox/management/commands/__init__.py create mode 100644 build/lib/django_mailbox/management/commands/getmail.py create mode 100644 build/lib/django_mailbox/management/commands/processincomingmessage.py create mode 100644 build/lib/django_mailbox/management/commands/rebuildmessageattachments.py create mode 100644 build/lib/django_mailbox/migrations/0001_initial.py create mode 100644 build/lib/django_mailbox/migrations/0002_add_eml_to_message.py create mode 100644 build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py create mode 100644 build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py create mode 100644 build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py create mode 100644 build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py create mode 100644 build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py create mode 100644 build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py create mode 100644 build/lib/django_mailbox/migrations/__init__.py create mode 100644 build/lib/django_mailbox/models.py create mode 100644 build/lib/django_mailbox/signals.py create mode 100644 build/lib/django_mailbox/south_migrations/0001_initial.py create mode 100644 build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py create mode 100644 build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py create mode 100644 build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py create mode 100644 build/lib/django_mailbox/south_migrations/0005_rename_fields.py create mode 100644 build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py create mode 100644 build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py create mode 100644 build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py create mode 100644 build/lib/django_mailbox/south_migrations/0009_remove_references_table.py create mode 100644 build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py create mode 100644 build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py create mode 100644 build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py create mode 100644 build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py create mode 100644 build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py create mode 100644 build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py create mode 100644 build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py create mode 100644 build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py create mode 100644 build/lib/django_mailbox/south_migrations/__init__.py create mode 100644 build/lib/django_mailbox/tests/__init__.py create mode 100644 build/lib/django_mailbox/tests/base.py create mode 100644 build/lib/django_mailbox/tests/settings.py create mode 100644 build/lib/django_mailbox/tests/test_integration_imap.py create mode 100644 build/lib/django_mailbox/tests/test_mailbox.py create mode 100644 build/lib/django_mailbox/tests/test_message_flattening.py create mode 100644 build/lib/django_mailbox/tests/test_process_email.py create mode 100644 build/lib/django_mailbox/tests/test_processincomingmessage.py create mode 100644 build/lib/django_mailbox/tests/test_transports.py create mode 100644 build/lib/django_mailbox/transports/__init__.py create mode 100644 build/lib/django_mailbox/transports/babyl.py create mode 100644 build/lib/django_mailbox/transports/base.py create mode 100644 build/lib/django_mailbox/transports/generic.py create mode 100644 build/lib/django_mailbox/transports/gmail.py create mode 100644 build/lib/django_mailbox/transports/imap.py create mode 100644 build/lib/django_mailbox/transports/maildir.py create mode 100644 build/lib/django_mailbox/transports/mbox.py create mode 100644 build/lib/django_mailbox/transports/mh.py create mode 100644 build/lib/django_mailbox/transports/mmdf.py create mode 100644 build/lib/django_mailbox/transports/office365.py create mode 100644 build/lib/django_mailbox/transports/pop3.py create mode 100644 build/lib/django_mailbox/utils.py diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/django-mailbox.iml b/.idea/django-mailbox.iml deleted file mode 100644 index 8e5446ac..00000000 --- a/.idea/django-mailbox.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 2d827cd2..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2da..00000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 921287b9..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9b00bcc0..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build/lib/django_mailbox/__init__.py b/build/lib/django_mailbox/__init__.py new file mode 100644 index 00000000..feda890b --- /dev/null +++ b/build/lib/django_mailbox/__init__.py @@ -0,0 +1,3 @@ +__version__ = '4.8.2' + +default_app_config = 'django_mailbox.apps.MailBoxConfig' diff --git a/build/lib/django_mailbox/admin.py b/build/lib/django_mailbox/admin.py new file mode 100644 index 00000000..4d489b1e --- /dev/null +++ b/build/lib/django_mailbox/admin.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python + +""" +Model configuration in application ``django_mailbox`` for administration +console. +""" + +import logging + +from django.conf import settings +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ + +from django_mailbox.models import MessageAttachment, Message, Mailbox +from django_mailbox.signals import message_received +from django_mailbox.utils import convert_header_to_unicode + + +logger = logging.getLogger(__name__) + + +def get_new_mail(mailbox_admin, request, queryset): + queryset.get_new_mail() + + +get_new_mail.short_description = _('Get new mail') + + +def resend_message_received_signal(message_admin, request, queryset): + for message in queryset.all(): + logger.debug('Resending \'message_received\' signal for %s' % message) + message_received.send(sender=message_admin, message=message) + + +resend_message_received_signal.short_description = ( + _('Re-send message received signal') +) + + +class MailboxAdmin(admin.ModelAdmin): + list_display = ( + 'name', + 'uri', + 'from_email', + 'active', + 'last_polling', + ) + readonly_fields = ['last_polling', ] + actions = [get_new_mail] + + +class MessageAttachmentAdmin(admin.ModelAdmin): + raw_id_fields = ('message', ) + list_display = ('message', 'document',) + + +class MessageAttachmentInline(admin.TabularInline): + model = MessageAttachment + extra = 0 + + +class MessageAdmin(admin.ModelAdmin): + def attachment_count(self, msg): + return msg.attachments.count() + + attachment_count.short_description = _('Attachment count') + + def subject(self, msg): + return convert_header_to_unicode(msg.subject) + + def envelope_headers(self, msg): + email = msg.get_email_object() + return '\n'.join( + [('{}: {}'.format(h, v)) for h, v in email.items()] + ) + + inlines = [ + MessageAttachmentInline, + ] + list_display = ( + 'subject', + 'processed', + 'read', + 'mailbox', + 'outgoing', + 'attachment_count', + ) + ordering = ['-processed'] + list_filter = ( + 'mailbox', + 'outgoing', + 'processed', + 'read', + ) + exclude = ( + 'body', + ) + raw_id_fields = ( + 'in_reply_to', + ) + readonly_fields = ( + 'envelope_headers', + 'text', + 'html', + ) + actions = [resend_message_received_signal] + + +if getattr(settings, 'DJANGO_MAILBOX_ADMIN_ENABLED', True): + admin.site.register(Message, MessageAdmin) + admin.site.register(MessageAttachment, MessageAttachmentAdmin) + admin.site.register(Mailbox, MailboxAdmin) diff --git a/build/lib/django_mailbox/apps.py b/build/lib/django_mailbox/apps.py new file mode 100644 index 00000000..13b7bf3c --- /dev/null +++ b/build/lib/django_mailbox/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class MailBoxConfig(AppConfig): + name = 'django_mailbox' + verbose_name = _("Mail Box") diff --git a/build/lib/django_mailbox/google_utils.py b/build/lib/django_mailbox/google_utils.py new file mode 100644 index 00000000..b834f59c --- /dev/null +++ b/build/lib/django_mailbox/google_utils.py @@ -0,0 +1,111 @@ +import logging + +from django.conf import settings +import requests +from social.apps.django_app.default.models import UserSocialAuth + + +logger = logging.getLogger(__name__) + + +class AccessTokenNotFound(Exception): + pass + + +class RefreshTokenNotFound(Exception): + pass + + +def get_google_consumer_key(): + return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY + + +def get_google_consumer_secret(): + return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET + + +def get_google_access_token(email): + # TODO: This should be cacheable + try: + me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") + return me.extra_data['access_token'] + except (UserSocialAuth.DoesNotExist, KeyError): + raise AccessTokenNotFound + + +def update_google_extra_data(email, extra_data): + try: + me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") + me.extra_data = extra_data + me.save() + except (UserSocialAuth.DoesNotExist, KeyError): + raise AccessTokenNotFound + + +def get_google_refresh_token(email): + try: + me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") + return me.extra_data['refresh_token'] + except (UserSocialAuth.DoesNotExist, KeyError): + raise RefreshTokenNotFound + + +def google_api_get(email, url): + headers = dict( + Authorization="Bearer %s" % get_google_access_token(email), + ) + r = requests.get(url, headers=headers) + logger.info("I got a %s", r.status_code) + if r.status_code == 401: + # Go use the refresh token + refresh_authorization(email) + r = requests.get(url, headers=headers) + logger.info("I got a %s", r.status_code) + if r.status_code == 200: + try: + return r.json() + except ValueError: + return r.text + + +def google_api_post(email, url, post_data, authorized=True): + # TODO: Make this a lot less ugly. especially the 401 handling + headers = dict() + if authorized is True: + headers.update(dict( + Authorization="Bearer %s" % get_google_access_token(email), + )) + r = requests.post(url, headers=headers, data=post_data) + if r.status_code == 401: + refresh_authorization(email) + r = requests.post(url, headers=headers, data=post_data) + if r.status_code == 200: + try: + return r.json() + except ValueError: + return r.text + + +def refresh_authorization(email): + refresh_token = get_google_refresh_token(email) + post_data = dict( + refresh_token=refresh_token, + client_id=get_google_consumer_key(), + client_secret=get_google_consumer_secret(), + grant_type='refresh_token', + ) + results = google_api_post( + email, + "https://accounts.google.com/o/oauth2/token?access_type=offline", + post_data, + authorized=False) + results.update({'refresh_token': refresh_token}) + update_google_extra_data(email, results) + + +def fetch_user_info(email): + result = google_api_get( + email, + "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" + ) + return result diff --git a/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.mo b/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..06dd2c9e8c0550671d97cef5a6fb48e395c4f0e7 GIT binary patch literal 5329 zcmbVQQEVJX8J0SBK~zHGrP3w%8rw-+9*}qf?Mg_zph`SdLV`!sN4{@n_k5Q) zDCOk4Z+GUOfByfQZ~htilgA!>U*NMH-|yo)^neh1fvU0L}xy3_K4UfZavl81Ro@5~2s~kD&8wz$btY19t#vzb9=k zr|qLa`ac20pLh*FUj^Ffy9cDdIpAZ!v%qfwF9R{9_#IFJKLpaxUw|C{H{iE{{{TJ% z{11@!kHZL$;z{5o;B!FQe+29W{sD-T#Xo_cA)kK($;+n*g8Y33M3dNtJr-AH$ z`0FOVr&25ezl-+6z(2w6HQ?WY+d&@Vc;Hc!*F(Sp-X8@rzk?~B06vBHY2XvUb17a0 zK8g2LAnpGO%mY6KGLCI%B<@UcA8-KguK@8Ueu^LR@*Z#y_*>v8@Dm{Z#14?l{04y~ z#Cr(H^$RiRC~y%-e*O$Zx4^Vpe#klFl=zZ2&iOrjnG>b=9el|H=VZKGgY3h6`C%NL zAI?h+a{WN3))%1tVVv9XCGXTUKhNR&IKI@-)A&N3`a=zG!S-CxI-ykgn&r6C@uvJUa@r51r{=T9nhHZ}MupOD z1dcZ&>%Qv}s|!_Eg#{@m>7hzz9BunB_54WAT60QTo>bG*sv5~g-S_1Dtcqq;AkAzT z1z+e?wnVF9w`T&P0lfvPs*piwMob5OO_nD*HDajRo_;&7&&9r>aHrwaYQ?ely|z(e&a4W zxZpPe+sB(%sQR^?GtvH`sX*>NkQ+r3Ipk~EAR_W*s3J|d--u>>607K)P3qWRNyk@Y~G1x;fG=Aapb6v(+9FU z^m)g1b0C%RBaIq~#gw{1k9_^e$mpSyM<-8>P8=IQH8Or^?C9%-P12m2RewGV-SY0; zwFTr9cvel7YYTNN4Cnp8J{bB~02R!ErYTDhs0_%D9`n)mMfrWZiH40T zog;`WXT94}XJKJ1;?|gQE$?)@@{u}lfwDstpquq9+ozkI(X54pD2+6vEcQ^&IaXSz z3S!bvce|Jj8cI6TdY7czZ3>l`GYngydSO5sx!B_5#29H|O8GH6|3fIi@x8L_nNU=G zemtCU?EG+JCd^OzXg+>yGJiNwRs^g35i3Gb+EXm;%NOzYqAcz$_rH)Y?kyH!n4eH{ z4!zxNSL!eK?|Z)34?Bt^KgpfqT9F@=<=C97=HwCOA=n|?$*bTe2Nyt(I#UQ52Vn2b zG;mDhCzVx0K-qg)28~k_Cws=n#z!{OC>4r5hm9iglM8i38>utV?z#){W6MJIV7qd1 zax_1hN=feA zDfw7@wo)p|mt;}G9PbAzd$O*f$^akrh~iLbk9^||Gvq*Npcg|*>5xjL$S!Snpt5&! z_=~;mcKF3K*_=Zacz8+ffrI|u{XIX*zpOkJ7;2CE%61}IDhw9(6-qrKelK26mg3d; zdVD*%5U+{&m+?k?GmrNTiJzNz`zUV3w_$?L5AcX45&t@FCW~=1z7@BUrDRbi%kes1 zZ@|ZOAQ~mcH2Kqli)+c7_+5dMTk%G67DL{a@yGD60rM7cJzhlv`pzR7Hg3efM+C{m z>$bZl0$5GXqqW60*d`Z}H)V1bkDTbOELwywlb`9)H)*?JCWK+LAma}?528%Y z!CwW47mp5#dd_rg+7U(7~={;OWsMHb!I>|+wtp>@y%ozfv4=G z$tKGZA>N?7CK+QiXt`G?j9KT=1V`%_yhYy0J8;tCx@v-NYnIkJf;Tgos5Q-;)1_vLR1aP$1+4t%yV1l~ARa9x zt&C=J5#-;MEt}gZHM_1i@%tbQ0>UaM7gCZ}(S+5&^1KZJFLT|Ym-|`N6^y}DaL^G{ z3+tZEPN8%<>fSUUgBr$^dW)rjZV_U(yR`os(T&(-%D0}g_@!k$Val2*SbZ5W?7O&))JHr zlElYr*zc?#=>-fz;(BIxFujs1I{ihAU}0+}?_vNmh9!hd7O~W%56gTGuWPykieLkG zP1o!YN{%OS_oa>dehG%_jO%_cY}9X;*4DJv+Lt#hUMmwQHb|;tqs(meum_FmjE>Fn zYxSC9z?Em}SZFP9aKX~I%tZwsADb-C>J7*`dlUKXGM7E@m@F0Il>)AO@l|eg{Osc8 zua_mCwya>Qnt!a8F23aKj^dkbew1*-ELd6<@g)>t#K4MgjtA{j=i5S*`i$wQ)225) z5W33g{r4cY>sTa>LUl!_MMDx{!~8idGVzg1F~bUN9A*g{I34NUu2ki-p&PNxG4 zO`GN_JjkT0qQK8p2%o~_NkrZ0LfW?byc000>n2eSH4DQ7ndgdGU*x}v z(?Op}=+`wZ6@a-9fKV24oY@?ag~Thut+)wl(1*uV!yT*KGDkZEiLKB|ey(HZX^)un zod6x!Tz6>y6sN;m`jvV8`+5bt_rkHR<=7 g`#TKGX$5-q#p^Cs$nFiD#a+iN??2|Mudn$31sHQPiU0rr literal 0 HcmV?d00001 diff --git a/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po b/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po new file mode 100644 index 00000000..d84c3cb8 --- /dev/null +++ b/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po @@ -0,0 +1,193 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-07-09 05:36-0500\n" +"PO-Revision-Date: 2017-07-09 13:37+0300\n" +"Last-Translator: Ivlev Denis \n" +"Language-Team: \n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" +"X-Generator: Poedit 1.8.7.1\n" + +#: django_mailbox/admin.py:29 +msgid "Get new mail" +msgstr "Получить новые сообщения" + +#: django_mailbox/admin.py:39 +msgid "Re-send message received signal" +msgstr "Повторно отправить сигнал о получении" + +#: django_mailbox/admin.py:69 +msgid "Attachment count" +msgstr "Кол-во вложений" + +#: django_mailbox/apps.py:7 +msgid "Mail Box" +msgstr "Почтовый ящик" + +#: django_mailbox/models.py:49 +msgid "Name" +msgstr "Название" + +#: django_mailbox/models.py:54 +msgid "URI" +msgstr "URI" + +#: django_mailbox/models.py:57 +msgid "" +"Example: imap+ssl://myusername:mypassword@someserver

Internet " +"transports include 'imap' and 'pop3'; common local file transports include " +"'maildir', 'mbox', and less commonly 'babyl', 'mh', and 'mmdf'.

Be sure to urlencode your username and password should they contain illegal " +"characters (like @, :, etc)." +msgstr "" +"Пример: imap+ssl://myusername:mypassword@someserver

Интернет-" +"транспорт может быть 'imap' или 'pop3'; поддерживаются локальные файловые " +"транспорты 'maildir', 'mbox', а также 'babyl', 'mh', and 'mmdf'.

Используйте urlencode, если имя пользователя или пароль содержат " +"недопустипые символы (@, :, и т.д.)." + +#: django_mailbox/models.py:72 +msgid "From email" +msgstr "От" + +#: django_mailbox/models.py:75 +msgid "" +"Example: MailBot <mailbot@yourdomain.com>
'From' header to set " +"for outgoing email.

If you do not use this e-mail inbox for " +"outgoing mail, this setting is unnecessary.
If you send e-mail without " +"setting this, your 'From' header will'be set to match the setting " +"`DEFAULT_FROM_EMAIL`." +msgstr "" +"Пример: MailBot <mailbot@yourdomain.com>
Исходящая электронная " +"почта.

Если вы не используете этот почтовый ящик для исходящей " +"почты, этот параметр не нужен.
Если вы не указали данный параметр, " +"будет использоваться указанный в настройках `DEFAULT_FROM_EMAIL`." + +#: django_mailbox/models.py:89 +msgid "Active" +msgstr "Активный" + +#: django_mailbox/models.py:91 +msgid "" +"Check this e-mail inbox for new e-mail messages during polling cycles. This " +"checkbox does not have an effect upon whether mail is collected here when " +"this mailbox receives mail from a pipe, and does not affect whether e-mail " +"messages can be dispatched from this mailbox. " +msgstr "" +"Параметр указывает на необходимость проверки почтового ящика на наличие " +"новых сообщений в цикле опроса. Этот флажок не влияет на сбор почты, когда " +"этот почтовый ящик получает почту из канала и не влияет на отправку " +"сообщений электронной почты из этого почтового ящика." + +#: django_mailbox/models.py:102 +msgid "Last polling" +msgstr "Последний опрос" + +#: django_mailbox/models.py:103 +msgid "" +"The time of last successful polling for messages.It is blank for new " +"mailboxes and is not set for mailboxes that only receive messages via a pipe." +msgstr "" +"Время последнего успешного опроса сообщений. Для нового почтового ящика не " +"установлено. Также не устанавливается для почтовых ящиков " +"обновляющих сообщения через pipe." + +#: django_mailbox/models.py:409 django_mailbox/models.py:438 +msgid "Mailbox" +msgstr "Почтовый ящик" + +#: django_mailbox/models.py:410 +msgid "Mailboxes" +msgstr "Почтовые ящики" + +#: django_mailbox/models.py:442 +msgid "Subject" +msgstr "Тема" + +#: django_mailbox/models.py:447 +msgid "Message ID" +msgstr "Идентификатор сообщения" + +#: django_mailbox/models.py:456 +msgid "In reply to" +msgstr "В ответ на" + +#: django_mailbox/models.py:460 +msgid "From header" +msgstr "От(From)" + +#: django_mailbox/models.py:465 +msgid "To header" +msgstr "Кому(To)" + +#: django_mailbox/models.py:469 +msgid "Outgoing" +msgstr "Исходящее" + +#: django_mailbox/models.py:475 +msgid "Body" +msgstr "Тело" + +#: django_mailbox/models.py:479 +msgid "Encoded" +msgstr "Закодировано" + +#: django_mailbox/models.py:481 +msgid "True if the e-mail body is Base64 encoded" +msgstr "True если тело сообщения закодировано в Base64" + +#: django_mailbox/models.py:485 +msgid "Processed" +msgstr "Обработано" + +#: django_mailbox/models.py:490 +msgid "Read" +msgstr "Прочитано" + +#: django_mailbox/models.py:497 +msgid "Raw message contents" +msgstr "Исходное содержимое сообщения" + +#: django_mailbox/models.py:500 +msgid "Original full content of message" +msgstr "Полное содержимое сообщения" + +#: django_mailbox/models.py:716 +msgid "E-mail message" +msgstr "Сообщение" + +#: django_mailbox/models.py:717 +msgid "E-mail messages" +msgstr "Сообщения" + +#: django_mailbox/models.py:726 +msgid "Message" +msgstr "Сообщение" + +#: django_mailbox/models.py:730 +msgid "Headers" +msgstr "Заголовки" + +#: django_mailbox/models.py:736 +msgid "Document" +msgstr "Документ" + +#: django_mailbox/models.py:793 +msgid "Message attachment" +msgstr "Вложение" + +#: django_mailbox/models.py:794 +msgid "Message attachments" +msgstr "Вложения" diff --git a/build/lib/django_mailbox/management/__init__.py b/build/lib/django_mailbox/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build/lib/django_mailbox/management/commands/__init__.py b/build/lib/django_mailbox/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build/lib/django_mailbox/management/commands/getmail.py b/build/lib/django_mailbox/management/commands/getmail.py new file mode 100644 index 00000000..31b2dbdc --- /dev/null +++ b/build/lib/django_mailbox/management/commands/getmail.py @@ -0,0 +1,30 @@ +import logging + +from django.core.management.base import BaseCommand + +from django_mailbox.models import Mailbox + + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class Command(BaseCommand): + def handle(self, *args, **options): + mailboxes = Mailbox.active_mailboxes.all() + if args: + mailboxes = mailboxes.filter( + name=' '.join(args) + ) + for mailbox in mailboxes: + logger.info( + 'Gathering messages for %s', + mailbox.name + ) + messages = mailbox.get_new_mail() + for message in messages: + logger.info( + 'Received %s (from %s)', + message.subject, + message.from_address + ) diff --git a/build/lib/django_mailbox/management/commands/processincomingmessage.py b/build/lib/django_mailbox/management/commands/processincomingmessage.py new file mode 100644 index 00000000..ec344111 --- /dev/null +++ b/build/lib/django_mailbox/management/commands/processincomingmessage.py @@ -0,0 +1,52 @@ +import email +import logging +import sys +try: + from email import utils +except ImportError: + import rfc822 as utils + +from django.core.management.base import BaseCommand + +from django_mailbox.models import Mailbox + + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class Command(BaseCommand): + args = "<[Mailbox Name (optional)]>" + help = "Receive incoming mail via stdin" + + def add_arguments(self, parser): + parser.add_argument( + 'mailbox_name', + nargs='?', + help="The name of the mailbox that will receive the message" + ) + + def handle(self, mailbox_name=None, *args, **options): + message = email.message_from_string(sys.stdin.read()) + if message: + if mailbox_name: + mailbox = self.get_mailbox_by_name(mailbox_name) + else: + mailbox = self.get_mailbox_for_message(message) + mailbox.process_incoming_message(message) + logger.info( + "Message received from %s", + message['from'] + ) + else: + logger.warning("Message not processable.") + + def get_mailbox_by_name(self, name): + mailbox, created = Mailbox.objects.get_or_create( + name=name, + ) + return mailbox + + def get_mailbox_for_message(self, message): + email_address = utils.parseaddr(message['to'])[1][0:255] + return self.get_mailbox_by_name(email_address) diff --git a/build/lib/django_mailbox/management/commands/rebuildmessageattachments.py b/build/lib/django_mailbox/management/commands/rebuildmessageattachments.py new file mode 100644 index 00000000..6bfc24f6 --- /dev/null +++ b/build/lib/django_mailbox/management/commands/rebuildmessageattachments.py @@ -0,0 +1,71 @@ +import email +import hashlib +import logging + +from django.core.management.base import BaseCommand + +from django_mailbox.models import MessageAttachment, Message + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class Command(BaseCommand): + """ Briefly, a bug existed in a migration that may have caused message + attachments to become disassociated with their messages. This management + command will read through existing message attachments and attempt to + re-associate them with their original message. + + This isn't foolproof, I'm afraid. If an attachment exists twice, it will + be associated only with the most recent e-mail message. That said, + I'm quite sure that the bug in the migration is gone (and you'd have to + have been quite unlucky to have ran the bad migration). + + """ + def handle(self, *args, **options): + attachment_hash_map = {} + + attachments_without_messages = MessageAttachment.objects.filter( + message=None + ).order_by( + 'id' + ) + + if attachments_without_messages.count() < 1: + return + + for attachment in attachments_without_messages: + md5 = hashlib.md5() + for chunk in attachment.document.file.chunks(): + md5.update(chunk) + attachment_hash_map[md5.hexdigest()] = attachment.pk + + for message_record in Message.objects.all().order_by('id'): + message = email.message_from_string(message_record.body) + if message.is_multipart(): + for part in message.walk(): + if part.get_content_maintype() == 'multipart': + continue + if part.get('Content-Disposition') is None: + continue + md5 = hashlib.md5() + md5.update(part.get_payload(decode=True)) + digest = md5.hexdigest() + if digest in attachment_hash_map: + attachment = MessageAttachment.objects.get( + pk=attachment_hash_map[digest] + ) + attachment.message = message_record + attachment.save() + logger.info( + "Associated message %s with attachment %s (%s)", + message_record.pk, + attachment.pk, + digest + ) + else: + logger.info( + "%s(%s) not found in currently-stored attachments", + part.get_filename(), + digest + ) diff --git a/build/lib/django_mailbox/migrations/0001_initial.py b/build/lib/django_mailbox/migrations/0001_initial.py new file mode 100644 index 00000000..13bb802d --- /dev/null +++ b/build/lib/django_mailbox/migrations/0001_initial.py @@ -0,0 +1,56 @@ +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Mailbox', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('uri', models.CharField(default=None, max_length=255, blank=True, help_text="Example: imap+ssl://myusername:mypassword@someserver

Internet transports include 'imap' and 'pop3'; common local file transports include 'maildir', 'mbox', and less commonly 'babyl', 'mh', and 'mmdf'.

Be sure to urlencode your username and password should they contain illegal characters (like @, :, etc).", null=True, verbose_name='URI')), + ('from_email', models.CharField(default=None, max_length=255, blank=True, help_text="Example: MailBot <mailbot@yourdomain.com>
'From' header to set for outgoing email.

If you do not use this e-mail inbox for outgoing mail, this setting is unnecessary.
If you send e-mail without setting this, your 'From' header will'be set to match the setting `DEFAULT_FROM_EMAIL`.", null=True, verbose_name='From email')), + ('active', models.BooleanField(default=True, help_text='Check this e-mail inbox for new e-mail messages during polling cycles. This checkbox does not have an effect upon whether mail is collected here when this mailbox receives mail from a pipe, and does not affect whether e-mail messages can be dispatched from this mailbox. ', verbose_name='Active')), + ], + options={ + 'verbose_name_plural': 'Mailboxes', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('subject', models.CharField(max_length=255, verbose_name='Subject')), + ('message_id', models.CharField(max_length=255, verbose_name='Message ID')), + ('from_header', models.CharField(max_length=255, verbose_name='From header')), + ('to_header', models.TextField(verbose_name='To header')), + ('outgoing', models.BooleanField(default=False, verbose_name='Outgoing')), + ('body', models.TextField(verbose_name='Body')), + ('encoded', models.BooleanField(default=False, help_text='True if the e-mail body is Base64 encoded', verbose_name='Encoded')), + ('processed', models.DateTimeField(auto_now_add=True, verbose_name='Processed')), + ('read', models.DateTimeField(default=None, null=True, verbose_name='Read', blank=True)), + ('in_reply_to', models.ForeignKey(related_name='replies', verbose_name='In reply to', blank=True, to='django_mailbox.Message', null=True, on_delete=models.CASCADE)), + ('mailbox', models.ForeignKey(related_name='messages', verbose_name='Mailbox', to='django_mailbox.Mailbox', on_delete=models.CASCADE)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='MessageAttachment', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('headers', models.TextField(null=True, verbose_name='Headers', blank=True)), + ('document', models.FileField(upload_to=b'mailbox_attachments/%Y/%m/%d/', verbose_name='Document')), + ('message', models.ForeignKey(related_name='attachments', verbose_name='Message', blank=True, to='django_mailbox.Message', null=True, on_delete=models.CASCADE)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/build/lib/django_mailbox/migrations/0002_add_eml_to_message.py b/build/lib/django_mailbox/migrations/0002_add_eml_to_message.py new file mode 100644 index 00000000..4204efb2 --- /dev/null +++ b/build/lib/django_mailbox/migrations/0002_add_eml_to_message.py @@ -0,0 +1,17 @@ +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='eml', + field=models.FileField(help_text='Original full content of message', upload_to=b'messages', null=True, verbose_name='Message as a file'), + preserve_default=True, + ), + ] diff --git a/build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py b/build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py new file mode 100644 index 00000000..1b5def07 --- /dev/null +++ b/build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py @@ -0,0 +1,16 @@ +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0002_add_eml_to_message'), + ] + + operations = [ + migrations.AlterField( + model_name='message', + name='eml', + field=models.FileField(help_text='Original full content of message', upload_to=b'messages', null=True, verbose_name='Raw message contents'), + ), + ] diff --git a/build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py b/build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py new file mode 100644 index 00000000..7716f2f5 --- /dev/null +++ b/build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py @@ -0,0 +1,21 @@ +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0003_auto_20150409_0316'), + ] + + operations = [ + migrations.AlterField( + model_name='message', + name='eml', + field=models.FileField(verbose_name='Raw message contents', upload_to='messages', null=True, help_text='Original full content of message'), + ), + migrations.AlterField( + model_name='messageattachment', + name='document', + field=models.FileField(verbose_name='Document', upload_to='mailbox_attachments/%Y/%m/%d/'), + ), + ] diff --git a/build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py b/build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py new file mode 100644 index 00000000..f4bdae65 --- /dev/null +++ b/build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py @@ -0,0 +1,17 @@ +from django.db import migrations, models +import django_mailbox.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0004_bytestring_to_unicode'), + ] + + operations = [ + migrations.AlterField( + model_name='messageattachment', + name='document', + field=models.FileField(upload_to=django_mailbox.utils.get_attachment_save_path, verbose_name='Document'), + ), + ] diff --git a/build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py b/build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py new file mode 100644 index 00000000..5f1524ec --- /dev/null +++ b/build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py @@ -0,0 +1,18 @@ +# Generated by Django 1.9.8 on 2016-08-15 22:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0005_auto_20160523_2240'), + ] + + operations = [ + migrations.AddField( + model_name='mailbox', + name='last_polling', + field=models.DateTimeField(blank=True, help_text='The time of last successful polling for messages.It is blank for new mailboxes and is not set for mailboxes that only receive messages via a pipe.', null=True, verbose_name='Last polling'), + ), + ] diff --git a/build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py b/build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py new file mode 100644 index 00000000..2e984d0d --- /dev/null +++ b/build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py @@ -0,0 +1,25 @@ +# Generated by Django 1.10.7 on 2018-04-21 00:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0006_mailbox_last_polling'), + ] + + operations = [ + migrations.AlterModelOptions( + name='mailbox', + options={'verbose_name': 'Mailbox', 'verbose_name_plural': 'Mailboxes'}, + ), + migrations.AlterModelOptions( + name='message', + options={'verbose_name': 'E-mail message', 'verbose_name_plural': 'E-mail messages'}, + ), + migrations.AlterModelOptions( + name='messageattachment', + options={'verbose_name': 'Message attachment', 'verbose_name_plural': 'Message attachments'}, + ), + ] diff --git a/build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py b/build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py new file mode 100644 index 00000000..e307c258 --- /dev/null +++ b/build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.7 on 2019-02-19 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_mailbox', '0007_auto_20180421_0026'), + ] + + operations = [ + migrations.AlterField( + model_name='mailbox', + name='active', + field=models.BooleanField(blank=True, default=True, help_text='Check this e-mail inbox for new e-mail messages during polling cycles. This checkbox does not have an effect upon whether mail is collected here when this mailbox receives mail from a pipe, and does not affect whether e-mail messages can be dispatched from this mailbox. ', verbose_name='Active'), + ), + migrations.AlterField( + model_name='message', + name='outgoing', + field=models.BooleanField(blank=True, default=False, verbose_name='Outgoing'), + ), + ] diff --git a/build/lib/django_mailbox/migrations/__init__.py b/build/lib/django_mailbox/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build/lib/django_mailbox/models.py b/build/lib/django_mailbox/models.py new file mode 100644 index 00000000..52e4c1e7 --- /dev/null +++ b/build/lib/django_mailbox/models.py @@ -0,0 +1,855 @@ +#!/usr/bin/env python + +""" +Models declaration for application ``django_mailbox``. +""" +import gzip +from email.encoders import encode_base64 +from email.message import Message as EmailMessage +from email.utils import formatdate, parseaddr +from urllib.parse import parse_qs, unquote, urlparse +from quopri import encode as encode_quopri +from io import BytesIO +import base64 +import email +import logging +import mimetypes +import os.path +import sys +import uuid +from tempfile import NamedTemporaryFile + +import django +from django.conf import settings as django_settings +from django.core.files.base import ContentFile, File +from django.core.mail.message import make_msgid +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.timezone import now + +from django_mailbox import utils +from django_mailbox.signals import message_received +from django_mailbox.transports import Pop3Transport, ImapTransport, \ + MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ + MMDFTransport, GmailImapTransport, Office365Transport + +logger = logging.getLogger(__name__) + + +class MailboxQuerySet(models.QuerySet): + def get_new_mail(self): + count = 0 + for mailbox in self.all(): + logger.debug('Receiving mail for %s' % mailbox) + count += sum(1 for i in mailbox.get_new_mail()) + logger.debug('Received %d %s.', count, 'mails' if count != 1 else 'mail') + + +class MailboxManager(models.Manager): + def get_queryset(self): + return MailboxQuerySet(self.model, using=self._db) + + +class ActiveMailboxManager(MailboxManager): + def get_queryset(self): + return super().get_queryset().filter( + active=True, + ) + + +class Mailbox(models.Model): + name = models.CharField( + _('Name'), + max_length=255, + ) + + uri = models.CharField( + _('URI'), + max_length=255, + help_text=(_( + "Example: imap+ssl://myusername:mypassword@someserver
" + "
" + "Internet transports include 'imap' and 'pop3'; " + "common local file transports include 'maildir', 'mbox', " + "and less commonly 'babyl', 'mh', and 'mmdf'.
" + "
" + "Be sure to urlencode your username and password should they " + "contain illegal characters (like @, :, etc)." + )), + blank=True, + null=True, + default=None, + ) + + from_email = models.CharField( + _('From email'), + max_length=255, + help_text=(_( + "Example: MailBot <mailbot@yourdomain.com>
" + "'From' header to set for outgoing email.
" + "
" + "If you do not use this e-mail inbox for outgoing mail, this " + "setting is unnecessary.
" + "If you send e-mail without setting this, your 'From' header will'" + "be set to match the setting `DEFAULT_FROM_EMAIL`." + )), + blank=True, + null=True, + default=None, + ) + + active = models.BooleanField( + _('Active'), + help_text=(_( + "Check this e-mail inbox for new e-mail messages during polling " + "cycles. This checkbox does not have an effect upon whether " + "mail is collected here when this mailbox receives mail from a " + "pipe, and does not affect whether e-mail messages can be " + "dispatched from this mailbox. " + )), + blank=True, + default=True, + ) + + last_polling = models.DateTimeField( + _("Last polling"), + help_text=(_("The time of last successful polling for messages." + "It is blank for new mailboxes and is not set for " + "mailboxes that only receive messages via a pipe.")), + blank=True, + null=True + ) + + objects = MailboxManager() + active_mailboxes = ActiveMailboxManager() + + @property + def _protocol_info(self): + return urlparse(self.uri) + + @property + def _query_string(self): + return parse_qs(self._protocol_info.query) + + @property + def _domain(self): + return self._protocol_info.hostname + + @property + def port(self): + """Returns the port to use for fetching messages.""" + return self._protocol_info.port + + @property + def username(self): + """Returns the username to use for fetching messages.""" + return unquote(self._protocol_info.username) + + @property + def password(self): + """Returns the password to use for fetching messages.""" + return unquote(self._protocol_info.password) + + @property + def location(self): + """Returns the location (domain and path) of messages.""" + return self._domain if self._domain else '' + self._protocol_info.path + + @property + def type(self): + """Returns the 'transport' name for this mailbox.""" + scheme = self._protocol_info.scheme.lower() + if '+' in scheme: + return scheme.split('+')[0] + return scheme + + @property + def use_ssl(self): + """Returns whether or not this mailbox's connection uses SSL.""" + return '+ssl' in self._protocol_info.scheme.lower() + + @property + def use_tls(self): + """Returns whether or not this mailbox's connection uses STARTTLS.""" + return '+tls' in self._protocol_info.scheme.lower() + + @property + def archive(self): + """Returns (if specified) the folder to archive messages to.""" + archive_folder = self._query_string.get('archive', None) + if not archive_folder: + return None + return archive_folder[0] + + @property + def folder(self): + """Returns (if specified) the folder to fetch mail from.""" + folder = self._query_string.get('folder', None) + if not folder: + return None + return folder[0] + + @property + def client_id(self): + """Returns (if specified) the client id for Office365.""" + client_id = self._query_string.get('client_id', None) + if not client_id: + return None + return client_id[0] + + @property + def client_secret(self): + """Returns (if specified) the client secret for Office365.""" + client_secret = self._query_string.get('client_secret', None) + if not client_secret: + return None + return client_secret[0] + + @property + def tenant_id(self): + """Returns (if specified) the tenant id for Office365.""" + tenant_id = self._query_string.get('tentant_id', None) + if not tenant_id: + return None + return tenant_id[0] + + def get_connection(self): + """Returns the transport instance for this mailbox. + + These will always be instances of + `django_mailbox.transports.base.EmailTransport`. + + """ + if not self.uri: + return None + elif self.type == 'imap': + conn = ImapTransport( + self.location, + port=self.port if self.port else None, + ssl=self.use_ssl, + tls=self.use_tls, + archive=self.archive, + folder=self.folder + ) + conn.connect(self.username, self.password) + elif self.type == 'gmail': + conn = GmailImapTransport( + self.location, + port=self.port if self.port else None, + ssl=True, + archive=self.archive + ) + conn.connect(self.username, self.password) + elif self.type == 'pop3': + conn = Pop3Transport( + self.location, + port=self.port if self.port else None, + ssl=self.use_ssl + ) + conn.connect(self.username, self.password) + elif self.type == 'office365': + conn = Office365Transport( + port=self.port if self.port else None, + ssl=True, + archive=self.archive, + client_id=self.client_id, + client_secret=self.client_secret, + tenant_id=self.tenant_id + ) + conn.connect(self.username, self.password) + elif self.type == 'maildir': + conn = MaildirTransport(self.location) + elif self.type == 'mbox': + conn = MboxTransport(self.location) + elif self.type == 'babyl': + conn = BabylTransport(self.location) + elif self.type == 'mh': + conn = MHTransport(self.location) + elif self.type == 'mmdf': + conn = MMDFTransport(self.location) + return conn + + def process_incoming_message(self, message): + """Process a message incoming to this mailbox.""" + msg = self._process_message(message) + if msg is None: + return None + msg.outgoing = False + msg.save() + + message_received.send(sender=self, message=msg) + + return msg + + def record_outgoing_message(self, message): + """Record an outgoing message associated with this mailbox.""" + msg = self._process_message(message) + if msg is None: + return None + msg.outgoing = True + msg.save() + 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(): + new[header] = value + for part in msg.get_payload(): + new.attach( + self._get_dehydrated_message(part, record) + ) + elif ( + 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[settings['altered_message_header']] = ( + 'Stripped; Content type %s not allowed' % ( + msg.get_content_type() + ) + ) + new.set_payload('') + elif ( + ( + msg.get_content_type() not in settings['text_stored_mimetypes'] + ) or + ('attachment' in msg.get('Content-Disposition', '')) + ): + filename = None + raw_filename = msg.get_filename() + if raw_filename: + filename = utils.convert_header_to_unicode(raw_filename) + if not filename: + extension = mimetypes.guess_extension(msg.get_content_type()) + else: + _, extension = os.path.splitext(filename) + if not extension: + extension = '.bin' + + attachment = MessageAttachment() + + attachment.document.save( + uuid.uuid4().hex + extension, + ContentFile( + BytesIO( + msg.get_payload(decode=True) + ).getvalue() + ) + ) + attachment.message = record + for key, value in msg.items(): + attachment[key] = value + attachment.save() + + placeholder = EmailMessage() + placeholder[ + settings['attachment_interpolation_header'] + ] = str(attachment.pk) + new = placeholder + else: + content_charset = msg.get_content_charset() + if not content_charset: + content_charset = 'ascii' + try: + # Make sure that the payload can be properly decoded in the + # defined charset, if it can't, let's mash some things + # inside the payload :-\ + msg.get_payload(decode=True).decode(content_charset) + except LookupError: + logger.warning( + "Unknown encoding %s; interpreting as ASCII!", + content_charset + ) + msg.set_payload( + msg.get_payload(decode=True).decode( + 'ascii', + 'ignore' + ) + ) + except ValueError: + logger.warning( + "Decoding error encountered; interpreting %s as ASCII!", + content_charset + ) + msg.set_payload( + msg.get_payload(decode=True).decode( + 'ascii', + 'ignore' + ) + ) + new = msg + return new + + def _process_message(self, message): + msg = Message() + msg._email_object = message + settings = utils.get_settings() + + if settings['store_original_message']: + self._process_save_original_message(message, msg) + msg.mailbox = self + if 'subject' in message: + 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 = utils.convert_header_to_unicode(message['from']) + if 'to' in message: + msg.to_header = utils.convert_header_to_unicode(message['to']) + elif 'Delivered-To' in message: + msg.to_header = utils.convert_header_to_unicode( + message['Delivered-To'] + ) + msg.save() + message = self._get_dehydrated_message(message, msg) + try: + body = message.as_string() + except KeyError as exc: + # email.message.replace_header may raise 'KeyError' if the header + # 'content-transfer-encoding' is missing + logger.warning("Failed to parse message: %s", exc,) + return None + msg.set_body(body) + if message['in-reply-to']: + try: + msg.in_reply_to = Message.objects.filter( + message_id=message['in-reply-to'].strip() + )[0] + except IndexError: + pass + msg.save() + return msg + + def _process_save_original_message(self, message, msg): + settings = utils.get_settings() + if settings['compress_original_message']: + with NamedTemporaryFile(suffix=".eml.gz") as fp_tmp: + with gzip.GzipFile(fileobj=fp_tmp, mode="w") as fp: + fp.write(message.as_string().encode('utf-8')) + msg.eml.save( + "{}.eml.gz".format(uuid.uuid4()), + File(fp_tmp), + save=False + ) + + else: + msg.eml.save( + '%s.eml' % uuid.uuid4(), + ContentFile(message.as_string()), + save=False + ) + + def get_new_mail(self, condition=None): + """Connect to this transport and fetch new messages.""" + new_mail = [] + connection = self.get_connection() + if not connection: + return + for message in connection.get_message(condition): + msg = self.process_incoming_message(message) + if not msg is None: + yield msg + self.last_polling = now() + if django.VERSION >= (1, 5): # Django 1.5 introduces update_fields + self.save(update_fields=['last_polling']) + else: + self.save() + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Mailbox') + verbose_name_plural = _('Mailboxes') + + +class IncomingMessageManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter( + outgoing=False, + ) + + +class OutgoingMessageManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter( + outgoing=True, + ) + + +class UnreadMessageManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter( + read=None + ) + + +class Message(models.Model): + mailbox = models.ForeignKey( + Mailbox, + related_name='messages', + verbose_name=_('Mailbox'), + on_delete=models.CASCADE + ) + + subject = models.CharField( + _('Subject'), + max_length=255 + ) + + message_id = models.CharField( + _('Message ID'), + max_length=255 + ) + + in_reply_to = models.ForeignKey( + 'django_mailbox.Message', + related_name='replies', + blank=True, + null=True, + verbose_name=_('In reply to'), + on_delete=models.CASCADE + ) + + from_header = models.CharField( + _('From header'), + max_length=255, + ) + + to_header = models.TextField( + _('To header'), + ) + + outgoing = models.BooleanField( + _('Outgoing'), + default=False, + blank=True, + ) + + body = models.TextField( + _('Body'), + ) + + encoded = models.BooleanField( + _('Encoded'), + default=False, + help_text=_('True if the e-mail body is Base64 encoded'), + ) + + processed = models.DateTimeField( + _('Processed'), + auto_now_add=True + ) + + read = models.DateTimeField( + _('Read'), + default=None, + blank=True, + null=True, + ) + + eml = models.FileField( + _('Raw message contents'), + null=True, + upload_to="messages", + help_text=_('Original full content of message') + ) + objects = models.Manager() + unread_messages = UnreadMessageManager() + incoming_messages = IncomingMessageManager() + outgoing_messages = OutgoingMessageManager() + + @property + def address(self): + """Property allowing one to get the relevant address(es). + + In earlier versions of this library, the model had an `address` field + storing the e-mail address from which a message was received. During + later refactorings, it became clear that perhaps storing sent messages + would also be useful, so the address field was replaced with two + separate fields. + + """ + addresses = [] + addresses = self.to_addresses + self.from_address + return addresses + + @property + def from_address(self): + """Returns the address (as a list) from which this message was received + + .. note:: + + This was once (and probably should be) a string rather than a list, + but in a pull request received long, long ago it was changed; + presumably to make the interface identical to that of + `to_addresses`. + + """ + if self.from_header: + return [parseaddr(self.from_header)[1].lower()] + else: + return [] + + @property + def to_addresses(self): + """Returns a list of addresses to which this message was sent.""" + addresses = [] + for address in self.to_header.split(','): + if address: + addresses.append( + parseaddr( + address + )[1].lower() + ) + return addresses + + def reply(self, message): + """Sends a message as a reply to this message instance. + + Although Django's e-mail processing will set both Message-ID + and Date upon generating the e-mail message, we will not be able + to retrieve that information through normal channels, so we must + pre-set it. + + """ + if not message.from_email: + if self.mailbox.from_email: + message.from_email = self.mailbox.from_email + else: + 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() + message.send() + return self.mailbox.record_outgoing_message( + email.message_from_string( + message.message().as_string() + ) + ) + + @property + def text(self): + """ + Returns the message body matching content type 'text/plain'. + """ + return utils.get_body_from_message( + self.get_email_object(), 'text', 'plain' + ).replace('=\n', '').strip() + + @property + def html(self): + """ + Returns the message body matching content type 'text/html'. + """ + 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 + for part in msg.get_payload(): + new.attach( + self._rehydrate(part) + ) + elif settings['attachment_interpolation_header'] in msg.keys(): + try: + attachment = MessageAttachment.objects.get( + pk=msg[settings['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 = BytesIO() + encode_quopri( + 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[settings['altered_message_header']] = ( + 'Missing; Attachment %s not found' % ( + msg[settings['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_body(self): + """Returns the `body` field of this record. + + This will automatically base64-decode the message contents + if they are encoded as such. + + """ + if self.encoded: + return base64.b64decode(self.body.encode('ascii')) + return self.body.encode('utf-8') + + def set_body(self, body): + """Set the `body` field of this record. + + This will automatically base64-encode the message contents to + circumvent a limitation in earlier versions of Django in which + no fields existed for storing arbitrary bytes. + + """ + self.encoded = True + self.body = base64.b64encode(body.encode('utf-8')).decode('ascii') + + def get_email_object(self): + """Returns an `email.message.EmailMessage` instance representing the + contents of this message and all attachments. + + See [email.message.EmailMessage]_ for more information as to what methods + and properties are available on `email.message.EmailMessage` instances. + + .. note:: + + Depending upon the storage methods in use (specifically -- + whether ``DJANGO_MAILBOX_STORE_ORIGINAL_MESSAGE`` is set + to ``True``, this may either create a "rehydrated" message + using stored attachments, or read the message contents stored + on-disk. + + .. [email.message.EmailMessage] Python's `email.message.EmailMessage` docs + (https://docs.python.org/3/library/email.message.html) + + """ + if not hasattr(self, '_email_object'): # Cache fill + if self.eml: + if self.eml.name.endswith('.gz'): + body = gzip.GzipFile(fileobj=self.eml).read() + else: + self.eml.open() + body = self.eml.file.read() + self.eml.close() + else: + body = self.get_body() + flat = email.message_from_bytes(body) + self._email_object = self._rehydrate(flat) + return self._email_object + + def delete(self, *args, **kwargs): + """Delete this message and all stored attachments.""" + for attachment in self.attachments.all(): + # This attachment is attached only to this message. + attachment.delete() + return super().delete(*args, **kwargs) + + def __str__(self): + return self.subject + + class Meta: + verbose_name = _('E-mail message') + verbose_name_plural = _('E-mail messages') + + +class MessageAttachment(models.Model): + message = models.ForeignKey( + Message, + related_name='attachments', + null=True, + blank=True, + verbose_name=_('Message'), + on_delete=models.CASCADE + ) + + headers = models.TextField( + _('Headers'), + null=True, + blank=True, + ) + + document = models.FileField( + _('Document'), + upload_to=utils.get_attachment_save_path, + ) + + def delete(self, *args, **kwargs): + """Deletes the attachment.""" + self.document.delete() + return super().delete(*args, **kwargs) + + def _get_rehydrated_headers(self): + headers = self.headers + if headers is None: + return EmailMessage() + 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): + """Returns the original filename of this attachment.""" + file_name = self._get_rehydrated_headers().get_filename() + if isinstance(file_name, str): + result = utils.convert_header_to_unicode(file_name) + if result is None: + return file_name + return result + else: + return None + + def items(self): + return self._get_rehydrated_headers().items() + + def __getitem__(self, name): + value = self._get_rehydrated_headers()[name] + if value is None: + raise KeyError('Header %s does not exist' % name) + return value + + def __str__(self): + return self.document.url + + class Meta: + verbose_name = _('Message attachment') + verbose_name_plural = _('Message attachments') diff --git a/build/lib/django_mailbox/signals.py b/build/lib/django_mailbox/signals.py new file mode 100644 index 00000000..b445183e --- /dev/null +++ b/build/lib/django_mailbox/signals.py @@ -0,0 +1,3 @@ +from django.dispatch.dispatcher import Signal + +message_received = Signal(providing_args=['message']) diff --git a/build/lib/django_mailbox/south_migrations/0001_initial.py b/build/lib/django_mailbox/south_migrations/0001_initial.py new file mode 100644 index 00000000..dcfdf602 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0001_initial.py @@ -0,0 +1,58 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Mailbox' + db.create_table('django_mailbox_mailbox', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('uri', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('django_mailbox', ['Mailbox']) + + # Adding model 'Message' + db.create_table('django_mailbox_message', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('mailbox', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['django_mailbox.Mailbox'])), + ('subject', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('message_id', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('from_address', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('body', self.gf('django.db.models.fields.TextField')()), + ('received', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('django_mailbox', ['Message']) + + + def backwards(self, orm): + # Deleting model 'Mailbox' + db.delete_table('django_mailbox_mailbox') + + # Deleting model 'Message' + db.delete_table('django_mailbox_message') + + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['django_mailbox.Mailbox']"}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py b/build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py new file mode 100644 index 00000000..47517b31 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py @@ -0,0 +1,38 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Mailbox.uri' + db.alter_column('django_mailbox_mailbox', 'uri', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)) + + def backwards(self, orm): + + # User chose to not deal with backwards NULL issues for 'Mailbox.uri' + raise RuntimeError("Cannot reverse this migration. 'Mailbox.uri' and its values cannot be restored.") + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py b/build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py new file mode 100644 index 00000000..30e43542 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py @@ -0,0 +1,41 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Mailbox.active' + db.add_column('django_mailbox_mailbox', 'active', + self.gf('django.db.models.fields.BooleanField')(default=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Mailbox.active' + db.delete_column('django_mailbox_mailbox', 'active') + + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py b/build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py new file mode 100644 index 00000000..245c43bb --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py @@ -0,0 +1,42 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Message.outgoing' + db.add_column('django_mailbox_message', 'outgoing', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Message.outgoing' + db.delete_column('django_mailbox_message', 'outgoing') + + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0005_rename_fields.py b/build/lib/django_mailbox/south_migrations/0005_rename_fields.py new file mode 100644 index 00000000..72cb4372 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0005_rename_fields.py @@ -0,0 +1,38 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.rename_column('django_mailbox_message', 'from_address', 'address') + db.rename_column('django_mailbox_message', 'received', 'processed') + + def backwards(self, orm): + db.rename_column('django_mailbox_message', 'address', 'from_address') + db.rename_column('django_mailbox_message', 'processed', 'received') + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'body': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py b/build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py new file mode 100644 index 00000000..4287fb26 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py @@ -0,0 +1,55 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Message.in_reply_to' + db.add_column('django_mailbox_message', 'in_reply_to', + self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='replies', null=True, to=orm['django_mailbox.Message']), + keep_default=False) + + # Adding M2M table for field references on 'Message' + db.create_table('django_mailbox_message_references', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('from_message', models.ForeignKey(orm['django_mailbox.message'], null=False)), + ('to_message', models.ForeignKey(orm['django_mailbox.message'], null=False)) + )) + db.create_unique('django_mailbox_message_references', ['from_message_id', 'to_message_id']) + + + def backwards(self, orm): + # Deleting field 'Message.in_reply_to' + db.delete_column('django_mailbox_message', 'in_reply_to_id') + + # Removing M2M table for field references on 'Message' + db.delete_table('django_mailbox_message_references') + + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'body': ('django.db.models.fields.TextField', [], {}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), + 'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py b/build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py new file mode 100644 index 00000000..cfa7d6f0 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py @@ -0,0 +1,59 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'Message.address' + db.delete_column('django_mailbox_message', 'address') + + # Adding field 'Message.from_header' + db.add_column('django_mailbox_message', 'from_header', + self.gf('django.db.models.fields.CharField')(default='', max_length=255), + keep_default=False) + + # Adding field 'Message.to_header' + db.add_column('django_mailbox_message', 'to_header', + self.gf('django.db.models.fields.TextField')(default=''), + keep_default=False) + + + def backwards(self, orm): + + # User chose to not deal with backwards NULL issues for 'Message.address' + raise RuntimeError("Cannot reverse this migration. 'Message.address' and its values cannot be restored.") + # Deleting field 'Message.from_header' + db.delete_column('django_mailbox_message', 'from_header') + + # Deleting field 'Message.to_header' + db.delete_column('django_mailbox_message', 'to_header') + + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), + 'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'to_header': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py b/build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py new file mode 100644 index 00000000..e047f223 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py @@ -0,0 +1,46 @@ +import datetime +import email +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + for message in orm['django_mailbox.message'].objects.all(): + msg_object = email.message_from_string( + message.body + ) + message.from_header = msg_object['from'] + message.to_header = msg_object['to'] + message.save() + + def backwards(self, orm): + raise RuntimeError('Cannot reverse this migration.') + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), + 'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'to_header': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['django_mailbox'] + symmetrical = True diff --git a/build/lib/django_mailbox/south_migrations/0009_remove_references_table.py b/build/lib/django_mailbox/south_migrations/0009_remove_references_table.py new file mode 100644 index 00000000..38685851 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0009_remove_references_table.py @@ -0,0 +1,47 @@ +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 references on 'Message' + db.delete_table('django_mailbox_message_references') + + + def backwards(self, orm): + # Adding M2M table for field references on 'Message' + db.create_table('django_mailbox_message_references', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('from_message', models.ForeignKey(orm['django_mailbox.message'], null=False)), + ('to_message', models.ForeignKey(orm['django_mailbox.message'], null=False)) + )) + db.create_unique('django_mailbox_message_references', ['from_message_id', 'to_message_id']) + + + models = { + 'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'to_header': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py b/build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py new file mode 100644 index 00000000..a988e5eb --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py @@ -0,0 +1,45 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Mailbox.from_email' + db.add_column('django_mailbox_mailbox', 'from_email', + self.gf('django.db.models.fields.CharField')(default=None, max_length=255, null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Mailbox.from_email' + db.delete_column('django_mailbox_mailbox', 'from_email') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'to_header': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py b/build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py new file mode 100644 index 00000000..c75db341 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py @@ -0,0 +1,46 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Message.read' + db.add_column('django_mailbox_message', 'read', + self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Message.read' + db.delete_column('django_mailbox_message', 'read') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py b/build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py new file mode 100644 index 00000000..b62e50e9 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py @@ -0,0 +1,66 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'MessageAttachment' + db.create_table('django_mailbox_messageattachment', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + )) + db.send_create_signal('django_mailbox', ['MessageAttachment']) + + # Adding M2M table for field attachments on 'Message' + db.create_table('django_mailbox_message_attachments', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('message', models.ForeignKey(orm['django_mailbox.message'], null=False)), + ('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False)) + )) + db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id']) + + + def backwards(self, orm): + + # Deleting model 'MessageAttachment' + db.delete_table('django_mailbox_messageattachment') + + # Removing M2M table for field attachments on 'Message' + db.delete_table('django_mailbox_message_attachments') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['django_mailbox.MessageAttachment']", 'symmetrical': 'False'}), + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + }, + 'django_mailbox.messageattachment': { + 'Meta': {'object_name': 'MessageAttachment'}, + 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py b/build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py new file mode 100644 index 00000000..01c80a2e --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py @@ -0,0 +1,53 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'MessageAttachment.message' + db.add_column('django_mailbox_messageattachment', 'message', + self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='attachments', null=True, to=orm['django_mailbox.Message']), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'MessageAttachment.message' + db.delete_column('django_mailbox_messageattachment', 'message_id') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': "orm['django_mailbox.MessageAttachment']"}), + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + }, + 'django_mailbox.messageattachment': { + 'Meta': {'object_name': 'MessageAttachment'}, + 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py b/build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py new file mode 100644 index 00000000..b018915e --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py @@ -0,0 +1,54 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + no_dry_run = True + def forwards(self, orm): + for message in orm['django_mailbox.Message'].objects.all(): + for attachment in message.attachments.all(): + attachment.message = message + attachment.save() + + def backwards(self, orm): + for attachment in orm['django_mailbox.MessageAttachment'].objects.all(): + if attachment.message: + attachment.message.attachments.add( + attachment + ) + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': "orm['django_mailbox.MessageAttachment']"}), + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + }, + 'django_mailbox.messageattachment': { + 'Meta': {'object_name': 'MessageAttachment'}, + 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py b/build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py new file mode 100644 index 00000000..71ecfbdc --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py @@ -0,0 +1,64 @@ +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('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('django_mailbox_message_attachments', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('message', models.ForeignKey(orm['django_mailbox.message'], null=False)), + ('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False)) + )) + db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id']) + + # Deleting field 'MessageAttachment.headers' + db.delete_column('django_mailbox_messageattachment', 'headers') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + }, + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py b/build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py new file mode 100644 index 00000000..cd4c6787 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py @@ -0,0 +1,54 @@ +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Message.encoded' + db.add_column('django_mailbox_message', 'encoded', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Message.encoded' + db.delete_column('django_mailbox_message', 'encoded') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + }, + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py b/build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py new file mode 100644 index 00000000..186c8bd4 --- /dev/null +++ b/build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py @@ -0,0 +1,55 @@ +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Message.eml' + db.add_column('django_mailbox_message', 'eml', + self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Message.eml' + db.delete_column('django_mailbox_message', 'eml') + + + models = { + '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'}), + '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'}) + }, + 'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'eml': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}), + 'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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': "orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) + }, + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/__init__.py b/build/lib/django_mailbox/south_migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build/lib/django_mailbox/tests/__init__.py b/build/lib/django_mailbox/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build/lib/django_mailbox/tests/base.py b/build/lib/django_mailbox/tests/base.py new file mode 100644 index 00000000..b1215c5e --- /dev/null +++ b/build/lib/django_mailbox/tests/base.py @@ -0,0 +1,191 @@ +import email +import os.path +import time + +from django.conf import settings +from django.test import TestCase + +from django_mailbox import models, utils +from django_mailbox.models import Mailbox, Message + + +class EmailIntegrationTimeout(Exception): + pass + + +def get_email_as_text(name): + with open( + os.path.join( + os.path.dirname(__file__), + 'messages', + name, + ), + 'rb' + ) as f: + return f.read() + + +class EmailMessageTestCase(TestCase): + ALLOWED_EXTRA_HEADERS = [ + 'MIME-Version', + 'Content-Transfer-Encoding', + ] + + def setUp(self): + 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') + + self.test_account = os.environ.get('EMAIL_ACCOUNT') + self.test_password = os.environ.get('EMAIL_PASSWORD') + self.test_smtp_server = os.environ.get('EMAIL_SMTP_SERVER') + self.test_from_email = 'nobody@nowhere.com' + + self.maximum_wait_seconds = 60 * 5 + + settings.EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + settings.EMAIL_HOST = self.test_smtp_server + settings.EMAIL_PORT = 587 + settings.EMAIL_HOST_USER = self.test_account + settings.EMAIL_HOST_PASSWORD = self.test_password + settings.EMAIL_USE_TLS = True + super().setUp() + + def _get_new_messages(self, mailbox, condition=None): + start_time = time.time() + # wait until there is at least one message + while time.time() - start_time < self.maximum_wait_seconds: + + messages = self.mailbox.get_new_mail(condition) + + try: + # check if generator contains at least one element + message = next(messages) + yield message + yield from messages + return + + except StopIteration: + time.sleep(5) + + raise EmailIntegrationTimeout() + + def _get_email_as_text(self, name): + with open( + os.path.join( + os.path.dirname(__file__), + 'messages', + name, + ), + 'rb' + ) as f: + return f.read() + + def _get_email_object(self, name): + copy = self._get_email_as_text(name) + return email.message_from_bytes(copy) + + def _headers_identical(self, left, right, header=None): + """ Check if headers are (close enough to) identical. + + * This is particularly tricky because Python 2.6, Python 2.7 and + Python 3 each handle header strings slightly differently. This + should mash away all of the differences, though. + * This also has a small loophole in that when re-writing e-mail + payload encodings, we re-build the Content-Type header, so if the + header was originally unquoted, it will be quoted when rehydrating + the e-mail message. + + """ + if header.lower() == 'content-type': + # Special case; given that we re-write the header, we'll be quoting + # the new content type; we need to make sure that doesn't cause + # this comparison to fail. Also, the case of the encoding could + # be changed, etc. etc. etc. + left = left.replace('"', '').upper() + right = right.replace('"', '').upper() + left = left.replace('\n\t', ' ').replace('\n ', ' ') + right = right.replace('\n\t', ' ').replace('\n ', ' ') + if right != left: + return False + return True + + def compare_email_objects(self, left, right): + # Compare headers + for key, value in left.items(): + if not right[key] and key in self.ALLOWED_EXTRA_HEADERS: + continue + if not right[key]: + raise AssertionError("Extra header '%s'" % key) + if not self._headers_identical(right[key], value, header=key): + raise AssertionError( + "Header '{}' unequal:\n{}\n{}".format( + key, + repr(value), + repr(right[key]), + ) + ) + for key, value in right.items(): + if not left[key] and key in self.ALLOWED_EXTRA_HEADERS: + continue + if not left[key]: + raise AssertionError("Extra header '%s'" % key) + if not self._headers_identical(left[key], value, header=key): + raise AssertionError( + "Header '{}' unequal:\n{}\n{}".format( + 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{}\n{}".format( + left.as_string(), + right.as_string() + ) + ) + + def assertEqual(self, left, right): # noqa: N802 + if not isinstance(left, email.message.Message): + return super().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().tearDown() diff --git a/build/lib/django_mailbox/tests/settings.py b/build/lib/django_mailbox/tests/settings.py new file mode 100644 index 00000000..66c7168e --- /dev/null +++ b/build/lib/django_mailbox/tests/settings.py @@ -0,0 +1,12 @@ +DATABASES = { + 'default': { + 'NAME': 'db.sqlite3', + 'ENGINE': 'django.db.backends.sqlite3', + }, +} +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django_mailbox', +] +SECRET_KEY = 'beepboop' diff --git a/build/lib/django_mailbox/tests/test_integration_imap.py b/build/lib/django_mailbox/tests/test_integration_imap.py new file mode 100644 index 00000000..6f875b74 --- /dev/null +++ b/build/lib/django_mailbox/tests/test_integration_imap.py @@ -0,0 +1,70 @@ +import os +import uuid +from urllib import parse + +from django.core.mail import EmailMultiAlternatives + +from django_mailbox.models import Mailbox +from django_mailbox.tests.base import EmailMessageTestCase + + +__all__ = ['TestImap'] + + +class TestImap(EmailMessageTestCase): + def setUp(self): + super().setUp() + + self.test_imap_server = ( + os.environ.get('EMAIL_IMAP_SERVER') + ) + + required_settings = [ + self.test_imap_server, + self.test_account, + self.test_password, + self.test_smtp_server, + self.test_from_email, + ] + if not all(required_settings): + self.skipTest( + "Integration tests are not available without having " + "the the following environment variables set: " + "EMAIL_ACCOUNT, EMAIL_PASSWORD, EMAIL_SMTP_SERVER, " + "EMAIL_IMAP_SERVER." + ) + + self.mailbox = Mailbox.objects.create( + name='Integration Test Imap', + uri=self.get_connection_string() + ) + self.arbitrary_identifier = str(uuid.uuid4()) + + def get_connection_string(self): + return "imap+ssl://{account}:{password}@{server}".format( + account=parse.quote(self.test_account), + password=parse.quote(self.test_password), + server=self.test_imap_server, + ) + + def test_get_imap_message(self): + text_content = 'This is some content' + msg = EmailMultiAlternatives( + self.arbitrary_identifier, + text_content, + self.test_from_email, + [ + self.test_account, + ] + ) + msg.send() + + messages = self._get_new_messages( + self.mailbox, + condition=lambda m: m['subject'] == self.arbitrary_identifier + ) + message = next(messages) + + self.assertEqual(message.subject, self.arbitrary_identifier) + self.assertEqual(message.text, text_content) + self.assertEqual(0, len(list(messages))) diff --git a/build/lib/django_mailbox/tests/test_mailbox.py b/build/lib/django_mailbox/tests/test_mailbox.py new file mode 100644 index 00000000..e233f753 --- /dev/null +++ b/build/lib/django_mailbox/tests/test_mailbox.py @@ -0,0 +1,46 @@ +import os + +from django.test import TestCase + +from django_mailbox.models import Mailbox + + +__all__ = ['TestMailbox'] + + +class TestMailbox(TestCase): + def test_protocol_info(self): + mailbox = Mailbox() + mailbox.uri = 'alpha://test.com' + + expected_protocol = 'alpha' + actual_protocol = mailbox._protocol_info.scheme + + self.assertEqual( + expected_protocol, + actual_protocol, + ) + + def test_last_polling_field_exists(self): + mailbox = Mailbox() + self.assertTrue(hasattr(mailbox, 'last_polling')) + + def test_get_new_mail_update_last_polling(self): + mailbox = Mailbox.objects.create(uri="mbox://" + os.path.join( + os.path.dirname(__file__), + 'messages', + 'generic_message.eml', + )) + self.assertEqual(mailbox.last_polling, None) + list(mailbox.get_new_mail()) + self.assertNotEqual(mailbox.last_polling, None) + + def test_queryset_get_new_mail(self): + mailbox = Mailbox.objects.create(uri="mbox://" + os.path.join( + os.path.dirname(__file__), + 'messages', + 'generic_message.eml', + )) + Mailbox.objects.filter(pk=mailbox.pk).get_new_mail() + mailbox.refresh_from_db() + self.assertNotEqual(mailbox.last_polling, None) diff --git a/build/lib/django_mailbox/tests/test_message_flattening.py b/build/lib/django_mailbox/tests/test_message_flattening.py new file mode 100644 index 00000000..669433ed --- /dev/null +++ b/build/lib/django_mailbox/tests/test_message_flattening.py @@ -0,0 +1,116 @@ +import copy + +from unittest import mock + +from django_mailbox import models, utils +from django_mailbox.models import Message +from django_mailbox.tests.base import EmailMessageTestCase + + +__all__ = ['TestMessageFlattening'] + + +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) + + del msg._email_object # Cache flush + 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', + ) + default_settings = utils.get_settings() + + 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) + + del msg._email_object # Cache flush + actual_email_object = msg.get_email_object() + + self.assertEqual( + actual_email_object, + expected_email_object, + ) + + def test_message_processing_unknown_encoding(self): + incoming_email_object = self._get_email_object( + 'message_with_invalid_encoding.eml', + ) + + msg = self.mailbox.process_incoming_message(incoming_email_object) + + expected_text = ( + "We offer loans to private individuals and corporate " + "organizations at 2% interest rate. Interested serious " + "applicants should apply via email with details of their " + "requirements.\n\nWarm Regards,\nLoan Team" + ) + actual_text = msg.text + + self.assertEqual(actual_text, expected_text) diff --git a/build/lib/django_mailbox/tests/test_process_email.py b/build/lib/django_mailbox/tests/test_process_email.py new file mode 100644 index 00000000..a6dbd044 --- /dev/null +++ b/build/lib/django_mailbox/tests/test_process_email.py @@ -0,0 +1,432 @@ +import gzip +import os.path +import sys + +import copy +from unittest import mock + +from django_mailbox.models import Mailbox, Message +from django_mailbox.utils import convert_header_to_unicode +from django_mailbox import utils +from django_mailbox.tests.base import EmailMessageTestCase +from django.utils.encoding import force_text +from django.core.mail import EmailMessage + +__all__ = ['TestProcessEmail'] + + +class TestProcessEmail(EmailMessageTestCase): + def test_message_without_attachments(self): + message = self._get_email_object('generic_message.eml') + + mailbox = Mailbox.objects.create() + msg = mailbox.process_incoming_message(message) + + self.assertEqual( + msg.mailbox, + mailbox + ) + self.assertEqual(msg.subject, 'Message Without Attachment') + self.assertEqual( + msg.message_id, + ( + '' + ) + ) + self.assertEqual( + msg.from_header, + 'Adam Coddington ', + ) + self.assertEqual( + msg.to_header, + 'Adam Coddington ', + ) + + def test_message_with_encoded_attachment_filenames(self): + message = self._get_email_object( + 'message_with_koi8r_filename_attachments.eml' + ) + + mailbox = Mailbox.objects.create() + msg = mailbox.process_incoming_message(message) + + attachments = msg.attachments.order_by('pk').all() + self.assertEqual( + '\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b' + '\u043e\u0436\u0435\u043d\u0438\u0439 HSE Career Fair 8 \u0430' + '\u043f\u0440\u0435\u043b\u044f 2016.pdf', + attachments[0].get_filename() + ) + self.assertEqual( + '\u0412\u0435\u0434\u043e\u043c\u043e\u0441\u0442\u0438.pdf', + attachments[1].get_filename() + ) + self.assertEqual( + '\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b' + '\u043e\u0436\u0435\u043d\u0438\u0439 2016.pptx', + attachments[2].get_filename() + ) + + def test_message_with_attachments(self): + message = self._get_email_object('message_with_attachment.eml') + + mailbox = Mailbox.objects.create() + msg = mailbox.process_incoming_message(message) + + expected_count = 1 + actual_count = msg.attachments.count() + + self.assertEqual( + expected_count, + actual_count, + ) + + attachment = msg.attachments.all()[0] + self.assertEqual( + attachment.get_filename(), + 'heart.png', + ) + + def test_message_with_utf8_attachment_header(self): + """ Ensure that we properly handle UTF-8 encoded attachment + + Safe for regress of #104 too + """ + email_object = self._get_email_object( + 'message_with_utf8_attachment.eml', + ) + mailbox = Mailbox.objects.create() + msg = mailbox.process_incoming_message(email_object) + + expected_count = 2 + actual_count = msg.attachments.count() + + self.assertEqual( + expected_count, + actual_count, + ) + + attachment = msg.attachments.all()[0] + self.assertEqual( + attachment.get_filename(), + 'pi\u0142kochwyty.jpg' + ) + + attachment = msg.attachments.all()[1] + self.assertEqual( + attachment.get_filename(), + 'odpowied\u017a Burmistrza.jpg' + ) + + def test_message_get_text_body(self): + message = self._get_email_object('multipart_text.eml') + + mailbox = Mailbox.objects.create() + msg = mailbox.process_incoming_message(message) + + expected_results = 'Hello there!' + actual_results = msg.text.strip() + + self.assertEqual( + expected_results, + actual_results, + ) + + def test_get_text_body_properly_recomposes_line_continuations(self): + message = Message() + email_object = self._get_email_object( + 'message_with_long_text_lines.eml' + ) + + message.get_email_object = lambda: email_object + + actual_text = message.text + expected_text = ( + 'The one of us with a bike pump is far ahead, ' + 'but a man stopped to help us and gave us his pump.' + ) + + self.assertEqual( + actual_text, + expected_text + ) + + def test_get_body_properly_handles_unicode_body(self): + with open( + os.path.join( + os.path.dirname(__file__), + 'messages/generic_message.eml' + ) + ) as f: + unicode_body = f.read() + + message = Message() + message.body = unicode_body + + expected_body = unicode_body + actual_body = message.get_email_object().as_string() + + self.assertEqual( + expected_body, + actual_body + ) + + def test_message_issue_82(self): + """ Ensure that we properly handle incorrectly encoded messages + + """ + email_object = self._get_email_object('email_issue_82.eml') + it = 'works' + try: + # it's ok to call as_string() before passing email_object + # to _get_dehydrated_message() + email_object.as_string() + except: + it = 'do not works' + + success = True + try: + self.mailbox.process_incoming_message(email_object) + except ValueError: + success = False + + self.assertEqual(it, 'works') + self.assertEqual(True, success) + + def test_message_issue_82_bis(self): + """ Ensure that the email object is good before and after + calling _get_dehydrated_message() + + """ + message = self._get_email_object('email_issue_82.eml') + + success = True + + # this is the code of _process_message() + msg = Message() + # if STORE_ORIGINAL_MESSAGE: + # msg.eml.save('%s.eml' % uuid.uuid4(), ContentFile(message), + # save=False) + msg.mailbox = self.mailbox + if 'subject' in message: + msg.subject = convert_header_to_unicode(message['subject'])[0:255] + if 'message-id' in message: + msg.message_id = message['message-id'][0:255] + if 'from' in message: + msg.from_header = convert_header_to_unicode(message['from']) + if 'to' in message: + msg.to_header = convert_header_to_unicode(message['to']) + elif 'Delivered-To' in message: + msg.to_header = convert_header_to_unicode(message['Delivered-To']) + msg.save() + + # here the message is ok + str_msg = message.as_string() + message = self.mailbox._get_dehydrated_message(message, msg) + try: + # here as_string raises UnicodeEncodeError + str_msg = message.as_string() + except: + success = False + + msg.set_body(str_msg) + if message['in-reply-to']: + try: + msg.in_reply_to = Message.objects.filter( + message_id=message['in-reply-to'] + )[0] + except IndexError: + pass + msg.save() + + self.assertEqual(True, success) + + def test_message_with_misplaced_utf8_content(self): + """ Ensure that we properly handle incorrectly encoded messages + + ``message_with_utf8_char.eml``'s primary text payload is marked + as being iso-8859-1 data, but actually contains UTF-8 bytes. + + """ + email_object = self._get_email_object('message_with_utf8_char.eml') + + msg = self.mailbox.process_incoming_message(email_object) + + expected_text = 'This message contains funny UTF16 characters ' + \ + 'like this one: "\xc2\xa0" and this one "\xe2\x9c\xbf".' + actual_text = msg.text + + self.assertEqual( + expected_text, + actual_text, + ) + + def test_message_with_invalid_content_for_declared_encoding(self): + """ Ensure that we gracefully handle mis-encoded bodies. + + Should a payload body be misencoded, we should: + + - Not explode + + Note: there is (intentionally) no assertion below; the only guarantee + we make via this library is that processing this e-mail message will + not cause an exception to be raised. + + """ + email_object = self._get_email_object( + 'message_with_invalid_content_for_declared_encoding.eml', + ) + + msg = self.mailbox.process_incoming_message(email_object) + + msg.text + + def test_message_with_valid_content_in_single_byte_encoding(self): + email_object = self._get_email_object( + 'message_with_single_byte_encoding.eml', + ) + + msg = self.mailbox.process_incoming_message(email_object) + + actual_text = msg.text + expected_body = '\u042d\u0442\u043e ' + \ + '\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 ' + \ + '\u0438\u043c\u0435\u0435\u0442 ' + \ + '\u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d' + \ + '\u0443\u044e ' + \ + '\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430.' + + self.assertEqual( + actual_text, + expected_body, + ) + + def test_message_with_single_byte_subject_encoding(self): + email_object = self._get_email_object( + 'message_with_single_byte_extended_subject_encoding.eml', + ) + + msg = self.mailbox.process_incoming_message(email_object) + + expected_subject = '\u00D3\u00E7\u00ED\u00E0\u00E9 \u00EA\u00E0\u00EA ' + \ + '\u00E7\u00E0\u00F0\u00E0\u00E1\u00E0\u00F2\u00FB\u00E2' + \ + '\u00E0\u00F2\u00FC \u00EE\u00F2 1000$ \u00E2 ' + \ + '\u00ED\u00E5\u00E4\u00E5\u00EB\u00FE!' + actual_subject = msg.subject + self.assertEqual(actual_subject, expected_subject) + + expected_from = 'test test ' + actual_from = msg.from_header + self.assertEqual(expected_from, actual_from) + + def test_message_reply(self): + email_object = EmailMessage( + 'Test subject', # subject + 'Test body', # body + 'username@example.com', # from + ['mr.test32@mail.ru'], # to + ) + msg = self.mailbox.record_outgoing_message(email_object.message()) + + self.assertTrue(msg.outgoing) + + actual_from = 'username@example.com' + reply_email_object = EmailMessage( + 'Test subject', # subject + 'Test body', # body + actual_from, # from + ['mr.test32@mail.ru'], # to + ) + + with mock.patch.object(reply_email_object, 'send'): + reply_msg = msg.reply(reply_email_object) + + self.assertEqual(reply_msg.in_reply_to, msg) + + self.assertEqual(actual_from, msg.from_header) + + reply_email_object.from_email = None + + with mock.patch.object(reply_email_object, 'send'): + second_reply_msg = msg.reply(reply_email_object) + + self.assertEqual(self.mailbox.from_email, second_reply_msg.from_header) + + def test_message_with_text_attachment(self): + email_object = self._get_email_object( + 'message_with_text_attachment.eml', + ) + + msg = self.mailbox.process_incoming_message(email_object) + + self.assertEqual(msg.attachments.all().count(), 1) + self.assertEqual('Has an attached text document, too!', msg.text) + + def test_message_with_long_content(self): + email_object = self._get_email_object( + 'message_with_long_content.eml', + ) + size = len(force_text(email_object.as_string())) + + msg = self.mailbox.process_incoming_message(email_object) + + self.assertEqual(size, + len(force_text(msg.get_email_object().as_string()))) + + def test_message_saved(self): + message = self._get_email_object('generic_message.eml') + + default_settings = utils.get_settings() + + with mock.patch('django_mailbox.utils.get_settings') as get_settings: + altered = copy.deepcopy(default_settings) + altered['store_original_message'] = True + get_settings.return_value = altered + + msg = self.mailbox.process_incoming_message(message) + + self.assertNotEquals(msg.eml, None) + + self.assertTrue(msg.eml.name.endswith('.eml')) + + with open(msg.eml.name, 'rb') as f: + self.assertEqual( + f.read(), + self._get_email_as_text('generic_message.eml') + ) + + def test_message_saving_ignored(self): + message = self._get_email_object('generic_message.eml') + + default_settings = utils.get_settings() + + with mock.patch('django_mailbox.utils.get_settings') as get_settings: + altered = copy.deepcopy(default_settings) + altered['store_original_message'] = False + get_settings.return_value = altered + + msg = self.mailbox.process_incoming_message(message) + + self.assertEquals(msg.eml, None) + + def test_message_compressed(self): + message = self._get_email_object('generic_message.eml') + + default_settings = utils.get_settings() + + with mock.patch('django_mailbox.utils.get_settings') as get_settings: + altered = copy.deepcopy(default_settings) + altered['compress_original_message'] = True + altered['store_original_message'] = True + get_settings.return_value = altered + + msg = self.mailbox.process_incoming_message(message) + + actual_email_object = msg.get_email_object() + + self.assertTrue(msg.eml.name.endswith('.eml.gz')) + + with gzip.open(msg.eml.name, 'rb') as f: + self.assertEqual(f.read(), + self._get_email_as_text('generic_message.eml')) diff --git a/build/lib/django_mailbox/tests/test_processincomingmessage.py b/build/lib/django_mailbox/tests/test_processincomingmessage.py new file mode 100644 index 00000000..8ba2fb29 --- /dev/null +++ b/build/lib/django_mailbox/tests/test_processincomingmessage.py @@ -0,0 +1,65 @@ +from distutils.version import LooseVersion +from unittest import mock + +from django.core.management import call_command, CommandError +from django.test import TestCase +import django + +class CommandsTestCase(TestCase): + def test_processincomingmessage_no_args(self): + """Check that processincomingmessage works with no args""" + + mailbox_name = None + # Mock handle so that the test doesn't hang waiting for input. Note that we are only testing + # the argument parsing here -- functionality should be tested elsewhere + with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle: + # Don't care about the return value + handle.return_value = None + + call_command('processincomingmessage') + args, kwargs = handle.call_args + + # Make sure that we called with the right arguments + try: + self.assertEqual(kwargs['mailbox_name'], mailbox_name) + except KeyError: + # Handle Django 1.7 + # It uses optparse instead of argparse, so instead of being + # set to None, mailbox_name is simply left out altogether + # Thus we expect an empty tuple here + self.assertEqual(args, tuple()) + + + def test_processincomingmessage_with_arg(self): + """Check that processincomingmessage works with mailbox_name given""" + + mailbox_name = 'foo_mailbox' + + with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle: + handle.return_value = None + + call_command('processincomingmessage', mailbox_name) + args, kwargs = handle.call_args + try: + self.assertEqual(kwargs['mailbox_name'], mailbox_name) + except (AssertionError, KeyError): + # Handle Django 1.7 + # It uses optparse instead of argparse, so instead of being + # in kwargs, mailbox_name is in args + self.assertEqual(args[0], mailbox_name) + + def test_processincomingmessage_too_many_args(self): + """Check that processincomingmessage raises an error if too many args""" + # Only perform this test for Django versions greater than 1.7.*. This + # is because, with optparse, too many arguments doesn't result in an + # error, which means this test is worthless anyway + # For the "compatibility" versions, unexpected arguments aren't handled + # very well, and result in a TypeError + if (LooseVersion(django.get_version()) >= LooseVersion('1.8') and + LooseVersion(django.get_version()) < LooseVersion('1.10')): + with self.assertRaises(TypeError): + call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') + # In 1.10 and later a proper CommandError should be raised + elif LooseVersion(django.get_version()) >= LooseVersion('1.10'): + with self.assertRaises(CommandError): + call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') diff --git a/build/lib/django_mailbox/tests/test_transports.py b/build/lib/django_mailbox/tests/test_transports.py new file mode 100644 index 00000000..403a3069 --- /dev/null +++ b/build/lib/django_mailbox/tests/test_transports.py @@ -0,0 +1,167 @@ +from unittest import mock + +from django.test.utils import override_settings + +from django_mailbox.tests.base import EmailMessageTestCase, get_email_as_text +from django_mailbox.transports import ImapTransport, Pop3Transport + +FAKE_UID_SEARCH_ANSWER = ( + 'OK', + [ + b'18 19 20 21 22 23 24 25 26 27 28 29 ' + + b'30 31 32 33 34 35 36 37 38 39 40 41 42 43 44' + ] +) +FAKE_UID_FETCH_SIZES = ( + 'OK', + [ + b'1 (UID 18 RFC822.SIZE 58070000000)', + b'2 (UID 19 RFC822.SIZE 2593)' + ] +) +FAKE_UID_FETCH_MSG = ( + 'OK', + [ + ( + b'1 (UID 18 RFC822 {5807}', + get_email_as_text('generic_message.eml') + ), + ] +) +FAKE_UID_COPY_MSG = ( + 'OK', + [ + b'[COPYUID 1 2 2] (Success)' + ] +) +FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS = ( + 'OK', + [ + b'(\\HasNoChildren \\All) "/" "[Gmail]/All Mail"' + ] +) + + +class IMAPTestCase(EmailMessageTestCase): + def setUp(self): + def imap_server_uid_method(*args): + cmd = args[0] + arg2 = args[2] + if cmd == 'search': + return FAKE_UID_SEARCH_ANSWER + if cmd == 'copy': + return FAKE_UID_COPY_MSG + if cmd == 'fetch': + if arg2 == '(RFC822.SIZE)': + return FAKE_UID_FETCH_SIZES + if arg2 == '(RFC822)': + return FAKE_UID_FETCH_MSG + + def imap_server_list_method(pattern=None): + return FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS + + self.imap_server = mock.Mock() + self.imap_server.uid = imap_server_uid_method + self.imap_server.list = imap_server_list_method + super().setUp() + + +class TestImapTransport(IMAPTestCase): + def setUp(self): + super().setUp() + 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 = self.imap_server + + def test_get_email_message(self): + actual_messages = list(self.transport.get_message()) + self.assertEqual(len(actual_messages), 27) + actual_message = actual_messages[0] + expected_message = self._get_email_object('generic_message.eml') + self.assertEqual(expected_message, actual_message) + + +class TestImapArchivedTransport(TestImapTransport): + def setUp(self): + super().setUp() + self.archive = 'Archive' + self.transport = ImapTransport( + self.arbitrary_hostname, + self.arbitrary_port, + self.ssl, + self.archive + ) + self.transport.server = self.imap_server + + +class TestMaxSizeImapTransport(TestImapTransport): + + @override_settings(DJANGO_MAILBOX_MAX_MESSAGE_SIZE=5807) + def setUp(self): + super().setUp() + + self.transport = ImapTransport( + self.arbitrary_hostname, + self.arbitrary_port, + self.ssl, + ) + self.transport.server = self.imap_server + + def test_size_limit(self): + all_message_ids = self.transport._get_all_message_ids() + small_message_ids = self.transport._get_small_message_ids( + all_message_ids, + ) + self.assertEqual(len(small_message_ids), 1) + + def test_get_email_message(self): + 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().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/build/lib/django_mailbox/transports/__init__.py b/build/lib/django_mailbox/transports/__init__.py new file mode 100644 index 00000000..9f733e34 --- /dev/null +++ b/build/lib/django_mailbox/transports/__init__.py @@ -0,0 +1,11 @@ +# all imports below are only used by external modules +# flake8: noqa +from django_mailbox.transports.imap import ImapTransport +from django_mailbox.transports.pop3 import Pop3Transport +from django_mailbox.transports.maildir import MaildirTransport +from django_mailbox.transports.mbox import MboxTransport +from django_mailbox.transports.babyl import BabylTransport +from django_mailbox.transports.mh import MHTransport +from django_mailbox.transports.mmdf import MMDFTransport +from django_mailbox.transports.gmail import GmailImapTransport +from django_mailbox.transports.office365 import Office365Transport diff --git a/build/lib/django_mailbox/transports/babyl.py b/build/lib/django_mailbox/transports/babyl.py new file mode 100644 index 00000000..eb888a6d --- /dev/null +++ b/build/lib/django_mailbox/transports/babyl.py @@ -0,0 +1,6 @@ +from mailbox import Babyl +from django_mailbox.transports.generic import GenericFileMailbox + + +class BabylTransport(GenericFileMailbox): + _variant = Babyl diff --git a/build/lib/django_mailbox/transports/base.py b/build/lib/django_mailbox/transports/base.py new file mode 100644 index 00000000..a90af9f7 --- /dev/null +++ b/build/lib/django_mailbox/transports/base.py @@ -0,0 +1,11 @@ +import email + +# Do *not* remove this, we need to use this in subclasses of EmailTransport +from email.errors import MessageParseError # noqa: F401 + + +class EmailTransport: + def get_email_from_bytes(self, contents): + message = email.message_from_bytes(contents) + + return message diff --git a/build/lib/django_mailbox/transports/generic.py b/build/lib/django_mailbox/transports/generic.py new file mode 100644 index 00000000..5832fda2 --- /dev/null +++ b/build/lib/django_mailbox/transports/generic.py @@ -0,0 +1,26 @@ +import sys + +from .base import EmailTransport + + +class GenericFileMailbox(EmailTransport): + _variant = None + _path = None + + def __init__(self, path): + super().__init__() + self._path = path + + def get_instance(self): + return self._variant(self._path) + + def get_message(self, condition=None): + repository = self.get_instance() + repository.lock() + for key, message in repository.items(): + if condition and not condition(message): + continue + repository.remove(key) + yield message + repository.flush() + repository.unlock() diff --git a/build/lib/django_mailbox/transports/gmail.py b/build/lib/django_mailbox/transports/gmail.py new file mode 100644 index 00000000..99e8189a --- /dev/null +++ b/build/lib/django_mailbox/transports/gmail.py @@ -0,0 +1,57 @@ +import logging + +from django_mailbox.transports.imap import ImapTransport + + +logger = logging.getLogger(__name__) + + +class GmailImapTransport(ImapTransport): + + def connect(self, username, password): + # Try to use oauth2 first. It's much safer + try: + self._connect_oauth(username) + except (TypeError, ValueError) as e: + logger.warning("Couldn't do oauth2 because %s" % e) + self.server = self.transport(self.hostname, self.port) + typ, msg = self.server.login(username, password) + self.server.select() + + def _connect_oauth(self, username): + # username should be an email address that has already been authorized + # for gmail access + try: + from django_mailbox.google_utils import ( + get_google_access_token, + fetch_user_info, + AccessTokenNotFound, + ) + except ImportError: + raise ValueError( + "Install python-social-auth to use oauth2 auth for gmail" + ) + + access_token = None + while access_token is None: + try: + access_token = get_google_access_token(username) + google_email_address = fetch_user_info(username)['email'] + except TypeError: + # This means that the google process took too long + # Trying again is the right thing to do + pass + except AccessTokenNotFound: + raise ValueError( + "No Token available in python-social-auth for %s" % ( + username + ) + ) + + auth_string = 'user={}\1auth=Bearer {}\1\1'.format( + google_email_address, + access_token + ) + self.server = self.transport(self.hostname, self.port) + self.server.authenticate('XOAUTH2', lambda x: auth_string) + self.server.select() diff --git a/build/lib/django_mailbox/transports/imap.py b/build/lib/django_mailbox/transports/imap.py new file mode 100644 index 00000000..2599adad --- /dev/null +++ b/build/lib/django_mailbox/transports/imap.py @@ -0,0 +1,137 @@ +import imaplib +import logging + +from django.conf import settings + +from .base import EmailTransport, MessageParseError + + +# By default, imaplib will raise an exception if it encounters more +# than 10k bytes; sometimes users attempt to consume mailboxes that +# have a more, and modern computers are skookum-enough to handle just +# a *few* more messages without causing any sort of problem. +imaplib._MAXLINE = 1000000 + + +logger = logging.getLogger(__name__) + + +class ImapTransport(EmailTransport): + def __init__( + self, hostname, port=None, ssl=False, tls=False, + archive='', folder=None, + ): + self.max_message_size = getattr( + settings, + 'DJANGO_MAILBOX_MAX_MESSAGE_SIZE', + False + ) + self.integration_testing_subject = getattr( + settings, + 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', + None + ) + self.hostname = hostname + self.port = port + self.archive = archive + self.folder = folder + self.tls = tls + if ssl: + self.transport = imaplib.IMAP4_SSL + if not self.port: + self.port = 993 + else: + self.transport = imaplib.IMAP4 + if not self.port: + self.port = 143 + + def connect(self, username, password): + self.server = self.transport(self.hostname, self.port) + if self.tls: + self.server.starttls() + typ, msg = self.server.login(username, password) + + if self.folder: + self.server.select(self.folder) + else: + self.server.select() + + def _get_all_message_ids(self): + # Fetch all the message uids + response, message_ids = self.server.uid('search', None, 'ALL') + message_id_string = message_ids[0].strip() + # Usually `message_id_string` will be a list of space-separated + # ids; we must make sure that it isn't an empty string before + # splitting into individual UIDs. + if message_id_string: + return message_id_string.decode().split(' ') + return [] + + def _get_small_message_ids(self, message_ids): + # Using existing message uids, get the sizes and + # return only those that are under the size + # limit + safe_message_ids = [] + + status, data = self.server.uid( + 'fetch', + ','.join(message_ids), + '(RFC822.SIZE)' + ) + + for each_msg in data: + each_msg = each_msg.decode() + try: + uid = each_msg.split(' ')[2] + size = each_msg.split(' ')[4].rstrip(')') + if int(size) <= int(self.max_message_size): + safe_message_ids.append(uid) + except ValueError as e: + logger.warning( + "ValueError: {} working on {}".format(e, each_msg[0]) + ) + pass + return safe_message_ids + + def get_message(self, condition=None): + message_ids = self._get_all_message_ids() + + if not message_ids: + return + + # Limit the uids to the small ones if we care about that + if self.max_message_size: + message_ids = self._get_small_message_ids(message_ids) + + if self.archive: + typ, folders = self.server.list(pattern=self.archive) + if folders[0] is None: + # If the archive folder does not exist, create it + self.server.create(self.archive) + + for uid in message_ids: + try: + typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') + if not msg_contents: + continue + try: + message = self.get_email_from_bytes(msg_contents[0][1]) + except TypeError: + # This happens if another thread/process deletes the + # message between our generating the ID list and our + # processing it here. + continue + + if condition and not condition(message): + continue + + yield message + except MessageParseError: + continue + + if self.archive: + self.server.uid('copy', uid, self.archive) + + self.server.uid('store', uid, "+FLAGS", "(\\Deleted)") + self.server.expunge() + return diff --git a/build/lib/django_mailbox/transports/maildir.py b/build/lib/django_mailbox/transports/maildir.py new file mode 100644 index 00000000..2d4af53d --- /dev/null +++ b/build/lib/django_mailbox/transports/maildir.py @@ -0,0 +1,9 @@ +from mailbox import Maildir +from django_mailbox.transports.generic import GenericFileMailbox + + +class MaildirTransport(GenericFileMailbox): + _variant = Maildir + + def get_instance(self): + return self._variant(self._path, None) diff --git a/build/lib/django_mailbox/transports/mbox.py b/build/lib/django_mailbox/transports/mbox.py new file mode 100644 index 00000000..d610deb8 --- /dev/null +++ b/build/lib/django_mailbox/transports/mbox.py @@ -0,0 +1,6 @@ +from mailbox import mbox +from django_mailbox.transports.generic import GenericFileMailbox + + +class MboxTransport(GenericFileMailbox): + _variant = mbox diff --git a/build/lib/django_mailbox/transports/mh.py b/build/lib/django_mailbox/transports/mh.py new file mode 100644 index 00000000..33fdc254 --- /dev/null +++ b/build/lib/django_mailbox/transports/mh.py @@ -0,0 +1,6 @@ +from mailbox import MH +from django_mailbox.transports.generic import GenericFileMailbox + + +class MHTransport(GenericFileMailbox): + _variant = MH diff --git a/build/lib/django_mailbox/transports/mmdf.py b/build/lib/django_mailbox/transports/mmdf.py new file mode 100644 index 00000000..270e4f34 --- /dev/null +++ b/build/lib/django_mailbox/transports/mmdf.py @@ -0,0 +1,6 @@ +from mailbox import MMDF +from django_mailbox.transports.generic import GenericFileMailbox + + +class MMDFTransport(GenericFileMailbox): + _variant = MMDF diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py new file mode 100644 index 00000000..f98f8c14 --- /dev/null +++ b/build/lib/django_mailbox/transports/office365.py @@ -0,0 +1,131 @@ +import O365 +import logging + +from django.conf import settings + +from .base import EmailTransport, MessageParseError + +logger = logging.getLogger(__name__) + + +class Office365Transport(EmailTransport): + def __init__( + self, hostname, port=None, ssl=False, tls=False, + archive='', folder=None, client_id=None, client_secret=None, tenant_id=None + ): + self.max_message_size = getattr( + settings, + 'DJANGO_MAILBOX_MAX_MESSAGE_SIZE', + False + ) + self.integration_testing_subject = getattr( + settings, + 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', + None + ) + self.hostname = hostname + self.port = port + self.archive = archive + self.folder = folder + self.tls = tls + if ssl: + #self.transport = imaplib.IMAP4_SSL + if not self.port: + self.port = 993 + else: + #self.transport = imaplib.IMAP4 + if not self.port: + self.port = 143 + self.client_id = client_id + self.client_secret = client_secret + self.tenant_id = tenant_id + + def connect(self, username, password): + credentials = (self.client_id, self.client_secret) + + # the default protocol will be Microsoft Graph + # the default authentication method will be "on behalf of a user" + + self.server = O365.Account(credentials, auth_flow_type='credentials', tenant_id=self.tenant_id) + if self.server.authenticate(scopes=['mailbox']): + print('Authenticated!') + + def _get_all_message_ids(self): + # # Fetch all the message uids + # response, message_ids = self.server.uid('search', None, 'ALL') + # message_id_string = message_ids[0].strip() + # # Usually `message_id_string` will be a list of space-separated + # # ids; we must make sure that it isn't an empty string before + # # splitting into individual UIDs. + # if message_id_string: + # return message_id_string.decode().split(' ') + # return [] + + def _get_small_message_ids(self, message_ids): + # Using existing message uids, get the sizes and + # return only those that are under the size + # limit + safe_message_ids = [] + + status, data = self.server.uid( + 'fetch', + ','.join(message_ids), + '(RFC822.SIZE)' + ) + + for each_msg in data: + each_msg = each_msg.decode() + try: + uid = each_msg.split(' ')[2] + size = each_msg.split(' ')[4].rstrip(')') + if int(size) <= int(self.max_message_size): + safe_message_ids.append(uid) + except ValueError as e: + logger.warning( + "ValueError: {} working on {}".format(e, each_msg[0]) + ) + pass + return safe_message_ids + + def get_message(self, condition=None): + message_ids = self._get_all_message_ids() + + if not message_ids: + return + + # Limit the uids to the small ones if we care about that + if self.max_message_size: + message_ids = self._get_small_message_ids(message_ids) + + if self.archive: + typ, folders = self.server.list(pattern=self.archive) + if folders[0] is None: + # If the archive folder does not exist, create it + self.server.create(self.archive) + + for uid in message_ids: + try: + typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') + if not msg_contents: + continue + try: + message = self.get_email_from_bytes(msg_contents[0][1]) + except TypeError: + # This happens if another thread/process deletes the + # message between our generating the ID list and our + # processing it here. + continue + + if condition and not condition(message): + continue + + yield message + except MessageParseError: + continue + + if self.archive: + self.server.uid('copy', uid, self.archive) + + self.server.uid('store', uid, "+FLAGS", "(\\Deleted)") + self.server.expunge() + return diff --git a/build/lib/django_mailbox/transports/pop3.py b/build/lib/django_mailbox/transports/pop3.py new file mode 100644 index 00000000..6dbe9953 --- /dev/null +++ b/build/lib/django_mailbox/transports/pop3.py @@ -0,0 +1,44 @@ +from poplib import POP3, POP3_SSL + +from .base import EmailTransport, MessageParseError + + +class Pop3Transport(EmailTransport): + def __init__(self, hostname, port=None, ssl=False): + self.hostname = hostname + self.port = port + if ssl: + self.transport = POP3_SSL + if not self.port: + self.port = 995 + else: + self.transport = POP3 + if not self.port: + self.port = 110 + + def connect(self, username, password): + self.server = self.transport(self.hostname, self.port) + self.server.user(username) + self.server.pass_(password) + + def get_message_body(self, message_lines): + return bytes('\r\n', 'ascii').join(message_lines) + + def get_message(self, condition=None): + message_count = len(self.server.list()[1]) + for i in range(message_count): + try: + msg_contents = self.get_message_body( + self.server.retr(i + 1)[1] + ) + message = self.get_email_from_bytes(msg_contents) + + if condition and not condition(message): + continue + + yield message + except MessageParseError: + continue + self.server.dele(i + 1) + self.server.quit() + return diff --git a/build/lib/django_mailbox/utils.py b/build/lib/django_mailbox/utils.py new file mode 100644 index 00000000..6603612f --- /dev/null +++ b/build/lib/django_mailbox/utils.py @@ -0,0 +1,151 @@ +import datetime +import email.header +import logging +import os + +from django.conf import settings + + +logger = logging.getLogger(__name__) + + +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 + ), + 'compress_original_message': getattr( + settings, + 'DJANGO_MAILBOX_COMPRESS_ORIGINAL_MESSAGE', + False + ), + 'original_message_compression': getattr( + settings, + 'DJANGO_MAILBOX_ORIGINAL_MESSAGE_COMPRESSION', + 6 + ), + '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, str): + return value + if not encoding or encoding == 'unknown-8bit': + encoding = default_charset + return value.decode(encoding, 'replace') + + try: + return ''.join( + [ + ( + _decode(bytestr, encoding) + ) for bytestr, encoding in email.header.decode_header(header) + ] + ) + except UnicodeDecodeError: + logger.exception( + 'Errors encountered decoding header %s into encoding %s.', + header, + default_charset, + ) + return header.decode(default_charset, 'replace') + + +def get_body_from_message(message, maintype, subtype): + """ + Fetchs the body message matching main/sub content type. + """ + body = '' + for part in message.walk(): + if part.get('content-disposition', '').startswith('attachment;'): + continue + if part.get_content_maintype() == maintype and \ + part.get_content_subtype() == subtype: + charset = part.get_content_charset() + this_part = part.get_payload(decode=True) + if charset: + try: + this_part = this_part.decode(charset, 'replace') + except LookupError: + this_part = this_part.decode('ascii', 'replace') + logger.warning( + 'Unknown encoding %s encountered while decoding ' + 'text payload. Interpreting as ASCII with ' + 'replacement, but some data may not be ' + 'represented as the sender intended.', + charset + ) + except ValueError: + this_part = this_part.decode('ascii', 'replace') + logger.warning( + 'Error encountered while decoding text ' + 'payload from an incorrectly-constructed ' + 'e-mail; payload was converted to ASCII with ' + 'replacement, but some data may not be ' + 'represented as the sender intended.' + ) + else: + this_part = this_part.decode('ascii', 'replace') + + body += this_part + + return body + + +def get_attachment_save_path(instance, filename): + settings = get_settings() + + path = settings['attachment_upload_to'] + if '%' in path: + path = datetime.datetime.utcnow().strftime(path) + + return os.path.join( + path, + filename, + ) From 912f091d3f840508e530ae623dc3f2476b1315a9 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Wed, 27 Jul 2022 10:58:48 +0200 Subject: [PATCH 06/18] Update models.py --- django_mailbox/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 52e4c1e7..28c2bb54 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -249,12 +249,13 @@ def get_connection(self): conn.connect(self.username, self.password) elif self.type == 'office365': conn = Office365Transport( + self.location, port=self.port if self.port else None, ssl=True, - archive=self.archive, client_id=self.client_id, client_secret=self.client_secret, - tenant_id=self.tenant_id + tenant_id=self.tenant_id, + folder=self.folder ) conn.connect(self.username, self.password) elif self.type == 'maildir': From dca20e1e1c2fd91a47e562ebb021d9784b0ec255 Mon Sep 17 00:00:00 2001 From: Serafim Bordei Date: Wed, 27 Jul 2022 11:49:48 +0200 Subject: [PATCH 07/18] Update office365.py and models.py --- build/lib/django_mailbox/transports/office365.py | 5 ++--- django_mailbox/models.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py index f98f8c14..674e910d 100644 --- a/build/lib/django_mailbox/transports/office365.py +++ b/build/lib/django_mailbox/transports/office365.py @@ -10,7 +10,7 @@ class Office365Transport(EmailTransport): def __init__( - self, hostname, port=None, ssl=False, tls=False, + self, port=None, ssl=False, tls=False, archive='', folder=None, client_id=None, client_secret=None, tenant_id=None ): self.max_message_size = getattr( @@ -23,7 +23,6 @@ def __init__( 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', None ) - self.hostname = hostname self.port = port self.archive = archive self.folder = folder @@ -59,7 +58,7 @@ def _get_all_message_ids(self): # # splitting into individual UIDs. # if message_id_string: # return message_id_string.decode().split(' ') - # return [] + return [] def _get_small_message_ids(self, message_ids): # Using existing message uids, get the sizes and diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 28c2bb54..1606f8b8 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -208,7 +208,7 @@ def client_secret(self): @property def tenant_id(self): """Returns (if specified) the tenant id for Office365.""" - tenant_id = self._query_string.get('tentant_id', None) + tenant_id = self._query_string.get('tenant_id', None) if not tenant_id: return None return tenant_id[0] From ced6b0997b4cdab60daded44e4b560a510fe391d Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Fri, 29 Jul 2022 16:01:30 +0200 Subject: [PATCH 08/18] office365 implementation --- django_mailbox/models.py | 6 +- django_mailbox/transports/base.py | 5 ++ django_mailbox/transports/office365.py | 111 +++++++------------------ 3 files changed, 38 insertions(+), 84 deletions(-) diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 1606f8b8..b9f84cc5 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -250,14 +250,12 @@ def get_connection(self): elif self.type == 'office365': conn = Office365Transport( self.location, + self.username, port=self.port if self.port else None, ssl=True, - client_id=self.client_id, - client_secret=self.client_secret, - tenant_id=self.tenant_id, folder=self.folder ) - conn.connect(self.username, self.password) + conn.connect(self.client_id, self.client_secret, self.tenant_id) elif self.type == 'maildir': conn = MaildirTransport(self.location) elif self.type == 'mbox': diff --git a/django_mailbox/transports/base.py b/django_mailbox/transports/base.py index a90af9f7..9d507a23 100644 --- a/django_mailbox/transports/base.py +++ b/django_mailbox/transports/base.py @@ -9,3 +9,8 @@ def get_email_from_bytes(self, contents): message = email.message_from_bytes(contents) return message + + def get_email_from_string(self, contents): + message = email.message_from_string(contents) + + return message diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index f98f8c14..bc8a2c68 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -10,8 +10,7 @@ class Office365Transport(EmailTransport): def __init__( - self, hostname, port=None, ssl=False, tls=False, - archive='', folder=None, client_id=None, client_secret=None, tenant_id=None + self, hostname, username, port=None, ssl=False, tls=False, folder=None ): self.max_message_size = getattr( settings, @@ -24,8 +23,8 @@ def __init__( None ) self.hostname = hostname + self.username = username self.port = port - self.archive = archive self.folder = folder self.tls = tls if ssl: @@ -36,85 +35,29 @@ def __init__( #self.transport = imaplib.IMAP4 if not self.port: self.port = 143 - self.client_id = client_id - self.client_secret = client_secret - self.tenant_id = tenant_id - - def connect(self, username, password): - credentials = (self.client_id, self.client_secret) - - # the default protocol will be Microsoft Graph - # the default authentication method will be "on behalf of a user" - - self.server = O365.Account(credentials, auth_flow_type='credentials', tenant_id=self.tenant_id) - if self.server.authenticate(scopes=['mailbox']): - print('Authenticated!') - - def _get_all_message_ids(self): - # # Fetch all the message uids - # response, message_ids = self.server.uid('search', None, 'ALL') - # message_id_string = message_ids[0].strip() - # # Usually `message_id_string` will be a list of space-separated - # # ids; we must make sure that it isn't an empty string before - # # splitting into individual UIDs. - # if message_id_string: - # return message_id_string.decode().split(' ') - # return [] - - def _get_small_message_ids(self, message_ids): - # Using existing message uids, get the sizes and - # return only those that are under the size - # limit - safe_message_ids = [] - - status, data = self.server.uid( - 'fetch', - ','.join(message_ids), - '(RFC822.SIZE)' - ) - for each_msg in data: - each_msg = each_msg.decode() - try: - uid = each_msg.split(' ')[2] - size = each_msg.split(' ')[4].rstrip(')') - if int(size) <= int(self.max_message_size): - safe_message_ids.append(uid) - except ValueError as e: - logger.warning( - "ValueError: {} working on {}".format(e, each_msg[0]) - ) - pass - return safe_message_ids - - def get_message(self, condition=None): - message_ids = self._get_all_message_ids() + def connect(self, client_id, client_secret, tenant_id): + credentials = (client_id, client_secret) - if not message_ids: - return + self.account = O365.Account(credentials, auth_flow_type='credentials', tenant_id=tenant_id) + self.account.authenticate() - # Limit the uids to the small ones if we care about that - if self.max_message_size: - message_ids = self._get_small_message_ids(message_ids) + self.mailbox = self.account.mailbox(resource=self.username) + self.mailbox_folder = self.mailbox.inbox_folder() + if self.folder: + self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) - if self.archive: - typ, folders = self.server.list(pattern=self.archive) - if folders[0] is None: - # If the archive folder does not exist, create it - self.server.create(self.archive) + def get_message_body(self, message_lines): + return bytes('\r\n', 'ascii').join(message_lines) - for uid in message_ids: + def get_message(self, condition=None): + message_count = len(self.server.list()[1]) + for i in range(message_count): try: - typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') - if not msg_contents: - continue - try: - message = self.get_email_from_bytes(msg_contents[0][1]) - except TypeError: - # This happens if another thread/process deletes the - # message between our generating the ID list and our - # processing it here. - continue + msg_contents = self.get_message_body( + self.server.retr(i + 1)[1] + ) + message = self.get_email_from_bytes(msg_contents) if condition and not condition(message): continue @@ -122,10 +65,18 @@ def get_message(self, condition=None): yield message except MessageParseError: continue + self.server.dele(i + 1) - if self.archive: - self.server.uid('copy', uid, self.archive) + for message in self.mailbox.get_messages(order_by='receivedDateTime'): + try: + mime_content = message.get_mime_content() + message = self.get_email_from_string(mime_content) + + if condition and not condition(message): + continue - self.server.uid('store', uid, "+FLAGS", "(\\Deleted)") - self.server.expunge() + yield message + except MessageParseError: + continue return + From b02fd73d1c4577e4cb66fe9227080d7fd15bd707 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Fri, 29 Jul 2022 16:06:30 +0200 Subject: [PATCH 09/18] Update office365.py --- django_mailbox/transports/office365.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index bc8a2c68..e7f4fab3 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -47,26 +47,7 @@ def connect(self, client_id, client_secret, tenant_id): if self.folder: self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) - def get_message_body(self, message_lines): - return bytes('\r\n', 'ascii').join(message_lines) - def get_message(self, condition=None): - message_count = len(self.server.list()[1]) - for i in range(message_count): - try: - msg_contents = self.get_message_body( - self.server.retr(i + 1)[1] - ) - message = self.get_email_from_bytes(msg_contents) - - if condition and not condition(message): - continue - - yield message - except MessageParseError: - continue - self.server.dele(i + 1) - for message in self.mailbox.get_messages(order_by='receivedDateTime'): try: mime_content = message.get_mime_content() From 8f944190adbdd4f5ded2e048cb19f25364b6b42c Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Fri, 29 Jul 2022 16:20:38 +0200 Subject: [PATCH 10/18] clear code --- build/lib/django_mailbox/models.py | 11 +-- build/lib/django_mailbox/transports/base.py | 5 + .../django_mailbox/transports/office365.py | 97 +++---------------- django_mailbox/transports/base.py | 4 - django_mailbox/transports/office365.py | 2 +- 5 files changed, 26 insertions(+), 93 deletions(-) diff --git a/build/lib/django_mailbox/models.py b/build/lib/django_mailbox/models.py index 52e4c1e7..b9f84cc5 100644 --- a/build/lib/django_mailbox/models.py +++ b/build/lib/django_mailbox/models.py @@ -208,7 +208,7 @@ def client_secret(self): @property def tenant_id(self): """Returns (if specified) the tenant id for Office365.""" - tenant_id = self._query_string.get('tentant_id', None) + tenant_id = self._query_string.get('tenant_id', None) if not tenant_id: return None return tenant_id[0] @@ -249,14 +249,13 @@ def get_connection(self): conn.connect(self.username, self.password) elif self.type == 'office365': conn = Office365Transport( + self.location, + self.username, port=self.port if self.port else None, ssl=True, - archive=self.archive, - client_id=self.client_id, - client_secret=self.client_secret, - tenant_id=self.tenant_id + folder=self.folder ) - conn.connect(self.username, self.password) + conn.connect(self.client_id, self.client_secret, self.tenant_id) elif self.type == 'maildir': conn = MaildirTransport(self.location) elif self.type == 'mbox': diff --git a/build/lib/django_mailbox/transports/base.py b/build/lib/django_mailbox/transports/base.py index a90af9f7..9d507a23 100644 --- a/build/lib/django_mailbox/transports/base.py +++ b/build/lib/django_mailbox/transports/base.py @@ -9,3 +9,8 @@ def get_email_from_bytes(self, contents): message = email.message_from_bytes(contents) return message + + def get_email_from_string(self, contents): + message = email.message_from_string(contents) + + return message diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py index 674e910d..e60531b7 100644 --- a/build/lib/django_mailbox/transports/office365.py +++ b/build/lib/django_mailbox/transports/office365.py @@ -10,8 +10,7 @@ class Office365Transport(EmailTransport): def __init__( - self, port=None, ssl=False, tls=False, - archive='', folder=None, client_id=None, client_secret=None, tenant_id=None + self, hostname, username, port=None, ssl=False, tls=False, folder=None ): self.max_message_size = getattr( settings, @@ -23,8 +22,9 @@ def __init__( 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', None ) + self.hostname = hostname + self.username = username self.port = port - self.archive = archive self.folder = folder self.tls = tls if ssl: @@ -35,85 +35,23 @@ def __init__( #self.transport = imaplib.IMAP4 if not self.port: self.port = 143 - self.client_id = client_id - self.client_secret = client_secret - self.tenant_id = tenant_id - def connect(self, username, password): - credentials = (self.client_id, self.client_secret) + def connect(self, client_id, client_secret, tenant_id): + credentials = (client_id, client_secret) - # the default protocol will be Microsoft Graph - # the default authentication method will be "on behalf of a user" + self.account = O365.Account(credentials, auth_flow_type='credentials', tenant_id=tenant_id) + self.account.authenticate() - self.server = O365.Account(credentials, auth_flow_type='credentials', tenant_id=self.tenant_id) - if self.server.authenticate(scopes=['mailbox']): - print('Authenticated!') - - def _get_all_message_ids(self): - # # Fetch all the message uids - # response, message_ids = self.server.uid('search', None, 'ALL') - # message_id_string = message_ids[0].strip() - # # Usually `message_id_string` will be a list of space-separated - # # ids; we must make sure that it isn't an empty string before - # # splitting into individual UIDs. - # if message_id_string: - # return message_id_string.decode().split(' ') - return [] - - def _get_small_message_ids(self, message_ids): - # Using existing message uids, get the sizes and - # return only those that are under the size - # limit - safe_message_ids = [] - - status, data = self.server.uid( - 'fetch', - ','.join(message_ids), - '(RFC822.SIZE)' - ) - - for each_msg in data: - each_msg = each_msg.decode() - try: - uid = each_msg.split(' ')[2] - size = each_msg.split(' ')[4].rstrip(')') - if int(size) <= int(self.max_message_size): - safe_message_ids.append(uid) - except ValueError as e: - logger.warning( - "ValueError: {} working on {}".format(e, each_msg[0]) - ) - pass - return safe_message_ids + self.mailbox = self.account.mailbox(resource=self.username) + self.mailbox_folder = self.mailbox.inbox_folder() + if self.folder: + self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) def get_message(self, condition=None): - message_ids = self._get_all_message_ids() - - if not message_ids: - return - - # Limit the uids to the small ones if we care about that - if self.max_message_size: - message_ids = self._get_small_message_ids(message_ids) - - if self.archive: - typ, folders = self.server.list(pattern=self.archive) - if folders[0] is None: - # If the archive folder does not exist, create it - self.server.create(self.archive) - - for uid in message_ids: + for message in self.mailbox.get_messages(order_by='receivedDateTime'): try: - typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') - if not msg_contents: - continue - try: - message = self.get_email_from_bytes(msg_contents[0][1]) - except TypeError: - # This happens if another thread/process deletes the - # message between our generating the ID list and our - # processing it here. - continue + mime_content = message.get_mime_content() + message = self.get_email_from_bytes(mime_content) if condition and not condition(message): continue @@ -121,10 +59,5 @@ def get_message(self, condition=None): yield message except MessageParseError: continue - - if self.archive: - self.server.uid('copy', uid, self.archive) - - self.server.uid('store', uid, "+FLAGS", "(\\Deleted)") - self.server.expunge() return + diff --git a/django_mailbox/transports/base.py b/django_mailbox/transports/base.py index 9d507a23..a3a2bc01 100644 --- a/django_mailbox/transports/base.py +++ b/django_mailbox/transports/base.py @@ -10,7 +10,3 @@ def get_email_from_bytes(self, contents): return message - def get_email_from_string(self, contents): - message = email.message_from_string(contents) - - return message diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index e7f4fab3..e60531b7 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -51,7 +51,7 @@ def get_message(self, condition=None): for message in self.mailbox.get_messages(order_by='receivedDateTime'): try: mime_content = message.get_mime_content() - message = self.get_email_from_string(mime_content) + message = self.get_email_from_bytes(mime_content) if condition and not condition(message): continue From 9bfc3e5df115d6d154568af327b898a4888d2839 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Fri, 29 Jul 2022 17:26:28 +0200 Subject: [PATCH 11/18] fix import --- build/lib/django_mailbox/models.py | 2 -- build/lib/django_mailbox/transports/base.py | 4 --- .../django_mailbox/transports/office365.py | 25 +++++----------- django_mailbox/models.py | 2 -- django_mailbox/transports/office365.py | 29 ++++++++----------- 5 files changed, 20 insertions(+), 42 deletions(-) diff --git a/build/lib/django_mailbox/models.py b/build/lib/django_mailbox/models.py index b9f84cc5..c7c03f32 100644 --- a/build/lib/django_mailbox/models.py +++ b/build/lib/django_mailbox/models.py @@ -251,8 +251,6 @@ def get_connection(self): conn = Office365Transport( self.location, self.username, - port=self.port if self.port else None, - ssl=True, folder=self.folder ) conn.connect(self.client_id, self.client_secret, self.tenant_id) diff --git a/build/lib/django_mailbox/transports/base.py b/build/lib/django_mailbox/transports/base.py index 9d507a23..a3a2bc01 100644 --- a/build/lib/django_mailbox/transports/base.py +++ b/build/lib/django_mailbox/transports/base.py @@ -10,7 +10,3 @@ def get_email_from_bytes(self, contents): return message - def get_email_from_string(self, contents): - message = email.message_from_string(contents) - - return message diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py index e60531b7..47c86d62 100644 --- a/build/lib/django_mailbox/transports/office365.py +++ b/build/lib/django_mailbox/transports/office365.py @@ -1,4 +1,3 @@ -import O365 import logging from django.conf import settings @@ -10,13 +9,8 @@ class Office365Transport(EmailTransport): def __init__( - self, hostname, username, port=None, ssl=False, tls=False, folder=None + self, hostname, username, folder=None ): - self.max_message_size = getattr( - settings, - 'DJANGO_MAILBOX_MAX_MESSAGE_SIZE', - False - ) self.integration_testing_subject = getattr( settings, 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', @@ -24,19 +18,16 @@ def __init__( ) self.hostname = hostname self.username = username - self.port = port self.folder = folder - self.tls = tls - if ssl: - #self.transport = imaplib.IMAP4_SSL - if not self.port: - self.port = 993 - else: - #self.transport = imaplib.IMAP4 - if not self.port: - self.port = 143 def connect(self, client_id, client_secret, tenant_id): + try: + import O365 + except ImportError: + raise ValueError( + "Install o365 to use oauth2 auth for office365" + ) + credentials = (client_id, client_secret) self.account = O365.Account(credentials, auth_flow_type='credentials', tenant_id=tenant_id) diff --git a/django_mailbox/models.py b/django_mailbox/models.py index b9f84cc5..c7c03f32 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -251,8 +251,6 @@ def get_connection(self): conn = Office365Transport( self.location, self.username, - port=self.port if self.port else None, - ssl=True, folder=self.folder ) conn.connect(self.client_id, self.client_secret, self.tenant_id) diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index e60531b7..2c137b35 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -1,4 +1,3 @@ -import O365 import logging from django.conf import settings @@ -10,13 +9,8 @@ class Office365Transport(EmailTransport): def __init__( - self, hostname, username, port=None, ssl=False, tls=False, folder=None + self, hostname, username, folder=None ): - self.max_message_size = getattr( - settings, - 'DJANGO_MAILBOX_MAX_MESSAGE_SIZE', - False - ) self.integration_testing_subject = getattr( settings, 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', @@ -24,19 +18,16 @@ def __init__( ) self.hostname = hostname self.username = username - self.port = port self.folder = folder - self.tls = tls - if ssl: - #self.transport = imaplib.IMAP4_SSL - if not self.port: - self.port = 993 - else: - #self.transport = imaplib.IMAP4 - if not self.port: - self.port = 143 def connect(self, client_id, client_secret, tenant_id): + try: + import O365 + except ImportError: + raise ValueError( + "Install o365 to use oauth2 auth for office365" + ) + credentials = (client_id, client_secret) self.account = O365.Account(credentials, auth_flow_type='credentials', tenant_id=tenant_id) @@ -56,8 +47,12 @@ def get_message(self, condition=None): if condition and not condition(message): continue + #TODO: implement archive function + yield message except MessageParseError: continue + + # TODO: delete all messages return From 1f6014918cd2e5e00ac2c47f1b00c719cb0b1b8e Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Fri, 29 Jul 2022 17:32:15 +0200 Subject: [PATCH 12/18] Update readme.rst --- readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index 0aac06b1..50342d6a 100644 --- a/readme.rst +++ b/readme.rst @@ -8,7 +8,7 @@ :target: https://pypi.python.org/pypi/django-mailbox -Easily ingest messages from POP3, IMAP, or local mailboxes into your Django application. +Easily ingest messages from POP3, IMAP, Office365 or local mailboxes into your Django application. This app allows you to either ingest e-mail content from common e-mail services (as long as the service provides POP3 or IMAP support), or directly receive e-mail messages from ``stdin`` (for locally processing messages from Postfix or Exim4). From b39299692a4bd8f58b4593818881c346ea4975f9 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Mon, 1 Aug 2022 12:39:09 +0200 Subject: [PATCH 13/18] archive and delete --- build/lib/django_mailbox/models.py | 1 + build/lib/django_mailbox/transports/office365.py | 11 ++++++++++- django_mailbox/transports/office365.py | 13 +++++++++---- readme.rst | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/build/lib/django_mailbox/models.py b/build/lib/django_mailbox/models.py index c7c03f32..cbd3d181 100644 --- a/build/lib/django_mailbox/models.py +++ b/build/lib/django_mailbox/models.py @@ -251,6 +251,7 @@ def get_connection(self): conn = Office365Transport( self.location, self.username, + archive=self.archive, folder=self.folder ) conn.connect(self.client_id, self.client_secret, self.tenant_id) diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py index 47c86d62..bca9b748 100644 --- a/build/lib/django_mailbox/transports/office365.py +++ b/build/lib/django_mailbox/transports/office365.py @@ -9,7 +9,7 @@ class Office365Transport(EmailTransport): def __init__( - self, hostname, username, folder=None + self, hostname, username, archive='', folder=None ): self.integration_testing_subject = getattr( settings, @@ -18,6 +18,7 @@ def __init__( ) self.hostname = hostname self.username = username + self.archive = archive self.folder = folder def connect(self, client_id, client_secret, tenant_id): @@ -39,6 +40,9 @@ def connect(self, client_id, client_secret, tenant_id): self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) def get_message(self, condition=None): + if self.archive: + self.mailbox.create_child_folder(self.archive) + for message in self.mailbox.get_messages(order_by='receivedDateTime'): try: mime_content = message.get_mime_content() @@ -50,5 +54,10 @@ def get_message(self, condition=None): yield message except MessageParseError: continue + + if self.archive: + message.copy(self.archive) + + message.delete() return diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index 2c137b35..bca9b748 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -9,7 +9,7 @@ class Office365Transport(EmailTransport): def __init__( - self, hostname, username, folder=None + self, hostname, username, archive='', folder=None ): self.integration_testing_subject = getattr( settings, @@ -18,6 +18,7 @@ def __init__( ) self.hostname = hostname self.username = username + self.archive = archive self.folder = folder def connect(self, client_id, client_secret, tenant_id): @@ -39,6 +40,9 @@ def connect(self, client_id, client_secret, tenant_id): self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) def get_message(self, condition=None): + if self.archive: + self.mailbox.create_child_folder(self.archive) + for message in self.mailbox.get_messages(order_by='receivedDateTime'): try: mime_content = message.get_mime_content() @@ -47,12 +51,13 @@ def get_message(self, condition=None): if condition and not condition(message): continue - #TODO: implement archive function - yield message except MessageParseError: continue - # TODO: delete all messages + if self.archive: + message.copy(self.archive) + + message.delete() return diff --git a/readme.rst b/readme.rst index 50342d6a..f03459e7 100644 --- a/readme.rst +++ b/readme.rst @@ -8,7 +8,7 @@ :target: https://pypi.python.org/pypi/django-mailbox -Easily ingest messages from POP3, IMAP, Office365 or local mailboxes into your Django application. +Easily ingest messages from POP3, IMAP, Office365 API or local mailboxes into your Django application. This app allows you to either ingest e-mail content from common e-mail services (as long as the service provides POP3 or IMAP support), or directly receive e-mail messages from ``stdin`` (for locally processing messages from Postfix or Exim4). From 9f314a38f2ec000e4daf19ea86165886df2033ad Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Mon, 1 Aug 2022 15:03:11 +0200 Subject: [PATCH 14/18] fix and documentation --- .../django_mailbox/transports/office365.py | 15 ++++++++------ django_mailbox/transports/office365.py | 15 ++++++++------ docs/topics/mailbox_types.rst | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py index bca9b748..4f748b2f 100644 --- a/build/lib/django_mailbox/transports/office365.py +++ b/build/lib/django_mailbox/transports/office365.py @@ -40,12 +40,15 @@ def connect(self, client_id, client_secret, tenant_id): self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) def get_message(self, condition=None): + archive_folder = None if self.archive: - self.mailbox.create_child_folder(self.archive) + archive_folder = self.mailbox.get_folder(folder_name=self.archive) + if not archive_folder: + archive_folder = self.mailbox.create_child_folder(self.archive) - for message in self.mailbox.get_messages(order_by='receivedDateTime'): + for o365message in self.mailbox_folder.get_messages(order_by='receivedDateTime'): try: - mime_content = message.get_mime_content() + mime_content = o365message.get_mime_content() message = self.get_email_from_bytes(mime_content) if condition and not condition(message): @@ -55,9 +58,9 @@ def get_message(self, condition=None): except MessageParseError: continue - if self.archive: - message.copy(self.archive) + if self.archive and archive_folder: + o365message.copy(archive_folder) - message.delete() + o365message.delete() return diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index bca9b748..4f748b2f 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -40,12 +40,15 @@ def connect(self, client_id, client_secret, tenant_id): self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) def get_message(self, condition=None): + archive_folder = None if self.archive: - self.mailbox.create_child_folder(self.archive) + archive_folder = self.mailbox.get_folder(folder_name=self.archive) + if not archive_folder: + archive_folder = self.mailbox.create_child_folder(self.archive) - for message in self.mailbox.get_messages(order_by='receivedDateTime'): + for o365message in self.mailbox_folder.get_messages(order_by='receivedDateTime'): try: - mime_content = message.get_mime_content() + mime_content = o365message.get_mime_content() message = self.get_email_from_bytes(mime_content) if condition and not condition(message): @@ -55,9 +58,9 @@ def get_message(self, condition=None): except MessageParseError: continue - if self.archive: - message.copy(self.archive) + if self.archive and archive_folder: + o365message.copy(archive_folder) - message.delete() + o365message.delete() return diff --git a/docs/topics/mailbox_types.rst b/docs/topics/mailbox_types.rst index ad8c9ff8..2d5a4573 100644 --- a/docs/topics/mailbox_types.rst +++ b/docs/topics/mailbox_types.rst @@ -13,6 +13,7 @@ POP3 and IMAP as well as local file-based mailboxes. POP3 ``pop3://`` Can also specify SSL with ``pop3+ssl://`` IMAP ``imap://`` Can also specify SSL with ``imap+ssl://`` or STARTTLS with ``imap+tls``; additional configuration is also possible: see :ref:`pop3-and-imap-mailboxes` for details. Gmail IMAP ``gmail+ssl://`` Uses OAuth authentication for Gmail's IMAP transport. See :ref:`gmail-oauth` for details. + Office365 API``office365://`` Uses OAuth authentication for Office365 API transport. See :ref:`office365-oauth` for details. Maildir ``maildir://`` Mbox ``mbox://`` Babyl ``babyl://`` @@ -115,6 +116,25 @@ Build your URI accordingly:: gmail+ssl://youremailaddress%40gmail.com:oauth2@imap.gmail.com?archive=Archived +.. _office365-oauth: +Office 365 API +------------------------------------- + +Office 365 allows through the API to read a mailbox with Oauth. +The O365_ library is used. + +.. _O365: https://github.com/O365/python-o365 +.. _configuration: https://github.com/O365/python-o365#authentication + +For the configuration_ you need to register an application and get a client_id, client_secret and tenant_id. + +This implementation uses the client credentials grant flow and the password you specify will be ignored. + +Build your URI accordingly:: + + office365://youremailaddress%40yourdomain.com:oauth2@outlook.office365.com?client_id=client_id&client_secret=client_secret&tenant_id=tenant_id&archive=Archived + + Local File-based Mailboxes -------------------------- From 70aa88bbd6d2f339540b62c8f0f2900fe43f12dd Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Mon, 1 Aug 2022 17:21:50 +0200 Subject: [PATCH 15/18] removed build folder --- build/lib/django_mailbox/__init__.py | 3 - build/lib/django_mailbox/admin.py | 112 --- build/lib/django_mailbox/apps.py | 7 - build/lib/django_mailbox/google_utils.py | 111 --- .../locale/ru_RU/LC_MESSAGES/django.mo | Bin 5329 -> 0 bytes .../locale/ru_RU/LC_MESSAGES/django.po | 193 ---- .../lib/django_mailbox/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/getmail.py | 30 - .../commands/processincomingmessage.py | 52 -- .../commands/rebuildmessageattachments.py | 71 -- .../django_mailbox/migrations/0001_initial.py | 56 -- .../migrations/0002_add_eml_to_message.py | 17 - .../migrations/0003_auto_20150409_0316.py | 16 - .../migrations/0004_bytestring_to_unicode.py | 21 - .../migrations/0005_auto_20160523_2240.py | 17 - .../migrations/0006_mailbox_last_polling.py | 18 - .../migrations/0007_auto_20180421_0026.py | 25 - .../migrations/0008_auto_20190219_1553.py | 23 - .../lib/django_mailbox/migrations/__init__.py | 0 build/lib/django_mailbox/models.py | 853 ------------------ build/lib/django_mailbox/signals.py | 3 - .../south_migrations/0001_initial.py | 58 -- .../0002_auto__chg_field_mailbox_uri.py | 38 - .../0003_auto__add_field_mailbox_active.py | 41 - .../0004_auto__add_field_message_outgoing.py | 42 - .../south_migrations/0005_rename_fields.py | 38 - ...006_auto__add_field_message_in_reply_to.py | 55 -- ...__add_field_message_from_header__add_fi.py | 59 -- .../0008_populate_from_to_fields.py | 46 - .../0009_remove_references_table.py | 47 - ...0010_auto__add_field_mailbox_from_email.py | 45 - .../0011_auto__add_field_message_read.py | 46 - .../0012_auto__add_messageattachment.py | 66 -- ...to__add_field_messageattachment_message.py | 53 -- .../0014_migrate_message_attachments.py | 54 -- ...to__add_field_messageattachment_headers.py | 64 -- .../0016_auto__add_field_message_encoded.py | 54 -- .../0017_auto__add_field_message_eml.py | 55 -- .../south_migrations/__init__.py | 0 build/lib/django_mailbox/tests/__init__.py | 0 build/lib/django_mailbox/tests/base.py | 191 ---- build/lib/django_mailbox/tests/settings.py | 12 - .../tests/test_integration_imap.py | 70 -- .../lib/django_mailbox/tests/test_mailbox.py | 46 - .../tests/test_message_flattening.py | 116 --- .../tests/test_process_email.py | 432 --------- .../tests/test_processincomingmessage.py | 65 -- .../django_mailbox/tests/test_transports.py | 167 ---- .../lib/django_mailbox/transports/__init__.py | 11 - build/lib/django_mailbox/transports/babyl.py | 6 - build/lib/django_mailbox/transports/base.py | 12 - .../lib/django_mailbox/transports/generic.py | 26 - build/lib/django_mailbox/transports/gmail.py | 57 -- build/lib/django_mailbox/transports/imap.py | 137 --- .../lib/django_mailbox/transports/maildir.py | 9 - build/lib/django_mailbox/transports/mbox.py | 6 - build/lib/django_mailbox/transports/mh.py | 6 - build/lib/django_mailbox/transports/mmdf.py | 6 - .../django_mailbox/transports/office365.py | 66 -- build/lib/django_mailbox/transports/pop3.py | 44 - build/lib/django_mailbox/utils.py | 151 ---- 62 files changed, 4125 deletions(-) delete mode 100644 build/lib/django_mailbox/__init__.py delete mode 100644 build/lib/django_mailbox/admin.py delete mode 100644 build/lib/django_mailbox/apps.py delete mode 100644 build/lib/django_mailbox/google_utils.py delete mode 100644 build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.mo delete mode 100644 build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po delete mode 100644 build/lib/django_mailbox/management/__init__.py delete mode 100644 build/lib/django_mailbox/management/commands/__init__.py delete mode 100644 build/lib/django_mailbox/management/commands/getmail.py delete mode 100644 build/lib/django_mailbox/management/commands/processincomingmessage.py delete mode 100644 build/lib/django_mailbox/management/commands/rebuildmessageattachments.py delete mode 100644 build/lib/django_mailbox/migrations/0001_initial.py delete mode 100644 build/lib/django_mailbox/migrations/0002_add_eml_to_message.py delete mode 100644 build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py delete mode 100644 build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py delete mode 100644 build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py delete mode 100644 build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py delete mode 100644 build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py delete mode 100644 build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py delete mode 100644 build/lib/django_mailbox/migrations/__init__.py delete mode 100644 build/lib/django_mailbox/models.py delete mode 100644 build/lib/django_mailbox/signals.py delete mode 100644 build/lib/django_mailbox/south_migrations/0001_initial.py delete mode 100644 build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py delete mode 100644 build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py delete mode 100644 build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py delete mode 100644 build/lib/django_mailbox/south_migrations/0005_rename_fields.py delete mode 100644 build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py delete mode 100644 build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py delete mode 100644 build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py delete mode 100644 build/lib/django_mailbox/south_migrations/0009_remove_references_table.py delete mode 100644 build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py delete mode 100644 build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py delete mode 100644 build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py delete mode 100644 build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py delete mode 100644 build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py delete mode 100644 build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py delete mode 100644 build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py delete mode 100644 build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py delete mode 100644 build/lib/django_mailbox/south_migrations/__init__.py delete mode 100644 build/lib/django_mailbox/tests/__init__.py delete mode 100644 build/lib/django_mailbox/tests/base.py delete mode 100644 build/lib/django_mailbox/tests/settings.py delete mode 100644 build/lib/django_mailbox/tests/test_integration_imap.py delete mode 100644 build/lib/django_mailbox/tests/test_mailbox.py delete mode 100644 build/lib/django_mailbox/tests/test_message_flattening.py delete mode 100644 build/lib/django_mailbox/tests/test_process_email.py delete mode 100644 build/lib/django_mailbox/tests/test_processincomingmessage.py delete mode 100644 build/lib/django_mailbox/tests/test_transports.py delete mode 100644 build/lib/django_mailbox/transports/__init__.py delete mode 100644 build/lib/django_mailbox/transports/babyl.py delete mode 100644 build/lib/django_mailbox/transports/base.py delete mode 100644 build/lib/django_mailbox/transports/generic.py delete mode 100644 build/lib/django_mailbox/transports/gmail.py delete mode 100644 build/lib/django_mailbox/transports/imap.py delete mode 100644 build/lib/django_mailbox/transports/maildir.py delete mode 100644 build/lib/django_mailbox/transports/mbox.py delete mode 100644 build/lib/django_mailbox/transports/mh.py delete mode 100644 build/lib/django_mailbox/transports/mmdf.py delete mode 100644 build/lib/django_mailbox/transports/office365.py delete mode 100644 build/lib/django_mailbox/transports/pop3.py delete mode 100644 build/lib/django_mailbox/utils.py diff --git a/build/lib/django_mailbox/__init__.py b/build/lib/django_mailbox/__init__.py deleted file mode 100644 index feda890b..00000000 --- a/build/lib/django_mailbox/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__version__ = '4.8.2' - -default_app_config = 'django_mailbox.apps.MailBoxConfig' diff --git a/build/lib/django_mailbox/admin.py b/build/lib/django_mailbox/admin.py deleted file mode 100644 index 4d489b1e..00000000 --- a/build/lib/django_mailbox/admin.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python - -""" -Model configuration in application ``django_mailbox`` for administration -console. -""" - -import logging - -from django.conf import settings -from django.contrib import admin -from django.utils.translation import gettext_lazy as _ - -from django_mailbox.models import MessageAttachment, Message, Mailbox -from django_mailbox.signals import message_received -from django_mailbox.utils import convert_header_to_unicode - - -logger = logging.getLogger(__name__) - - -def get_new_mail(mailbox_admin, request, queryset): - queryset.get_new_mail() - - -get_new_mail.short_description = _('Get new mail') - - -def resend_message_received_signal(message_admin, request, queryset): - for message in queryset.all(): - logger.debug('Resending \'message_received\' signal for %s' % message) - message_received.send(sender=message_admin, message=message) - - -resend_message_received_signal.short_description = ( - _('Re-send message received signal') -) - - -class MailboxAdmin(admin.ModelAdmin): - list_display = ( - 'name', - 'uri', - 'from_email', - 'active', - 'last_polling', - ) - readonly_fields = ['last_polling', ] - actions = [get_new_mail] - - -class MessageAttachmentAdmin(admin.ModelAdmin): - raw_id_fields = ('message', ) - list_display = ('message', 'document',) - - -class MessageAttachmentInline(admin.TabularInline): - model = MessageAttachment - extra = 0 - - -class MessageAdmin(admin.ModelAdmin): - def attachment_count(self, msg): - return msg.attachments.count() - - attachment_count.short_description = _('Attachment count') - - def subject(self, msg): - return convert_header_to_unicode(msg.subject) - - def envelope_headers(self, msg): - email = msg.get_email_object() - return '\n'.join( - [('{}: {}'.format(h, v)) for h, v in email.items()] - ) - - inlines = [ - MessageAttachmentInline, - ] - list_display = ( - 'subject', - 'processed', - 'read', - 'mailbox', - 'outgoing', - 'attachment_count', - ) - ordering = ['-processed'] - list_filter = ( - 'mailbox', - 'outgoing', - 'processed', - 'read', - ) - exclude = ( - 'body', - ) - raw_id_fields = ( - 'in_reply_to', - ) - readonly_fields = ( - 'envelope_headers', - 'text', - 'html', - ) - actions = [resend_message_received_signal] - - -if getattr(settings, 'DJANGO_MAILBOX_ADMIN_ENABLED', True): - admin.site.register(Message, MessageAdmin) - admin.site.register(MessageAttachment, MessageAttachmentAdmin) - admin.site.register(Mailbox, MailboxAdmin) diff --git a/build/lib/django_mailbox/apps.py b/build/lib/django_mailbox/apps.py deleted file mode 100644 index 13b7bf3c..00000000 --- a/build/lib/django_mailbox/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class MailBoxConfig(AppConfig): - name = 'django_mailbox' - verbose_name = _("Mail Box") diff --git a/build/lib/django_mailbox/google_utils.py b/build/lib/django_mailbox/google_utils.py deleted file mode 100644 index b834f59c..00000000 --- a/build/lib/django_mailbox/google_utils.py +++ /dev/null @@ -1,111 +0,0 @@ -import logging - -from django.conf import settings -import requests -from social.apps.django_app.default.models import UserSocialAuth - - -logger = logging.getLogger(__name__) - - -class AccessTokenNotFound(Exception): - pass - - -class RefreshTokenNotFound(Exception): - pass - - -def get_google_consumer_key(): - return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY - - -def get_google_consumer_secret(): - return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET - - -def get_google_access_token(email): - # TODO: This should be cacheable - try: - me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") - return me.extra_data['access_token'] - except (UserSocialAuth.DoesNotExist, KeyError): - raise AccessTokenNotFound - - -def update_google_extra_data(email, extra_data): - try: - me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") - me.extra_data = extra_data - me.save() - except (UserSocialAuth.DoesNotExist, KeyError): - raise AccessTokenNotFound - - -def get_google_refresh_token(email): - try: - me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") - return me.extra_data['refresh_token'] - except (UserSocialAuth.DoesNotExist, KeyError): - raise RefreshTokenNotFound - - -def google_api_get(email, url): - headers = dict( - Authorization="Bearer %s" % get_google_access_token(email), - ) - r = requests.get(url, headers=headers) - logger.info("I got a %s", r.status_code) - if r.status_code == 401: - # Go use the refresh token - refresh_authorization(email) - r = requests.get(url, headers=headers) - logger.info("I got a %s", r.status_code) - if r.status_code == 200: - try: - return r.json() - except ValueError: - return r.text - - -def google_api_post(email, url, post_data, authorized=True): - # TODO: Make this a lot less ugly. especially the 401 handling - headers = dict() - if authorized is True: - headers.update(dict( - Authorization="Bearer %s" % get_google_access_token(email), - )) - r = requests.post(url, headers=headers, data=post_data) - if r.status_code == 401: - refresh_authorization(email) - r = requests.post(url, headers=headers, data=post_data) - if r.status_code == 200: - try: - return r.json() - except ValueError: - return r.text - - -def refresh_authorization(email): - refresh_token = get_google_refresh_token(email) - post_data = dict( - refresh_token=refresh_token, - client_id=get_google_consumer_key(), - client_secret=get_google_consumer_secret(), - grant_type='refresh_token', - ) - results = google_api_post( - email, - "https://accounts.google.com/o/oauth2/token?access_type=offline", - post_data, - authorized=False) - results.update({'refresh_token': refresh_token}) - update_google_extra_data(email, results) - - -def fetch_user_info(email): - result = google_api_get( - email, - "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" - ) - return result diff --git a/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.mo b/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.mo deleted file mode 100644 index 06dd2c9e8c0550671d97cef5a6fb48e395c4f0e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5329 zcmbVQQEVJX8J0SBK~zHGrP3w%8rw-+9*}qf?Mg_zph`SdLV`!sN4{@n_k5Q) zDCOk4Z+GUOfByfQZ~htilgA!>U*NMH-|yo)^neh1fvU0L}xy3_K4UfZavl81Ro@5~2s~kD&8wz$btY19t#vzb9=k zr|qLa`ac20pLh*FUj^Ffy9cDdIpAZ!v%qfwF9R{9_#IFJKLpaxUw|C{H{iE{{{TJ% z{11@!kHZL$;z{5o;B!FQe+29W{sD-T#Xo_cA)kK($;+n*g8Y33M3dNtJr-AH$ z`0FOVr&25ezl-+6z(2w6HQ?WY+d&@Vc;Hc!*F(Sp-X8@rzk?~B06vBHY2XvUb17a0 zK8g2LAnpGO%mY6KGLCI%B<@UcA8-KguK@8Ueu^LR@*Z#y_*>v8@Dm{Z#14?l{04y~ z#Cr(H^$RiRC~y%-e*O$Zx4^Vpe#klFl=zZ2&iOrjnG>b=9el|H=VZKGgY3h6`C%NL zAI?h+a{WN3))%1tVVv9XCGXTUKhNR&IKI@-)A&N3`a=zG!S-CxI-ykgn&r6C@uvJUa@r51r{=T9nhHZ}MupOD z1dcZ&>%Qv}s|!_Eg#{@m>7hzz9BunB_54WAT60QTo>bG*sv5~g-S_1Dtcqq;AkAzT z1z+e?wnVF9w`T&P0lfvPs*piwMob5OO_nD*HDajRo_;&7&&9r>aHrwaYQ?ely|z(e&a4W zxZpPe+sB(%sQR^?GtvH`sX*>NkQ+r3Ipk~EAR_W*s3J|d--u>>607K)P3qWRNyk@Y~G1x;fG=Aapb6v(+9FU z^m)g1b0C%RBaIq~#gw{1k9_^e$mpSyM<-8>P8=IQH8Or^?C9%-P12m2RewGV-SY0; zwFTr9cvel7YYTNN4Cnp8J{bB~02R!ErYTDhs0_%D9`n)mMfrWZiH40T zog;`WXT94}XJKJ1;?|gQE$?)@@{u}lfwDstpquq9+ozkI(X54pD2+6vEcQ^&IaXSz z3S!bvce|Jj8cI6TdY7czZ3>l`GYngydSO5sx!B_5#29H|O8GH6|3fIi@x8L_nNU=G zemtCU?EG+JCd^OzXg+>yGJiNwRs^g35i3Gb+EXm;%NOzYqAcz$_rH)Y?kyH!n4eH{ z4!zxNSL!eK?|Z)34?Bt^KgpfqT9F@=<=C97=HwCOA=n|?$*bTe2Nyt(I#UQ52Vn2b zG;mDhCzVx0K-qg)28~k_Cws=n#z!{OC>4r5hm9iglM8i38>utV?z#){W6MJIV7qd1 zax_1hN=feA zDfw7@wo)p|mt;}G9PbAzd$O*f$^akrh~iLbk9^||Gvq*Npcg|*>5xjL$S!Snpt5&! z_=~;mcKF3K*_=Zacz8+ffrI|u{XIX*zpOkJ7;2CE%61}IDhw9(6-qrKelK26mg3d; zdVD*%5U+{&m+?k?GmrNTiJzNz`zUV3w_$?L5AcX45&t@FCW~=1z7@BUrDRbi%kes1 zZ@|ZOAQ~mcH2Kqli)+c7_+5dMTk%G67DL{a@yGD60rM7cJzhlv`pzR7Hg3efM+C{m z>$bZl0$5GXqqW60*d`Z}H)V1bkDTbOELwywlb`9)H)*?JCWK+LAma}?528%Y z!CwW47mp5#dd_rg+7U(7~={;OWsMHb!I>|+wtp>@y%ozfv4=G z$tKGZA>N?7CK+QiXt`G?j9KT=1V`%_yhYy0J8;tCx@v-NYnIkJf;Tgos5Q-;)1_vLR1aP$1+4t%yV1l~ARa9x zt&C=J5#-;MEt}gZHM_1i@%tbQ0>UaM7gCZ}(S+5&^1KZJFLT|Ym-|`N6^y}DaL^G{ z3+tZEPN8%<>fSUUgBr$^dW)rjZV_U(yR`os(T&(-%D0}g_@!k$Val2*SbZ5W?7O&))JHr zlElYr*zc?#=>-fz;(BIxFujs1I{ihAU}0+}?_vNmh9!hd7O~W%56gTGuWPykieLkG zP1o!YN{%OS_oa>dehG%_jO%_cY}9X;*4DJv+Lt#hUMmwQHb|;tqs(meum_FmjE>Fn zYxSC9z?Em}SZFP9aKX~I%tZwsADb-C>J7*`dlUKXGM7E@m@F0Il>)AO@l|eg{Osc8 zua_mCwya>Qnt!a8F23aKj^dkbew1*-ELd6<@g)>t#K4MgjtA{j=i5S*`i$wQ)225) z5W33g{r4cY>sTa>LUl!_MMDx{!~8idGVzg1F~bUN9A*g{I34NUu2ki-p&PNxG4 zO`GN_JjkT0qQK8p2%o~_NkrZ0LfW?byc000>n2eSH4DQ7ndgdGU*x}v z(?Op}=+`wZ6@a-9fKV24oY@?ag~Thut+)wl(1*uV!yT*KGDkZEiLKB|ey(HZX^)un zod6x!Tz6>y6sN;m`jvV8`+5bt_rkHR<=7 g`#TKGX$5-q#p^Cs$nFiD#a+iN??2|Mudn$31sHQPiU0rr diff --git a/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po b/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po deleted file mode 100644 index d84c3cb8..00000000 --- a/build/lib/django_mailbox/locale/ru_RU/LC_MESSAGES/django.po +++ /dev/null @@ -1,193 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-07-09 05:36-0500\n" -"PO-Revision-Date: 2017-07-09 13:37+0300\n" -"Last-Translator: Ivlev Denis \n" -"Language-Team: \n" -"Language: ru_RU\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" -"%100>=11 && n%100<=14)? 2 : 3);\n" -"X-Generator: Poedit 1.8.7.1\n" - -#: django_mailbox/admin.py:29 -msgid "Get new mail" -msgstr "Получить новые сообщения" - -#: django_mailbox/admin.py:39 -msgid "Re-send message received signal" -msgstr "Повторно отправить сигнал о получении" - -#: django_mailbox/admin.py:69 -msgid "Attachment count" -msgstr "Кол-во вложений" - -#: django_mailbox/apps.py:7 -msgid "Mail Box" -msgstr "Почтовый ящик" - -#: django_mailbox/models.py:49 -msgid "Name" -msgstr "Название" - -#: django_mailbox/models.py:54 -msgid "URI" -msgstr "URI" - -#: django_mailbox/models.py:57 -msgid "" -"Example: imap+ssl://myusername:mypassword@someserver

Internet " -"transports include 'imap' and 'pop3'; common local file transports include " -"'maildir', 'mbox', and less commonly 'babyl', 'mh', and 'mmdf'.

Be sure to urlencode your username and password should they contain illegal " -"characters (like @, :, etc)." -msgstr "" -"Пример: imap+ssl://myusername:mypassword@someserver

Интернет-" -"транспорт может быть 'imap' или 'pop3'; поддерживаются локальные файловые " -"транспорты 'maildir', 'mbox', а также 'babyl', 'mh', and 'mmdf'.

Используйте urlencode, если имя пользователя или пароль содержат " -"недопустипые символы (@, :, и т.д.)." - -#: django_mailbox/models.py:72 -msgid "From email" -msgstr "От" - -#: django_mailbox/models.py:75 -msgid "" -"Example: MailBot <mailbot@yourdomain.com>
'From' header to set " -"for outgoing email.

If you do not use this e-mail inbox for " -"outgoing mail, this setting is unnecessary.
If you send e-mail without " -"setting this, your 'From' header will'be set to match the setting " -"`DEFAULT_FROM_EMAIL`." -msgstr "" -"Пример: MailBot <mailbot@yourdomain.com>
Исходящая электронная " -"почта.

Если вы не используете этот почтовый ящик для исходящей " -"почты, этот параметр не нужен.
Если вы не указали данный параметр, " -"будет использоваться указанный в настройках `DEFAULT_FROM_EMAIL`." - -#: django_mailbox/models.py:89 -msgid "Active" -msgstr "Активный" - -#: django_mailbox/models.py:91 -msgid "" -"Check this e-mail inbox for new e-mail messages during polling cycles. This " -"checkbox does not have an effect upon whether mail is collected here when " -"this mailbox receives mail from a pipe, and does not affect whether e-mail " -"messages can be dispatched from this mailbox. " -msgstr "" -"Параметр указывает на необходимость проверки почтового ящика на наличие " -"новых сообщений в цикле опроса. Этот флажок не влияет на сбор почты, когда " -"этот почтовый ящик получает почту из канала и не влияет на отправку " -"сообщений электронной почты из этого почтового ящика." - -#: django_mailbox/models.py:102 -msgid "Last polling" -msgstr "Последний опрос" - -#: django_mailbox/models.py:103 -msgid "" -"The time of last successful polling for messages.It is blank for new " -"mailboxes and is not set for mailboxes that only receive messages via a pipe." -msgstr "" -"Время последнего успешного опроса сообщений. Для нового почтового ящика не " -"установлено. Также не устанавливается для почтовых ящиков " -"обновляющих сообщения через pipe." - -#: django_mailbox/models.py:409 django_mailbox/models.py:438 -msgid "Mailbox" -msgstr "Почтовый ящик" - -#: django_mailbox/models.py:410 -msgid "Mailboxes" -msgstr "Почтовые ящики" - -#: django_mailbox/models.py:442 -msgid "Subject" -msgstr "Тема" - -#: django_mailbox/models.py:447 -msgid "Message ID" -msgstr "Идентификатор сообщения" - -#: django_mailbox/models.py:456 -msgid "In reply to" -msgstr "В ответ на" - -#: django_mailbox/models.py:460 -msgid "From header" -msgstr "От(From)" - -#: django_mailbox/models.py:465 -msgid "To header" -msgstr "Кому(To)" - -#: django_mailbox/models.py:469 -msgid "Outgoing" -msgstr "Исходящее" - -#: django_mailbox/models.py:475 -msgid "Body" -msgstr "Тело" - -#: django_mailbox/models.py:479 -msgid "Encoded" -msgstr "Закодировано" - -#: django_mailbox/models.py:481 -msgid "True if the e-mail body is Base64 encoded" -msgstr "True если тело сообщения закодировано в Base64" - -#: django_mailbox/models.py:485 -msgid "Processed" -msgstr "Обработано" - -#: django_mailbox/models.py:490 -msgid "Read" -msgstr "Прочитано" - -#: django_mailbox/models.py:497 -msgid "Raw message contents" -msgstr "Исходное содержимое сообщения" - -#: django_mailbox/models.py:500 -msgid "Original full content of message" -msgstr "Полное содержимое сообщения" - -#: django_mailbox/models.py:716 -msgid "E-mail message" -msgstr "Сообщение" - -#: django_mailbox/models.py:717 -msgid "E-mail messages" -msgstr "Сообщения" - -#: django_mailbox/models.py:726 -msgid "Message" -msgstr "Сообщение" - -#: django_mailbox/models.py:730 -msgid "Headers" -msgstr "Заголовки" - -#: django_mailbox/models.py:736 -msgid "Document" -msgstr "Документ" - -#: django_mailbox/models.py:793 -msgid "Message attachment" -msgstr "Вложение" - -#: django_mailbox/models.py:794 -msgid "Message attachments" -msgstr "Вложения" diff --git a/build/lib/django_mailbox/management/__init__.py b/build/lib/django_mailbox/management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/build/lib/django_mailbox/management/commands/__init__.py b/build/lib/django_mailbox/management/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/build/lib/django_mailbox/management/commands/getmail.py b/build/lib/django_mailbox/management/commands/getmail.py deleted file mode 100644 index 31b2dbdc..00000000 --- a/build/lib/django_mailbox/management/commands/getmail.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging - -from django.core.management.base import BaseCommand - -from django_mailbox.models import Mailbox - - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -class Command(BaseCommand): - def handle(self, *args, **options): - mailboxes = Mailbox.active_mailboxes.all() - if args: - mailboxes = mailboxes.filter( - name=' '.join(args) - ) - for mailbox in mailboxes: - logger.info( - 'Gathering messages for %s', - mailbox.name - ) - messages = mailbox.get_new_mail() - for message in messages: - logger.info( - 'Received %s (from %s)', - message.subject, - message.from_address - ) diff --git a/build/lib/django_mailbox/management/commands/processincomingmessage.py b/build/lib/django_mailbox/management/commands/processincomingmessage.py deleted file mode 100644 index ec344111..00000000 --- a/build/lib/django_mailbox/management/commands/processincomingmessage.py +++ /dev/null @@ -1,52 +0,0 @@ -import email -import logging -import sys -try: - from email import utils -except ImportError: - import rfc822 as utils - -from django.core.management.base import BaseCommand - -from django_mailbox.models import Mailbox - - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -class Command(BaseCommand): - args = "<[Mailbox Name (optional)]>" - help = "Receive incoming mail via stdin" - - def add_arguments(self, parser): - parser.add_argument( - 'mailbox_name', - nargs='?', - help="The name of the mailbox that will receive the message" - ) - - def handle(self, mailbox_name=None, *args, **options): - message = email.message_from_string(sys.stdin.read()) - if message: - if mailbox_name: - mailbox = self.get_mailbox_by_name(mailbox_name) - else: - mailbox = self.get_mailbox_for_message(message) - mailbox.process_incoming_message(message) - logger.info( - "Message received from %s", - message['from'] - ) - else: - logger.warning("Message not processable.") - - def get_mailbox_by_name(self, name): - mailbox, created = Mailbox.objects.get_or_create( - name=name, - ) - return mailbox - - def get_mailbox_for_message(self, message): - email_address = utils.parseaddr(message['to'])[1][0:255] - return self.get_mailbox_by_name(email_address) diff --git a/build/lib/django_mailbox/management/commands/rebuildmessageattachments.py b/build/lib/django_mailbox/management/commands/rebuildmessageattachments.py deleted file mode 100644 index 6bfc24f6..00000000 --- a/build/lib/django_mailbox/management/commands/rebuildmessageattachments.py +++ /dev/null @@ -1,71 +0,0 @@ -import email -import hashlib -import logging - -from django.core.management.base import BaseCommand - -from django_mailbox.models import MessageAttachment, Message - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -class Command(BaseCommand): - """ Briefly, a bug existed in a migration that may have caused message - attachments to become disassociated with their messages. This management - command will read through existing message attachments and attempt to - re-associate them with their original message. - - This isn't foolproof, I'm afraid. If an attachment exists twice, it will - be associated only with the most recent e-mail message. That said, - I'm quite sure that the bug in the migration is gone (and you'd have to - have been quite unlucky to have ran the bad migration). - - """ - def handle(self, *args, **options): - attachment_hash_map = {} - - attachments_without_messages = MessageAttachment.objects.filter( - message=None - ).order_by( - 'id' - ) - - if attachments_without_messages.count() < 1: - return - - for attachment in attachments_without_messages: - md5 = hashlib.md5() - for chunk in attachment.document.file.chunks(): - md5.update(chunk) - attachment_hash_map[md5.hexdigest()] = attachment.pk - - for message_record in Message.objects.all().order_by('id'): - message = email.message_from_string(message_record.body) - if message.is_multipart(): - for part in message.walk(): - if part.get_content_maintype() == 'multipart': - continue - if part.get('Content-Disposition') is None: - continue - md5 = hashlib.md5() - md5.update(part.get_payload(decode=True)) - digest = md5.hexdigest() - if digest in attachment_hash_map: - attachment = MessageAttachment.objects.get( - pk=attachment_hash_map[digest] - ) - attachment.message = message_record - attachment.save() - logger.info( - "Associated message %s with attachment %s (%s)", - message_record.pk, - attachment.pk, - digest - ) - else: - logger.info( - "%s(%s) not found in currently-stored attachments", - part.get_filename(), - digest - ) diff --git a/build/lib/django_mailbox/migrations/0001_initial.py b/build/lib/django_mailbox/migrations/0001_initial.py deleted file mode 100644 index 13bb802d..00000000 --- a/build/lib/django_mailbox/migrations/0001_initial.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Mailbox', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('uri', models.CharField(default=None, max_length=255, blank=True, help_text="Example: imap+ssl://myusername:mypassword@someserver

Internet transports include 'imap' and 'pop3'; common local file transports include 'maildir', 'mbox', and less commonly 'babyl', 'mh', and 'mmdf'.

Be sure to urlencode your username and password should they contain illegal characters (like @, :, etc).", null=True, verbose_name='URI')), - ('from_email', models.CharField(default=None, max_length=255, blank=True, help_text="Example: MailBot <mailbot@yourdomain.com>
'From' header to set for outgoing email.

If you do not use this e-mail inbox for outgoing mail, this setting is unnecessary.
If you send e-mail without setting this, your 'From' header will'be set to match the setting `DEFAULT_FROM_EMAIL`.", null=True, verbose_name='From email')), - ('active', models.BooleanField(default=True, help_text='Check this e-mail inbox for new e-mail messages during polling cycles. This checkbox does not have an effect upon whether mail is collected here when this mailbox receives mail from a pipe, and does not affect whether e-mail messages can be dispatched from this mailbox. ', verbose_name='Active')), - ], - options={ - 'verbose_name_plural': 'Mailboxes', - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Message', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('subject', models.CharField(max_length=255, verbose_name='Subject')), - ('message_id', models.CharField(max_length=255, verbose_name='Message ID')), - ('from_header', models.CharField(max_length=255, verbose_name='From header')), - ('to_header', models.TextField(verbose_name='To header')), - ('outgoing', models.BooleanField(default=False, verbose_name='Outgoing')), - ('body', models.TextField(verbose_name='Body')), - ('encoded', models.BooleanField(default=False, help_text='True if the e-mail body is Base64 encoded', verbose_name='Encoded')), - ('processed', models.DateTimeField(auto_now_add=True, verbose_name='Processed')), - ('read', models.DateTimeField(default=None, null=True, verbose_name='Read', blank=True)), - ('in_reply_to', models.ForeignKey(related_name='replies', verbose_name='In reply to', blank=True, to='django_mailbox.Message', null=True, on_delete=models.CASCADE)), - ('mailbox', models.ForeignKey(related_name='messages', verbose_name='Mailbox', to='django_mailbox.Mailbox', on_delete=models.CASCADE)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='MessageAttachment', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('headers', models.TextField(null=True, verbose_name='Headers', blank=True)), - ('document', models.FileField(upload_to=b'mailbox_attachments/%Y/%m/%d/', verbose_name='Document')), - ('message', models.ForeignKey(related_name='attachments', verbose_name='Message', blank=True, to='django_mailbox.Message', null=True, on_delete=models.CASCADE)), - ], - options={ - }, - bases=(models.Model,), - ), - ] diff --git a/build/lib/django_mailbox/migrations/0002_add_eml_to_message.py b/build/lib/django_mailbox/migrations/0002_add_eml_to_message.py deleted file mode 100644 index 4204efb2..00000000 --- a/build/lib/django_mailbox/migrations/0002_add_eml_to_message.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='message', - name='eml', - field=models.FileField(help_text='Original full content of message', upload_to=b'messages', null=True, verbose_name='Message as a file'), - preserve_default=True, - ), - ] diff --git a/build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py b/build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py deleted file mode 100644 index 1b5def07..00000000 --- a/build/lib/django_mailbox/migrations/0003_auto_20150409_0316.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0002_add_eml_to_message'), - ] - - operations = [ - migrations.AlterField( - model_name='message', - name='eml', - field=models.FileField(help_text='Original full content of message', upload_to=b'messages', null=True, verbose_name='Raw message contents'), - ), - ] diff --git a/build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py b/build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py deleted file mode 100644 index 7716f2f5..00000000 --- a/build/lib/django_mailbox/migrations/0004_bytestring_to_unicode.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0003_auto_20150409_0316'), - ] - - operations = [ - migrations.AlterField( - model_name='message', - name='eml', - field=models.FileField(verbose_name='Raw message contents', upload_to='messages', null=True, help_text='Original full content of message'), - ), - migrations.AlterField( - model_name='messageattachment', - name='document', - field=models.FileField(verbose_name='Document', upload_to='mailbox_attachments/%Y/%m/%d/'), - ), - ] diff --git a/build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py b/build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py deleted file mode 100644 index f4bdae65..00000000 --- a/build/lib/django_mailbox/migrations/0005_auto_20160523_2240.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db import migrations, models -import django_mailbox.utils - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0004_bytestring_to_unicode'), - ] - - operations = [ - migrations.AlterField( - model_name='messageattachment', - name='document', - field=models.FileField(upload_to=django_mailbox.utils.get_attachment_save_path, verbose_name='Document'), - ), - ] diff --git a/build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py b/build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py deleted file mode 100644 index 5f1524ec..00000000 --- a/build/lib/django_mailbox/migrations/0006_mailbox_last_polling.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 1.9.8 on 2016-08-15 22:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0005_auto_20160523_2240'), - ] - - operations = [ - migrations.AddField( - model_name='mailbox', - name='last_polling', - field=models.DateTimeField(blank=True, help_text='The time of last successful polling for messages.It is blank for new mailboxes and is not set for mailboxes that only receive messages via a pipe.', null=True, verbose_name='Last polling'), - ), - ] diff --git a/build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py b/build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py deleted file mode 100644 index 2e984d0d..00000000 --- a/build/lib/django_mailbox/migrations/0007_auto_20180421_0026.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 1.10.7 on 2018-04-21 00:26 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0006_mailbox_last_polling'), - ] - - operations = [ - migrations.AlterModelOptions( - name='mailbox', - options={'verbose_name': 'Mailbox', 'verbose_name_plural': 'Mailboxes'}, - ), - migrations.AlterModelOptions( - name='message', - options={'verbose_name': 'E-mail message', 'verbose_name_plural': 'E-mail messages'}, - ), - migrations.AlterModelOptions( - name='messageattachment', - options={'verbose_name': 'Message attachment', 'verbose_name_plural': 'Message attachments'}, - ), - ] diff --git a/build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py b/build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py deleted file mode 100644 index e307c258..00000000 --- a/build/lib/django_mailbox/migrations/0008_auto_20190219_1553.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.7 on 2019-02-19 14:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_mailbox', '0007_auto_20180421_0026'), - ] - - operations = [ - migrations.AlterField( - model_name='mailbox', - name='active', - field=models.BooleanField(blank=True, default=True, help_text='Check this e-mail inbox for new e-mail messages during polling cycles. This checkbox does not have an effect upon whether mail is collected here when this mailbox receives mail from a pipe, and does not affect whether e-mail messages can be dispatched from this mailbox. ', verbose_name='Active'), - ), - migrations.AlterField( - model_name='message', - name='outgoing', - field=models.BooleanField(blank=True, default=False, verbose_name='Outgoing'), - ), - ] diff --git a/build/lib/django_mailbox/migrations/__init__.py b/build/lib/django_mailbox/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/build/lib/django_mailbox/models.py b/build/lib/django_mailbox/models.py deleted file mode 100644 index cbd3d181..00000000 --- a/build/lib/django_mailbox/models.py +++ /dev/null @@ -1,853 +0,0 @@ -#!/usr/bin/env python - -""" -Models declaration for application ``django_mailbox``. -""" -import gzip -from email.encoders import encode_base64 -from email.message import Message as EmailMessage -from email.utils import formatdate, parseaddr -from urllib.parse import parse_qs, unquote, urlparse -from quopri import encode as encode_quopri -from io import BytesIO -import base64 -import email -import logging -import mimetypes -import os.path -import sys -import uuid -from tempfile import NamedTemporaryFile - -import django -from django.conf import settings as django_settings -from django.core.files.base import ContentFile, File -from django.core.mail.message import make_msgid -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.utils.timezone import now - -from django_mailbox import utils -from django_mailbox.signals import message_received -from django_mailbox.transports import Pop3Transport, ImapTransport, \ - MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ - MMDFTransport, GmailImapTransport, Office365Transport - -logger = logging.getLogger(__name__) - - -class MailboxQuerySet(models.QuerySet): - def get_new_mail(self): - count = 0 - for mailbox in self.all(): - logger.debug('Receiving mail for %s' % mailbox) - count += sum(1 for i in mailbox.get_new_mail()) - logger.debug('Received %d %s.', count, 'mails' if count != 1 else 'mail') - - -class MailboxManager(models.Manager): - def get_queryset(self): - return MailboxQuerySet(self.model, using=self._db) - - -class ActiveMailboxManager(MailboxManager): - def get_queryset(self): - return super().get_queryset().filter( - active=True, - ) - - -class Mailbox(models.Model): - name = models.CharField( - _('Name'), - max_length=255, - ) - - uri = models.CharField( - _('URI'), - max_length=255, - help_text=(_( - "Example: imap+ssl://myusername:mypassword@someserver
" - "
" - "Internet transports include 'imap' and 'pop3'; " - "common local file transports include 'maildir', 'mbox', " - "and less commonly 'babyl', 'mh', and 'mmdf'.
" - "
" - "Be sure to urlencode your username and password should they " - "contain illegal characters (like @, :, etc)." - )), - blank=True, - null=True, - default=None, - ) - - from_email = models.CharField( - _('From email'), - max_length=255, - help_text=(_( - "Example: MailBot <mailbot@yourdomain.com>
" - "'From' header to set for outgoing email.
" - "
" - "If you do not use this e-mail inbox for outgoing mail, this " - "setting is unnecessary.
" - "If you send e-mail without setting this, your 'From' header will'" - "be set to match the setting `DEFAULT_FROM_EMAIL`." - )), - blank=True, - null=True, - default=None, - ) - - active = models.BooleanField( - _('Active'), - help_text=(_( - "Check this e-mail inbox for new e-mail messages during polling " - "cycles. This checkbox does not have an effect upon whether " - "mail is collected here when this mailbox receives mail from a " - "pipe, and does not affect whether e-mail messages can be " - "dispatched from this mailbox. " - )), - blank=True, - default=True, - ) - - last_polling = models.DateTimeField( - _("Last polling"), - help_text=(_("The time of last successful polling for messages." - "It is blank for new mailboxes and is not set for " - "mailboxes that only receive messages via a pipe.")), - blank=True, - null=True - ) - - objects = MailboxManager() - active_mailboxes = ActiveMailboxManager() - - @property - def _protocol_info(self): - return urlparse(self.uri) - - @property - def _query_string(self): - return parse_qs(self._protocol_info.query) - - @property - def _domain(self): - return self._protocol_info.hostname - - @property - def port(self): - """Returns the port to use for fetching messages.""" - return self._protocol_info.port - - @property - def username(self): - """Returns the username to use for fetching messages.""" - return unquote(self._protocol_info.username) - - @property - def password(self): - """Returns the password to use for fetching messages.""" - return unquote(self._protocol_info.password) - - @property - def location(self): - """Returns the location (domain and path) of messages.""" - return self._domain if self._domain else '' + self._protocol_info.path - - @property - def type(self): - """Returns the 'transport' name for this mailbox.""" - scheme = self._protocol_info.scheme.lower() - if '+' in scheme: - return scheme.split('+')[0] - return scheme - - @property - def use_ssl(self): - """Returns whether or not this mailbox's connection uses SSL.""" - return '+ssl' in self._protocol_info.scheme.lower() - - @property - def use_tls(self): - """Returns whether or not this mailbox's connection uses STARTTLS.""" - return '+tls' in self._protocol_info.scheme.lower() - - @property - def archive(self): - """Returns (if specified) the folder to archive messages to.""" - archive_folder = self._query_string.get('archive', None) - if not archive_folder: - return None - return archive_folder[0] - - @property - def folder(self): - """Returns (if specified) the folder to fetch mail from.""" - folder = self._query_string.get('folder', None) - if not folder: - return None - return folder[0] - - @property - def client_id(self): - """Returns (if specified) the client id for Office365.""" - client_id = self._query_string.get('client_id', None) - if not client_id: - return None - return client_id[0] - - @property - def client_secret(self): - """Returns (if specified) the client secret for Office365.""" - client_secret = self._query_string.get('client_secret', None) - if not client_secret: - return None - return client_secret[0] - - @property - def tenant_id(self): - """Returns (if specified) the tenant id for Office365.""" - tenant_id = self._query_string.get('tenant_id', None) - if not tenant_id: - return None - return tenant_id[0] - - def get_connection(self): - """Returns the transport instance for this mailbox. - - These will always be instances of - `django_mailbox.transports.base.EmailTransport`. - - """ - if not self.uri: - return None - elif self.type == 'imap': - conn = ImapTransport( - self.location, - port=self.port if self.port else None, - ssl=self.use_ssl, - tls=self.use_tls, - archive=self.archive, - folder=self.folder - ) - conn.connect(self.username, self.password) - elif self.type == 'gmail': - conn = GmailImapTransport( - self.location, - port=self.port if self.port else None, - ssl=True, - archive=self.archive - ) - conn.connect(self.username, self.password) - elif self.type == 'pop3': - conn = Pop3Transport( - self.location, - port=self.port if self.port else None, - ssl=self.use_ssl - ) - conn.connect(self.username, self.password) - elif self.type == 'office365': - conn = Office365Transport( - self.location, - self.username, - archive=self.archive, - folder=self.folder - ) - conn.connect(self.client_id, self.client_secret, self.tenant_id) - elif self.type == 'maildir': - conn = MaildirTransport(self.location) - elif self.type == 'mbox': - conn = MboxTransport(self.location) - elif self.type == 'babyl': - conn = BabylTransport(self.location) - elif self.type == 'mh': - conn = MHTransport(self.location) - elif self.type == 'mmdf': - conn = MMDFTransport(self.location) - return conn - - def process_incoming_message(self, message): - """Process a message incoming to this mailbox.""" - msg = self._process_message(message) - if msg is None: - return None - msg.outgoing = False - msg.save() - - message_received.send(sender=self, message=msg) - - return msg - - def record_outgoing_message(self, message): - """Record an outgoing message associated with this mailbox.""" - msg = self._process_message(message) - if msg is None: - return None - msg.outgoing = True - msg.save() - 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(): - new[header] = value - for part in msg.get_payload(): - new.attach( - self._get_dehydrated_message(part, record) - ) - elif ( - 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[settings['altered_message_header']] = ( - 'Stripped; Content type %s not allowed' % ( - msg.get_content_type() - ) - ) - new.set_payload('') - elif ( - ( - msg.get_content_type() not in settings['text_stored_mimetypes'] - ) or - ('attachment' in msg.get('Content-Disposition', '')) - ): - filename = None - raw_filename = msg.get_filename() - if raw_filename: - filename = utils.convert_header_to_unicode(raw_filename) - if not filename: - extension = mimetypes.guess_extension(msg.get_content_type()) - else: - _, extension = os.path.splitext(filename) - if not extension: - extension = '.bin' - - attachment = MessageAttachment() - - attachment.document.save( - uuid.uuid4().hex + extension, - ContentFile( - BytesIO( - msg.get_payload(decode=True) - ).getvalue() - ) - ) - attachment.message = record - for key, value in msg.items(): - attachment[key] = value - attachment.save() - - placeholder = EmailMessage() - placeholder[ - settings['attachment_interpolation_header'] - ] = str(attachment.pk) - new = placeholder - else: - content_charset = msg.get_content_charset() - if not content_charset: - content_charset = 'ascii' - try: - # Make sure that the payload can be properly decoded in the - # defined charset, if it can't, let's mash some things - # inside the payload :-\ - msg.get_payload(decode=True).decode(content_charset) - except LookupError: - logger.warning( - "Unknown encoding %s; interpreting as ASCII!", - content_charset - ) - msg.set_payload( - msg.get_payload(decode=True).decode( - 'ascii', - 'ignore' - ) - ) - except ValueError: - logger.warning( - "Decoding error encountered; interpreting %s as ASCII!", - content_charset - ) - msg.set_payload( - msg.get_payload(decode=True).decode( - 'ascii', - 'ignore' - ) - ) - new = msg - return new - - def _process_message(self, message): - msg = Message() - msg._email_object = message - settings = utils.get_settings() - - if settings['store_original_message']: - self._process_save_original_message(message, msg) - msg.mailbox = self - if 'subject' in message: - 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 = utils.convert_header_to_unicode(message['from']) - if 'to' in message: - msg.to_header = utils.convert_header_to_unicode(message['to']) - elif 'Delivered-To' in message: - msg.to_header = utils.convert_header_to_unicode( - message['Delivered-To'] - ) - msg.save() - message = self._get_dehydrated_message(message, msg) - try: - body = message.as_string() - except KeyError as exc: - # email.message.replace_header may raise 'KeyError' if the header - # 'content-transfer-encoding' is missing - logger.warning("Failed to parse message: %s", exc,) - return None - msg.set_body(body) - if message['in-reply-to']: - try: - msg.in_reply_to = Message.objects.filter( - message_id=message['in-reply-to'].strip() - )[0] - except IndexError: - pass - msg.save() - return msg - - def _process_save_original_message(self, message, msg): - settings = utils.get_settings() - if settings['compress_original_message']: - with NamedTemporaryFile(suffix=".eml.gz") as fp_tmp: - with gzip.GzipFile(fileobj=fp_tmp, mode="w") as fp: - fp.write(message.as_string().encode('utf-8')) - msg.eml.save( - "{}.eml.gz".format(uuid.uuid4()), - File(fp_tmp), - save=False - ) - - else: - msg.eml.save( - '%s.eml' % uuid.uuid4(), - ContentFile(message.as_string()), - save=False - ) - - def get_new_mail(self, condition=None): - """Connect to this transport and fetch new messages.""" - new_mail = [] - connection = self.get_connection() - if not connection: - return - for message in connection.get_message(condition): - msg = self.process_incoming_message(message) - if not msg is None: - yield msg - self.last_polling = now() - if django.VERSION >= (1, 5): # Django 1.5 introduces update_fields - self.save(update_fields=['last_polling']) - else: - self.save() - - def __str__(self): - return self.name - - class Meta: - verbose_name = _('Mailbox') - verbose_name_plural = _('Mailboxes') - - -class IncomingMessageManager(models.Manager): - def get_queryset(self): - return super().get_queryset().filter( - outgoing=False, - ) - - -class OutgoingMessageManager(models.Manager): - def get_queryset(self): - return super().get_queryset().filter( - outgoing=True, - ) - - -class UnreadMessageManager(models.Manager): - def get_queryset(self): - return super().get_queryset().filter( - read=None - ) - - -class Message(models.Model): - mailbox = models.ForeignKey( - Mailbox, - related_name='messages', - verbose_name=_('Mailbox'), - on_delete=models.CASCADE - ) - - subject = models.CharField( - _('Subject'), - max_length=255 - ) - - message_id = models.CharField( - _('Message ID'), - max_length=255 - ) - - in_reply_to = models.ForeignKey( - 'django_mailbox.Message', - related_name='replies', - blank=True, - null=True, - verbose_name=_('In reply to'), - on_delete=models.CASCADE - ) - - from_header = models.CharField( - _('From header'), - max_length=255, - ) - - to_header = models.TextField( - _('To header'), - ) - - outgoing = models.BooleanField( - _('Outgoing'), - default=False, - blank=True, - ) - - body = models.TextField( - _('Body'), - ) - - encoded = models.BooleanField( - _('Encoded'), - default=False, - help_text=_('True if the e-mail body is Base64 encoded'), - ) - - processed = models.DateTimeField( - _('Processed'), - auto_now_add=True - ) - - read = models.DateTimeField( - _('Read'), - default=None, - blank=True, - null=True, - ) - - eml = models.FileField( - _('Raw message contents'), - null=True, - upload_to="messages", - help_text=_('Original full content of message') - ) - objects = models.Manager() - unread_messages = UnreadMessageManager() - incoming_messages = IncomingMessageManager() - outgoing_messages = OutgoingMessageManager() - - @property - def address(self): - """Property allowing one to get the relevant address(es). - - In earlier versions of this library, the model had an `address` field - storing the e-mail address from which a message was received. During - later refactorings, it became clear that perhaps storing sent messages - would also be useful, so the address field was replaced with two - separate fields. - - """ - addresses = [] - addresses = self.to_addresses + self.from_address - return addresses - - @property - def from_address(self): - """Returns the address (as a list) from which this message was received - - .. note:: - - This was once (and probably should be) a string rather than a list, - but in a pull request received long, long ago it was changed; - presumably to make the interface identical to that of - `to_addresses`. - - """ - if self.from_header: - return [parseaddr(self.from_header)[1].lower()] - else: - return [] - - @property - def to_addresses(self): - """Returns a list of addresses to which this message was sent.""" - addresses = [] - for address in self.to_header.split(','): - if address: - addresses.append( - parseaddr( - address - )[1].lower() - ) - return addresses - - def reply(self, message): - """Sends a message as a reply to this message instance. - - Although Django's e-mail processing will set both Message-ID - and Date upon generating the e-mail message, we will not be able - to retrieve that information through normal channels, so we must - pre-set it. - - """ - if not message.from_email: - if self.mailbox.from_email: - message.from_email = self.mailbox.from_email - else: - 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() - message.send() - return self.mailbox.record_outgoing_message( - email.message_from_string( - message.message().as_string() - ) - ) - - @property - def text(self): - """ - Returns the message body matching content type 'text/plain'. - """ - return utils.get_body_from_message( - self.get_email_object(), 'text', 'plain' - ).replace('=\n', '').strip() - - @property - def html(self): - """ - Returns the message body matching content type 'text/html'. - """ - 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 - for part in msg.get_payload(): - new.attach( - self._rehydrate(part) - ) - elif settings['attachment_interpolation_header'] in msg.keys(): - try: - attachment = MessageAttachment.objects.get( - pk=msg[settings['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 = BytesIO() - encode_quopri( - 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[settings['altered_message_header']] = ( - 'Missing; Attachment %s not found' % ( - msg[settings['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_body(self): - """Returns the `body` field of this record. - - This will automatically base64-decode the message contents - if they are encoded as such. - - """ - if self.encoded: - return base64.b64decode(self.body.encode('ascii')) - return self.body.encode('utf-8') - - def set_body(self, body): - """Set the `body` field of this record. - - This will automatically base64-encode the message contents to - circumvent a limitation in earlier versions of Django in which - no fields existed for storing arbitrary bytes. - - """ - self.encoded = True - self.body = base64.b64encode(body.encode('utf-8')).decode('ascii') - - def get_email_object(self): - """Returns an `email.message.EmailMessage` instance representing the - contents of this message and all attachments. - - See [email.message.EmailMessage]_ for more information as to what methods - and properties are available on `email.message.EmailMessage` instances. - - .. note:: - - Depending upon the storage methods in use (specifically -- - whether ``DJANGO_MAILBOX_STORE_ORIGINAL_MESSAGE`` is set - to ``True``, this may either create a "rehydrated" message - using stored attachments, or read the message contents stored - on-disk. - - .. [email.message.EmailMessage] Python's `email.message.EmailMessage` docs - (https://docs.python.org/3/library/email.message.html) - - """ - if not hasattr(self, '_email_object'): # Cache fill - if self.eml: - if self.eml.name.endswith('.gz'): - body = gzip.GzipFile(fileobj=self.eml).read() - else: - self.eml.open() - body = self.eml.file.read() - self.eml.close() - else: - body = self.get_body() - flat = email.message_from_bytes(body) - self._email_object = self._rehydrate(flat) - return self._email_object - - def delete(self, *args, **kwargs): - """Delete this message and all stored attachments.""" - for attachment in self.attachments.all(): - # This attachment is attached only to this message. - attachment.delete() - return super().delete(*args, **kwargs) - - def __str__(self): - return self.subject - - class Meta: - verbose_name = _('E-mail message') - verbose_name_plural = _('E-mail messages') - - -class MessageAttachment(models.Model): - message = models.ForeignKey( - Message, - related_name='attachments', - null=True, - blank=True, - verbose_name=_('Message'), - on_delete=models.CASCADE - ) - - headers = models.TextField( - _('Headers'), - null=True, - blank=True, - ) - - document = models.FileField( - _('Document'), - upload_to=utils.get_attachment_save_path, - ) - - def delete(self, *args, **kwargs): - """Deletes the attachment.""" - self.document.delete() - return super().delete(*args, **kwargs) - - def _get_rehydrated_headers(self): - headers = self.headers - if headers is None: - return EmailMessage() - 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): - """Returns the original filename of this attachment.""" - file_name = self._get_rehydrated_headers().get_filename() - if isinstance(file_name, str): - result = utils.convert_header_to_unicode(file_name) - if result is None: - return file_name - return result - else: - return None - - def items(self): - return self._get_rehydrated_headers().items() - - def __getitem__(self, name): - value = self._get_rehydrated_headers()[name] - if value is None: - raise KeyError('Header %s does not exist' % name) - return value - - def __str__(self): - return self.document.url - - class Meta: - verbose_name = _('Message attachment') - verbose_name_plural = _('Message attachments') diff --git a/build/lib/django_mailbox/signals.py b/build/lib/django_mailbox/signals.py deleted file mode 100644 index b445183e..00000000 --- a/build/lib/django_mailbox/signals.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.dispatch.dispatcher import Signal - -message_received = Signal(providing_args=['message']) diff --git a/build/lib/django_mailbox/south_migrations/0001_initial.py b/build/lib/django_mailbox/south_migrations/0001_initial.py deleted file mode 100644 index dcfdf602..00000000 --- a/build/lib/django_mailbox/south_migrations/0001_initial.py +++ /dev/null @@ -1,58 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Mailbox' - db.create_table('django_mailbox_mailbox', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('uri', self.gf('django.db.models.fields.CharField')(max_length=255)), - )) - db.send_create_signal('django_mailbox', ['Mailbox']) - - # Adding model 'Message' - db.create_table('django_mailbox_message', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('mailbox', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['django_mailbox.Mailbox'])), - ('subject', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('message_id', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('from_address', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('body', self.gf('django.db.models.fields.TextField')()), - ('received', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - )) - db.send_create_signal('django_mailbox', ['Message']) - - - def backwards(self, orm): - # Deleting model 'Mailbox' - db.delete_table('django_mailbox_mailbox') - - # Deleting model 'Message' - db.delete_table('django_mailbox_message') - - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['django_mailbox.Mailbox']"}), - 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py b/build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py deleted file mode 100644 index 47517b31..00000000 --- a/build/lib/django_mailbox/south_migrations/0002_auto__chg_field_mailbox_uri.py +++ /dev/null @@ -1,38 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - - # Changing field 'Mailbox.uri' - db.alter_column('django_mailbox_mailbox', 'uri', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)) - - def backwards(self, orm): - - # User chose to not deal with backwards NULL issues for 'Mailbox.uri' - raise RuntimeError("Cannot reverse this migration. 'Mailbox.uri' and its values cannot be restored.") - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}), - 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py b/build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py deleted file mode 100644 index 30e43542..00000000 --- a/build/lib/django_mailbox/south_migrations/0003_auto__add_field_mailbox_active.py +++ /dev/null @@ -1,41 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Mailbox.active' - db.add_column('django_mailbox_mailbox', 'active', - self.gf('django.db.models.fields.BooleanField')(default=True), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Mailbox.active' - db.delete_column('django_mailbox_mailbox', 'active') - - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}), - 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py b/build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py deleted file mode 100644 index 245c43bb..00000000 --- a/build/lib/django_mailbox/south_migrations/0004_auto__add_field_message_outgoing.py +++ /dev/null @@ -1,42 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Message.outgoing' - db.add_column('django_mailbox_message', 'outgoing', - self.gf('django.db.models.fields.BooleanField')(default=False), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Message.outgoing' - db.delete_column('django_mailbox_message', 'outgoing') - - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}), - 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - } - } - - complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0005_rename_fields.py b/build/lib/django_mailbox/south_migrations/0005_rename_fields.py deleted file mode 100644 index 72cb4372..00000000 --- a/build/lib/django_mailbox/south_migrations/0005_rename_fields.py +++ /dev/null @@ -1,38 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - db.rename_column('django_mailbox_message', 'from_address', 'address') - db.rename_column('django_mailbox_message', 'received', 'processed') - - def backwards(self, orm): - db.rename_column('django_mailbox_message', 'address', 'from_address') - db.rename_column('django_mailbox_message', 'processed', 'received') - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'body': ('django.db.models.fields.TextField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - } - } - - complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py b/build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py deleted file mode 100644 index 4287fb26..00000000 --- a/build/lib/django_mailbox/south_migrations/0006_auto__add_field_message_in_reply_to.py +++ /dev/null @@ -1,55 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Message.in_reply_to' - db.add_column('django_mailbox_message', 'in_reply_to', - self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='replies', null=True, to=orm['django_mailbox.Message']), - keep_default=False) - - # Adding M2M table for field references on 'Message' - db.create_table('django_mailbox_message_references', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('from_message', models.ForeignKey(orm['django_mailbox.message'], null=False)), - ('to_message', models.ForeignKey(orm['django_mailbox.message'], null=False)) - )) - db.create_unique('django_mailbox_message_references', ['from_message_id', 'to_message_id']) - - - def backwards(self, orm): - # Deleting field 'Message.in_reply_to' - db.delete_column('django_mailbox_message', 'in_reply_to_id') - - # Removing M2M table for field references on 'Message' - db.delete_table('django_mailbox_message_references') - - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'body': ('django.db.models.fields.TextField', [], {}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), - 'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py b/build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py deleted file mode 100644 index cfa7d6f0..00000000 --- a/build/lib/django_mailbox/south_migrations/0007_auto__del_field_message_address__add_field_message_from_header__add_fi.py +++ /dev/null @@ -1,59 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Deleting field 'Message.address' - db.delete_column('django_mailbox_message', 'address') - - # Adding field 'Message.from_header' - db.add_column('django_mailbox_message', 'from_header', - self.gf('django.db.models.fields.CharField')(default='', max_length=255), - keep_default=False) - - # Adding field 'Message.to_header' - db.add_column('django_mailbox_message', 'to_header', - self.gf('django.db.models.fields.TextField')(default=''), - keep_default=False) - - - def backwards(self, orm): - - # User chose to not deal with backwards NULL issues for 'Message.address' - raise RuntimeError("Cannot reverse this migration. 'Message.address' and its values cannot be restored.") - # Deleting field 'Message.from_header' - db.delete_column('django_mailbox_message', 'from_header') - - # Deleting field 'Message.to_header' - db.delete_column('django_mailbox_message', 'to_header') - - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), - 'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'to_header': ('django.db.models.fields.TextField', [], {}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py b/build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py deleted file mode 100644 index e047f223..00000000 --- a/build/lib/django_mailbox/south_migrations/0008_populate_from_to_fields.py +++ /dev/null @@ -1,46 +0,0 @@ -import datetime -import email -from south.db import db -from south.v2 import DataMigration -from django.db import models - -class Migration(DataMigration): - - def forwards(self, orm): - for message in orm['django_mailbox.message'].objects.all(): - msg_object = email.message_from_string( - message.body - ) - message.from_header = msg_object['from'] - message.to_header = msg_object['to'] - message.save() - - def backwards(self, orm): - raise RuntimeError('Cannot reverse this migration.') - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), - 'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'to_header': ('django.db.models.fields.TextField', [], {}) - } - } - - complete_apps = ['django_mailbox'] - symmetrical = True diff --git a/build/lib/django_mailbox/south_migrations/0009_remove_references_table.py b/build/lib/django_mailbox/south_migrations/0009_remove_references_table.py deleted file mode 100644 index 38685851..00000000 --- a/build/lib/django_mailbox/south_migrations/0009_remove_references_table.py +++ /dev/null @@ -1,47 +0,0 @@ -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 references on 'Message' - db.delete_table('django_mailbox_message_references') - - - def backwards(self, orm): - # Adding M2M table for field references on 'Message' - db.create_table('django_mailbox_message_references', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('from_message', models.ForeignKey(orm['django_mailbox.message'], null=False)), - ('to_message', models.ForeignKey(orm['django_mailbox.message'], null=False)) - )) - db.create_unique('django_mailbox_message_references', ['from_message_id', 'to_message_id']) - - - models = { - 'django_mailbox.mailbox': { - 'Meta': {'object_name': 'Mailbox'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'to_header': ('django.db.models.fields.TextField', [], {}) - } - } - - complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py b/build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py deleted file mode 100644 index a988e5eb..00000000 --- a/build/lib/django_mailbox/south_migrations/0010_auto__add_field_mailbox_from_email.py +++ /dev/null @@ -1,45 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Mailbox.from_email' - db.add_column('django_mailbox_mailbox', 'from_email', - self.gf('django.db.models.fields.CharField')(default=None, max_length=255, null=True, blank=True), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Mailbox.from_email' - db.delete_column('django_mailbox_mailbox', 'from_email') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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'}), - 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'to_header': ('django.db.models.fields.TextField', [], {}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py b/build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py deleted file mode 100644 index c75db341..00000000 --- a/build/lib/django_mailbox/south_migrations/0011_auto__add_field_message_read.py +++ /dev/null @@ -1,46 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Message.read' - db.add_column('django_mailbox_message', 'read', - self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Message.read' - db.delete_column('django_mailbox_message', 'read') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py b/build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py deleted file mode 100644 index b62e50e9..00000000 --- a/build/lib/django_mailbox/south_migrations/0012_auto__add_messageattachment.py +++ /dev/null @@ -1,66 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -class Migration(SchemaMigration): - - def forwards(self, orm): - - # Adding model 'MessageAttachment' - db.create_table('django_mailbox_messageattachment', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('document', self.gf('django.db.models.fields.files.FileField')(max_length=100)), - )) - db.send_create_signal('django_mailbox', ['MessageAttachment']) - - # Adding M2M table for field attachments on 'Message' - db.create_table('django_mailbox_message_attachments', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('message', models.ForeignKey(orm['django_mailbox.message'], null=False)), - ('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False)) - )) - db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id']) - - - def backwards(self, orm): - - # Deleting model 'MessageAttachment' - db.delete_table('django_mailbox_messageattachment') - - # Removing M2M table for field attachments on 'Message' - db.delete_table('django_mailbox_message_attachments') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['django_mailbox.MessageAttachment']", 'symmetrical': 'False'}), - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - }, - 'django_mailbox.messageattachment': { - 'Meta': {'object_name': 'MessageAttachment'}, - 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) - } - } - - complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py b/build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py deleted file mode 100644 index 01c80a2e..00000000 --- a/build/lib/django_mailbox/south_migrations/0013_auto__add_field_messageattachment_message.py +++ /dev/null @@ -1,53 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'MessageAttachment.message' - db.add_column('django_mailbox_messageattachment', 'message', - self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='attachments', null=True, to=orm['django_mailbox.Message']), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'MessageAttachment.message' - db.delete_column('django_mailbox_messageattachment', 'message_id') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': "orm['django_mailbox.MessageAttachment']"}), - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - }, - 'django_mailbox.messageattachment': { - 'Meta': {'object_name': 'MessageAttachment'}, - 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) - } - } - - complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py b/build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py deleted file mode 100644 index b018915e..00000000 --- a/build/lib/django_mailbox/south_migrations/0014_migrate_message_attachments.py +++ /dev/null @@ -1,54 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - no_dry_run = True - def forwards(self, orm): - for message in orm['django_mailbox.Message'].objects.all(): - for attachment in message.attachments.all(): - attachment.message = message - attachment.save() - - def backwards(self, orm): - for attachment in orm['django_mailbox.MessageAttachment'].objects.all(): - if attachment.message: - attachment.message.attachments.add( - attachment - ) - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': "orm['django_mailbox.MessageAttachment']"}), - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - }, - 'django_mailbox.messageattachment': { - 'Meta': {'object_name': 'MessageAttachment'}, - 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) - } - } - - complete_apps = ['django_mailbox'] diff --git a/build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py b/build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py deleted file mode 100644 index 71ecfbdc..00000000 --- a/build/lib/django_mailbox/south_migrations/0015_auto__add_field_messageattachment_headers.py +++ /dev/null @@ -1,64 +0,0 @@ -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('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('django_mailbox_message_attachments', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('message', models.ForeignKey(orm['django_mailbox.message'], null=False)), - ('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False)) - )) - db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id']) - - # Deleting field 'MessageAttachment.headers' - db.delete_column('django_mailbox_messageattachment', 'headers') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - }, - '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'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py b/build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py deleted file mode 100644 index cd4c6787..00000000 --- a/build/lib/django_mailbox/south_migrations/0016_auto__add_field_message_encoded.py +++ /dev/null @@ -1,54 +0,0 @@ -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Message.encoded' - db.add_column('django_mailbox_message', 'encoded', - self.gf('django.db.models.fields.BooleanField')(default=False), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Message.encoded' - db.delete_column('django_mailbox_message', 'encoded') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - }, - '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'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py b/build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py deleted file mode 100644 index 186c8bd4..00000000 --- a/build/lib/django_mailbox/south_migrations/0017_auto__add_field_message_eml.py +++ /dev/null @@ -1,55 +0,0 @@ -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Message.eml' - db.add_column('django_mailbox_message', 'eml', - self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Message.eml' - db.delete_column('django_mailbox_message', 'eml') - - - models = { - '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'}), - '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'}) - }, - 'django_mailbox.message': { - 'Meta': {'object_name': 'Message'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'eml': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}), - 'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - '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': "orm['django_mailbox.Message']"}), - 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "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', [], {}) - }, - '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'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}) - } - } - - complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/build/lib/django_mailbox/south_migrations/__init__.py b/build/lib/django_mailbox/south_migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/build/lib/django_mailbox/tests/__init__.py b/build/lib/django_mailbox/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/build/lib/django_mailbox/tests/base.py b/build/lib/django_mailbox/tests/base.py deleted file mode 100644 index b1215c5e..00000000 --- a/build/lib/django_mailbox/tests/base.py +++ /dev/null @@ -1,191 +0,0 @@ -import email -import os.path -import time - -from django.conf import settings -from django.test import TestCase - -from django_mailbox import models, utils -from django_mailbox.models import Mailbox, Message - - -class EmailIntegrationTimeout(Exception): - pass - - -def get_email_as_text(name): - with open( - os.path.join( - os.path.dirname(__file__), - 'messages', - name, - ), - 'rb' - ) as f: - return f.read() - - -class EmailMessageTestCase(TestCase): - ALLOWED_EXTRA_HEADERS = [ - 'MIME-Version', - 'Content-Transfer-Encoding', - ] - - def setUp(self): - 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') - - self.test_account = os.environ.get('EMAIL_ACCOUNT') - self.test_password = os.environ.get('EMAIL_PASSWORD') - self.test_smtp_server = os.environ.get('EMAIL_SMTP_SERVER') - self.test_from_email = 'nobody@nowhere.com' - - self.maximum_wait_seconds = 60 * 5 - - settings.EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - settings.EMAIL_HOST = self.test_smtp_server - settings.EMAIL_PORT = 587 - settings.EMAIL_HOST_USER = self.test_account - settings.EMAIL_HOST_PASSWORD = self.test_password - settings.EMAIL_USE_TLS = True - super().setUp() - - def _get_new_messages(self, mailbox, condition=None): - start_time = time.time() - # wait until there is at least one message - while time.time() - start_time < self.maximum_wait_seconds: - - messages = self.mailbox.get_new_mail(condition) - - try: - # check if generator contains at least one element - message = next(messages) - yield message - yield from messages - return - - except StopIteration: - time.sleep(5) - - raise EmailIntegrationTimeout() - - def _get_email_as_text(self, name): - with open( - os.path.join( - os.path.dirname(__file__), - 'messages', - name, - ), - 'rb' - ) as f: - return f.read() - - def _get_email_object(self, name): - copy = self._get_email_as_text(name) - return email.message_from_bytes(copy) - - def _headers_identical(self, left, right, header=None): - """ Check if headers are (close enough to) identical. - - * This is particularly tricky because Python 2.6, Python 2.7 and - Python 3 each handle header strings slightly differently. This - should mash away all of the differences, though. - * This also has a small loophole in that when re-writing e-mail - payload encodings, we re-build the Content-Type header, so if the - header was originally unquoted, it will be quoted when rehydrating - the e-mail message. - - """ - if header.lower() == 'content-type': - # Special case; given that we re-write the header, we'll be quoting - # the new content type; we need to make sure that doesn't cause - # this comparison to fail. Also, the case of the encoding could - # be changed, etc. etc. etc. - left = left.replace('"', '').upper() - right = right.replace('"', '').upper() - left = left.replace('\n\t', ' ').replace('\n ', ' ') - right = right.replace('\n\t', ' ').replace('\n ', ' ') - if right != left: - return False - return True - - def compare_email_objects(self, left, right): - # Compare headers - for key, value in left.items(): - if not right[key] and key in self.ALLOWED_EXTRA_HEADERS: - continue - if not right[key]: - raise AssertionError("Extra header '%s'" % key) - if not self._headers_identical(right[key], value, header=key): - raise AssertionError( - "Header '{}' unequal:\n{}\n{}".format( - key, - repr(value), - repr(right[key]), - ) - ) - for key, value in right.items(): - if not left[key] and key in self.ALLOWED_EXTRA_HEADERS: - continue - if not left[key]: - raise AssertionError("Extra header '%s'" % key) - if not self._headers_identical(left[key], value, header=key): - raise AssertionError( - "Header '{}' unequal:\n{}\n{}".format( - 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{}\n{}".format( - left.as_string(), - right.as_string() - ) - ) - - def assertEqual(self, left, right): # noqa: N802 - if not isinstance(left, email.message.Message): - return super().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().tearDown() diff --git a/build/lib/django_mailbox/tests/settings.py b/build/lib/django_mailbox/tests/settings.py deleted file mode 100644 index 66c7168e..00000000 --- a/build/lib/django_mailbox/tests/settings.py +++ /dev/null @@ -1,12 +0,0 @@ -DATABASES = { - 'default': { - 'NAME': 'db.sqlite3', - 'ENGINE': 'django.db.backends.sqlite3', - }, -} -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django_mailbox', -] -SECRET_KEY = 'beepboop' diff --git a/build/lib/django_mailbox/tests/test_integration_imap.py b/build/lib/django_mailbox/tests/test_integration_imap.py deleted file mode 100644 index 6f875b74..00000000 --- a/build/lib/django_mailbox/tests/test_integration_imap.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import uuid -from urllib import parse - -from django.core.mail import EmailMultiAlternatives - -from django_mailbox.models import Mailbox -from django_mailbox.tests.base import EmailMessageTestCase - - -__all__ = ['TestImap'] - - -class TestImap(EmailMessageTestCase): - def setUp(self): - super().setUp() - - self.test_imap_server = ( - os.environ.get('EMAIL_IMAP_SERVER') - ) - - required_settings = [ - self.test_imap_server, - self.test_account, - self.test_password, - self.test_smtp_server, - self.test_from_email, - ] - if not all(required_settings): - self.skipTest( - "Integration tests are not available without having " - "the the following environment variables set: " - "EMAIL_ACCOUNT, EMAIL_PASSWORD, EMAIL_SMTP_SERVER, " - "EMAIL_IMAP_SERVER." - ) - - self.mailbox = Mailbox.objects.create( - name='Integration Test Imap', - uri=self.get_connection_string() - ) - self.arbitrary_identifier = str(uuid.uuid4()) - - def get_connection_string(self): - return "imap+ssl://{account}:{password}@{server}".format( - account=parse.quote(self.test_account), - password=parse.quote(self.test_password), - server=self.test_imap_server, - ) - - def test_get_imap_message(self): - text_content = 'This is some content' - msg = EmailMultiAlternatives( - self.arbitrary_identifier, - text_content, - self.test_from_email, - [ - self.test_account, - ] - ) - msg.send() - - messages = self._get_new_messages( - self.mailbox, - condition=lambda m: m['subject'] == self.arbitrary_identifier - ) - message = next(messages) - - self.assertEqual(message.subject, self.arbitrary_identifier) - self.assertEqual(message.text, text_content) - self.assertEqual(0, len(list(messages))) diff --git a/build/lib/django_mailbox/tests/test_mailbox.py b/build/lib/django_mailbox/tests/test_mailbox.py deleted file mode 100644 index e233f753..00000000 --- a/build/lib/django_mailbox/tests/test_mailbox.py +++ /dev/null @@ -1,46 +0,0 @@ -import os - -from django.test import TestCase - -from django_mailbox.models import Mailbox - - -__all__ = ['TestMailbox'] - - -class TestMailbox(TestCase): - def test_protocol_info(self): - mailbox = Mailbox() - mailbox.uri = 'alpha://test.com' - - expected_protocol = 'alpha' - actual_protocol = mailbox._protocol_info.scheme - - self.assertEqual( - expected_protocol, - actual_protocol, - ) - - def test_last_polling_field_exists(self): - mailbox = Mailbox() - self.assertTrue(hasattr(mailbox, 'last_polling')) - - def test_get_new_mail_update_last_polling(self): - mailbox = Mailbox.objects.create(uri="mbox://" + os.path.join( - os.path.dirname(__file__), - 'messages', - 'generic_message.eml', - )) - self.assertEqual(mailbox.last_polling, None) - list(mailbox.get_new_mail()) - self.assertNotEqual(mailbox.last_polling, None) - - def test_queryset_get_new_mail(self): - mailbox = Mailbox.objects.create(uri="mbox://" + os.path.join( - os.path.dirname(__file__), - 'messages', - 'generic_message.eml', - )) - Mailbox.objects.filter(pk=mailbox.pk).get_new_mail() - mailbox.refresh_from_db() - self.assertNotEqual(mailbox.last_polling, None) diff --git a/build/lib/django_mailbox/tests/test_message_flattening.py b/build/lib/django_mailbox/tests/test_message_flattening.py deleted file mode 100644 index 669433ed..00000000 --- a/build/lib/django_mailbox/tests/test_message_flattening.py +++ /dev/null @@ -1,116 +0,0 @@ -import copy - -from unittest import mock - -from django_mailbox import models, utils -from django_mailbox.models import Message -from django_mailbox.tests.base import EmailMessageTestCase - - -__all__ = ['TestMessageFlattening'] - - -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) - - del msg._email_object # Cache flush - 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', - ) - default_settings = utils.get_settings() - - 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) - - del msg._email_object # Cache flush - actual_email_object = msg.get_email_object() - - self.assertEqual( - actual_email_object, - expected_email_object, - ) - - def test_message_processing_unknown_encoding(self): - incoming_email_object = self._get_email_object( - 'message_with_invalid_encoding.eml', - ) - - msg = self.mailbox.process_incoming_message(incoming_email_object) - - expected_text = ( - "We offer loans to private individuals and corporate " - "organizations at 2% interest rate. Interested serious " - "applicants should apply via email with details of their " - "requirements.\n\nWarm Regards,\nLoan Team" - ) - actual_text = msg.text - - self.assertEqual(actual_text, expected_text) diff --git a/build/lib/django_mailbox/tests/test_process_email.py b/build/lib/django_mailbox/tests/test_process_email.py deleted file mode 100644 index a6dbd044..00000000 --- a/build/lib/django_mailbox/tests/test_process_email.py +++ /dev/null @@ -1,432 +0,0 @@ -import gzip -import os.path -import sys - -import copy -from unittest import mock - -from django_mailbox.models import Mailbox, Message -from django_mailbox.utils import convert_header_to_unicode -from django_mailbox import utils -from django_mailbox.tests.base import EmailMessageTestCase -from django.utils.encoding import force_text -from django.core.mail import EmailMessage - -__all__ = ['TestProcessEmail'] - - -class TestProcessEmail(EmailMessageTestCase): - def test_message_without_attachments(self): - message = self._get_email_object('generic_message.eml') - - mailbox = Mailbox.objects.create() - msg = mailbox.process_incoming_message(message) - - self.assertEqual( - msg.mailbox, - mailbox - ) - self.assertEqual(msg.subject, 'Message Without Attachment') - self.assertEqual( - msg.message_id, - ( - '' - ) - ) - self.assertEqual( - msg.from_header, - 'Adam Coddington ', - ) - self.assertEqual( - msg.to_header, - 'Adam Coddington ', - ) - - def test_message_with_encoded_attachment_filenames(self): - message = self._get_email_object( - 'message_with_koi8r_filename_attachments.eml' - ) - - mailbox = Mailbox.objects.create() - msg = mailbox.process_incoming_message(message) - - attachments = msg.attachments.order_by('pk').all() - self.assertEqual( - '\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b' - '\u043e\u0436\u0435\u043d\u0438\u0439 HSE Career Fair 8 \u0430' - '\u043f\u0440\u0435\u043b\u044f 2016.pdf', - attachments[0].get_filename() - ) - self.assertEqual( - '\u0412\u0435\u0434\u043e\u043c\u043e\u0441\u0442\u0438.pdf', - attachments[1].get_filename() - ) - self.assertEqual( - '\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b' - '\u043e\u0436\u0435\u043d\u0438\u0439 2016.pptx', - attachments[2].get_filename() - ) - - def test_message_with_attachments(self): - message = self._get_email_object('message_with_attachment.eml') - - mailbox = Mailbox.objects.create() - msg = mailbox.process_incoming_message(message) - - expected_count = 1 - actual_count = msg.attachments.count() - - self.assertEqual( - expected_count, - actual_count, - ) - - attachment = msg.attachments.all()[0] - self.assertEqual( - attachment.get_filename(), - 'heart.png', - ) - - def test_message_with_utf8_attachment_header(self): - """ Ensure that we properly handle UTF-8 encoded attachment - - Safe for regress of #104 too - """ - email_object = self._get_email_object( - 'message_with_utf8_attachment.eml', - ) - mailbox = Mailbox.objects.create() - msg = mailbox.process_incoming_message(email_object) - - expected_count = 2 - actual_count = msg.attachments.count() - - self.assertEqual( - expected_count, - actual_count, - ) - - attachment = msg.attachments.all()[0] - self.assertEqual( - attachment.get_filename(), - 'pi\u0142kochwyty.jpg' - ) - - attachment = msg.attachments.all()[1] - self.assertEqual( - attachment.get_filename(), - 'odpowied\u017a Burmistrza.jpg' - ) - - def test_message_get_text_body(self): - message = self._get_email_object('multipart_text.eml') - - mailbox = Mailbox.objects.create() - msg = mailbox.process_incoming_message(message) - - expected_results = 'Hello there!' - actual_results = msg.text.strip() - - self.assertEqual( - expected_results, - actual_results, - ) - - def test_get_text_body_properly_recomposes_line_continuations(self): - message = Message() - email_object = self._get_email_object( - 'message_with_long_text_lines.eml' - ) - - message.get_email_object = lambda: email_object - - actual_text = message.text - expected_text = ( - 'The one of us with a bike pump is far ahead, ' - 'but a man stopped to help us and gave us his pump.' - ) - - self.assertEqual( - actual_text, - expected_text - ) - - def test_get_body_properly_handles_unicode_body(self): - with open( - os.path.join( - os.path.dirname(__file__), - 'messages/generic_message.eml' - ) - ) as f: - unicode_body = f.read() - - message = Message() - message.body = unicode_body - - expected_body = unicode_body - actual_body = message.get_email_object().as_string() - - self.assertEqual( - expected_body, - actual_body - ) - - def test_message_issue_82(self): - """ Ensure that we properly handle incorrectly encoded messages - - """ - email_object = self._get_email_object('email_issue_82.eml') - it = 'works' - try: - # it's ok to call as_string() before passing email_object - # to _get_dehydrated_message() - email_object.as_string() - except: - it = 'do not works' - - success = True - try: - self.mailbox.process_incoming_message(email_object) - except ValueError: - success = False - - self.assertEqual(it, 'works') - self.assertEqual(True, success) - - def test_message_issue_82_bis(self): - """ Ensure that the email object is good before and after - calling _get_dehydrated_message() - - """ - message = self._get_email_object('email_issue_82.eml') - - success = True - - # this is the code of _process_message() - msg = Message() - # if STORE_ORIGINAL_MESSAGE: - # msg.eml.save('%s.eml' % uuid.uuid4(), ContentFile(message), - # save=False) - msg.mailbox = self.mailbox - if 'subject' in message: - msg.subject = convert_header_to_unicode(message['subject'])[0:255] - if 'message-id' in message: - msg.message_id = message['message-id'][0:255] - if 'from' in message: - msg.from_header = convert_header_to_unicode(message['from']) - if 'to' in message: - msg.to_header = convert_header_to_unicode(message['to']) - elif 'Delivered-To' in message: - msg.to_header = convert_header_to_unicode(message['Delivered-To']) - msg.save() - - # here the message is ok - str_msg = message.as_string() - message = self.mailbox._get_dehydrated_message(message, msg) - try: - # here as_string raises UnicodeEncodeError - str_msg = message.as_string() - except: - success = False - - msg.set_body(str_msg) - if message['in-reply-to']: - try: - msg.in_reply_to = Message.objects.filter( - message_id=message['in-reply-to'] - )[0] - except IndexError: - pass - msg.save() - - self.assertEqual(True, success) - - def test_message_with_misplaced_utf8_content(self): - """ Ensure that we properly handle incorrectly encoded messages - - ``message_with_utf8_char.eml``'s primary text payload is marked - as being iso-8859-1 data, but actually contains UTF-8 bytes. - - """ - email_object = self._get_email_object('message_with_utf8_char.eml') - - msg = self.mailbox.process_incoming_message(email_object) - - expected_text = 'This message contains funny UTF16 characters ' + \ - 'like this one: "\xc2\xa0" and this one "\xe2\x9c\xbf".' - actual_text = msg.text - - self.assertEqual( - expected_text, - actual_text, - ) - - def test_message_with_invalid_content_for_declared_encoding(self): - """ Ensure that we gracefully handle mis-encoded bodies. - - Should a payload body be misencoded, we should: - - - Not explode - - Note: there is (intentionally) no assertion below; the only guarantee - we make via this library is that processing this e-mail message will - not cause an exception to be raised. - - """ - email_object = self._get_email_object( - 'message_with_invalid_content_for_declared_encoding.eml', - ) - - msg = self.mailbox.process_incoming_message(email_object) - - msg.text - - def test_message_with_valid_content_in_single_byte_encoding(self): - email_object = self._get_email_object( - 'message_with_single_byte_encoding.eml', - ) - - msg = self.mailbox.process_incoming_message(email_object) - - actual_text = msg.text - expected_body = '\u042d\u0442\u043e ' + \ - '\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 ' + \ - '\u0438\u043c\u0435\u0435\u0442 ' + \ - '\u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d' + \ - '\u0443\u044e ' + \ - '\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430.' - - self.assertEqual( - actual_text, - expected_body, - ) - - def test_message_with_single_byte_subject_encoding(self): - email_object = self._get_email_object( - 'message_with_single_byte_extended_subject_encoding.eml', - ) - - msg = self.mailbox.process_incoming_message(email_object) - - expected_subject = '\u00D3\u00E7\u00ED\u00E0\u00E9 \u00EA\u00E0\u00EA ' + \ - '\u00E7\u00E0\u00F0\u00E0\u00E1\u00E0\u00F2\u00FB\u00E2' + \ - '\u00E0\u00F2\u00FC \u00EE\u00F2 1000$ \u00E2 ' + \ - '\u00ED\u00E5\u00E4\u00E5\u00EB\u00FE!' - actual_subject = msg.subject - self.assertEqual(actual_subject, expected_subject) - - expected_from = 'test test ' - actual_from = msg.from_header - self.assertEqual(expected_from, actual_from) - - def test_message_reply(self): - email_object = EmailMessage( - 'Test subject', # subject - 'Test body', # body - 'username@example.com', # from - ['mr.test32@mail.ru'], # to - ) - msg = self.mailbox.record_outgoing_message(email_object.message()) - - self.assertTrue(msg.outgoing) - - actual_from = 'username@example.com' - reply_email_object = EmailMessage( - 'Test subject', # subject - 'Test body', # body - actual_from, # from - ['mr.test32@mail.ru'], # to - ) - - with mock.patch.object(reply_email_object, 'send'): - reply_msg = msg.reply(reply_email_object) - - self.assertEqual(reply_msg.in_reply_to, msg) - - self.assertEqual(actual_from, msg.from_header) - - reply_email_object.from_email = None - - with mock.patch.object(reply_email_object, 'send'): - second_reply_msg = msg.reply(reply_email_object) - - self.assertEqual(self.mailbox.from_email, second_reply_msg.from_header) - - def test_message_with_text_attachment(self): - email_object = self._get_email_object( - 'message_with_text_attachment.eml', - ) - - msg = self.mailbox.process_incoming_message(email_object) - - self.assertEqual(msg.attachments.all().count(), 1) - self.assertEqual('Has an attached text document, too!', msg.text) - - def test_message_with_long_content(self): - email_object = self._get_email_object( - 'message_with_long_content.eml', - ) - size = len(force_text(email_object.as_string())) - - msg = self.mailbox.process_incoming_message(email_object) - - self.assertEqual(size, - len(force_text(msg.get_email_object().as_string()))) - - def test_message_saved(self): - message = self._get_email_object('generic_message.eml') - - default_settings = utils.get_settings() - - with mock.patch('django_mailbox.utils.get_settings') as get_settings: - altered = copy.deepcopy(default_settings) - altered['store_original_message'] = True - get_settings.return_value = altered - - msg = self.mailbox.process_incoming_message(message) - - self.assertNotEquals(msg.eml, None) - - self.assertTrue(msg.eml.name.endswith('.eml')) - - with open(msg.eml.name, 'rb') as f: - self.assertEqual( - f.read(), - self._get_email_as_text('generic_message.eml') - ) - - def test_message_saving_ignored(self): - message = self._get_email_object('generic_message.eml') - - default_settings = utils.get_settings() - - with mock.patch('django_mailbox.utils.get_settings') as get_settings: - altered = copy.deepcopy(default_settings) - altered['store_original_message'] = False - get_settings.return_value = altered - - msg = self.mailbox.process_incoming_message(message) - - self.assertEquals(msg.eml, None) - - def test_message_compressed(self): - message = self._get_email_object('generic_message.eml') - - default_settings = utils.get_settings() - - with mock.patch('django_mailbox.utils.get_settings') as get_settings: - altered = copy.deepcopy(default_settings) - altered['compress_original_message'] = True - altered['store_original_message'] = True - get_settings.return_value = altered - - msg = self.mailbox.process_incoming_message(message) - - actual_email_object = msg.get_email_object() - - self.assertTrue(msg.eml.name.endswith('.eml.gz')) - - with gzip.open(msg.eml.name, 'rb') as f: - self.assertEqual(f.read(), - self._get_email_as_text('generic_message.eml')) diff --git a/build/lib/django_mailbox/tests/test_processincomingmessage.py b/build/lib/django_mailbox/tests/test_processincomingmessage.py deleted file mode 100644 index 8ba2fb29..00000000 --- a/build/lib/django_mailbox/tests/test_processincomingmessage.py +++ /dev/null @@ -1,65 +0,0 @@ -from distutils.version import LooseVersion -from unittest import mock - -from django.core.management import call_command, CommandError -from django.test import TestCase -import django - -class CommandsTestCase(TestCase): - def test_processincomingmessage_no_args(self): - """Check that processincomingmessage works with no args""" - - mailbox_name = None - # Mock handle so that the test doesn't hang waiting for input. Note that we are only testing - # the argument parsing here -- functionality should be tested elsewhere - with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle: - # Don't care about the return value - handle.return_value = None - - call_command('processincomingmessage') - args, kwargs = handle.call_args - - # Make sure that we called with the right arguments - try: - self.assertEqual(kwargs['mailbox_name'], mailbox_name) - except KeyError: - # Handle Django 1.7 - # It uses optparse instead of argparse, so instead of being - # set to None, mailbox_name is simply left out altogether - # Thus we expect an empty tuple here - self.assertEqual(args, tuple()) - - - def test_processincomingmessage_with_arg(self): - """Check that processincomingmessage works with mailbox_name given""" - - mailbox_name = 'foo_mailbox' - - with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle: - handle.return_value = None - - call_command('processincomingmessage', mailbox_name) - args, kwargs = handle.call_args - try: - self.assertEqual(kwargs['mailbox_name'], mailbox_name) - except (AssertionError, KeyError): - # Handle Django 1.7 - # It uses optparse instead of argparse, so instead of being - # in kwargs, mailbox_name is in args - self.assertEqual(args[0], mailbox_name) - - def test_processincomingmessage_too_many_args(self): - """Check that processincomingmessage raises an error if too many args""" - # Only perform this test for Django versions greater than 1.7.*. This - # is because, with optparse, too many arguments doesn't result in an - # error, which means this test is worthless anyway - # For the "compatibility" versions, unexpected arguments aren't handled - # very well, and result in a TypeError - if (LooseVersion(django.get_version()) >= LooseVersion('1.8') and - LooseVersion(django.get_version()) < LooseVersion('1.10')): - with self.assertRaises(TypeError): - call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') - # In 1.10 and later a proper CommandError should be raised - elif LooseVersion(django.get_version()) >= LooseVersion('1.10'): - with self.assertRaises(CommandError): - call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') diff --git a/build/lib/django_mailbox/tests/test_transports.py b/build/lib/django_mailbox/tests/test_transports.py deleted file mode 100644 index 403a3069..00000000 --- a/build/lib/django_mailbox/tests/test_transports.py +++ /dev/null @@ -1,167 +0,0 @@ -from unittest import mock - -from django.test.utils import override_settings - -from django_mailbox.tests.base import EmailMessageTestCase, get_email_as_text -from django_mailbox.transports import ImapTransport, Pop3Transport - -FAKE_UID_SEARCH_ANSWER = ( - 'OK', - [ - b'18 19 20 21 22 23 24 25 26 27 28 29 ' + - b'30 31 32 33 34 35 36 37 38 39 40 41 42 43 44' - ] -) -FAKE_UID_FETCH_SIZES = ( - 'OK', - [ - b'1 (UID 18 RFC822.SIZE 58070000000)', - b'2 (UID 19 RFC822.SIZE 2593)' - ] -) -FAKE_UID_FETCH_MSG = ( - 'OK', - [ - ( - b'1 (UID 18 RFC822 {5807}', - get_email_as_text('generic_message.eml') - ), - ] -) -FAKE_UID_COPY_MSG = ( - 'OK', - [ - b'[COPYUID 1 2 2] (Success)' - ] -) -FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS = ( - 'OK', - [ - b'(\\HasNoChildren \\All) "/" "[Gmail]/All Mail"' - ] -) - - -class IMAPTestCase(EmailMessageTestCase): - def setUp(self): - def imap_server_uid_method(*args): - cmd = args[0] - arg2 = args[2] - if cmd == 'search': - return FAKE_UID_SEARCH_ANSWER - if cmd == 'copy': - return FAKE_UID_COPY_MSG - if cmd == 'fetch': - if arg2 == '(RFC822.SIZE)': - return FAKE_UID_FETCH_SIZES - if arg2 == '(RFC822)': - return FAKE_UID_FETCH_MSG - - def imap_server_list_method(pattern=None): - return FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS - - self.imap_server = mock.Mock() - self.imap_server.uid = imap_server_uid_method - self.imap_server.list = imap_server_list_method - super().setUp() - - -class TestImapTransport(IMAPTestCase): - def setUp(self): - super().setUp() - 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 = self.imap_server - - def test_get_email_message(self): - actual_messages = list(self.transport.get_message()) - self.assertEqual(len(actual_messages), 27) - actual_message = actual_messages[0] - expected_message = self._get_email_object('generic_message.eml') - self.assertEqual(expected_message, actual_message) - - -class TestImapArchivedTransport(TestImapTransport): - def setUp(self): - super().setUp() - self.archive = 'Archive' - self.transport = ImapTransport( - self.arbitrary_hostname, - self.arbitrary_port, - self.ssl, - self.archive - ) - self.transport.server = self.imap_server - - -class TestMaxSizeImapTransport(TestImapTransport): - - @override_settings(DJANGO_MAILBOX_MAX_MESSAGE_SIZE=5807) - def setUp(self): - super().setUp() - - self.transport = ImapTransport( - self.arbitrary_hostname, - self.arbitrary_port, - self.ssl, - ) - self.transport.server = self.imap_server - - def test_size_limit(self): - all_message_ids = self.transport._get_all_message_ids() - small_message_ids = self.transport._get_small_message_ids( - all_message_ids, - ) - self.assertEqual(len(small_message_ids), 1) - - def test_get_email_message(self): - 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().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/build/lib/django_mailbox/transports/__init__.py b/build/lib/django_mailbox/transports/__init__.py deleted file mode 100644 index 9f733e34..00000000 --- a/build/lib/django_mailbox/transports/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# all imports below are only used by external modules -# flake8: noqa -from django_mailbox.transports.imap import ImapTransport -from django_mailbox.transports.pop3 import Pop3Transport -from django_mailbox.transports.maildir import MaildirTransport -from django_mailbox.transports.mbox import MboxTransport -from django_mailbox.transports.babyl import BabylTransport -from django_mailbox.transports.mh import MHTransport -from django_mailbox.transports.mmdf import MMDFTransport -from django_mailbox.transports.gmail import GmailImapTransport -from django_mailbox.transports.office365 import Office365Transport diff --git a/build/lib/django_mailbox/transports/babyl.py b/build/lib/django_mailbox/transports/babyl.py deleted file mode 100644 index eb888a6d..00000000 --- a/build/lib/django_mailbox/transports/babyl.py +++ /dev/null @@ -1,6 +0,0 @@ -from mailbox import Babyl -from django_mailbox.transports.generic import GenericFileMailbox - - -class BabylTransport(GenericFileMailbox): - _variant = Babyl diff --git a/build/lib/django_mailbox/transports/base.py b/build/lib/django_mailbox/transports/base.py deleted file mode 100644 index a3a2bc01..00000000 --- a/build/lib/django_mailbox/transports/base.py +++ /dev/null @@ -1,12 +0,0 @@ -import email - -# Do *not* remove this, we need to use this in subclasses of EmailTransport -from email.errors import MessageParseError # noqa: F401 - - -class EmailTransport: - def get_email_from_bytes(self, contents): - message = email.message_from_bytes(contents) - - return message - diff --git a/build/lib/django_mailbox/transports/generic.py b/build/lib/django_mailbox/transports/generic.py deleted file mode 100644 index 5832fda2..00000000 --- a/build/lib/django_mailbox/transports/generic.py +++ /dev/null @@ -1,26 +0,0 @@ -import sys - -from .base import EmailTransport - - -class GenericFileMailbox(EmailTransport): - _variant = None - _path = None - - def __init__(self, path): - super().__init__() - self._path = path - - def get_instance(self): - return self._variant(self._path) - - def get_message(self, condition=None): - repository = self.get_instance() - repository.lock() - for key, message in repository.items(): - if condition and not condition(message): - continue - repository.remove(key) - yield message - repository.flush() - repository.unlock() diff --git a/build/lib/django_mailbox/transports/gmail.py b/build/lib/django_mailbox/transports/gmail.py deleted file mode 100644 index 99e8189a..00000000 --- a/build/lib/django_mailbox/transports/gmail.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging - -from django_mailbox.transports.imap import ImapTransport - - -logger = logging.getLogger(__name__) - - -class GmailImapTransport(ImapTransport): - - def connect(self, username, password): - # Try to use oauth2 first. It's much safer - try: - self._connect_oauth(username) - except (TypeError, ValueError) as e: - logger.warning("Couldn't do oauth2 because %s" % e) - self.server = self.transport(self.hostname, self.port) - typ, msg = self.server.login(username, password) - self.server.select() - - def _connect_oauth(self, username): - # username should be an email address that has already been authorized - # for gmail access - try: - from django_mailbox.google_utils import ( - get_google_access_token, - fetch_user_info, - AccessTokenNotFound, - ) - except ImportError: - raise ValueError( - "Install python-social-auth to use oauth2 auth for gmail" - ) - - access_token = None - while access_token is None: - try: - access_token = get_google_access_token(username) - google_email_address = fetch_user_info(username)['email'] - except TypeError: - # This means that the google process took too long - # Trying again is the right thing to do - pass - except AccessTokenNotFound: - raise ValueError( - "No Token available in python-social-auth for %s" % ( - username - ) - ) - - auth_string = 'user={}\1auth=Bearer {}\1\1'.format( - google_email_address, - access_token - ) - self.server = self.transport(self.hostname, self.port) - self.server.authenticate('XOAUTH2', lambda x: auth_string) - self.server.select() diff --git a/build/lib/django_mailbox/transports/imap.py b/build/lib/django_mailbox/transports/imap.py deleted file mode 100644 index 2599adad..00000000 --- a/build/lib/django_mailbox/transports/imap.py +++ /dev/null @@ -1,137 +0,0 @@ -import imaplib -import logging - -from django.conf import settings - -from .base import EmailTransport, MessageParseError - - -# By default, imaplib will raise an exception if it encounters more -# than 10k bytes; sometimes users attempt to consume mailboxes that -# have a more, and modern computers are skookum-enough to handle just -# a *few* more messages without causing any sort of problem. -imaplib._MAXLINE = 1000000 - - -logger = logging.getLogger(__name__) - - -class ImapTransport(EmailTransport): - def __init__( - self, hostname, port=None, ssl=False, tls=False, - archive='', folder=None, - ): - self.max_message_size = getattr( - settings, - 'DJANGO_MAILBOX_MAX_MESSAGE_SIZE', - False - ) - self.integration_testing_subject = getattr( - settings, - 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', - None - ) - self.hostname = hostname - self.port = port - self.archive = archive - self.folder = folder - self.tls = tls - if ssl: - self.transport = imaplib.IMAP4_SSL - if not self.port: - self.port = 993 - else: - self.transport = imaplib.IMAP4 - if not self.port: - self.port = 143 - - def connect(self, username, password): - self.server = self.transport(self.hostname, self.port) - if self.tls: - self.server.starttls() - typ, msg = self.server.login(username, password) - - if self.folder: - self.server.select(self.folder) - else: - self.server.select() - - def _get_all_message_ids(self): - # Fetch all the message uids - response, message_ids = self.server.uid('search', None, 'ALL') - message_id_string = message_ids[0].strip() - # Usually `message_id_string` will be a list of space-separated - # ids; we must make sure that it isn't an empty string before - # splitting into individual UIDs. - if message_id_string: - return message_id_string.decode().split(' ') - return [] - - def _get_small_message_ids(self, message_ids): - # Using existing message uids, get the sizes and - # return only those that are under the size - # limit - safe_message_ids = [] - - status, data = self.server.uid( - 'fetch', - ','.join(message_ids), - '(RFC822.SIZE)' - ) - - for each_msg in data: - each_msg = each_msg.decode() - try: - uid = each_msg.split(' ')[2] - size = each_msg.split(' ')[4].rstrip(')') - if int(size) <= int(self.max_message_size): - safe_message_ids.append(uid) - except ValueError as e: - logger.warning( - "ValueError: {} working on {}".format(e, each_msg[0]) - ) - pass - return safe_message_ids - - def get_message(self, condition=None): - message_ids = self._get_all_message_ids() - - if not message_ids: - return - - # Limit the uids to the small ones if we care about that - if self.max_message_size: - message_ids = self._get_small_message_ids(message_ids) - - if self.archive: - typ, folders = self.server.list(pattern=self.archive) - if folders[0] is None: - # If the archive folder does not exist, create it - self.server.create(self.archive) - - for uid in message_ids: - try: - typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') - if not msg_contents: - continue - try: - message = self.get_email_from_bytes(msg_contents[0][1]) - except TypeError: - # This happens if another thread/process deletes the - # message between our generating the ID list and our - # processing it here. - continue - - if condition and not condition(message): - continue - - yield message - except MessageParseError: - continue - - if self.archive: - self.server.uid('copy', uid, self.archive) - - self.server.uid('store', uid, "+FLAGS", "(\\Deleted)") - self.server.expunge() - return diff --git a/build/lib/django_mailbox/transports/maildir.py b/build/lib/django_mailbox/transports/maildir.py deleted file mode 100644 index 2d4af53d..00000000 --- a/build/lib/django_mailbox/transports/maildir.py +++ /dev/null @@ -1,9 +0,0 @@ -from mailbox import Maildir -from django_mailbox.transports.generic import GenericFileMailbox - - -class MaildirTransport(GenericFileMailbox): - _variant = Maildir - - def get_instance(self): - return self._variant(self._path, None) diff --git a/build/lib/django_mailbox/transports/mbox.py b/build/lib/django_mailbox/transports/mbox.py deleted file mode 100644 index d610deb8..00000000 --- a/build/lib/django_mailbox/transports/mbox.py +++ /dev/null @@ -1,6 +0,0 @@ -from mailbox import mbox -from django_mailbox.transports.generic import GenericFileMailbox - - -class MboxTransport(GenericFileMailbox): - _variant = mbox diff --git a/build/lib/django_mailbox/transports/mh.py b/build/lib/django_mailbox/transports/mh.py deleted file mode 100644 index 33fdc254..00000000 --- a/build/lib/django_mailbox/transports/mh.py +++ /dev/null @@ -1,6 +0,0 @@ -from mailbox import MH -from django_mailbox.transports.generic import GenericFileMailbox - - -class MHTransport(GenericFileMailbox): - _variant = MH diff --git a/build/lib/django_mailbox/transports/mmdf.py b/build/lib/django_mailbox/transports/mmdf.py deleted file mode 100644 index 270e4f34..00000000 --- a/build/lib/django_mailbox/transports/mmdf.py +++ /dev/null @@ -1,6 +0,0 @@ -from mailbox import MMDF -from django_mailbox.transports.generic import GenericFileMailbox - - -class MMDFTransport(GenericFileMailbox): - _variant = MMDF diff --git a/build/lib/django_mailbox/transports/office365.py b/build/lib/django_mailbox/transports/office365.py deleted file mode 100644 index 4f748b2f..00000000 --- a/build/lib/django_mailbox/transports/office365.py +++ /dev/null @@ -1,66 +0,0 @@ -import logging - -from django.conf import settings - -from .base import EmailTransport, MessageParseError - -logger = logging.getLogger(__name__) - - -class Office365Transport(EmailTransport): - def __init__( - self, hostname, username, archive='', folder=None - ): - self.integration_testing_subject = getattr( - settings, - 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT', - None - ) - self.hostname = hostname - self.username = username - self.archive = archive - self.folder = folder - - def connect(self, client_id, client_secret, tenant_id): - try: - import O365 - except ImportError: - raise ValueError( - "Install o365 to use oauth2 auth for office365" - ) - - credentials = (client_id, client_secret) - - self.account = O365.Account(credentials, auth_flow_type='credentials', tenant_id=tenant_id) - self.account.authenticate() - - self.mailbox = self.account.mailbox(resource=self.username) - self.mailbox_folder = self.mailbox.inbox_folder() - if self.folder: - self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder) - - def get_message(self, condition=None): - archive_folder = None - if self.archive: - archive_folder = self.mailbox.get_folder(folder_name=self.archive) - if not archive_folder: - archive_folder = self.mailbox.create_child_folder(self.archive) - - for o365message in self.mailbox_folder.get_messages(order_by='receivedDateTime'): - try: - mime_content = o365message.get_mime_content() - message = self.get_email_from_bytes(mime_content) - - if condition and not condition(message): - continue - - yield message - except MessageParseError: - continue - - if self.archive and archive_folder: - o365message.copy(archive_folder) - - o365message.delete() - return - diff --git a/build/lib/django_mailbox/transports/pop3.py b/build/lib/django_mailbox/transports/pop3.py deleted file mode 100644 index 6dbe9953..00000000 --- a/build/lib/django_mailbox/transports/pop3.py +++ /dev/null @@ -1,44 +0,0 @@ -from poplib import POP3, POP3_SSL - -from .base import EmailTransport, MessageParseError - - -class Pop3Transport(EmailTransport): - def __init__(self, hostname, port=None, ssl=False): - self.hostname = hostname - self.port = port - if ssl: - self.transport = POP3_SSL - if not self.port: - self.port = 995 - else: - self.transport = POP3 - if not self.port: - self.port = 110 - - def connect(self, username, password): - self.server = self.transport(self.hostname, self.port) - self.server.user(username) - self.server.pass_(password) - - def get_message_body(self, message_lines): - return bytes('\r\n', 'ascii').join(message_lines) - - def get_message(self, condition=None): - message_count = len(self.server.list()[1]) - for i in range(message_count): - try: - msg_contents = self.get_message_body( - self.server.retr(i + 1)[1] - ) - message = self.get_email_from_bytes(msg_contents) - - if condition and not condition(message): - continue - - yield message - except MessageParseError: - continue - self.server.dele(i + 1) - self.server.quit() - return diff --git a/build/lib/django_mailbox/utils.py b/build/lib/django_mailbox/utils.py deleted file mode 100644 index 6603612f..00000000 --- a/build/lib/django_mailbox/utils.py +++ /dev/null @@ -1,151 +0,0 @@ -import datetime -import email.header -import logging -import os - -from django.conf import settings - - -logger = logging.getLogger(__name__) - - -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 - ), - 'compress_original_message': getattr( - settings, - 'DJANGO_MAILBOX_COMPRESS_ORIGINAL_MESSAGE', - False - ), - 'original_message_compression': getattr( - settings, - 'DJANGO_MAILBOX_ORIGINAL_MESSAGE_COMPRESSION', - 6 - ), - '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, str): - return value - if not encoding or encoding == 'unknown-8bit': - encoding = default_charset - return value.decode(encoding, 'replace') - - try: - return ''.join( - [ - ( - _decode(bytestr, encoding) - ) for bytestr, encoding in email.header.decode_header(header) - ] - ) - except UnicodeDecodeError: - logger.exception( - 'Errors encountered decoding header %s into encoding %s.', - header, - default_charset, - ) - return header.decode(default_charset, 'replace') - - -def get_body_from_message(message, maintype, subtype): - """ - Fetchs the body message matching main/sub content type. - """ - body = '' - for part in message.walk(): - if part.get('content-disposition', '').startswith('attachment;'): - continue - if part.get_content_maintype() == maintype and \ - part.get_content_subtype() == subtype: - charset = part.get_content_charset() - this_part = part.get_payload(decode=True) - if charset: - try: - this_part = this_part.decode(charset, 'replace') - except LookupError: - this_part = this_part.decode('ascii', 'replace') - logger.warning( - 'Unknown encoding %s encountered while decoding ' - 'text payload. Interpreting as ASCII with ' - 'replacement, but some data may not be ' - 'represented as the sender intended.', - charset - ) - except ValueError: - this_part = this_part.decode('ascii', 'replace') - logger.warning( - 'Error encountered while decoding text ' - 'payload from an incorrectly-constructed ' - 'e-mail; payload was converted to ASCII with ' - 'replacement, but some data may not be ' - 'represented as the sender intended.' - ) - else: - this_part = this_part.decode('ascii', 'replace') - - body += this_part - - return body - - -def get_attachment_save_path(instance, filename): - settings = get_settings() - - path = settings['attachment_upload_to'] - if '%' in path: - path = datetime.datetime.utcnow().strftime(path) - - return os.path.join( - path, - filename, - ) From bd4d80a2b737b165b39498afebf689dab8108cf6 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Tue, 30 Aug 2022 11:59:35 +0200 Subject: [PATCH 16/18] missing archive --- django_mailbox/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_mailbox/models.py b/django_mailbox/models.py index c7c03f32..42296e98 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -251,7 +251,8 @@ def get_connection(self): conn = Office365Transport( self.location, self.username, - folder=self.folder + folder=self.folder, + archive=self.archive ) conn.connect(self.client_id, self.client_secret, self.tenant_id) elif self.type == 'maildir': From 21faa22404e23cd0535e0b15ff0cad1d0a5c0535 Mon Sep 17 00:00:00 2001 From: Pietro Mingo Date: Sun, 24 Dec 2023 12:53:18 +0100 Subject: [PATCH 17/18] MailboxAttachment __str__ change --- django_mailbox/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 4e0c94b4..c623cdf3 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -881,7 +881,8 @@ def __getitem__(self, name): return value def __str__(self): - return self.document.url + return f'{self.message}: {self.document.url if self.document else None}' + class Meta: verbose_name = _('Message attachment') From 7570a0d37a05951b8f2211de72713bd5d7248a01 Mon Sep 17 00:00:00 2001 From: Pietro Date: Wed, 27 Dec 2023 00:40:48 +0100 Subject: [PATCH 18/18] Update django_mailbox/models.py Co-authored-by: Pascal Fouque --- django_mailbox/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django_mailbox/models.py b/django_mailbox/models.py index c623cdf3..a27b5b25 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -881,7 +881,9 @@ def __getitem__(self, name): return value def __str__(self): - return f'{self.message}: {self.document.url if self.document else None}' + if self.document: + return f'{self.get_filename()}: {self.document.url}' + return self.get_filename() class Meta: