65 Commits

Author SHA1 Message Date
12a8cd9a41 added accidentally deleted 'flagged' query option back in, fixed typo uid_range (needed two underscores), updated and filled in query tests. made dictionary combination compatible with < 3.5. 2018-10-18 15:47:20 -04:00
70b0552df4 removed test file 2018-10-18 14:33:05 -04:00
b2f82b6fb7 added support in vendors.gmail for 'label' and 'raw' searches, added these to documentation. 2018-10-18 14:30:53 -04:00
76fc7bfe8c synced with master including #153 supporting search for UID ranges 2018-10-18 13:15:22 -04:00
0f8abf3e4a Merge branch 'vendors' of https://github.com/zevaverbach/imbox into vendors 2018-10-18 13:07:27 -04:00
fee17fbc76 added .DS_Store to gitignore 2018-10-18 13:06:59 -04:00
27d19a209a moved build_search_query to Messages, refactored to use a class attribute of IMAP_ATTRIBUTE_LOOKUP, so that the vendors package can overwrite and add entries to it for, for example, Gmail's IMAP extensions. added X-GM-RAW to GmailMessages' copy of the lookup to make partial subject searches work. 2018-10-18 13:06:59 -04:00
0a94f2e4a4 added .DS_Store to gitignore 2018-10-18 13:01:59 -04:00
810e6a0cd8 moved build_search_query to Messages, refactored to use a class attribute of IMAP_ATTRIBUTE_LOOKUP, so that the vendors package can overwrite and add entries to it for, for example, Gmail's IMAP extensions. added X-GM-RAW to GmailMessages' copy of the lookup to make partial subject searches work. 2018-10-18 13:00:42 -04:00
Martin Rusev
782d3977f9 Merge pull request #153 from SkullTech/query-uid__range
Add query uid__range
2018-09-10 10:10:15 +02:00
Sumit Ghosh
c21f2ec9f7 Edit README mentioning usage of uid__range 2018-09-08 21:57:57 +05:30
Sumit Ghosh
1f0b952fbc Add query uid__range 2018-09-08 21:54:59 +05:30
Martin Rusev
d0b3fa495e Update CHANGELOG.md 2018-08-14 17:30:56 +02:00
Martin Rusev
038094e16d Update version and changelog 2018-08-14 08:24:18 -07:00
Martin Rusev
e1eb91b689 Update CHANGELOG.md 2018-08-10 12:34:53 +02:00
Martin Rusev
dbdc96f91e Merge pull request #147 from sblondon/master
fix: readd the flag Deleted to delete a message
2018-07-31 09:53:04 +02:00
Stephane Blondon
4dc43d2ba2 fix: readd the flag Deleted to delete a message 2018-07-31 13:17:18 +08:00
Martin Rusev
fd35764586 Merge pull request #146 from sblondon/remove_a_print
remove a print() call
2018-07-30 11:15:05 +02:00
Stephane Blondon
acfd162c8a remove a print() call 2018-07-30 16:17:18 +08:00
Martin Rusev
9dbf9a9071 Merge pull request #145 from sblondon/add_python_3.7
Library is compatible with python 3.7
2018-07-30 10:17:02 +02:00
Stephane Blondon
0249d59a73 Library is compatible with python 3.7 2018-07-30 16:08:27 +08:00
Martin Rusev
1ad18477f1 Merge pull request #144 from sblondon/add_vendors_directory_to_package
Add vendors directory to package
2018-07-28 17:39:18 +02:00
Martin Rusev
079569dac9 Merge pull request #143 from sblondon/master
Add a forgotten parameter in a log line
2018-07-28 17:39:01 +02:00
Stephane Blondon
7ea534fbda Add imbox/vendors/ directory to python package 2018-07-28 16:13:13 +02:00
Stephane Blondon
db30a8044f Add a forgotten parameter in a log line 2018-07-28 15:43:15 +02:00
Martin Rusev
d5a4e47904 Merge pull request #142 from sblondon/master
Remove the minus in charset if the charset is not found
2018-07-28 15:29:31 +02:00
Stephane Blondon
330a989452 Remove the minus in charset if it's not found to fix Lookup error when searching the appropriate codec 2018-07-28 12:45:19 +02:00
Martin Rusev
aa6739d91f Merge pull request #139 from zevaverbach/vendors
created vendors package
2018-07-27 22:33:10 +02:00
f82ce52220 added type hints for new stuff 2018-07-27 16:00:17 -04:00
87e42a9fdd Merge branch 'vendors' of https://github.com/zevaverbach/imbox into vendors 2018-07-27 15:41:52 -04:00
86cf1fbf9b added basic structure for vendors module, including mostly blank GmailMessages class. 'vendor' is now a kwarg for Imbox, but there's also a lookup dict created in vendors/__init__.py for 'magic' determination of the vendor from the hostname.
added `authentication_error_message` as a class attribute to `Imbox`.  If `Imbox.vendor` is not None, look for a custom authentication error string, and raise it if there's a problem logging in.  The default error value is raised if there is no custom string.

