YetiForceCompany/YetiForceCRM

View on GitHub
modules/OSSMail/models/Record.php

Summary

Maintainability
F
6 days
Test Coverage
<?php
/**
 * OSSMail record model file.
 *
 * @package Model
 *
 * @copyright YetiForce S.A.
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
 * @author    Radosław Skrzypczak <r.skrzypczak@yetiforce.com>
 * @author    Mariusz Krzaczkowski <m.krzaczkowski@yetiforce.com>
 */
/**
 * OSSMail record model class.
 */
class OSSMail_Record_Model extends Vtiger_Record_Model
{
    /** @var int Mailbox Status: Active */
    const MAIL_BOX_STATUS_ACTIVE = 0;

    /** @var int Mailbox Status: Invalid access data */
    const MAIL_BOX_STATUS_INVALID_ACCESS = 1;

    /** @var int Mailbox Status: Blocked temporarily */
    const MAIL_BOX_STATUS_BLOCKED_TEMP = 2;

    /** @var int Mailbox Status: Disabled */
    const MAIL_BOX_STATUS_DISABLED = 3;

    /** @var int Mailbox Status: Blocked permanently */
    const MAIL_BOX_STATUS_BLOCKED_PERM = 4;

    /** @var string[] Mailbox status labels */
    const MAIL_BOX_STATUS_LABELS = [
        self::MAIL_BOX_STATUS_INVALID_ACCESS => 'LBL_ACCOUNT_INVALID_ACCESS',
        self::MAIL_BOX_STATUS_DISABLED => 'LBL_ACCOUNT_IS_DISABLED',
        self::MAIL_BOX_STATUS_BLOCKED_TEMP => 'LBL_ACCOUNT_IS_BLOCKED_TEMP',
        self::MAIL_BOX_STATUS_BLOCKED_PERM => 'LBL_ACCOUNT_IS_BLOCKED_PERM',
    ];

    /**
     * Get status label.
     *
     * @param int $status
     *
     * @return string
     */
    public static function getStatusLabel(int $status): string
    {
        return self::MAIL_BOX_STATUS_LABELS[$status];
    }

    /**
     * Return accounts array.
     *
     * @param int|bool $user
     * @param bool     $onlyMy
     * @param bool     $password
     * @param bool     $onlyActive
     *
     * @return array
     */
    public static function getAccountsList($user = false, bool $onlyMy = false, bool $password = false, bool $onlyActive = true)
    {
        $users = [];
        $query = (new \App\Db\Query())->from('roundcube_users');
        if ($onlyActive) {
            $query->where(['crm_status' => [self::MAIL_BOX_STATUS_INVALID_ACCESS, self::MAIL_BOX_STATUS_ACTIVE]]);
        }
        if ($user) {
            $query->andWhere(['user_id' => $user]);
        }
        if ($onlyMy) {
            $userModel = \App\User::getCurrentUserModel();
            $crmUsers = $userModel->getGroups();
            $crmUsers[] = $userModel->getId();
            $query->innerJoin('roundcube_users_autologin', 'roundcube_users_autologin.rcuser_id = roundcube_users.user_id');
            $query->andWhere(['roundcube_users_autologin.crmuser_id' => $crmUsers]);
        }
        if ($password) {
            $query->andWhere(['<>', 'password', '']);
        }
        $dataReader = $query->createCommand()->query();
        while ($row = $dataReader->read()) {
            $row['actions'] = empty($row['actions']) ? [] : explode(',', $row['actions']);
            $users[$row['user_id']] = $row;
        }
        $dataReader->close();
        return $users;
    }

    /**
     * Returns Roundcube configuration.
     *
     * @return array
     */
    public static function loadRoundcubeConfig()
    {
        $configMail = \App\Config::module('OSSMail');
        if (!\defined('RCMAIL_VERSION') && file_exists(RCUBE_INSTALL_PATH . '/program/include/iniset.php')) {
            // read rcube version from iniset
            $iniset = file_get_contents(RCUBE_INSTALL_PATH . '/program/include/iniset.php');
            if (preg_match('/define\(.RCMAIL_VERSION.,\s*.([0-9.]+[a-z-]*)?/', $iniset, $matches)) {
                $rcubeVersion = str_replace('-git', '.999', $matches[1]);
                \define('RCMAIL_VERSION', $rcubeVersion);
                \define('RCUBE_VERSION', $rcubeVersion);
            } else {
                throw new \App\Exceptions\AppException('Unable to find a Roundcube version');
            }
        }
        include 'public_html/modules/OSSMail/roundcube/config/defaults.inc.php';
        return $configMail + $config;
    }

