diff --git a/.travis.yml b/.travis.yml index 8dfd536..00a7ddf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,7 @@ python: - "3.4" - "3.5" - "3.6" - - "3.6-dev" - "3.7-dev" install: - python setup.py -q install -script: nosetests +script: nosetests -v diff --git a/CHANGELOG.md b/CHANGELOG.md index b89a1bd..447d960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.9 (18 September 2017) + +IMPROVEMENTS: + + * Permissively Decode Emails: ([#78](https://github.com/martinrusev/imbox/pull/78)) + * "With" statement for automatic cleanup/logout ([#92](https://github.com/martinrusev/imbox/pull/92)) + + + ## 0.8.6 (6 December 2016) IMPROVEMENTS: diff --git a/MANIFEST.in b/MANIFEST.in index 0e0f070..f46e3b6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include LICENSE include MANIFEST.in -include README.md \ No newline at end of file +include README.rst +include CHANGELOG.md +graft tests diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1733cfe --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +upload_to_pypi: + pip install twine setuptools + rm -rf dist/* + rm -rf build/* + python setup.py sdist build + twine upload dist/* + +test: + nosetests -v \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 61a4bfd..0000000 --- a/README.md +++ /dev/null @@ -1,116 +0,0 @@ -Imbox - Python IMAP for Humans -======= - -[![Build Status](https://travis-ci.org/martinrusev/imbox.svg?branch=master)](https://travis-ci.org/martinrusev/imbox) - - - -Python library for reading IMAP mailboxes and converting email content to machine readable data - -Requirements -============ - -Python (3.2, 3.3, 3.4, 3.5, 3.6) - - -Installation -============ - - pip install imbox - - -Usage -===== - -```python -from imbox import Imbox - -# SSL Context docs https://docs.python.org/2/library/ssl.html#ssl.create_default_context - -imbox = Imbox('imap.gmail.com', - username='username', - password='password', - ssl=True, - ssl_context=None) - -# Gets all messages -all_messages = imbox.messages() - -# Unread messages -unread_messages = imbox.messages(unread=True) - -# Messages sent FROM -messages_from = imbox.messages(sent_from='martin@amon.cx') - -# Messages sent TO -messages_from = imbox.messages(sent_to='martin@amon.cx') - -# Messages received before specific date -messages_from = imbox.messages(date__lt='31-July-2013') - -# Messages received after specific date -messages_from = imbox.messages(date__gt='30-July-2013') - -# Messages from a specific folder -messages_folder = imbox.messages(folder='Social') - - - -for uid, message in all_messages: - ........ -# Every message is an object with the following keys - - message.sent_from - message.sent_to - message.subject - message.headers - message.message_id - message.date - message.body.plain - message.body.html - message.attachments - -# To check all available keys - print message.keys() - - -# To check the whole object, just write - - print message - - { - 'headers': - [{ - 'Name': 'Received-SPF', - 'Value': 'pass (google.com: domain of ......;' - }, - { - 'Name': 'MIME-Version', - 'Value': '1.0' - }], - 'body': { - 'plain: ['ASCII'], - 'html': ['HTML BODY'] - }, - 'attachments': [{ - 'content': , - 'filename': "avatar.png", - 'content-type': 'image/png', - 'size': 80264 - }], - 'date': u 'Fri, 26 Jul 2013 10:56:26 +0300', - 'message_id': u '51F22BAA.1040606', - 'sent_from': [{ - 'name': u 'Martin Rusev', - 'email': 'martin@amon.cx' - }], - 'sent_to': [{ - 'name': u 'John Doe', - 'email': 'john@gmail.com' - }], - 'subject': u 'Hello John, How are you today' - } -``` - - -# [Changelog](https://github.com/martinrusev/imbox/blob/master/CHANGELOG.md) diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..02852c3 --- /dev/null +++ b/README.rst @@ -0,0 +1,117 @@ +Imbox - Python IMAP for Humans +============================== + + +.. image:: https://travis-ci.org/martinrusev/imbox.svg?branch=master + :target: https://travis-ci.org/martinrusev/imbox + :alt: Build Status + + +Python library for reading IMAP mailboxes and converting email content to machine readable data + +Requirements +------------ + +Python (3.2, 3.3, 3.4, 3.5, 3.6) + + +Installation +------------ + +``pip install imbox`` + + +Usage +----- + +.. code:: python + + from imbox import Imbox + + # SSL Context docs https://docs.python.org/3/library/ssl.html#ssl.create_default_context + + with Imbox('imap.gmail.com', + username='username', + password='password', + ssl=True, + ssl_context=None) as imbox: + + # Gets all messages + all_messages = imbox.messages() + + # Unread messages + unread_messages = imbox.messages(unread=True) + + # Messages sent FROM + messages_from = imbox.messages(sent_from='martin@amon.cx') + + # Messages sent TO + messages_from = imbox.messages(sent_to='martin@amon.cx') + + # Messages received before specific date + messages_from = imbox.messages(date__lt='31-July-2013') + + # Messages received after specific date + messages_from = imbox.messages(date__gt='30-July-2013') + + # Messages from a specific folder + messages_folder = imbox.messages(folder='Social') + + + + for uid, message in all_messages: + # Every message is an object with the following keys + + message.sent_from + message.sent_to + message.subject + message.headers + message.message_id + message.date + message.body.plain + message.body.html + message.attachments + + # To check all available keys + print(message.keys()) + + + # To check the whole object, just write + + print(message) + + { + 'headers': + [{ + 'Name': 'Received-SPF', + 'Value': 'pass (google.com: domain of ......;' + }, + { + 'Name': 'MIME-Version', + 'Value': '1.0' + }], + 'body': { + 'plain': ['ASCII'], + 'html': ['HTML BODY'] + }, + 'attachments': [{ + 'content': , + 'filename': "avatar.png", + 'content-type': 'image/png', + 'size': 80264 + }], + 'date': u 'Fri, 26 Jul 2013 10:56:26 +0300', + 'message_id': u '51F22BAA.1040606', + 'sent_from': [{ + 'name': u 'Martin Rusev', + 'email': 'martin@amon.cx' + }], + 'sent_to': [{ + 'name': u 'John Doe', + 'email': 'john@gmail.com' + }], + 'subject': u 'Hello John, How are you today' + } + + +`Changelog `_ diff --git a/imbox/__init__.py b/imbox/__init__.py index a8ddd04..657af78 100644 --- a/imbox/__init__.py +++ b/imbox/__init__.py @@ -6,7 +6,7 @@ import logging logger = logging.getLogger(__name__) -class Imbox(object): +class Imbox: def __init__(self, hostname, username=None, password=None, ssl=True, port=None, ssl_context=None, policy=None): @@ -21,6 +21,12 @@ class Imbox(object): logger.info("Connected to IMAP Server with user {username} on {hostname}{ssl}".format( hostname=hostname, username=username, ssl=(" over SSL" if ssl else ""))) + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.logout() + def logout(self): self.connection.close() self.connection.logout() diff --git a/imbox/imap.py b/imbox/imap.py index d4496d8..b99e2f4 100644 --- a/imbox/imap.py +++ b/imbox/imap.py @@ -6,7 +6,7 @@ import ssl as pythonssllib logger = logging.getLogger(__name__) -class ImapTransport(object): +class ImapTransport: def __init__(self, hostname, port=None, ssl=True, ssl_context=None): self.hostname = hostname diff --git a/imbox/parser.py b/imbox/parser.py index 90cda87..1cab82f 100644 --- a/imbox/parser.py +++ b/imbox/parser.py @@ -1,6 +1,4 @@ -from __future__ import unicode_literals -from six import BytesIO, binary_type - +import io import re import email import base64 @@ -14,7 +12,7 @@ import logging logger = logging.getLogger(__name__) -class Struct(object): +class Struct: def __init__(self, **entries): self.__dict__.update(entries) @@ -71,7 +69,7 @@ def decode_param(param): if type_ == 'Q': value = quopri.decodestring(code) elif type_ == 'B': - value = base64.decodestring(code) + value = base64.decodebytes(code.encode()) value = str_encode(value, encoding) value_results.append(value) if value_results: @@ -92,7 +90,7 @@ def parse_attachment(message_part): attachment = { 'content-type': message_part.get_content_type(), 'size': len(file_data), - 'content': BytesIO(file_data) + 'content': io.BytesIO(file_data) } filename = message_part.get_param('name') if filename: @@ -122,7 +120,7 @@ def decode_content(message): def parse_email(raw_email, policy=None): - if isinstance(raw_email, binary_type): + if isinstance(raw_email, bytes): raw_email = str_encode(raw_email, 'utf-8', errors='ignore') if policy is not None: email_parse_kwargs = dict(policy=policy) diff --git a/imbox/utils.py b/imbox/utils.py index 1558830..46d881d 100644 --- a/imbox/utils.py +++ b/imbox/utils.py @@ -1,24 +1,14 @@ -from __future__ import unicode_literals -from six import PY3 - import logging logger = logging.getLogger(__name__) -if PY3: - def str_encode(value='', encoding=None, errors='strict'): - logger.debug("Encode str {} with and errors {}".format(value, encoding, errors)) - return str(value, encoding, errors) +def str_encode(value='', encoding=None, errors='strict'): + logger.debug("Encode str {} with and errors {}".format(value, encoding, errors)) + return str(value, encoding, errors) - def str_decode(value='', encoding=None, errors='strict'): - if isinstance(value, str): - return bytes(value, encoding, errors).decode('utf-8') - elif isinstance(value, bytes): - return value.decode(encoding or 'utf-8', errors=errors) - else: - raise TypeError( "Cannot decode '{}' object".format(value.__class__) ) -else: - def str_encode(string='', encoding=None, errors='strict'): - return unicode(string, encoding, errors) - - def str_decode(value='', encoding=None, errors='strict'): - return value.decode(encoding, errors) +def str_decode(value='', encoding=None, errors='strict'): + if isinstance(value, str): + return bytes(value, encoding, errors).decode('utf-8') + elif isinstance(value, bytes): + return value.decode(encoding or 'utf-8', errors=errors) + else: + raise TypeError( "Cannot decode '{}' object".format(value.__class__) ) diff --git a/setup.py b/setup.py index b8b02a3..6812fe2 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup import os -version = '0.8.5' +version = '0.9' def read(filename): @@ -11,7 +11,7 @@ setup( name='imbox', version=version, description="Python IMAP for Human beings", - long_description=read('README.md'), + long_description=read('README.rst'), keywords='email, IMAP, parsing emails', author='Martin Rusev', author_email='martin@amon.cx', @@ -20,12 +20,13 @@ setup( packages=['imbox'], package_dir={'imbox': 'imbox'}, zip_safe=False, - install_requires=['six'], classifiers=( 'Programming Language :: Python', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6' ), + test_suite='tests', ) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 4d7bfa1..6069a14 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -1,11 +1,9 @@ -# Encoding: utf-8 -from __future__ import unicode_literals import unittest from imbox.parser import * import os import sys -if sys.version_info.major < 3 or sys.version_info.minor < 3: +if sys.version_info.minor < 3: SMTP = False else: from email.policy import SMTP @@ -86,6 +84,46 @@ Content-Transfer-Encoding: quoted-printable """ +raw_email_encoded_bad_multipart = b"""Delivered-To: receiver@example.com +Return-Path: +From: sender@example.com +To: "Receiver" , "Second\r\n Receiver" +Subject: Re: Looking to connect with you... +Date: Thu, 20 Apr 2017 15:32:52 +0000 +Message-ID: +Content-Type: multipart/related; + boundary="_004_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_"; + type="multipart/alternative" +MIME-Version: 1.0 +--_004_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_ +Content-Type: multipart/alternative; + boundary="_000_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_" +--_000_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_ +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: base64 +SGkgRGFuaWVsbGUsDQoNCg0KSSBhY3R1YWxseSBhbSBoYXBweSBpbiBteSBjdXJyZW50IHJvbGUs +Y3J1aXRlciB8IENoYXJsb3R0ZSwgTkMNClNlbnQgdmlhIEhhcHBpZQ0KDQoNCg== +--_000_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_ +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: base64 +PGh0bWw+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0i +CjwvZGl2Pg0KPC9kaXY+DQo8L2JvZHk+DQo8L2h0bWw+DQo= +--_000_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_-- +--_004_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_ +Content-Type: image/png; name="=?utf-8?B?T3V0bG9va0Vtb2ppLfCfmIoucG5n?=" +Content-Description: =?utf-8?B?T3V0bG9va0Vtb2ppLfCfmIoucG5n?= +Content-Disposition: inline; + filename="=?utf-8?B?T3V0bG9va0Vtb2ppLfCfmIoucG5n?="; size=488; + creation-date="Thu, 20 Apr 2017 15:32:52 GMT"; + modification-date="Thu, 20 Apr 2017 15:32:52 GMT" +Content-ID: <254962e2-f05c-40d1-aa11-0d34671b056c> +Content-Transfer-Encoding: base64 +iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ +cvED9AIR3TCAAAMAqh+p+YMVeBQAAAAASUVORK5CYII= +--_004_BN6PR16MB179579288933D60C4016D078C31B0BN6PR16MB1795namp_-- +""" + + class TestParser(unittest.TestCase): def test_parse_email(self): @@ -106,6 +144,10 @@ class TestParser(unittest.TestCase): parsed_email = parse_email(open(os.path.join(TEST_DIR, '8422.msg'), 'rb').read()) self.assertEqual("Following up Re: Looking to connect, let's schedule a call!", parsed_email.subject) + def test_parse_email_bad_multipart(self): + parsed_email = parse_email(raw_email_encoded_bad_multipart) + self.assertEqual("Re: Looking to connect with you...", parsed_email.subject) + def test_parse_email_ignores_header_casing(self): self.assertEqual('one', parse_email('Message-ID: one').message_id) self.assertEqual('one', parse_email('Message-Id: one').message_id)