created `name_authentication_string_dict` from all subclasses of `Messages` in `vendors/__init__.py`, for the lookup mentioned above.

Implement architecture discussed in #131, deciding whether to use `Messages` or a subclass of it in `Imbox.messages` based on the `Imbox.vendor` value.  Use the `folder_lookup` dict, present on `Messages` but only filled in on its subclasses, to select the right folder based on a lowercased "standard" name given as the value of `folder`.

created a blank `folder_lookup` on `Messages`.

filled in the first vendor that subclasses `Messages`, `GmailMessages`.  It contains class attributes `authentication_error_message`, `hostname`, `name`, used for purposes described above, as well as `folder_lookup` which is a dict containing aliases, sometimes several, for Gmail's unique folder naming scheme.  These aliases are meant to be "human-friendly" for intuitive `folder` selection in calls to `Imbox.messages`.

fixes #131.

prevent too much bookkeeping in vendors/__init__.py by adding each vendor.__name__ to __all__ from the vendors list.
2018-07-27 15:41:13 -04:00
6f3cc1ab92 prevent too much bookkeeping in vendors/__init__.py by adding each vendor.__name__ to __all__ from the vendors list. 2018-07-27 14:44:35 -04:00
1fbb8511b9 added authentication_error_message as a class attribute to Imbox. If Imbox.vendor is not None, look for a custom authentication error string, and raise it if there's a problem logging in. The default error value is raised if there is no custom string.
created `name_authentication_string_dict` from all subclasses of `Messages` in `vendors/__init__.py`, for the lookup mentioned above.

Implement architecture discussed in #131, deciding whether to use `Messages` or a subclass of it in `Imbox.messages` based on the `Imbox.vendor` value.  Use the `folder_lookup` dict, present on `Messages` but only filled in on its subclasses, to select the right folder based on a lowercased "standard" name given as the value of `folder`.

created a blank `folder_lookup` on `Messages`.

filled in the first vendor that subclasses `Messages`, `GmailMessages`.  It contains class attributes `authentication_error_message`, `hostname`, `name`, used for purposes described above, as well as `folder_lookup` which is a dict containing aliases, sometimes several, for Gmail's unique folder naming scheme.  These aliases are meant to be "human-friendly" for intuitive `folder` selection in calls to `Imbox.messages`.