    /**
     * Imap connection cache.
     *
     * @var array
     */
    protected static $imapConnectCache = [];

    /**
     * $imapConnectMailbox.
     *
     * @var string
     */
    public static $imapConnectMailbox = '';

    /**
     * Return imap connection resource.
     *
     * @param string $user
     * @param string $password
     * @param string $host
     * @param string $folder     Character encoding UTF7-IMAP
     * @param bool   $dieOnError
     * @param array  $config
     * @param array  $account
     *
     * @return IMAP\Connection|false
     */
    public static function imapConnect($user, $password, $host = '', $folder = 'INBOX', $dieOnError = true, $config = [], array $account = [])
    {
        \App\Log::trace("Entering OSSMail_Record_Model::imapConnect($user , '****' , $folder) method ...");
        if (!$config) {
            $config = self::loadRoundcubeConfig();
        }
        $cacheName = $user . $host . $folder;
        if (isset(self::$imapConnectCache[$cacheName])) {
            return self::$imapConnectCache[$cacheName];
        }
        $parseHost = parse_url($host);
        if (empty($parseHost['host'])) {
            $hosts = [];
            if ($imapHost = $config['imap_host'] ?? '') {
                $hosts = \is_string($imapHost) ? [$imapHost => $imapHost] : $imapHost;
            }
            foreach ($hosts as $configHost => $hostDomain) {
                $parsedConfigHost = parse_url($configHost);
                if (isset($parsedConfigHost['host']) && $parsedConfigHost['host'] === $host) {
                    $parseHost = $parsedConfigHost;
                    break;
                }
            }
        }
        $port = 143;
        $sslMode = 'tls';
        if (!empty($parseHost['host'])) {
            $host = $parseHost['host'];
            $sslMode = (isset($parseHost['scheme']) && \in_array($parseHost['scheme'], ['ssl', 'imaps', 'tls'])) ? $parseHost['scheme'] : null;
            if (!empty($parseHost['port'])) {
                $port = $parseHost['port'];
            } elseif ($sslMode && 'tls' !== $sslMode) {
                $port = 993;
            }
        }
        $validateCert = '';
        if (!$config['validate_cert'] && $config['imap_open_add_connection_type']) {
            $validateCert = '/novalidate-cert';
        }
        if ($config['imap_open_add_connection_type'] && $sslMode) {
            $sslMode = '/' . $sslMode;
        } else {
            $sslMode = '';
        }
        imap_timeout(IMAP_OPENTIMEOUT, 5);
        $maxRetries = $options = 0;
        if (isset($config['imap_max_retries'])) {
            $maxRetries = $config['imap_max_retries'];
        }
        $params = [];
        if (isset($config['imap_params'])) {
            $params = $config['imap_params'];
        }
        static::$imapConnectMailbox = "{{$host}:{$port}/imap{$sslMode}{$validateCert}}{$folder}";
        \App\Log::trace('imap_open(({' . static::$imapConnectMailbox . ", $user , '****'. $options, $maxRetries, " . var_export($params, true) . ') method ...');
        \App\Log::beginProfile(__METHOD__ . '|imap_open|' . $user, 'Mail|IMAP');
        $mbox = imap_open(static::$imapConnectMailbox, $user, $password, $options, $maxRetries, $params);
        \App\Log::endProfile(__METHOD__ . '|imap_open|' . $user, 'Mail|IMAP');
        self::$imapConnectCache[$cacheName] = $mbox;
        if ($mbox) {
            if ($account) {
                \App\Db::getInstance()->createCommand()
                    ->update('roundcube_users', ['crm_error' => null, 'crm_status' => self::MAIL_BOX_STATUS_ACTIVE], ['user_id' => $account['user_id']])
                    ->execute();
            }
            \App\Log::trace('Exit OSSMail_Record_Model::imapConnect() method ...');
            register_shutdown_function(function () use ($mbox, $user) {
                try {
                    \App\Log::beginProfile('OSSMail_Record_Model|imap_close|' . $user, 'Mail|IMAP');
                    imap_close($mbox);
                    \App\Log::endProfile('OSSMail_Record_Model|imap_close|' . $user, 'Mail|IMAP');
                } catch (\Throwable $e) {
                    \App\Log::error($e->getMessage() . PHP_EOL . $e->__toString());
                    throw $e;
                }
            });
        } else {
            if ($account) {
                $status = self::MAIL_BOX_STATUS_ACTIVE == $account['crm_status'] ? self::MAIL_BOX_STATUS_INVALID_ACCESS : self::MAIL_BOX_STATUS_BLOCKED_TEMP;
                [$date] = explode('||', $account['crm_error'] ?: '');
                if (empty($date) || false === strtotime($date)) {
                    $date = date('Y-m-d H:i:s');
                }
                if (self::MAIL_BOX_STATUS_BLOCKED_TEMP === $status && strtotime('-' . (OSSMailScanner_Record_Model::getConfig('blocked')['permanentTime'] ?? '2 day')) > strtotime($date)) {
                    $status = self::MAIL_BOX_STATUS_BLOCKED_PERM;
                }
                \App\Db::getInstance()->createCommand()
                    ->update('roundcube_users', [
                        'crm_error' => \App\TextUtils::textTruncate($date . '||' . imap_last_error(), 250),
                        'crm_status' => $status,
                        'failed_login' => date('Y-m-d H:i:s'),
                    ], ['user_id' => $account['user_id']])
                    ->execute();
            }
            \App\Log::error('Error OSSMail_Record_Model::imapConnect(' . static::$imapConnectMailbox . '): ' . imap_last_error());
            if ($dieOnError) {
                throw new \App\Exceptions\AppException('IMAP_ERROR' . ': ' . imap_last_error());
            }
        }
        return $mbox;
    }

