1<?php
2
3/*
4 * This file is part of MailSo.
5 *
6 * (c) 2014 Usenko Timur
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace MailSo\Imap;
13
14/**
15 * @category MailSo
16 * @package Imap
17 */
18class ImapClient extends \MailSo\Net\NetClient
19{
20	/**
21	 * @var string
22	 */
23	const TAG_PREFIX = 'TAG';
24
25	/**
26	 * @var int
27	 */
28	private $iResponseBufParsedPos;
29
30	/**
31	 * @var int
32	 */
33	private $iTagCount;
34
35	/**
36	 * @var array
37	 */
38	private $aCapabilityItems;
39
40	/**
41	 * @var \MailSo\Imap\FolderInformation
42	 */
43	private $oCurrentFolderInfo;
44
45	/**
46	 * @var array
47	 */
48	private $aLastResponse;
49
50	/**
51	 * @var array
52	 */
53	private $aFetchCallbacks;
54
55	/**
56	 * @var bool
57	 */
58	private $bNeedNext;
59
60	/**
61	 * @var array
62	 */
63	private $aPartialResponses;
64
65	/**
66	 * @var array
67	 */
68	private $aTagTimeouts;
69
70	/**
71	 * @var bool
72	 */
73	private $bIsLoggined;
74
75	/**
76	 * @var bool
77	 */
78	private $bIsSelected;
79
80	/**
81	 * @var string
82	 */
83	private $sLogginedUser;
84
85	/**
86	 * @var bool
87	 */
88	public $__FORCE_SELECT_ON_EXAMINE__;
89
90	/**
91	 * @access protected
92	 */
93	protected function __construct()
94	{
95		parent::__construct();
96
97		$this->iTagCount = 0;
98		$this->aCapabilityItems = null;
99		$this->oCurrentFolderInfo = null;
100		$this->aFetchCallbacks = null;
101		$this->iResponseBufParsedPos = 0;
102
103		$this->aLastResponse = array();
104		$this->bNeedNext = true;
105		$this->aPartialResponses = array();
106
107		$this->aTagTimeouts = array();
108
109		$this->bIsLoggined = false;
110		$this->bIsSelected = false;
111		$this->sLogginedUser = '';
112
113		$this->__FORCE_SELECT_ON_EXAMINE__ = false;
114
115		@\ini_set('xdebug.max_nesting_level', 500);
116	}
117
118	/**
119	 * @return \MailSo\Imap\ImapClient
120	 */
121	public static function NewInstance()
122	{
123		return new self();
124	}
125
126	/**
127	 * @return string
128	 */
129	public function GetLogginedUser()
130	{
131		return $this->sLogginedUser;
132	}
133
134	/**
135	 * @param string $sServerName
136	 * @param int $iPort = 143
137	 * @param int $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT
138	 * @param bool $bVerifySsl = false
139	 * @param bool $bAllowSelfSigned = true
140	 * @param string $sClientCert = ''
141	 *
142	 * @return \MailSo\Imap\ImapClient
143	 *
144	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
145	 * @throws \MailSo\Net\Exceptions\Exception
146	 * @throws \MailSo\Imap\Exceptions\Exception
147	 */
148	public function Connect($sServerName, $iPort = 143,
149		$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT,
150		$bVerifySsl = false, $bAllowSelfSigned = true,
151		$sClientCert = '')
152	{
153		$this->aTagTimeouts['*'] = \microtime(true);
154
155		parent::Connect($sServerName, $iPort, $iSecurityType, $bVerifySsl, $bAllowSelfSigned, $sClientCert);
156
157		$this->parseResponseWithValidation('*', true);
158
159		if (\MailSo\Net\Enumerations\ConnectionSecurityType::UseStartTLS(
160			$this->IsSupported('STARTTLS'), $this->iSecurityType))
161		{
162			$this->SendRequestWithCheck('STARTTLS');
163			$this->EnableCrypto();
164
165			$this->aCapabilityItems = null;
166		}
167		else if (\MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS === $this->iSecurityType)
168		{
169			$this->writeLogException(
170				new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'),
171				\MailSo\Log\Enumerations\Type::ERROR, true);
172		}
173
174		return $this;
175	}
176
177	protected function _xor($string, $string2)
178    {
179        $result = '';
180        $size   = strlen($string);
181        for ($i=0; $i<$size; $i++) {
182            $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
183        }
184        return $result;
185    }
186
187	/**
188	 * @param string $sLogin
189	 * @param string $sPassword
190	 * @param string $sProxyAuthUser = ''
191	 * @param bool $bUseAuthPlainIfSupported = true
192	 * @param bool $bUseAuthCramMd5IfSupported = true
193	 *
194	 * @return \MailSo\Imap\ImapClient
195	 *
196	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
197	 * @throws \MailSo\Net\Exceptions\Exception
198	 * @throws \MailSo\Imap\Exceptions\Exception
199	 */
200	public function Login($sLogin, $sPassword, $sProxyAuthUser = '',
201		$bUseAuthPlainIfSupported = true, $bUseAuthCramMd5IfSupported = true)
202	{
203		if (!\MailSo\Base\Validator::NotEmptyString($sLogin, true) ||
204			!\MailSo\Base\Validator::NotEmptyString($sPassword, true))
205		{
206			$this->writeLogException(
207				new \MailSo\Base\Exceptions\InvalidArgumentException(),
208				\MailSo\Log\Enumerations\Type::ERROR, true);
209		}
210
211		$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sLogin));
212
213		$sPassword = $sPassword;
214
215		$this->sLogginedUser = $sLogin;
216
217		try
218		{
219			if ($bUseAuthCramMd5IfSupported && $this->IsSupported('AUTH=CRAM-MD5'))
220			{
221				$this->SendRequest('AUTHENTICATE', array('CRAM-MD5'));
222
223				$aResponse = $this->parseResponseWithValidation();
224				if ($aResponse && \is_array($aResponse) && 0 < \count($aResponse) &&
225					\MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $aResponse[\count($aResponse) - 1]->ResponseType)
226				{
227					$oContinuationResponse = null;
228					foreach ($aResponse as $oResponse)
229					{
230						if ($oResponse && \MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $oResponse->ResponseType)
231						{
232							$oContinuationResponse = $oResponse;
233						}
234					}
235
236					if ($oContinuationResponse && !empty($oContinuationResponse->ResponseList[1]))
237					{
238						$sTicket = @\base64_decode($oContinuationResponse->ResponseList[1]);
239						$this->oLogger->Write('ticket: '.$sTicket);
240
241						$sToken = \base64_encode($sLogin.' '.\MailSo\Base\Utils::Hmac($sTicket, $sPassword));
242
243						if ($this->oLogger)
244						{
245							$this->oLogger->AddSecret($sToken);
246						}
247
248						$this->sendRaw($sToken, true, '*******');
249						$this->parseResponseWithValidation();
250					}
251					else
252					{
253						$this->writeLogException(
254							new \MailSo\Imap\Exceptions\LoginException(),
255							\MailSo\Log\Enumerations\Type::NOTICE, true);
256					}
257				}
258				else
259				{
260					$this->writeLogException(
261						new \MailSo\Imap\Exceptions\LoginException(),
262						\MailSo\Log\Enumerations\Type::NOTICE, true);
263				}
264			}
265			else if ($bUseAuthPlainIfSupported && $this->IsSupported('AUTH=PLAIN'))
266			{
267				$sToken = \base64_encode("\0".$sLogin."\0".$sPassword);
268				if ($this->oLogger)
269				{
270					$this->oLogger->AddSecret($sToken);
271				}
272
273				if ($this->IsSupported('AUTH=SASL-IR') && false)
274				{
275					$this->SendRequestWithCheck('AUTHENTICATE', array('PLAIN', $sToken));
276				}
277				else
278				{
279					$this->SendRequest('AUTHENTICATE', array('PLAIN'));
280					$this->parseResponseWithValidation();
281
282					$this->sendRaw($sToken, true, '*******');
283					$this->parseResponseWithValidation();
284				}
285			}
286			else
287			{
288				if ($this->oLogger)
289				{
290					$this->oLogger->AddSecret($this->EscapeString($sPassword));
291				}
292
293				$this->SendRequestWithCheck('LOGIN',
294					array(
295						$this->EscapeString($sLogin),
296						$this->EscapeString($sPassword)
297					));
298			}
299//			else
300//			{
301//				$this->writeLogException(
302//					new \MailSo\Imap\Exceptions\LoginBadMethodException(),
303//					\MailSo\Log\Enumerations\Type::NOTICE, true);
304//			}
305
306			if (0 < \strlen($sProxyAuthUser))
307			{
308				$this->SendRequestWithCheck('PROXYAUTH', array($this->EscapeString($sProxyAuthUser)));
309			}
310		}
311		catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException)
312		{
313			$this->writeLogException(
314				new \MailSo\Imap\Exceptions\LoginBadCredentialsException(
315					$oException->GetResponses(), '', 0, $oException),
316				\MailSo\Log\Enumerations\Type::NOTICE, true);
317		}
318
319		$this->bIsLoggined = true;
320		$this->aCapabilityItems = null;
321
322		return $this;
323	}
324
325	/**
326	 * @param string $sXOAuth2Token
327	 *
328	 * @return \MailSo\Imap\ImapClient
329	 *
330	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
331	 * @throws \MailSo\Net\Exceptions\Exception
332	 * @throws \MailSo\Imap\Exceptions\Exception
333	 */
334	public function LoginWithXOauth2($sXOAuth2Token)
335	{
336		if (!\MailSo\Base\Validator::NotEmptyString($sXOAuth2Token, true))
337		{
338			$this->writeLogException(
339				new \MailSo\Base\Exceptions\InvalidArgumentException(),
340				\MailSo\Log\Enumerations\Type::ERROR, true);
341		}
342
343		if (!$this->IsSupported('AUTH=XOAUTH2'))
344		{
345			$this->writeLogException(
346				new \MailSo\Imap\Exceptions\LoginBadMethodException(),
347				\MailSo\Log\Enumerations\Type::NOTICE, true);
348		}
349
350		try
351		{
352			$this->SendRequest('AUTHENTICATE', array('XOAUTH2', \trim($sXOAuth2Token)));
353			$aR = $this->parseResponseWithValidation();
354
355			if (\is_array($aR) && 0 < \count($aR) && isset($aR[\count($aR) - 1]))
356			{
357				$oR = $aR[\count($aR) - 1];
358				if (\MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $oR->ResponseType)
359				{
360					if (!empty($oR->ResponseList[1]) && preg_match('/^[a-zA-Z0-9=+\/]+$/', $oR->ResponseList[1]))
361					{
362						$this->Logger()->Write(\base64_decode($oR->ResponseList[1]),
363							\MailSo\Log\Enumerations\Type::WARNING);
364					}
365
366					$this->sendRaw('');
367					$this->parseResponseWithValidation();
368				}
369			}
370		}
371		catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException)
372		{
373			$this->writeLogException(
374				new \MailSo\Imap\Exceptions\LoginBadCredentialsException(
375					$oException->GetResponses(), '', 0, $oException),
376				\MailSo\Log\Enumerations\Type::NOTICE, true);
377		}
378
379		$this->bIsLoggined = true;
380		$this->aCapabilityItems = null;
381
382		return $this;
383	}
384
385	/**
386	 * @return \MailSo\Imap\ImapClient
387	 *
388	 * @throws \MailSo\Net\Exceptions\Exception
389	 */
390	public function Logout()
391	{
392		if ($this->bIsLoggined)
393		{
394			$this->bIsLoggined = false;
395			$this->SendRequestWithCheck('LOGOUT', array());
396		}
397
398		return $this;
399	}
400
401	/**
402	 * @return \MailSo\Imap\ImapClient
403	 */
404	public function ForceCloseConnection()
405	{
406		$this->Disconnect();
407
408		return $this;
409	}
410
411	/**
412	 * @return bool
413	 */
414	public function IsLoggined()
415	{
416		return $this->IsConnected() && $this->bIsLoggined;
417	}
418
419	/**
420	 * @return bool
421	 */
422	public function IsSelected()
423	{
424		return $this->IsLoggined() && $this->bIsSelected;
425	}
426
427	/**
428	 * @return array|null
429	 *
430	 * @throws \MailSo\Net\Exceptions\Exception
431	 * @throws \MailSo\Imap\Exceptions\Exception
432	 */
433	public function Capability()
434	{
435		$this->SendRequestWithCheck('CAPABILITY', array(), true);
436		return $this->aCapabilityItems;
437	}
438
439	/**
440	 * @param string $sExtentionName
441	 * @return bool
442	 *
443	 * @throws \MailSo\Net\Exceptions\Exception
444	 * @throws \MailSo\Imap\Exceptions\Exception
445	 */
446	public function IsSupported($sExtentionName)
447	{
448		$bResult = \MailSo\Base\Validator::NotEmptyString($sExtentionName, true);
449		if ($bResult && null === $this->aCapabilityItems)
450		{
451			$this->aCapabilityItems = $this->Capability();
452		}
453
454		return $bResult && \is_array($this->aCapabilityItems) &&
455			\in_array(\strtoupper($sExtentionName), $this->aCapabilityItems);
456	}
457
458	/**
459	 * @return \MailSo\Imap\NamespaceResult|null
460	 *
461	 * @throws \MailSo\Net\Exceptions\Exception
462	 * @throws \MailSo\Imap\Exceptions\Exception
463	 */
464	public function GetNamespace()
465	{
466		if (!$this->IsSupported('NAMESPACE'))
467		{
468			return null;
469		}
470
471		$oReturn = false;
472
473		$this->SendRequest('NAMESPACE');
474		$aResult = $this->parseResponseWithValidation();
475
476		$oImapResponse = null;
477		foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
478		{
479			if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType &&
480				'NAMESPACE' === $oImapResponse->StatusOrIndex)
481			{
482				$oReturn = NamespaceResult::NewInstance();
483				$oReturn->InitByImapResponse($oImapResponse);
484				break;
485			}
486		}
487
488		if (false === $oReturn)
489		{
490			$this->writeLogException(
491				new \MailSo\Imap\Exceptions\ResponseException(),
492				\MailSo\Log\Enumerations\Type::ERROR, true);
493		}
494
495		return $oReturn;
496	}
497
498	/**
499	 * @return \MailSo\Imap\ImapClient
500	 *
501	 * @throws \MailSo\Net\Exceptions\Exception
502	 * @throws \MailSo\Imap\Exceptions\Exception
503	 */
504	public function Noop()
505	{
506		return $this->SendRequestWithCheck('NOOP');
507	}
508
509	/**
510	 * @param string $sFolderName
511	 *
512	 * @return \MailSo\Imap\ImapClient
513	 *
514	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
515	 * @throws \MailSo\Net\Exceptions\Exception
516	 * @throws \MailSo\Imap\Exceptions\Exception
517	 */
518	public function FolderCreate($sFolderName)
519	{
520		return $this->SendRequestWithCheck('CREATE',
521			array($this->EscapeString($sFolderName)));
522	}
523
524	/**
525	 * @param string $sFolderName
526	 *
527	 * @return \MailSo\Imap\ImapClient
528	 *
529	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
530	 * @throws \MailSo\Net\Exceptions\Exception
531	 * @throws \MailSo\Imap\Exceptions\Exception
532	 */
533	public function FolderDelete($sFolderName)
534	{
535		return $this->SendRequestWithCheck('DELETE',
536			array($this->EscapeString($sFolderName)));
537	}
538
539	/**
540	 * @param string $sFolderName
541	 *
542	 * @return \MailSo\Imap\ImapClient
543	 *
544	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
545	 * @throws \MailSo\Net\Exceptions\Exception
546	 * @throws \MailSo\Imap\Exceptions\Exception
547	 */
548	public function FolderSubscribe($sFolderName)
549	{
550		return $this->SendRequestWithCheck('SUBSCRIBE',
551			array($this->EscapeString($sFolderName)));
552	}
553
554	/**
555	 * @param string $sFolderName
556	 *
557	 * @return \MailSo\Imap\ImapClient
558	 *
559	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
560	 * @throws \MailSo\Net\Exceptions\Exception
561	 * @throws \MailSo\Imap\Exceptions\Exception
562	 */
563	public function FolderUnSubscribe($sFolderName)
564	{
565		return $this->SendRequestWithCheck('UNSUBSCRIBE',
566			array($this->EscapeString($sFolderName)));
567	}
568
569	/**
570	 * @param string $sOldFolderName
571	 * @param string $sNewFolderName
572	 *
573	 * @return \MailSo\Imap\ImapClient
574	 *
575	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
576	 * @throws \MailSo\Net\Exceptions\Exception
577	 * @throws \MailSo\Imap\Exceptions\Exception
578	 */
579	public function FolderRename($sOldFolderName, $sNewFolderName)
580	{
581		return $this->SendRequestWithCheck('RENAME', array(
582			$this->EscapeString($sOldFolderName),
583			$this->EscapeString($sNewFolderName)));
584	}
585
586	/**
587	 * @param array $aResult
588	 *
589	 * @return array
590	 */
591	protected function getStatusFolderInformation($aResult)
592	{
593		$aReturn = array();
594
595		if (\is_array($aResult))
596		{
597			$oImapResponse = null;
598			foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
599			{
600				if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType &&
601					'STATUS' === $oImapResponse->StatusOrIndex && isset($oImapResponse->ResponseList[3]) &&
602					\is_array($oImapResponse->ResponseList[3]))
603				{
604					$sName = null;
605					foreach ($oImapResponse->ResponseList[3] as $sArrayItem)
606					{
607						if (null === $sName)
608						{
609							$sName = $sArrayItem;
610						}
611						else
612						{
613							$aReturn[$sName] = $sArrayItem;
614							$sName = null;
615						}
616					}
617				}
618			}
619		}
620
621		return $aReturn;
622	}
623
624	/**
625	 * @param string $sFolderName
626	 * @param array $aStatusItems
627	 *
628	 * @return array|bool
629	 *
630	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
631	 * @throws \MailSo\Net\Exceptions\Exception
632	 * @throws \MailSo\Imap\Exceptions\Exception
633	 */
634	public function FolderStatus($sFolderName, array $aStatusItems)
635	{
636		$aResult = false;
637		if (\count($aStatusItems) > 0)
638		{
639			$this->SendRequest('STATUS',
640				array($this->EscapeString($sFolderName), $aStatusItems));
641
642			$aResult = $this->getStatusFolderInformation(
643				$this->parseResponseWithValidation());
644		}
645
646		return $aResult;
647	}
648
649	/**
650	 * @param array|string $mName
651	 *
652	 * @return string
653	 */
654	private function getArrayNameToStringName($mName)
655	{
656		if (\is_string($mName))
657		{
658			return $mName;
659		}
660
661		if (\is_array($mName))
662		{
663			if (0 === \count($mName))
664			{
665				return '[]';
666			}
667
668			foreach ($mName as &$mSubName)
669			{
670				$mSubName = "[{$this->getArrayNameToStringName($mSubName)}]";
671			}
672
673			return \implode('', $mName);
674		}
675
676		return '';
677	}
678
679	/**
680	 * @param array $aResult
681	 * @param string $sStatus
682	 * @param bool $bUseListStatus = false
683	 *
684	 * @return array
685	 */
686	private function getFoldersFromResult(array $aResult, $sStatus, $bUseListStatus = false)
687	{
688		$aReturn = array();
689
690		$sDelimiter = '';
691		$bInbox = false;
692
693		$oImapResponse = null;
694		foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
695		{
696			if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType &&
697				$sStatus === $oImapResponse->StatusOrIndex && 5 <= count($oImapResponse->ResponseList))
698			{
699				try
700				{
701					/**
702					 * A bug in the parser converts folder names that start with '[' into arrays,
703					 * and subfolders are in $oImapResponse->ResponseList[5+]
704					 * https://github.com/the-djmaze/snappymail/issues/1
705					 * https://github.com/the-djmaze/snappymail/issues/70
706					 * https://github.com/RainLoop/rainloop-webmail/issues/2037
707					 */
708					$aFullNameRawList = \array_slice($oImapResponse->ResponseList, 4);
709					foreach ($aFullNameRawList as &$sName)
710					{
711						$sName = $this->getArrayNameToStringName($sName);
712					}
713
714					$sFullNameRaw = \implode('', $aFullNameRawList);
715
716					$oFolder = Folder::NewInstance($sFullNameRaw,
717						$oImapResponse->ResponseList[3], $oImapResponse->ResponseList[2]);
718
719					if ($oFolder->IsInbox())
720					{
721						$bInbox = true;
722					}
723
724					if (empty($sDelimiter))
725					{
726						$sDelimiter = $oFolder->Delimiter();
727					}
728
729					$aReturn[] = $oFolder;
730				}
731				catch (\MailSo\Base\Exceptions\InvalidArgumentException $oException)
732				{
733					$this->writeLogException($oException, \MailSo\Log\Enumerations\Type::WARNING, false);
734				}
735			}
736		}
737
738		if (!$bInbox && !empty($sDelimiter))
739		{
740			$aReturn[] = Folder::NewInstance('INBOX', $sDelimiter);
741		}
742
743		if ($bUseListStatus)
744		{
745			foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
746			{
747				if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType &&
748					'STATUS' === $oImapResponse->StatusOrIndex &&
749					isset($oImapResponse->ResponseList[2]) &&
750					isset($oImapResponse->ResponseList[3]) &&
751					\is_array($oImapResponse->ResponseList[3]))
752				{
753					$sFolderNameRaw = $oImapResponse->ResponseList[2];
754
755					$oCurrentFolder = null;
756					foreach ($aReturn as &$oFolder)
757					{
758						if ($oFolder && $sFolderNameRaw === $oFolder->FullNameRaw())
759						{
760							$oCurrentFolder =& $oFolder;
761							break;
762						}
763					}
764
765					if (null !== $oCurrentFolder)
766					{
767						$sName = null;
768						$aStatus = array();
769
770						foreach ($oImapResponse->ResponseList[3] as $sArrayItem)
771						{
772							if (null === $sName)
773							{
774								$sName = $sArrayItem;
775							}
776							else
777							{
778								$aStatus[$sName] = $sArrayItem;
779								$sName = null;
780							}
781						}
782
783						if (0 < count($aStatus))
784						{
785							$oCurrentFolder->SetExtended('STATUS', $aStatus);
786						}
787					}
788
789					unset($oCurrentFolder);
790				}
791			}
792		}
793
794		return $aReturn;
795	}
796
797	/**
798	 * @param bool $bIsSubscribeList
799	 * @param string $sParentFolderName = ''
800	 * @param string $sListPattern = '*'
801	 * @param bool $bUseListStatus = false
802	 *
803	 * @return array
804	 *
805	 * @throws \MailSo\Net\Exceptions\Exception
806	 * @throws \MailSo\Imap\Exceptions\Exception
807	 */
808	private function specificFolderList($bIsSubscribeList, $sParentFolderName = '', $sListPattern = '*', $bUseListStatus = false)
809	{
810		$sCmd = 'LSUB';
811		if (!$bIsSubscribeList)
812		{
813			$sCmd = 'LIST';
814		}
815
816		$sListPattern = 0 === strlen(trim($sListPattern)) ? '*' : $sListPattern;
817
818		$aParameters = array(
819			$this->EscapeString($sParentFolderName),
820			$this->EscapeString($sListPattern)
821		);
822
823		if ($bUseListStatus && !$bIsSubscribeList && $this->IsSupported('LIST-STATUS'))
824		{
825			$aL = array(
826				\MailSo\Imap\Enumerations\FolderStatus::MESSAGES,
827				\MailSo\Imap\Enumerations\FolderStatus::UNSEEN,
828				\MailSo\Imap\Enumerations\FolderStatus::UIDNEXT
829			);
830
831//			if ($this->IsSupported('CONDSTORE'))
832//			{
833//				$aL[] = \MailSo\Imap\Enumerations\FolderStatus::HIGHESTMODSEQ;
834//			}
835
836			$aParameters[] = 'RETURN';
837			$aParameters[] = array('STATUS', $aL);
838		}
839		else
840		{
841			$bUseListStatus = false;
842		}
843
844		$this->SendRequest($sCmd, $aParameters);
845
846		return $this->getFoldersFromResult(
847			$this->parseResponseWithValidation(), $sCmd, $bUseListStatus);
848	}
849
850	/**
851	 * @param string $sParentFolderName = ''
852	 * @param string $sListPattern = '*'
853	 *
854	 * @return array
855	 *
856	 * @throws \MailSo\Net\Exceptions\Exception
857	 * @throws \MailSo\Imap\Exceptions\Exception
858	 */
859	public function FolderList($sParentFolderName = '', $sListPattern = '*')
860	{
861		return $this->specificFolderList(false, $sParentFolderName, $sListPattern);
862	}
863
864	/**
865	 * @param string $sParentFolderName = ''
866	 * @param string $sListPattern = '*'
867	 *
868	 * @return array
869	 *
870	 * @throws \MailSo\Net\Exceptions\Exception
871	 * @throws \MailSo\Imap\Exceptions\Exception
872	 */
873	public function FolderSubscribeList($sParentFolderName = '', $sListPattern = '*')
874	{
875		return $this->specificFolderList(true, $sParentFolderName, $sListPattern);
876	}
877
878	/**
879	 * @param string $sParentFolderName = ''
880	 * @param string $sListPattern = '*'
881	 *
882	 * @return array
883	 *
884	 * @throws \MailSo\Net\Exceptions\Exception
885	 * @throws \MailSo\Imap\Exceptions\Exception
886	 */
887	public function FolderStatusList($sParentFolderName = '', $sListPattern = '*')
888	{
889		return $this->specificFolderList(false, $sParentFolderName, $sListPattern, true);
890	}
891
892	/**
893	 * @param array $aResult
894	 * @param string $sFolderName
895	 * @param bool $bIsWritable
896	 *
897	 * @return void
898	 */
899	protected function initCurrentFolderInformation($aResult, $sFolderName, $bIsWritable)
900	{
901		if (\is_array($aResult))
902		{
903			$oImapResponse = null;
904			$oResult = FolderInformation::NewInstance($sFolderName, $bIsWritable);
905
906			foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
907			{
908				if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType)
909				{
910					if (\count($oImapResponse->ResponseList) > 2 &&
911						'FLAGS' === $oImapResponse->ResponseList[1] && \is_array($oImapResponse->ResponseList[2]))
912					{
913						$oResult->Flags = $oImapResponse->ResponseList[2];
914					}
915
916					if (is_array($oImapResponse->OptionalResponse) && \count($oImapResponse->OptionalResponse) > 1)
917					{
918						if ('PERMANENTFLAGS' === $oImapResponse->OptionalResponse[0] &&
919							is_array($oImapResponse->OptionalResponse[1]))
920						{
921							$oResult->PermanentFlags = $oImapResponse->OptionalResponse[1];
922						}
923						else if ('UIDVALIDITY' === $oImapResponse->OptionalResponse[0] &&
924							isset($oImapResponse->OptionalResponse[1]))
925						{
926							$oResult->Uidvalidity = $oImapResponse->OptionalResponse[1];
927						}
928						else if ('UNSEEN' === $oImapResponse->OptionalResponse[0] &&
929							isset($oImapResponse->OptionalResponse[1]) &&
930							is_numeric($oImapResponse->OptionalResponse[1]))
931						{
932							$oResult->Unread = (int) $oImapResponse->OptionalResponse[1];
933						}
934						else if ('UIDNEXT' === $oImapResponse->OptionalResponse[0] &&
935							isset($oImapResponse->OptionalResponse[1]))
936						{
937							$oResult->Uidnext = $oImapResponse->OptionalResponse[1];
938						}
939						else if ('HIGHESTMODSEQ' === $oImapResponse->OptionalResponse[0] &&
940							isset($oImapResponse->OptionalResponse[1]) &&
941							\is_numeric($oImapResponse->OptionalResponse[1]))
942						{
943							$oResult->HighestModSeq = \trim($oImapResponse->OptionalResponse[1]);
944						}
945					}
946
947					if (\count($oImapResponse->ResponseList) > 2 &&
948						\is_string($oImapResponse->ResponseList[2]) &&
949						\is_numeric($oImapResponse->ResponseList[1]))
950					{
951						switch($oImapResponse->ResponseList[2])
952						{
953							case 'EXISTS':
954								$oResult->Exists = (int) $oImapResponse->ResponseList[1];
955								break;
956							case 'RECENT':
957								$oResult->Recent = (int) $oImapResponse->ResponseList[1];
958								break;
959						}
960					}
961				}
962			}
963
964			$this->oCurrentFolderInfo = $oResult;
965		}
966	}
967
968	/**
969	 * @param string $sFolderName
970	 * @param bool $bIsWritable
971	 * @param bool $bReSelectSameFolders
972	 *
973	 * @return \MailSo\Imap\ImapClient
974	 *
975	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
976	 * @throws \MailSo\Net\Exceptions\Exception
977	 * @throws \MailSo\Imap\Exceptions\Exception
978	 */
979	protected function selectOrExamineFolder($sFolderName, $bIsWritable, $bReSelectSameFolders)
980	{
981		if (!$bReSelectSameFolders)
982		{
983			if ($this->oCurrentFolderInfo &&
984				$sFolderName === $this->oCurrentFolderInfo->FolderName &&
985				$bIsWritable === $this->oCurrentFolderInfo->IsWritable)
986			{
987				return $this;
988			}
989		}
990
991		if (!\MailSo\Base\Validator::NotEmptyString($sFolderName, true))
992		{
993			throw new \MailSo\Base\Exceptions\InvalidArgumentException();
994		}
995
996		$this->SendRequest(($bIsWritable) ? 'SELECT' : 'EXAMINE',
997			array($this->EscapeString($sFolderName)));
998
999		$this->initCurrentFolderInformation(
1000			$this->parseResponseWithValidation(), $sFolderName, $bIsWritable);
1001
1002		$this->bIsSelected = true;
1003
1004		return $this;
1005	}
1006
1007	/**
1008	 * @param string $sFolderName
1009	 * @param bool $bReSelectSameFolders = false
1010	 *
1011	 * @return \MailSo\Imap\ImapClient
1012	 *
1013	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1014	 * @throws \MailSo\Net\Exceptions\Exception
1015	 * @throws \MailSo\Imap\Exceptions\Exception
1016	 */
1017	public function FolderSelect($sFolderName, $bReSelectSameFolders = false)
1018	{
1019		return $this->selectOrExamineFolder($sFolderName, true, $bReSelectSameFolders);
1020	}
1021
1022	/**
1023	 * @param string $sFolderName
1024	 * @param bool $bReSelectSameFolders = false
1025	 *
1026	 * @return \MailSo\Imap\ImapClient
1027	 *
1028	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1029	 * @throws \MailSo\Net\Exceptions\Exception
1030	 * @throws \MailSo\Imap\Exceptions\Exception
1031	 */
1032	public function FolderExamine($sFolderName, $bReSelectSameFolders = false)
1033	{
1034		return $this->selectOrExamineFolder($sFolderName, $this->__FORCE_SELECT_ON_EXAMINE__, $bReSelectSameFolders);
1035	}
1036
1037	/**
1038	 * @return \MailSo\Imap\ImapClient
1039	 *
1040	 * @throws \MailSo\Net\Exceptions\Exception
1041	 * @throws \MailSo\Imap\Exceptions\Exception
1042	 */
1043	public function FolderUnSelect()
1044	{
1045		if ($this->IsSelected() && $this->IsSupported('UNSELECT'))
1046		{
1047			$this->SendRequestWithCheck('UNSELECT');
1048			$this->bIsSelected = false;
1049		}
1050
1051		return $this;
1052	}
1053
1054	/**
1055	 * @param array $aInputFetchItems
1056	 * @param string $sIndexRange
1057	 * @param bool $bIndexIsUid
1058	 *
1059	 * @return array
1060	 *
1061	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1062	 * @throws \MailSo\Net\Exceptions\Exception
1063	 * @throws \MailSo\Imap\Exceptions\Exception
1064	 */
1065	public function Fetch(array $aInputFetchItems, $sIndexRange, $bIndexIsUid)
1066	{
1067		$sIndexRange = (string) $sIndexRange;
1068		if (!\MailSo\Base\Validator::NotEmptyString($sIndexRange, true))
1069		{
1070			$this->writeLogException(
1071				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1072				\MailSo\Log\Enumerations\Type::ERROR, true);
1073		}
1074
1075		$aFetchItems = \MailSo\Imap\Enumerations\FetchType::ChangeFetchItemsBefourRequest($aInputFetchItems);
1076		foreach ($aFetchItems as $sName => $mItem)
1077		{
1078			if (0 < \strlen($sName) && '' !== $mItem)
1079			{
1080				if (null === $this->aFetchCallbacks)
1081				{
1082					$this->aFetchCallbacks = array();
1083				}
1084
1085				$this->aFetchCallbacks[$sName] = $mItem;
1086			}
1087		}
1088
1089		$this->SendRequest((($bIndexIsUid) ? 'UID ' : '').'FETCH', array($sIndexRange, \array_keys($aFetchItems)));
1090		$aResult = $this->validateResponse($this->parseResponse());
1091		$this->aFetchCallbacks = null;
1092
1093		$aReturn = array();
1094		$oImapResponse = null;
1095		foreach ($aResult as $oImapResponse)
1096		{
1097			if (FetchResponse::IsValidFetchImapResponse($oImapResponse))
1098			{
1099				if (FetchResponse::IsNotEmptyFetchImapResponse($oImapResponse))
1100				{
1101					$aReturn[] = FetchResponse::NewInstance($oImapResponse);
1102				}
1103				else
1104				{
1105					if ($this->oLogger)
1106					{
1107						$this->oLogger->Write('Skipped Imap Response! ['.$oImapResponse->ToLine().']', \MailSo\Log\Enumerations\Type::NOTICE);
1108					}
1109				}
1110			}
1111		}
1112
1113		return $aReturn;
1114	}
1115
1116
1117	/**
1118	 * @return array|false
1119	 *
1120	 * @throws \MailSo\Net\Exceptions\Exception
1121	 * @throws \MailSo\Imap\Exceptions\Exception
1122	 */
1123	public function Quota()
1124	{
1125		$aReturn = false;
1126		if ($this->IsSupported('QUOTA'))
1127		{
1128			$this->SendRequest('GETQUOTAROOT "INBOX"');
1129			$aResult = $this->parseResponseWithValidation();
1130
1131			$aReturn = array(0, 0);
1132			$oImapResponse = null;
1133			foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
1134			{
1135				if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType
1136					&& 'QUOTA' === $oImapResponse->StatusOrIndex
1137					&& \is_array($oImapResponse->ResponseList)
1138					&& isset($oImapResponse->ResponseList[3])
1139					&& \is_array($oImapResponse->ResponseList[3])
1140					&& 2 < \count($oImapResponse->ResponseList[3])
1141					&& 'STORAGE' === \strtoupper($oImapResponse->ResponseList[3][0])
1142					&& \is_numeric($oImapResponse->ResponseList[3][1])
1143					&& \is_numeric($oImapResponse->ResponseList[3][2])
1144				)
1145				{
1146					$aReturn = array(
1147						(int) $oImapResponse->ResponseList[3][1],
1148						(int) $oImapResponse->ResponseList[3][2],
1149						0,
1150						0
1151					);
1152
1153					if (5 < \count($oImapResponse->ResponseList[3])
1154						&& 'MESSAGE' === \strtoupper($oImapResponse->ResponseList[3][3])
1155						&& \is_numeric($oImapResponse->ResponseList[3][4])
1156						&& \is_numeric($oImapResponse->ResponseList[3][5])
1157					)
1158					{
1159						$aReturn[2] = (int) $oImapResponse->ResponseList[3][4];
1160						$aReturn[3] = (int) $oImapResponse->ResponseList[3][5];
1161					}
1162				}
1163			}
1164		}
1165
1166		return $aReturn;
1167	}
1168
1169	/**
1170	 * @param array $aSortTypes
1171	 * @param string $sSearchCriterias
1172	 * @param bool $bReturnUid
1173	 *
1174	 * @return array
1175	 *
1176	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1177	 * @throws \MailSo\Net\Exceptions\Exception
1178	 * @throws \MailSo\Imap\Exceptions\Exception
1179	 */
1180	public function MessageSimpleSort($aSortTypes, $sSearchCriterias = 'ALL', $bReturnUid = true)
1181	{
1182		$sCommandPrefix = ($bReturnUid) ? 'UID ' : '';
1183		$sSearchCriterias = !\MailSo\Base\Validator::NotEmptyString($sSearchCriterias, true) || '*' === $sSearchCriterias
1184			? 'ALL' : $sSearchCriterias;
1185
1186		if (!\is_array($aSortTypes) || 0 === \count($aSortTypes))
1187		{
1188			$this->writeLogException(
1189				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1190				\MailSo\Log\Enumerations\Type::ERROR, true);
1191		}
1192		else if (!$this->IsSupported('SORT'))
1193		{
1194			$this->writeLogException(
1195				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1196				\MailSo\Log\Enumerations\Type::ERROR, true);
1197		}
1198
1199		$aRequest = array();
1200		$aRequest[] = $aSortTypes;
1201		$aRequest[] = \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? 'US-ASCII' : 'UTF-8';
1202		$aRequest[] = $sSearchCriterias;
1203
1204		$sCmd = 'SORT';
1205
1206		$this->SendRequest($sCommandPrefix.$sCmd, $aRequest);
1207		$aResult = $this->parseResponseWithValidation();
1208
1209		$aReturn = array();
1210		$oImapResponse = null;
1211		foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
1212		{
1213			if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType
1214				&& ($sCmd === $oImapResponse->StatusOrIndex ||
1215					($bReturnUid && 'UID' === $oImapResponse->StatusOrIndex) && !empty($oImapResponse->ResponseList[2]) &&
1216						$sCmd === $oImapResponse->ResponseList[2])
1217				&& \is_array($oImapResponse->ResponseList)
1218				&& 2 < \count($oImapResponse->ResponseList))
1219			{
1220				$iStart = 2;
1221				if ($bReturnUid && 'UID' === $oImapResponse->StatusOrIndex &&
1222					!empty($oImapResponse->ResponseList[2]) &&
1223					$sCmd === $oImapResponse->ResponseList[2])
1224				{
1225					$iStart = 3;
1226				}
1227
1228				for ($iIndex = $iStart, $iLen = \count($oImapResponse->ResponseList); $iIndex < $iLen; $iIndex++)
1229				{
1230					$aReturn[] = (int) $oImapResponse->ResponseList[$iIndex];
1231				}
1232			}
1233		}
1234
1235		return $aReturn;
1236	}
1237
1238	/**
1239	 * @param bool $bSort = false
1240	 * @param string $sSearchCriterias = 'ALL'
1241	 * @param array $aSearchOrSortReturn = null
1242	 * @param bool $bReturnUid = true
1243	 * @param string $sLimit = ''
1244	 * @param string $sCharset = ''
1245	 * @param array $aSortTypes = null
1246	 *
1247	 * @return array
1248	 *
1249	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1250	 * @throws \MailSo\Net\Exceptions\Exception
1251	 * @throws \MailSo\Imap\Exceptions\Exception
1252	 */
1253	private function simpleESearchOrESortHelper($bSort = false, $sSearchCriterias = 'ALL', $aSearchOrSortReturn = null, $bReturnUid = true, $sLimit = '', $sCharset = '', $aSortTypes = null)
1254	{
1255		$sCommandPrefix = ($bReturnUid) ? 'UID ' : '';
1256		$sSearchCriterias = 0 === \strlen($sSearchCriterias) || '*' === $sSearchCriterias
1257			? 'ALL' : $sSearchCriterias;
1258
1259		$sCmd = $bSort ? 'SORT': 'SEARCH';
1260		if ($bSort && (!\is_array($aSortTypes) || 0 === \count($aSortTypes) || !$this->IsSupported('SORT')))
1261		{
1262			$this->writeLogException(
1263				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1264				\MailSo\Log\Enumerations\Type::ERROR, true);
1265		}
1266
1267		if (!$this->IsSupported($bSort ? 'ESORT' : 'ESEARCH'))
1268		{
1269			$this->writeLogException(
1270				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1271				\MailSo\Log\Enumerations\Type::ERROR, true);
1272		}
1273
1274		if (!\is_array($aSearchOrSortReturn) || 0 === \count($aSearchOrSortReturn))
1275		{
1276			$aSearchOrSortReturn = array('ALL');
1277		}
1278
1279		$aRequest = array();
1280		if ($bSort)
1281		{
1282			$aRequest[] = 'RETURN';
1283			$aRequest[] = $aSearchOrSortReturn;
1284
1285			$aRequest[] = $aSortTypes;
1286			$aRequest[] = \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? 'US-ASCII' : 'UTF-8';
1287		}
1288		else
1289		{
1290			if (0 < \strlen($sCharset))
1291			{
1292				$aRequest[] = 'CHARSET';
1293				$aRequest[] = \strtoupper($sCharset);
1294			}
1295
1296			$aRequest[] = 'RETURN';
1297			$aRequest[] = $aSearchOrSortReturn;
1298		}
1299
1300		$aRequest[] = $sSearchCriterias;
1301
1302		if (0 < \strlen($sLimit))
1303		{
1304			$aRequest[] = $sLimit;
1305		}
1306
1307		$this->SendRequest($sCommandPrefix.$sCmd, $aRequest);
1308		$sRequestTag = $this->getCurrentTag();
1309
1310		$aResult = array();
1311		$aResponse = $this->parseResponseWithValidation();
1312
1313		if (\is_array($aResponse))
1314		{
1315			$oImapResponse = null;
1316			foreach ($aResponse as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
1317			{
1318				if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType
1319					&& ('ESEARCH' === $oImapResponse->StatusOrIndex || 'ESORT' === $oImapResponse->StatusOrIndex)
1320					&& \is_array($oImapResponse->ResponseList)
1321					&& isset($oImapResponse->ResponseList[2], $oImapResponse->ResponseList[2][0], $oImapResponse->ResponseList[2][1])
1322					&& 'TAG' === $oImapResponse->ResponseList[2][0] && $sRequestTag === $oImapResponse->ResponseList[2][1]
1323					&& (!$bReturnUid || ($bReturnUid && !empty($oImapResponse->ResponseList[3]) && 'UID' === $oImapResponse->ResponseList[3]))
1324				)
1325				{
1326					$iStart = 3;
1327					foreach ($oImapResponse->ResponseList as $iIndex => $mItem)
1328					{
1329						if ($iIndex >= $iStart)
1330						{
1331							switch ($mItem)
1332							{
1333								case 'ALL':
1334								case 'MAX':
1335								case 'MIN':
1336								case 'COUNT':
1337									if (isset($oImapResponse->ResponseList[$iIndex + 1]))
1338									{
1339										$aResult[$mItem] = $oImapResponse->ResponseList[$iIndex + 1];
1340									}
1341									break;
1342							}
1343						}
1344					}
1345				}
1346			}
1347		}
1348
1349		return $aResult;
1350	}
1351
1352	/**
1353	 * @param string $sSearchCriterias = 'ALL'
1354	 * @param array $aSearchReturn = null
1355	 * @param bool $bReturnUid = true
1356	 * @param string $sLimit = ''
1357	 * @param string $sCharset = ''
1358	 *
1359	 * @return array
1360	 *
1361	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1362	 * @throws \MailSo\Net\Exceptions\Exception
1363	 * @throws \MailSo\Imap\Exceptions\Exception
1364	 */
1365	public function MessageSimpleESearch($sSearchCriterias = 'ALL', $aSearchReturn = null, $bReturnUid = true, $sLimit = '', $sCharset = '')
1366	{
1367		return $this->simpleESearchOrESortHelper(false, $sSearchCriterias, $aSearchReturn, $bReturnUid, $sLimit, $sCharset);
1368	}
1369
1370	/**
1371	 * @param array $aSortTypes
1372	 * @param string $sSearchCriterias = 'ALL'
1373	 * @param array $aSearchReturn = null
1374	 * @param bool $bReturnUid = true
1375	 * @param string $sLimit = ''
1376	 *
1377	 * @return array
1378	 *
1379	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1380	 * @throws \MailSo\Net\Exceptions\Exception
1381	 * @throws \MailSo\Imap\Exceptions\Exception
1382	 */
1383	public function MessageSimpleESort($aSortTypes, $sSearchCriterias = 'ALL', $aSearchReturn = null, $bReturnUid = true, $sLimit = '')
1384	{
1385		return $this->simpleESearchOrESortHelper(true, $sSearchCriterias, $aSearchReturn, $bReturnUid, $sLimit, '', $aSortTypes);
1386	}
1387
1388	/**
1389	 * @param array $aResult
1390	 * @return \MailSo\Imap\Response
1391	 */
1392	private function findLastResponse($aResult)
1393	{
1394		$oResult = null;
1395		if (\is_array($aResult) && 0 < \count($aResult))
1396		{
1397			$oResult = $aResult[\count($aResult) - 1];
1398			if (!($oResult instanceof \MailSo\Imap\Response))
1399			{
1400				$oResult = null;
1401			}
1402		}
1403
1404		return $oResult;
1405	}
1406
1407	/**
1408	 * @param string $sSearchCriterias
1409	 * @param bool $bReturnUid = true
1410	 * @param string $sCharset = ''
1411	 *
1412	 * @return array
1413	 *
1414	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1415	 * @throws \MailSo\Net\Exceptions\Exception
1416	 * @throws \MailSo\Imap\Exceptions\Exception
1417	 */
1418	public function MessageSimpleSearch($sSearchCriterias = 'ALL', $bReturnUid = true, $sCharset = '')
1419	{
1420		$sCommandPrefix = ($bReturnUid) ? 'UID ' : '';
1421		$sSearchCriterias = 0 === \strlen($sSearchCriterias) || '*' === $sSearchCriterias
1422			? 'ALL' : $sSearchCriterias;
1423
1424		$aRequest = array();
1425		if (0 < \strlen($sCharset))
1426		{
1427			$aRequest[] = 'CHARSET';
1428			$aRequest[] = \strtoupper($sCharset);
1429		}
1430
1431		$aRequest[] = $sSearchCriterias;
1432
1433		$sCmd = 'SEARCH';
1434
1435		$sCont = $this->SendRequest($sCommandPrefix.$sCmd, $aRequest, true);
1436		if ('' !== $sCont)
1437		{
1438			$aResult = $this->parseResponseWithValidation();
1439			$oItem = $this->findLastResponse($aResult);
1440
1441			if ($oItem && \MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $oItem->ResponseType)
1442			{
1443				$aParts = explode("\r\n", $sCont);
1444				foreach ($aParts as $sLine)
1445				{
1446					$this->sendRaw($sLine);
1447
1448					$aResult = $this->parseResponseWithValidation();
1449					$oItem = $this->findLastResponse($aResult);
1450					if ($oItem && \MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $oItem->ResponseType)
1451					{
1452						continue;
1453					}
1454				}
1455			}
1456		}
1457		else
1458		{
1459			$aResult = $this->parseResponseWithValidation();
1460		}
1461
1462		$aReturn = array();
1463		$oImapResponse = null;
1464		foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
1465		{
1466			if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType
1467				&& ($sCmd === $oImapResponse->StatusOrIndex ||
1468					($bReturnUid && 'UID' === $oImapResponse->StatusOrIndex) && !empty($oImapResponse->ResponseList[2]) &&
1469						$sCmd === $oImapResponse->ResponseList[2])
1470				&& \is_array($oImapResponse->ResponseList)
1471				&& 2 < count($oImapResponse->ResponseList))
1472			{
1473				$iStart = 2;
1474				if ($bReturnUid && 'UID' === $oImapResponse->StatusOrIndex &&
1475					!empty($oImapResponse->ResponseList[2]) &&
1476					$sCmd === $oImapResponse->ResponseList[2])
1477				{
1478					$iStart = 3;
1479				}
1480
1481				for ($iIndex = $iStart, $iLen = \count($oImapResponse->ResponseList); $iIndex < $iLen; $iIndex++)
1482				{
1483					$aReturn[] = (int) $oImapResponse->ResponseList[$iIndex];
1484				}
1485			}
1486		}
1487
1488		$aReturn = \array_reverse($aReturn);
1489		return $aReturn;
1490	}
1491
1492	/**
1493	 * @param mixed $aValue
1494	 *
1495	 * @return mixed
1496	 */
1497	private function validateThreadItem($aValue)
1498	{
1499		$mResult = false;
1500		if (\is_numeric($aValue))
1501		{
1502			$mResult = (int) $aValue;
1503			if (0 >= $mResult)
1504			{
1505				$mResult = false;
1506			}
1507		}
1508		else if (\is_array($aValue))
1509		{
1510			if (1 === \count($aValue) && \is_numeric($aValue[0]))
1511			{
1512				$mResult = (int) $aValue[0];
1513				if (0 >= $mResult)
1514				{
1515					$mResult = false;
1516				}
1517			}
1518			else
1519			{
1520				$mResult = array();
1521				foreach ($aValue as $aValueItem)
1522				{
1523					$mTemp = $this->validateThreadItem($aValueItem);
1524					if (false !== $mTemp)
1525					{
1526						$mResult[] = $mTemp;
1527					}
1528				}
1529			}
1530		}
1531
1532		return $mResult;
1533	}
1534
1535	/**
1536	 * @param string $sSearchCriterias = 'ALL'
1537	 * @param bool $bReturnUid = true
1538	 * @param string $sCharset = \MailSo\Base\Enumerations\Charset::UTF_8
1539	 *
1540	 * @return array
1541	 *
1542	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1543	 * @throws \MailSo\Net\Exceptions\Exception
1544	 * @throws \MailSo\Imap\Exceptions\Exception
1545	 */
1546	public function MessageSimpleThread($sSearchCriterias = 'ALL', $bReturnUid = true, $sCharset = \MailSo\Base\Enumerations\Charset::UTF_8)
1547	{
1548		$sCommandPrefix = ($bReturnUid) ? 'UID ' : '';
1549		$sSearchCriterias = !\MailSo\Base\Validator::NotEmptyString($sSearchCriterias, true) || '*' === $sSearchCriterias
1550			? 'ALL' : $sSearchCriterias;
1551
1552		$sThreadType = '';
1553		switch (true)
1554		{
1555			case $this->IsSupported('THREAD=REFS'):
1556				$sThreadType = 'REFS';
1557				break;
1558			case $this->IsSupported('THREAD=REFERENCES'):
1559				$sThreadType = 'REFERENCES';
1560				break;
1561			case $this->IsSupported('THREAD=ORDEREDSUBJECT'):
1562				$sThreadType = 'ORDEREDSUBJECT';
1563				break;
1564			default:
1565				$this->writeLogException(
1566					new Exceptions\RuntimeException('Thread is not supported'),
1567					\MailSo\Log\Enumerations\Type::ERROR, true);
1568				break;
1569		}
1570
1571		$aRequest = array();
1572		$aRequest[] = $sThreadType;
1573		$aRequest[] = \strtoupper($sCharset);
1574		$aRequest[] = $sSearchCriterias;
1575
1576		$sCmd = 'THREAD';
1577
1578		$this->SendRequest($sCommandPrefix.$sCmd, $aRequest);
1579		$aResult = $this->parseResponseWithValidation();
1580
1581		$aReturn = array();
1582		$oImapResponse = null;
1583
1584		foreach ($aResult as /* @var $oImapResponse \MailSo\Imap\Response */ $oImapResponse)
1585		{
1586			if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType
1587				&& ($sCmd === $oImapResponse->StatusOrIndex ||
1588					($bReturnUid && 'UID' === $oImapResponse->StatusOrIndex) && !empty($oImapResponse->ResponseList[2]) &&
1589						$sCmd === $oImapResponse->ResponseList[2])
1590				&& \is_array($oImapResponse->ResponseList)
1591				&& 2 < \count($oImapResponse->ResponseList))
1592			{
1593				$iStart = 2;
1594				if ($bReturnUid && 'UID' === $oImapResponse->StatusOrIndex &&
1595					!empty($oImapResponse->ResponseList[2]) &&
1596					$sCmd === $oImapResponse->ResponseList[2])
1597				{
1598					$iStart = 3;
1599				}
1600
1601				for ($iIndex = $iStart, $iLen = \count($oImapResponse->ResponseList); $iIndex < $iLen; $iIndex++)
1602				{
1603					$aNewValue = $this->validateThreadItem($oImapResponse->ResponseList[$iIndex]);
1604					if (false !== $aNewValue)
1605					{
1606						$aReturn[] = $aNewValue;
1607					}
1608				}
1609			}
1610		}
1611
1612		return $aReturn;
1613	}
1614
1615	/**
1616	 * @param string $sToFolder
1617	 * @param string $sIndexRange
1618	 * @param bool $bIndexIsUid
1619	 *
1620	 * @return \MailSo\Imap\ImapClient
1621	 *
1622	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1623	 * @throws \MailSo\Net\Exceptions\Exception
1624	 * @throws \MailSo\Imap\Exceptions\Exception
1625	 */
1626	public function MessageCopy($sToFolder, $sIndexRange, $bIndexIsUid)
1627	{
1628		if (0 === \strlen($sIndexRange))
1629		{
1630			$this->writeLogException(
1631				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1632				\MailSo\Log\Enumerations\Type::ERROR, true);
1633		}
1634
1635		$sCommandPrefix = ($bIndexIsUid) ? 'UID ' : '';
1636		return $this->SendRequestWithCheck($sCommandPrefix.'COPY',
1637			array($sIndexRange, $this->EscapeString($sToFolder)));
1638	}
1639
1640	/**
1641	 * @param string $sToFolder
1642	 * @param string $sIndexRange
1643	 * @param bool $bIndexIsUid
1644	 *
1645	 * @return \MailSo\Imap\ImapClient
1646	 *
1647	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1648	 * @throws \MailSo\Net\Exceptions\Exception
1649	 * @throws \MailSo\Imap\Exceptions\Exception
1650	 */
1651	public function MessageMove($sToFolder, $sIndexRange, $bIndexIsUid)
1652	{
1653		if (0 === \strlen($sIndexRange))
1654		{
1655			$this->writeLogException(
1656				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1657				\MailSo\Log\Enumerations\Type::ERROR, true);
1658		}
1659
1660		if (!$this->IsSupported('MOVE'))
1661		{
1662			$this->writeLogException(
1663				new Exceptions\RuntimeException('Move is not supported'),
1664				\MailSo\Log\Enumerations\Type::ERROR, true);
1665		}
1666
1667		$sCommandPrefix = ($bIndexIsUid) ? 'UID ' : '';
1668		return $this->SendRequestWithCheck($sCommandPrefix.'MOVE',
1669			array($sIndexRange, $this->EscapeString($sToFolder)));
1670	}
1671
1672	/**
1673	 * @param string $sUidRangeIfSupported = ''
1674	 * @param bool $bForceUidExpunge = false
1675	 * @param bool $bExpungeAll = false
1676	 *
1677	 * @return \MailSo\Imap\ImapClient
1678	 *
1679	 * @throws \MailSo\Net\Exceptions\Exception
1680	 * @throws \MailSo\Imap\Exceptions\Exception
1681	 */
1682	public function MessageExpunge($sUidRangeIfSupported = '', $bForceUidExpunge = false, $bExpungeAll = false)
1683	{
1684		$sUidRangeIfSupported = \trim($sUidRangeIfSupported);
1685
1686		$sCmd = 'EXPUNGE';
1687		$aArguments = array();
1688
1689		if (!$bExpungeAll && $bForceUidExpunge && 0 < \strlen($sUidRangeIfSupported) && $this->IsSupported('UIDPLUS'))
1690		{
1691			$sCmd = 'UID '.$sCmd;
1692			$aArguments = array($sUidRangeIfSupported);
1693		}
1694
1695		return $this->SendRequestWithCheck($sCmd, $aArguments);
1696	}
1697
1698	/**
1699	 * @param string $sIndexRange
1700	 * @param bool $bIndexIsUid
1701	 * @param array $aInputStoreItems
1702	 * @param string $sStoreAction
1703	 *
1704	 * @return \MailSo\Imap\ImapClient
1705	 *
1706	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1707	 * @throws \MailSo\Net\Exceptions\Exception
1708	 * @throws \MailSo\Imap\Exceptions\Exception
1709	 */
1710	public function MessageStoreFlag($sIndexRange, $bIndexIsUid, $aInputStoreItems, $sStoreAction)
1711	{
1712		if (!\MailSo\Base\Validator::NotEmptyString($sIndexRange, true) ||
1713			!\MailSo\Base\Validator::NotEmptyString($sStoreAction, true) ||
1714			0 === \count($aInputStoreItems))
1715		{
1716			return false;
1717		}
1718
1719		$sCmd = ($bIndexIsUid) ? 'UID STORE' : 'STORE';
1720		return $this->SendRequestWithCheck($sCmd, array($sIndexRange, $sStoreAction, $aInputStoreItems));
1721	}
1722
1723	/**
1724	 * @param string $sFolderName
1725	 * @param resource $rMessageAppendStream
1726	 * @param int $iStreamSize
1727	 * @param array	$aAppendFlags = null
1728	 * @param int $iUid = null
1729	 * @param int $sDateTime = 0
1730	 *
1731	 * @return \MailSo\Imap\ImapClient
1732	 *
1733	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1734	 * @throws \MailSo\Net\Exceptions\Exception
1735	 * @throws \MailSo\Imap\Exceptions\Exception
1736	 */
1737	public function MessageAppendStream($sFolderName, $rMessageAppendStream, $iStreamSize, $aAppendFlags = null, &$iUid = null, $sDateTime = 0)
1738	{
1739		$aData = array($this->EscapeString($sFolderName), $aAppendFlags);
1740		if (0 < $sDateTime)
1741		{
1742			$aData[] = $this->EscapeString(\gmdate('d-M-Y H:i:s', $sDateTime).' +0000');
1743		}
1744
1745		$aData[] = '{'.$iStreamSize.'}';
1746
1747		$this->SendRequest('APPEND', $aData);
1748		$this->parseResponseWithValidation();
1749
1750		$this->writeLog('Write to connection stream', \MailSo\Log\Enumerations\Type::NOTE);
1751
1752		\MailSo\Base\Utils::MultipleStreamWriter($rMessageAppendStream, array($this->rConnect));
1753
1754		$this->sendRaw('');
1755		$this->parseResponseWithValidation();
1756
1757		if (null !== $iUid)
1758		{
1759			$aLastResponse = $this->GetLastResponse();
1760			if (\is_array($aLastResponse) && 0 < \count($aLastResponse) && $aLastResponse[\count($aLastResponse) - 1])
1761			{
1762				$oLast = $aLastResponse[count($aLastResponse) - 1];
1763				if ($oLast && \MailSo\Imap\Enumerations\ResponseType::TAGGED === $oLast->ResponseType && \is_array($oLast->OptionalResponse))
1764				{
1765					if (0 < \strlen($oLast->OptionalResponse[0]) &&
1766						0 < \strlen($oLast->OptionalResponse[2]) &&
1767						'APPENDUID' === strtoupper($oLast->OptionalResponse[0]) &&
1768						\is_numeric($oLast->OptionalResponse[2])
1769					)
1770					{
1771						$iUid = (int) $oLast->OptionalResponse[2];
1772					}
1773				}
1774			}
1775		}
1776
1777		return $this;
1778	}
1779
1780	/**
1781	 * @return \MailSo\Imap\FolderInformation
1782	 */
1783	public function FolderCurrentInformation()
1784	{
1785		return $this->oCurrentFolderInfo;
1786	}
1787
1788	/**
1789	 * @param string $sCommand
1790	 * @param array $aParams = array()
1791	 * @param bool $bBreakOnLiteral = false
1792	 *
1793	 * @return string
1794	 *
1795	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1796	 * @throws \MailSo\Net\Exceptions\Exception
1797	 */
1798	public function SendRequest($sCommand, $aParams = array(), $bBreakOnLiteral = false)
1799	{
1800		if (!\MailSo\Base\Validator::NotEmptyString($sCommand, true) || !\is_array($aParams))
1801		{
1802			$this->writeLogException(
1803				new \MailSo\Base\Exceptions\InvalidArgumentException(),
1804				\MailSo\Log\Enumerations\Type::ERROR, true);
1805		}
1806
1807		$this->IsConnected(true);
1808
1809		$sTag = $this->getNewTag();
1810
1811		$sCommand = \trim($sCommand);
1812		$sRealCommand = $sTag.' '.$sCommand.$this->prepearParamLine($aParams);
1813
1814		$sFakeCommand = '';
1815		$aFakeParams = $this->secureRequestParams($sCommand, $aParams);
1816		if (null !== $aFakeParams)
1817		{
1818			$sFakeCommand = $sTag.' '.$sCommand.$this->prepearParamLine($aFakeParams);
1819		}
1820
1821		$this->aTagTimeouts[$sTag] = \microtime(true);
1822
1823		if ($bBreakOnLiteral && !\preg_match('/\d\+\}\r\n/', $sRealCommand))
1824		{
1825			$iPos = \strpos($sRealCommand, "}\r\n");
1826			if (false !== $iPos)
1827			{
1828				$iFakePos = \strpos($sFakeCommand, "}\r\n");
1829
1830				$this->sendRaw(\substr($sRealCommand, 0, $iPos + 1), true,
1831					false !== $iFakePos ? \substr($sFakeCommand, 0, $iFakePos + 3) : '');
1832
1833				return \substr($sRealCommand, $iPos + 3);
1834			}
1835		}
1836
1837		$this->sendRaw($sRealCommand, true, $sFakeCommand);
1838		return '';
1839	}
1840
1841	/**
1842	 * @param string $sCommand
1843	 * @param array $aParams
1844	 *
1845	 * @return array|null
1846	 */
1847	private function secureRequestParams($sCommand, $aParams)
1848	{
1849		$aResult = null;
1850		switch ($sCommand)
1851		{
1852			case 'LOGIN':
1853				$aResult = $aParams;
1854				if (\is_array($aResult) && 2 === count($aResult))
1855				{
1856					$aResult[1] = '"********"';
1857				}
1858				break;
1859		}
1860
1861		return $aResult;
1862	}
1863
1864	/**
1865	 * @param string $sCommand
1866	 * @param array $aParams = array()
1867	 * @param bool $bFindCapa = false
1868	 *
1869	 * @return \MailSo\Imap\ImapClient
1870	 *
1871	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
1872	 * @throws \MailSo\Net\Exceptions\Exception
1873	 * @throws \MailSo\Imap\Exceptions\Exception
1874	 */
1875	public function SendRequestWithCheck($sCommand, $aParams = array(), $bFindCapa = false)
1876	{
1877		$this->SendRequest($sCommand, $aParams);
1878		$this->parseResponseWithValidation(null, $bFindCapa);
1879
1880		return $this;
1881	}
1882
1883	/**
1884	 * @return array
1885	 */
1886	public function GetLastResponse()
1887	{
1888		return $this->aLastResponse;
1889	}
1890
1891	/**
1892	 * @param mixed $aResult
1893	 *
1894	 * @return array
1895	 *
1896	 * @throws \MailSo\Imap\Exceptions\ResponseNotFoundException
1897	 * @throws \MailSo\Imap\Exceptions\InvalidResponseException
1898	 * @throws \MailSo\Imap\Exceptions\NegativeResponseException
1899	 */
1900	private function validateResponse($aResult)
1901	{
1902		if (!\is_array($aResult) || 0 === $iCnt = \count($aResult))
1903		{
1904			$this->writeLogException(
1905				new Exceptions\ResponseNotFoundException(),
1906				\MailSo\Log\Enumerations\Type::WARNING, true);
1907		}
1908
1909		if ($aResult[$iCnt - 1]->ResponseType !== \MailSo\Imap\Enumerations\ResponseType::CONTINUATION)
1910		{
1911			if (!$aResult[$iCnt - 1]->IsStatusResponse)
1912			{
1913				$this->writeLogException(
1914					new Exceptions\InvalidResponseException($aResult),
1915					\MailSo\Log\Enumerations\Type::WARNING, true);
1916			}
1917
1918			if (\MailSo\Imap\Enumerations\ResponseStatus::OK !== $aResult[$iCnt - 1]->StatusOrIndex)
1919			{
1920				$this->writeLogException(
1921					new Exceptions\NegativeResponseException($aResult),
1922					\MailSo\Log\Enumerations\Type::WARNING, true);
1923			}
1924		}
1925
1926		return $aResult;
1927	}
1928
1929	/**
1930	 * @param string $sEndTag = null
1931	 * @param bool $bFindCapa = false
1932	 *
1933	 * @return array|bool
1934	 */
1935	protected function parseResponse($sEndTag = null, $bFindCapa = false)
1936	{
1937		if (\is_resource($this->rConnect))
1938		{
1939			$oImapResponse = null;
1940			$sEndTag = (null === $sEndTag) ? $this->getCurrentTag() : $sEndTag;
1941
1942			while (true)
1943			{
1944				$oImapResponse = Response::NewInstance();
1945
1946				$this->partialParseResponseBranch($oImapResponse);
1947
1948				if ($oImapResponse)
1949				{
1950					if (\MailSo\Imap\Enumerations\ResponseType::UNKNOWN === $oImapResponse->ResponseType)
1951					{
1952						return false;
1953					}
1954
1955					if ($bFindCapa)
1956					{
1957						$this->initCapabilityImapResponse($oImapResponse);
1958					}
1959
1960					$this->aPartialResponses[] = $oImapResponse;
1961					if ($sEndTag === $oImapResponse->Tag || \MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $oImapResponse->ResponseType)
1962					{
1963						if (isset($this->aTagTimeouts[$sEndTag]))
1964						{
1965							$this->writeLog((\microtime(true) - $this->aTagTimeouts[$sEndTag]).' ('.$sEndTag.')',
1966								\MailSo\Log\Enumerations\Type::TIME);
1967
1968							unset($this->aTagTimeouts[$sEndTag]);
1969						}
1970
1971						break;
1972					}
1973				}
1974				else
1975				{
1976					return false;
1977				}
1978
1979				unset($oImapResponse);
1980			}
1981		}
1982
1983		$this->iResponseBufParsedPos = 0;
1984		$this->aLastResponse = $this->aPartialResponses;
1985		$this->aPartialResponses = array();
1986
1987		return $this->aLastResponse;
1988	}
1989
1990	/**
1991	 * @param string $sEndTag = null
1992	 * @param bool $bFindCapa = false
1993	 *
1994	 * @return array
1995	 */
1996	private function parseResponseWithValidation($sEndTag = null, $bFindCapa = false)
1997	{
1998		return $this->validateResponse($this->parseResponse($sEndTag, $bFindCapa));
1999	}
2000
2001	/**
2002	 * @param \MailSo\Imap\Response $oImapResponse
2003	 *
2004	 * @return void
2005	 */
2006	private function initCapabilityImapResponse($oImapResponse)
2007	{
2008		if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType
2009			&& \is_array($oImapResponse->ResponseList))
2010		{
2011			$aList = null;
2012			if (isset($oImapResponse->ResponseList[1]) && \is_string($oImapResponse->ResponseList[1]) &&
2013				'CAPABILITY' === \strtoupper($oImapResponse->ResponseList[1]))
2014			{
2015				$aList = \array_slice($oImapResponse->ResponseList, 2);
2016			}
2017			else if ($oImapResponse->OptionalResponse && \is_array($oImapResponse->OptionalResponse) &&
2018				1 < \count($oImapResponse->OptionalResponse) && \is_string($oImapResponse->OptionalResponse[0]) &&
2019				'CAPABILITY' === \strtoupper($oImapResponse->OptionalResponse[0]))
2020			{
2021				$aList = \array_slice($oImapResponse->OptionalResponse, 1);
2022			}
2023
2024			if (\is_array($aList) && 0 < \count($aList))
2025			{
2026				$this->aCapabilityItems = \array_map('strtoupper', $aList);
2027			}
2028		}
2029	}
2030
2031
2032
2033	/**
2034	 * @return bool
2035	 */
2036	private function skipBracketParse($oImapResponse)
2037	{
2038		return $oImapResponse &&
2039			$oImapResponse->ResponseType === \MailSo\Imap\Enumerations\ResponseType::UNTAGGED &&
2040			(
2041				($oImapResponse->StatusOrIndex === 'STATUS' && 2 === \count($oImapResponse->ResponseList)) ||
2042				($oImapResponse->StatusOrIndex === 'LIST' && 4 === \count($oImapResponse->ResponseList)) ||
2043				($oImapResponse->StatusOrIndex === 'LSUB' && 4 === \count($oImapResponse->ResponseList))
2044			);
2045	}
2046
2047	/**
2048	 * @return array|string
2049	 *
2050	 * @throws \MailSo\Net\Exceptions\Exception
2051	 */
2052	private function partialParseResponseBranch(&$oImapResponse,
2053		$bTreatAsAtom = false, $sParentToken = '', $sOpenBracket = '')
2054	{
2055		$mNull = null;
2056
2057		$iPos = $this->iResponseBufParsedPos;
2058		$sClosingBracket = '';
2059
2060		$sPreviousAtomUpperCase = null;
2061		$bIsEndOfList = false;
2062		$iLiteralLen = 0;
2063		$iBufferEndIndex = 0;
2064		$iDebugCount = 0;
2065
2066		$rImapLiteralStream = null;
2067
2068		$bIsGotoDefault = false;
2069		$bIsGotoLiteral = false;
2070		$bIsGotoLiteralEnd = false;
2071		$bIsGotoAtomBracket = false;
2072		$bIsGotoNotAtomBracket = false;
2073
2074		$bCountOneInited = false;
2075		$bCountTwoInited = false;
2076
2077		$sAtomBuilder = $bTreatAsAtom ? '' : null;
2078		$aList = array();
2079		if (null !== $oImapResponse)
2080		{
2081			$aList =& $oImapResponse->ResponseList;
2082		}
2083
2084		while (!$bIsEndOfList)
2085		{
2086			$iDebugCount++;
2087			if (100000 === $iDebugCount)
2088			{
2089				$this->Logger()->Write('PartialParseOver: '.$iDebugCount, \MailSo\Log\Enumerations\Type::ERROR);
2090			}
2091
2092			if ($this->bNeedNext)
2093			{
2094				$iPos = 0;
2095				$this->getNextBuffer();
2096				$this->iResponseBufParsedPos = $iPos;
2097				$this->bNeedNext = false;
2098			}
2099
2100			$sChar = null;
2101			if ($bIsGotoDefault)
2102			{
2103				$sChar = 'GOTO_DEFAULT';
2104				$bIsGotoDefault = false;
2105			}
2106			else if ($bIsGotoLiteral)
2107			{
2108				$bIsGotoLiteral = false;
2109				$bIsGotoLiteralEnd = true;
2110
2111				if ($this->partialResponseLiteralCallbackCallable(
2112					$sParentToken, null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), $this->rConnect, $iLiteralLen))
2113				{
2114					if (!$bTreatAsAtom)
2115					{
2116						$aList[] = '';
2117					}
2118				}
2119				else
2120				{
2121					$sLiteral = '';
2122					$iRead = $iLiteralLen;
2123
2124					while (0 < $iRead)
2125					{
2126						$sAddRead = \fread($this->rConnect, $iRead);
2127						if (false === $sAddRead)
2128						{
2129							$sLiteral = false;
2130							break;
2131						}
2132
2133						$sLiteral .= $sAddRead;
2134						$iRead -= \strlen($sAddRead);
2135
2136						\MailSo\Base\Utils::ResetTimeLimit();
2137					}
2138
2139					if (false !== $sLiteral)
2140					{
2141						$iLiteralSize = \strlen($sLiteral);
2142						\MailSo\Base\Loader::IncStatistic('NetRead', $iLiteralSize);
2143						if ($iLiteralLen !== $iLiteralSize)
2144						{
2145							$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.
2146								$iLiteralLen.'" bytes', \MailSo\Log\Enumerations\Type::WARNING);
2147						}
2148
2149						if (!$bTreatAsAtom)
2150						{
2151							$aList[] = $sLiteral;
2152
2153							if (\MailSo\Config::$LogSimpleLiterals)
2154							{
2155								$this->writeLog('{'.\strlen($sLiteral).'} '.$sLiteral, \MailSo\Log\Enumerations\Type::INFO);
2156							}
2157						}
2158					}
2159					else
2160					{
2161						$this->writeLog('Can\'t read imap stream', \MailSo\Log\Enumerations\Type::NOTE);
2162					}
2163
2164					unset($sLiteral);
2165				}
2166
2167				continue;
2168			}
2169			else if ($bIsGotoLiteralEnd)
2170			{
2171				$rImapLiteralStream = null;
2172				$sPreviousAtomUpperCase = null;
2173				$this->bNeedNext = true;
2174				$bIsGotoLiteralEnd = false;
2175
2176				continue;
2177			}
2178			else if ($bIsGotoAtomBracket)
2179			{
2180				if ($bTreatAsAtom)
2181				{
2182					$sAtomBlock = $this->partialParseResponseBranch($mNull, true,
2183						null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), $sOpenBracket);
2184
2185					$sAtomBuilder .= $sAtomBlock;
2186					$iPos = $this->iResponseBufParsedPos;
2187					$sAtomBuilder .= $sClosingBracket;
2188				}
2189
2190				$sPreviousAtomUpperCase = null;
2191				$bIsGotoAtomBracket = false;
2192
2193				continue;
2194			}
2195			else if ($bIsGotoNotAtomBracket)
2196			{
2197				$aSubItems = $this->partialParseResponseBranch($mNull, false,
2198					null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), $sOpenBracket);
2199
2200				$aList[] = $aSubItems;
2201				$iPos = $this->iResponseBufParsedPos;
2202				$sPreviousAtomUpperCase = null;
2203				if (null !== $oImapResponse && $oImapResponse->IsStatusResponse)
2204				{
2205					$oImapResponse->OptionalResponse = $aSubItems;
2206
2207					$bIsGotoDefault = true;
2208					$bIsGotoNotAtomBracket = false;
2209					continue;
2210				}
2211				$bIsGotoNotAtomBracket = false;
2212
2213				continue;
2214			}
2215			else
2216			{
2217				$iBufferEndIndex = \strlen($this->sResponseBuffer) - 3;
2218				$this->bResponseBufferChanged = false;
2219
2220				if ($iPos > $iBufferEndIndex)
2221				{
2222					break;
2223				}
2224
2225				$sChar = $this->sResponseBuffer[$iPos];
2226			}
2227
2228			switch (true)
2229			{
2230				case ']' === $sChar:
2231				case ')' === $sChar:
2232					if ($this->skipBracketParse($oImapResponse))
2233					{
2234						$bIsGotoDefault = true;
2235						$bIsGotoNotAtomBracket = false;
2236					}
2237					else
2238					{
2239						$iPos++;
2240						$sPreviousAtomUpperCase = null;
2241						$bIsEndOfList = true;
2242					}
2243					break;
2244				case ' ' === $sChar:
2245					if ($bTreatAsAtom)
2246					{
2247						$sAtomBuilder .= ' ';
2248					}
2249					$iPos++;
2250					break;
2251				case '[' === $sChar:
2252				case '(' === $sChar:
2253					$sClosingBracket = '[' === $sChar ? ']' : ')';
2254					$sOpenBracket = $sChar;
2255
2256					if ($bTreatAsAtom)
2257					{
2258						$sAtomBuilder .= $sChar;
2259						$bIsGotoAtomBracket = true;
2260						$this->iResponseBufParsedPos = ++$iPos;
2261					}
2262					else if ($this->skipBracketParse($oImapResponse))
2263					{
2264						$sOpenBracket = '';
2265						$sClosingBracket = '';
2266						$bIsGotoDefault = true;
2267						$bIsGotoNotAtomBracket = false;
2268					}
2269					else
2270					{
2271						$bIsGotoNotAtomBracket = true;
2272						$this->iResponseBufParsedPos = ++$iPos;
2273					}
2274					break;
2275				case '{' === $sChar:
2276					$bIsLiteralParsed = false;
2277					$mLiteralEndPos = \strpos($this->sResponseBuffer, '}', $iPos);
2278					if (false !== $mLiteralEndPos && $mLiteralEndPos > $iPos)
2279					{
2280						$sLiteralLenAsString = \substr($this->sResponseBuffer, $iPos + 1, $mLiteralEndPos - $iPos - 1);
2281						if (\is_numeric($sLiteralLenAsString))
2282						{
2283							$iLiteralLen = (int) $sLiteralLenAsString;
2284							$bIsLiteralParsed = true;
2285							$iPos = $mLiteralEndPos + 3;
2286							$bIsGotoLiteral = true;
2287							break;
2288						}
2289					}
2290					if (!$bIsLiteralParsed)
2291					{
2292						$iPos = $iBufferEndIndex;
2293					}
2294					$sPreviousAtomUpperCase = null;
2295					break;
2296				case '"' === $sChar:
2297					$bIsQuotedParsed = false;
2298					while (true)
2299					{
2300						$iClosingPos = $iPos + 1;
2301						if ($iClosingPos > $iBufferEndIndex)
2302						{
2303							break;
2304						}
2305
2306						while (true)
2307						{
2308							$iClosingPos = \strpos($this->sResponseBuffer, '"', $iClosingPos);
2309							if (false === $iClosingPos)
2310							{
2311								break;
2312							}
2313
2314							// TODO
2315							$iClosingPosNext = $iClosingPos + 1;
2316							if (
2317								isset($this->sResponseBuffer[$iClosingPosNext]) &&
2318								' ' !== $this->sResponseBuffer[$iClosingPosNext] &&
2319								"\r" !== $this->sResponseBuffer[$iClosingPosNext] &&
2320								"\n" !== $this->sResponseBuffer[$iClosingPosNext] &&
2321								']' !== $this->sResponseBuffer[$iClosingPosNext] &&
2322								')' !== $this->sResponseBuffer[$iClosingPosNext]
2323								)
2324							{
2325								$iClosingPos++;
2326								continue;
2327							}
2328
2329							$iSlashCount = 0;
2330							while ('\\' === $this->sResponseBuffer[$iClosingPos - $iSlashCount - 1])
2331							{
2332								$iSlashCount++;
2333							}
2334
2335							if ($iSlashCount % 2 == 1)
2336							{
2337								$iClosingPos++;
2338								continue;
2339							}
2340							else
2341							{
2342								break;
2343							}
2344						}
2345
2346						if (false === $iClosingPos)
2347						{
2348							break;
2349						}
2350						else
2351						{
2352//							$iSkipClosingPos = 0;
2353							$bIsQuotedParsed = true;
2354							if ($bTreatAsAtom)
2355							{
2356								$sAtomBuilder .= \strtr(
2357									\substr($this->sResponseBuffer, $iPos, $iClosingPos - $iPos + 1),
2358									array('\\\\' => '\\', '\\"' => '"')
2359								);
2360							}
2361							else
2362							{
2363								$aList[] = \strtr(
2364									\substr($this->sResponseBuffer, $iPos + 1, $iClosingPos - $iPos - 1),
2365									array('\\\\' => '\\', '\\"' => '"')
2366								);
2367							}
2368
2369							$iPos = $iClosingPos + 1;
2370							break;
2371						}
2372					}
2373
2374					if (!$bIsQuotedParsed)
2375					{
2376						$iPos = $iBufferEndIndex;
2377					}
2378
2379					$sPreviousAtomUpperCase = null;
2380					break;
2381
2382				case 'GOTO_DEFAULT' === $sChar:
2383				default:
2384					$iCharBlockStartPos = $iPos;
2385
2386					if (null !== $oImapResponse && $oImapResponse->IsStatusResponse)
2387					{
2388						$iPos = $iBufferEndIndex;
2389
2390						while ($iPos > $iCharBlockStartPos && $this->sResponseBuffer[$iCharBlockStartPos] === ' ')
2391						{
2392							$iCharBlockStartPos++;
2393						}
2394					}
2395
2396					$bIsAtomDone = false;
2397					while (!$bIsAtomDone && ($iPos <= $iBufferEndIndex))
2398					{
2399						$sCharDef = $this->sResponseBuffer[$iPos];
2400						switch (true)
2401						{
2402							case ('[' === $sCharDef || ']' === $sCharDef || '(' === $sCharDef || ')' === $sCharDef) &&
2403								$this->skipBracketParse($oImapResponse):
2404								$iPos++;
2405								break;
2406							case '[' === $sCharDef:
2407								if (null === $sAtomBuilder)
2408								{
2409									$sAtomBuilder = '';
2410								}
2411
2412								$sAtomBuilder .= \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos + 1);
2413
2414								$iPos++;
2415								$this->iResponseBufParsedPos = $iPos;
2416
2417								$sListBlock = $this->partialParseResponseBranch($mNull, true,
2418									null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), '[');
2419
2420								if (null !== $sListBlock)
2421								{
2422									$sAtomBuilder .= $sListBlock.']';
2423								}
2424
2425								$this->Logger()->Write('$sAtomBuilder='.$sAtomBuilder);
2426
2427								$iPos = $this->iResponseBufParsedPos;
2428								$iCharBlockStartPos = $iPos;
2429								break;
2430							case ' ' === $sCharDef:
2431							case ')' === $sCharDef && '(' === $sOpenBracket:
2432							case ']' === $sCharDef && '[' === $sOpenBracket:
2433								$bIsAtomDone = true;
2434								break;
2435							default:
2436								$iPos++;
2437								break;
2438						}
2439					}
2440
2441					if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder)
2442					{
2443						$sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
2444						if (null === $sAtomBuilder)
2445						{
2446							$aList[] = $sLastCharBlock;
2447							$sPreviousAtomUpperCase = $sLastCharBlock;
2448						}
2449						else
2450						{
2451							$sAtomBuilder .= $sLastCharBlock;
2452
2453							if (!$bTreatAsAtom)
2454							{
2455								$aList[] = $sAtomBuilder;
2456								$sPreviousAtomUpperCase = $sAtomBuilder;
2457								$sAtomBuilder = null;
2458							}
2459						}
2460
2461						if (null !== $oImapResponse)
2462						{
2463//							if (1 === \count($aList))
2464							if (!$bCountOneInited && 1 === \count($aList))
2465//							if (isset($aList[0]) && !isset($aList[1])) // fast 1 === \count($aList)
2466							{
2467								$bCountOneInited = true;
2468
2469								$oImapResponse->Tag = $aList[0];
2470								if ('+' === $oImapResponse->Tag)
2471								{
2472									$oImapResponse->ResponseType = \MailSo\Imap\Enumerations\ResponseType::CONTINUATION;
2473								}
2474								else if ('*' === $oImapResponse->Tag)
2475								{
2476									$oImapResponse->ResponseType = \MailSo\Imap\Enumerations\ResponseType::UNTAGGED;
2477								}
2478								else if ($this->getCurrentTag() === $oImapResponse->Tag)
2479								{
2480									$oImapResponse->ResponseType = \MailSo\Imap\Enumerations\ResponseType::TAGGED;
2481								}
2482								else
2483								{
2484									$oImapResponse->ResponseType = \MailSo\Imap\Enumerations\ResponseType::UNKNOWN;
2485								}
2486							}
2487//							else if (2 === \count($aList))
2488							else if (!$bCountTwoInited && 2 === \count($aList))
2489//							else if (isset($aList[1]) && !isset($aList[2])) // fast 2 === \count($aList)
2490							{
2491								$bCountTwoInited = true;
2492
2493								$oImapResponse->StatusOrIndex = strtoupper($aList[1]);
2494
2495								if ($oImapResponse->StatusOrIndex == \MailSo\Imap\Enumerations\ResponseStatus::OK ||
2496									$oImapResponse->StatusOrIndex == \MailSo\Imap\Enumerations\ResponseStatus::NO ||
2497									$oImapResponse->StatusOrIndex == \MailSo\Imap\Enumerations\ResponseStatus::BAD ||
2498									$oImapResponse->StatusOrIndex == \MailSo\Imap\Enumerations\ResponseStatus::BYE ||
2499									$oImapResponse->StatusOrIndex == \MailSo\Imap\Enumerations\ResponseStatus::PREAUTH)
2500								{
2501									$oImapResponse->IsStatusResponse = true;
2502								}
2503							}
2504							else if (\MailSo\Imap\Enumerations\ResponseType::CONTINUATION === $oImapResponse->ResponseType)
2505							{
2506								$oImapResponse->HumanReadable = $sLastCharBlock;
2507							}
2508							else if ($oImapResponse->IsStatusResponse)
2509							{
2510								$oImapResponse->HumanReadable = $sLastCharBlock;
2511							}
2512						}
2513					}
2514			}
2515		}
2516
2517		$this->iResponseBufParsedPos = $iPos;
2518		if (null !== $oImapResponse)
2519		{
2520			$this->bNeedNext = true;
2521			$this->iResponseBufParsedPos = 0;
2522		}
2523
2524		if (100000 < $iDebugCount)
2525		{
2526			$this->Logger()->Write('PartialParseOverResult: '.$iDebugCount, \MailSo\Log\Enumerations\Type::ERROR);
2527		}
2528
2529		return $bTreatAsAtom ? $sAtomBuilder : $aList;
2530	}
2531
2532	/**
2533	 * @param string $sParent
2534	 * @param string $sLiteralAtomUpperCase
2535	 * @param resource $rImapStream
2536	 * @param int $iLiteralLen
2537	 *
2538	 * @return bool
2539	 */
2540	private function partialResponseLiteralCallbackCallable($sParent, $sLiteralAtomUpperCase, $rImapStream, $iLiteralLen)
2541	{
2542		$sLiteralAtomUpperCasePeek = '';
2543		if (0 === \strpos($sLiteralAtomUpperCase, 'BODY'))
2544		{
2545			$sLiteralAtomUpperCasePeek = \str_replace('BODY', 'BODY.PEEK', $sLiteralAtomUpperCase);
2546		}
2547
2548		$sFetchKey = '';
2549		if (\is_array($this->aFetchCallbacks))
2550		{
2551			if (0 < \strlen($sLiteralAtomUpperCasePeek) && isset($this->aFetchCallbacks[$sLiteralAtomUpperCasePeek]))
2552			{
2553				$sFetchKey = $sLiteralAtomUpperCasePeek;
2554			}
2555			else if (0 < \strlen($sLiteralAtomUpperCase) && isset($this->aFetchCallbacks[$sLiteralAtomUpperCase]))
2556			{
2557				$sFetchKey = $sLiteralAtomUpperCase;
2558			}
2559		}
2560
2561		$bResult = false;
2562		if (0 < \strlen($sFetchKey) && '' !== $this->aFetchCallbacks[$sFetchKey] &&
2563			\is_callable($this->aFetchCallbacks[$sFetchKey]))
2564		{
2565			$rImapLiteralStream =
2566				\MailSo\Base\StreamWrappers\Literal::CreateStream($rImapStream, $iLiteralLen);
2567
2568			$bResult = true;
2569			$this->writeLog('Start Callback for '.$sParent.' / '.$sLiteralAtomUpperCase.
2570				' - try to read '.$iLiteralLen.' bytes.', \MailSo\Log\Enumerations\Type::NOTE);
2571
2572			$this->bRunningCallback = true;
2573
2574			try
2575			{
2576				\call_user_func($this->aFetchCallbacks[$sFetchKey],
2577					$sParent, $sLiteralAtomUpperCase, $rImapLiteralStream);
2578			}
2579			catch (\Exception $oException)
2580			{
2581				$this->writeLog('Callback Exception', \MailSo\Log\Enumerations\Type::NOTICE);
2582				$this->writeLogException($oException);
2583			}
2584
2585			if (\is_resource($rImapLiteralStream))
2586			{
2587				$iNotReadLiteralLen = 0;
2588
2589				$bFeof = \feof($rImapLiteralStream);
2590				$this->writeLog('End Callback for '.$sParent.' / '.$sLiteralAtomUpperCase.
2591					' - feof = '.($bFeof ? 'good' : 'BAD'), $bFeof ?
2592						\MailSo\Log\Enumerations\Type::NOTE : \MailSo\Log\Enumerations\Type::WARNING);
2593
2594				if (!$bFeof)
2595				{
2596					while (!@\feof($rImapLiteralStream))
2597					{
2598						$sBuf = @\fread($rImapLiteralStream, 1024 * 1024);
2599						if (false === $sBuf || 0 === \strlen($sBuf) ||  null === $sBuf)
2600						{
2601							break;
2602						}
2603
2604						\MailSo\Base\Utils::ResetTimeLimit();
2605						$iNotReadLiteralLen += \strlen($sBuf);
2606					}
2607
2608					if (\is_resource($rImapLiteralStream) && !@\feof($rImapLiteralStream))
2609					{
2610						@\stream_get_contents($rImapLiteralStream);
2611					}
2612				}
2613
2614				if (\is_resource($rImapLiteralStream))
2615				{
2616					@\fclose($rImapLiteralStream);
2617				}
2618
2619				if ($iNotReadLiteralLen > 0)
2620				{
2621					$this->writeLog('Not read literal size is '.$iNotReadLiteralLen.' bytes.',
2622						\MailSo\Log\Enumerations\Type::WARNING);
2623				}
2624			}
2625			else
2626			{
2627				$this->writeLog('Literal stream is not resource after callback.',
2628					\MailSo\Log\Enumerations\Type::WARNING);
2629			}
2630
2631			\MailSo\Base\Loader::IncStatistic('NetRead', $iLiteralLen);
2632
2633			$this->bRunningCallback = false;
2634		}
2635
2636		return $bResult;
2637	}
2638
2639	/**
2640	 * @param array $aParams = null
2641	 *
2642	 * @return string
2643	 */
2644	private function prepearParamLine($aParams = array())
2645	{
2646		$sReturn = '';
2647		if (\is_array($aParams) && 0 < \count($aParams))
2648		{
2649			foreach ($aParams as $mParamItem)
2650			{
2651				if (\is_array($mParamItem) && 0 < \count($mParamItem))
2652				{
2653					$sReturn .= ' ('.\trim($this->prepearParamLine($mParamItem)).')';
2654				}
2655				else if (\is_string($mParamItem))
2656				{
2657					$sReturn .= ' '.$mParamItem;
2658				}
2659			}
2660		}
2661		return $sReturn;
2662	}
2663
2664	/**
2665	 * @return string
2666	 */
2667	private function getNewTag()
2668	{
2669		$this->iTagCount++;
2670		return $this->getCurrentTag();
2671	}
2672
2673	/**
2674	 * @return string
2675	 */
2676	private function getCurrentTag()
2677	{
2678		return self::TAG_PREFIX.$this->iTagCount;
2679	}
2680
2681	/**
2682	 * @param string $sStringForEscape
2683	 *
2684	 * @return string
2685	 */
2686	public function EscapeString($sStringForEscape)
2687	{
2688		return '"'.\str_replace(array('\\', '"'), array('\\\\', '\\"'), $sStringForEscape).'"';
2689	}
2690
2691	/**
2692	 * @return string
2693	 */
2694	protected function getLogName()
2695	{
2696		return 'IMAP';
2697	}
2698
2699	/**
2700	 * @param \MailSo\Log\Logger $oLogger
2701	 *
2702	 * @return \MailSo\Imap\ImapClient
2703	 *
2704	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
2705	 */
2706	public function SetLogger($oLogger)
2707	{
2708		parent::SetLogger($oLogger);
2709
2710		return $this;
2711	}
2712
2713	/**
2714	 * @param resource $rConnect
2715	 * @param array $aCapabilityItems = array()
2716	 *
2717	 * @return \MailSo\Imap\ImapClient
2718	 */
2719	public function TestSetValues($rConnect, $aCapabilityItems = array())
2720	{
2721		$this->rConnect = $rConnect;
2722		$this->aCapabilityItems = $aCapabilityItems;
2723
2724		return $this;
2725	}
2726
2727	/**
2728	 * @param string $sEndTag = null
2729	 * @param string $bFindCapa = false
2730	 *
2731	 * @return array
2732	 */
2733	public function TestParseResponseWithValidationProxy($sEndTag = null, $bFindCapa = false)
2734	{
2735		return $this->parseResponseWithValidation($sEndTag, $bFindCapa);
2736	}
2737}
2738