Skip to content

Commit

Permalink
flags logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ikvk committed Jun 25, 2021
1 parent 502c445 commit 5d534e7
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 64 deletions.
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ MailMessage and MailAttachment public attributes are cached by functools.lru_cac
msg.date_str # str: original date - 'Tue, 03 Jan 2017 22:26:59 +0500'
msg.text # str: 'Hello 你 Привет'
msg.html # str: '<b>Hello 你 Привет</b>'
msg.flags # tuple: ('SEEN', 'FLAGGED', 'ENCRYPTED')
msg.flags # tuple: ('\\Seen', '\\Flagged', 'ENCRYPTED')
msg.headers # dict: {'received': ('from 1.m.ru', 'from 2.m.ru'), 'anti-virus': ('Clean',)}
msg.size_rfc822 # int: 20664 bytes - size info from server (*useful with headers_only arg)
msg.size # int: 20377 bytes - size of received message
Expand Down Expand Up @@ -241,7 +241,7 @@ use 'limit' argument for fetch in this case.
# SEEN: flag as unseen all messages sent at 05.03.2007 in current folder, *in bulk
mailbox.seen(mailbox.fetch("SENTON 05-Mar-2007"), False)
# APPEND: add message to mailbox directly, to INBOX folder with SEEN flag and now date
# APPEND: add message to mailbox directly, to INBOX folder with \SEEN flag and now date
with open('/tmp/message.eml', 'rb') as f:
msg = imap_tools.MailMessage.from_bytes(f.read()) # *or use bytes instead MailMessage
mailbox.append(msg, 'INBOX', dt=None, flag_set=[imap_tools.MailMessageFlags.SEEN])
Expand Down Expand Up @@ -351,7 +351,8 @@ Big thanks to people who helped develop this library:
`bhernacki <https://github.com/bhernacki>`_,
`ilep <https://github.com/ilep>`_,
`ThKue <https://github.com/ThKue>`_,
`repodiac <https://github.com/repodiac>`_
`repodiac <https://github.com/repodiac>`_,
`tiuub <https://github.com/tiuub>`_

Donate
------
Expand Down
10 changes: 10 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
0.42.0
======
* Fixed MessageFlags values - all system flags begin with "\"
* Fixed BaseMailBox.flag, BaseMailBox.append, MailMessage.flags - now works with system/custom flags correctly, -upper
* Added utils.clean_flags
* Moved message.MessageFlags -> consts.MailMessageFlags
* Moved folder.MailBoxFolderStatusOptions -> consts.MailBoxFolderStatusOptions
* Moved utils.SHORT_MONTH_NAMES -> consts.SHORT_MONTH_NAMES
* Renamed utils.cleaned_uid_set -> utils.clean_uids

0.41.0
======
* Fixed multiple encodings case bug at MailMessage.subject
Expand Down
7 changes: 4 additions & 3 deletions imap_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .query import AND, OR, NOT, Header, UidRange, A, O, N, H, U
from .mailbox import BaseMailBox, MailBox, MailBoxUnencrypted
from .message import MailMessage, MailAttachment, MailMessageFlags
from .folder import MailBoxFolderManager, MailBoxFolderStatusOptions
from .message import MailMessage, MailAttachment
from .folder import MailBoxFolderManager
from .consts import MailMessageFlags, MailBoxFolderStatusOptions
from .errors import *