    /**
     * Update mailbox mesages info for users.
     *
     * @param array $users
     *
     * @return array
     */
    public static function updateMailBoxCounter(array $users): array
    {
        if (empty($users)) {
            return [];
        }
        $dbCommand = \App\Db::getInstance()->createCommand();
        $config = Settings_Mail_Config_Model::getConfig('mailIcon');
        $interval = $config['timeCheckingMail'] ?? 30;
        $date = strtotime("-{$interval} seconds");
        $counter = [];
        $all = (new \App\Db\Query())->from('u_#__mail_quantities')->where(['userid' => $users])->indexBy('userid')->all();
        foreach ($users as $user) {
            if (empty($all[$user]['date']) || $date > strtotime($all[$user]['date'])) {
                if ($account = self::getMailAccountDetail($user)) {
                    if (empty($all[$user])) {
                        $dbCommand->insert('u_#__mail_quantities', ['userid' => $user, 'num' => 0, 'date' => date('Y-m-d H:i:s')])->execute();
                    } else {
                        $dbCommand->update('u_#__mail_quantities', ['date' => date('Y-m-d H:i:s')], ['userid' => $user])->execute();
                    }
                    try {
                        $mbox = self::imapConnect($account['username'], \App\Encryption::getInstance()->decrypt($account['password']), $account['mail_host'], 'INBOX', false, [], $account);
                        if ($mbox) {
                            \App\Log::beginProfile(__METHOD__ . '|imap_status|' . $user, 'Mail|IMAP');
                            $info = imap_status($mbox, static::$imapConnectMailbox, SA_UNSEEN);
                            \App\Log::endProfile(__METHOD__ . '|imap_status|' . $user, 'Mail|IMAP');
                            $counter[$user] = $info->unseen ?? 0;
                            $dbCommand->update('u_#__mail_quantities', ['num' => $counter[$user], 'date' => date('Y-m-d H:i:s')], ['userid' => $user])->execute();
                        }
                    } catch (\Throwable $th) {
                        \App\Log::error($th->__toString());
                    }
                }
            } else {
                $counter[$user] = $all[$user]['num'] ?? 0;
            }
        }
        return $counter;
    }

