Skip to content

Commit bccb5e0

Browse files
committed
lru_cache->cached_property, f-strs, imap_utf7 speed, AnyStr->StrOrBytes
1 parent 3e3549b commit bccb5e0

File tree

12 files changed

+154
-169
lines changed

12 files changed

+154
-169
lines changed

README.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Email attributes
9494

9595
Email has 2 basic body variants: text and html. Sender can choose to include: one, other, both or neither(rare).
9696

97-
MailMessage and MailAttachment public attributes are cached by functools.lru_cache
97+
MailMessage and MailAttachment public attributes are cached by functools.cached_property
9898

9999
.. code-block:: python
100100
@@ -428,7 +428,8 @@ Big thanks to people who helped develop this library:
428428
`K900 <https://github.com/K900>`_,
429429
`homoLudenus <https://github.com/homoLudenus>`_,
430430
`sphh <https://github.com/sphh>`_,
431-
`bh <https://github.com/bh>`_
431+
`bh <https://github.com/bh>`_,
432+
`tomasmach <https://github.com/tomasmach>`_
432433

433434
Help the project
434435
----------------

docs/dev_notes.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ icons
4040
📨 📬 📪 📭 📫 ✉ 📧 🖂 🖃 🖅 📩
4141

4242

43+
fetch
44+
=====
45+
FULL - Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
46+
47+
4348
code
4449
====
4550
def eml_to_python_structs(eml_path: str):

docs/release_notes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
1.9.1
2+
=====
3+
* Replaced: functools.lru_cache to functools.cached_property
4+
* Replaced: .format() to f''
5+
* Optimized: speed for imap_utf7
6+
* Replaced: typing.AnyStr to utils.StrOrBytes
7+
18
1.9.0
29
=====
310
* Added: __str__ to MailAttachment

docs/todo.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11

2-
FULL
3-
Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
4-
52
add servers for tests
63
rambler
74
GMX Mail

imap_tools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
from .utils import EmailAddress
1212
from .errors import *
1313

14-
__version__ = '1.9.0'
14+
__version__ = '1.9.1'

imap_tools/errors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ def __init__(self, command_result: tuple, expected: Any):
2121
self.expected = expected
2222

2323
def __str__(self):
24-
return 'Response status "{exp}" expected, but "{typ}" received. Data: {data}'.format(
25-
exp=self.expected, typ=self.command_result[0], data=str(self.command_result[1]))
24+
return (f'Response status "{self.expected}" expected, '
25+
f'but "{self.command_result[0]}" received. Data: {str(self.command_result[1])}')
2626

2727

2828
class MailboxFolderSelectError(UnexpectedCommandStatusError):

imap_tools/folder.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import re
2-
from typing import AnyStr, Optional, Iterable, List, Dict, Tuple
2+
from typing import Optional, Iterable, List, Dict, Tuple
33

44
from .imap_utf7 import utf7_decode
55
from .consts import MailBoxFolderStatusOptions
6-
from .utils import check_command_status, pairs_to_dict, encode_folder
6+
from .utils import check_command_status, pairs_to_dict, encode_folder, StrOrBytes
77
from .errors import MailboxFolderStatusValueError, MailboxFolderSelectError, MailboxFolderCreateError, \
88
MailboxFolderRenameError, MailboxFolderDeleteError, MailboxFolderStatusError, MailboxFolderSubscribeError
99

@@ -24,8 +24,7 @@ def __init__(self, name: str, delim: str, flags: Tuple[str, ...]):
2424
self.flags = flags
2525

2626
def __repr__(self):
27-
return "{}(name={}, delim={}, flags={})".format(
28-
self.__class__.__name__, repr(self.name), repr(self.delim), repr(self.flags))
27+
return f"{self.__class__.__name__}(name={repr(self.name)}, delim={repr(self.delim)}, flags={repr(self.flags)})"
2928

3029
def __eq__(self, other):
3130
return all(getattr(self, i) == getattr(other, i) for i in self.__slots__)
@@ -38,7 +37,7 @@ def __init__(self, mailbox):
3837
self.mailbox = mailbox
3938
self._current_folder = None
4039

41-
def set(self, folder: AnyStr, readonly: bool = False) -> tuple:
40+
def set(self, folder: StrOrBytes, readonly: bool = False) -> tuple:
4241
"""Select current folder"""
4342
result = self.mailbox.client.select(encode_folder(folder), readonly)
4443
check_command_status(result, MailboxFolderSelectError)
@@ -49,7 +48,7 @@ def exists(self, folder: str) -> bool:
4948
"""Checks whether a folder exists on the server."""
5049
return len(self.list('', folder)) > 0
5150

