Skip to content

Commit

Permalink
lru_cache->cached_property, f-strs, imap_utf7 speed, AnyStr->StrOrBytes
Browse files Browse the repository at this point in the history
  • Loading branch information
ikvk committed Jan 17, 2025
1 parent 3e3549b commit bccb5e0
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 169 deletions.
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Email attributes

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

MailMessage and MailAttachment public attributes are cached by functools.lru_cache
MailMessage and MailAttachment public attributes are cached by functools.cached_property

.. code-block:: python
Expand Down Expand Up @@ -428,7 +428,8 @@ Big thanks to people who helped develop this library:
`K900 <https://github.com/K900>`_,
`homoLudenus <https://github.com/homoLudenus>`_,
`sphh <https://github.com/sphh>`_,
`bh <https://github.com/bh>`_
`bh <https://github.com/bh>`_,
`tomasmach <https://github.com/tomasmach>`_

Help the project
----------------
Expand Down
5 changes: 5 additions & 0 deletions docs/dev_notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ icons
📨 📬 📪 📭 📫 ✉ 📧 🖂 🖃 🖅 📩


fetch
=====
FULL - Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)


code
====
def eml_to_python_structs(eml_path: str):
Expand Down
7 changes: 7 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
1.9.1
=====
* Replaced: functools.lru_cache to functools.cached_property
* Replaced: .format() to f''
* Optimized: speed for imap_utf7
* Replaced: typing.AnyStr to utils.StrOrBytes

1.9.0
=====
* Added: __str__ to MailAttachment
Expand Down
3 changes: 0 additions & 3 deletions docs/todo.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@

FULL
Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)

add servers for tests
rambler
GMX Mail
Expand Down
2 changes: 1 addition & 1 deletion imap_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
from .utils import EmailAddress
from .errors import *

__version__ = '1.9.0'
__version__ = '1.9.1'
4 changes: 2 additions & 2 deletions imap_tools/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def __init__(self, command_result: tuple, expected: Any):
self.expected = expected

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


class MailboxFolderSelectError(UnexpectedCommandStatusError):
Expand Down
23 changes: 11 additions & 12 deletions imap_tools/folder.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import re
from typing import AnyStr, Optional, Iterable, List, Dict, Tuple
from typing import Optional, Iterable, List, Dict, Tuple

from .imap_utf7 import utf7_decode
from .consts import MailBoxFolderStatusOptions
from .utils import check_command_status, pairs_to_dict, encode_folder
from .utils import check_command_status, pairs_to_dict, encode_folder, StrOrBytes
from .errors import MailboxFolderStatusValueError, MailboxFolderSelectError, MailboxFolderCreateError, \
MailboxFolderRenameError, MailboxFolderDeleteError, MailboxFolderStatusError, MailboxFolderSubscribeError

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

def __repr__(self):
return "{}(name={}, delim={}, flags={})".format(
self.__class__.__name__, repr(self.name), repr(self.delim), repr(self.flags))
return f"{self.__class__.__name__}(name={repr(self.name)}, delim={repr(self.delim)}, flags={repr(self.flags)})"

def __eq__(self, other):
return all(getattr(self, i) == getattr(other, i) for i in self.__slots__)
Expand All @@ -38,7 +37,7 @@ def __init__(self, mailbox):
self.mailbox = mailbox
self._current_folder = None

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

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

def rename(self, old_name: AnyStr, new_name: AnyStr) -> tuple:
def rename(self, old_name: StrOrBytes, new_name: StrOrBytes) -> tuple:
"""Rename folder from old_name to new_name"""
result = self.mailbox.client._simple_command(
'RENAME', encode_folder(old_name), encode_folder(new_name))
check_command_status(result, MailboxFolderRenameError)
return result

def delete(self, folder: AnyStr) -> tuple:
def delete(self, folder: StrOrBytes) -> tuple:
"""Delete folder"""
result = self.mailbox.client._simple_command('DELETE', encode_folder(folder))
check_command_status(result, MailboxFolderDeleteError)
return result

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

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

