Your IP : 18.117.119.158


Current Path : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/mail/lib/
Upload File :
Current File : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/mail/lib/imap.php

<?php

namespace Bitrix\Mail;

use Bitrix\Main;
use Bitrix\Main\Text\Encoding;
use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

class Imap
{

	const ERR_CONNECT          = 101;
	const ERR_REJECTED         = 102;
	const ERR_COMMUNICATE      = 103;
	const ERR_EMPTY_RESPONSE   = 104;
	const ERR_BAD_SERVER       = 105;

	const ERR_STARTTLS         = 201;
	const ERR_COMMAND_REJECTED = 202;
	const ERR_CAPABILITY       = 203;
	const ERR_AUTH             = 204;
	const ERR_AUTH_MECH        = 205;
	const ERR_AUTH_OAUTH       = 206;
	const ERR_LIST             = 207;
	const ERR_SELECT           = 208;
	const ERR_SEARCH           = 209;
	const ERR_FETCH            = 210;
	const ERR_APPEND           = 211;
	const ERR_STORE            = 212;

	protected $stream, $errors;
	protected $sessState, $sessCapability, $sessCounter, $sessUntagged, $sessMailbox;

	protected $options = array();

	protected static $atomRegex    = '[^\x00-\x20\x22\x25\x28-\x2a\x5c\x5d\x7b\x7f-\xff]+';
	protected static $qcharRegex   = '[^\x00\x0a\x0d\x22\x5c\x80-\xff]|\x5c[\x5c\x22]';
	protected static $astringRegex = '[^\x00-\x20\x22\x25\x28-\x2a\x5c\x7b\x7f-\xff]+';

	public function __construct($host, $port, $tls, $strict, $login, $password, $encoding)
	{
		$this->reset();

		$strict = PHP_VERSION_ID < 50600 ? false : (bool) $strict;

		$this->options = array(
			'host'    => $host,
			'port'    => $port,
			'tls'     => $tls,
			'socket'  => sprintf('%s://%s:%s', ($tls ? 'ssl' : 'tcp'), $host, $port),
			'timeout' => \COption::getOptionInt('mail', 'connect_timeout', B_MAIL_TIMEOUT),
			'context' => stream_context_create(array(
				'ssl' => array(
					'verify_peer'      => $strict,
					'verify_peer_name' => $strict
				)
			)),
			'login'    => $login,
			'password' => $password,
			'encoding' => $encoding,
		);
	}

	public function __destruct()
	{
		$this->disconnect();
	}

	protected function disconnect()
	{
		if (!is_null($this->stream))
			@fclose($this->stream);
		unset($this->stream);
	}

	protected function reset()
	{
		$this->disconnect();

		unset($this->errors);
		unset($this->sessState);
		unset($this->sessCapability);
		$this->sessCounter = 0;
		$this->sessUntagged = array();
		$this->sessMailbox = array(
			'name'        => null,
			'exists'      => null,
			'uidvalidity' => null,
		);
	}

	public function getState()
	{
		return $this->sessState;
	}

	protected function connect(&$error)
	{
		$error = null;

		if ($this->sessState)
			return true;

		$resource = @stream_socket_client(
			$this->options['socket'], $errno, $errstr, $this->options['timeout'],
			STREAM_CLIENT_CONNECT, $this->options['context']
		);

		if ($resource === false)
		{
			$error = $this->errorMessage(Imap::ERR_CONNECT, $errno ?: null);
			return false;
		}

		$this->stream = $resource;

		if ($this->options['timeout'] > 0)
			stream_set_timeout($this->stream, $this->options['timeout']);

		$prompt = $this->readLine();

		if ($prompt !== false && preg_match('/^\* (OK|PREAUTH)/i', $prompt, $matches))
		{
			if ($matches[1] == 'OK')
				$this->sessState = 'no_auth';
			elseif ($matches[1] == 'PREAUTH')
				$this->sessState = 'auth';
		}
		else
		{
			if ($prompt === false)
				$error = Imap::ERR_EMPTY_RESPONSE;
			elseif (preg_match('/^\* BYE/i', $prompt))
				$error = Imap::ERR_REJECTED;
			else
				$error = Imap::ERR_BAD_SERVER;

			$error = $this->errorMessage(array(Imap::ERR_CONNECT, $error));

			return false;
		}

		if (!$this->capability($error))
			return false;

		if (!$this->options['tls'] && preg_match('/ \x20 STARTTLS ( \x20 | \r\n ) /ix', $this->sessCapability))
		{
			if (!$this->starttls($error))
				return false;
		}

		return true;
	}