    /**
     * @param resource $mbox
     * @param int      $id
     * @param int      $msgno
     * @param bool     $fullMode
     *
     * @return bool|\OSSMail_Mail_Model
     */
    public static function getMail($mbox, $id, $msgno = false, bool $fullMode = true)
    {
        if (!$msgno) {
            \App\Log::beginProfile(__METHOD__ . '|imap_msgno', 'Mail|IMAP');
            $msgno = imap_msgno($mbox, $id);
            \App\Log::endProfile(__METHOD__ . '|imap_msgno', 'Mail|IMAP');
        }
        if (!$id) {
            \App\Log::beginProfile(__METHOD__ . '|imap_uid', 'Mail|IMAP');
            $id = imap_uid($mbox, $msgno);
            \App\Log::endProfile(__METHOD__ . '|imap_uid', 'Mail|IMAP');
        }
        if (!$msgno) {
            return false;
        }
        \App\Log::beginProfile(__METHOD__ . '|imap_headerinfo', 'Mail|IMAP');
        $header = imap_headerinfo($mbox, $msgno);
        \App\Log::endProfile(__METHOD__ . '|imap_headerinfo', 'Mail|IMAP');
        $messageId = '';
        if (property_exists($header, 'message_id')) {
            $messageId = $header->message_id;
        }
        $mail = new OSSMail_Mail_Model();
        $mail->set('header', $header);
        $mail->set('id', $id);
        $mail->set('Msgno', $header->Msgno);
        $mail->set('message_id', $messageId ? \App\Purifier::purifyByType($messageId, 'MailId') : '');
        $mail->set('to_email', \App\Purifier::purify($mail->getEmail('to')));
        $mail->set('from_email', \App\Purifier::purify($mail->getEmail('from')));
        $mail->set('reply_toaddress', \App\Purifier::purify($mail->getEmail('reply_to')));
        $mail->set('cc_email', \App\Purifier::purify($mail->getEmail('cc')));
        $mail->set('bcc_email', \App\Purifier::purify($mail->getEmail('bcc')));
        $mail->set('firstLetterBg', strtoupper(\App\TextUtils::textTruncate(trim(strip_tags(App\Purifier::purify($mail->getEmail('from')))), 1, false)));
        $mail->set('subject', isset($header->subject) ? \App\TextUtils::textTruncate(\App\Purifier::purify(self::decodeText($header->subject)), 65535, false) : '');
        $mail->set('date', date('Y-m-d H:i:s', $header->udate));
        if ($fullMode) {
            $structure = self::getBodyAttach($mbox, $id, $msgno);
            $mail->set('body', $structure['body']);
            $mail->set('attachments', $structure['attachment']);
            $mail->set('isHtml', $structure['isHtml']);

            $clean = '';
            \App\Log::beginProfile(__METHOD__ . '|imap_fetch_overview', 'Mail|IMAP');
            $msgs = imap_fetch_overview($mbox, $msgno);
            \App\Log::endProfile(__METHOD__ . '|imap_fetch_overview', 'Mail|IMAP');

            foreach ($msgs as $msg) {
                \App\Log::beginProfile(__METHOD__ . '|imap_fetchheader', 'Mail|IMAP');
                $clean .= imap_fetchheader($mbox, $msg->msgno);
                \App\Log::endProfile(__METHOD__ . '|imap_fetchheader', 'Mail|IMAP');
            }
            $mail->set('clean', $clean);
        }
        return $mail;
    }

    /**
     * Users cache.
     *
     * @var array
     */
    protected static $usersCache = [];

    /**
     * Return user account detal.
     *
     * @param int $userid
     *
     * @return array
     */
    public static function getMailAccountDetail($userid)
    {
        if (isset(self::$usersCache[$userid])) {
            return self::$usersCache[$userid];
        }
        $user = (new \App\Db\Query())->from('roundcube_users')->where(['user_id' => $userid, 'crm_status' => [self::MAIL_BOX_STATUS_INVALID_ACCESS, self::MAIL_BOX_STATUS_ACTIVE]])->one();
        self::$usersCache[$userid] = $user;
        return $user;
    }

    /**
     * Convert text encoding.
     *
     * @param string $text
     *
     * @return string
     */
    public static function decodeText($text)
    {
        $data = imap_mime_header_decode($text);
        $text = '';
        foreach ($data as &$row) {
            $charset = ('default' == $row->charset) ? 'ASCII' : $row->charset;
            if (\function_exists('mb_convert_encoding') && \in_array($charset, mb_list_encodings())) {
                $text .= mb_convert_encoding($row->text, 'utf-8', $charset);
            } else {
                $text .= iconv($charset, 'UTF-8', $row->text);
            }
        }
        return $text;
    }

