Improved IMAP sort/search/thread command handling

This commit is contained in:
djmaze 2021-11-03 11:24:26 +01:00
parent 93ced6f5f0
commit 0055eb6016
7 changed files with 285 additions and 166 deletions

View file

@ -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.

View file

@ -0,0 +1,82 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2021 DJMaze
*
* 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/rfc5464
*/
namespace MailSo\Imap\Commands;
/**
* @category MailSo
* @package Imap
*/
trait Metadata
{
public 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();
}
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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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)) {

View file

@ -0,0 +1,70 @@
<?php
/*
* 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.
*
* https://datatracker.ietf.org/doc/html/rfc5256
*/
namespace MailSo\Imap\Requests;
/**
* @category MailSo
* @package Imap
*/
class THREAD extends Request
{
public
$sAlgorithm = '', // ORDEREDSUBJECT or REFERENCES or REFS
$sCriterias = 'ALL',
$sCharset = '',
$bUid = true;
function __construct(\MailSo\Imap\ImapClient $oImapClient)
{
if ($oImapClient->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
);
}
}

View file

@ -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))
{