	protected function starttls(&$error)
	{
		$error = null;

		if (!$this->sessState)
		{
			$error = $this->errorMessage(Imap::ERR_STARTTLS);
			return false;
		}

		$this->executeCommand('STARTTLS', $error);

		if (!$error)
		{
			if (stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT))
			{
				if (!$this->capability($error))
					return false;
			}
			else
			{
				$this->reset();

				$error = $this->errorMessage(Imap::ERR_STARTTLS);

				return false;
			}
		}

		return true;
	}

	protected function capability(&$error)
	{
		$error = null;

		if (!$this->sessState)
		{
			$error = $this->errorMessage(Imap::ERR_CAPABILITY);
			return false;
		}

		$response = $this->executeCommand('CAPABILITY', $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_CAPABILITY, $error), $response);

			return false;
		}

		$regex = '/^ \* \x20 CAPABILITY /ix';
		foreach ($this->getUntagged($regex, true) as $item)
			$this->sessCapability = $item[0];

		return true;
	}

	protected function authenticate(&$error)
	{
		$error = null;

		if (!$this->connect($error))
			return false;
		if (in_array($this->sessState, array('auth', 'select')))
			return true;

		$mech = false;

		if (preg_match('/^\x00oauth\x00(google|liveid)\x00(\d+)$/', $this->options['password'], $matches))
		{
			if (preg_match('/ \x20 AUTH=XOAUTH2 ( \x20 | \r\n ) /ix', $this->sessCapability))
			{
				if (\CModule::includeModule('socialservices'))
				{
					switch ($matches[1])
					{
						case 'google':
							$oauthClient = new \CSocServGoogleOAuth($matches[2]);
							$oauthClient->getUrl('modal', array('https://mail.google.com/'));
							break;
						case 'liveid':
							$oauthClient = new \CSocServLiveIDOAuth($matches[2]);
							$oauthClient->getUrl('modal', array('wl.imap', 'wl.offline_access'));
							break;
					}

					if (!empty($oauthClient))
					{
						$token = $oauthClient->getStorageToken();
						if (!$token)
						{
							$error = $this->errorMessage(array(Imap::ERR_AUTH, Imap::ERR_AUTH_OAUTH));
							return false;
						}

						$mech = 'oauth';
					}
				}
			}
		}

		if ($mech == false)
		{
			if (preg_match('/ \x20 AUTH=PLAIN ( \x20 | \r\n ) /ix', $this->sessCapability))
				$mech = 'plain';
			elseif (!preg_match('/ \x20 LOGINDISABLED ( \x20 | \r\n ) /ix', $this->sessCapability))
				$mech = 'login';
		}

		if (!$mech)
		{
			$error = $this->errorMessage(array(Imap::ERR_AUTH, Imap::ERR_AUTH_MECH));
			return false;
		}

		if ($mech == 'oauth')
		{
			$response = $this->executeCommand('AUTHENTICATE XOAUTH2', $error);

			if (strpos($response, '+') !== 0)
			{
				$error = $error == Imap::ERR_COMMAND_REJECTED ? Imap::ERR_AUTH_MECH : $error;
				$error = $this->errorMessage(array(Imap::ERR_AUTH, $error), $response);

				return false;
			}

			$response = $this->exchange(base64_encode(sprintf(
				"user=%s\x01auth=Bearer %s\x01\x01", $this->options['login'], $token
			)), $error);

			if (strpos($response, '+') === 0)
				$response = $this->exchange("\r\n", $error);
		}
		elseif ($mech == 'plain')
		{
			$response = $this->executeCommand('AUTHENTICATE PLAIN', $error);

			if (strpos($response, '+') !== 0)
			{
				$error = $error == Imap::ERR_COMMAND_REJECTED ? Imap::ERR_AUTH_MECH : $error;
				$error = $this->errorMessage(array(Imap::ERR_AUTH, $error), $response);

				return false;
			}

			$response = $this->exchange(base64_encode(sprintf(
				"\x00%s\x00%s", $this->options['login'], $this->options['password']
			)), $error);
		}
		else // if ($mech == 'login')
		{
			$response = $this->executeCommand(sprintf(
				'LOGIN %s %s',
				static::prepareString($this->options['login']),
				static::prepareString($this->options['password'])
			), $error);
		}

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_AUTH, $error), $response);

			return false;
		}

		$this->sessState = 'auth';

		if (!$this->capability($error))
			return false;

		return true;
	}

	protected function select($mailbox, &$error)
	{
		$error = null;

		if (!$this->authenticate($error))
			return false;
		if ($this->sessState == 'select' && $mailbox == $this->sessMailbox['name'])
			return true;

		$response = $this->executeCommand(sprintf(
			'SELECT "%s"', static::escapeQuoted($this->encodeUtf7Imap($mailbox))
		), $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_SELECT, $error), $response);

			return false;
		}

		$this->sessState = 'select';
		$this->sessMailbox = array(
			'name'        => $mailbox,
			'exists'      => null,
			'uidvalidity' => null,
		);

		$regex = '/^ \* \x20 ( \d+ ) \x20 EXISTS /ix';
		foreach ($this->getUntagged($regex, true) as $item)
			$this->sessMailbox['exists'] = $item[1][1];

		$regex = '/^ \* \x20 OK \x20 \[ UIDVALIDITY \x20 ( \d+ ) \] /ix';
		foreach ($this->getUntagged($regex, true) as $item)
			$this->sessMailbox['uidvalidity'] = $item[1][1];

		if (!$this->capability($error))
			return false;

		return true;
	}

	/**
	 * Connects to server and authenticate client
	 *
	 * @param string &$error Error message.
	 * @return boolean
	 */
	public function singin(&$error)
	{
		$error = null;

		return $this->authenticate($error);
	}

	/**
	 * Returns unseen messages count
	 *
	 * @param string $mailbox Mailbox name.
	 * @param string &$error Error message.
	 * @return int|false
	 */
	public function getUnseen($mailbox, &$error)
	{
		$error = null;

		if (!$this->select($mailbox, $error))
			return false;

		$unseen = 0;

		$response = $this->executeCommand('SEARCH UNSEEN', $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_SEARCH, $error), $response);

			return false;
		}

		$regex = '/^ \* \x20 SEARCH ( (?: \x20 \d+ )* ) /ix';
		foreach ($this->getUntagged($regex, true) as $item)
			$unseen = preg_match_all('/\d+/', $item[1][1], $dummy);

		return $unseen;
	}

	/**
	 * Returns mailboxes list
	 *
	 * @param string $pattern Mailbox name pattern.
	 * @param string &$error Error message.
	 * @return array|false
	 */
	public function listMailboxes($pattern, &$error)
	{
		$error = null;

		if (!$this->authenticate($error))
			return false;

		$pattern = preg_replace('/ \*+ /x', '*', $pattern);

		$response = $this->executeCommand(sprintf(
			'LIST "" "%s"',
			static::escapeQuoted($this->encodeUtf7Imap(
				preg_replace('/ \* /x', '%', $pattern)
			))
		), $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_LIST, $error), $response);

			return false;
		}

		$list = array();

		$regex = sprintf(
			'/^ \* \x20 LIST \x20
				\( (?<flags> ( \x5c? %1$s ( \x20 \x5c? %1$s )* )? ) \) \x20
				(?<delim> NIL | " ( %2$s ) " ) \x20
				(?<name> \{ \d+ \} | " ( %2$s )* " | %3$s ) \r\n
			/ix',
			self::$atomRegex, self::$qcharRegex, self::$astringRegex
		);
		foreach ($this->getUntagged($regex, true) as $item)
		{
			list($item, $matches) = $item;

			$sflags = $matches['flags'];
			$sdelim = $matches['delim'];
			$sname  = $matches['name'];

			if (preg_match('/^ " ( .+ ) " $/ix', $sdelim, $quoted))
				$sdelim = static::unescapeQuoted($quoted[1]);

			if (preg_match('/^ \{ ( \d+ ) \} $/ix', $sname, $literal))
				$sname = \CUtil::binSubstr($item, \CUtil::binStrlen($matches[0]), $literal[1]);
			elseif (preg_match('/^ " ( .* ) " $/ix', $sname, $quoted))
				$sname = static::unescapeQuoted($quoted[1]);

			$sname = $this->decodeUtf7Imap($sname);

			// #79498
			if (strtoupper($sdelim) != 'NIL')
				$sname = rtrim($sname, $sdelim);

			$list[] = array(
				'flags' => $sflags,
				'delim' => strtoupper($sdelim) == 'NIL' ? null : $sdelim,
				'name'  => $sname,
			);
		}

		if (preg_match('/ \* $/x', $pattern))
		{
			foreach ($list as $i => $item)
			{
				if ($item['delim'] === null)
					continue;

				$subpattern = sprintf('%s%s*', $item['name'], $item['delim']);
				if ($subpattern == $pattern)
				{
					continue;
				}

				if (!preg_match('/ ( ^ | \x20 ) \x5c ( Noinferiors | HasNoChildren ) ( \x20 | $ ) /ix', $item['flags']))
				{
					$children = $this->listMailboxes($subpattern, $error);

					if ($children === false)
						return false;

					$regex = sprintf(
						'/^%s%s./i',
						preg_quote($item['name'], '/'),
						preg_quote($item['delim'], '/')
					);
					$children = array_filter(
						$children,
						function ($child) use (&$regex)
						{
							return preg_match($regex, $child['name']);
						}
					);

					if (!empty($children))
						$list[$i]['children'] = $children;
				}
			}
		}

		return $list;
	}

	public function listMessages($mailbox, &$uidtoken, &$error)
	{
		$error = null;

		$params = array(
			'offset' => 0,
			'limit'  => -1,
		);
		if (is_array($mailbox))
		{
			$params  = array_merge($params, $mailbox);
			$mailbox = $mailbox['mailbox'];
		}

		if (!($params['offset'] > 0))
			$params['offset'] = 0;
		if (!($params['limit'] > 0))
			$params['limit'] = -1;

		if (!$this->select($mailbox, $error))
			return false;

		$uidtoken = $this->sessMailbox['uidvalidity'];

		$list = array();

		if ($this->sessMailbox['exists'] > 0)
		{
			$regex = '/^ \* \x20 (?<id> \d+ ) \x20 FETCH \x20 \( (?<data> .+ ) \) \r\n $/isx';
			$this->getUntagged($regex, true);

			$response = $this->executeCommand(
				sprintf(
					'FETCH %u:%s (%sINTERNALDATE RFC822.SIZE FLAGS)',
					$params['offset']+1,
					$params['limit'] > 0 ? $params['offset']+$params['limit'] : '*',
					!is_null($uidtoken) ? 'UID ' : ''
				),
				$error
			);

			if ($error)
			{
				$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
				$error = $this->errorMessage(array(Imap::ERR_FETCH, $error), $response);

				return false;
			}

			foreach ($this->getUntagged($regex, true) as $item)
			{
				$data = array(
					'id'   => $item[1]['id'],
					'uid'  => null,
					'date' => null,
					'size' => null,
				);

				$regex = '/ ( ^ | \x20 ) UID \x20 (?<uid> \d+ ) ( \x20 | $ ) /ix';
				if (preg_match($regex, $item[1]['data'], $matches))
				{
					$data['uid'] = $matches['uid'];
				}

				$regex = sprintf(
					'/ ( ^ | \x20 ) INTERNALDATE \x20 " (?<date> ( %s )+ ) " ( \x20 | $ ) /ix',
					self::$qcharRegex
				);
				if (preg_match($regex, $item[1]['data'], $matches))
					$data['date'] = $matches['date'];

				$regex = '/ ( ^ | \x20 ) RFC822\.SIZE \x20 (?<size> \d+ ) ( \x20 | $ ) /ix';
				if (preg_match($regex, $item[1]['data'], $matches))
					$data['size'] = $matches['size'];

				$regex = sprintf(
					'/ ( ^ | \x20 ) FLAGS \x20 \( (?<flags> ( \x5c? %1$s ( \x20 \x5c? %1$s )* )? ) \) ( \x20 | $ ) /ix',
					self::$atomRegex
				);
				if (preg_match($regex, $item[1]['data'], $matches))
					$data['flags'] = $matches['flags'];

				if (isset($data['date'], $data['size'], $data['flags']))
				{
					$list[] = $data;

					if (!is_null($uidtoken) && !isset($data['uid']))
					{
						addMessage2Log(
							sprintf('IMAP: UID not found v4 (%s)', $item[0]),
							'mail', 0, false
						);
					}
				}
			}
		}

		return $list;
	}

	/**
	 * Adds message
	 *
	 * @param string $mailbox Mailbox name.
	 * @param string $data Message.
	 * @param string &$error Error message.
	 * @return string|false
	 */
	public function addMessage($mailbox, $data, &$error)
	{
		$error = null;

		if (!$this->authenticate($error))
			return false;

		$response = $this->executeCommand(sprintf(
			'APPEND "%s" (\Seen) %s',
			static::escapeQuoted($this->encodeUtf7Imap($mailbox)),
			static::prepareString($data)
		), $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_APPEND, $error), $response);

			return false;
		}

		$regex = sprintf('/^ OK \x20 \[ APPENDUID \x20 ( \d+ ) \x20 ( \d+ ) \] /ix', $this->getTag());
		if (preg_match($regex, $response, $matches))
			return sprintf('%u:%u', $matches[1], $matches[2]);

		return true;
	}

	public function updateMessageFlags($mailbox, $id, $flags, &$error)
	{
		$error = null;

		if (!$this->select($mailbox, $error))
			return false;

		$addFlags = array();
		$delFlags = array();
		foreach ($flags as $name => $value)
		{
			if (preg_match(sprintf('/ ^ \x5c? %s $ /ix', self::$atomRegex), $name))
			{
				if ($value)
					$addFlags[] = $name;
				else
					$delFlags[] = $name;
			}
		}

		if ($addFlags)
			$response = $this->executeCommand(sprintf('STORE %u +FLAGS (%s)', $id, join(' ', $addFlags)), $error);

		if (!$error && $delFlags)
			$response = $this->executeCommand(sprintf('STORE %u -FLAGS (%s)', $id, join(' ', $delFlags)), $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_STORE, $error), $response);

			return false;
		}

		$this->getUntagged(sprintf('/^ \* \x20 %u \x20 FETCH \x20 \( .+ \) \r\n $/isx', $id), true);

		return true;
	}

	/**
	 * Returns message
	 *
	 * @param string $mailbox Mailbox name.
	 * @param int $id Message ID.
	 * @param string $section Message section.
	 * @param string &$error Error message.
	 * @return string|false
	 */
	public function getMessage($mailbox, $id, $section, &$error)
	{
		$error = null;

		if (!$this->select($mailbox, $error))
			return false;

		if (!in_array(strtoupper($section), array('HEADER', 'TEXT')))
			$section = '';

		$response = $this->executeCommand(sprintf('FETCH %u (BODY.PEEK[%s])', $id, $section), $error);

		if ($error)
		{
			$error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
			$error = $this->errorMessage(array(Imap::ERR_FETCH, $error), $response);

			return false;
		}

		$sbody = false;
		$regex = sprintf('/^ \* \x20 %u \x20 FETCH \x20 \( (?<data> .+ ) \) \r\n $/isx', $id);
		foreach ($this->getUntagged($regex, true) as $item)
		{
			$data  = $item[1]['data'];
			$sbody = false;

			$regex = sprintf('/ ( ^ | \x20 ) BODY \[%s\] \x20 NIL ( \x20 | $ ) /ix', $section);
			if (preg_match($regex, $data))
			{
				$sbody = null;
				continue;
			}

			$regex = sprintf(
				'/ ( ^ | \x20 ) BODY \[%s\] \x20 " (?<body> ( %s )* ) " ( \x20 | $ ) /ix',
				$section, self::$qcharRegex
			);
			if (preg_match($regex, $data, $quoted))
			{
				$sbody = static::unescapeQuoted($quoted['body']);
				continue;
			}

			$regex = sprintf('/ ( ^ | \x20 ) BODY \[%s\] \x20  \{ (?<size> \d+ ) \} \r\n /ix', $section);
			if (preg_match($regex, $data, $literal, PREG_OFFSET_CAPTURE))
			{
				$sbody = \CUtil::binSubstr($data, $literal[0][1]+\CUtil::binStrlen($literal[0][0]), $literal['size'][0]);
				continue;
			}
		}

		return $sbody;
	}

	protected function getUntagged($regex, $unset = false)
	{
		$result = array();

		$length = count($this->sessUntagged);
		for ($i = 0; $i < $length; $i++)
		{
			if (!preg_match($regex, $this->sessUntagged[$i], $matches))
				continue;

			$result[] = array($this->sessUntagged[$i], $matches);

			if ($unset)
				unset($this->sessUntagged[$i]);
		}

		if ($unset && !empty($result))
			$this->sessUntagged = array_values($this->sessUntagged);

		return $result;
	}

	protected function getTag($next = false)
	{
		if ($next)
			$this->sessCounter++;

		return sprintf('A%03u', $this->sessCounter);
	}

	protected function executeCommand($command, &$error)
	{
		$error = null;

		$chunks = explode("\x00", sprintf('%s %s', $this->getTag(true), $command));

		$k = count($chunks);
		foreach ($chunks as $chunk)
		{
			$k--;

			$response = $this->exchange($chunk, $error);

			if ($k > 0 && strpos($response, '+') !== 0)
				break;
		}

		return $response;
	}

	protected function exchange($data, &$error)
	{
		$error = null;

		if ($this->sendData(sprintf("%s\r\n", $data)) === false)
		{
			$error = Imap::ERR_COMMUNICATE;
			return false;
		}

		$response = $this->readResponse();

		if ($response === false)
		{
			$error = Imap::ERR_EMPTY_RESPONSE;
			return false;
		}

		$response = trim($response);

		if (!preg_match(sprintf('/^ %s \x20 OK /ix', $this->getTag()), $response))
		{
			if (preg_match(sprintf('/^ %s \x20 ( NO | BAD ) /ix', $this->getTag()), $response))
				$error = Imap::ERR_COMMAND_REJECTED;
			else
				$error = Imap::ERR_BAD_SERVER;
		}

		return preg_replace(sprintf('/^ %s \x20 /ix', $this->getTag()), '', $response);
		return $response;
	}

	protected function sendData($data)
	{
		$fails = 0;
		while (\CUtil::binStrlen($data) > 0 && !feof($this->stream))
		{
			$bytes = @fputs($this->stream, $data);

			if (false == $bytes)
			{
				if (false === $bytes || ++$fails >= 3)
				{
					break;
				}

				continue;
			}

			$fails = 0;

			$data = \CUtil::binSubstr($data, $bytes);
		}

		if (\CUtil::binStrlen($data) > 0)
		{
			$this->reset();
			return false;
		}

		return true;
	}

	protected function readBytes($bytes)
	{
		$data = '';

		while ($bytes > 0 && !feof($this->stream))
		{
			$buffer = @fread($this->stream, $bytes);
			if ($buffer === false)
				break;

			$meta = $this->options['timeout'] > 0
				? stream_get_meta_data($this->stream)
				: array('timed_out' => false);

			$data  .= $buffer;
			$bytes -= \CUtil::binStrlen($buffer);

			if ($meta['timed_out'])
				break;
		}

		if ($bytes > 0)
		{
			$this->reset();
			return false;
		}

		return $data;
	}

	protected function readLine()
	{
		$line = '';

		while (!feof($this->stream))
		{
			$buffer = @fgets($this->stream, 4096);
			if ($buffer === false)
				break;

			$meta = $this->options['timeout'] > 0
				? stream_get_meta_data($this->stream)
				: array('timed_out' => false);

			$line .= $buffer;

			$eolRegex = '/ (?<literal> \{ (?<bytes> \d+ ) \} )? \r\n $ /x';
			if (preg_match($eolRegex, $line, $matches))
			{
				if (empty($matches['literal']))
					break;

				if ($meta['timed_out'])
					return false;

				$data = $this->readBytes($matches['bytes']);
				if ($data === false)
					return false;

				$line .= $data;
			}

			if ($meta['timed_out'])
				break;
		}

		if (!preg_match('/\r\n$/', $line, $matches))
		{
			$this->reset();
			return false;
		}

		return $line;
	}

	protected function readResponse()
	{
		do
		{
			$line = $this->readLine();
			if ($line === false)
				return false;

			if (strpos($line, '*') === 0)
				$this->sessUntagged[] = $line;
		}
		while (strpos($line, '*') === 0);

		return $line;
	}

	protected static function prepareString($data)
	{
		if (preg_match('/^[^\x00\x0a\x0d\x80-\xff]*$/', $data))
			return sprintf('"%s"', static::escapeQuoted($data));
		else
			return sprintf("{%u}\x00%s", \CUtil::binStrlen($data), $data);
	}

	protected static function escapeQuoted($data)
	{
		return str_replace(array('\\', '"'), array('\\\\', '\\"'), $data);
	}

	protected static function unescapeQuoted($data)
	{
		return str_replace(array('\\\\', '\\"'), array('\\', '"'), $data);
	}

	protected function encodeUtf7Imap($data)
	{
		if (!$data)
			return $data;

		$result = Encoding::convertEncoding($data, $this->options['encoding'], 'UTF7-IMAP');

		if ($result === false)
		{
			$result = $data;

			$result = Encoding::convertEncoding($result, $this->options['encoding'], 'UTF-8');
			$result = str_replace('&', '&-', $result);

			$result = preg_replace_callback('/[\x00-\x1f\x7f-\xff]+/', function($matches)
			{
				$result = $matches[0];

				$result = Encoding::convertEncoding($result, 'UTF-8', 'UTF-16BE');
				$result = base64_encode($result);
				$result = str_replace('/', ',', $result);
				$result = str_replace('=', '', $result);
				$result = '&' . $result . '-';

				return $result;
			}, $result);
		}

		return $result;
	}

	protected function decodeUtf7Imap($data)
	{
		if (!$data)
			return $data;

		$result = Encoding::convertEncoding($data, 'UTF7-IMAP', $this->options['encoding']);

		if ($result === false)
		{
			$result = $data;

			$result = preg_replace_callback('/&([\x2b\x2c\x30-\x39\x41-\x5a\x61-\x7a]+)-/', function($matches)
			{
				$result = $matches[1];

				$result = str_replace(',', '/', $result);
				$result = base64_decode($result);
				$result = Encoding::convertEncoding($result, 'UTF-16BE', 'UTF-8');

				return $result;
			}, $result);

			$result = str_replace('&-', '&', $result);
			$result = Encoding::convertEncoding($result, 'UTF-8', $this->options['encoding']);
		}

		return $result;
	}

	protected function errorMessage($errors, $details = null)
	{
		$errors  = array_filter((array) $errors);
		$details = array_filter((array) $details);

		$this->errors = new Main\ErrorCollection();
		foreach ($errors as $i => $error)
		{
			$errors[$i] = static::decodeError($error);
			$this->errors->setError(new Main\Error($errors[$i], $error > 0 ? $error : 0));
		}

		$error = join(': ', $errors);
		if ($details)
		{
			$error .= sprintf(' (%s)', join(': ', $details));
			foreach ($details as $item)
				$this->errors->setError(new Main\Error($item, -1));
		}

		return $error;
	}

	public function getErrors()
	{
		return $this->errors;
	}

	/**
	 * Returns error message
	 *
	 * @param int $code Error code.
	 * @return string
	 */
	public static function decodeError($code)
	{
		switch ($code)
		{
			case self::ERR_CONNECT:
				return Loc::getMessage('MAIL_IMAP_ERR_CONNECT');
			case self::ERR_REJECTED:
				return Loc::getMessage('MAIL_IMAP_ERR_REJECTED');
			case self::ERR_COMMUNICATE:
				return Loc::getMessage('MAIL_IMAP_ERR_COMMUNICATE');
			case self::ERR_EMPTY_RESPONSE:
				return Loc::getMessage('MAIL_IMAP_ERR_EMPTY_RESPONSE');
			case self::ERR_BAD_SERVER:
				return Loc::getMessage('MAIL_IMAP_ERR_BAD_SERVER');
			case self::ERR_STARTTLS:
				return Loc::getMessage('MAIL_IMAP_ERR_STARTTLS');
			case self::ERR_COMMAND_REJECTED:
				return Loc::getMessage('MAIL_IMAP_ERR_COMMAND_REJECTED');
			case self::ERR_CAPABILITY:
				return Loc::getMessage('MAIL_IMAP_ERR_CAPABILITY');
			case self::ERR_AUTH:
				return Loc::getMessage('MAIL_IMAP_ERR_AUTH');
			case self::ERR_AUTH_MECH:
				return Loc::getMessage('MAIL_IMAP_ERR_AUTH_MECH');
			case self::ERR_AUTH_OAUTH:
				return Loc::getMessage('MAIL_IMAP_ERR_AUTH_OAUTH');
			case self::ERR_LIST:
				return Loc::getMessage('MAIL_IMAP_ERR_LIST');
			case self::ERR_SELECT:
				return Loc::getMessage('MAIL_IMAP_ERR_SELECT');
			case self::ERR_SEARCH:
				return Loc::getMessage('MAIL_IMAP_ERR_SEARCH');
			case self::ERR_FETCH:
				return Loc::getMessage('MAIL_IMAP_ERR_FETCH');
			case self::ERR_APPEND:
				return Loc::getMessage('MAIL_IMAP_ERR_APPEND');
			case self::ERR_STORE:
				return Loc::getMessage('MAIL_IMAP_ERR_STORE');

			default:
				return Loc::getMessage('MAIL_IMAP_ERR_DEFAULT');
		}
	}

}