    /**
     * Return full name.
     *
     * @param string $text
     *
     * @return string
     */
    public static function getFullName($text)
    {
        $return = '';
        foreach ($text as $row) {
            if ('' != $return) {
                $return .= ',';
            }
            if ('' == $row->personal) {
                $return .= $row->mailbox . '@' . $row->host;
            } else {
                $return .= self::decodeText($row->personal) . ' - ' . $row->mailbox . '@' . $row->host;
            }
        }
        return $return;
    }

    /**
     * Return body and attachments.
     *
     * @param resource $mbox
     * @param int      $id
     * @param int      $msgno
     *
     * @return array
     */
    public static function getBodyAttach($mbox, $id, $msgno)
    {
        \App\Log::beginProfile(__METHOD__ . '|imap_fetchstructure', 'Mail|IMAP');
        $struct = imap_fetchstructure($mbox, $id, FT_UID);
        \App\Log::endProfile(__METHOD__ . '|imap_fetchstructure', 'Mail|IMAP');
        $mail = ['id' => $id];
        if (empty($struct->parts)) {
            $mail = self::initMailPart($mbox, $mail, $struct, 0);
        } else {
            foreach ($struct->parts as $partNum => $partStructure) {
                $mail = self::initMailPart($mbox, $mail, $partStructure, $partNum + 1);
            }
        }
        $body = '';
        $body = (!empty($mail['textPlain'])) ? $mail['textPlain'] : $body;
        $body = (!empty($mail['textHtml'])) ? $mail['textHtml'] : $body;
        $attachment = (isset($mail['attachments'])) ? $mail['attachments'] : [];

        return [
            'body' => $body,
            'attachment' => $attachment,
            'isHtml' => !empty($mail['textHtml']),
        ];
    }

    /**
     * Init mail part.
     *
     * @param resource $mbox
     * @param array    $mail
     * @param object   $partStructure
     * @param int      $partNum
     *
     * @return array
     */
    protected static function initMailPart($mbox, $mail, $partStructure, $partNum)
    {
        if ($partNum) {
            \App\Log::beginProfile(__METHOD__ . '|imap_fetchbody', 'Mail|IMAP');
            $data = $orgData = imap_fetchbody($mbox, $mail['id'], $partNum, FT_UID | FT_PEEK);
            \App\Log::endProfile(__METHOD__ . '|imap_fetchbody', 'Mail|IMAP');
        } else {
            \App\Log::beginProfile(__METHOD__ . '|imap_body', 'Mail|IMAP');
            $data = $orgData = imap_body($mbox, $mail['id'], FT_UID | FT_PEEK);
            \App\Log::endProfile(__METHOD__ . '|imap_body', 'Mail|IMAP');
        }
        if (1 == $partStructure->encoding) {
            $data = imap_utf8($data);
        } elseif (2 == $partStructure->encoding) {
            $data = imap_binary($data);
        } elseif (3 == $partStructure->encoding) {
            $data = imap_base64($data);
        } elseif (4 == $partStructure->encoding) {
            $data = imap_qprint($data);
        }
        $params = [];
        if (!empty($partStructure->parameters)) {
            foreach ($partStructure->parameters as $param) {
                $params[strtolower($param->attribute)] = $param->value;
            }
        }
        if (!empty($partStructure->dparameters)) {
            foreach ($partStructure->dparameters as $param) {
                $paramName = strtolower(preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute);
                if (isset($params[$paramName])) {
                    $params[$paramName] .= $param->value;
                } else {
                    $params[$paramName] = $param->value;
                }
            }
        }
        if (!empty($params['charset']) && 'utf-8' !== strtolower($params['charset'])) {
            if (\function_exists('mb_convert_encoding') && \in_array($params['charset'], mb_list_encodings())) {
                $encodedData = mb_convert_encoding($data, 'UTF-8', $params['charset']);
            } else {
                $encodedData = iconv($params['charset'], 'UTF-8', $data);
            }
            if ($encodedData) {
                $data = $encodedData;
            }
        }
        $attachmentId = $partStructure->ifid ? trim($partStructure->id, ' <>') : (isset($params['filename']) || isset($params['name']) ? random_int(0, PHP_INT_MAX) . random_int(0, PHP_INT_MAX) : null);
        if ($attachmentId) {
            if (empty($params['filename']) && empty($params['name'])) {
                $fileName = $attachmentId . '.' . strtolower($partStructure->subtype);
            } else {
                $fileName = !empty($params['filename']) ? $params['filename'] : $params['name'];
                $fileName = self::decodeText($fileName);
                $fileName = self::decodeRFC2231($fileName);
            }
            $mail['attachments'][$attachmentId]['filename'] = $fileName;
            $mail['attachments'][$attachmentId]['attachment'] = $data;
        } elseif (0 == $partStructure->type && $data) {
            if (preg_match('/^([a-zA-Z0-9]{76} )+[a-zA-Z0-9]{76}$/', $data) && base64_decode($data, true)) {
                $data = base64_decode($data);
            }
            if ('plain' == strtolower($partStructure->subtype)) {
                $uuDecode = self::uuDecode($data);
                if (isset($uuDecode['attachments'])) {
                    $mail['attachments'] = $uuDecode['attachments'];
                }
                if (!isset($mail['textPlain'])) {
                    $mail['textPlain'] = '';
                }
                if (isset($params['format']) && 'flowed' === $params['format']) {
                    $uuDecode['text'] = self::unfoldFlowed($uuDecode['text'], isset($params['delsp']) && 'yes' === strtolower($params['delsp']));
                }
                $mail['textPlain'] .= $uuDecode['text'];
            } else {
                if (!isset($mail['textHtml'])) {
                    $mail['textHtml'] = '';
                }
                if ($data && '<' !== $data[0] && '<' === $orgData[0]) {
                    $data = $orgData;
                }
                $mail['textHtml'] .= $data;
            }
        } elseif (2 == $partStructure->type && $data) {
            if (!isset($mail['textPlain'])) {
                $mail['textPlain'] = '';
            }
            $mail['textPlain'] .= trim($data);
        }
        if (!empty($partStructure->parts)) {
            foreach ($partStructure->parts as $subPartNum => $subPartStructure) {
                if (2 == $partStructure->type && 'RFC822' == $partStructure->subtype) {
                    $mail = self::initMailPart($mbox, $mail, $subPartStructure, $partNum);
                } else {
                    $mail = self::initMailPart($mbox, $mail, $subPartStructure, $partNum . '.' . ($subPartNum + 1));
                }
            }
        }
        return $mail;
    }

