# Path: imap_tools/utils.py
# def decode_value(value: AnyStr, encoding: Optional[str] = None) -> str:
#     """Converts value to utf-8 encoding"""
#     if isinstance(encoding, str):
#         encoding = encoding.lower()
#     if isinstance(value, bytes):
#         try:
#             return value.decode(encoding or 'utf-8', 'ignore')
#         except LookupError:  # unknown encoding
#             return value.decode('utf-8', 'ignore')
#     return value
# 
# def parse_email_addresses(raw_header: Union[str, Header]) -> Tuple[EmailAddress, ...]:
#     """
#     Parse email addresses from header
#     :param raw_header: example: '=?UTF-8?B?0J7Qu9C1=?= <name@company.ru>,\r\n "\'\\"z, z\\"\'" <imap.tools@ya.ru>'
#     :return: (EmailAddress, ...)
#     """
#     result = []
#     if type(raw_header) is Header:
#         raw_header = decode_value(*decode_header(raw_header)[0])
#     for raw_name, email in getaddresses([raw_header.replace('\r\n', '').replace('\n', '')]):
#         name = decode_value(*decode_header(raw_name)[0]).strip()
#         email = email.strip()
#         if not (name or email):
#             continue
#         result.append(EmailAddress(
#             name=name,
#             email=email if '@' in email else '',
#             full='{} <{}>'.format(name, email) if name and email else name or email
#         ))
#     return tuple(result)
# 
# def parse_email_date(value: str) -> datetime.datetime:
#     """
#     Parsing the date described in rfc2822
#     Result datetime may be naive or with tzinfo
#     1900-1-1 for unparsed
#     """
#     try:
#         return parsedate_to_datetime(value)
#     except Exception:  # noqa
#         pass
#     match = re.search(
#         r'(?P<date>\d{1,2}\s+(' + '|'.join(SHORT_MONTH_NAMES) + r')\s+\d{4})\s+' +
#         r'(?P<time>\d{1,2}:\d{1,2}(:\d{1,2})?)\s*' +
#         r'(?P<zone_sign>[+-])?(?P<zone>\d{4})?',
#         value
#     )
#     if match:
#         group = match.groupdict()
#         day, month, year = group['date'].split()
#         time_values = group['time'].split(':')
#         zone_sign = int('{}1'.format(group.get('zone_sign') or '+'))
#         zone = group['zone']
#         try:
#             return datetime.datetime(
#                 year=int(year),
#                 month=SHORT_MONTH_NAMES.index(month) + 1,
#                 day=int(day),
#                 hour=int(time_values[0]),
#                 minute=int(time_values[1]),
#                 second=int(time_values[2]) if len(time_values) > 2 else 0,
#                 tzinfo=datetime.timezone(datetime.timedelta(
#                     hours=int(zone[:2]) * zone_sign,
#                     minutes=int(zone[2:]) * zone_sign
#                 )) if zone else None,
#             )
#         except ValueError:
#             pass
#     return datetime.datetime(1900, 1, 1)
# 
# class EmailAddress:
#     """Parsed email address info"""
#     __slots__ = 'name', 'email', 'full'
# 
#     def __init__(self, name: str, email: str, full: str):
#         self.name = name
#         self.email = email
#         self.full = full
# 
#     def __repr__(self):
#         return "{}(name={}, email={}, full={})".format(
#             self.__class__.__name__, repr(self.name), repr(self.email), repr(self.full))
# 
#     def __eq__(self, other):
#         return all(getattr(self, i) == getattr(other, i) for i in self.__slots__)
# 
# Path: imap_tools/consts.py
# UID_PATTERN = re.compile(r'(^|\s+|\W)UID\s+(?P<uid>\d+)')


import re
import email
import base64
import imaplib
import datetime
from itertools import chain
from functools import lru_cache
from email.header import decode_header
from typing import Tuple, Dict, Optional, List
from .utils import decode_value, parse_email_addresses, parse_email_date, EmailAddress
from .consts import UID_PATTERN
        return cls([(b'', raw_message_data)])

    @staticmethod
    def _get_message_data_parts(fetch_data: list) -> (bytes, bytes, List[bytes]):
        """
        :param fetch_data: Message object model
        :returns (raw_message_data: bytes, raw_uid_data: bytes, raw_flag_data: [bytes])
        *Elements may contain byte strings in any order, like: b'4517 (FLAGS (\\Recent NonJunk))'
        """
        raw_message_data = b''
        raw_uid_data = b''
        raw_flag_data = []
        for fetch_item in fetch_data:
            # flags
            if type(fetch_item) is bytes:
                raw_flag_data.append(fetch_item)
            # data, uid
            if type(fetch_item) is tuple:
                raw_uid_data = fetch_item[0]
                raw_flag_data.append(fetch_item[0])  # may contains flags (google, dovecot)
                raw_message_data = fetch_item[1]
        return raw_message_data, raw_uid_data, raw_flag_data

    @property
    @lru_cache()
    def uid(self) -> Optional[str]:
        """Message UID"""
        # _raw_uid_data - zimbra, yandex, gmail, gmx
        # _raw_flag_data - mail.ru, ms exchange server
        for raw_uid_item in [self._raw_uid_data] + self._raw_flag_data:

