<?php

namespace Mailbox\Infrastructure\Repositories;

use Mailbox\Application\Config;
use Mailbox\Application\MailBoxSearchCriteria;
use Mailbox\Domain\Attachment\Attachment;
use Mailbox\Domain\Attachment\AttachmentCollection;
use Mailbox\Domain\MailBox\Contracts\MailBoxCriteriaInterface;
use Mailbox\Domain\MailBox\MailBox;
use Mailbox\Domain\MailBox\MailBoxCollection;
use Mailbox\Domain\Message\Contracts\MessageAttachmentCriteriaInterface;
use Mailbox\Domain\Message\Contracts\MessageCriteriaInterface;
use Mailbox\Domain\Message\Contracts\MessageRepositoryInterface;
use Mailbox\Domain\Message\Message;
use Mailbox\Domain\Message\MessageCollection;
use Mailbox\Domain\Message\VolueObject\From;
use Mailbox\Infrastructure\ImapAdapter\ImapException;
use Mailbox\Infrastructure\ProtocolAdapter\ProtocolException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use RecursiveIteratorIterator;
use Zend\Mail\Exception\ExceptionInterface;
use Zend\Mail\Exception\RuntimeException;
use Zend\Mail\Header\Date;
use Zend\Mail\Header\Exception\InvalidArgumentException;
use Zend\Mail\Header\HeaderWrap;
use Zend\Mail\Headers;
use Zend\Mail\Storage;
use Zend\Mail\Storage\Exception\InvalidArgumentException as StorageInvalidArgumentException;
use Zend\Mail\Storage\Folder;
use Zend\Mail\Storage\Message as ZendMessage;
use Zend\Mail\Storage\Part;

class ImapMessageRepository implements MessageRepositoryInterface, LoggerAwareInterface
{
    use Helpers\FileHelperTrait;
    use Helpers\DateHelperTrait;
    use Helpers\StringHelperTrait;
    use Helpers\CacheHelperTrait;

    /**
     * @var Config
     */
    protected $config;

    /**
     * переменная Кеша папок сервера
     * @var array
     */
    protected $cacheFolders = [];


    /**
     * @var FilesystemAdapter
     */
    protected $cacheAdapter;


    protected $protocol;

    protected $logger;

    private $delimiter;

    /**
     * ImapMessageRepository constructor.
     * @param ProtocolInterface $protocol
     * @param RepositoryConfigInterface $config
     */
    public function __construct(ProtocolInterface $protocol, RepositoryConfigInterface $config)
    {

        $this->config = $config;
        $this->logger = new NullLogger();
        $this->protocol = $protocol;

        /** @noinspection PhpParamsInspection */
        $this->initCache($config);

        if (!$this->config->getAttachmentsDir()) {
            throw new RepositoryException('Attachment dir not set');
        }

        if (
            !is_dir($this->createAttachmentsDirName()) &&
            (!mkdir($this->createAttachmentsDirName(), 0777, true) ||
                !is_dir($this->createAttachmentsDirName())
            )
        ) {
            throw new RepositoryException('Not create attachment dir ' . $this->createAttachmentsDirName());
        }
    }

    /**
     * @param LoggerInterface $logger
     */
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;

