snappymail/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php
2022-05-16 19:57:35 +02:00

443 lines
14 KiB
PHP

<?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.
*/
namespace MailSo\Imap\Commands;
use MailSo\Imap\FetchResponse;
use MailSo\Imap\Enumerations\FetchType;
use MailSo\Imap\ResponseCollection;
use MailSo\Imap\SequenceSet;
use MailSo\Imap\Enumerations\ResponseType;
use MailSo\Log\Enumerations\Type as LogType;
use MailSo\Base\Exceptions\InvalidArgumentException;
/**
* @category MailSo
* @package Imap
*/
trait Messages
{
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function Fetch(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : array
{
if (!\strlen(\trim($sIndexRange)))
{
$this->writeLogException(new InvalidArgumentException, LogType::ERROR, true);
}
$aReturn = array();
$this->aFetchCallbacks = array();
try {
$aFetchItems = array(
FetchType::UID,
FetchType::RFC822_SIZE
);
foreach ($aInputFetchItems as $mFetchKey)
{
switch ($mFetchKey)
{
case FetchType::UID:
case FetchType::RFC822_SIZE:
// Already defined by default
break;
// Macro's
case FetchType::FULL:
$aFetchItems[] = FetchType::BODY;
// Falls through
case FetchType::ALL:
$aFetchItems[] = FetchType::ENVELOPE;
// Falls through
case FetchType::FAST:
$aFetchItems[] = FetchType::FLAGS;
$aFetchItems[] = FetchType::INTERNALDATE;
break;
default:
if (\is_string($mFetchKey)) {
$aFetchItems[] = $mFetchKey;
} else if (\is_array($mFetchKey) && 2 === \count($mFetchKey)
&& \is_string($mFetchKey[0]) && \is_callable($mFetchKey[1]))
{
$aFetchItems[] = $mFetchKey[0];
$this->aFetchCallbacks[$mFetchKey[0]] = $mFetchKey[1];
}
break;
}
}
$aParams = array($sIndexRange, $aFetchItems);
/**
* TODO:
* https://datatracker.ietf.org/doc/html/rfc4551#section-3.3.1
* $aParams[1][] = FLAGS
* $aParams[] = (CHANGEDSINCE $modsequence)
* https://datatracker.ietf.org/doc/html/rfc4551#section-3.3.2
* $aParams[1][] = MODSEQ
* https://datatracker.ietf.org/doc/html/rfc5162#section-3.2
* $bIndexIsUid && $aParams[] = (CHANGEDSINCE $modsequence VANISHED)
*/
$this->SendRequest($bIndexIsUid ? 'UID FETCH' : 'FETCH', $aParams);
foreach ($this->yieldUntaggedResponses() as $oResponse) {
if (FetchResponse::isValidImapResponse($oResponse)) {
if (FetchResponse::hasUidAndSize($oResponse)) {
$aReturn[] = new FetchResponse($oResponse);
} else if ($this->oLogger) {
$this->oLogger->Write('Skipped Imap Response! ['.$oResponse.']', LogType::NOTICE);
}
}
}
} finally {
$this->aFetchCallbacks = array();
}
return $aReturn;
}
/**
* Appends message to specified folder
*
* @param resource $rMessageAppendStream
*
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageAppendStream(string $sFolderName, $rMessageAppendStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
{
$aParams = array(
$this->EscapeFolderName($sFolderName),
$aFlagsList
);
if (0 < $iDateTime) {
$aParams[] = $this->EscapeString(\gmdate('d-M-Y H:i:s', $iDateTime).' +0000');
}
/*
// RFC 3516 || RFC 6855 section-4
if ($this->IsSupported('BINARY') || $this->IsSupported('UTF8=ACCEPT')) {
$aParams[] = '~{'.$iStreamSize.'}';
}
*/
$aParams[] = '{'.$iStreamSize.'}';
$this->SendRequestGetResponse('APPEND', $aParams);
return $this->writeMessageStream($rMessageAppendStream);
}
private function writeMessageStream($rMessageStream) : ?int
{
$this->writeLog('Write to connection stream', LogType::NOTE);
\MailSo\Base\Utils::MultipleStreamWriter($rMessageStream, array($this->ConnectionResource()));
$this->sendRaw('');
$oResponses = $this->getResponse();
/**
* Can be tagged
S: A003 OK [APPENDUID 1 2001] APPEND completed
* Or untagged
S: * OK [APPENDUID 1 2001] Replacement Message ready
*/
foreach ($oResponses as $oResponse) {
if (\is_array($oResponse->OptionalResponse)
&& !empty($oResponse->OptionalResponse[2])
&& \is_numeric($oResponse->OptionalResponse[2])
&& 'APPENDUID' === \strtoupper($oResponse->OptionalResponse[0])
) {
return (int) $oResponse->OptionalResponse[2];
}
}
return null;
}
/**
* RFC 3502 MULTIAPPEND
public function MessageAppendStreams(string $sFolderName, $rMessageAppendStream, int $iStreamSize, array $aFlagsList = null, int &$iUid = null, int $iDateTime = 0) : ?int
*/
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageCopy(string $sToFolder, SequenceSet $oRange) : ResponseCollection
{
if (!\count($oRange)) {
$this->writeLogException(new InvalidArgumentException, LogType::ERROR, true);
}
return $this->SendRequestGetResponse(
$oRange->UID ? 'UID COPY' : 'COPY',
array((string) $oRange, $this->EscapeFolderName($sToFolder))
);
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageMove(string $sToFolder, SequenceSet $oRange) : ResponseCollection
{
if (!\count($oRange)) {
$this->writeLogException(new InvalidArgumentException, LogType::ERROR, true);
}
if (!$this->IsSupported('MOVE')) {
$this->writeLogException(
new \MailSo\IMAP\Exceptions\RuntimeException('Move is not supported'),
LogType::ERROR, true);
}
return $this->SendRequestGetResponse(
$oRange->UID ? 'UID MOVE' : 'MOVE',
array((string) $oRange, $this->EscapeFolderName($sToFolder))
);
}
/**
* RFC 8508 REPLACE
* Replaces message in specified folder
* When $iUid < 1 it only appends the message
*
* @param resource $rMessageStream
*
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageReplaceStream(string $sFolderName, int $iUid, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
{
if (1 > $iUid || !$this->IsSupported('REPLACE')) {
$this->FolderSelect($sFolderName);
$iNewUid = $this->MessageAppendStream($sFolderName, $rMessageStream, $iStreamSize, $aFlagsList, $iDateTime);
if ($iUid) {
$oRange = new SequenceSet([$iUid]);
$this->MessageStoreFlag($oRange,
array(\MailSo\Imap\Enumerations\MessageFlag::DELETED),
\MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT
);
$this->FolderExpunge($oRange);
}
return $iNewUid;
}
$aParams = array(
$iUid,
$this->EscapeFolderName($sFolderName),
$aFlagsList
);
if (0 < $iDateTime) {
$aParams[] = $this->EscapeString(\gmdate('d-M-Y H:i:s', $iDateTime).' +0000');
}
/*
// RFC 3516 || RFC 6855 section-4
if ($this->IsSupported('BINARY') || $this->IsSupported('UTF8=ACCEPT')) {
$aParams[] = '~{'.$iStreamSize.'}';
}
*/
$aParams[] = '{'.$iStreamSize.'}';
$this->SendRequestGetResponse('UID REPLACE', $aParams);
return $this->writeMessageStream($rMessageStream);
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageStoreFlag(SequenceSet $oRange, array $aInputStoreItems, string $sStoreAction) : ?ResponseCollection
{
if (!\count($oRange) || !\strlen(\trim($sStoreAction)) || !\count($aInputStoreItems)) {
return null;
}
/**
* TODO:
* https://datatracker.ietf.org/doc/html/rfc4551#section-3.2
* $sStoreAction[] = (UNCHANGEDSINCE $modsequence)
*/
return $this->SendRequestGetResponse(
$oRange->UID ? 'UID STORE' : 'STORE',
array((string) $oRange, $sStoreAction, $aInputStoreItems)
);
}
/**
* See https://tools.ietf.org/html/rfc5256
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageSimpleSort(array $aSortTypes, string $sSearchCriterias = 'ALL', bool $bReturnUid = true) : array
{
$oSort = new \MailSo\Imap\Requests\SORT($this);
$oSort->sCriterias = $sSearchCriterias;
$oSort->bUid = $bReturnUid;
$oSort->aSortTypes = $aSortTypes;
$oSort->SendRequest();
$aReturn = array();
foreach ($this->yieldUntaggedResponses() as $oResponse) {
$iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'SORT' === $oResponse->ResponseList[2]) ? 1 : 0;
if (\is_array($oResponse->ResponseList)
&& 2 < \count($oResponse->ResponseList)
&& ('SORT' === $oResponse->StatusOrIndex || $iOffset))
{
$iLen = \count($oResponse->ResponseList);
for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) {
$aReturn[] = (int) $oResponse->ResponseList[$iIndex];
}
}
}
return $aReturn;
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageSimpleESearch(string $sSearchCriterias = 'ALL', array $aSearchReturn = null, bool $bReturnUid = true, string $sCharset = '', string $sLimit = '') : array
{
$oESearch = new \MailSo\Imap\Requests\ESEARCH($this);
$oESearch->sCriterias = $sSearchCriterias;
$oESearch->aReturn = $aSearchReturn;
$oESearch->bUid = $bReturnUid;
$oESearch->sLimit = $sLimit;
$oESearch->sCharset = $sCharset;
$oESearch->SendRequest();
return $this->getSimpleESearchOrESortResult($bReturnUid);
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageSimpleESort(array $aSortTypes, string $sSearchCriterias = 'ALL', array $aSearchReturn = ['ALL'], bool $bReturnUid = true, string $sLimit = '') : array
{
$oSort = new \MailSo\Imap\Requests\SORT($this);
$oSort->sCriterias = $sSearchCriterias;
$oSort->bUid = $bReturnUid;
$oSort->aSortTypes = $aSortTypes;
$oSort->aReturn = $aSearchReturn ?: ['ALL'];
$oSort->sLimit = $sLimit;
$oSort->SendRequest();
return $this->getSimpleESearchOrESortResult($bReturnUid);
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageSimpleSearch(string $sSearchCriterias = 'ALL', bool $bReturnUid = true, string $sCharset = '') : array
{
$aRequest = array();
if (\strlen($sCharset)) {
$aRequest[] = 'CHARSET';
$aRequest[] = \strtoupper($sCharset);
}
$aRequest[] = !\strlen($sSearchCriterias) || '*' === $sSearchCriterias ? 'ALL' : $sSearchCriterias;
$sCont = $this->SendRequest($bReturnUid ? 'UID SEARCH' : 'SEARCH', $aRequest, true);
$oResult = $this->getResponse();
if ('' !== $sCont) {
$oItem = $oResult->getLast();
if ($oItem && ResponseType::CONTINUATION === $oItem->ResponseType) {
$aParts = \explode("\r\n", $sCont);
foreach ($aParts as $sLine) {
$this->sendRaw($sLine);
$oResult = $this->getResponse();
$oItem = $oResult->getLast();
if ($oItem && ResponseType::CONTINUATION === $oItem->ResponseType) {
continue;
}
}
}
}
$aReturn = array();
foreach ($oResult as $oResponse) {
$iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'SEARCH' === $oResponse->ResponseList[2]) ? 1 : 0;
if (ResponseType::UNTAGGED === $oResponse->ResponseType
&& ('SEARCH' === $oResponse->StatusOrIndex || $iOffset)
&& \is_array($oResponse->ResponseList)
&& 2 < \count($oResponse->ResponseList))
{
$iLen = \count($oResponse->ResponseList);
for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) {
$aReturn[] = (int) $oResponse->ResponseList[$iIndex];
}
}
}
return \array_reverse($aReturn);
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function MessageSimpleThread(string $sSearchCriterias = 'ALL', bool $bReturnUid = true) : array
{
$oThread = new \MailSo\Imap\Requests\THREAD($this);
$oThread->sCriterias = $sSearchCriterias;
$oThread->bUid = $bReturnUid;
return $oThread->SendRequestGetResponse();
}
private function getSimpleESearchOrESortResult(bool $bReturnUid) : array
{
$sRequestTag = $this->getCurrentTag();
$aResult = array();
foreach ($this->yieldUntaggedResponses() as $oResponse) {
if (\is_array($oResponse->ResponseList)
&& isset($oResponse->ResponseList[2][1])
&& ('ESEARCH' === $oResponse->StatusOrIndex || 'SORT' === $oResponse->StatusOrIndex)
&& 'TAG' === $oResponse->ResponseList[2][0] && $sRequestTag === $oResponse->ResponseList[2][1]
&& (!$bReturnUid || (!empty($oResponse->ResponseList[3]) && 'UID' === $oResponse->ResponseList[3]))
)
{
$i = \count($oResponse->ResponseList) - 1;
while (3 < --$i) {
$sItem = $oResponse->ResponseList[$i];
switch ($sItem)
{
case 'ALL':
case 'MAX':
case 'MIN':
case 'COUNT':
$aResult[$sItem] = $oResponse->ResponseList[$i + 1];
break;
}
}
}
}
return $aResult;
}
}