def subscribe(self, folder: AnyStr, value: bool) -> tuple:
def subscribe(self, folder: StrOrBytes, value: bool) -> tuple:
"""subscribe/unsubscribe to folder"""
method = self.mailbox.client.subscribe if value else self.mailbox.client.unsubscribe
result = method(encode_folder(folder))
Expand Down
31 changes: 17 additions & 14 deletions imap_tools/imap_utf7.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"""

import binascii
from typing import Iterable, MutableSequence
from typing import MutableSequence

AMPERSAND_ORD = ord('&')
HYPHEN_ORD = ord('-')


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


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


def utf7_encode(value: str) -> bytes:
Expand Down Expand Up @@ -48,20 +51,20 @@ def _modified_unbase64(value: bytearray) -> str:

def utf7_decode(value: bytes) -> str:
res = []
decode_arr = bytearray()
encoded_chars = bytearray()
for char in value:
if char == ord('&') and not decode_arr:
decode_arr.append(ord('&'))
elif char == ord('-') and decode_arr:
if len(decode_arr) == 1:
if char == AMPERSAND_ORD and not encoded_chars:
encoded_chars.append(AMPERSAND_ORD)
elif char == HYPHEN_ORD and encoded_chars:
if len(encoded_chars) == 1:
res.append('&')
else:
res.append(_modified_unbase64(decode_arr[1:]))
decode_arr = bytearray()
elif decode_arr:
decode_arr.append(char)
res.append(_modified_unbase64(encoded_chars[1:]))
encoded_chars = bytearray()
elif encoded_chars:
encoded_chars.append(char)
else:
res.append(chr(char))
if decode_arr:
res.append(_modified_unbase64(decode_arr[1:]))
if encoded_chars:
res.append(_modified_unbase64(encoded_chars[1:]))
return ''.join(res)
29 changes: 14 additions & 15 deletions imap_tools/mailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import imaplib
import datetime
from collections import UserString
from typing import AnyStr, Optional, List, Iterable, Sequence, Union, Tuple, Iterator
from typing import Optional, List, Iterable, Sequence, Union, Tuple, Iterator

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

Criteria = Union[AnyStr, UserString]
Criteria = Union[StrOrBytes, UserString]


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

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

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

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

def append(self, message: Union[MailMessage, bytes],
folder: AnyStr = 'INBOX',
folder: StrOrBytes = 'INBOX',
dt: Optional[datetime.datetime] = None,
flag_set: Optional[Union[str, Iterable[str]]] = None) -> tuple:
"""
Expand All @@ -281,7 +280,7 @@ def append(self, message: Union[MailMessage, bytes],
cleaned_flags = clean_flags(flag_set or [])
typ, dat = self.client.append(
encode_folder(folder), # noqa
'({})'.format(' '.join(cleaned_flags)) if cleaned_flags else None,
f'({" ".join(cleaned_flags)})' if cleaned_flags else None,
dt or datetime.datetime.now(timezone), # noqa
message if type(message) is bytes else message.obj.as_bytes()
)
Expand Down Expand Up @@ -339,10 +338,10 @@ def __init__(self, host='', port=993, timeout=None, keyfile=None, certfile=None,

def _get_mailbox_client(self) -> imaplib.IMAP4:
if PYTHON_VERSION_MINOR < 9:
return imaplib.IMAP4_SSL(self._host, self._port, self._keyfile, self._certfile, self._ssl_context)
return imaplib.IMAP4_SSL(self._host, self._port, self._keyfile, self._certfile, self._ssl_context) # noqa
elif PYTHON_VERSION_MINOR < 12:
return imaplib.IMAP4_SSL(
self._host, self._port, self._keyfile, self._certfile, self._ssl_context, self._timeout)
self._host, self._port, self._keyfile, self._certfile, self._ssl_context, self._timeout) # noqa
else:
return imaplib.IMAP4_SSL(self._host, self._port, ssl_context=self._ssl_context, timeout=self._timeout)

Expand Down
Loading

0 comments on commit bccb5e0

Please sign in to comment.