__version__ = '0.41.0'
__version__ = '0.42.0'
38 changes: 38 additions & 0 deletions imap_tools/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Lib constants"""

SHORT_MONTH_NAMES = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')


class MailMessageFlags:
"""
System email message flags
All system flags begin with "\"
"""
SEEN = '\\Seen'
ANSWERED = '\\Answered'
FLAGGED = '\\Flagged'
DELETED = '\\Deleted'
DRAFT = '\\Draft'
RECENT = '\\Recent'
all = (
SEEN, ANSWERED, FLAGGED, DELETED, DRAFT, RECENT
)


class MailBoxFolderStatusOptions:
"""Valid mailbox folder status options"""
MESSAGES = 'MESSAGES'
RECENT = 'RECENT'
UIDNEXT = 'UIDNEXT'
UIDVALIDITY = 'UIDVALIDITY'
UNSEEN = 'UNSEEN'
all = (
MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
)
description = (
(MESSAGES, "The number of messages in the mailbox"),
(RECENT, "The number of messages with the Recent flag set"),
(UIDNEXT, "The next unique identifier value of the mailbox"),
(UIDVALIDITY, "The unique identifier validity value of the mailbox"),
(UNSEEN, "The number of messages which do not have the Seen flag set"),
)
20 changes: 1 addition & 19 deletions imap_tools/folder.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import re

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


class MailBoxFolderStatusOptions:
"""Valid mailbox folder status options"""
MESSAGES = 'MESSAGES'
RECENT = 'RECENT'
UIDNEXT = 'UIDNEXT'
UIDVALIDITY = 'UIDVALIDITY'
UNSEEN = 'UNSEEN'
all = (
MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
)
description = (
(MESSAGES, "The number of messages in the mailbox"),
(RECENT, "The number of messages with the Recent flag set"),
(UIDNEXT, "The next unique identifier value of the mailbox"),
(UIDVALIDITY, "The unique identifier validity value of the mailbox"),
(UNSEEN, "The number of messages which do not have the Seen flag set"),
)


class MailBoxFolderManager:
"""Operations with mail box folders"""

Expand Down
26 changes: 12 additions & 14 deletions imap_tools/mailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import warnings
from email.errors import StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect

from .message import MailMessage, MailMessageFlags
from .consts import MailMessageFlags
from .message import MailMessage
from .folder import MailBoxFolderManager
from .utils import cleaned_uid_set, check_command_status, chunks, encode_folder
from .utils import clean_uids, check_command_status, chunks, encode_folder, clean_flags
from .errors import MailboxStarttlsError, MailboxLoginError, MailboxLogoutError, MailboxSearchError, \
MailboxFetchError, MailboxExpungeError, MailboxDeleteError, MailboxCopyError, MailboxFlagError, MailboxAppendError

Expand Down Expand Up @@ -124,7 +125,7 @@ def delete(self, uid_list) -> (tuple, tuple) or None:
Do nothing on empty uid_list
:return: None on empty uid_list, command results otherwise
"""
uid_str = cleaned_uid_set(uid_list)
uid_str = clean_uids(uid_list)
if not uid_str:
return None
store_result = self.box.uid('STORE', uid_str, '+FLAGS', r'(\Deleted)')
Expand All @@ -138,7 +139,7 @@ def copy(self, uid_list, destination_folder: str or bytes) -> tuple or None:
Do nothing on empty uid_list
:return: None on empty uid_list, command results otherwise
"""
uid_str = cleaned_uid_set(uid_list)
uid_str = clean_uids(uid_list)
if not uid_str:
return None
copy_result = self.box.uid('COPY', uid_str, encode_folder(destination_folder)) # noqa
Expand All @@ -152,7 +153,7 @@ def move(self, uid_list, destination_folder: str or bytes) -> (tuple, tuple) or
:return: None on empty uid_list, command results otherwise
"""
# here for avoid double fetch in uid_set
uid_str = cleaned_uid_set(uid_list)
uid_str = clean_uids(uid_list)
if not uid_str:
return None
copy_result = self.copy(uid_str, destination_folder)
Expand All @@ -163,17 +164,15 @@ def flag(self, uid_list, flag_set: [str] or str, value: bool) -> (tuple, tuple)
"""
Set/unset email flags
Do nothing on empty uid_list
Standard flags contains in message.MailMessageFlags.all
System flags contains in consts.MailMessageFlags.all
:return: None on empty uid_list, command results otherwise
"""
uid_str = cleaned_uid_set(uid_list)
uid_str = clean_uids(uid_list)
if not uid_str:
return None
if type(flag_set) is str:
flag_set = [flag_set]
store_result = self.box.uid(
'STORE', uid_str, ('+' if value else '-') + 'FLAGS',
'({})'.format(' '.join(('\\' + i for i in flag_set))))
'({})'.format(' '.join(clean_flags(flag_set))))
check_command_status(store_result, MailboxFlagError)
expunge_result = self.expunge()
return store_result, expunge_result
Expand All @@ -194,18 +193,17 @@ def append(self, message: MailMessage or bytes,
:param message: MailMessage object or bytes
:param folder: destination folder, INBOX by default
:param dt: email message datetime with tzinfo, now by default, imaplib.Time2Internaldate types supported
:param flag_set: email message flags, no flags by default. Standard flags at message.MailMessageFlags.all
:param flag_set: email message flags, no flags by default. System flags at consts.MailMessageFlags.all
:return: command results
"""
if sys.version_info.minor < 6:
timezone = datetime.timezone(datetime.timedelta(hours=0))
else:
timezone = datetime.datetime.now().astimezone().tzinfo # system timezone
if type(flag_set) is str:
flag_set = [flag_set]
cleaned_flags = clean_flags(flag_set)
typ, dat = self.box.append(
encode_folder(folder), # noqa
'({})'.format(' '.join(('\\' + i for i in flag_set))) if flag_set else None,
'({})'.format(' '.join(cleaned_flags)) if cleaned_flags else None,
dt or datetime.datetime.now(timezone),
message if type(message) is bytes else message.obj.as_bytes()
)
Expand Down
17 changes: 2 additions & 15 deletions imap_tools/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,6 @@
from .utils import decode_value, parse_email_addresses, parse_email_date


class MailMessageFlags:
"""Standard email message flags"""
SEEN = 'SEEN'
ANSWERED = 'ANSWERED'
FLAGGED = 'FLAGGED'
DELETED = 'DELETED'
DRAFT = 'DRAFT'
RECENT = 'RECENT'
all = (
SEEN, ANSWERED, FLAGGED, DELETED, DRAFT, RECENT
)


class MailMessage:
"""The email message"""

Expand Down Expand Up @@ -95,7 +82,7 @@ def flags(self) -> (str,):
result = []
for raw_flag_item in self._raw_flag_data:
result.extend(imaplib.ParseFlags(raw_flag_item))
return tuple(i.decode().strip().replace('\\', '').upper() for i in result) # noqa
return tuple(i.decode().strip() for i in result) # noqa

@property
@lru_cache()
Expand Down Expand Up @@ -227,7 +214,7 @@ def attachments(self) -> ['MailAttachment']:
for part in self.obj.walk():
if part.get_content_maintype() == 'multipart': # multipart/* are containers
continue
if part.get('Content-ID') is None and part.get_filename() is None \
if part.get('Content-ID') is None and part.get_filename() is None \
and part.get_content_type() != 'message/rfc822':
continue
results.append(MailAttachment(part))
Expand Down
5 changes: 3 additions & 2 deletions imap_tools/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import functools
import collections

from .utils import cleaned_uid_set, SHORT_MONTH_NAMES, quote
from .consts import SHORT_MONTH_NAMES
from .utils import clean_uids, quote


class LogicOperator(collections.UserString):
Expand Down Expand Up @@ -177,7 +178,7 @@ def cleaned_uid(key, value) -> str:
return str(value)
# set
try:
return cleaned_uid_set(value)
return clean_uids(value)
except TypeError as e:
raise TypeError('{} parse error: {}'.format(key, str(e)))

Expand Down
23 changes: 19 additions & 4 deletions imap_tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
from email.utils import getaddresses, parsedate_to_datetime
from email.header import decode_header, Header

from .consts import SHORT_MONTH_NAMES, MailMessageFlags
from . import imap_utf7

SHORT_MONTH_NAMES = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')


def cleaned_uid_set(uid_set: str or [str] or iter) -> str:
def clean_uids(uid_set: str or [str] or iter) -> str:
"""
Prepare set of uid for use in IMAP commands
:param uid_set:
Expand Down Expand Up @@ -38,7 +37,7 @@ def cleaned_uid_set(uid_set: str or [str] or iter) -> str:
raise TypeError('uid "{}" is not string'.format(str(uid)))
if not uid.strip().isdigit():
raise TypeError('Wrong uid: "{}"'.format(uid))
return ','.join((i.strip() for i in uid_set))
return ','.join(i.strip() for i in uid_set)


def check_command_status(command_result: tuple, exception: type, expected='OK'):
Expand Down Expand Up @@ -158,3 +157,19 @@ def encode_folder(folder: str or bytes) -> bytes:
return folder
else:
return quote(imap_utf7.encode(folder))


def clean_flags(flag_set: [str] or str) -> [str]:
"""
Check the correctness of the flags
:return: list of str - flags
"""
if type(flag_set) is str:
flag_set = [flag_set]
upper_sys_flags = tuple(i.upper() for i in MailMessageFlags.all)
for flag in flag_set:
if not type(flag) is str:
raise ValueError('Flag - str value expected, but {} received'.format(type(flag_set)))
if flag.upper() not in upper_sys_flags and flag.startswith('\\'):
raise ValueError('Non system flag must not start with "\\"')
return flag_set
19 changes: 15 additions & 4 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@

from imap_tools import utils
from imap_tools.errors import ImapToolsError, UnexpectedCommandStatusError, MailboxCopyError
from imap_tools.consts import MailMessageFlags


class UtilsTest(unittest.TestCase):

def test_cleaned_uid_set(self):
# *cleaned_uid_set tested enough in test_query.py
def test_clean_uids(self):
# *clean_uids tested enough in test_query.py
pass

def test_clean_flags(self):
self.assertEqual(utils.clean_flags([MailMessageFlags.FLAGGED, MailMessageFlags.SEEN]), ['\\Flagged', '\\Seen'])
self.assertEqual(utils.clean_flags(['\\FLAGGED', '\\seen']), ['\\FLAGGED', '\\seen'])
self.assertEqual(utils.clean_flags(['TAG1']), ['TAG1',])
self.assertEqual(utils.clean_flags(['tag2']), ['tag2',])
for flag in MailMessageFlags.all:
self.assertEqual(utils.clean_flags(flag), ['\\' + flag.replace('\\', '', 1).capitalize()])
with self.assertRaises(ValueError):
utils.clean_flags([MailMessageFlags.FLAGGED, '\\CUSTOM_TAG_WITH_SLASH'])

def test_chunks(self):
self.assertEqual(list(utils.chunks('ABCDE', 2, '=')), [('A', 'B'), ('C', 'D'), ('E', '=')])
self.assertEqual(list(utils.chunks([1, 2, 3, 4, 5, 6], 3)), [(1, 2, 3), (4, 5, 6)])
Expand Down Expand Up @@ -40,9 +51,9 @@ def test_check_command_status(self):
self.assertIsNone(utils.check_command_status(('EXP', 'command_result_data'), MailboxCopyError, expected='EXP'))
self.assertIsNone(utils.check_command_status(('OK', 'res'), UnexpectedCommandStatusError))
with self.assertRaises(TypeError):
self.assertFalse(utils.check_command_status(('NOT_OK', 'test'), ImapToolsError))
utils.check_command_status(('NOT_OK', 'test'), ImapToolsError)
with self.assertRaises(MailboxCopyError):
self.assertFalse(utils.check_command_status(('BYE', ''), MailboxCopyError, expected='OK'))
utils.check_command_status(('BYE', ''), MailboxCopyError, expected='OK')

def test_parse_email_date(self):
for val, exp in (
Expand Down

0 comments on commit 5d534e7

Please sign in to comment.