# Path: imap_tools/utils.py
# def decode_value(value: AnyStr, encoding: Optional[str] = None) -> str:
#     """Converts value to utf-8 encoding"""
#     if isinstance(encoding, str):
#         encoding = encoding.lower()
#     if isinstance(value, bytes):
#         try:
#             return value.decode(encoding or 'utf-8', 'ignore')
#         except LookupError:  # unknown encoding
#             return value.decode('utf-8', 'ignore')
#     return value
# 
# def parse_email_addresses(raw_header: Union[str, Header]) -> Tuple[EmailAddress, ...]:
#     """
#     Parse email addresses from header
#     :param raw_header: example: '=?UTF-8?B?0J7Qu9C1=?= <name@company.ru>,\r\n "\'\\"z, z\\"\'" <imap.tools@ya.ru>'
#     :return: (EmailAddress, ...)
#     """
#     result = []
#     if type(raw_header) is Header:
#         raw_header = decode_value(*decode_header(raw_header)[0])
#     for raw_name, email in getaddresses([raw_header.replace('\r\n', '').replace('\n', '')]):
#         name = decode_value(*decode_header(raw_name)[0]).strip()
#         email = email.strip()
#         if not (name or email):
#             continue
#         result.append(EmailAddress(
#             name=name,
#             email=email if '@' in email else '',
#             full='{} <{}>'.format(name, email) if name and email else name or email
#         ))
#     return tuple(result)
# 
# def parse_email_date(value: str) -> datetime.datetime:
#     """
#     Parsing the date described in rfc2822
#     Result datetime may be naive or with tzinfo
#     1900-1-1 for unparsed
#     """
#     try:
#         return parsedate_to_datetime(value)
#     except Exception:  # noqa
#         pass
#     match = re.search(
#         r'(?P<date>\d{1,2}\s+(' + '|'.join(SHORT_MONTH_NAMES) + r')\s+\d{4})\s+' +
#         r'(?P<time>\d{1,2}:\d{1,2}(:\d{1,2})?)\s*' +
#         r'(?P<zone_sign>[+-])?(?P<zone>\d{4})?',
#         value
#     )
#     if match:
#         group = match.groupdict()
#         day, month, year = group['date'].split()
#         time_values = group['time'].split(':')
#         zone_sign = int('{}1'.format(group.get('zone_sign') or '+'))
#         zone = group['zone']
#         try:
#             return datetime.datetime(
#                 year=int(year),
#                 month=SHORT_MONTH_NAMES.index(month) + 1,
#                 day=int(day),
#                 hour=int(time_values[0]),
#                 minute=int(time_values[1]),
#                 second=int(time_values[2]) if len(time_values) > 2 else 0,
#                 tzinfo=datetime.timezone(datetime.timedelta(
#                     hours=int(zone[:2]) * zone_sign,
#                     minutes=int(zone[2:]) * zone_sign
#                 )) if zone else None,
#             )
#         except ValueError:
#             pass
#     return datetime.datetime(1900, 1, 1)
# 
# class EmailAddress:
#     """Parsed email address info"""
#     __slots__ = 'name', 'email', 'full'
# 
#     def __init__(self, name: str, email: str, full: str):
#         self.name = name
#         self.email = email
#         self.full = full
# 
#     def __repr__(self):
#         return "{}(name={}, email={}, full={})".format(
#             self.__class__.__name__, repr(self.name), repr(self.email), repr(self.full))
# 
#     def __eq__(self, other):
#         return all(getattr(self, i) == getattr(other, i) for i in self.__slots__)
# 
# Path: imap_tools/consts.py
# UID_PATTERN = re.compile(r'(^|\s+|\W)UID\s+(?P<uid>\d+)')


# Path: imap_tools/message.py
import re
import email
import base64
import imaplib
import datetime
from itertools import chain
from functools import lru_cache
from email.header import decode_header
from typing import Tuple, Dict, Optional, List
from .utils import decode_value, parse_email_addresses, parse_email_date, EmailAddress
from .consts import UID_PATTERN

        return cls([(b'', raw_message_data)])

    @staticmethod
    def _get_message_data_parts(fetch_data: list) -> (bytes, bytes, List[bytes]):
        """
        :param fetch_data: Message object model
        :returns (raw_message_data: bytes, raw_uid_data: bytes, raw_flag_data: [bytes])
        *Elements may contain byte strings in any order, like: b'4517 (FLAGS (\\Recent NonJunk))'
        """
        raw_message_data = b''
        raw_uid_data = b''
        raw_flag_data = []
        for fetch_item in fetch_data:
            # flags
            if type(fetch_item) is bytes:
                raw_flag_data.append(fetch_item)
            # data, uid
            if type(fetch_item) is tuple:
                raw_uid_data = fetch_item[0]
                raw_flag_data.append(fetch_item[0])  # may contains flags (google, dovecot)
                raw_message_data = fetch_item[1]
        return raw_message_data, raw_uid_data, raw_flag_data

    @property
    @lru_cache()
    def uid(self) -> Optional[str]:
        """Message UID"""
        # _raw_uid_data - zimbra, yandex, gmail, gmx
        # _raw_flag_data - mail.ru, ms exchange server
        for raw_uid_item in [self._raw_uid_data] + self._raw_flag_data:

            uid_match = re.search(UID_PATTERN, raw_uid_item.decode())