Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12a8cd9a41 | |||
| 70b0552df4 | |||
| b2f82b6fb7 | |||
| 76fc7bfe8c | |||
| 0f8abf3e4a | |||
| fee17fbc76 | |||
| 27d19a209a | |||
| 0a94f2e4a4 | |||
| 810e6a0cd8 | |||
|
|
782d3977f9 | ||
|
|
c21f2ec9f7 | ||
|
|
1f0b952fbc | ||
|
|
d0b3fa495e | ||
|
|
038094e16d | ||
|
|
e1eb91b689 | ||
|
|
dbdc96f91e | ||
|
|
4dc43d2ba2 | ||
|
|
fd35764586 | ||
|
|
acfd162c8a | ||
|
|
9dbf9a9071 | ||
|
|
0249d59a73 | ||
|
|
1ad18477f1 | ||
|
|
079569dac9 | ||
|
|
7ea534fbda | ||
|
|
db30a8044f | ||
|
|
d5a4e47904 | ||
|
|
330a989452 | ||
|
|
aa6739d91f | ||
| f82ce52220 | |||
| 87e42a9fdd | |||
| 86cf1fbf9b | |||
| 6f3cc1ab92 | |||
| 1fbb8511b9 | |||
|
|
adf70cbed5 | ||
|
|
c04c340bb7 | ||
|
|
b52d43c496 | ||
|
|
a8440f60ca | ||
|
|
c86cbe766d | ||
|
|
16a05e15e5 | ||
| 7d11b590d8 | |||
| dfca46886f | |||
| 6d527a2f1d | |||
| 80d6104387 | |||
|
|
0464a5a74d | ||
| 5b27fa1b76 | |||
| 9cffb51a81 | |||
|
|
06aa4e054b | ||
|
|
63bddbd73c | ||
| 2838639bfa | |||
|
|
34149efed5 | ||
| 73fafcb368 | |||
| 8e26e92a39 | |||
| 635d15441e | |||
| 55f64a1922 | |||
| 71942a69e8 | |||
| b4cb03e145 | |||
| b53a1e6837 | |||
|
|
a1801af56e | ||
|
|
6a0da4b105 | ||
|
|
5d09b6f3e4 | ||
|
|
ecb62b585c | ||
|
|
cbb46ef078 | ||
|
|
79ce81aa9d | ||
|
|
96ce737df5 | ||
|
|
a94682fc3c | ||
|
|
85abe48c8c | ||
|
|
9956d182eb | ||
|
|
ef68d92021 | ||
|
|
43a37818ab | ||
|
|
7b1bda6126 | ||
|
|
fe965e7d19 | ||
|
|
cfca92df60 | ||
|
|
5664e9c48a | ||
|
|
6c11c759c0 | ||
|
|
2f72aa13df | ||
|
|
ffd4550524 | ||
|
|
14c592136e | ||
|
|
85025b34ff | ||
|
|
ed251ce999 | ||
|
|
6dc0e7b3f2 | ||
|
|
79601bef7a | ||
|
|
88143c7a1b | ||
|
|
7c6cc2fb5f | ||
|
|
5460bec4b5 | ||
|
|
ea4fd7d9ea | ||
|
|
c64bbd73e9 | ||
|
|
acfb2adc47 | ||
|
|
050e2d8f7f | ||
|
|
6e0ee232fe | ||
|
|
a490081e57 | ||
|
|
a8b5ce1c31 | ||
|
|
7c5a639cc8 | ||
|
|
a78641c79a | ||
|
|
7dad0edb72 | ||
|
|
2ed728485b | ||
|
|
4559149dc0 | ||
|
|
0a98b960ac | ||
|
|
ed86228e86 | ||
|
|
eadddd6c0b | ||
|
|
878c7991bf | ||
|
|
8a537de2f9 | ||
|
|
da450551d4 | ||
|
|
61a6c87fe6 | ||
|
|
05683b3765 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -31,3 +31,9 @@ nosetests.xml
|
|||||||
|
|
||||||
example.*
|
example.*
|
||||||
example.py
|
example.py
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "3.2"
|
|
||||||
- "3.3"
|
- "3.3"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"python.pythonPath": "/usr/local/bin/python3"
|
||||||
|
}
|
||||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,9 +1,32 @@
|
|||||||
|
## 0.9.6 (14 August 2018)
|
||||||
|
|
||||||
|
IMPROVEMENTS:
|
||||||
|
|
||||||
|
* Vendors package, adding provider specific functionality ([#139](https://github.com/martinrusev/imbox/pull/139)) - Contributed by @zevaverbach
|
||||||
|
* Type hints for every method and function ([#136](https://github.com/martinrusev/imbox/pull/136)) - Contributed by @zevaverbach
|
||||||
|
* Move all code out of __init__.py and into a separate module ([#130](https://github.com/martinrusev/imbox/pull/130)) - Contributed by @zevaverbach
|
||||||
|
* Enhance `messages' generator: ([#129](https://github.com/martinrusev/imbox/pull/129)) - Contributed by @zevaverbach
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.5 (5 December 2017)
|
||||||
|
|
||||||
|
IMPROVEMENTS:
|
||||||
|
|
||||||
|
* `date__on` support: ([#109](https://github.com/martinrusev/imbox/pull/109)) - Contributed by @balsagoth
|
||||||
|
* Starttls support: ([#108](https://github.com/martinrusev/imbox/pull/108)) - Contributed by @balsagoth
|
||||||
|
* Mark emails as flagged/starred: ([#107](https://github.com/martinrusev/imbox/pull/107)) - Contributed by @memanikantan
|
||||||
|
* Messages filter can use date objects instead of stringified dates: ([#104](https://github.com/martinrusev/imbox/pull/104)) - Contributed by @sblondon
|
||||||
|
* Fix attachment parsing when a semicolon character ends the Content-Disposition line: ([#100](https://github.com/martinrusev/imbox/pull/100)) - Contributed by @sblondon
|
||||||
|
* Parsing - UnicecodeDecodeError() fixes: ([#96](https://github.com/martinrusev/imbox/pull/96)) - Contributed by @am0z
|
||||||
|
* Imbox() `with` support: ([#92](https://github.com/martinrusev/imbox/pull/92)) - Contributed by @sblondon
|
||||||
|
|
||||||
|
|
||||||
## 0.9 (18 September 2017)
|
## 0.9 (18 September 2017)
|
||||||
|
|
||||||
IMPROVEMENTS:
|
IMPROVEMENTS:
|
||||||
|
|
||||||
* Permissively Decode Emails: ([#78](https://github.com/martinrusev/imbox/pull/78))
|
* Permissively Decode Emails: ([#78](https://github.com/martinrusev/imbox/pull/78)) - Contributed by @AdamNiederer
|
||||||
* "With" statement for automatic cleanup/logout ([#92](https://github.com/martinrusev/imbox/pull/92))
|
* "With" statement for automatic cleanup/logout ([#92](https://github.com/martinrusev/imbox/pull/92)) - Contributed by @sblondon
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +34,7 @@ IMPROVEMENTS:
|
|||||||
|
|
||||||
IMPROVEMENTS:
|
IMPROVEMENTS:
|
||||||
|
|
||||||
* Add support for Python 3.3+ Parsing policies: ([#75](https://github.com/martinrusev/imbox/pull/75))
|
* Add support for Python 3.3+ Parsing policies: ([#75](https://github.com/martinrusev/imbox/pull/75)) - Contributed by @bhtucker
|
||||||
|
|
||||||
BACKWARDS INCOMPATIBILITIES / NOTES:
|
BACKWARDS INCOMPATIBILITIES / NOTES:
|
||||||
|
|
||||||
@@ -22,4 +45,4 @@ BACKWARDS INCOMPATIBILITIES / NOTES:
|
|||||||
|
|
||||||
IMPROVEMENTS:
|
IMPROVEMENTS:
|
||||||
|
|
||||||
* ssl_context: Check SSLContext for IMAP4_SSL connections ([#69](https://github.com/martinrusev/imbox/pull/69))
|
* ssl_context: Check SSLContext for IMAP4_SSL connections ([#69](https://github.com/martinrusev/imbox/pull/69)) - Contributed by @dmth
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include MANIFEST.in
|
include MANIFEST.in
|
||||||
include README.rst
|
include README.rst
|
||||||
|
include CHANGELOG.md
|
||||||
|
graft tests
|
||||||
|
|||||||
80
README.rst
80
README.rst
@@ -12,7 +12,7 @@ Python library for reading IMAP mailboxes and converting email content to machin
|
|||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Python (3.2, 3.3, 3.4, 3.5, 3.6)
|
Python (3.3, 3.4, 3.5, 3.6)
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
@@ -34,32 +34,59 @@ Usage
|
|||||||
username='username',
|
username='username',
|
||||||
password='password',
|
password='password',
|
||||||
ssl=True,
|
ssl=True,
|
||||||
ssl_context=None) as imbox:
|
ssl_context=None,
|
||||||
|
starttls=False) as imbox:
|
||||||
|
|
||||||
# Gets all messages
|
# Get all folders
|
||||||
all_messages = imbox.messages()
|
status, folders_with_additional_info = imbox.folders()
|
||||||
|
|
||||||
|
# Gets all messages from the inbox
|
||||||
|
all_inbox_messages = imbox.messages()
|
||||||
|
|
||||||
# Unread messages
|
# Unread messages
|
||||||
unread_messages = imbox.messages(unread=True)
|
unread_inbox_messages = imbox.messages(unread=True)
|
||||||
|
|
||||||
|
# Flagged messages
|
||||||
|
inbox_flagged_messages = imbox.messages(flagged=True)
|
||||||
|
|
||||||
|
# Un-flagged messages
|
||||||
|
inbox_unflagged_messages = imbox.messages(unflagged=True)
|
||||||
|
|
||||||
|
# Flagged messages
|
||||||
|
flagged_messages = imbox.messages(flagged=True)
|
||||||
|
|
||||||
|
# Un-flagged messages
|
||||||
|
unflagged_messages = imbox.messages(unflagged=True)
|
||||||
|
|
||||||
# Messages sent FROM
|
# Messages sent FROM
|
||||||
messages_from = imbox.messages(sent_from='martin@amon.cx')
|
inbox_messages_from = imbox.messages(sent_from='sender@example.org')
|
||||||
|
|
||||||
# Messages sent TO
|
# Messages sent TO
|
||||||
messages_from = imbox.messages(sent_to='martin@amon.cx')
|
inbox_messages_to = imbox.messages(sent_to='receiver@example.org')
|
||||||
|
|
||||||
# Messages received before specific date
|
# Messages received before specific date
|
||||||
messages_from = imbox.messages(date__lt='31-July-2013')
|
inbox_messages_received_before = imbox.messages(date__lt=datetime.date(2018, 7, 31))
|
||||||
|
|
||||||
# Messages received after specific date
|
# Messages received after specific date
|
||||||
messages_from = imbox.messages(date__gt='30-July-2013')
|
inbox_messages_received_after = imbox.messages(date__gt=datetime.date(2018, 7, 30))
|
||||||
|
|
||||||
|
# Messages received on a specific date
|
||||||
|
inbox_messages_received_on_date = imbox.messages(date__on=datetime.date(2018, 7, 30))
|
||||||
|
|
||||||
|
# Messages whose subjects contain a string
|
||||||
|
inbox_messages_subject_christmas = imbox.messages(subject='Christmas')
|
||||||
|
|
||||||
|
# Messages whose UID is greater than 1050
|
||||||
|
inbox_messages_uids_greater_than_1050 = imbox.messages(uid__range='1050:*')
|
||||||
|
|
||||||
# Messages from a specific folder
|
# Messages from a specific folder
|
||||||
messages_folder = imbox.messages(folder='Social')
|
messages_in_folder_social = imbox.messages(folder='Social')
|
||||||
|
|
||||||
|
# Some of Gmail's IMAP Extensions are supported (label and raw):
|
||||||
|
all_messages_with_an_attachment_from_martin = imbox.messages(folder='all', raw='from:martin@amon.cx has:attachment')
|
||||||
|
all_messages_labeled_finance = imbox.messages(folder='all', label='finance')
|
||||||
|
|
||||||
|
for uid, message in all_inbox_messages:
|
||||||
for uid, message in all_messages:
|
|
||||||
# Every message is an object with the following keys
|
# Every message is an object with the following keys
|
||||||
|
|
||||||
message.sent_from
|
message.sent_from
|
||||||
@@ -113,5 +140,34 @@ Usage
|
|||||||
'subject': u 'Hello John, How are you today'
|
'subject': u 'Hello John, How are you today'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# With the message id, several actions on the message are available:
|
||||||
|
# delete the message
|
||||||
|
imbox.delete(uid)
|
||||||
|
|
||||||
|
# mark the message as read
|
||||||
|
imbox.mark_seen(uid)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
---------
|
||||||
|
|
||||||
`Changelog <https://github.com/martinrusev/imbox/blob/master/CHANGELOG.md>`_
|
`Changelog <https://github.com/martinrusev/imbox/blob/master/CHANGELOG.md>`_
|
||||||
|
|
||||||
|
|
||||||
|
Running the tests
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
You can run the imbox tests with ``tox``.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
* the supported python versions
|
||||||
|
* ``tox``. Tox is packaged in Debian and derivatives distributions.
|
||||||
|
|
||||||
|
On Ubuntu, you can install several python versions with:
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.X
|
||||||
|
|||||||
@@ -1,89 +1,9 @@
|
|||||||
from imbox.imap import ImapTransport
|
from imbox.imbox import Imbox
|
||||||
from imbox.parser import parse_email
|
|
||||||
from imbox.query import build_search_query
|
|
||||||
|
|
||||||
import logging
|
__version_info__ = (0, 9, 6)
|
||||||
logger = logging.getLogger(__name__)
|
__version__ = '.'.join([str(x) for x in __version_info__])
|
||||||
|
|
||||||
|
|
||||||
class Imbox:
|
__all__ = ['Imbox']
|
||||||
|
|
||||||
def __init__(self, hostname, username=None, password=None, ssl=True,
|
|
||||||
port=None, ssl_context=None, policy=None):
|
|
||||||
|
|
||||||
self.server = ImapTransport(hostname, ssl=ssl, port=port,
|
|
||||||
ssl_context=ssl_context)
|
|
||||||
self.hostname = hostname
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.parser_policy = policy
|
|
||||||
self.connection = self.server.connect(username, password)
|
|
||||||
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()
|
|
||||||
logger.info("Disconnected from IMAP Server {username}@{hostname}".format(
|
|
||||||
hostname=self.hostname, username=self.username))
|
|
||||||
|
|
||||||
def query_uids(self, **kwargs):
|
|
||||||
query = build_search_query(**kwargs)
|
|
||||||
message, data = self.connection.uid('search', None, query)
|
|
||||||
if data[0] is None:
|
|
||||||
return []
|
|
||||||
return data[0].split()
|
|
||||||
|
|
||||||
def fetch_by_uid(self, uid):
|
|
||||||
message, data = self.connection.uid('fetch', uid, '(BODY.PEEK[])')
|
|
||||||
logger.debug("Fetched message for UID {}".format(int(uid)))
|
|
||||||
raw_email = data[0][1]
|
|
||||||
|
|
||||||
email_object = parse_email(raw_email, policy=self.parser_policy)
|
|
||||||
|
|
||||||
return email_object
|
|
||||||
|
|
||||||
def fetch_list(self, **kwargs):
|
|
||||||
uid_list = self.query_uids(**kwargs)
|
|
||||||
logger.debug("Fetch all messages for UID in {}".format(uid_list))
|
|
||||||
|
|
||||||
for uid in uid_list:
|
|
||||||
yield (uid, self.fetch_by_uid(uid))
|
|
||||||
|
|
||||||
def mark_seen(self, uid):
|
|
||||||
logger.info("Mark UID {} with \\Seen FLAG".format(int(uid)))
|
|
||||||
self.connection.uid('STORE', uid, '+FLAGS', '(\\Seen)')
|
|
||||||
|
|
||||||
def delete(self, uid):
|
|
||||||
logger.info("Mark UID {} with \\Deleted FLAG and expunge.".format(int(uid)))
|
|
||||||
mov, data = self.connection.uid('STORE', uid, '+FLAGS', '(\\Deleted)')
|
|
||||||
self.connection.expunge()
|
|
||||||
|
|
||||||
def copy(self, uid, destination_folder):
|
|
||||||
logger.info("Copy UID {} to {} folder".format(int(uid), str(destination_folder)))
|
|
||||||
return self.connection.uid('COPY', uid, destination_folder)
|
|
||||||
|
|
||||||
def move(self, uid, destination_folder):
|
|
||||||
logger.info("Move UID {} to {} folder".format(int(uid), str(destination_folder)))
|
|
||||||
if self.copy(uid, destination_folder):
|
|
||||||
self.delete(uid)
|
|
||||||
|
|
||||||
def messages(self, *args, **kwargs):
|
|
||||||
folder = kwargs.get('folder', False)
|
|
||||||
msg = ""
|
|
||||||
|
|
||||||
if folder:
|
|
||||||
self.connection.select(folder)
|
|
||||||
msg = " from folder '{}'".format(folder)
|
|
||||||
|
|
||||||
logger.info("Fetch list of messages{}".format(msg))
|
|
||||||
return self.fetch_list(**kwargs)
|
|
||||||
|
|
||||||
def folders(self):
|
|
||||||
return self.connection.list()
|
|
||||||
|
|||||||
@@ -8,24 +8,20 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ImapTransport:
|
class ImapTransport:
|
||||||
|
|
||||||
def __init__(self, hostname, port=None, ssl=True, ssl_context=None):
|
def __init__(self, hostname, port=None, ssl=True, ssl_context=None, starttls=False):
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.port = port
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
if ssl:
|
if ssl:
|
||||||
self.transport = IMAP4_SSL
|
self.port = port or 993
|
||||||
if not self.port:
|
|
||||||
self.port = 993
|
|
||||||
if ssl_context is None:
|
if ssl_context is None:
|
||||||
ssl_context = pythonssllib.create_default_context()
|
ssl_context = pythonssllib.create_default_context()
|
||||||
kwargs["ssl_context"] = ssl_context
|
self.server = IMAP4_SSL(self.hostname, self.port, ssl_context=ssl_context)
|
||||||
else:
|
else:
|
||||||
self.transport = IMAP4
|
self.port = port or 143
|
||||||
if not self.port:
|
self.server = IMAP4(self.hostname, self.port)
|
||||||
self.port = 143
|
|
||||||
|
|
||||||
self.server = self.transport(self.hostname, self.port, **kwargs)
|
if starttls:
|
||||||
|
self.server.starttls()
|
||||||
logger.debug("Created IMAP4 transport for {host}:{port}"
|
logger.debug("Created IMAP4 transport for {host}:{port}"
|
||||||
.format(host=self.hostname, port=self.port))
|
.format(host=self.hostname, port=self.port))
|
||||||
|
|
||||||
|
|||||||
13
imbox/imap.pyi
Normal file
13
imbox/imap.pyi
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from imaplib import IMAP4, IMAP4_SSL
|
||||||
|
from ssl import SSLContext
|
||||||
|
from typing import Optional, Union, Tuple, List
|
||||||
|
|
||||||
|
|
||||||
|
class ImapTransport:
|
||||||
|
|
||||||
|
def __init__(self, hostname: str, port: Optional[int], ssl: bool,
|
||||||
|
ssl_context: Optional[SSLContext], starttls: bool) -> None: ...
|
||||||
|
|
||||||
|
def list_folders(self) -> Tuple[str, List[bytes]]: ...
|
||||||
|
|
||||||
|
def connect(self, username: str, password: str) -> Union[IMAP4, IMAP4_SSL]: ...
|
||||||
104
imbox/imbox.py
Normal file
104
imbox/imbox.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import imaplib
|
||||||
|
|
||||||
|
from imbox.imap import ImapTransport
|
||||||
|
from imbox.messages import Messages
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from imbox.vendors import GmailMessages, hostname_vendorname_dict, name_authentication_string_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Imbox:
|
||||||
|
|
||||||
|
authentication_error_message = None
|
||||||
|
|
||||||
|
def __init__(self, hostname, username=None, password=None, ssl=True,
|
||||||
|
port=None, ssl_context=None, policy=None, starttls=False,
|
||||||
|
vendor=None):
|
||||||
|
|
||||||
|
self.server = ImapTransport(hostname, ssl=ssl, port=port,
|
||||||
|
ssl_context=ssl_context, starttls=starttls)
|
||||||
|
|
||||||
|
self.hostname = hostname
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.parser_policy = policy
|
||||||
|
self.vendor = vendor or hostname_vendorname_dict.get(self.hostname)
|
||||||
|
|
||||||
|
if self.vendor is not None:
|
||||||
|
self.authentication_error_message = name_authentication_string_dict.get(
|
||||||
|
self.vendor)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.connection = self.server.connect(username, password)
|
||||||
|
except imaplib.IMAP4.error as e:
|
||||||
|
if self.authentication_error_message is None:
|
||||||
|
raise
|
||||||
|
raise imaplib.IMAP4.error(
|
||||||
|
self.authentication_error_message + '\n' + str(e))
|
||||||
|
|
||||||
|
logger.info("Connected to IMAP Server with user {username} on {hostname}{ssl}".format(
|
||||||
|
hostname=hostname, username=username, ssl=(" over SSL" if ssl or starttls else "")))
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.logout()
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
self.connection.close()
|
||||||
|
self.connection.logout()
|
||||||
|
logger.info("Disconnected from IMAP Server {username}@{hostname}".format(
|
||||||
|
hostname=self.hostname, username=self.username))
|
||||||
|
|
||||||
|
def mark_seen(self, uid):
|
||||||
|
logger.info("Mark UID {} with \\Seen FLAG".format(int(uid)))
|
||||||
|
self.connection.uid('STORE', uid, '+FLAGS', '(\\Seen)')
|
||||||
|
|
||||||
|
def mark_flag(self, uid):
|
||||||
|
logger.info("Mark UID {} with \\Flagged FLAG".format(int(uid)))
|
||||||
|
self.connection.uid('STORE', uid, '+FLAGS', '(\\Flagged)')
|
||||||
|
|
||||||
|
def delete(self, uid):
|
||||||
|
logger.info(
|
||||||
|
"Mark UID {} with \\Deleted FLAG and expunge.".format(int(uid)))
|
||||||
|
self.connection.expunge()
|
||||||
|
|
||||||
|
def copy(self, uid, destination_folder):
|
||||||
|
logger.info("Copy UID {} to {} folder".format(
|
||||||
|
int(uid), str(destination_folder)))
|
||||||
|
return self.connection.uid('COPY', uid, destination_folder)
|
||||||
|
|
||||||
|
def move(self, uid, destination_folder):
|
||||||
|
logger.info("Move UID {} to {} folder".format(
|
||||||
|
int(uid), str(destination_folder)))
|
||||||
|
if self.copy(uid, destination_folder):
|
||||||
|
self.delete(uid)
|
||||||
|
|
||||||
|
def messages(self, **kwargs):
|
||||||
|
folder = kwargs.get('folder', False)
|
||||||
|
|
||||||
|
messages_class = Messages
|
||||||
|
|
||||||
|
if self.vendor == 'gmail':
|
||||||
|
messages_class = GmailMessages
|
||||||
|
|
||||||
|
if folder:
|
||||||
|
self.connection.select(
|
||||||
|
messages_class.FOLDER_LOOKUP.get((folder.lower())) or folder)
|
||||||
|
msg = " from folder '{}'".format(folder)
|
||||||
|
del kwargs['folder']
|
||||||
|
else:
|
||||||
|
msg = " from inbox"
|
||||||
|
|
||||||
|
logger.info("Fetch list of messages{}".format(msg))
|
||||||
|
|
||||||
|
return messages_class(connection=self.connection,
|
||||||
|
parser_policy=self.parser_policy,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def folders(self):
|
||||||
|
return self.connection.list()
|
||||||
31
imbox/imbox.pyi
Normal file
31
imbox/imbox.pyi
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import datetime
|
||||||
|
from email._policybase import Policy
|
||||||
|
from inspect import Traceback
|
||||||
|
from ssl import SSLContext
|
||||||
|
from typing import Optional, Union, Tuple, List
|
||||||
|
|
||||||
|
|
||||||
|
class Imbox:
|
||||||
|
|
||||||
|
def __init__(self, hostname: str, username: Optional[str], password: Optional[str], ssl: bool,
|
||||||
|
port: Optional[int], ssl_context: Optional[SSLContext], policy: Optional[Policy], starttls: bool): ...
|
||||||
|
|
||||||
|
def __enter__(self) -> 'Imbox': ...
|
||||||
|
|
||||||
|
def __exit__(self, type: Exception, value: str, traceback: Traceback) -> None: ...
|
||||||
|
|
||||||
|
def logout(self) -> None: ...
|
||||||
|
|
||||||
|
def mark_seen(self, uid: bytes) -> None: ...
|
||||||
|
|
||||||
|
def mark_flag(self, uid: bytes) -> None: ...
|
||||||
|
|
||||||
|
def delete(self, uid: bytes) -> None: ...
|
||||||
|
|
||||||
|
def copy(self, uid: bytes, destination_folder: Union[bytes, str]) -> Tuple[str, Union[list, List[None, bytes]]]: ...
|
||||||
|
|
||||||
|
def move(self, uid: bytes, destination_folder: Union[bytes, str]) -> None: ...
|
||||||
|
|
||||||
|
def messages(self, **kwargs: Union[bool, str, datetime.date]) -> 'Messages': ...
|
||||||
|
|
||||||
|
def folders(self) -> Tuple[str, List[bytes]]: ...
|
||||||
79
imbox/messages.py
Normal file
79
imbox/messages.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from imbox.query import build_search_query
|
||||||
|
from imbox.parser import fetch_email_by_uid
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Messages:
|
||||||
|
|
||||||
|
IMAP_ATTRIBUTE_LOOKUP = {
|
||||||
|
'unread': '(UNSEEN)',
|
||||||
|
'flagged': '(FLAGGED)',
|
||||||
|
'unflagged': '(UNFLAGGED)',
|
||||||
|
'sent_from': '(FROM "{}")',
|
||||||
|
'sent_to': '(TO "{}")',
|
||||||
|
'date__gt': '(SINCE "{}")',
|
||||||
|
'date__lt': '(BEFORE "{}")',
|
||||||
|
'date__on': '(ON "{}")',
|
||||||
|
'subject': '(SUBJECT "{}")',
|
||||||
|
'uid__range': '(UID {})',
|
||||||
|
}
|
||||||
|
|
||||||
|
FOLDER_LOOKUP = {}
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
connection,
|
||||||
|
parser_policy,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
self.connection = connection
|
||||||
|
self.parser_policy = parser_policy
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self._uid_list = self._query_uids(**kwargs)
|
||||||
|
|
||||||
|
logger.debug("Fetch all messages for UID in {}".format(self._uid_list))
|
||||||
|
|
||||||
|
def _fetch_email(self, uid):
|
||||||
|
return fetch_email_by_uid(uid=uid,
|
||||||
|
connection=self.connection,
|
||||||
|
parser_policy=self.parser_policy)
|
||||||
|
|
||||||
|
def _query_uids(self, **kwargs):
|
||||||
|
query_ = build_search_query(self.IMAP_ATTRIBUTE_LOOKUP, **kwargs)
|
||||||
|
_, data = self.connection.uid('search', None, query_)
|
||||||
|
if data[0] is None:
|
||||||
|
return []
|
||||||
|
return data[0].split()
|
||||||
|
|
||||||
|
def _fetch_email_list(self):
|
||||||
|
for uid in self._uid_list:
|
||||||
|
yield uid, self._fetch_email(uid)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if len(self.kwargs) > 0:
|
||||||
|
return 'Messages({})'.format('\n'.join('{}={}'.format(key, value)
|
||||||
|
for key, value in self.kwargs.items()))
|
||||||
|
return 'Messages(ALL)'
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._fetch_email_list()
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._uid_list)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
uids = self._uid_list[index]
|
||||||
|
|
||||||
|
if not isinstance(uids, list):
|
||||||
|
uid = uids
|
||||||
|
return uid, self._fetch_email(uid)
|
||||||
|
|
||||||
|
return [(uid, self._fetch_email(uid))
|
||||||
|
for uid in uids]
|
||||||
28
imbox/messages.pyi
Normal file
28
imbox/messages.pyi
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import datetime
|
||||||
|
from email._policybase import Policy
|
||||||
|
from imaplib import IMAP4, IMAP4_SSL
|
||||||
|
from typing import Union, List, Generator, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class Messages:
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
connection: Union[IMAP4, IMAP4_SSL],
|
||||||
|
parser_policy: Policy,
|
||||||
|
**kwargs: Union[bool, str, datetime.date]) -> None: ...
|
||||||
|
|
||||||
|
def _fetch_email(self, uid: bytes) -> 'Struct': ...
|
||||||
|
|
||||||
|
def _query_uids(self, **kwargs: Union[bool, str, datetime.date]) -> List[bytes]: ...
|
||||||
|
|
||||||
|
def _fetch_email_list(self) -> Generator[Tuple[bytes, 'Struct']]: ...
|
||||||
|
|
||||||
|
def __repr__(self) -> str: ...
|
||||||
|
|
||||||
|
def __iter__(self) -> Generator[Tuple[bytes, 'Struct']]: ...
|
||||||
|
|
||||||
|
def __next__(self) -> 'Messages': ...
|
||||||
|
|
||||||
|
def __len__(self) -> int: ...
|
||||||
|
|
||||||
|
def __getitem__(self, index) -> Union['Struct', List['Struct']]: ...
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
|
import imaplib
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
import email
|
import email
|
||||||
import base64
|
import base64
|
||||||
import quopri
|
import quopri
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.header import decode_header
|
from email.header import decode_header
|
||||||
from imbox.utils import str_encode, str_decode
|
from imbox.utils import str_encode, str_decode
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +36,10 @@ def decode_mail_header(value, default_charset='us-ascii'):
|
|||||||
return str_decode(str_encode(value, default_charset, 'replace'), default_charset)
|
return str_decode(str_encode(value, default_charset, 'replace'), default_charset)
|
||||||
else:
|
else:
|
||||||
for index, (text, charset) in enumerate(headers):
|
for index, (text, charset) in enumerate(headers):
|
||||||
logger.debug("Mail header no. {}: {} encoding {}".format(index, str_decode(text, charset or 'utf-8'), charset))
|
logger.debug("Mail header no. {index}: {data} encoding {charset}".format(
|
||||||
|
index=index,
|
||||||
|
data=str_decode(text, charset or 'utf-8', 'replace'),
|
||||||
|
charset=charset))
|
||||||
try:
|
try:
|
||||||
headers[index] = str_decode(text, charset or default_charset,
|
headers[index] = str_decode(text, charset or default_charset,
|
||||||
'replace')
|
'replace')
|
||||||
@@ -54,7 +60,8 @@ def get_mail_addresses(message, header_name):
|
|||||||
for index, (address_name, address_email) in enumerate(addresses):
|
for index, (address_name, address_email) in enumerate(addresses):
|
||||||
addresses[index] = {'name': decode_mail_header(address_name),
|
addresses[index] = {'name': decode_mail_header(address_name),
|
||||||
'email': address_email}
|
'email': address_email}
|
||||||
logger.debug("{} Mail addressees in message: <{}> {}".format(header_name.upper(), address_name, address_email))
|
logger.debug("{} Mail address in message: <{}> {}".format(
|
||||||
|
header_name.upper(), address_name, address_email))
|
||||||
return addresses
|
return addresses
|
||||||
|
|
||||||
|
|
||||||
@@ -63,13 +70,13 @@ def decode_param(param):
|
|||||||
values = v.split('\n')
|
values = v.split('\n')
|
||||||
value_results = []
|
value_results = []
|
||||||
for value in values:
|
for value in values:
|
||||||
match = re.search(r'=\?((?:\w|-)+)\?(Q|B)\?(.+)\?=', value)
|
match = re.search(r'=\?((?:\w|-)+)\?([QB])\?(.+)\?=', value)
|
||||||
if match:
|
if match:
|
||||||
encoding, type_, code = match.groups()
|
encoding, type_, code = match.groups()
|
||||||
if type_ == 'Q':
|
if type_ == 'Q':
|
||||||
value = quopri.decodestring(code)
|
value = quopri.decodestring(code)
|
||||||
elif type_ == 'B':
|
elif type_ == 'B':
|
||||||
value = base64.decodestring(code)
|
value = base64.decodebytes(code.encode())
|
||||||
value = str_encode(value, encoding)
|
value = str_encode(value, encoding)
|
||||||
value_results.append(value)
|
value_results.append(value)
|
||||||
if value_results:
|
if value_results:
|
||||||
@@ -82,7 +89,11 @@ def parse_attachment(message_part):
|
|||||||
# Check again if this is a valid attachment
|
# Check again if this is a valid attachment
|
||||||
content_disposition = message_part.get("Content-Disposition", None)
|
content_disposition = message_part.get("Content-Disposition", None)
|
||||||
if content_disposition is not None and not message_part.is_multipart():
|
if content_disposition is not None and not message_part.is_multipart():
|
||||||
dispositions = content_disposition.strip().split(";")
|
dispositions = [
|
||||||
|
disposition.strip()
|
||||||
|
for disposition in content_disposition.split(";")
|
||||||
|
if disposition.strip()
|
||||||
|
]
|
||||||
|
|
||||||
if dispositions[0].lower() in ["attachment", "inline"]:
|
if dispositions[0].lower() in ["attachment", "inline"]:
|
||||||
file_data = message_part.get_payload(decode=True)
|
file_data = message_part.get_payload(decode=True)
|
||||||
@@ -97,10 +108,12 @@ def parse_attachment(message_part):
|
|||||||
attachment['filename'] = filename
|
attachment['filename'] = filename
|
||||||
|
|
||||||
for param in dispositions[1:]:
|
for param in dispositions[1:]:
|
||||||
|
if param:
|
||||||
name, value = decode_param(param)
|
name, value = decode_param(param)
|
||||||
|
|
||||||
if 'file' in name:
|
if 'file' in name:
|
||||||
attachment['filename'] = value
|
attachment['filename'] = value[1:-
|
||||||
|
1] if value.startswith('"') else value
|
||||||
|
|
||||||
if 'create-date' in name:
|
if 'create-date' in name:
|
||||||
attachment['create-date'] = value
|
attachment['create-date'] = value
|
||||||
@@ -115,26 +128,50 @@ def decode_content(message):
|
|||||||
charset = message.get_content_charset('utf-8')
|
charset = message.get_content_charset('utf-8')
|
||||||
try:
|
try:
|
||||||
return content.decode(charset, 'ignore')
|
return content.decode(charset, 'ignore')
|
||||||
|
except LookupError:
|
||||||
|
return content.decode(charset.replace("-", ""), 'ignore')
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_email_by_uid(uid, connection, parser_policy):
|
||||||
|
message, data = connection.uid('fetch', uid, '(BODY.PEEK[] FLAGS)')
|
||||||
|
logger.debug("Fetched message for UID {}".format(int(uid)))
|
||||||
|
|
||||||
|
raw_headers, raw_email = data[0]
|
||||||
|
|
||||||
|
email_object = parse_email(raw_email, policy=parser_policy)
|
||||||
|
flags = parse_flags(raw_headers.decode())
|
||||||
|
email_object.__dict__['flags'] = flags
|
||||||
|
|
||||||
|
return email_object
|
||||||
|
|
||||||
|
|
||||||
|
def parse_flags(headers):
|
||||||
|
"""Copied from https://github.com/girishramnani/gmail/blob/master/gmail/message.py"""
|
||||||
|
if len(headers) == 0:
|
||||||
|
return []
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
headers = bytes(headers, "ascii")
|
||||||
|
return list(imaplib.ParseFlags(headers))
|
||||||
|
|
||||||
|
|
||||||
def parse_email(raw_email, policy=None):
|
def parse_email(raw_email, policy=None):
|
||||||
if isinstance(raw_email, bytes):
|
if isinstance(raw_email, bytes):
|
||||||
raw_email = str_encode(raw_email, 'utf-8')
|
raw_email = str_encode(raw_email, 'utf-8', errors='ignore')
|
||||||
if policy is not None:
|
if policy is not None:
|
||||||
email_parse_kwargs = dict(policy=policy)
|
email_parse_kwargs = dict(policy=policy)
|
||||||
else:
|
else:
|
||||||
email_parse_kwargs = {}
|
email_parse_kwargs = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
email_message = email.message_from_string(raw_email, **email_parse_kwargs)
|
email_message = email.message_from_string(
|
||||||
|
raw_email, **email_parse_kwargs)
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
email_message = email.message_from_string(raw_email.encode('utf-8'), **email_parse_kwargs)
|
email_message = email.message_from_string(
|
||||||
|
raw_email.encode('utf-8'), **email_parse_kwargs)
|
||||||
maintype = email_message.get_content_maintype()
|
maintype = email_message.get_content_maintype()
|
||||||
parsed_email = {}
|
parsed_email = {'raw_email': raw_email}
|
||||||
|
|
||||||
parsed_email['raw_email'] = raw_email
|
|
||||||
|
|
||||||
body = {
|
body = {
|
||||||
"plain": [],
|
"plain": [],
|
||||||
@@ -154,7 +191,7 @@ def parse_email(raw_email, policy=None):
|
|||||||
content = decode_content(part)
|
content = decode_content(part)
|
||||||
|
|
||||||
is_inline = content_disposition is None \
|
is_inline = content_disposition is None \
|
||||||
or content_disposition == "inline"
|
or content_disposition.startswith("inline")
|
||||||
if content_type == "text/plain" and is_inline:
|
if content_type == "text/plain" and is_inline:
|
||||||
body['plain'].append(content)
|
body['plain'].append(content)
|
||||||
elif content_type == "text/html" and is_inline:
|
elif content_type == "text/html" and is_inline:
|
||||||
|
|||||||
34
imbox/parser.pyi
Normal file
34
imbox/parser.pyi
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import datetime
|
||||||
|
from email._policybase import Policy
|
||||||
|
from email.message import Message
|
||||||
|
from imaplib import IMAP4_SSL
|
||||||
|
import io
|
||||||
|
from typing import Union, Dict, List, KeysView, Tuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Struct:
|
||||||
|
def __init__(self, **entries: Union[
|
||||||
|
str, datetime.datetime, Dict[str, str], list, List[Dict[str, str]]
|
||||||
|
]) -> None: ...
|
||||||
|
|
||||||
|
def keys(self) -> KeysView: ...
|
||||||
|
|
||||||
|
def __repr__(self) -> str: ...
|
||||||
|
|
||||||
|
def decode_mail_header(value: str, default_charset: str) -> str: ...
|
||||||
|
|
||||||
|
def get_mail_addresses(message: Message, header_name: str) -> List[Dict[str, str]]: ...
|
||||||
|
|
||||||
|
def decode_param(param: str) -> Tuple[str, str]: ...
|
||||||
|
|
||||||
|
def parse_attachment(message_part: Message) -> Optional[Dict[str, Union[int, str, io.BytesIO]]]: ...
|
||||||
|
|
||||||
|
def decode_content(message: Message) -> str: ...
|
||||||
|
|
||||||
|
def fetch_email_by_uid(uid: bytes, connection: IMAP4_SSL, parser_policy: Optional[Policy]) -> Struct: ...
|
||||||
|
raw_headers: bytes
|
||||||
|
raw_email: bytes
|
||||||
|
|
||||||
|
def parse_flags(headers: str) -> Union[list, List[bytes]]: ...
|
||||||
|
|
||||||
|
def parse_email(raw_email: bytes, policy: Optional[Policy]) -> Struct: ...
|
||||||
@@ -1,55 +1,17 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
|
||||||
# TODO - Validate query arguments
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
IMAP_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
||||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
||||||
|
|
||||||
|
|
||||||
def format_date(date):
|
def build_search_query(imap_attribute_lookup, **kwargs):
|
||||||
|
|
||||||
return "%s-%s-%s" % (date.day, IMAP_MONTHS[date.month - 1], date.year)
|
|
||||||
|
|
||||||
|
|
||||||
def build_search_query(**kwargs):
|
|
||||||
|
|
||||||
# Parse keyword arguments
|
|
||||||
unread = kwargs.get('unread', False)
|
|
||||||
sent_from = kwargs.get('sent_from', False)
|
|
||||||
sent_to = kwargs.get('sent_to', False)
|
|
||||||
date__gt = kwargs.get('date__gt', False)
|
|
||||||
if type(date__gt) is datetime.date:
|
|
||||||
date__gt = format_date(date__gt)
|
|
||||||
date__lt = kwargs.get('date__lt', False)
|
|
||||||
if type(date__lt) is datetime.date:
|
|
||||||
date__lt = format_date(date__lt)
|
|
||||||
subject = kwargs.get('subject')
|
|
||||||
|
|
||||||
query = []
|
query = []
|
||||||
|
for name, value in kwargs.items():
|
||||||
if unread:
|
if value is not None:
|
||||||
query.append("(UNSEEN)")
|
if isinstance(value, datetime.date):
|
||||||
|
value = value.strftime('%d-%b-%Y')
|
||||||
if sent_from:
|
if isinstance(value, str) and '"' in value:
|
||||||
query.append('(FROM "%s")' % sent_from)
|
value = value.replace('"', "'")
|
||||||
|
query.append(imap_attribute_lookup[name].format(value))
|
||||||
if sent_to:
|
|
||||||
query.append('(TO "%s")' % sent_to)
|
|
||||||
|
|
||||||
if date__gt:
|
|
||||||
query.append('(SINCE "%s")' % date__gt)
|
|
||||||
|
|
||||||
if date__lt:
|
|
||||||
query.append('(BEFORE "%s")' % date__lt)
|
|
||||||
|
|
||||||
if subject is not None:
|
|
||||||
query.append('(SUBJECT "%s")' % subject)
|
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
logger.debug("IMAP query: {}".format(" ".join(query)))
|
|
||||||
return " ".join(query)
|
return " ".join(query)
|
||||||
|
|
||||||
logger.debug("IMAP query: {}".format("(ALL)"))
|
|
||||||
return "(ALL)"
|
return "(ALL)"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def str_encode(value='', encoding=None, errors='strict'):
|
def str_encode(value='', encoding=None, errors='strict'):
|
||||||
logger.debug("Encode str {} with and errors {}".format(value, encoding, errors))
|
logger.debug("Encode str {} with and errors {}".format(value, encoding, errors))
|
||||||
return str(value, encoding, errors)
|
return str(value, encoding, errors)
|
||||||
|
|
||||||
|
|
||||||
def str_decode(value='', encoding=None, errors='strict'):
|
def str_decode(value='', encoding=None, errors='strict'):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return bytes(value, encoding, errors).decode('utf-8')
|
return bytes(value, encoding, errors).decode('utf-8')
|
||||||
elif isinstance(value, bytes):
|
elif isinstance(value, bytes):
|
||||||
return value.decode(encoding or 'utf-8', errors=errors)
|
return value.decode(encoding or 'utf-8', errors=errors)
|
||||||
else:
|
else:
|
||||||
raise TypeError( "Cannot decode '{}' object".format(value.__class__) )
|
raise TypeError("Cannot decode '{}' object".format(value.__class__))
|
||||||
|
|||||||
6
imbox/utils.pyi
Normal file
6
imbox/utils.pyi
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
def str_encode(value: Union[str, bytes], encoding: Optional[str], errors: str) -> str: ...
|
||||||
|
|
||||||
|
def str_decode(value: Union[str, bytes], encoding: Optional[str], errors: str) -> Union[str, bytes]: ...
|
||||||
11
imbox/vendors/__init__.py
vendored
Normal file
11
imbox/vendors/__init__.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from imbox.vendors.gmail import GmailMessages
|
||||||
|
|
||||||
|
vendors = [GmailMessages]
|
||||||
|
|
||||||
|
hostname_vendorname_dict = {vendor.hostname: vendor.name for vendor in vendors}
|
||||||
|
name_authentication_string_dict = {vendor.name: vendor.authentication_error_message for vendor in vendors}
|
||||||
|
|
||||||
|
__all__ = [v.__name__ for v in vendors]
|
||||||
|
|
||||||
|
__all__ += ['hostname_vendorname_dict',
|
||||||
|
'name_authentication_string_dict']
|
||||||
9
imbox/vendors/__init__.pyi
vendored
Normal file
9
imbox/vendors/__init__.pyi
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
|
from imbox.messages import Messages
|
||||||
|
|
||||||
|
vendors: List[Messages]
|
||||||
|
|
||||||
|
hostname_vendorname_dict: Dict[str, str]
|
||||||
|
name_authentication_string_dict: Dict[str, Optional[str]]
|
||||||
|
|
||||||
39
imbox/vendors/gmail.py
vendored
Normal file
39
imbox/vendors/gmail.py
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from imbox.messages import Messages
|
||||||
|
from imbox.vendors.helpers import merge_two_dicts
|
||||||
|
|
||||||
|
|
||||||
|
class GmailMessages(Messages):
|
||||||
|
authentication_error_message = ('If you\'re not using an app-specific password, grab one here: '
|
||||||
|
'https://myaccount.google.com/apppasswords')
|
||||||
|
hostname = 'imap.gmail.com'
|
||||||
|
name = 'gmail'
|
||||||
|
FOLDER_LOOKUP = {
|
||||||
|
|
||||||
|
'all_mail': '"[Gmail]/All Mail"',
|
||||||
|
'all': '"[Gmail]/All Mail"',
|
||||||
|
'all mail': '"[Gmail]/All Mail"',
|
||||||
|
'sent': '"[Gmail]/Sent Mail"',
|
||||||
|
'sent mail': '"[Gmail]/Sent Mail"',
|
||||||
|
'sent_mail': '"[Gmail]/Sent Mail"',
|
||||||
|
'drafts': '"[Gmail]/Drafts"',
|
||||||
|
'important': '"[Gmail]/Important"',
|
||||||
|
'spam': '"[Gmail]/Spam"',
|
||||||
|
'starred': '"[Gmail]/Starred"',
|
||||||
|
'trash': '"[Gmail]/Trash"',
|
||||||
|
}
|
||||||
|
|
||||||
|
GMAIL_IMAP_ATTRIBUTE_LOOKUP_DIFF = {
|
||||||
|
'subject': '(X-GM-RAW "subject:\'{}\'")',
|
||||||
|
'label': '(X-GM-LABELS "{}")',
|
||||||
|
'raw': '(X-GM-RAW "{}")'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
connection,
|
||||||
|
parser_policy,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
self.IMAP_ATTRIBUTE_LOOKUP = merge_two_dicts(self.IMAP_ATTRIBUTE_LOOKUP,
|
||||||
|
self.GMAIL_IMAP_ATTRIBUTE_LOOKUP_DIFF)
|
||||||
|
|
||||||
|
super().__init__(connection, parser_policy, **kwargs)
|
||||||
14
imbox/vendors/gmail.pyi
vendored
Normal file
14
imbox/vendors/gmail.pyi
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import datetime
|
||||||
|
from email._policybase import Policy
|
||||||
|
from imaplib import IMAP4, IMAP4_SSL
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from imbox.messages import Messages
|
||||||
|
|
||||||
|
|
||||||
|
class GmailMessages(Messages):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
connection: Union[IMAP4, IMAP4_SSL],
|
||||||
|
parser_policy: Policy,
|
||||||
|
**kwargs: Union[bool, str, datetime.date]) -> None: ...
|
||||||
6
imbox/vendors/helpers.py
vendored
Normal file
6
imbox/vendors/helpers.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
def merge_two_dicts(x, y):
|
||||||
|
"""from https://stackoverflow.com/a/26853961/4386191"""
|
||||||
|
z = x.copy() # start with x's keys and values
|
||||||
|
z.update(y) # modifies z with y's keys and values & returns None
|
||||||
|
return z
|
||||||
11
setup.py
11
setup.py
@@ -1,7 +1,9 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
import os
|
import os
|
||||||
|
|
||||||
version = '0.9'
|
import imbox
|
||||||
|
|
||||||
|
version = imbox.__version__
|
||||||
|
|
||||||
|
|
||||||
def read(filename):
|
def read(filename):
|
||||||
@@ -17,15 +19,16 @@ setup(
|
|||||||
author_email='martin@amon.cx',
|
author_email='martin@amon.cx',
|
||||||
url='https://github.com/martinrusev/imbox',
|
url='https://github.com/martinrusev/imbox',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
packages=['imbox'],
|
packages=['imbox', 'imbox.vendors'],
|
||||||
package_dir={'imbox': 'imbox'},
|
package_dir={'imbox': 'imbox'},
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
classifiers=(
|
classifiers=(
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3.2',
|
|
||||||
'Programming Language :: Python :: 3.3',
|
'Programming Language :: Python :: 3.3',
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6'
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
),
|
),
|
||||||
|
test_suite='tests',
|
||||||
)
|
)
|
||||||
|
|||||||
22
tests/8422.msg
Normal file
22
tests/8422.msg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Delivered-To: receiver@example.com
|
||||||
|
Return-Path: <sender@example.com>
|
||||||
|
Date: Thu, 20 Jul 2017 07:34:22 -0500
|
||||||
|
Message-ID: <59705CFE.A95F.0016.0@journeys.com>
|
||||||
|
Subject: Following up Re: Looking to connect, let's schedule a call!
|
||||||
|
From: sender@example.com
|
||||||
|
To: "Receiver" <receiver@example.com>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed; boundary="=__PartBD85995F.0__="
|
||||||
|
|
||||||
|
This is a MIME message. If you are reading this text, you may want to
|
||||||
|
consider changing to a mail reader or gateway that understands how to
|
||||||
|
properly handle MIME multipart messages.
|
||||||
|
|
||||||
|
--=__PartBD85995F.0__=
|
||||||
|
Content-Type: multipart/alternative; boundary="=__PartBD85995F.1__="
|
||||||
|
|
||||||
|
--=__PartBD85995F.1__=
|
||||||
|
Content-Type: text/plain; charset=Windows-1252
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Following up on my previous message. I’d love to connect you with
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from imbox.parser import *
|
from imbox.parser import *
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
if sys.version_info.minor < 3:
|
if sys.version_info.minor < 3:
|
||||||
SMTP = False
|
SMTP = False
|
||||||
@@ -8,6 +9,9 @@ else:
|
|||||||
from email.policy import SMTP
|
from email.policy import SMTP
|
||||||
|
|
||||||
|
|
||||||
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
raw_email = """Delivered-To: johndoe@gmail.com
|
raw_email = """Delivered-To: johndoe@gmail.com
|
||||||
X-Originating-Email: [martin@amon.cx]
|
X-Originating-Email: [martin@amon.cx]
|
||||||
Message-ID: <test0@example.com>
|
Message-ID: <test0@example.com>
|
||||||
@@ -80,6 +84,235 @@ Content-Transfer-Encoding: quoted-printable
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
raw_email_encoded_multipart = b"""Delivered-To: receiver@example.com
|
||||||
|
Return-Path: <kkoudelka@wallvet.com>
|
||||||
|
Date: Tue, 08 Aug 2017 08:15:11 -0700
|
||||||
|
From: <kkoudelka@wallvet.com>
|
||||||
|
To: interviews+347243@gethappie.me
|
||||||
|
Message-Id: <20170808081511.2b876c018dd94666bcc18e28cf079afb.99766f164b.wbe@email24.godaddy.com>
|
||||||
|
Subject: RE: Kari, are you open to this?
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/related;
|
||||||
|
boundary="=_7c18e0b95b772890a22ed6c0f810a434"
|
||||||
|
|
||||||
|
--=_7c18e0b95b772890a22ed6c0f810a434
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Type: text/html; charset="utf-8"
|
||||||
|
|
||||||
|
<html><body><span style=3D"font-family:Verdana; color:#000; font-size:10pt;=
|
||||||
|
"><div>Hi Richie,</div></span></body></html>
|
||||||
|
--=_7c18e0b95b772890a22ed6c0f810a434
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Type: image/jpeg; charset=binary;
|
||||||
|
name="sigimg0";
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename="sigimg0";
|
||||||
|
|
||||||
|
/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
|
||||||
|
jt0JaKhjm3xq23GR60UuZBZn/9k=
|
||||||
|
--=_7c18e0b95b772890a22ed6c0f810a434
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Type: image/jpeg; charset=binary;
|
||||||
|
name="sigimg1";
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename="sigimg1";
|
||||||
|
|
||||||
|
/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
|
||||||
|
SlBFRyB2NjIpLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMP
|
||||||
|
ooooA//Z
|
||||||
|
--=_7c18e0b95b772890a22ed6c0f810a434--
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
raw_email_encoded_bad_multipart = b"""Delivered-To: receiver@example.com
|
||||||
|
Return-Path: <sender@example.com>
|
||||||
|
From: sender@example.com
|
||||||
|
To: "Receiver" <receiver@example.com>, "Second\r\n Receiver" <recipient@example.com>
|
||||||
|
Subject: Re: Looking to connect with you...
|
||||||
|
Date: Thu, 20 Apr 2017 15:32:52 +0000
|
||||||
|
Message-ID: <BN6PR16MB179579288933D60C4016D078C31B0@BN6PR16MB1795.namprd16.prod.outlook.com>
|
||||||
|
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_--
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
raw_email_encoded_another_bad_multipart = b"""Delivered-To: receiver@example.com
|
||||||
|
Return-Path: <sender@example.com>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Date: Wed, 22 Mar 2017 15:21:55 -0500
|
||||||
|
Message-ID: <58D29693.192A.0075.1@wimort.com>
|
||||||
|
Subject: Re: Reaching Out About Peoples Home Equity
|
||||||
|
From: sender@example.com
|
||||||
|
To: receiver@example.com
|
||||||
|
Content-Type: multipart/alternative; boundary="____NOIBTUQXSYRVOOAFLCHY____"
|
||||||
|
|
||||||
|
|
||||||
|
--____NOIBTUQXSYRVOOAFLCHY____
|
||||||
|
Content-Type: text/plain; charset=iso-8859-15
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Disposition: inline;
|
||||||
|
modification-date="Wed, 22 Mar 2017 15:21:55 -0500"
|
||||||
|
|
||||||
|
Chloe,
|
||||||
|
|
||||||
|
--____NOIBTUQXSYRVOOAFLCHY____
|
||||||
|
Content-Type: multipart/related; boundary="____XTSWHCFJMONXSVGPVDLY____"
|
||||||
|
|
||||||
|
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____
|
||||||
|
Content-Type: text/html; charset=iso-8859-15
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Disposition: inline;
|
||||||
|
modification-date="Wed, 22 Mar 2017 15:21:55 -0500"
|
||||||
|
|
||||||
|
<HTML xmlns=3D"http://www.w3.org/1999/xhtml">
|
||||||
|
<BODY style=3D"COLOR: black; FONT: 10pt Segoe UI; MARGIN: 4px 4px 1px" =
|
||||||
|
leftMargin=3D0 topMargin=3D0 offset=3D"0" marginwidth=3D"0" marginheight=3D=
|
||||||
|
"0">
|
||||||
|
<DIV>Chloe,</DIV>
|
||||||
|
<IMG src=3D"cid:VFXVGHA=
|
||||||
|
GXNMI.36b3148cbf284ba18d35bdd8386ac266" width=3D1 height=3D1> </BODY></HTML=
|
||||||
|
>
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____
|
||||||
|
Content-ID: <TLUACRGXVUBY.IMAGE_3.gif>
|
||||||
|
Content-Type: image/gif
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
R0lGODlhHgHCAPf/AIOPr9GvT7SFcZZjVTEuMLS1tZKUlJN0Znp4eEA7PV1aWvz8+8V6Zl1BNYxX
|
||||||
|
HvOZ1/zmOd95agUEADs=
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____
|
||||||
|
Content-ID: <VFXVGHAGXNMI.36b3148cbf284ba18d35bdd8386ac266>
|
||||||
|
Content-Type: image/xxx
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____--
|
||||||
|
|
||||||
|
--____NOIBTUQXSYRVOOAFLCHY____--
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
raw_email_with_trailing_semicolon_to_disposition_content = b"""Delivered-To: receiver@example.com
|
||||||
|
Return-Path: <sender@example.com>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Date: Wed, 22 Mar 2017 15:21:55 -0500
|
||||||
|
Message-ID: <58D29693.192A.0075.1@wimort.com>
|
||||||
|
Subject: Re: Reaching Out About Peoples Home Equity
|
||||||
|
From: sender@example.com
|
||||||
|
To: receiver@example.com
|
||||||
|
Content-Type: multipart/alternative; boundary="____NOIBTUQXSYRVOOAFLCHY____"
|
||||||
|
|
||||||
|
|
||||||
|
--____NOIBTUQXSYRVOOAFLCHY____
|
||||||
|
Content-Type: text/plain; charset=iso-8859-15
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Disposition: inline;
|
||||||
|
modification-date="Wed, 22 Mar 2017 15:21:55 -0500"
|
||||||
|
|
||||||
|
Hello Chloe
|
||||||
|
|
||||||
|
--____NOIBTUQXSYRVOOAFLCHY____
|
||||||
|
Content-Type: multipart/related; boundary="____XTSWHCFJMONXSVGPVDLY____"
|
||||||
|
|
||||||
|
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____
|
||||||
|
Content-Type: text/html; charset=iso-8859-15
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Disposition: inline;
|
||||||
|
modification-date="Wed, 22 Mar 2017 15:21:55 -0500"
|
||||||
|
|
||||||
|
<HTML xmlns=3D"http://www.w3.org/1999/xhtml">
|
||||||
|
<BODY>
|
||||||
|
<DIV>Hello Chloe</DIV>
|
||||||
|
</BODY>
|
||||||
|
</HTML>
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____
|
||||||
|
Content-Type: application/octet-stream; name="abc.xyz"
|
||||||
|
Content-Description: abc.xyz
|
||||||
|
Content-Disposition: attachment; filename="abc.xyz";
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
R0lGODlhHgHCAPf/AIOPr9GvT7SFcZZjVTEuMLS1tZKUlJN0Znp4eEA7PV1aWvz8+8V6Zl1BNYxX
|
||||||
|
HvOZ1/zmOd95agUEADs=
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____
|
||||||
|
Content-ID: <VFXVGHAGXNMI.36b3148cbf284ba18d35bdd8386ac266>
|
||||||
|
Content-Type: image/xxx
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
|
||||||
|
--____XTSWHCFJMONXSVGPVDLY____--
|
||||||
|
|
||||||
|
--____NOIBTUQXSYRVOOAFLCHY____--
|
||||||
|
"""
|
||||||
|
|
||||||
|
raw_email_encoded_encoding_charset_contains_a_minus = b"""Delivered-To: <receiver@example.org>
|
||||||
|
Return-Path: <sender@example.org>
|
||||||
|
Message-ID: <74836CF6FF9B1965927DE7EE8A087483@NXOFGRQFQW2>
|
||||||
|
From: <sender@example.org>
|
||||||
|
To: <sender@example.org>
|
||||||
|
Subject: Salut, mon cher.
|
||||||
|
Date: 30 May 2018 22:47:37 +0200
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="----=_NextPart_000_0038_01D3F85C.02934C4A"
|
||||||
|
|
||||||
|
------=_NextPart_000_0038_01D3F85C.02934C4A
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset="cp-850"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
spam here
|
||||||
|
|
||||||
|
|
||||||
|
cliquez ici
|
||||||
|
------=_NextPart_000_0038_01D3F85C.02934C4A
|
||||||
|
Content-Type: text/html;
|
||||||
|
charset="cp-850"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||||
|
<HTML><HEAD>
|
||||||
|
<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dcp-850">
|
||||||
|
<META content=3D"MSHTML 6.00.2900.2456" name=3DGENERATOR>
|
||||||
|
<STYLE></STYLE>
|
||||||
|
</HEAD>
|
||||||
|
<BODY bgColor=3D#ffffff>
|
||||||
|
spam here<br>
|
||||||
|
<br>
|
||||||
|
<a href=3D"http://spammer-url"><b>cliquez =
|
||||||
|
ici</b></a></br></BODY></HTML>
|
||||||
|
------=_NextPart_000_0038_01D3F85C.02934C4A--
|
||||||
|
"""
|
||||||
|
|
||||||
class TestParser(unittest.TestCase):
|
class TestParser(unittest.TestCase):
|
||||||
|
|
||||||
def test_parse_email(self):
|
def test_parse_email(self):
|
||||||
@@ -96,16 +329,46 @@ class TestParser(unittest.TestCase):
|
|||||||
self.assertEqual('Выписка по карте', parsed_email.subject)
|
self.assertEqual('Выписка по карте', parsed_email.subject)
|
||||||
self.assertEqual('Выписка по карте 1234', parsed_email.body['html'][0])
|
self.assertEqual('Выписка по карте 1234', parsed_email.body['html'][0])
|
||||||
|
|
||||||
|
def test_parse_email_invalid_unicode(self):
|
||||||
|
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_inline_body(self):
|
||||||
|
parsed_email = parse_email(raw_email_encoded_another_bad_multipart)
|
||||||
|
self.assertEqual("Re: Reaching Out About Peoples Home Equity", parsed_email.subject)
|
||||||
|
self.assertTrue(parsed_email.body['plain'])
|
||||||
|
self.assertTrue(parsed_email.body['html'])
|
||||||
|
|
||||||
|
def test_parse_email_multipart(self):
|
||||||
|
parsed_email = parse_email(raw_email_encoded_multipart)
|
||||||
|
self.assertEqual("RE: Kari, are you open to this?", 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):
|
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)
|
||||||
self.assertEqual('one', parse_email('Message-Id: one').message_id)
|
self.assertEqual('one', parse_email('Message-Id: one').message_id)
|
||||||
self.assertEqual('one', parse_email('Message-id: one').message_id)
|
self.assertEqual('one', parse_email('Message-id: one').message_id)
|
||||||
self.assertEqual('one', parse_email('message-id: one').message_id)
|
self.assertEqual('one', parse_email('message-id: one').message_id)
|
||||||
|
|
||||||
# TODO - Complete the test suite
|
|
||||||
def test_parse_attachment(self):
|
def test_parse_attachment(self):
|
||||||
pass
|
parsed_email = parse_email(raw_email_with_trailing_semicolon_to_disposition_content)
|
||||||
|
self.assertEqual(1, len(parsed_email.attachments))
|
||||||
|
attachment = parsed_email.attachments[0]
|
||||||
|
self.assertEqual('application/octet-stream', attachment['content-type'])
|
||||||
|
self.assertEqual(71, attachment['size'])
|
||||||
|
self.assertEqual('abc.xyz', attachment['filename'])
|
||||||
|
self.assertTrue(attachment['content'])
|
||||||
|
|
||||||
|
def test_parse_email_accept_if_declared_charset_contains_a_minus_character(self):
|
||||||
|
parsed_email = parse_email(raw_email_encoded_encoding_charset_contains_a_minus)
|
||||||
|
self.assertEqual("Salut, mon cher.", parsed_email.subject)
|
||||||
|
self.assertTrue(parsed_email.body['plain'])
|
||||||
|
self.assertTrue(parsed_email.body['html'])
|
||||||
|
|
||||||
|
# TODO - Complete the test suite
|
||||||
def test_decode_mail_header(self):
|
def test_decode_mail_header(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -117,6 +380,9 @@ class TestParser(unittest.TestCase):
|
|||||||
from_message_object = email.message_from_string("From: John Smith <johnsmith@gmail.com>")
|
from_message_object = email.message_from_string("From: John Smith <johnsmith@gmail.com>")
|
||||||
self.assertEqual([{'email': 'johnsmith@gmail.com', 'name': 'John Smith'}], get_mail_addresses(from_message_object, 'from'))
|
self.assertEqual([{'email': 'johnsmith@gmail.com', 'name': 'John Smith'}], get_mail_addresses(from_message_object, 'from'))
|
||||||
|
|
||||||
|
invalid_encoding_in_from_message_object = email.message_from_string("From: =?UTF-8?Q?C=E4cilia?= <caciliahxg827m@example.org>")
|
||||||
|
self.assertEqual([{'email': 'caciliahxg827m@example.org', 'name': 'C<EFBFBD>cilia'}], get_mail_addresses(invalid_encoding_in_from_message_object, 'from'))
|
||||||
|
|
||||||
def test_parse_email_with_policy(self):
|
def test_parse_email_with_policy(self):
|
||||||
if not SMTP:
|
if not SMTP:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,36 +1,83 @@
|
|||||||
import unittest
|
|
||||||
from imbox.query import build_search_query
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from imbox.query import build_search_query
|
||||||
|
from imbox.messages import Messages
|
||||||
|
from imbox.vendors.helpers import merge_two_dicts
|
||||||
|
from imbox.vendors.gmail import GmailMessages
|
||||||
|
|
||||||
|
IMAP_ATTRIBUTE_LOOKUP = Messages.IMAP_ATTRIBUTE_LOOKUP
|
||||||
|
GMAIL_ATTRIBUTE_LOOKUP = merge_two_dicts(IMAP_ATTRIBUTE_LOOKUP,
|
||||||
|
GmailMessages.GMAIL_IMAP_ATTRIBUTE_LOOKUP_DIFF)
|
||||||
|
|
||||||
|
|
||||||
class TestQuery(unittest.TestCase):
|
class TestQuery(unittest.TestCase):
|
||||||
|
|
||||||
def test_all(self):
|
def test_all(self):
|
||||||
|
|
||||||
res = build_search_query()
|
res = build_search_query(IMAP_ATTRIBUTE_LOOKUP)
|
||||||
self.assertEqual(res, "(ALL)")
|
self.assertEqual(res, "(ALL)")
|
||||||
|
|
||||||
|
def test_subject(self):
|
||||||
|
|
||||||
|
res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, subject='hi')
|
||||||
|
self.assertEqual(res, '(SUBJECT "hi")')
|
||||||
|
|
||||||
|
res = build_search_query(GMAIL_ATTRIBUTE_LOOKUP, subject='hi')
|
||||||
|
self.assertEqual(res, '(X-GM-RAW "subject:\'hi\'")')
|
||||||
|
|
||||||
def test_unread(self):
|
def test_unread(self):
|
||||||
|
|
||||||
res = build_search_query(unread=True)
|
res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, unread=True)
|
||||||
self.assertEqual(res, "(UNSEEN)")
|
self.assertEqual(res, "(UNSEEN)")
|
||||||
|
|
||||||
|
def test_unflagged(self):
|
||||||
|
|
||||||
|
res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, unflagged=True)
|
||||||
|
self.assertEqual(res, "(UNFLAGGED)")
|
||||||
|
|
||||||
|
def test_flagged(self):
|
||||||
|
|
||||||
|
res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, flagged=True)
|
||||||
|
self.assertEqual(res, "(FLAGGED)")
|
||||||
|
|
||||||
def test_sent_from(self):
|
def test_sent_from(self):
|
||||||
|
|
||||||
res = build_search_query(sent_from='test@example.com')
|
res = build_search_query(
|
||||||
self.assertEqual(res, "(FROM \"test@example.com\")")
|
IMAP_ATTRIBUTE_LOOKUP, sent_from='test@example.com')
|
||||||
|
self.assertEqual(res, '(FROM "test@example.com")')
|
||||||
|
|
||||||
def test_sent_to(self):
|
def test_sent_to(self):
|
||||||
|
|
||||||
res = build_search_query(sent_to='test@example.com')
|
res = build_search_query(
|
||||||
self.assertEqual(res, "(TO \"test@example.com\")")
|
IMAP_ATTRIBUTE_LOOKUP, sent_to='test@example.com')
|
||||||
|
self.assertEqual(res, '(TO "test@example.com")')
|
||||||
|
|
||||||
def test_date__gt(self):
|
def test_date__gt(self):
|
||||||
|
|
||||||
res = build_search_query(date__gt=date(2014, 12, 31))
|
res = build_search_query(
|
||||||
self.assertEqual(res, "(SINCE \"31-Dec-2014\")")
|
IMAP_ATTRIBUTE_LOOKUP, date__gt=date(2014, 12, 31))
|
||||||
|
self.assertEqual(res, '(SINCE "31-Dec-2014")')
|
||||||
|
|
||||||
def test_date__lt(self):
|
def test_date__lt(self):
|
||||||
|
|
||||||
res = build_search_query(date__lt=date(2014, 1, 1))
|
res = build_search_query(
|
||||||
self.assertEqual(res, "(BEFORE \"1-Jan-2014\")")
|
IMAP_ATTRIBUTE_LOOKUP, date__lt=date(2014, 1, 1))
|
||||||
|
self.assertEqual(res, '(BEFORE "01-Jan-2014")')
|
||||||
|
|
||||||
|
def test_date__on(self):
|
||||||
|
res = build_search_query(
|
||||||
|
IMAP_ATTRIBUTE_LOOKUP, date__on=date(2014, 1, 1))
|
||||||
|
self.assertEqual(res, '(ON "01-Jan-2014")')
|
||||||
|
|
||||||
|
def test_uid__range(self):
|
||||||
|
res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, uid__range='1000:*')
|
||||||
|
self.assertEqual(res, '(UID 1000:*)')
|
||||||
|
|
||||||
|
def test_gmail_raw(self):
|
||||||
|
res = build_search_query(GMAIL_ATTRIBUTE_LOOKUP, raw='has:attachment subject:"hey"')
|
||||||
|
self.assertEqual(res, '(X-GM-RAW "has:attachment subject:\'hey\'")')
|
||||||
|
|
||||||
|
def test_gmail_label(self):
|
||||||
|
res = build_search_query(GMAIL_ATTRIBUTE_LOOKUP, label='finance')
|
||||||
|
self.assertEqual(res, '(X-GM-LABELS "finance")')
|
||||||
|
|||||||
Reference in New Issue
Block a user