Skip to content

Commit

Permalink
removed Q alias for AND, added new aliases: A for AND, O for OR, N fo…
Browse files Browse the repository at this point in the history
…r NOT
  • Loading branch information
ikvk committed Jul 3, 2020
1 parent b06f226 commit f2b10e3
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 69 deletions.
32 changes: 18 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Basic
^^^^^
.. code-block:: python
from imap_tools import MailBox, Q
from imap_tools import MailBox, AND
# get list of email subjects from INBOX folder
with MailBox('imap.mail.com').login('[email protected]', 'password') as mailbox:
Expand All @@ -42,7 +42,7 @@ Basic
# get list of email subjects from INBOX folder - equivalent verbose version
mailbox = MailBox('imap.mail.com')
mailbox.login('[email protected]', 'password', initial_folder='INBOX') # or mailbox.folder.set instead 3d arg
subjects = [msg.subject for msg in mailbox.fetch(Q(all=True))]
subjects = [msg.subject for msg in mailbox.fetch(AND(all=True))]
mailbox.logout()
MailBox/MailBoxUnencrypted for create mailbox instance.
Expand Down Expand Up @@ -101,38 +101,42 @@ Possible search approaches:

.. code-block:: python
from imap_tools import Q, AND, OR, NOT
from imap_tools import AND, OR, NOT
mailbox.fetch(Q(subject='weather')) # query, the str-like object - see below
mailbox.fetch(AND(subject='weather')) # query, the str-like object - see below
mailbox.fetch('TEXT "hello"') # str, use charset arg for non US-ASCII chars
mailbox.fetch(b'TEXT "\xd1\x8f"') # bytes, charset arg is ignored
Implemented query builder for search logic described in `rfc3501 <https://tools.ietf.org/html/rfc3501#section-6.4.4>`_.
See `query examples <https://github.com/ikvk/imap_tools/blob/master/examples/search.py>`_.

* Class AND and its alias Q are used to combine keys by the logical "and" condition.
* Class OR is used to combine keys by the logical "or" condition.
* Class NOT is used to invert the result of a logical expression.
* Class H (Header) is used to search by headers.
====== ===== ========================================== ============================================================
Class Alias Usage Arguments
====== ===== ========================================== ============================================================
AND A combines keys by logical "AND" condition Search keys (see below) | str
OR O combines keys by logical "OR" condition Search keys (see below) | str
NOT N invert the result of a logical expression AND/OR instances | str
Header H for search by headers name: str, value: str
====== ===== ========================================== ============================================================

If the "charset" argument is specified in MailBox.fetch, the search string will be encoded to this encoding.
You can change this behavior by overriding MailBox._criteria_encoder or pass criteria as bytes in desired encoding.