52-
def create(self, folder: AnyStr) -> tuple:
51+
def create(self, folder: StrOrBytes) -> tuple:
5352
"""
5453
Create folder on the server.
5554
Use email box delimiter to separate folders. Example for "|" delimiter: "folder|sub folder"
@@ -67,20 +66,20 @@ def get(self) -> Optional[str]:
6766
"""
6867
return self._current_folder
6968

70-
def rename(self, old_name: AnyStr, new_name: AnyStr) -> tuple:
69+
def rename(self, old_name: StrOrBytes, new_name: StrOrBytes) -> tuple:
7170
"""Rename folder from old_name to new_name"""
7271
result = self.mailbox.client._simple_command(
7372
'RENAME', encode_folder(old_name), encode_folder(new_name))
7473
check_command_status(result, MailboxFolderRenameError)
7574
return result
7675

77-
def delete(self, folder: AnyStr) -> tuple:
76+
def delete(self, folder: StrOrBytes) -> tuple:
7877
"""Delete folder"""
7978
result = self.mailbox.client._simple_command('DELETE', encode_folder(folder))
8079
check_command_status(result, MailboxFolderDeleteError)
8180
return result
8281

83-
def status(self, folder: Optional[AnyStr] = None, options: Optional[Iterable[str]] = None) -> Dict[str, int]:
82+
def status(self, folder: Optional[StrOrBytes] = None, options: Optional[Iterable[str]] = None) -> Dict[str, int]:
8483
"""
8584
Get the status of a folder
8685
:param folder: mailbox folder, current folder if None
@@ -97,15 +96,15 @@ def status(self, folder: Optional[AnyStr] = None, options: Optional[Iterable[str
9796
if opt not in MailBoxFolderStatusOptions.all:
9897
raise MailboxFolderStatusValueError(str(opt))
9998
status_result = self.mailbox.client._simple_command(
100-
command, encode_folder(folder), '({})'.format(' '.join(options)))
99+
command, encode_folder(folder), f'({" ".join(options)})')
101100
check_command_status(status_result, MailboxFolderStatusError)
102101
result = self.mailbox.client._untagged_response(status_result[0], status_result[1], command)
103102
check_command_status(result, MailboxFolderStatusError)
104103
status_data = [i for i in result[1] if type(i) is bytes][0] # may contain tuples with encoded names
105104
values = status_data.decode().split('(')[-1].split(')')[0].split(' ')
106105
return {k: int(v) for k, v in pairs_to_dict(values).items() if str(v).isdigit()}
107106

108-
def list(self, folder: AnyStr = '', search_args: str = '*', subscribed_only: bool = False) -> List[FolderInfo]:
107+
def list(self, folder: StrOrBytes = '', search_args: str = '*', subscribed_only: bool = False) -> List[FolderInfo]:
109108
"""
110109
Get a listing of folders on the server
111110
:param folder: mailbox folder, if empty - get from root
@@ -148,7 +147,7 @@ def list(self, folder: AnyStr = '', search_args: str = '*', subscribed_only: boo
148147
))
149148
return result
150149

151-
def subscribe(self, folder: AnyStr, value: bool) -> tuple:
150+
def subscribe(self, folder: StrOrBytes, value: bool) -> tuple:
152151
"""subscribe/unsubscribe to folder"""
153152
method = self.mailbox.client.subscribe if value else self.mailbox.client.unsubscribe
154153
result = method(encode_folder(folder))