    /**
     * Decode string.
     *
     * @param string $input
     *
     * @return array
     */
    protected static function uuDecode($input)
    {
        $attachments = [];
        $uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s';
        $uu_regexp_end = '/`\r?\nend((\r?\n)|($))/s';

        while (preg_match($uu_regexp_begin, $input, $matches, PREG_OFFSET_CAPTURE)) {
            $startpos = $matches[0][1];
            if (!preg_match($uu_regexp_end, $input, $m, PREG_OFFSET_CAPTURE, $startpos)) {
                break;
            }

            $endpos = $m[0][1];
            $begin_len = \strlen($matches[0][0]);
            $end_len = \strlen($m[0][0]);

            // extract attachment body
            $filebody = substr($input, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1);
            $filebody = str_replace("\r\n", "\n", $filebody);

            // remove attachment body from the message body
            $input = substr_replace($input, '', $startpos, $endpos + $end_len - $startpos);

            // add attachments to the structure
            $attachments[] = [
                'filename' => trim($matches[1][0]),
                'attachment' => convert_uudecode($filebody),
            ];
        }
        return ['attachments' => $attachments, 'text' => $input];
    }

    /**
     * Parse format=flowed message body.
     *
     * @param string $text
     * @param bool   $delSp
     *
     * @return string
     */
    protected static function unfoldFlowed(string $text, bool $delSp = false): string
    {
        $text = preg_split('/\r?\n/', $text);
        $last = -1;
        $qLevel = 0;
        foreach ($text as $idx => $line) {
            if ($q = strspn($line, '>')) {
                $line = substr($line, $q);
                if (isset($line[0]) && ' ' === $line[0]) {
                    $line = substr($line, 1);
                }
                if ($q == $qLevel
                    && isset($text[$last]) && ' ' == $text[$last][\strlen($text[$last]) - 1]
                    && !preg_match('/^>+ {0,1}$/', $text[$last])
                ) {
                    if ($delSp) {
                        $text[$last] = substr($text[$last], 0, -1);
                    }
                    $text[$last] .= $line;
                    unset($text[$idx]);
                } else {
                    $last = $idx;
                }
            } else {
                if ('-- ' == $line) {
                    $last = $idx;
                } else {
                    if (isset($line[0]) && ' ' === $line[0]) {
                        $line = substr($line, 1);
                    }
                    if (isset($text[$last]) && $line && !$qLevel
                        && '-- ' !== $text[$last]
                        && isset($text[$last][\strlen($text[$last]) - 1]) && ' ' === $text[$last][\strlen($text[$last]) - 1]
                    ) {
                        if ($delSp) {
                            $text[$last] = substr($text[$last], 0, -1);
                        }
                        $text[$last] .= $line;
                        unset($text[$idx]);
                    } else {
                        $text[$idx] = $line;
                        $last = $idx;
                    }
                }
            }
            $qLevel = $q;
        }

        return implode("\r\n", $text);
    }