        if ($this->config->isSocketDebug() === true) {
            $this->protocol->setLogger($logger);
        }
    }


    /**
     * @param int $id
     * @return Message|null
     * @throws \Exception
     */
    public function findById(int $id): ?Message
    {
        $message = $messageData = null;
        $folder = $this->protocol->getCurrentFolder();

        for ($i = 0; $i < 2; $i++) {
            //пробуем получить вложение в противном не найдено
            try {
                $this->logger->debug(sprintf('Получение %d-го письма с вложением', $id));
                $messageData = $this->getMessageFromProtocol($id);
                $message = $this->buildMessageFromMessageData($messageData, $id);//, $currentMailBox
                break;
            } catch (ProtocolException  $ex) {
                $this->logger->debug('пробуем переподключится так как сервер отдал: ' . $ex->getMessage());
                $this->protocol->selectFolder($folder);
                $this->logger->debug('переподключение выполнено: ' . $this->protocol->getCurrentFolder());
            } catch (Storage\Exception\InvalidArgumentException $ex) {
                $this->logger->error($ex->getMessage());
                return null;
            } catch (RuntimeException $ex) {
                $this->logger->error($ex->getMessage());
                return null;
            }
        }

        if (!$message || $i >= 2) {
            $this->logger->debug('сообщение не найдено');
            return null;
        }

        //пробуем вытянуть вложения в противном выходим просто с письмом
        try {
            foreach (new RecursiveIteratorIterator($messageData) as $partNumber => $partStorage) {
                $this->createAttachment($partNumber, $partStorage, $message);
            }

            //пробуем добавить вложение костыльным способом
            if ($message->getAttachmentCollection()->count() === 0) {
                $this->createAttachment(1, $messageData, $message);
            }
        } catch (ExceptionInterface $ex) {
            $this->logger->error($ex->getMessage());
        }

        if (!$message->isFlagSeen()) {
            $this->setFlagUnSeen($message->getId());
        }

        return $message;
    }

    /**
     * @param int $id
     * @return ZendMessage
     */
    protected function getMessageFromProtocol(int $id): ZendMessage
    {
        $uid = $this->protocol->getUidById($id);

        $key = $uid;

        if (!$raw = $this->needInCache($key)) {
            $this->logger->debug(sprintf('Получение тела %d-го письма - качаем из источника', $id));
            $raw = $this->protocol->getRawContentById($id);
            $this->saveInCache($raw, $key);
        }

        $decode = utf8_decode($raw);
        if (strlen($decode) !== strlen($raw)) {
            $raw = $decode;
            $this->logger->debug('use utf8_decode');
        }

        try {
            $message = new Storage\Message([
                'raw' => $raw
            ]);
        } catch (InvalidArgumentException $ex) {
            $this->logger->error($ex);
            throw new ImapException('message not parse');
        }

        return $message;
    }

    /**
     * @param int $id
     * @return ZendMessage
     */
    protected function getHeaderFromProtocol(int $id): ZendMessage
    {

        try {
            $uid = $this->protocol->getUidById($id);
        } catch (ProtocolException $ex) {
            throw new ImapException($ex->getMessage());
        }


        $key = 'header_' . $uid;

        if (!$raw = $this->needInCache($key)) {
            $this->logger->debug(sprintf('Получение заголовка %d-го письма - качаем из источника', $id));
            $raw = $this->protocol->getRawHeaderById($id);
            $this->saveInCache($raw, $key);
        }

        $decode = utf8_decode($raw);
        if (strlen($decode) !== strlen($raw)) {
            $raw = $decode;
            $this->logger->debug('use utf8_decode');
        }

//dump($raw);
        try {
            $header = new Storage\Message([
                'raw' => $raw
            ]);
        } catch (InvalidArgumentException $ex) {
            $this->logger->error($ex);
            throw new ImapException('header not parse');
        }
        return $header;
    }

    /**
     * @param $partNumber
     * @param ZendMessage $partStorage
     * @param Message $message
     * @throws \Repo\Concrete\Exceptions\Collection
     */
    private function createAttachment($partNumber, ZendMessage $partStorage, Message $message): void
    {
        /**
         * @var ZendMessage $partStorage
         */
        if (!$fileName = $this->find($partStorage)) {
            return;
        }

        $attachment = new Attachment($fileName);
        $attachment->setId($partNumber);
        $attachment->setMessageId($message->getId());
        $attachment->setMessageKey($message->getUid());

        //если есть каталог для вложений есть будем качать в него

        $this->saveAttachment($attachment, $partStorage);

        $message->getAttachmentCollection()->push($attachment);
    }


    /**
     * @param ZendMessage $messageData
     * @param int $id
     * @return Message
     * @throws \Repo\Concrete\Exceptions\Collection
     */
    protected function buildMessageFromMessageData(Storage\Message $messageData, int $id): Message
    {

        $flagSeen = $messageData->hasFlag(Storage ::FLAG_SEEN);

        try {
            $datetime = self::toDatetime($messageData->getHeader('date', 'string'));
        } catch (Storage\Exception\InvalidArgumentException $ex) {
            /**
             * @todo временный костыль для zend либы
             */
            $datetime = new \DateTime();
        }

        $addressee = self::toAddressee($messageData->getHeader('from', 'string'));

        $currentMailBox = $this->findMailBoxesByCriteria(
            (new MailBoxSearchCriteria())->setFilterById($this->protocol->getCurrentFolder())
        )->current();

        try {
            $messageId = $messageData->getHeader('messageId', 'string');
        } catch (StorageInvalidArgumentException $ex) {
            $messageId = sprintf('<%s@auto.generated>' . "\r\n", md5($id));
        }

        $message =
            (new Message())
                ->setId($id)
                ->setFlagSeen($flagSeen)
                ->setMessageId($messageId)
                ->setUid($this->protocol->getUidById($id))
                ->setFromEmail(new From($addressee['from'], $addressee['fromName']))
                ->setDateTime($datetime)
                //если понадобятся body песем можно будет открыть но и корректировка тестово необходима будет
                //->setBody(
                //$messageData->countParts() > 0 ? $messageData->getPart(1)->getContent():$messageData->getContent())
                ->setMailBox($currentMailBox);

        //на случай если прилетело некодированная тема
        if ($messageData->getHeaders()->has('subject')) {
            $subject = $messageData->getHeader('subject', 'string');

            if (strpos($subject, '=?') !== false) {
                $subject = array_reduce(
                    imap_mime_header_decode(imap_utf8($subject)),
                    function ($accumulator, $headerPart) {
                        return $accumulator . $headerPart->text;
                    },
                    ''
                );
            }
            $message->setSubject($subject);
        }

        $this->logger->debug('build message', [
            $message->getSubject(),
            $message->getFromEmail()->getEmail(),
            $message->getDateTime()->format('Y-m-d H:i')
        ]);

        return $message;
    }

    /**
     * Попытка поставить флаг прочитанного сообщения.
     *
     * @param int $messageNumber
     */
    protected function setFlagSeen(int $messageNumber): void
    {
        $this->protocol->setFlags($messageNumber, [Storage::FLAG_SEEN]);
        $this->logger->debug('Помечено как прочтенное');
    }

    /**
     * Попытка поставить флаг непрочитанного сообщения.
     *
     * @param int $messageNumber
     */
    protected function setFlagUnSeen(int $messageNumber): void
    {
        try {
            $this->protocol->setFlags($messageNumber, [Storage::FLAG_UNSEEN]);
            $this->logger->debug('Помечено как не прочтенное');
        } catch (\Exception $e) {
            try {
                $this->protocol->unsetFlags($messageNumber, [Storage::FLAG_SEEN]);
                $this->logger->debug('Снята пометка о прочтении');
            } catch (\Exception $e) {
                $this->logger->error($e->getMessage());
            }
        }
    }


    /**
     * @return string
     */
    protected function createAttachmentsDirName(): string
    {
        return $this->config->getAttachmentsDir() . '/' . $this->config->getLogin();
    }


    /**
     * сохранение содержимого вложения в файл
     * @param Attachment $attachment
     * @param ZendMessage $part
     */
    protected function saveAttachment(Attachment $attachment, ZendMessage $part): void
    {

        if (!$attachment->getMessageKey()) {
            throw new RepositoryException('message key not ben empty');
        }

        $attachmentDir =
            $this->createAttachmentsDirName() .
            '/' .
            str_replace('.', '_', $this->normalizeFileName(
                $attachment->getMessageKey()
            )) .
            '/' . $attachment->getId();

        /** @noinspection MkdirRaceConditionInspection */
        if (!is_dir($attachmentDir) && !mkdir($attachmentDir, 0750, true)) {
            throw new RepositoryException('error create dir ' . $attachmentDir);
        }

        $filePath = $attachmentDir . '/' . $this->normalizeFileName($attachment->getFileName());

        if (!file_exists($filePath)) {
            try {
                // $encoding = $part->getHeaders()->getEncoding();
                $attachmentContent = $part->getContent();
            } catch (RuntimeException $ex) {
                //$encoding = "base64";//по умолчанию попробуем этот вариант в случае если пошло не так
                $attachmentContent = $part->getContent();
            }

            $encoding = $this->findEncoding($part->getHeaders());

            if (false === file_put_contents($filePath, $this->decodeContent($attachmentContent, $encoding))) {
                throw new RepositoryException('Not save attachment content in file ' . $attachment->getFileName());
            }
        }

        $attachment->setFilePath($filePath);
    }

    /**
     * @param Headers $headers
     * @return string
     */
    protected function findEncoding(Headers $headers): string
    {

        $transferEncoding = $headers->get('contenttransferencoding');

        return $transferEncoding->getFieldValue();
    }

    /**
     * кодируем
     * @param string $body
     * @param string $encoding
     * @return string
     */
    private function decodeContent(string $body, string $encoding): string
    {

        switch (strtolower($encoding)) {
            case 'binary':
                $body = imap_binary($body);
                break;
            case 'base64':
                $body = imap_base64($body);
                break;
            case 'quoted-printable':
                $body = quoted_printable_decode($body);
                break;
            case 'ascii':
                $body = iconv('CP1251', 'UTF-8', quoted_printable_decode($body));
                break;
            case '7bit':
                $body = $this->decode7Bit($body);
                break;
            default:
                break;
        }

        return $body;
    }

    /**
     * function to decode 7BIT encoded message
     *
     * @param string $text
     * @return string
     */
    private function decode7Bit(string $text): string
    {
        // Manually convert common encoded characters into their UTF-8 equivalents.
        $characters = array(
            '=20' => ' ', // space.
            '=E2=80=99' => "'", // single quote.
            '=0A' => "\r\n", // line break.
            '=A0' => ' ', // non-breaking space.
            '=C2=A0' => ' ', // non-breaking space.
            "=\r\n" => '', // joined line.
            '=E2=80=A6' => '…', // ellipsis.
            '=E2=80=A2' => '•', // bullet.
        );

        // Loop through the encoded characters and replace any that are found.
        foreach ($characters as $key => $value) {
            $text = str_replace($key, $value, $text);
        }
        return $text;
    }


    /**
     * Основной метод поиска писем
     * @param MessageCriteriaInterface $criteria
     * @return MessageCollection
     * @throws \Exception
     */
    public function findByCriteria(MessageCriteriaInterface $criteria): MessageCollection
    {
        $this->logger->debug('Лимит поиска сообщений: ' . $this->config->getLimit());
        $this->logger->debug('Лимит коллекции: ' . $criteria->getLimit());

        //ищем нужную папку
        try {
            $mailBoxCollection = $this->findMailBoxesByCriteria(
                (new MailBoxSearchCriteria())
                    ->setFilterByName(
                        !$criteria->getFilterByMailBoxName() ? 'inbox' : $criteria->getFilterByMailBoxName()
                    )
            );
        } catch (ProtocolException $e) {
            throw new RepositoryException($e->getMessage());
        }

        /**
         * @var MailBox $mailBox
         */
        $mailBox = $mailBoxCollection->current();

        if (!$mailBox) {
            throw new RepositoryException('Cannot find folder, maybe it does not exist.');
        }

        //переключаемся на нужную папку
        try {
            $this->protocol->selectFolder($mailBox->getId());
        } catch (ExceptionInterface $e) {
            throw new RepositoryException('Cannot find folder, maybe it does not exist.');
        }

        $this->logger->debug(
            sprintf('Успешное подключение к папке `%s`, получаем кол-во сообщений ...', $mailBox->getId())
        );

        try {
            $numberOfMessages = $this->protocol->countMessages();
        } catch (ExceptionInterface $e) {
            throw new RepositoryException($e->getMessage());
        }

        $this->protocol->noop();

        $this->logger->debug(sprintf('найдено %d сообщений', $numberOfMessages));

        $this->logger->debug('Создание коллекции');
        $messageCollection = new MessageCollection();

        $configLimit = $this->config->getLimit();

        //проход по номерам
        $this->logger->debug('проход по номерам');
        for ($messageNumber = $numberOfMessages; $messageNumber >= 1; $messageNumber--) {
            $this->protocol->noop();

            $this->logger->debug('Текущий элемент: ' . $messageNumber);

            if (null !== $configLimit && $numberOfMessages - $messageNumber > $configLimit) {
                $this->logger->debug('Выходим по лимиту просмотренных сообщений');
                break;
            }

            //формируем письмо на основе заголовка
            try {
                $headerPart = $this->getHeaderFromProtocol($messageNumber);
            } catch (ImapException  | RuntimeException $ex) {
                $this->logger->error($ex->getMessage());
                $this->logger->debug('Не удалось распарсить сообщение, в лог добавленна ошибка, продолжнаем поиск ...');
                continue;
            }

            try {
                $messagePart = $this->buildMessageFromMessageData($headerPart, $messageNumber);
            } catch (Storage\Exception\InvalidArgumentException $ex) {
                $this->logger->error($ex->getMessage());
                continue;
            }

            if (!$messagePart) {
                continue;
            }

            //если фильтр не отработал с заголовком письма то пропускаем
            if (!$checkAddCollection = $this->checkAddCollection($messagePart, $criteria)) {
                continue;
            }

            //фильтр пройден делаем запрос на полное письмо
            $message = $this->findById($messageNumber);

            if (!$message) {
                continue;
            }

            $this->logger->debug('Критерии письма совпадают, проверяем вложение...');

            //фильтр по вложениям
            /** @noinspection PhpParamsInspection */
            if (!$checkAddCollection = $this->checkAddAttachmentCollection($message, $criteria)) {
                continue;
            }

            $this->logger->debug('Письмо и вложение подходит под критерии поиска');

            $messageCollection->push($message);

            if ($criteria->isMarkSeenIfFound() === true) {
                $this->setFlagSeen($message->getId());
            }

            $limit = $criteria->getLimit();
            if (null !== $limit && $messageCollection->count() >= $limit) {
                $this->logger->debug('Выходим по лимиту коллекции');
                break;
            }
        }

        $this->logger->debug(sprintf('Возвращаем %s писем', $messageCollection->count()));

        return $messageCollection;
    }


    /**
     * Нахождение прикрепленного файла.
     *
     * @param Part $part
     *
     * @return null|string
     */
    protected function find(Part $part): ?string
    {
        try {
            $fileName = $this->getFileName($part);
        } catch (RuntimeException $e) {
            return null;
        }

        if ($fileName === null) {
            return null;
        }

        return $this->clearFileName($fileName);
    }


    /**
     * @param Part $part
     * @return string|null
     */
    private function getFileNameWithRegexp(Part $part): ?string
    {
        if (
            $part->getHeaders()->has('Content-Disposition')
            && preg_match(
                '~.*?filename.*?=\s*\"*(.*[^\"])\s*\"*$~isu',
                $part->getHeader('Content-Disposition')->getFieldValue(),
                $matches
            ) !== false
            && !empty($matches[1])
        ) {
            return $matches[1];
        }

        return null;
    }

    /**
     * @param Part $part
     * @return string|null
     */
    private function getFileNameWithContentType(Part $part): ?string
    {
        $contentType = $part->getHeader('ContentType');

        if (is_object($contentType) && method_exists($contentType, 'getParameters')) {
            $result = $contentType->getParameters();
            if (isset($result['name'])) {
                $parts = imap_mime_header_decode($result['name']);
                if (count($parts)) {
                    $subject = '';
                    foreach ($parts as $subpart) {
                        $subject .= trim($subpart->text);
                    }
                    $mailCharset = mb_detect_encoding($subject, array('UTF-8', 'GBK', 'LATIN1', 'BIG5'));
                    $subject = mb_convert_encoding($subject, 'utf-8', $mailCharset);
                    return $subject;
                }

                return $result['name'];
            }
        }

        return null;
    }

    /**
     * Нахождение названия прикрепленного файла.
     *
     * @param Part $part
     * @return null|string
     */
    protected function getFileName(Part $part): ?string
    {
        $results = [];

        //первый вариант разбора
        $results [] = $this->getFileNameWithRegexp($part);

        //второй вариант разбора
        $results [] = $this->getFileNameWithContentType($part);

        //третий вариант самый лайтовый
        if ($part->getHeaders()->has('X-Attachment-Id')) {
            $results [] = $part->getHeader('X-Attachment-Id')->getFieldValue();
        }

        foreach ($results as $fileName) {
            if (HeaderWrap::canBeEncoded($fileName)) {
                $fileName = array_reduce(
                    imap_mime_header_decode(imap_utf8($fileName)),
                    function ($accumulator, $headerPart) {
                        return $accumulator . $headerPart->text;
                    },
                    ''
                );
            }

            if (preg_match("~.+?\.([a-z]+)$~is", $fileName)) {
                return $fileName;
            }
        }

        return null;
    }


    /**
     * фильтро по свойствам письма
     * @param Message $message
     * @param MessageCriteriaInterface $messageCriteria
     * @return bool
     */
    protected function checkAddCollection(Message $message, MessageCriteriaInterface $messageCriteria): bool
    {

        $filterByUid = $messageCriteria->getFilterByUid();

        if (null !== $filterByUid && $filterByUid !== $message->getUid()) {
            return false;
        }

        $filterById = $messageCriteria->getFilterById();

        if (null !== $filterById && $filterById !== $message->getId()) {
            return false;
        }

        $timeFrom = $messageCriteria->getFilterByDateTimeFrom();
        $timeTo = $messageCriteria->getFilterByDateTimeTo();

        $messageTime = $message->getDateTime()->getTimestamp();

        //2019-09-30 (filter) > 2019-09-30 (message)
        if ($timeTo !== null && $messageTime > $timeTo->getTimestamp()) {
            return false;
        }

        if ($timeFrom !== null && $messageTime < $timeFrom->getTimestamp()) {
            return false;
        }

        if (
            !empty($messageCriteria->getFilterByFromEmails()) &&
            !in_array(strtolower($message->getFromEmail()->getEmail()), $messageCriteria->getFilterByFromEmails())
        ) {
            return false;
        }
        $subject = mb_strtolower($message->getSubject());
        $searchSubject = mb_strtolower($messageCriteria->getSearchBySubject());

        if (!empty($searchSubject) && strpos($subject, $searchSubject) === false) {
            return false;
        }

        return true;
    }

    /**
     * фильтр по коллекции вложений
     * @param Message $message
     * @param MessageAttachmentCriteriaInterface $messageCriteria
     * @return bool
     * @throws \Repo\Concrete\Exceptions\Collection
     */
    protected function checkAddAttachmentCollection(
        Message $message,
        MessageAttachmentCriteriaInterface $messageCriteria
    ): bool {
        if (
            !empty($messageCriteria->getSearchByAttachmentName()) ||
            !empty($messageCriteria->getFilterByAttachmentExt()) ||
            !empty($messageCriteria->getFilterByAttachmentRegexp())
        ) {
            $messageCollectionNew = new AttachmentCollection();

            foreach ($message->getAttachmentCollection() as $attachment) {


                /**
                 * @var Attachment $attachment
                 */
                if (
                    $messageCriteria->getSearchByAttachmentName() !== null &&
                    stripos($attachment->getFileName(), $messageCriteria->getSearchByAttachmentName()) === false
                ) {
                    continue;
                }

                if (
                    count($messageCriteria->getFilterByAttachmentExt()) &&
                    !preg_match("~.*\.("
                        . implode('|', $messageCriteria->getFilterByAttachmentExt())
                        . ')$~is', $attachment->getFileName())
                ) {
                    continue;
                }

                if (
                    $messageCriteria->getFilterByAttachmentRegexp() !== null &&
                    $this->findNameByRule(
                        $attachment->getFileName(),
                        $messageCriteria->getFilterByAttachmentRegexp()
                    ) === false
                ) {
                    continue;
                }

                $messageCollectionNew->push($attachment);
            }


            if ($messageCollectionNew->count() === 0) {
                return false;
            }

            $message->setAttachmentCollection($messageCollectionNew);
        }

        return true;
    }

    /**
     * @param MailBoxCriteriaInterface $criteria
     * @return MailBoxCollection
     * @throws \Repo\Concrete\Exceptions\Collection
     */
    public function findMailBoxesByCriteria(MailBoxCriteriaInterface $criteria): MailBoxCollection
    {

        if (empty($this->cacheFolders)) {
            $this->cacheFolders = $folderList = new RecursiveIteratorIterator(
                $this->getFoldersFromProtocol(),
                RecursiveIteratorIterator::SELF_FIRST
            );
        } else {
            $folderList = $this->cacheFolders;
        }

        $mailBoxCollection = new MailBoxCollection();

        foreach ($folderList as $key => $folder) {
            /* @var Folder $folder */

            $localName = hex2bin(str_replace(
                'c2a0',
                '20',
                bin2hex(mb_convert_encoding($folder->getLocalName(), 'UTF-8', 'UTF7-IMAP'))
            ));

            if ($criteria->getfilterByName() !== null && strcasecmp($localName, $criteria->getfilterByName())) {
                continue;
            }

            if (
                $criteria->getfilterById() !== null
                && strcasecmp($folder->getGlobalName(), $criteria->getfilterById())
            ) {
                continue;
            }

            $mailBoxCollection->push(
                (new  MailBox($folder->getLocalName()))
                    ->setId($folder->getGlobalName())
            );
        }

        return $mailBoxCollection;
    }


    /**
     * @return Folder
     */
    protected function getFoldersFromProtocol(): Folder
    {

        if (!$folders = $this->needBoxesInCache()) {
            $folders = $this->protocol->getFolders();
            $this->saveBoxesInCache($folders);
        }


        if (!$folders) {
            throw new StorageInvalidArgumentException('folder not found');
        }

        ksort($folders, SORT_STRING);
        $root = new Folder('/', '/', false);
        $stack = [null];
        $folderStack = [null];
        $parentFolder = $root;
        $parent = '';

        foreach ($folders as $globalName => $data) {
            do {
                if (!$parent || strpos($globalName, $parent) === 0) {
                    $pos = strrpos($globalName, $data['delim']);
                    if ($pos === false) {
                        $localName = $globalName;
                    } else {
                        $localName = substr($globalName, $pos + 1);
                    }
                    $selectable = !$data['flags'] || !in_array('\\Noselect', $data['flags'], true);

                    $stack[] = $parent;
                    $parent = $globalName . $data['delim'];
                    $folder = new Folder($localName, $globalName, $selectable);
                    $parentFolder->$localName = $folder;
                    $folderStack[] = $parentFolder;
                    $parentFolder = $folder;
                    $this->delimiter = $data['delim'];
                    break;
                }

                if ($stack) {
                    $parent = array_pop($stack);
                    $parentFolder = array_pop($folderStack);
                }
            } while ($stack);
            if (!$stack) {
                throw new RuntimeException('error while constructing folder tree');
            }
        }

        return $root;
    }

    /**
     * @param Message $message
     * @param MailBox $mailBox
     */
    public function moveMessage(Message $message, MailBox $mailBox): void
    {
        try {
            $this->protocol->moveMessage($message->getId(), 'INBOX.' . $mailBox->getName());
        } catch (\Exception $ex) {
            $this->protocol->moveMessage($message->getId(), $mailBox->getName());
        }
    }
}