imap_tools/imap_utf7.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"""
99

1010
import binascii
11-
from typing import Iterable, MutableSequence
11+
from typing import MutableSequence
12+
13+
AMPERSAND_ORD = ord('&')
14+
HYPHEN_ORD = ord('-')
1215

1316

1417
# ENCODING
@@ -17,10 +20,10 @@ def _modified_base64(value: str) -> bytes:
1720
return binascii.b2a_base64(value.encode('utf-16be')).rstrip(b'\n=').replace(b'/', b',')
1821

1922

20-
def _do_b64(_in: Iterable[str], r: MutableSequence[bytes]):
23+
def _do_b64(_in: MutableSequence[str], r: MutableSequence[bytes]):
2124
if _in:
2225
r.append(b'&' + _modified_base64(''.join(_in)) + b'-')
23-
del _in[:]
26+
_in.clear()
2427

2528

2629
def utf7_encode(value: str) -> bytes:
@@ -48,20 +51,20 @@ def _modified_unbase64(value: bytearray) -> str:
4851

4952
def utf7_decode(value: bytes) -> str:
5053
res = []
51-
decode_arr = bytearray()
54+
encoded_chars = bytearray()
5255
for char in value:
53-
if char == ord('&') and not decode_arr:
54-
decode_arr.append(ord('&'))
55-
elif char == ord('-') and decode_arr:
56-
if len(decode_arr) == 1:
56+
if char == AMPERSAND_ORD and not encoded_chars:
57+
encoded_chars.append(AMPERSAND_ORD)
58+
elif char == HYPHEN_ORD and encoded_chars:
59+
if len(encoded_chars) == 1:
5760
res.append('&')
5861
else:
59-
res.append(_modified_unbase64(decode_arr[1:]))
60-
decode_arr = bytearray()
61-
elif decode_arr:
62-
decode_arr.append(char)
62+
res.append(_modified_unbase64(encoded_chars[1:]))
63+
encoded_chars = bytearray()
64+
elif encoded_chars:
65+
encoded_chars.append(char)
6366
else:
6467
res.append(chr(char))
65-
if decode_arr:
66-
res.append(_modified_unbase64(decode_arr[1:]))
68+
if encoded_chars:
69+
res.append(_modified_unbase64(encoded_chars[1:]))
6770
return ''.join(res)

imap_tools/mailbox.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import imaplib
33
import datetime
44
from collections import UserString
5-
from typing import AnyStr, Optional, List, Iterable, Sequence, Union, Tuple, Iterator
5+
from typing import Optional, List, Iterable, Sequence, Union, Tuple, Iterator
66

77
from .message import MailMessage
88
from .folder import MailBoxFolderManager
99
from .idle import IdleManager
1010
from .consts import UID_PATTERN, PYTHON_VERSION_MINOR
1111
from .utils import clean_uids, check_command_status, chunks, encode_folder, clean_flags, check_timeout_arg_support, \
12-
chunks_crop
12+
chunks_crop, StrOrBytes
1313
from .errors import MailboxStarttlsError, MailboxLoginError, MailboxLogoutError, MailboxNumbersError, \
1414
MailboxFetchError, MailboxExpungeError, MailboxDeleteError, MailboxCopyError, MailboxFlagError, \
1515
MailboxAppendError, MailboxUidsError, MailboxTaggedResponseError
@@ -18,7 +18,7 @@
1818
# 20Mb is enough for search response with about 2 000 000 message numbers
1919
imaplib._MAXLINE = 20 * 1024 * 1024 # 20Mb
2020

21-
Criteria = Union[AnyStr, UserString]
21+
Criteria = Union[StrOrBytes, UserString]
2222

2323

2424
class BaseMailBox:
@@ -80,7 +80,7 @@ def login_utf8(self, username: str, password: str, initial_folder: Optional[str]
8080

8181
def xoauth2(self, username: str, access_token: str, initial_folder: Optional[str] = 'INBOX') -> 'BaseMailBox':
8282
"""Authenticate to account using OAuth 2.0 mechanism"""
83-
auth_string = 'user={}\1auth=Bearer {}\1\1'.format(username, access_token)
83+
auth_string = f'user={username}\1auth=Bearer {access_token}\1\1'
8484
result = self.client.authenticate('XOAUTH2', lambda x: auth_string) # noqa
8585
check_command_status(result, MailboxLoginError)
8686
if initial_folder is not None:
@@ -133,7 +133,7 @@ def uids(self, criteria: Criteria = 'ALL', charset: str = 'US-ASCII',
133133
encoded_criteria = criteria if type(criteria) is bytes else str(criteria).encode(charset)
134134
if sort:
135135
sort = (sort,) if isinstance(sort, str) else sort
136-
uid_result = self.client.uid('SORT', '({})'.format(' '.join(sort)), charset, encoded_criteria)
136+
uid_result = self.client.uid('SORT', f'({" ".join(sort)})', charset, encoded_criteria)
137137
else:
138138
uid_result = self.client.uid('SEARCH', 'CHARSET', charset, encoded_criteria) # *charset are opt here
139139
check_command_status(uid_result, MailboxUidsError)
@@ -187,8 +187,8 @@ def fetch(self, criteria: Criteria = 'ALL', charset: str = 'US-ASCII', limit: Op
187187
:param sort: criteria for sort messages on server, use SortCriteria constants. Charset arg is important for sort
188188
:return generator: MailMessage
189189
"""
190-
message_parts = "(BODY{}[{}] UID FLAGS RFC822.SIZE)".format(
191-
'' if mark_seen else '.PEEK', 'HEADER' if headers_only else '')
190+
message_parts = \
191+
f"(BODY{'' if mark_seen else '.PEEK'}[{'HEADER' if headers_only else ''}] UID FLAGS RFC822.SIZE)"
192192
limit_range = slice(0, limit) if type(limit) is int else limit or slice(None)
193193
assert type(limit_range) is slice
194194
uids = tuple((reversed if reverse else iter)(self.uids(criteria, charset, sort)))[limit_range]
@@ -218,7 +218,7 @@ def delete(self, uid_list: Union[str, Iterable[str]]) -> Optional[Tuple[tuple, t
218218
expunge_result = self.expunge()
219219
return store_result, expunge_result
220220

221-
def copy(self, uid_list: Union[str, Iterable[str]], destination_folder: AnyStr) -> Optional[tuple]:
221+
def copy(self, uid_list: Union[str, Iterable[str]], destination_folder: StrOrBytes) -> Optional[tuple]:
222222
"""
223223
Copy email messages into the specified folder
224224
Do nothing on empty uid_list
@@ -231,7 +231,7 @@ def copy(self, uid_list: Union[str, Iterable[str]], destination_folder: AnyStr)
231231
check_command_status(copy_result, MailboxCopyError)
232232
return copy_result
233233

234-
def move(self, uid_list: Union[str, Iterable[str]], destination_folder: AnyStr) -> Optional[Tuple[tuple, tuple]]:
234+
def move(self, uid_list: Union[str, Iterable[str]], destination_folder: StrOrBytes) -> Optional[Tuple[tuple, tuple]]:
235235
"""
236236
Move email messages into the specified folder
237237
Do nothing on empty uid_list
@@ -256,14 +256,13 @@ def flag(self, uid_list: Union[str, Iterable[str]], flag_set: Union[str, Iterabl
256256
if not uid_str:
257257
return None
258258
store_result = self.client.uid(
259-
'STORE', uid_str, ('+' if value else '-') + 'FLAGS',
260-
'({})'.format(' '.join(clean_flags(flag_set))))
259+
'STORE', uid_str, ('+' if value else '-') + 'FLAGS', f'({" ".join(clean_flags(flag_set))})')
261260
check_command_status(store_result, MailboxFlagError)
262261
expunge_result = self.expunge()
263262
return store_result, expunge_result
264263

265264
def append(self, message: Union[MailMessage, bytes],
266-
folder: AnyStr = 'INBOX',
265+
folder: StrOrBytes = 'INBOX',
267266
dt: Optional[datetime.datetime] = None,
268267
flag_set: Optional[Union[str, Iterable[str]]] = None) -> tuple:
269268
"""
@@ -281,7 +280,7 @@ def append(self, message: Union[MailMessage, bytes],
281280
cleaned_flags = clean_flags(flag_set or [])
282281
typ, dat = self.client.append(
283282
encode_folder(folder), # noqa
284-
'({})'.format(' '.join(cleaned_flags)) if cleaned_flags else None,
283+
f'({" ".join(cleaned_flags)})' if cleaned_flags else None,
285284
dt or datetime.datetime.now(timezone), # noqa
286285
message if type(message) is bytes else message.obj.as_bytes()
287286
)
@@ -339,10 +338,10 @@ def __init__(self, host='', port=993, timeout=None, keyfile=None, certfile=None,
339338

340339
def _get_mailbox_client(self) -> imaplib.IMAP4:
341340
if PYTHON_VERSION_MINOR < 9:
342-
return imaplib.IMAP4_SSL(self._host, self._port, self._keyfile, self._certfile, self._ssl_context)
341+
return imaplib.IMAP4_SSL(self._host, self._port, self._keyfile, self._certfile, self._ssl_context) # noqa
343342
elif PYTHON_VERSION_MINOR < 12:
344343
return imaplib.IMAP4_SSL(
345-
self._host, self._port, self._keyfile, self._certfile, self._ssl_context, self._timeout)
344+
self._host, self._port, self._keyfile, self._certfile, self._ssl_context, self._timeout) # noqa
346345
else:
347346
return imaplib.IMAP4_SSL(self._host, self._port, ssl_context=self._ssl_context, timeout=self._timeout)
348347

0 commit comments

Comments
 (0)