    /**
     * Check if url is encoded.
     *
     * @param string $string
     *
     * @return bool
     */
    public static function isUrlEncoded($string)
    {
        $string = str_replace('%20', '+', $string);
        $decoded = urldecode($string);

        return $decoded != $string && urlencode($decoded) == $string;
    }

    /**
     * decode RFC2231 formatted string.
     *
     * @param string $string
     * @param string $charset
     *
     * @return string
     */
    protected static function decodeRFC2231($string, $charset = 'utf-8')
    {
        if (preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) {
            $encoding = $matches[1];
            $data = $matches[2];
            if (self::isUrlEncoded($data)) {
                $string = iconv(strtoupper($encoding), $charset, urldecode($data));
            }
        }
        return $string;
    }

    /**
     * Return user folders.
     *
     * @param int $user
     *
     * @return array
     */
    public static function getFolders($user)
    {
        $account = self::getAccountsList($user);
        $account = reset($account);
        $folders = false;
        $mbox = self::imapConnect($account['username'], \App\Encryption::getInstance()->decrypt($account['password']), $account['mail_host'], 'INBOX', false, [], $account);
        if ($mbox) {
            $folders = [];
            $ref = '{' . $account['mail_host'] . '}';
            $list = imap_list($mbox, $ref, '*');
            foreach ($list as $mailboxname) {
                $name = str_replace($ref, '', $mailboxname);
                $name = \App\Utils::convertCharacterEncoding($name, 'UTF7-IMAP', 'UTF-8');
                $folders[$name] = $name;
            }
        }
        return $folders;
    }

    /**
     * Return site URL.
     *
     * @return string
     */
    public static function getSiteUrl()
    {
        $site_URL = App\Config::main('site_URL');
        if ('/' != substr($site_URL, -1)) {
            $site_URL = $site_URL . '/';
        }
        return $site_URL;
    }

    /**
     * Fetch mails from IMAP.
     *
     * @param int|null $user
     *
     * @return array
     */
    public static function getMailsFromIMAP(?int $user = null)
    {
        $accounts = self::getAccountsList(false, true);
        $mails = [];
        $mailLimit = 5;
        if ($accounts) {
            if ($user && isset($accounts[$user])) {
                $account = $accounts[$user];
            } else {
                $account = reset($accounts);
            }
            $imap = self::imapConnect($account['username'], \App\Encryption::getInstance()->decrypt($account['password']), $account['mail_host'], 'INBOX', true, [], $account);
            \App\Log::beginProfile(__METHOD__ . '|imap_num_msg', 'Mail|IMAP');
            $numMessages = imap_num_msg($imap);
            \App\Log::endProfile(__METHOD__ . '|imap_num_msg', 'Mail|IMAP');
            if ($numMessages < $mailLimit) {
                $mailLimit = $numMessages;
            }
            for ($i = $numMessages; $i > ($numMessages - $mailLimit); --$i) {
                $mail = self::getMail($imap, false, $i);
                $mails[] = $mail;
            }
        }
        return $mails;
    }

    /**
     * Get mail account detail by hash ID.
     *
     * @param string $hash
     *
     * @return bool|array
     */
    public static function getAccountByHash($hash)
    {
        if (preg_match('/^[_a-zA-Z0-9.,]+$/', $hash)) {
            $result = (new \App\Db\Query())
                ->from('roundcube_users')
                ->where(['like', 'preferences', "%:\"$hash\";%", false])
                ->one();
            if ($result) {
                return $result;
            }
        }
        return false;
    }

    /**
     * Update user data for account.
     *
     * @param int   $userId
     * @param array $data
     *
     * @return bool
     */
    public static function setAccountUserData(int $userId, array $data): bool
    {
        return \App\Db::getInstance()->createCommand()->update('roundcube_users', $data, ['user_id' => $userId])->execute();
    }
}