fixes #131.
2018-07-27 14:31:21 -04:00
Martin Rusev
adf70cbed5 Update .travis.yml 2018-07-27 14:52:06 +02:00
Martin Rusev
c04c340bb7 Update .travis.yml 2018-07-27 14:34:24 +02:00
Martin Rusev
b52d43c496 Update CHANGELOG.md 2018-07-27 10:53:58 +02:00
Martin Rusev
a8440f60ca Update CHANGELOG.md 2018-07-27 10:52:21 +02:00
Martin Rusev
c86cbe766d Update README.rst 2018-07-27 10:38:07 +02:00
Martin Rusev
16a05e15e5 Merge pull request #136 from zevaverbach/stubs
Stubs!
2018-07-27 10:37:08 +02:00
7d11b590d8 added basic structure for vendors module, including mostly blank GmailMessages class. 'vendor' is now a kwarg for Imbox, but there's also a lookup dict created in vendors/__init__.py for 'magic' determination of the vendor from the hostname. 2018-07-26 15:48:31 -04:00
dfca46886f filled in stub files 2018-07-26 15:30:02 -04:00
6d527a2f1d added type hint stubs for all modules 2018-07-26 15:24:20 -04:00
80d6104387 conflict 2018-07-26 15:08:08 -04:00
Martin Rusev
0464a5a74d Merge pull request #134 from zevaverbach/126_128_130
Added flags to messages, moved classes out of __init__.py, and documented that the inbox is where Imbox.messages are from without any folder argument.
2018-07-26 21:06:51 +02:00
5b27fa1b76 removed errant dash that was causing Travis-CI to fail 2018-07-26 11:06:48 -04:00
9cffb51a81 Added missing documentation for all supported query keyword arguments. Fixes #124. Added Pycharm directory to .gitignore.
Fixed var names in documentation of query keywords

moved Messages and Imbox to their own modules, imported Imbox.imbox into __init__.py and put it in __all__. fixes #130.

clarified in documentation and Imbox.messages logging that, unless a folder is specified in the kwargs to Imbox.messages, the returned messages will be from the inbox.  In the documentation this is accomplished exclusively by the var names. fixes #128.

amended `8df7d7c` to reflect manual changes made to `README.rst` in current master, but also added `inbox_` to several var names to make that explicit in the documentation.  Added flags to messages returned by `fetch_email_by_uid`, using the new function `parse_flags` in `parser.py`.  Fixes #126.

added TODO back into query.py
2018-07-26 10:50:10 -04:00
Martin Rusev
06aa4e054b Update README.rst 2018-07-26 14:47:02 +02:00
Martin Rusev
63bddbd73c Update README.rst 2018-07-26 14:45:34 +02:00
2838639bfa conflict in .gitignore 2018-07-25 14:25:14 -04:00
Martin Rusev
34149efed5 Merge pull request #129 from zevaverbach/encapsulate_generator
Created Message class to encapsulate the "messages" generator
2018-07-25 19:46:49 +02:00
73fafcb368 made Messages methods private, as well as uid_list. 2018-07-25 12:11:59 -04:00
8e26e92a39 Moved query_ids from Imbox to Messages.
Moved `fetch_list` to `Messages` and renamed `fetch_email_list`, moving assignment of uid_list to the constructor of `Messages`.

Moved `fetch_by_uid` from `Imbox` to a pure function in `parser` module.

Replaced call to `Imbox.fetch_list` with a call to `Messages` instead.

