File: //usr/local/CyberCP/public/snappymail/snappymail/v/2.38.2/app/libraries/MailSo/Mail/MailClient.php
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Mail;
use MailSo\Imap\FolderCollection;
use MailSo\Imap\FolderInformation;
use MailSo\Imap\Enumerations\FetchType;
use MailSo\Imap\Enumerations\MessageFlag;
use MailSo\Imap\Enumerations\StoreAction;
use MailSo\Imap\SequenceSet;
use MailSo\Mime\Enumerations\Header as MimeHeader;
use MailSo\Mime\Enumerations\Parameter as MimeParameter;
/**
* @category MailSo
* @package Mail
*/
class MailClient
{
use \MailSo\Log\Inherit;
private \MailSo\Imap\ImapClient $oImapClient;
private bool $bThreadSort = false;
function __construct()
{
$this->oImapClient = new \MailSo\Imap\ImapClient;
}
public function ImapClient() : \MailSo\Imap\ImapClient
{
return $this->oImapClient;
}
private function getEnvelopeOrHeadersRequestString() : string
{
if ($this->oImapClient->Settings->message_all_headers) {
return FetchType::BODY_HEADER_PEEK;
}
$aHeaders = array(
// MimeHeader::RETURN_PATH,
// MimeHeader::RECEIVED,
// MimeHeader::MIME_VERSION,
MimeHeader::MESSAGE_ID,
MimeHeader::CONTENT_TYPE,
MimeHeader::FROM_,
MimeHeader::TO_,
MimeHeader::CC,
MimeHeader::BCC,
MimeHeader::SENDER,
MimeHeader::REPLY_TO,
MimeHeader::DELIVERED_TO,
MimeHeader::IN_REPLY_TO,
MimeHeader::REFERENCES,
MimeHeader::DATE,
MimeHeader::SUBJECT,
MimeHeader::X_MSMAIL_PRIORITY,
MimeHeader::IMPORTANCE,
MimeHeader::X_PRIORITY,
MimeHeader::X_DRAFT_INFO,
// MimeHeader::RETURN_RECEIPT_TO,
MimeHeader::DISPOSITION_NOTIFICATION_TO,
MimeHeader::X_CONFIRM_READING_TO,
MimeHeader::AUTHENTICATION_RESULTS,
MimeHeader::X_DKIM_AUTHENTICATION_RESULTS,
MimeHeader::LIST_UNSUBSCRIBE,
// https://autocrypt.org/level1.html#the-autocrypt-header
MimeHeader::AUTOCRYPT
);
// SPAM
$spam_headers = \explode(',', $this->oImapClient->Settings->spam_headers);
if (\in_array('rspamd', $spam_headers)) {
$aHeaders[] = MimeHeader::X_SPAMD_RESULT;
}
if (\in_array('spamassassin', $spam_headers)) {
$aHeaders[] = MimeHeader::X_SPAM_STATUS;
$aHeaders[] = MimeHeader::X_SPAM_FLAG;
$aHeaders[] = MimeHeader::X_SPAM_INFO;
}
if (\in_array('bogofilter', $spam_headers)) {
$aHeaders[] = MimeHeader::X_BOGOSITY;
}
// Virus
$virus_headers = \explode(',', $this->oImapClient->Settings->virus_headers);
if (\in_array('rspamd', $virus_headers)) {
$aHeaders[] = MimeHeader::X_VIRUS;
}
if (\in_array('clamav', $virus_headers)) {
$aHeaders[] = MimeHeader::X_VIRUS_SCANNED;
$aHeaders[] = MimeHeader::X_VIRUS_STATUS;
}
\RainLoop\Api::Actions()->Plugins()->RunHook('imap.message-headers', array(&$aHeaders));
return FetchType::BuildBodyCustomHeaderRequest($aHeaders, true);
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
* @throws \MailSo\Mail\Exceptions\*
*/
public function MessageSetFlag(string $sFolderName, SequenceSet $oRange, string $sMessageFlag, bool $bSetAction = true, bool $bSkipUnsupportedFlag = false) : void
{
if (\count($oRange)) {
if ($this->oImapClient->FolderSelect($sFolderName)->IsFlagSupported($sMessageFlag)) {
$sStoreAction = $bSetAction ? StoreAction::ADD_FLAGS_SILENT : StoreAction::REMOVE_FLAGS_SILENT;
$this->oImapClient->MessageStoreFlag($oRange, array($sMessageFlag), $sStoreAction);
} else if (!$bSkipUnsupportedFlag) {
throw new \MailSo\RuntimeException('Message flag "'.$sMessageFlag.'" is not supported.');
}
}
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null) : ?Message
{
if (1 > $iIndex) {
throw new \ValueError;
}
$this->oImapClient->FolderExamine($sFolderName);
$oBodyStructure = null;
$aFetchItems = array(
FetchType::UID,
// FetchType::FAST,
FetchType::RFC822_SIZE,
FetchType::INTERNALDATE,
FetchType::FLAGS,
$this->getEnvelopeOrHeadersRequestString()
);
$aFetchResponse = $this->oImapClient->Fetch(array(FetchType::BODYSTRUCTURE), $iIndex, $bIndexIsUid);
if (\count($aFetchResponse) && isset($aFetchResponse[0])) {
$oBodyStructure = $aFetchResponse[0]->GetFetchBodyStructure();
if ($oBodyStructure) {
$iBodyTextLimit = $this->oImapClient->Settings->body_text_limit;
foreach ($oBodyStructure->GetHtmlAndPlainParts() as $oPart) {
$sLine = FetchType::BODY_PEEK.'['.$oPart->PartID().']';
if (0 < $iBodyTextLimit && $iBodyTextLimit < $oPart->EstimatedSize()) {
$sLine .= "<0.{$iBodyTextLimit}>";
}
$aFetchItems[] = $sLine;
}
}
}
if (!$oBodyStructure) {
$aFetchItems[] = FetchType::BODYSTRUCTURE;
}
$aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid);
return \count($aFetchResponse)
? Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure)
: null;
}
/**
* Streams mime part to $mCallback
*
* @param mixed $mCallback
*
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function MessageMimeStream($mCallback, string $sFolderName, int $iIndex, string $sMimeIndex) : bool
{
if (!\is_callable($mCallback)) {
throw new \ValueError;
}
$this->oImapClient->FolderExamine($sFolderName);
$sFileName = '';
$sContentType = '';
$sMailEncoding = '';
$sPeek = FetchType::BODY_PEEK;
$sMimeIndex = \trim($sMimeIndex);
$aFetchResponse = $this->oImapClient->Fetch(array(
\strlen($sMimeIndex)
? FetchType::BODY_PEEK.'['.$sMimeIndex.'.MIME]'
: FetchType::BODY_HEADER_PEEK),
$iIndex, true);
if (\count($aFetchResponse)) {
$sMime = $aFetchResponse[0]->GetFetchValue(
\strlen($sMimeIndex)
? FetchType::BODY.'['.$sMimeIndex.'.MIME]'
: FetchType::BODY_HEADER
);
if (\strlen($sMime)) {
$oHeaders = new \MailSo\Mime\HeaderCollection($sMime);
if (\strlen($sMimeIndex)) {
$sFileName = $oHeaders->ParameterValue(MimeHeader::CONTENT_DISPOSITION, MimeParameter::FILENAME);
if (!\strlen($sFileName)) {
$sFileName = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::NAME);
}
$sMailEncoding = \MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName(
$oHeaders->ValueByName(MimeHeader::CONTENT_TRANSFER_ENCODING)
);
// RFC 3516
// Should mailserver decode or PHP?
if ($sMailEncoding && $this->oImapClient->hasCapability('BINARY')) {
$sMailEncoding = '';
$sPeek = FetchType::BINARY_PEEK;
}
$sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE);
} else {
$sFileName = ($oHeaders->ValueByName(MimeHeader::SUBJECT) ?: $iIndex) . '.eml';
$sContentType = 'message/rfc822';
}
}
}
$callback = function ($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream)
use ($mCallback, $sMimeIndex, $sMailEncoding, $sContentType, $sFileName)
{
if (\strlen($sLiteralAtomUpperCase) && \is_resource($rImapLiteralStream) && 'FETCH' === $sParent) {
$mCallback($sMailEncoding
? \MailSo\Base\StreamWrappers\Binary::CreateStream($rImapLiteralStream, $sMailEncoding)
: $rImapLiteralStream,
$sContentType, $sFileName, $sMimeIndex);
}
};
try {
$aFetchResponse = $this->oImapClient->Fetch(array(
// FetchType::BINARY_SIZE.'['.$sMimeIndex.']',
// Push in the aFetchCallbacks array and then called by \MailSo\Imap\Traits\ResponseParser::partialResponseLiteralCallbackCallable
array(
$sPeek.'['.$sMimeIndex.']',
$callback
)), $iIndex, true);
} catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException) {
if (FetchType::BINARY_PEEK === $sPeek && \preg_match('/UNKNOWN-CTE|PARSE/', $oException->getMessage())) {
$this->logException($oException, \LOG_WARNING);
$aFetchResponse = $this->oImapClient->Fetch(array(
array(
FetchType::BODY_PEEK . '[' . $sMimeIndex . ']',
$callback
)), $iIndex, true);
} else {
throw $e;
}
}
return ($aFetchResponse && 1 === \count($aFetchResponse));
}
public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, ?array $aAppendFlags = null) : int
{
if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) {
throw new \ValueError;
}
$iMessageStreamSize = \filesize($sMessageFileName);
$rMessageStream = \fopen($sMessageFileName, 'rb');
$iUid = $this->oImapClient->MessageAppendStream($sFolderToSave, $rMessageStream, $iMessageStreamSize, $aAppendFlags);
\fclose($rMessageStream);
return $iUid;
}
/**
* Returns list of new messages since $iPrevUidNext
* Currently only for INBOX
*/
private function getFolderNextMessageInformation(string $sFolderName, int $iPrevUidNext, int $iCurrentUidNext) : array
{
$aNewMessages = array();
if ($this->oImapClient->Settings->fetch_new_messages && $iPrevUidNext && $iPrevUidNext != $iCurrentUidNext && 'INBOX' === $sFolderName) {
$this->oImapClient->FolderExamine($sFolderName);
$aFetchResponse = $this->oImapClient->Fetch(array(
FetchType::UID,
FetchType::FLAGS,
FetchType::BuildBodyCustomHeaderRequest(array(
MimeHeader::FROM_,
MimeHeader::SUBJECT,
MimeHeader::CONTENT_TYPE
))
), $iPrevUidNext.':*', true);
foreach ($aFetchResponse as $oFetchResponse) {
$aFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS));
if (!\in_array(\strtolower(MessageFlag::SEEN), $aFlags)) {
$iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
$oHeaders = new \MailSo\Mime\HeaderCollection($oFetchResponse->GetHeaderFieldsValue());
$sContentTypeCharset = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::CHARSET);
if ($sContentTypeCharset) {
$oHeaders->SetParentCharset($sContentTypeCharset);
}
$aNewMessages[] = array(
'folder' => $sFolderName,
'uid' => $iUid,
'subject' => $oHeaders->ValueByName(MimeHeader::SUBJECT, !$sContentTypeCharset),
'from' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_)
);
}
}
}
return $aNewMessages;
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, ?SequenceSet $oRange = null) : array
{
if ($oRange) {
// $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize();
$aInfo = $this->oImapClient->FolderStatusAndSelect($sFolderName)->jsonSerialize();
$aInfo['messagesFlags'] = array();
if (\count($oRange)) {
$aFetchResponse = $this->oImapClient->Fetch(array(
FetchType::UID,
FetchType::FLAGS
), (string) $oRange, $oRange->UID);
foreach ($aFetchResponse as $oFetchResponse) {
$iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
$aLowerFlags = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oFetchResponse->GetFetchValue(FetchType::FLAGS)));
$aInfo['messagesFlags'][] = array(
'uid' => $iUid,
'flags' => $aLowerFlags
);
}
}
} else {
$aInfo = $this->oImapClient->FolderStatus($sFolderName)->jsonSerialize();
}
if ($iPrevUidNext) {
$aInfo['newMessages'] = $this->getFolderNextMessageInformation(
$sFolderName,
$iPrevUidNext,
\intval($aInfo['uidNext'])
);
}
// $aInfo['appendLimit'] = $aInfo['appendLimit'] ?: $this->oImapClient->AppendLimit();
return $aInfo;
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderHash(string $sFolderName) : string
{
try
{
// return $this->oImapClient->FolderStatusAndSelect($sFolderName)->etag;
return $this->oImapClient->FolderStatus($sFolderName)->etag;
}
catch (\Throwable $oException)
{
\SnappyMail\Log::warning('IMAP', "FolderHash({$sFolderName}) Exception: {$oException->getMessage()}");
}
return '';
}
public function MessageThread(string $sFolderName, string $sMessageID) : MessageCollection
{
$this->oImapClient->FolderExamine($sFolderName);
$sMessageID = \MailSo\Imap\SearchCriterias::escapeSearchString($this->oImapClient, $sMessageID);
$sSearch = "OR HEADER Message-ID {$sMessageID} HEADER References {$sMessageID}";
$aResult = [];
try
{
foreach ($this->oImapClient->MessageThread($sSearch) as $mItem) {
// Flatten to single level
\array_walk_recursive($mItem, fn($a) => $aResult[] = $a);
}
}
catch (\MailSo\RuntimeException $oException)
{
\SnappyMail\Log::warning('MailClient', 'MessageThread ' . $oException->getMessage());
unset($oException);
}
// $this->logWrite('MessageThreadList: '.\print_r($threads, 1));
return $aResult;
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
protected function ThreadsMap(string $sAlgorithm, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false) : array
{
$oFolderInfo = $oMessageCollection->FolderInfo;
$sFolderName = $oFolderInfo->FullName;
$sSearch = 'ALL';
// $sSearch = 'UNDELETED';
/*
$iThreadLimit = $this->oImapClient->Settings->thread_limit;
if ($iThreadLimit && $iThreadLimit < $oFolderInfo->MESSAGES) {
$sSearch = ($oFolderInfo->MESSAGES - $iThreadLimit) . ':*';
}
*/
/*
$sAlgorithm = '';
if ($this->oImapClient->hasCapability('THREAD=REFS')) {
$sAlgorithm = 'REFS';
} else if ($this->oImapClient->hasCapability('THREAD=REFERENCES')) {
$sAlgorithm = 'REFERENCES';
} else if ($this->oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')) {
$sAlgorithm = 'ORDEREDSUBJECT';
}
*/
$sSerializedHashKey = null;
if ($oCacher && $oCacher->IsInited()) {
$sSerializedHashKey = "ThreadsMap/{$sAlgorithm}/{$sSearch}/{$oFolderInfo->etag}";
// $sSerializedHashKey = "ThreadsMap/{$sAlgorithm}/{$sSearch}/{$iThreadLimit}/{$oFolderInfo->etag}";
$sSerializedUids = $oCacher->Get($sSerializedHashKey);
if (!empty($sSerializedUids)) {
$aSerializedUids = \json_decode($sSerializedUids, true);
if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) {
$oMessageCollection->totalThreads = \count($aSerializedUids['ThreadsUids']);
$this->logWrite('Get Threads from cache ("'.$sFolderName.'" / '.$sSearch.') [count:'.\count($aSerializedUids['ThreadsUids']).']');
return $aSerializedUids['ThreadsUids'];
}
}
/*
// Idea to fetch all UID's in background
else if (!$bBackground) {
$this->logWrite('Set ThreadsMap() as background task ("'.$sFolderName.'" / '.$sSearch.')');
\SnappyMail\Shutdown::add(function($oMailClient, $oFolderInfo, $oCacher) {
$oFolderInfo->MESSAGES = 0;
$oMailClient->ThreadsMap($sAlgorithm, $oMessageCollection, $oCacher, true);
}, [$this, $oFolderInfo, $oCacher]);
return [];
}
*/
}
$this->oImapClient->FolderExamine($sFolderName);
$aResult = array();
try
{
foreach ($this->oImapClient->MessageThread($sSearch, $sAlgorithm) as $mItem) {
// Flatten to single level
$aMap = [];
\array_walk_recursive($mItem, function($a) use (&$aMap) { $aMap[] = $a; });
$aResult[] = $aMap;
}
}
catch (\MailSo\RuntimeException $oException)
{
\SnappyMail\Log::warning('MailClient', 'ThreadsMap ' . $oException->getMessage());
unset($oException);
}
if ($sSerializedHashKey) {
$oCacher->Set($sSerializedHashKey, \json_encode(array('ThreadsUids' => $aResult)));
$this->logWrite('Save Threads to cache ("'.$sFolderName.'" / '.$sSearch.') [count:'.\count($aResult).']');
}
$oMessageCollection->totalThreads = \count($aResult);
return $aResult;
}
// All threads UID's except the most recent UID of each thread
protected function ThreadsOldUids(array $aAllThreads, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false) : array
{
$oFolderInfo = $oMessageCollection->FolderInfo;
$bThreadSort = $this->bThreadSort && $this->oImapClient->hasCapability('SORT');
$sSerializedHashKey = null;
if ($oCacher && $oCacher->IsInited()) {
$sSerializedHashKey = "ThreadsOldUids/{$oFolderInfo->etag}/" . ($bThreadSort ? 'S' : 'N');
$sSerializedUids = $oCacher->Get($sSerializedHashKey);
if (!empty($sSerializedUids)) {
$aSerializedUids = \json_decode($sSerializedUids, true);
if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) {
$this->logWrite('Get old Threads UIDs from cache ("'.$oFolderInfo->FullName.'") [count:'.\count($aSerializedUids['ThreadsUids']).']');
return $aSerializedUids['ThreadsUids'];
}
}
}
$aUids = [];
if ($bThreadSort) {
$oParams = new MessageListParams;
$oParams->sFolderName = $oFolderInfo->FullName;
$oParams->sSort = 'DATE';
$oParams->bUseSort = true;
$oParams->bHideDeleted = false;
foreach ($aAllThreads as $aThreadUIDs) {
$oParams->oSequenceSet = new \MailSo\Imap\SequenceSet($aThreadUIDs);
$aThreadUIDs = $this->GetUids($oParams, $oFolderInfo);
if ($aThreadUIDs) {
// Remove the most recent UID
\array_pop($aThreadUIDs);
$aUids = \array_merge($aUids, $aThreadUIDs);
}
}
/*
// Idea to use one SORT for all threads instead of per thread
$aSortUids = \array_reduce($aAllThreads, 'array_merge', []);
$oParams->oSequenceSet = new \MailSo\Imap\SequenceSet($aSortUids);
$aSortUids = $this->GetUids($oParams, $oFolderInfo);
if ($aSortUids) {
foreach ($aAllThreads as $aThreadUIDs) {
$aThreadUIDs = \array_intersect($aSortUids, $aThreadUIDs);
// Remove the most recent UID
\array_pop($aThreadUIDs);
$aUids = \array_merge($aUids, $aThreadUIDs);
}
}
*/
} else {
// Not the best solution to remove the most recent UID,
// as older messages could have a higher UID
foreach ($aAllThreads as $aThreadUIDs) {
unset($aThreadUIDs[\array_search(\max($aThreadUIDs), $aThreadUIDs)]);
$aUids = \array_merge($aUids, $aThreadUIDs);
}
}
if ($sSerializedHashKey) {
$oCacher->Set($sSerializedHashKey, \json_encode(array('ThreadsUids' => $aUids)));
$this->logWrite('Save old Threads UIDs to cache ("'.$oFolderInfo->FullName.'") [count:'.\count($aUids).']');
}
return $aUids;
}
/**
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange,
array &$aAllThreads = [], array &$aUnseenUIDs = []) : void
{
if (\count($oRange)) {
$aFetchItems = array(
FetchType::UID,
FetchType::RFC822_SIZE,
FetchType::INTERNALDATE,
FetchType::FLAGS,
FetchType::BODYSTRUCTURE
);
if ($this->oImapClient->hasCapability('PREVIEW')) {
$aFetchItems[] = FetchType::PREVIEW; // . ' (LAZY)';
}
$aFetchItems[] = $this->getEnvelopeOrHeadersRequestString();
$aFetchIterator = $this->oImapClient->FetchIterate($aFetchItems, (string) $oRange, $oRange->UID);
// FETCH does not respond in the id order of the SequenceSet, so we prefill $aCollection for the right sort order.
$aCollection = \array_fill_keys($oRange->getArrayCopy(), null);
foreach ($aFetchIterator as $oFetchResponse) {
$id = $oRange->UID
? $oFetchResponse->GetFetchValue(FetchType::UID)
: $oFetchResponse->oImapResponse->ResponseList[1];
$oMessage = Message::fromFetchResponse($oMessageCollection->FolderName, $oFetchResponse);
if ($oMessage) {
if ($aAllThreads) {
$iUid = $oMessage->Uid;
// Find thread and set it.
// Used by GUI to delete/move the whole thread or other features
foreach ($aAllThreads as $aMap) {
if (\in_array($iUid, $aMap)) {
$oMessage->SetThreads($aMap);
$oMessage->SetThreadUnseen(\array_values(\array_intersect($aUnseenUIDs, $aMap)));
break;
}
}
}
$aCollection[$id] = $oMessage;
}
}
$oMessageCollection->exchangeArray(\array_values(\array_filter($aCollection)));
}
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
private function GetUids(MessageListParams $oParams, FolderInformation $oInfo, bool $onlyCache = false) : ?array
{
$oCacher = $oParams->oCacher;
$sFolderName = $oParams->sFolderName;
$bUseSort = $oParams->bUseSort && $this->oImapClient->hasCapability('SORT');
$aSortTypes = [];
if ($bUseSort) {
if ($oParams->sSort) {
// TODO: $oParams->sortValid($this->oImapClient);
$aSortTypes[] = $oParams->sSort;
}
if (!\str_contains($oParams->sSort, 'DATE')) {
// Always also sort DATE descending when DATE is not defined
$aSortTypes[] = 'REVERSE DATE';
}
}
$oParams->sSort = \implode(' ', $aSortTypes);
$bUseCache = $oCacher && $oCacher->IsInited();
$oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString(
$this->oImapClient,
$sFolderName,
$oParams->sSearch,
$oParams->bHideDeleted,
$bUseCache
);
// Disable? as there are many cases that change the result
// $bUseCache = false;
$bReturnUid = true;
if ($oParams->oSequenceSet) {
$bReturnUid = $oParams->oSequenceSet->UID;
$oSearchCriterias->prepend(($bReturnUid ? 'UID ' : '') . $oParams->oSequenceSet);
}
/*
$oSearchCriterias->fuzzy = $oParams->bSearchFuzzy && $this->oImapClient->hasCapability('SEARCH=FUZZY');
*/
$sSerializedHash = '';
$sSerializedLog = '';
if ($bUseCache && $oInfo->etag) {
$sSerializedHash = 'Get'
. ($bReturnUid ? 'UIDS/' : 'IDS/')
. "{$oParams->sSort}/{$this->oImapClient->Hash()}/{$sFolderName}/{$oSearchCriterias}";
$sSerializedLog = "\"{$sFolderName}\" / {$oParams->sSort} / {$oSearchCriterias}";
$sSerialized = $oCacher->Get($sSerializedHash);
if (!empty($sSerialized)) {
$aSerialized = \json_decode($sSerialized, true);
if (\is_array($aSerialized)
&& isset($aSerialized['FolderHash'], $aSerialized['Uids'])
&& $oInfo->etag === $aSerialized['FolderHash']
&& \is_array($aSerialized['Uids'])
) {
$this->logWrite('Get Serialized '.($bReturnUid?'UIDS':'IDS').' from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']');
return $aSerialized['Uids'];
}
}
}
if ($onlyCache) {
return null;
}
$this->oImapClient->FolderExamine($sFolderName);
$aResultUids = [];
if ($bUseSort) {
// $this->oImapClient->hasCapability('ESORT')
// $aResultUids = $this->oImapClient->MessageESort($aSortTypes, $oSearchCriterias)['ALL'];
$aResultUids = $this->oImapClient->MessageSort($aSortTypes, $oSearchCriterias, $bReturnUid);
} else {
// $this->oImapClient->hasCapability('ESEARCH')
// $aResultUids = $this->oImapClient->MessageESearch($oSearchCriterias, null, $bReturnUid)
$aResultUids = $this->oImapClient->MessageSearch($oSearchCriterias, $bReturnUid);
}
if ($bUseCache) {
$oCacher->Set($sSerializedHash, \json_encode(array(
'FolderHash' => $oInfo->etag,
'Uids' => $aResultUids
)));
$this->logWrite('Save Serialized '.($bReturnUid?'UIDS':'IDS').' to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']');
}
// $oSequenceSet = new SequenceSet($aResultUids, false);
// $oSequenceSet->UID = $bReturnUid;
// return $oSequenceSet;
return $aResultUids;
}
public function MessageListUnseen(MessageListParams $oParams, FolderInformation $oInfo) : array
{
$oUnseenParams = new MessageListParams;
$oUnseenParams->sFolderName = $oParams->sFolderName;
$oUnseenParams->sSearch = 'unseen';
// $oUnseenParams->sSort = $oParams->sSort;
$oUnseenParams->oCacher = $oParams->oCacher;
$oUnseenParams->bUseSort = false; // $oParams->bUseSort
$oUnseenParams->bUseThreads = false; // $oParams->bUseThreads;
$oUnseenParams->bHideDeleted = $oParams->bHideDeleted;
// $oUnseenParams->iOffset = $oParams->iOffset;
// $oUnseenParams->iLimit = $oParams->iLimit;
// $oUnseenParams->iPrevUidNext = $oParams->iPrevUidNext;
// $oUnseenParams->iThreadUid = $oParams->iThreadUid;
return $this->GetUids($oUnseenParams, $oInfo);
}
/**
* Runs SORT/SEARCH when $sSearch is provided
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function MessageList(MessageListParams $oParams) : MessageCollection
{
if (0 > $oParams->iOffset || 0 > $oParams->iLimit) {
throw new \ValueError;
}
if (10 > $oParams->iLimit) {
$oParams->iLimit = 10;
} else if (999 < $oParams->iLimit) {
$oParams->iLimit = 50;
}
$sSearch = \trim($oParams->sSearch);
$oMessageCollection = new MessageCollection;
$oMessageCollection->FolderName = $oParams->sFolderName;
$oMessageCollection->Offset = $oParams->iOffset;
$oMessageCollection->Limit = $oParams->iLimit;
$oMessageCollection->Search = $sSearch;
$oMessageCollection->ThreadUid = $oParams->iThreadUid;
// $oMessageCollection->Filtered = '' !== $this->oImapClient->Settings->search_filter;
$oInfo = $this->oImapClient->FolderStatusAndSelect($oParams->sFolderName);
$oMessageCollection->FolderInfo = $oInfo;
$oMessageCollection->totalEmails = $oInfo->MESSAGES;
$oParams->bUseThreads = $oParams->bUseThreads && $this->oImapClient->CapabilityValue('THREAD');
// && ($this->oImapClient->hasCapability('THREAD=REFS') || $this->oImapClient->hasCapability('THREAD=REFERENCES') || $this->oImapClient->hasCapability('THREAD=ORDEREDSUBJECT'));
if ($oParams->iThreadUid && !$oParams->bUseThreads) {
throw new \ValueError('THREAD not supported');
}
if (!$oInfo->MESSAGES || $oParams->iOffset > $oInfo->MESSAGES) {
return $oMessageCollection;
}
if (!$oParams->iThreadUid) {
$oMessageCollection->NewMessages = $this->getFolderNextMessageInformation(
$oParams->sFolderName, $oParams->iPrevUidNext, $oInfo->UIDNEXT
);
}
$bUseSort = ($oParams->bUseSort || $oParams->sSort) && $this->oImapClient->hasCapability('SORT');
$oParams->bUseSort = $bUseSort;
$oParams->sSearch = $sSearch;
$aAllThreads = [];
$aUnseenUIDs = [];
$aUids = null;
$message_list_limit = $this->oImapClient->Settings->message_list_limit;
if (100 > $message_list_limit || $message_list_limit > $oInfo->MESSAGES) {
$message_list_limit = 0;
}
// Idea to fetch all UID's in background
$oAllParams = clone $oParams;
$oAllParams->sSearch = '';
$oAllParams->oSequenceSet = null;
if ($message_list_limit && !$oParams->iThreadUid && $oParams->oCacher && $oParams->oCacher->IsInited()) {
$aUids = $this->GetUids($oAllParams, $oInfo, true);
if (null !== $aUids) {
$message_list_limit = 0;
$oMessageCollection->Sort = $oAllParams->sSort;
} else {
\SnappyMail\Shutdown::add(function($oMailClient, $oAllParams, $oInfo, $oMessageCollection) {
$oMailClient->GetUids($oAllParams, $oInfo);
if ($oAllParams->bUseThreads) {
$oMailClient->ThreadsMap($oAllParams->sThreadAlgorithm, $oMessageCollection, $oAllParams->oCacher, true);
}
}, [$this, $oAllParams, $oInfo, $oMessageCollection]);
}
}
if ($message_list_limit && !$aUids) {
// if ($message_list_limit || (!$this->oImapClient->hasCapability('SORT') && !$this->oImapClient->CapabilityValue('THREAD'))) {
// Don't use THREAD for speed
$oMessageCollection->Limited = true;
$this->logWrite('List optimization (count: '.$oInfo->MESSAGES.', limit:'.$message_list_limit.')');
if (\strlen($sSearch)) {
// Don't use SORT for speed
$oParams->bUseSort = false;
$aUids = $this->GetUids($oParams, $oInfo);
} else {
if ($bUseSort) {
// Attempt to sort REVERSE DATE with a bigger range then $oParams->iLimit
$end = \min($oInfo->MESSAGES, \max(1, $oInfo->MESSAGES - $oParams->iOffset + $oParams->iLimit));
$start = \max(1, $end - ($oParams->iLimit * 3) + 1);
$oParams->oSequenceSet = new SequenceSet(\range($end, $start), false);
$aRequestIndexes = $this->GetUids($oParams, $oInfo);
// Attempt to get the correct $oParams->iLimit slice
$aRequestIndexes = \array_slice($aRequestIndexes, $oParams->iOffset ? $oParams->iLimit : 0, $oParams->iLimit);
} else {
// Fetch ID's from high to low
$end = \max(1, $oInfo->MESSAGES - $oParams->iOffset);
$start = \max(1, $end - $oParams->iLimit + 1);
$aRequestIndexes = \range($end, $start);
}
$this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aRequestIndexes, false));
}
$oMessageCollection->Sort = $oParams->sSort;
} else {
if ($oParams->bUseThreads && $oParams->iThreadUid) {
$aUids = [$oParams->iThreadUid];
} else if (!$aUids) {
$aUids = $this->GetUids($oAllParams, $oInfo);
$oMessageCollection->Sort = $oAllParams->sSort;
}
if ($oParams->bUseThreads) {
$aAllThreads = $this->ThreadsMap($oParams->sThreadAlgorithm, $oMessageCollection, $oParams->oCacher);
// $iThreadLimit = $this->oImapClient->Settings->thread_limit;
if ($oParams->iThreadUid) {
// Only show the selected thread messages
foreach ($aAllThreads as $aMap) {
if (\in_array($oParams->iThreadUid, $aMap)) {
$aUids = $aMap;
break;
}
}
$aAllThreads = [$aUids];
// This only speeds up the search when not cached
// $oParams->oSequenceSet = new SequenceSet($aUids);
} else {
// Remove all threaded UID's except the most recent of each thread
$aUids = \array_diff($aUids, $this->ThreadsOldUids($aAllThreads, $oMessageCollection, $oParams->oCacher));
// Get all unseen
$aUnseenUIDs = $this->MessageListUnseen($oParams, $oInfo);
}
}
if ($aUids && \strlen($sSearch)) {
$oParams->bUseSort = false;
$aSearchedUids = $this->GetUids($oParams, $oInfo);
if ($oParams->bUseThreads && !$oParams->iThreadUid) {
$matchingThreadUids = [];
foreach ($aAllThreads as $aMap) {
if (\array_intersect($aSearchedUids, $aMap)) {
$matchingThreadUids = \array_merge($matchingThreadUids, $aMap);
}
}
$aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids, $matchingThreadUids) {
return \in_array($iUid, $aSearchedUids) || \in_array($iUid, $matchingThreadUids);
});
} else {
$aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids) {
return \in_array($iUid, $aSearchedUids);
});
}
}
}
if (\is_array($aUids)) {
$oMessageCollection->totalEmails = \count($aUids);
if ($oMessageCollection->totalEmails) {
$aUids = \array_slice($aUids, $oParams->iOffset, $oParams->iLimit);
$this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aUids), $aAllThreads, $aUnseenUIDs);
}
}
return $oMessageCollection;
}
public function FindMessageUidByMessageId(string $sFolderName, string $sMessageId) : ?int
{
if (!\strlen($sMessageId)) {
throw new \ValueError;
}
$this->oImapClient->FolderExamine($sFolderName);
$aUids = $this->oImapClient->MessageSearch('HEADER Message-ID '.$sMessageId);
return 1 === \count($aUids) && \is_numeric($aUids[0]) ? (int) $aUids[0] : null;
}
public function Folders(string $sParent, string $sListPattern, bool $bUseListSubscribeStatus) : ?FolderCollection
{
$oFolderCollection = $this->oImapClient->FolderStatusList($sParent, $sListPattern);
if (!$oFolderCollection->count()) {
return null;
}
if ($bUseListSubscribeStatus && !$this->oImapClient->hasCapability('LIST-EXTENDED')) {
// $this->logWrite('RFC5258 not supported, using LSUB');
// \SnappyMail\Log::warning('IMAP', 'RFC5258 not supported, using LSUB');
try
{
$oSubscribedFolders = $this->oImapClient->FolderSubscribeList($sParent, $sListPattern);
foreach ($oSubscribedFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) {
isset($oFolderCollection[$oImapFolder->FullName])
&& $oFolderCollection[$oImapFolder->FullName]->setSubscribed();
}
}
catch (\Throwable $oException)
{
\SnappyMail\Log::error('IMAP', 'FolderSubscribeList: ' . $oException->getMessage());
foreach ($oFolderCollection as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) {
$oImapFolder->setSubscribed();
}
}
}
return $oFolderCollection;
}
/**
* @throws \ValueError
*/
public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = '') : ?\MailSo\Imap\Folder
{
$sFolderNameInUtf8 = \trim($sFolderNameInUtf8);
$sFolderParentFullName = \trim($sFolderParentFullName);
if (!\strlen($sFolderNameInUtf8)) {
throw new \ValueError;
}
if (!\strlen($sDelimiter) || \strlen($sFolderParentFullName)) {
$sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sFolderParentFullName);
if (null === $sDelimiter) {
// TODO: Translate
throw new \MailSo\RuntimeException(
\strlen($sFolderParentFullName)
? 'Cannot create folder in non-existent parent folder.'
: 'Cannot get folder delimiter.');
}
if (\strlen($sDelimiter) && \strlen($sFolderParentFullName)) {
$sFolderParentFullName .= $sDelimiter;
}
}
/* // Allow non existent parent folders
if (\strlen($sDelimiter) && false !== \strpos($sFolderNameInUtf8, $sDelimiter)) {
// TODO: Translate
throw new \MailSo\RuntimeException('New folder name contains delimiter.');
}
*/
$sFullNameToCreate = $sFolderParentFullName.$sFolderNameInUtf8;
$this->oImapClient->FolderCreate($sFullNameToCreate, $bSubscribeOnCreation);
$aFolders = $this->oImapClient->FolderStatusList($sFullNameToCreate, '');
if (isset($aFolders[$sFullNameToCreate])) {
$oImapFolder = $aFolders[$sFullNameToCreate];
$bSubscribeOnCreation && $oImapFolder->setSubscribed();
return $oImapFolder;
}
return null;
}
/**
* @throws \InvalidArgumentException
*/
public function FolderRename(string $sPrevFolderFullName, string $sNewFolderFullName) : self
{
if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) {
throw new \ValueError;
}
if (!$this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName)) {
// TODO: Translate
throw new \MailSo\RuntimeException('Cannot rename non-existent folder.');
}
/*
if (\strlen($sDelimiter) && false !== \strpos($sNewFolderFullName, $sDelimiter)) {
// TODO: Translate
throw new \MailSo\RuntimeException('New folder name contains delimiter.');
}
*/
/**
* https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.5
* Does not mention subscriptions
* https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.6
* Mentions that a server doesn't automatically manage subscriptions
*/
$oSubscribedFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*');
$this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName);
foreach ($oSubscribedFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) {
$sFolderFullNameForResubscribe = $oFolder->FullName;
if (\str_starts_with($sFolderFullNameForResubscribe, $sPrevFolderFullName)) {
$this->oImapClient->FolderUnsubscribe($sFolderFullNameForResubscribe);
$this->oImapClient->FolderSubscribe(
$sNewFolderFullName . \substr($sFolderFullNameForResubscribe, \strlen($sPrevFolderFullName))
);
}
}
return $this;
}
/**
* @throws \InvalidArgumentException
*/
public function SetLogger(?\MailSo\Log\Logger $oLogger) : void
{
$this->oLogger = $oLogger;
$this->oImapClient->SetLogger($oLogger);
}
public function __call(string $name, array $arguments) /*: mixed*/
{
return $this->oImapClient->{$name}(...$arguments);
}
}