.. code-block:: python
from imap_tools import Q, AND, OR, NOT
from imap_tools import A, AND, OR, NOT
# AND
Q(text='hello', new=True) # '(TEXT "hello" NEW)'
A(text='hello', new=True) # '(TEXT "hello" NEW)'
# OR
OR(text='hello', date=datetime.date(2000, 3, 15)) # '(OR TEXT "hello" ON 15-Mar-2000)'
# NOT
NOT(text='hello', new=True) # 'NOT (TEXT "hello" NEW)'
# complex
Q(OR(from_='[email protected]', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='[email protected]')
A(OR(from_='[email protected]', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='[email protected]')
# encoding
mailbox.fetch(Q(subject='привет'), charset='utf8') # 'привет' will be encoded by MailBox._criteria_encoder
# python note: you can't do: Q(text='two', NOT(subject='one'))
Q(NOT(subject='one'), text='two') # use kwargs after logic classes
mailbox.fetch(A(subject='привет'), charset='utf8') # 'привет' will be encoded by MailBox._criteria_encoder
# python note: you can't do: A(text='two', NOT(subject='one'))
A(NOT(subject='one'), text='two') # use kwargs after logic classes (args)
The search key types are marked with `*` can accepts a sequence of values like list, tuple, set or generator.

Expand Down
8 changes: 4 additions & 4 deletions examples/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"""

import datetime as dt
from imap_tools import AND, OR, NOT, Q, H
from imap_tools import AND, OR, NOT, A, H

# date in the date list (date=date1 OR date=date3 OR date=date2)
q1 = OR(date=[dt.date(2019, 10, 1), dt.date(2019, 10, 10), dt.date(2019, 10, 15)])
Expand All @@ -31,7 +31,7 @@
# "NOT ((OR OR ON 1-Oct-2019 ON 10-Oct-2019 ON 15-Oct-2019))"

# subject contains "hello" AND date greater than or equal dt.date(2019, 10, 10)
q3 = Q(subject='hello', date_gte=dt.date(2019, 10, 10))
q3 = A(subject='hello', date_gte=dt.date(2019, 10, 10))
# "(SUBJECT "hello" SINCE 10-Oct-2019)"

# from contains one of the address parts
Expand All @@ -51,9 +51,9 @@
# "(OR (OR TEXT "tag15" SUBJECT "tag15") (OR TEXT "tag10" SUBJECT "tag10"))"

# header IsSpam contains '++' AND header CheckAntivirus contains '-'
q8 = Q(header=[H('IsSpam', '++'), H('CheckAntivirus', '-')])
q8 = A(header=[H('IsSpam', '++'), H('CheckAntivirus', '-')])
# "(HEADER "IsSpam" "++" HEADER "CheckAntivirus" "-")"

# complex from README
q9 = Q(OR(from_='[email protected]', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='[email protected]')
q9 = A(OR(from_='[email protected]', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='[email protected]')
# "((OR FROM "[email protected]" TEXT "\\"the text\\"") NOT ((OR (UNANSWERED) (NEW))) TO "[email protected]")"
4 changes: 2 additions & 2 deletions imap_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .query import Q, AND, OR, NOT, H
from .query import AND, OR, NOT, Header, A, O, N, H

This comment has been minimized.

Copy link
@PH89

PH89 Jul 13, 2020

Q is missing here, causing old scripts that relay on Q to fail.

from .mailbox import *
from .message import *
from .folder import *
from .utils import *

__version__ = '0.16.1'
__version__ = '0.17.0'
14 changes: 12 additions & 2 deletions imap_tools/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ def combine_params(self) -> str:
return 'NOT {}'.format(self.prefix_join('', itertools.chain(self.converted_strings, self.converted_params)))


Q = AND # Short alias
# Short alias set:
A = AND
O = OR # noqa
N = NOT


class Q(AND):
def __init__(self, *args, **kwargs):
import warnings
warnings.warn('alias Q are deprecated and will be removed soon, use A instead')
super().__init__(*args, **kwargs)


class Header:
Expand Down Expand Up @@ -304,4 +314,4 @@ def convert_uid(self, key, value):
return 'UID {}'.format(self.cleaned_uid(key, value))

def convert_gmail_label(self, key, value):
return 'X-GM-LABELS {}'.format(quote(self.cleaned_str(key, value)))
return 'X-GM-LABELS {}'.format(quote(self.cleaned_str(key, value)))
5 changes: 5 additions & 0 deletions release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
0.17.0
======
* Query builder: removed Q alias for AND
* Query builder: added new aliases: A for AND, O for OR, N for NOT

0.16.1
======
* Added X-GM-LABELS support to query builder (gmail_label)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def get_version(package: str) -> str:
author='v.kaukin',
author_email='[email protected]',
description='Working with email and mailbox using IMAP protocol.',
keywords=['imap', 'imap-client', 'python3', 'email'],
keywords=['imap', 'imap-client', 'python3', 'python', 'email'],
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
Expand Down
92 changes: 46 additions & 46 deletions tests/test_query.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
import datetime as dt

from imap_tools.query import ParamConverter, Q, AND, OR, NOT, H
from imap_tools.query import ParamConverter, A, AND, OR, NOT, H


class QueryTest(unittest.TestCase):
Expand Down Expand Up @@ -33,49 +33,49 @@ def not_fetch():
cleaned_fn('key_does_not_matter', bad)

def test_converters(self):
self.assertEqual(Q(answered=True), '(ANSWERED)')
self.assertEqual(Q(answered=False), '(UNANSWERED)')
self.assertEqual(Q(seen=True), '(SEEN)')
self.assertEqual(Q(seen=False), '(UNSEEN)')
self.assertEqual(Q(flagged=True), '(FLAGGED)')
self.assertEqual(Q(flagged=False), '(UNFLAGGED)')
self.assertEqual(Q(draft=True), '(DRAFT)')
self.assertEqual(Q(draft=False), '(UNDRAFT)')
self.assertEqual(Q(deleted=True), '(DELETED)')
self.assertEqual(Q(deleted=False), '(UNDELETED)')
self.assertEqual(Q(keyword='KEY1'), '(KEYWORD KEY1)')
self.assertEqual(Q(no_keyword='KEY2'), '(UNKEYWORD KEY2)')

self.assertEqual(Q(from_='[email protected]'), '(FROM "[email protected]")')
self.assertEqual(Q(to='[email protected]'), '(TO "[email protected]")')
self.assertEqual(Q(subject='hello'), '(SUBJECT "hello")')
self.assertEqual(Q(body='body text'), '(BODY "body text")')
self.assertEqual(Q(body='hi'), '(BODY "hi")')
self.assertEqual(Q(text='"quoted text"'), '(TEXT "\\"quoted text\\"")')
self.assertEqual(Q(text='hi'), '(TEXT "hi")')
self.assertEqual(Q(bcc='[email protected]'), '(BCC "[email protected]")')
self.assertEqual(Q(cc='[email protected]'), '(CC "[email protected]")')

self.assertEqual(Q(date=dt.date(2000, 3, 15)), '(ON 15-Mar-2000)')
self.assertEqual(Q(date_gte=dt.date(2000, 3, 15)), '(SINCE 15-Mar-2000)')
self.assertEqual(Q(date_lt=dt.date(2000, 3, 15)), '(BEFORE 15-Mar-2000)')
self.assertEqual(Q(sent_date=dt.date(2000, 3, 15)), '(SENTON 15-Mar-2000)')
self.assertEqual(Q(sent_date_gte=dt.date(2000, 3, 15)), '(SENTSINCE 15-Mar-2000)')
self.assertEqual(Q(sent_date_lt=dt.date(2000, 3, 15)), '(SENTBEFORE 15-Mar-2000)')

self.assertEqual(Q(size_gt=1024), '(LARGER 1024)')
self.assertEqual(Q(size_lt=512), '(SMALLER 512)')

self.assertEqual(Q(new=True), '(NEW)')
self.assertEqual(Q(old=True), '(OLD)')
self.assertEqual(Q(recent=True), '(RECENT)')
self.assertEqual(Q(all=True), '(ALL)')

self.assertEqual(Q(header=H('X-Google-Smtp-Source', '123')), '(HEADER "X-Google-Smtp-Source" "123")')
self.assertEqual(Q(uid='1,2'), '(UID 1,2)')
self.assertEqual(Q(uid=['3', '4']), '(UID 3,4)')

self.assertEqual(Q(gmail_label="TestLabel"), '(X-GM-LABELS "TestLabel")')
self.assertEqual(A(answered=True), '(ANSWERED)')
self.assertEqual(A(answered=False), '(UNANSWERED)')
self.assertEqual(A(seen=True), '(SEEN)')
self.assertEqual(A(seen=False), '(UNSEEN)')
self.assertEqual(A(flagged=True), '(FLAGGED)')
self.assertEqual(A(flagged=False), '(UNFLAGGED)')
self.assertEqual(A(draft=True), '(DRAFT)')
self.assertEqual(A(draft=False), '(UNDRAFT)')
self.assertEqual(A(deleted=True), '(DELETED)')
self.assertEqual(A(deleted=False), '(UNDELETED)')
self.assertEqual(A(keyword='KEY1'), '(KEYWORD KEY1)')
self.assertEqual(A(no_keyword='KEY2'), '(UNKEYWORD KEY2)')

self.assertEqual(A(from_='[email protected]'), '(FROM "[email protected]")')
self.assertEqual(A(to='[email protected]'), '(TO "[email protected]")')
self.assertEqual(A(subject='hello'), '(SUBJECT "hello")')
self.assertEqual(A(body='body text'), '(BODY "body text")')
self.assertEqual(A(body='hi'), '(BODY "hi")')
self.assertEqual(A(text='"quoted text"'), '(TEXT "\\"quoted text\\"")')
self.assertEqual(A(text='hi'), '(TEXT "hi")')
self.assertEqual(A(bcc='[email protected]'), '(BCC "[email protected]")')
self.assertEqual(A(cc='[email protected]'), '(CC "[email protected]")')

self.assertEqual(A(date=dt.date(2000, 3, 15)), '(ON 15-Mar-2000)')
self.assertEqual(A(date_gte=dt.date(2000, 3, 15)), '(SINCE 15-Mar-2000)')
self.assertEqual(A(date_lt=dt.date(2000, 3, 15)), '(BEFORE 15-Mar-2000)')
self.assertEqual(A(sent_date=dt.date(2000, 3, 15)), '(SENTON 15-Mar-2000)')
self.assertEqual(A(sent_date_gte=dt.date(2000, 3, 15)), '(SENTSINCE 15-Mar-2000)')
self.assertEqual(A(sent_date_lt=dt.date(2000, 3, 15)), '(SENTBEFORE 15-Mar-2000)')

self.assertEqual(A(size_gt=1024), '(LARGER 1024)')
self.assertEqual(A(size_lt=512), '(SMALLER 512)')

self.assertEqual(A(new=True), '(NEW)')
self.assertEqual(A(old=True), '(OLD)')
self.assertEqual(A(recent=True), '(RECENT)')
self.assertEqual(A(all=True), '(ALL)')

self.assertEqual(A(header=H('X-Google-Smtp-Source', '123')), '(HEADER "X-Google-Smtp-Source" "123")')
self.assertEqual(A(uid='1,2'), '(UID 1,2)')
self.assertEqual(A(uid=['3', '4']), '(UID 3,4)')

self.assertEqual(A(gmail_label="TestLabel"), '(X-GM-LABELS "TestLabel")')

def test_format_date(self):
self.assertEqual(ParamConverter.format_date(dt.date(2000, 1, 15)), '15-Jan-2000')
Expand All @@ -85,12 +85,12 @@ def test_logic_operators(self):
self.assertEqual(AND(text='hello', new=True), '(TEXT "hello" NEW)')
self.assertEqual(OR(text='hello', new=True), '(OR TEXT "hello" NEW)')
self.assertEqual(NOT(text='hello', new=True), 'NOT (TEXT "hello" NEW)')
self.assertEqual(Q(AND(to='[email protected]'), AND(to='[email protected]')), '((TO "[email protected]") (TO "[email protected]"))')
self.assertEqual(A(AND(to='[email protected]'), AND(to='[email protected]')), '((TO "[email protected]") (TO "[email protected]"))')
self.assertEqual(
OR(date=[dt.date(2019, 10, 1), dt.date(2019, 10, 10), dt.date(2019, 10, 15), dt.date(2019, 10, 20)]),
'(OR OR OR ON 1-Oct-2019 ON 10-Oct-2019 ON 15-Oct-2019 ON 20-Oct-2019)')
self.assertEqual(
Q(OR(from_='[email protected]', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='[email protected]'),
A(OR(from_='[email protected]', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='[email protected]'),
'((OR FROM "[email protected]" TEXT "\\"the text\\"") NOT ((OR (UNANSWERED) (NEW))) TO "[email protected]")')

def test_header(self):
Expand Down

0 comments on commit f2b10e3

Please sign in to comment.