created `Messages` class, which encapsulates the generator `Messages.fetch_email_list`, while also implementing `__len__` to show how many emails match a given query, and `__iter__` to refresh the `fetch_email_list` generator when it's exhausted.  It also implements `__getitem__` to support indexing of the emails matching a query.  Messages requires the `connection` and `parser_policy` established in `Imbox` and accepts arbitrary keyword arguments, which it uses in the IMAP query as well as in the `__repr__`.
2018-07-25 11:48:19 -04:00
635d15441e shortened up the ImapTransport constructor, a couple of similar cleanups 2018-07-25 09:00:21 -04:00
55f64a1922 removed some unused args, renamed to avoid shadowing a name in the global scope. 2018-07-25 08:46:59 -04:00
71942a69e8 fixed var names in documentation of query keywords 2018-07-25 08:27:40 -04:00
b4cb03e145 added Pycharm directory to .gitignore 2018-07-25 08:24:05 -04:00
b53a1e6837 Added missing documentation for all supported query keyword arguments. Fixes #124. 2018-07-25 08:22:50 -04:00
Martin Rusev
a1801af56e Merge pull request #119 from sblondon/factorize-version
Reuse __version__ for the library version
2018-04-05 10:21:07 +02:00
Stephane Blondon
6a0da4b105 reuse __version__ for the library version 2018-04-04 18:27:27 +02:00
Martin Rusev
5d09b6f3e4 Merge pull request #117 from sblondon/fix-encoding-in-logger
Fix encoding in logger
2018-04-04 15:43:56 +02:00
Stephane Blondon
ecb62b585c refactoring: string formatting more readable 2018-03-21 18:31:08 +01:00
Stephane Blondon
cbb46ef078 fix decoding of sender e-mail if badly encoded 2018-03-21 18:24:59 +01:00
Stephane Blondon
79ce81aa9d fix spelling error 2018-03-19 20:56:22 +01:00
Martin Rusev
96ce737df5 Merge pull request #112 from wagner-certat/version
Expose version in library
2018-01-08 12:56:01 +01:00
Sebastian Wagner
a94682fc3c Expose version in library 2018-01-08 12:07:21 +01:00
25 changed files with 594 additions and 199 deletions

6
.gitignore vendored
View File

@@ -31,3 +31,9 @@ nosetests.xml
example.* example.*
example.py example.py
# PyCharm
.idea/
# Mac
.DS_Store

View File

@@ -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
View File

@@ -0,0 +1,3 @@
{
"python.pythonPath": "/usr/local/bin/python3"
}

View File

@@ -1,22 +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) ## 0.9.5 (5 December 2017)
IMPROVEMENTS: IMPROVEMENTS:
* `date__on` support: ([#109](https://github.com/martinrusev/imbox/pull/109)) * `date__on` support: ([#109](https://github.com/martinrusev/imbox/pull/109)) - Contributed by @balsagoth
* Starttls support: ([#108](https://github.com/martinrusev/imbox/pull/108)) * 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)) * 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)) * 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)) * 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)) * 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)) * 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
@@ -24,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:
@@ -35,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

View File

@@ -36,36 +36,57 @@ Usage
ssl=True, ssl=True,
ssl_context=None, ssl_context=None,
starttls=False) as imbox: starttls=False) as imbox:
# Get all folders # Get all folders
status, folders_with_additional_info = imbox.folders() status, folders_with_additional_info = imbox.folders()
# Gets all messages # Gets all messages from the inbox
all_messages = imbox.messages() 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=datetime.date(2013, 7, 31)) 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=datetime.date(2013, 7, 30)) inbox_messages_received_after = imbox.messages(date__gt=datetime.date(2018, 7, 30))
# Messages received on a specific date # Messages received on a specific date
messages_from = imbox.messages(date__on=datetime.date(2013, 7, 30)) 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
@@ -125,7 +146,7 @@ Usage
# mark the message as read # mark the message as read
imbox.mark_seen(uid) imbox.mark_seen(uid)
Changelog Changelog

View File

@@ -1,93 +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, starttls=False):
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.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 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 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 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)))
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()

View File

