From 0055eb6016bb23a098a556be3a67823e9ea77f1f Mon Sep 17 00:00:00 2001 From: djmaze Date: Wed, 3 Nov 2021 11:24:26 +0100 Subject: [PATCH] Improved IMAP sort/search/thread command handling --- .../libraries/MailSo/Imap/Commands/ACL.php | 2 +- .../MailSo/Imap/Commands/Metadata.php | 82 +++++++++++ .../app/libraries/MailSo/Imap/ImapClient.php | 139 ++---------------- .../MailSo/Imap/Requests/ESEARCH.php | 66 +++++++-- .../libraries/MailSo/Imap/Requests/SORT.php | 74 +++++++--- .../libraries/MailSo/Imap/Requests/THREAD.php | 70 +++++++++ .../MailSo/Imap/ResponseCollection.php | 18 +-- 7 files changed, 285 insertions(+), 166 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php create mode 100644 snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php index cd74716d3..6e2e92541 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php @@ -3,7 +3,7 @@ /* * This file is part of MailSo. * - * (c) 2014 Usenko Timur + * (c) 2021 DJMaze * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php new file mode 100644 index 000000000..4bf6d7cbd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php @@ -0,0 +1,82 @@ + 0, 'DEPTH' => 0] + ); + if (isset($aOptions['MAXSIZE']) && 0 < \intval($aOptions['MAXSIZE'])) { + $options[] = 'MAXSIZE ' . \intval($aOptions['MAXSIZE']); + } + if (isset($aOptions['DEPTH']) && (1 == $aOptions['DEPTH'] || 'infinity' === $aOptions['DEPTH'])) { + $options[] = "DEPTH {$aOptions['DEPTH']}"; + } + if ($options) { + $arguments[] = '(' . \implode(' ', $options) . ')'; + } + } + + $arguments[] = $this->EscapeString($sFolderName); + + $arguments[] = '(' . \implode(' ', \array_map([$this, 'EscapeString'], $aEntries)) . ')'; + return $this->SendRequestGetResponse('GETMETADATA', $arguments)->getFolderMetadataResult(); + } + + public function ServerGetMetadata(array $aEntries, array $aOptions = []) : array + { + return $this->IsSupported('METADATA-SERVER') + ? $this->getMetadata('', $aEntries, $aOptions) + : []; + } + + public function FolderGetMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array + { + return $this->IsSupported('METADATA') + ? $this->getMetadata($sFolderName, $aEntries, $aOptions) + : []; + } + + public function FolderSetMetadata(string $sFolderName, array $aEntries) : void + { + if ($this->IsSupported('METADATA')) { + if (!$aEntries) { + throw new \MailSo\Base\Exceptions\InvalidArgumentException("Wrong argument for SETMETADATA command"); + } + + $arguments = [$this->EscapeString($sFolderName)]; + + \array_walk($aEntries, function(&$v, $k){ + $v = $this->EscapeString($k) . ' ' . $this->EscapeString($v); + }); + $arguments[] = '(' . \implode(' ', $aEntries) . ')'; + + $this->SendRequestGetResponse('SETMETADATA', $arguments); + } + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php index 26ae8b701..bffdb3cd0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php @@ -778,32 +778,12 @@ class ImapClient extends \MailSo\Net\NetClient */ public function MessageSimpleSort(array $aSortTypes, string $sSearchCriterias = 'ALL', bool $bReturnUid = true) : array { - if (!$aSortTypes) - { - $this->writeLogException( - new \MailSo\Base\Exceptions\InvalidArgumentException, - \MailSo\Log\Enumerations\Type::ERROR, true); - } - if (!$this->IsSupported('SORT')) - { - $this->writeLogException( - new \MailSo\Base\Exceptions\InvalidArgumentException, - \MailSo\Log\Enumerations\Type::ERROR, true); - } - - $sCommandPrefix = ($bReturnUid) ? 'UID ' : ''; - $sSearchCriterias = !\strlen(\trim($sSearchCriterias)) || '*' === $sSearchCriterias - ? 'ALL' : $sSearchCriterias; - - $aRequest = array(); - $aRequest[] = $aSortTypes; - $aRequest[] = \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? 'US-ASCII' : 'UTF-8'; - $aRequest[] = $sSearchCriterias; - - $sCmd = 'SORT'; - - return $this->SendRequestGetResponse($sCommandPrefix.$sCmd, $aRequest) - ->getMessageSimpleSortResult($sCmd, $bReturnUid); + $oSort = new Requests\SORT($this); + $oSort->sCriterias = $sSearchCriterias; + $oSort->bUid = $bReturnUid; + $oSort->aSortTypes = $aSortTypes; + return $oSort->SendRequestGetResponse() + ->getMessageSimpleSortResult($bReturnUid); } /** @@ -832,10 +812,10 @@ class ImapClient extends \MailSo\Net\NetClient { $oSort = new Requests\SORT($this); $oSort->sCriterias = $sSearchCriterias; - $oSort->aReturn = $aSearchReturn ?: ['ALL']; $oSort->bUid = $bReturnUid; - $oSort->sLimit = $sLimit; $oSort->aSortTypes = $aSortTypes; + $oSort->aReturn = $aSearchReturn ?: ['ALL']; + $oSort->sLimit = $sLimit; return $oSort->SendRequestGetResponse() ->getSimpleESearchOrESortResult($this->getCurrentTag(), $bReturnUid); } @@ -883,7 +863,7 @@ class ImapClient extends \MailSo\Net\NetClient } } - return $oResult->getMessageSimpleSearchResult($sCmd, $bReturnUid); + return $oResult->getMessageSimpleSearchResult($bReturnUid); } /** @@ -893,38 +873,12 @@ class ImapClient extends \MailSo\Net\NetClient */ public function MessageSimpleThread(string $sSearchCriterias = 'ALL', bool $bReturnUid = true, string $sCharset = \MailSo\Base\Enumerations\Charset::UTF_8) : array { - $sCommandPrefix = ($bReturnUid) ? 'UID ' : ''; - $sSearchCriterias = !\strlen(\trim($sSearchCriterias)) || '*' === $sSearchCriterias - ? 'ALL' : $sSearchCriterias; - - $sThreadType = ''; - switch (true) - { - case $this->IsSupported('THREAD=REFS'): - $sThreadType = 'REFS'; - break; - case $this->IsSupported('THREAD=REFERENCES'): - $sThreadType = 'REFERENCES'; - break; - case $this->IsSupported('THREAD=ORDEREDSUBJECT'): - $sThreadType = 'ORDEREDSUBJECT'; - break; - default: - $this->writeLogException( - new Exceptions\RuntimeException('Thread is not supported'), - \MailSo\Log\Enumerations\Type::ERROR, true); - break; - } - - $aRequest = array(); - $aRequest[] = $sThreadType; - $aRequest[] = \strtoupper($sCharset); - $aRequest[] = $sSearchCriterias; - - $sCmd = 'THREAD'; - - return $this->SendRequestGetResponse($sCommandPrefix.$sCmd, $aRequest) - ->getMessageSimpleThreadResult($sCmd, $bReturnUid); + $oThread = new Requests\THREAD($this); + $oThread->sCriterias = $sSearchCriterias; + $oThread->sCharset = $sCharset; + $oThread->bUid = $bReturnUid; + return $oThread->SendRequestGetResponse() + ->getMessageSimpleThreadResult($bReturnUid); } /** @@ -1248,37 +1202,6 @@ class ImapClient extends \MailSo\Net\NetClient return 'IMAP'; } - /** - * RFC 5464 - */ - - private function getMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array - { - $arguments = []; - - if ($aOptions) { - $options = []; - $aOptions = \array_intersect_key( - \array_change_key_case($aOptions, CASE_UPPER), - ['MAXSIZE' => 0, 'DEPTH' => 0] - ); - if (isset($aOptions['MAXSIZE']) && 0 < \intval($aOptions['MAXSIZE'])) { - $options[] = 'MAXSIZE ' . \intval($aOptions['MAXSIZE']); - } - if (isset($aOptions['DEPTH']) && (1 == $aOptions['DEPTH'] || 'infinity' === $aOptions['DEPTH'])) { - $options[] = "DEPTH {$aOptions['DEPTH']}"; - } - if ($options) { - $arguments[] = '(' . \implode(' ', $options) . ')'; - } - } - - $arguments[] = $this->EscapeString($sFolderName); - - $arguments[] = '(' . \implode(' ', \array_map([$this, 'EscapeString'], $aEntries)) . ')'; - return $this->SendRequestGetResponse('GETMETADATA', $arguments)->getFolderMetadataResult(); - } - /** * Don't have to be logged in to call this command */ @@ -1299,36 +1222,4 @@ class ImapClient extends \MailSo\Net\NetClient return 'UNKNOWN'; } - public function ServerGetMetadata(array $aEntries, array $aOptions = []) : array - { - return $this->IsSupported('METADATA-SERVER') - ? $this->getMetadata('', $aEntries, $aOptions) - : []; - } - - public function FolderGetMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array - { - return $this->IsSupported('METADATA') - ? $this->getMetadata($sFolderName, $aEntries, $aOptions) - : []; - } - - public function FolderSetMetadata(string $sFolderName, array $aEntries) : void - { - if ($this->IsSupported('METADATA')) { - if (!$aEntries) { - throw new \MailSo\Base\Exceptions\InvalidArgumentException("Wrong argument for SETMETADATA command"); - } - - $arguments = [$this->EscapeString($sFolderName)]; - - \array_walk($aEntries, function(&$v, $k){ - $v = $this->EscapeString($k) . ' ' . $this->EscapeString($v); - }); - $arguments[] = '(' . \implode(' ', $aEntries) . ')'; - - $this->SendRequestGetResponse('SETMETADATA', $arguments); - } - } - } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php index fa4cf65f1..cdb91b629 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php @@ -12,6 +12,9 @@ namespace MailSo\Imap\Requests; +use MailSo\Imap\Exceptions\RuntimeException; +use MailSo\Log\Enumerations\Type; + /** * @category MailSo * @package Imap @@ -20,27 +23,51 @@ class ESEARCH extends Request { public $sCriterias = 'ALL', - $aReturn = [], + $aReturn = [ + /** + ALL + Return all message numbers/UIDs which match the search criteria, + in the requested sort order, using a sequence-set. + + COUNT + As in [ESEARCH]. + + MAX + Return the message number/UID of the highest sorted message + satisfying the search criteria. + + MIN + Return the message number/UID of the lowest sorted message + satisfying the search criteria. + + PARTIAL 1:500 + Return all message numbers/UIDs which match the search criteria, + in the requested sort order, using a sequence-set. + */ + ], $bUid = true, $sLimit = '', $sCharset = '', + // https://datatracker.ietf.org/doc/html/rfc7377 $aMailboxes = [], $aSubtrees = [], $aSubtreesOne = []; + function __construct(\MailSo\Imap\ImapClient $oImapClient) + { + if (!$oImapClient->IsSupported('ESEARCH')) { + $oImapClient->writeLogException( + new RuntimeException('ESEARCH is not supported'), + Type::ERROR, true); + } + parent::__construct($oImapClient); + } + public function SendRequestGetResponse() : \MailSo\Imap\ResponseCollection { - if (!$this->oImapClient->IsSupported('ESEARCH')) { - $this->oImapClient->writeLogException( - new \MailSo\Base\Exceptions\InvalidArgumentException, - \MailSo\Log\Enumerations\Type::ERROR, true); - } - $sCmd = 'SEARCH'; $aRequest = array(); - // TODO: https://github.com/the-djmaze/snappymail/issues/154 - // https://datatracker.ietf.org/doc/html/rfc7377 $aFolders = []; if ($this->aMailboxes) { $aFolders[] = 'mailboxes'; @@ -54,7 +81,12 @@ class ESEARCH extends Request $aFolders[] = 'subtree-one'; $aFolders[] = $this->aSubtreesOne; } - if ($aFolders && $this->oImapClient->IsSupported('MULTISEARCH')) { + if ($aFolders) { + if (!$this->oImapClient->IsSupported('MULTISEARCH')) { + $this->oImapClient->writeLogException( + new RuntimeException('MULTISEARCH is not supported'), + Type::ERROR, true); + } $sCmd = 'ESEARCH'; $aReques[] = 'IN'; $aReques[] = $aFolders; @@ -67,14 +99,22 @@ class ESEARCH extends Request $aRequest[] = 'RETURN'; if ($this->aReturn) { + // RFC 5267 checks + if (!$this->oImapClient->IsSupported('CONTEXT=SEARCH')) { + foreach ($this->aReturn as $sReturn) { + if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) { + $this->oImapClient->writeLogException( + new RuntimeException('CONTEXT=SEARCH is not supported'), + Type::ERROR, true); + } + } + } $aRequest[] = $this->aReturn; } else { - // ALL OR COUNT | MIN | MAX $aRequest[] = array('ALL'); } - $aRequest[] = !\strlen($this->sCriterias) || '*' === $this->sCriterias - ? 'ALL' : $this->sCriterias; + $aRequest[] = (\strlen($this->sCriterias) && '*' !== $this->sCriterias) ? $this->sCriterias : 'ALL'; if (\strlen($this->sLimit)) { $aRequest[] = $this->sLimit; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php index 1e4080bb2..e93f82f41 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php @@ -8,10 +8,16 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. + * + * https://datatracker.ietf.org/doc/html/rfc5256 + * https://datatracker.ietf.org/doc/html/rfc5957 */ namespace MailSo\Imap\Requests; +use MailSo\Imap\Exceptions\RuntimeException; +use MailSo\Log\Enumerations\Type; + /** * @category MailSo * @package Imap @@ -20,44 +26,69 @@ class SORT extends Request { public $sCriterias = 'ALL', + $sCharset = '', $bUid = true, - $sLimit = '', $aSortTypes = [], + $sLimit = '', // rfc5267 $aReturn = [ - /* - MIN - Return the message number/UID of the lowest sorted message - satisfying the search criteria. + /** + ALL + Return all message numbers/UIDs which match the search criteria, + in the requested sort order, using a sequence-set. - MAX - Return the message number/UID of the highest sorted message - satisfying the search criteria. + COUNT + As in [ESEARCH]. - ALL - Return all message numbers/UIDs which match the search criteria, - in the requested sort order, using a sequence-set. + MAX + Return the message number/UID of the highest sorted message + satisfying the search criteria. - COUNT - As in [ESEARCH]. - */ + MIN + Return the message number/UID of the lowest sorted message + satisfying the search criteria. + + PARTIAL 1:500 + Return all message numbers/UIDs which match the search criteria, + in the requested sort order, using a sequence-set. + */ ]; + function __construct(\MailSo\Imap\ImapClient $oImapClient) + { + if (!$oImapClient->IsSupported('SORT')) { + $oImapClient->writeLogException( + new RuntimeException('SORT is not supported'), + Type::ERROR, true); + } + parent::__construct($oImapClient); + } + public function SendRequestGetResponse() : \MailSo\Imap\ResponseCollection { if (!$this->aSortTypes) { $this->oImapClient->writeLogException( - new \MailSo\Base\Exceptions\InvalidArgumentException, - \MailSo\Log\Enumerations\Type::ERROR, true); + new RuntimeException('SortTypes are missing'), + Type::ERROR, true); } $aRequest = array(); if ($this->aReturn) { + // RFC 5267 checks if (!$this->oImapClient->IsSupported('ESORT')) { $this->oImapClient->writeLogException( - new \MailSo\Base\Exceptions\InvalidArgumentException, - \MailSo\Log\Enumerations\Type::ERROR, true); + new RuntimeException('ESORT is not supported'), + Type::ERROR, true); + } + if (!$this->oImapClient->IsSupported('CONTEXT=SORT')) { + foreach ($this->aReturn as $sReturn) { + if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) { + $this->oImapClient->writeLogException( + new RuntimeException('CONTEXT=SORT is not supported'), + Type::ERROR, true); + } + } } $aRequest[] = 'RETURN'; $aRequest[] = $this->aReturn; @@ -66,7 +97,12 @@ class SORT extends Request $aRequest[] = $this->aSortTypes; $sSearchCriterias = (\strlen($this->sCriterias) && '*' !== $this->sCriterias) ? $this->sCriterias : 'ALL'; - $aRequest[] = \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? 'US-ASCII' : 'UTF-8'; + + if (!$this->sCharset) { + $this->sCharset = \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? 'US-ASCII' : 'UTF-8'; + } + + $aRequest[] = \strtoupper($this->sCharset); $aRequest[] = $sSearchCriterias; if (\strlen($this->sLimit)) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php new file mode 100644 index 000000000..0c7e35e30 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php @@ -0,0 +1,70 @@ +IsSupported('THREAD=REFS')) { + $this->sAlgorithm = 'REFS'; + } else if ($oImapClient->IsSupported('THREAD=REFERENCES')) { + $this->sAlgorithm = 'REFERENCES'; + } else if ($oImapClient->IsSupported('THREAD=ORDEREDSUBJECT')) { + $this->sAlgorithm = 'ORDEREDSUBJECT'; + } else { + $oImapClient->writeLogException( + new RuntimeException('THREAD is not supported'), + Type::ERROR, true); + } + parent::__construct($oImapClient); + } + + public function SendRequestGetResponse() : \MailSo\Imap\ResponseCollection + { + if (!$this->oImapClient->IsSupported(\strtoupper("THREAD={$this->sAlgorithm}"))) { + $this->oImapClient->writeLogException( + new Exceptions\RuntimeException("THREAD={$this->sAlgorithm} is not supported"), + \MailSo\Log\Enumerations\Type::ERROR, true); + } + + $aRequest = array(); + $aRequest[] = $this->sAlgorithm; + + $sSearchCriterias = (\strlen($this->sCriterias) && '*' !== $this->sCriterias) ? $this->sCriterias : 'ALL'; + + if (!$this->sCharset) { + $this->sCharset = \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? 'US-ASCII' : 'UTF-8'; + } + + $aRequest[] = \strtoupper($this->sCharset); + $aRequest[] = $sSearchCriterias; + + return $this->oImapClient->SendRequestGetResponse( + ($this->bUid ? 'UID THREAD' : 'THREAD'), + $aRequest + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php index 01d07114d..f99d38809 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php @@ -219,13 +219,13 @@ class ResponseCollection extends \MailSo\Base\Collection return $aReturn; } - public function getMessageSimpleSearchResult(string $sStatus, bool $bReturnUid) : array + public function getMessageSimpleSearchResult(bool $bReturnUid) : array { $aReturn = array(); foreach ($this as $oResponse) { - $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && $sStatus === $oResponse->ResponseList[2]) ? 1 : 0; + $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'SEARCH' === $oResponse->ResponseList[2]) ? 1 : 0; if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType - && ($sStatus === $oResponse->StatusOrIndex || $iOffset) + && ('SEARCH' === $oResponse->StatusOrIndex || $iOffset) && \is_array($oResponse->ResponseList) && 2 < \count($oResponse->ResponseList)) { @@ -238,13 +238,13 @@ class ResponseCollection extends \MailSo\Base\Collection return \array_reverse($aReturn); } - public function getMessageSimpleSortResult(string $sStatus, bool $bReturnUid) : array + public function getMessageSimpleSortResult(bool $bReturnUid) : array { $aReturn = array(); foreach ($this as $oResponse) { - $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && $sStatus === $oResponse->ResponseList[2]) ? 1 : 0; + $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'SORT' === $oResponse->ResponseList[2]) ? 1 : 0; if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType - && ($sStatus === $oResponse->StatusOrIndex || $iOffset) + && ('SORT' === $oResponse->StatusOrIndex || $iOffset) && \is_array($oResponse->ResponseList) && 2 < \count($oResponse->ResponseList)) { @@ -257,13 +257,13 @@ class ResponseCollection extends \MailSo\Base\Collection return $aReturn; } - public function getMessageSimpleThreadResult(string $sStatus, bool $bReturnUid) : array + public function getMessageSimpleThreadResult(bool $bReturnUid) : array { $aReturn = array(); foreach ($this as $oResponse) { - $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && $sStatus === $oResponse->ResponseList[2]) ? 1 : 0; + $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'THREAD' === $oResponse->ResponseList[2]) ? 1 : 0; if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType - && ($sStatus === $oResponse->StatusOrIndex || $iOffset) + && ('THREAD' === $oResponse->StatusOrIndex || $iOffset) && \is_array($oResponse->ResponseList) && 2 < \count($oResponse->ResponseList)) {