oLogger = null; $this->oImapClient = new \MailSo\Imap\ImapClient; $this->oImapClient->SetTimeOuts(10, \MailSo\Config::$ImapTimeout); } public function ImapClient() : \MailSo\Imap\ImapClient { return $this->oImapClient; } /** * @throws \MailSo\Net\Exceptions\Exception */ public function Disconnect() : self { $this->oImapClient->Disconnect(); return $this; } public function IsConnected() : bool { return $this->oImapClient->IsConnected(); } public function IsLoggined() : bool { return $this->oImapClient->IsLoggined(); } public function Capabilities() : array { return $this->oImapClient->Capability(); } private function getEnvelopeOrHeadersRequestString() : string { if (\MailSo\Config::$MessageAllHeaders) { return FetchType::BODY_HEADER_PEEK; } return FetchType::BuildBodyCustomHeaderRequest(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 MimeHeader::X_SPAM_STATUS, // MimeHeader::X_SPAM_FLAG, MimeHeader::X_SPAMD_RESULT, MimeHeader::X_BOGOSITY, // Virus MimeHeader::X_VIRUS, MimeHeader::X_VIRUS_SCANNED, MimeHeader::X_VIRUS_STATUS ), true); // // return FetchType::ENVELOPE; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception * @throws \MailSo\Mail\Exceptions\Exception */ public function MessageSetFlag(string $sFolderName, SequenceSet $oRange, string $sMessageFlag, bool $bSetAction = true, bool $bSkipUnsupportedFlag = false) : void { if (\count($oRange)) { $oFolderInfo = $this->oImapClient->FolderSelect($sFolderName); if ($oFolderInfo->IsFlagSupported($sMessageFlag)) { $sStoreAction = $bSetAction ? StoreAction::ADD_FLAGS_SILENT : StoreAction::REMOVE_FLAGS_SILENT; $this->oImapClient->MessageStoreFlag($oRange, array($sMessageFlag), $sStoreAction); } else if (!$bSkipUnsupportedFlag) { throw new Exceptions\RuntimeException('Message flag "'.$sMessageFlag.'" is not supported.'); } } } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null, int $iBodyTextLimit = 0) : ?Message { if (!\MailSo\Base\Validator::RangeInt($iIndex, 1)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oImapClient->FolderExamine($sFolderName); $oBodyStructure = null; $oMessage = 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) { foreach ($oBodyStructure->GetHtmlAndPlainParts() as $oPart) { $sLine = FetchType::BODY_PEEK.'['.$oPart->PartID().']'; if (0 < $iBodyTextLimit && $iBodyTextLimit < $oPart->Size()) { $sLine .= "<0.{$iBodyTextLimit}>"; } $aFetchItems[] = $sLine; } /* $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); foreach ($gSignatureParts as $oPart) { if ($oPart->IsPgpSigned()) { // An empty section specification refers to the entire message, including the header. // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[0]->PartID().'.MIME]'; $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[0]->PartID().']'; $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[1]->PartID().']'; } } */ } } if (!$oBodyStructure) { $aFetchItems[] = FetchType::BODYSTRUCTURE; } $aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid); if (\count($aFetchResponse)) { $oMessage = Message::NewFetchResponseInstance($sFolderName, $aFetchResponse[0], $oBodyStructure); } return $oMessage; } /** * Streams mime part to $mCallback * * @param mixed $mCallback * * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageMimeStream($mCallback, string $sFolderName, int $iIndex, string $sMimeIndex) : bool { if (!\is_callable($mCallback)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $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->IsSupported('BINARY')) { $sMailEncoding = ''; $sPeek = FetchType::BINARY_PEEK; } $sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE); } else { $sFileName = ($oHeaders->ValueByName(MimeHeader::SUBJECT) ?: $iIndex) . '.eml'; $sContentType = 'message/rfc822'; } } } $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.']', 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); } } )), $iIndex, true); return ($aFetchResponse && 1 === \count($aFetchResponse)); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageDelete(string $sFolder, SequenceSet $oRange, bool $bExpungeAll = false) : self { if (!\strlen($sFolder) || !\count($oRange)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oImapClient->FolderSelect($sFolder); $this->oImapClient->MessageStoreFlag($oRange, array(MessageFlag::DELETED), StoreAction::ADD_FLAGS_SILENT ); if ($bExpungeAll) { $this->oImapClient->FolderExpunge(); } else { $this->oImapClient->FolderExpunge($oRange); } return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageMove(string $sFromFolder, string $sToFolder, SequenceSet $oRange, bool $bUseMoveSupported = true, bool $bExpungeAll = false) : self { if (!$sFromFolder || !$sToFolder || !\count($oRange)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oImapClient->FolderSelect($sFromFolder); if ($bUseMoveSupported && $this->oImapClient->IsSupported('MOVE')) { $this->oImapClient->MessageMove($sToFolder, $oRange); } else { $this->oImapClient->MessageCopy($sToFolder, $oRange); $this->MessageDelete($sFromFolder, $oRange, $bExpungeAll); } return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageCopy(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : self { if (!$sFromFolder || !$sToFolder || !\count($oRange)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oImapClient->FolderSelect($sFromFolder); $this->oImapClient->MessageCopy($sToFolder, $oRange); return $this; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderUnselect() : self { if ($this->oImapClient->IsSelected()) { $this->oImapClient->FolderUnselect(); } return $this; } /** * @param resource $rMessageStream */ public function MessageAppendStream($rMessageStream, int $iMessageStreamSize, string $sFolderToSave, array $aAppendFlags = null, int &$iUid = null) : self { if (!\is_resource($rMessageStream) || !\strlen($sFolderToSave)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $iUid = $this->oImapClient->MessageAppendStream( $sFolderToSave, $rMessageStream, $iMessageStreamSize, $aAppendFlags); return $this; } public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, array $aAppendFlags = null, int &$iUid = null) : self { if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $iMessageStreamSize = \filesize($sMessageFileName); $rMessageStream = \fopen($sMessageFileName, 'rb'); $this->MessageAppendStream($rMessageStream, $iMessageStreamSize, $sFolderToSave, $aAppendFlags, $iUid); if (\is_resource($rMessageStream)) { fclose($rMessageStream); } return $this; } protected function initFolderValues(string $sFolderName) : array { $aFolderStatus = $this->oImapClient->FolderStatus($sFolderName)->getStatusItems(); return [ \max(0, $aFolderStatus[FolderResponseStatus::MESSAGES] ?: 0), \max(0, $aFolderStatus[FolderResponseStatus::UNSEEN] ?: 0), \max(0, $aFolderStatus[FolderResponseStatus::UIDNEXT] ?: 0), \max(0, $aFolderStatus[FolderResponseStatus::HIGHESTMODSEQ] ?: 0), $aFolderStatus[FolderResponseStatus::APPENDLIMIT] ?: $this->oImapClient->AppendLimit(), $aFolderStatus[FolderResponseStatus::MAILBOXID] ?: '' ]; } public function GenerateImapClientHash() : string { return \md5('ImapClientHash/'. $this->oImapClient->GetLogginedUser().'@'. $this->oImapClient->GetConnectedHost().':'. $this->oImapClient->GetConnectedPort() ); } public function GenerateFolderHash(string $sFolder, int $iCount, int $iUidNext, int $iHighestModSeq) : string { return \md5('FolderHash/'.$sFolder.'-'.$iCount.'-'.$iUidNext.'-'. $iHighestModSeq.'-'.$this->GenerateImapClientHash().'-'. \MailSo\Config::$MessageListPermanentFilter ); } /** * Returns list of new messages since $iPrevUidNext * Currently only for INBOX */ private function getFolderNextMessageInformation(string $sFolderName, int $iPrevUidNext, int $iCurrentUidNext) : array { $aNewMessages = array(); if ($iPrevUidNext && $iPrevUidNext != $iCurrentUidNext && 'INBOX' === $sFolderName && \MailSo\Config::$CheckNewMessages) { $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 /* @var $oFetchResponse \MailSo\Imap\FetchResponse */ $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_, !$sContentTypeCharset) ); } } } return $aNewMessages; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array { list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName); $aFlags = array(); if ($oRange && \count($oRange)) { $oInfo = $this->oImapClient->FolderExamine($sFolderName); // $oInfo->PermanentFlags $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('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS)); $aFlags[] = array( 'Uid' => $iUid, 'Flags' => $aLowerFlags ); } } return array( 'Folder' => $sFolderName, 'Hash' => $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq), 'MessageCount' => $iCount, 'MessageUnseenCount' => $iUnseenCount, 'UidNext' => $iUidNext, 'MessagesFlags' => $aFlags, 'HighestModSeq' => $iHighestModSeq, 'AppendLimit' => $iAppendLimit, 'MailboxId' => $sMailboxId, 'NewMessages' => $this->getFolderNextMessageInformation($sFolderName, $iPrevUidNext, $iUidNext) ); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderHash(string $sFolderName) : string { list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($sFolderName); return $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageListThreadsMap(string $sFolderName, string $sFolderHash, ?\MailSo\Cache\CacheClient $oCacher) : array { $sSearchHash = ''; if (0 < \MailSo\Config::$MessageListDateFilter) { $iD = \time() - 3600 * 24 * 30 * \MailSo\Config::$MessageListDateFilter; $iTimeFilter = \gmmktime(1, 1, 1, \gmdate('n', $iD), 1, \gmdate('Y', $iD)); $sSearchHash .= ' SINCE '.\gmdate('j-M-Y', $iTimeFilter); } if ('' === \trim($sSearchHash)) { $sSearchHash = 'ALL'; } if ($oCacher && $oCacher->IsInited()) { $sSerializedHashKey = 'ThreadsMapSorted/'.$sSearchHash.'/'.$sFolderName.'/'.$sFolderHash; if ($this->oLogger) { $this->oLogger->Write($sSerializedHashKey); } $sSerializedUids = $oCacher->Get($sSerializedHashKey); if (!empty($sSerializedUids)) { $aSerializedUids = \json_decode($sSerializedUids, true); if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) { if ($this->oLogger) { $this->oLogger->Write('Get Serialized Thread UIDS from cache ("'.$sFolderName.'" / '.$sSearchHash.') [count:'.\count($aSerializedUids['ThreadsUids']).']'); } return $aSerializedUids['ThreadsUids']; } } } $this->oImapClient->FolderExamine($sFolderName); $aResult = array(); try { foreach ($this->oImapClient->MessageSimpleThread($sSearchHash) as $mItem) { // Flatten to single level $aMap = []; \array_walk_recursive($mItem, function($a) use (&$aMap) { $aMap[] = $a; }); $aResult[] = $aMap; } } catch (\MailSo\Imap\Exceptions\RuntimeException $oException) { \SnappyMail\Log::warning('MessageListThreadsMap ' . $oException->getMessage()); unset($oException); } if ($oCacher && $oCacher->IsInited() && !empty($sSerializedHashKey)) { $oCacher->Set($sSerializedHashKey, \json_encode(array( 'ThreadsUids' => $aResult ))); if ($this->oLogger) { $this->oLogger->Write('Save Serialized Thread UIDS to cache ("'.$sFolderName.'" / '.$sSearchHash.') [count:'.\count($aResult).']'); } } return $aResult; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange) : void { if (\count($oRange)) { $aFetchResponse = $this->oImapClient->Fetch(array( FetchType::UID, FetchType::RFC822_SIZE, FetchType::INTERNALDATE, FetchType::FLAGS, FetchType::BODYSTRUCTURE, $this->getEnvelopeOrHeadersRequestString() ), (string) $oRange, $oRange->UID); if (\count($aFetchResponse)) { $aCollection = \array_fill_keys($oRange->getArrayCopy(), null); foreach ($aFetchResponse as /* @var $oFetchResponseItem \MailSo\Imap\FetchResponse */ $oFetchResponseItem) { $id = $oRange->UID ? $oFetchResponseItem->GetFetchValue(FetchType::UID) : $this->oImapResponse->ResponseList[1]; $aCollection[$id] = Message::NewFetchResponseInstance($oMessageCollection->FolderName, $oFetchResponseItem); } $oMessageCollection->exchangeArray(\array_values(\array_filter($aCollection))); } } } /** * @throws \MailSo\Net\Exceptions\Exception */ public function IsThreadsSupported() : bool { return $this->oImapClient->IsSupported('THREAD=REFS') || $this->oImapClient->IsSupported('THREAD=REFERENCES') || $this->oImapClient->IsSupported('THREAD=ORDEREDSUBJECT'); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function GetUids(?\MailSo\Cache\CacheClient $oCacher, string $sSearch, string $sFolderName, string $sFolderHash, bool $bUseSortIfSupported = false, string $sSort = '') : array { /* TODO: Validate $sSort ARRIVAL Internal date and time of the message. This differs from the ON criteria in SEARCH, which uses just the internal date. CC [IMAP] addr-mailbox of the first "cc" address. DATE Sent date and time, as described in section 2.2. FROM [IMAP] addr-mailbox of the first "From" address. REVERSE Followed by another sort criterion, has the effect of that criterion but in reverse (descending) order. Note: REVERSE only reverses a single criterion, and does not affect the implicit "sequence number" sort criterion if all other criteria are identical. Consequently, a sort of REVERSE SUBJECT is not the same as a reverse ordering of a SUBJECT sort. This can be avoided by use of additional criteria, e.g., SUBJECT DATE vs. REVERSE SUBJECT REVERSE DATE. In general, however, it's better (and faster, if the client has a "reverse current ordering" command) to reverse the results in the client instead of issuing a new SORT. SIZE Size of the message in octets. SUBJECT Base subject text. TO [IMAP] addr-mailbox of the first "To" address. RFC 5957: $this->oImapClient->IsSupported('SORT=DISPLAY') DISPLAYFROM, DISPLAYTO */ $aResultUids = false; $bUidsFromCacher = false; $bUseCacheAfterSearch = true; $sSerializedHash = ''; $sSerializedLog = ''; $bUseSortIfSupported = $bUseSortIfSupported && !\strlen($sSearch) && $this->oImapClient->IsSupported('SORT'); $sSearchCriterias = \MailSo\Imap\SearchCriterias::fromString($this->oImapClient, $sFolderName, $sSearch, $bUseCacheAfterSearch); if ($bUseCacheAfterSearch && $oCacher && $oCacher->IsInited()) { $sSerializedHash = 'GetUids/'. ($bUseSortIfSupported ? 'S' . $sSort : 'N').'/'. $this->GenerateImapClientHash().'/'. $sFolderName.'/'.$sSearchCriterias; $sSerializedLog = '"'.$sFolderName.'" / '.$sSearchCriterias.''; // $sSerialized = $oCacher->Get($sSerializedHash); if (!empty($sSerialized)) { $aSerialized = \json_decode($sSerialized, true); if (\is_array($aSerialized) && isset($aSerialized['FolderHash'], $aSerialized['Uids']) && $sFolderHash === $aSerialized['FolderHash'] && \is_array($aSerialized['Uids']) ) { if ($this->oLogger) { $this->oLogger->Write('Get Serialized UIDS from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']'); } $aResultUids = $aSerialized['Uids']; $bUidsFromCacher = true; } } } if (!\is_array($aResultUids)) { if ($bUseSortIfSupported) { // $this->oImapClient->IsSupported('ESORT') // $aResultUids = $this->oImapClient->MessageSimpleESort(array($sSort ?: 'REVERSE DATE'), $sSearchCriterias)['ALL']; $aResultUids = $this->oImapClient->MessageSimpleSort(array($sSort ?: 'REVERSE DATE'), $sSearchCriterias); } else { // $this->oImapClient->IsSupported('ESEARCH') // $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, true, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8') $aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, true, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8'); } if (!$bUidsFromCacher && $bUseCacheAfterSearch && \is_array($aResultUids) && $oCacher && $oCacher->IsInited() && \strlen($sSerializedHash)) { $oCacher->Set($sSerializedHash, \json_encode(array( 'FolderHash' => $sFolderHash, 'Uids' => $aResultUids ))); if ($this->oLogger) { $this->oLogger->Write('Save Serialized UIDS to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']'); } } } return \is_array($aResultUids) ? $aResultUids : array(); } /** * Runs SORT/SEARCH when $sSearch is provided * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageList(MessageListParams $oParams) : MessageCollection { if (!\MailSo\Base\Validator::RangeInt($oParams->iOffset, 0) || !\MailSo\Base\Validator::RangeInt($oParams->iLimit, 0, 999)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $sSearch = \trim($oParams->sSearch); list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($oParams->sFolderName); // Don't use FolderExamine, else PERMANENTFLAGS is empty $oInfo = $this->oImapClient->FolderSelect($oParams->sFolderName); $oMessageCollection = new MessageCollection; $oMessageCollection->FolderName = $oParams->sFolderName; $oMessageCollection->FolderInfo = $oInfo; $oMessageCollection->Offset = $oParams->iOffset; $oMessageCollection->Limit = $oParams->iLimit; $oMessageCollection->Search = $sSearch; $oMessageCollection->ThreadUid = $oParams->iThreadUid; $oMessageCollection->Filtered = '' !== \MailSo\Config::$MessageListPermanentFilter; $oMessageCollection->MessageCount = $iMessageRealCount; $oMessageCollection->MessageUnseenCount = $iMessageUnseenCount; $aUids = array(); $aAllThreads = []; $bUseSortIfSupported = $oParams->bUseSortIfSupported && $this->oImapClient->IsSupported('SORT'); $bUseThreads = $oParams->bUseThreads ? ($this->oImapClient->IsSupported('THREAD=REFS') || $this->oImapClient->IsSupported('THREAD=REFERENCES') || $this->oImapClient->IsSupported('THREAD=ORDEREDSUBJECT')) : false; if ($oParams->iThreadUid && !$bUseThreads) { throw new \MailSo\Base\Exceptions\InvalidArgumentException('THREAD not supported'); } if (!$oParams->oCacher || !($oParams->oCacher instanceof \MailSo\Cache\CacheClient)) { $oParams->oCacher = null; } $oMessageCollection->FolderHash = $this->GenerateFolderHash( $oParams->sFolderName, $iMessageRealCount, $iUidNext, $iHighestModSeq); $oMessageCollection->UidNext = $iUidNext; if (!$oParams->iThreadUid) { $oMessageCollection->NewMessages = $this->getFolderNextMessageInformation( $oParams->sFolderName, $oParams->iPrevUidNext, $iUidNext); } $bSearch = false; $bMessageListOptimization = 0 < \MailSo\Config::$MessageListCountLimitTrigger && \MailSo\Config::$MessageListCountLimitTrigger < $iMessageRealCount; if ($bMessageListOptimization) { $bUseSortIfSupported = false; $bUseThreads = false; } if ($iMessageRealCount && !$bMessageListOptimization) { if ($bUseThreads) { $aAllThreads = $this->MessageListThreadsMap($oMessageCollection->FolderName, $oMessageCollection->FolderHash, $oParams->oCacher); // $iThreadLimit = \MailSo\Config::$LargeThreadLimit; if ($oParams->iThreadUid) { $aUids = [$oParams->iThreadUid]; // Only show the selected thread messages foreach ($aAllThreads as $aMap) { if (\in_array($oParams->iThreadUid, $aMap)) { $aUids = $aMap; break; } } } else { $aUids = $this->GetUids($oParams->oCacher, '', $oMessageCollection->FolderName, $oMessageCollection->FolderHash, $bUseSortIfSupported, $oParams->sSort); // Remove all threaded UID's except the most recent of each thread $threadedUids = []; foreach ($aAllThreads as $aMap) { unset($aMap[\array_key_last($aMap)]); $threadedUids = \array_merge($threadedUids, $aMap); } $aUids = \array_diff($aUids, $threadedUids); } } else { $aUids = $this->GetUids($oParams->oCacher, '', $oMessageCollection->FolderName, $oMessageCollection->FolderHash, $bUseSortIfSupported, $oParams->sSort); } if ($aUids && \strlen($sSearch)) { $aSearchedUids = $this->GetUids($oParams->oCacher, $sSearch, $oMessageCollection->FolderName, $oMessageCollection->FolderHash); if ($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); }); } } $oMessageCollection->MessageResultCount = \count($aUids); if (\count($aUids)) { $this->MessageListByRequestIndexOrUids( $oMessageCollection, new SequenceSet(\array_slice($aUids, $oParams->iOffset, $oParams->iLimit)) ); } } else if ($iMessageRealCount) { if ($this->oLogger) { $this->oLogger->Write('List optimization (count: '.$iMessageRealCount. ', limit:'.\MailSo\Config::$MessageListCountLimitTrigger.')'); } if (\strlen($sSearch)) { $aUids = $this->GetUids($oParams->oCacher, $sSearch, $oMessageCollection->FolderName, $oMessageCollection->FolderHash); $oMessageCollection->MessageResultCount = \count($aUids); if ($oMessageCollection->MessageResultCount) { $this->MessageListByRequestIndexOrUids( $oMessageCollection, new SequenceSet(\array_slice($aUids, $oParams->iOffset, $oParams->iLimit)) ); } } else { $oMessageCollection->MessageResultCount = $iMessageRealCount; if (1 < $iMessageRealCount) { $aRequestIndexes = \array_slice(\array_reverse(\range(1, $iMessageRealCount)), $oParams->iOffset, $oParams->iLimit); } else { $aRequestIndexes = \array_slice(array(1), $oParams->iOffset, $oParams->iLimit); } $this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aRequestIndexes, false)); } } if ($aAllThreads && !$oParams->iThreadUid) { foreach ($oMessageCollection as $oMessage) { $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); break; } } } } return $oMessageCollection; } public function Quota(string $sRootName = '') : ?array { return $this->oImapClient->Quota($sRootName); } public function QuotaRoot(string $sFolderName = 'INBOX') : ?array { return $this->oImapClient->QuotaRoot($sFolderName); } public function FindMessageUidByMessageId(string $sFolderName, string $sMessageId) : ?int { if (!\strlen($sMessageId)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oImapClient->FolderExamine($sFolderName); $aUids = $this->oImapClient->MessageSimpleSearch('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, int $iOptimizationLimit, bool $bUseListStatus) : ?FolderCollection { $aImapSubscribedFoldersHelper = null; if ($this->oImapClient->IsSupported('LIST-EXTENDED')) { $bUseListSubscribeStatus = false; } else if ($bUseListSubscribeStatus) { //\SnappyMail\Log::warning('IMAP', 'RFC5258 not supported, using LSUB'); try { $aSubscribedFolders = $this->oImapClient->FolderSubscribeList($sParent, $sListPattern); $aImapSubscribedFoldersHelper = array(); foreach ($aSubscribedFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { $aImapSubscribedFoldersHelper[] = $oImapFolder->FullName(); } } catch (\Throwable $oException) { \SnappyMail\Log::error('IMAP', 'FolderSubscribeList: ' . $oException->getMessage()); } } $aFolders = $bUseListStatus ? $this->oImapClient->FolderStatusList($sParent, $sListPattern) : $this->oImapClient->FolderList($sParent, $sListPattern); if (!$aFolders) { return null; } $oFolderCollection = new FolderCollection; $oFolderCollection->Optimized = 10 < $iOptimizationLimit && \count($aFolders) > $iOptimizationLimit; $sINBOX = 'INBOX'; foreach ($aFolders as $sFullName => /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { $oMailFolder = new Folder($oImapFolder, ($bUseListSubscribeStatus && (null === $aImapSubscribedFoldersHelper || \in_array($sFullName, $aImapSubscribedFoldersHelper))) || $oImapFolder->IsInbox() ); if ($oImapFolder->IsInbox()) { $sINBOX = $sFullName; } $aFolders[$sFullName] = $oMailFolder; // Add NonExistent folders $sDelimiter = $oMailFolder->Delimiter(); $aFolderExplode = \explode($sDelimiter, $sFullName); \array_pop($aFolderExplode); while ($aFolderExplode) { $sNonExistentFolderFullName = \implode($sDelimiter, $aFolderExplode); if (!isset($aFolders[$sNonExistentFolderFullName])) { try { $aFolders[$sNonExistentFolderFullName] = Folder::NewNonExistentInstance($sNonExistentFolderFullName, $sDelimiter); } catch (\Throwable $oExc) { unset($oExc); } } \array_pop($aFolderExplode); } } $oFolderCollection->exchangeArray(\array_values($aFolders)); $oFolderCollection->TotalCount = \count($aFolders); return $oFolderCollection; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = '') : ?Folder { $sFolderNameInUtf8 = \trim($sFolderNameInUtf8); $sFolderParentFullName = \trim($sFolderParentFullName); if (!\strlen($sFolderNameInUtf8)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } if (!\strlen($sDelimiter) || \strlen($sFolderParentFullName)) { $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sFolderParentFullName); if (null === $sDelimiter) { // TODO: Translate throw new Exceptions\RuntimeException( \strlen($sFolderParentFullName) ? 'Cannot create folder in non-existent parent folder.' : 'Cannot get folder delimiter.'); } if (\strlen($sDelimiter) && \strlen($sFolderParentFullName)) { $sFolderParentFullName .= $sDelimiter; } } if (\strlen($sDelimiter) && false !== \strpos($sFolderNameInUtf8, $sDelimiter)) { // TODO: Translate throw new Exceptions\RuntimeException( 'New folder name contains delimiter.'); } $sFullNameToCreate = $sFolderParentFullName.$sFolderNameInUtf8; $this->oImapClient->FolderCreate($sFullNameToCreate); if ($bSubscribeOnCreation) { $this->oImapClient->FolderSubscribe($sFullNameToCreate); } $aFolders = $this->oImapClient->IsSupported('LIST-STATUS') ? $this->oImapClient->FolderStatusList($sFullNameToCreate, '') : $this->oImapClient->FolderList($sFullNameToCreate, ''); $oImapFolder = $aFolders[$sFullNameToCreate]; return $oImapFolder ? new Folder($oImapFolder, $bSubscribeOnCreation) : null; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ public function FolderMove(string $sPrevFolderFullName, string $sNextFolderFullNameInUtf, bool $bSubscribeOnMove = true) : self { if (!$this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName)) { // TODO: Translate throw new Exceptions\RuntimeException('Cannot move non-existent folder.'); } return $this->folderModify($sPrevFolderFullName, $sNextFolderFullNameInUtf, $bSubscribeOnMove); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ public function FolderRename(string $sPrevFolderFullName, string $sNewTopFolderNameInUtf, bool $bSubscribeOnRename = true) : string { $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName); if (!$sDelimiter) { // TODO: Translate throw new Exceptions\RuntimeException('Cannot rename non-existent folder.'); } if (\strlen($sDelimiter) && false !== \strpos($sNewTopFolderNameInUtf, $sDelimiter)) { // TODO: Translate throw new Exceptions\RuntimeException('New folder name contains delimiter.'); } $iLast = \strrpos($sPrevFolderFullName, $sDelimiter); $sNewFolderFullName = (false === $iLast ? '' : \substr($sPrevFolderFullName, 0, $iLast + 1)) . $sNewTopFolderNameInUtf; $this->folderModify($sPrevFolderFullName, $sNewFolderFullName, $bSubscribeOnRename); return $sNewFolderFullName; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Base\Exceptions\RuntimeException */ protected function folderModify(string $sPrevFolderFullName, string $sNewFolderFullName, bool $bSubscribe) : self { if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $aSubscribeFolders = array(); if ($bSubscribe) { $aSubscribeFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*'); foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) { $this->oImapClient->FolderUnsubscribe($oFolder->FullName()); } } $this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName); foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) { $sFolderFullNameForResubscrine = $oFolder->FullName(); if (0 === \strpos($sFolderFullNameForResubscrine, $sPrevFolderFullName)) { $sNewFolderFullNameForResubscrine = $sNewFolderFullName. \substr($sFolderFullNameForResubscrine, \strlen($sPrevFolderFullName)); $this->oImapClient->FolderSubscribe($sNewFolderFullNameForResubscrine); } } return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Mail\Exceptions\RuntimeException */ public function FolderDelete(string $sFolderFullName) : self { if (!\strlen($sFolderFullName) || 'INBOX' === $sFolderFullName) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } if ($this->oImapClient->IsSupported('IMAP4rev2')) { $oInfo = $this->oImapClient->FolderExamine($sFolderFullName); } else { $oInfo = $this->oImapClient->FolderStatus($sFolderFullName); } if ($oInfo->MESSAGES) { throw new Exceptions\NonEmptyFolder; } $this->oImapClient->FolderUnsubscribe($sFolderFullName); $this->oImapClient->FolderUnselect(); $this->oImapClient->FolderDelete($sFolderFullName); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ public function FolderClear(string $sFolderFullName) : self { $oFolderInformation = $this->oImapClient->FolderSelect($sFolderFullName); if (0 < $oFolderInformation->MESSAGES) { $this->oImapClient->MessageStoreFlag(new SequenceSet('1:*', false), array(MessageFlag::DELETED), StoreAction::ADD_FLAGS_SILENT ); $this->oImapClient->FolderExpunge(); } return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ public function FolderSubscribe(string $sFolderFullName, bool $bSubscribe) : self { if (!\strlen($sFolderFullName)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oImapClient->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ public function SetLogger(\MailSo\Log\Logger $oLogger) : self { if (!($oLogger instanceof \MailSo\Log\Logger)) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $this->oLogger = $oLogger; $this->oImapClient->SetLogger($this->oLogger); return $this; } public function GetNamespace() : string { $oNamespace = $this->oImapClient->GetNamespace(); return $oNamespace ? $oNamespace->GetPersonalNamespace() : ''; } /** * RFC 5464 */ public function ServerGetMetadata(array $aEntries, array $aOptions = []) : array { return $this->oImapClient->ServerGetMetadata($aEntries, $aOptions); } public function FolderGetMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array { return $this->oImapClient->FolderGetMetadata($sFolderName, $aEntries, $aOptions); } public function FolderSetMetadata(string $sFolderName, array $aEntries) : void { $this->oImapClient->FolderSetMetadata($sFolderName, $aEntries); } public function FolderDeleteMetadata($sFolderName, array $aEntries) : void { $this->oImapClient->FolderSetMetadata($sFolderName, \array_fill_keys(\array_keys($aEntries), null)); } }