@@ -10,22 +10,16 @@ class ImapTransport:
def __init__(self, hostname, port=None, ssl=True, ssl_context=None, starttls=False): 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: if starttls:
self.server.starttls() self.server.starttls()
logger.debug("Created IMAP4 transport for {host}:{port}" logger.debug("Created IMAP4 transport for {host}:{port}"

13
imbox/imap.pyi Normal file
View 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
View 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
View 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
View 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
View 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']]: ...

View File

@@ -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,7 +70,7 @@ 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':
@@ -105,7 +112,8 @@ def parse_attachment(message_part):
name, value = decode_param(param) name, value = decode_param(param)
if 'file' in name: if 'file' in name:
attachment['filename'] = value[1:-1] if value.startswith('"') else 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
@@ -120,10 +128,34 @@ 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', errors='ignore') raw_email = str_encode(raw_email, 'utf-8', errors='ignore')
@@ -133,13 +165,13 @@ def parse_email(raw_email, policy=None):
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": [],

34
imbox/parser.pyi Normal file
View 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: ...

View File

@@ -1,62 +1,17 @@
import datetime import datetime
import logging
# TODO - Validate query arguments
logger = logging.getLogger(__name__)
def format_date(date): def build_search_query(imap_attribute_lookup, **kwargs):
if isinstance(date, datetime.date):
return date.strftime('%d-%b-%Y')
else:
return date
def build_search_query(**kwargs):
# Parse keyword arguments
unread = kwargs.get('unread', False)
unflagged = kwargs.get('unflagged', False)
flagged = kwargs.get('flagged', False)
sent_from = kwargs.get('sent_from', False)
sent_to = kwargs.get('sent_to', False)
date__gt = kwargs.get('date__gt', False)
date__lt = kwargs.get('date__lt', False)
date__on = kwargs.get('date__on', False)
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 unflagged: if isinstance(value, str) and '"' in value:
query.append("(UNFLAGGED)") value = value.replace('"', "'")
query.append(imap_attribute_lookup[name].format(value))
if flagged:
query.append("(FLAGGED)")
if sent_from:
query.append('(FROM "%s")' % sent_from)
if sent_to:
query.append('(TO "%s")' % sent_to)
if date__gt:
query.append('(SINCE "%s")' % format_date(date__gt))
if date__lt:
query.append('(BEFORE "%s")' % format_date(date__lt))
if date__on:
query.append('(ON "%s")' % format_date(date__on))
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)"

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -1,7 +1,9 @@
from setuptools import setup from setuptools import setup
import os import os
version = '0.9.5' import imbox
version = imbox.__version__
def read(filename): def read(filename):
@@ -17,7 +19,7 @@ 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=(
@@ -25,7 +27,8 @@ setup(
'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', test_suite='tests',
) )

View File

@@ -274,6 +274,44 @@ R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
--____NOIBTUQXSYRVOOAFLCHY____-- --____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):
@@ -324,6 +362,12 @@ class TestParser(unittest.TestCase):
self.assertEqual('abc.xyz', attachment['filename']) self.assertEqual('abc.xyz', attachment['filename'])
self.assertTrue(attachment['content']) 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 # TODO - Complete the test suite
def test_decode_mail_header(self): def test_decode_mail_header(self):
pass pass
@@ -336,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

View File

@@ -1,50 +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): def test_unflagged(self):
res = build_search_query(unflagged=True) res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, unflagged=True)
self.assertEqual(res, "(UNFLAGGED)") self.assertEqual(res, "(UNFLAGGED)")
def test_flagged(self): def test_flagged(self):
res = build_search_query(flagged=True) res = build_search_query(IMAP_ATTRIBUTE_LOOKUP, flagged=True)
self.assertEqual(res, "(FLAGGED)") 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(
IMAP_ATTRIBUTE_LOOKUP, sent_from='test@example.com')
self.assertEqual(res, '(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(
IMAP_ATTRIBUTE_LOOKUP, sent_to='test@example.com')
self.assertEqual(res, '(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(
IMAP_ATTRIBUTE_LOOKUP, date__gt=date(2014, 12, 31))
self.assertEqual(res, '(SINCE "31-Dec-2014")') 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(
IMAP_ATTRIBUTE_LOOKUP, date__lt=date(2014, 1, 1))
self.assertEqual(res, '(BEFORE "01-Jan-2014")') self.assertEqual(res, '(BEFORE "01-Jan-2014")')
def test_date__on(self): def test_date__on(self):
res = build_search_query(date__on=date(2014, 1, 1)) res = build_search_query(
IMAP_ATTRIBUTE_LOOKUP, date__on=date(2014, 1, 1))
self.assertEqual(res, '(ON "01-Jan-2014")') 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")')