sLogginedUser; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function Connect(string $sServerName, int $iPort = 143, int $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT, bool $bVerifySsl = false, bool $bAllowSelfSigned = true, string $sClientCert = '') : void { $this->aTagTimeouts['*'] = \microtime(true); parent::Connect($sServerName, $iPort, $iSecurityType, $bVerifySsl, $bAllowSelfSigned, $sClientCert); $this->aCapabilityItems = $this->getResponse('*')->getCapabilityResult(); if (\MailSo\Net\Enumerations\ConnectionSecurityType::UseStartTLS( $this->IsSupported('STARTTLS'), $this->iSecurityType)) { $this->SendRequestGetResponse('STARTTLS'); $this->EnableCrypto(); $this->aCapabilityItems = null; } else if (\MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS === $this->iSecurityType) { $this->writeLogException( new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), \MailSo\Log\Enumerations\Type::ERROR, true); } } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function Login(array $aCredentials) : self { if (!empty($aCredentials['ProxyAuthUser']) && !empty($aCredentials['ProxyAuthPassword'])) { $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['ProxyAuthUser'])); $sPassword = $aCredentials['ProxyAuthPassword']; $sProxyAuthUser = $aCredentials['Login']; } else { $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login'])); $sPassword = $aCredentials['Password']; $sProxyAuthUser = ''; } if (!\strlen($sLogin) || !\strlen($sPassword)) { $this->writeLogException( new \MailSo\Base\Exceptions\InvalidArgumentException, \MailSo\Log\Enumerations\Type::ERROR, true); } $this->sLogginedUser = $sLogin; // $encrypted = !empty(\stream_get_meta_data($this->ConnectionResource())['crypto']); $type = $this->IsSupported('LOGINDISABLED') ? '' : 'LOGIN'; // RFC3501 6.2.3 $types = [ // if !$encrypted: // 'SCRAM-SHA-256' => 1, // 'SCRAM-SHA-1' => 1, // if $encrypted: 'CRAM-MD5' => $aCredentials['UseAuthCramMd5IfSupported'], 'PLAIN' => $aCredentials['UseAuthPlainIfSupported'], 'OAUTHBEARER' => $aCredentials['UseAuthOAuth2IfSupported'], 'XOAUTH2' => $aCredentials['UseAuthOAuth2IfSupported'], 'LOGIN' => 1 ]; foreach ($types as $sasl_type => $active) { if ($active && $this->IsSupported("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) { $type = $sasl_type; break; } } $SASL = \SnappyMail\SASL::factory($type); $SASL->base64 = true; try { if (0 === \strpos($type, 'SCRAM-')) { $sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); $this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid*/), true); $sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), Enumerations\ResponseType::CONTINUATION)); if ($this->oLogger) { $this->oLogger->AddSecret($sChallenge); } $this->sendRaw($sChallenge, true, '*******'); $oResponse = $this->getResponse(); $sSignature = $this->getResponseValue($oResponse); $SASL->verify($sSignature); } else if ('CRAM-MD5' === $type) { $sChallenge = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); $this->oLogger->Write('challenge: '.\base64_decode($sChallenge)); $sAuth = $SASL->authenticate($sLogin, $sPassword, $sChallenge); if ($this->oLogger) { $this->oLogger->AddSecret($sAuth); } $this->sendRaw($sAuth, true, '*******'); $oResponse = $this->getResponse(); } else if ('PLAIN' === $type || 'OAUTHBEARER' === $type) { $sAuth = $SASL->authenticate($sLogin, $sPassword); if ($this->oLogger) { $this->oLogger->AddSecret($sAuth); } if ($this->IsSupported('SASL-IR')) { $oResponse = $this->SendRequestGetResponse('AUTHENTICATE', array($type, $sAuth)); } else { $this->SendRequestGetResponse('AUTHENTICATE', array($type)); $this->sendRaw($sAuth, true, '*******'); $oResponse = $this->getResponse(); } } else if ('XOAUTH2' === $type) { $sAuth = $SASL->authenticate($sLogin, $sPassword); $this->SendRequest('AUTHENTICATE', array($type, $sAuth)); $oResponse = $this->getResponse(); $oR = $oResponse->getLast(); if ($oR && Enumerations\ResponseType::CONTINUATION === $oR->ResponseType) { if (!empty($oR->ResponseList[1]) && preg_match('/^[a-zA-Z0-9=+\/]+$/', $oR->ResponseList[1])) { $this->Logger()->Write(\base64_decode($oR->ResponseList[1]), \MailSo\Log\Enumerations\Type::WARNING); } $this->sendRaw(''); $oResponse = $this->getResponse(); } } else if ($this->IsSupported('LOGINDISABLED')) { $sB64 = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); $this->sendRaw($SASL->authenticate($sLogin, $sPassword, $sB64), true); $this->getResponse(); $sPass = $SASL->challenge(''/*UGFzc3dvcmQ6*/); if ($this->oLogger) { $this->oLogger->AddSecret($sPass); } $this->sendRaw($sPass, true, '*******'); $oResponse = $this->getResponse(); } else { if ($this->oLogger) { $this->oLogger->AddSecret($this->EscapeString($sPassword)); } $oResponse = $this->SendRequestGetResponse('LOGIN', array( $this->EscapeString($sLogin), $this->EscapeString($sPassword) )); } $this->aCapabilityItems = $oResponse->getCapabilityResult(); if (\strlen($sProxyAuthUser)) { $this->SendRequestGetResponse('PROXYAUTH', array($this->EscapeString($sProxyAuthUser))); } /* // RFC 9051 if ($this->IsSupported('IMAP4rev2')) { $this->SendRequestGetResponse('ENABLE', array('IMAP4rev1')); } */ } catch (Exceptions\NegativeResponseException $oException) { $this->writeLogException( new Exceptions\LoginBadCredentialsException( $oException->GetResponses(), '', 0, $oException), \MailSo\Log\Enumerations\Type::NOTICE, true); } $this->bIsLoggined = true; return $this; } /** * @throws \MailSo\Net\Exceptions\Exception */ public function Logout() : void { if ($this->bIsLoggined) { $this->bIsLoggined = false; $this->SendRequestGetResponse('LOGOUT'); } } public function IsLoggined() : bool { return $this->IsConnected() && $this->bIsLoggined; } public function IsSelected() : bool { return $this->IsLoggined() && $this->oCurrentFolderInfo; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function Capability() : ?array { if (!$this->aCapabilityItems) { $this->aCapabilityItems = $this->SendRequestGetResponse('CAPABILITY') ->getCapabilityResult(); } return $this->aCapabilityItems; } /** * Test support for things like: * IMAP4rev1 IMAP4rev2 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY * THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND * URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED * I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH * LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY STATUS=SIZE LITERAL+ NOTIFY SPECIAL-USE * STARTTLS AUTH= LOGIN LOGINDISABLED QUOTA * METADATA METADATA-SERVER * * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function IsSupported(string $sExtentionName) : bool { $sExtentionName = \trim($sExtentionName); return $sExtentionName && \in_array(\strtoupper($sExtentionName), $this->Capability() ?: []); } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function GetNamespace() : ?NamespaceResult { if (!$this->IsSupported('NAMESPACE')) { return null; } try { return $this->SendRequestGetResponse('NAMESPACE')->getNamespaceResult(); } catch (\Throwable $e) { $this->writeLogException($e, \MailSo\Log\Enumerations\Type::ERROR); throw $e; } } /** * RFC 7889 * APPENDLIMIT= indicates that the IMAP server has the same upload limit for all mailboxes. * APPENDLIMIT without any value indicates that the IMAP server supports this extension, * and that the client will need to discover upload limits for each mailbox, * as they might differ from mailbox to mailbox. */ public function AppendLimit() : ?int { if ($this->Capability()) { foreach ($this->aCapabilityItems as $string) { if ('APPENDLIMIT=' === \substr($string, 0, 12)) { return (int) \substr($string, 12); } } } return null; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function Noop() : self { $this->SendRequestGetResponse('NOOP'); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderCreate(string $sFolderName) : self { $this->SendRequestGetResponse('CREATE', array($this->EscapeString($sFolderName))); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderDelete(string $sFolderName) : self { // Uncomment will work issue #124 ? // $this->selectOrExamineFolder($sFolderName, true); $this->SendRequestGetResponse('DELETE', array($this->EscapeString($sFolderName))); // $this->FolderCheck(); // $this->FolderUnSelect(); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderSubscribe(string $sFolderName) : self { $this->SendRequestGetResponse('SUBSCRIBE', array($this->EscapeString($sFolderName))); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderUnSubscribe(string $sFolderName) : self { $this->SendRequestGetResponse('UNSUBSCRIBE', array($this->EscapeString($sFolderName))); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderRename(string $sOldFolderName, string $sNewFolderName) : self { $this->SendRequestGetResponse('RENAME', array( $this->EscapeString($sOldFolderName), $this->EscapeString($sNewFolderName))); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderStatus(string $sFolderName, array $aStatusItems) : ?array { if (!\count($aStatusItems)) { return null; } $oFolderInfo = $this->oCurrentFolderInfo; $bReselect = false; $bWritable = false; if ($oFolderInfo && $sFolderName === $oFolderInfo->FolderName) { /** * There's a long standing IMAP CLIENTBUG where STATUS command is executed * after SELECT/EXAMINE on same folder (it should not). * So we must unselect the folder to be able to get the APPENDLIMIT and UNSEEN. */ /* if ($this->IsSupported('ESEARCH')) { $aResult = $oFolderInfo->getStatusItems(); // SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message $aResult['UNSEEN'] = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT']; return $aResult; } */ $bWritable = $oFolderInfo->IsWritable; $bReselect = true; $this->FolderUnSelect(); } $aResult = $this->SendRequestGetResponse('STATUS', array($this->EscapeString($sFolderName), $aStatusItems)) ->getStatusFolderInformationResult(); if ($bReselect) { $this->selectOrExamineFolder($sFolderName, $bWritable, false); } return $aResult; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ private function specificFolderList(bool $bIsSubscribeList, string $sParentFolderName = '', string $sListPattern = '*', bool $bUseListStatus = false) : array { $sCmd = 'LIST'; $aParameters = array(); $aReturnParams = array(); if ($bIsSubscribeList) { // IMAP4rev2 deprecated $sCmd = 'LSUB'; } else if ($this->IsSupported('LIST-EXTENDED')) { // RFC 5258 $aReturnParams[] = 'SUBSCRIBED'; // $aReturnParams[] = 'CHILDREN'; if ($bIsSubscribeList) { $aParameters[] = ['SUBSCRIBED'/*,'REMOTE','RECURSIVEMATCH'*/]; } else { // $aParameters[0] = '()'; } } $aParameters[] = $this->EscapeString($sParentFolderName); $aParameters[] = $this->EscapeString(\strlen(\trim($sListPattern)) ? $sListPattern : '*'); // RFC 5819 if ($bUseListStatus && !$bIsSubscribeList && $this->IsSupported('LIST-STATUS')) { $aL = array( Enumerations\FolderStatus::MESSAGES, Enumerations\FolderStatus::UNSEEN, Enumerations\FolderStatus::UIDNEXT ); // RFC 4551 if ($this->IsSupported('CONDSTORE')) { $aL[] = Enumerations\FolderStatus::HIGHESTMODSEQ; } // RFC 7889 if ($this->IsSupported('APPENDLIMIT')) { $aL[] = Enumerations\FolderStatus::APPENDLIMIT; } // RFC 8474 if ($this->IsSupported('OBJECTID')) { $aTypes[] = Enumerations\FolderStatus::MAILBOXID; } $aReturnParams[] = 'STATUS'; $aReturnParams[] = $aL; } if ($aReturnParams) { $aParameters[] = 'RETURN'; $aParameters[] = $aReturnParams; } $aReturn = $this->SendRequestGetResponse($sCmd, $aParameters)->getFoldersResult($sCmd); // RFC 5464 if (!$bIsSubscribeList && $this->IsSupported('METADATA')) { foreach ($aReturn as $oFolder) { try { foreach ($this->getMetadata($oFolder->FullNameRaw(), ['/shared', '/private'], ['DEPTH'=>'infinity']) as $key => $value) { $oFolder->SetMetadata($key, $value); } } catch (\Throwable $e) { // Ignore error } } } return $aReturn; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderList(string $sParentFolderName = '', string $sListPattern = '*') : array { return $this->specificFolderList(false, $sParentFolderName, $sListPattern); } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderSubscribeList(string $sParentFolderName = '', string $sListPattern = '*') : array { return $this->specificFolderList(true, $sParentFolderName, $sListPattern); } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderStatusList(string $sParentFolderName = '', string $sListPattern = '*') : array { return $this->specificFolderList(false, $sParentFolderName, $sListPattern, true); } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderHierarchyDelimiter(string $sFolderName = '') : ?string { $oResponse = $this->SendRequestGetResponse('LIST', ['""', $this->EscapeString($sParentFolderName)]); return ('LIST' === $oResponse[0]->ResponseList[1]) ? $oResponse[0]->ResponseList[3] : null; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bReSelectSameFolders) : self { if (!$bReSelectSameFolders) { if ($this->oCurrentFolderInfo && $sFolderName === $this->oCurrentFolderInfo->FolderName && $bIsWritable === $this->oCurrentFolderInfo->IsWritable) { return $this; } } if (!\strlen(\trim($sFolderName))) { throw new \MailSo\Base\Exceptions\InvalidArgumentException; } $aParams = array($this->EscapeString($sFolderName)); /* if ($this->IsSupported('CONDSTORE')) { $aParams[] = ['CONDSTORE']; } */ /** * IMAP4rev2 SELECT/EXAMINE are now required to return an untagged LIST response. */ $this->oCurrentFolderInfo = $this->SendRequestGetResponse($bIsWritable ? 'SELECT' : 'EXAMINE', $aParams) ->getCurrentFolderInformation($sFolderName, $bIsWritable); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderSelect(string $sFolderName, bool $bReSelectSameFolders = false) : self { return $this->selectOrExamineFolder($sFolderName, true, $bReSelectSameFolders); } /** * The EXAMINE command is identical to SELECT and returns the same output; * however, the selected mailbox is identified as read-only. * No changes to the permanent state of the mailbox, including per-user state, * are permitted; in particular, EXAMINE MUST NOT cause messages to lose the \Recent flag. * * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderExamine(string $sFolderName, bool $bReSelectSameFolders = false) : self { return $this->selectOrExamineFolder($sFolderName, $this->__FORCE_SELECT_ON_EXAMINE__, $bReSelectSameFolders); } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderCheck() : self { if ($this->IsSelected()) { $this->SendRequestGetResponse('CHECK'); } return $this; } /** * This also expunge the mailbox * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderClose() : self { if ($this->IsSelected()) { $this->SendRequestGetResponse('CLOSE'); $this->oCurrentFolderInfo = null; } return $this; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function FolderUnSelect() : self { if ($this->IsSelected()) { if ($this->IsSupported('UNSELECT')) { $this->SendRequestGetResponse('UNSELECT'); $this->oCurrentFolderInfo = null; } else { try { $this->SendRequestGetResponse('SELECT', '""'); // * OK [CLOSED] Previous mailbox closed. // 3 NO [CANNOT] Invalid mailbox name: Name is empty } catch (Exceptions\NegativeResponseException $e) { if ('NO' === $e->GetResponseStatus()) { $this->oCurrentFolderInfo = null; } } } } return $this; } /** * @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 \MailSo\Base\Exceptions\InvalidArgumentException, \MailSo\Log\Enumerations\Type::ERROR, true); } $this->aFetchCallbacks = array(); try { $aFetchItems = Enumerations\FetchType::ChangeFetchItemsBefourRequest($aInputFetchItems); foreach ($aFetchItems as $sName => $mItem) { if (\strlen($sName) && '' !== $mItem) { $this->aFetchCallbacks[$sName] = $mItem; } } $aParams = array($sIndexRange, \array_keys($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 */ $oResult = $this->SendRequestGetResponse($bIndexIsUid ? 'UID FETCH' : 'FETCH', $aParams); } finally { $this->aFetchCallbacks = array(); } return $oResult->getFetchResult($this->oLogger); } /** * 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 Requests\SORT($this); $oSort->sCriterias = $sSearchCriterias; $oSort->bUid = $bReturnUid; $oSort->aSortTypes = $aSortTypes; return $oSort->SendRequestGetResponse() ->getMessageSimpleSortResult($bReturnUid); } /** * @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 Requests\ESEARCH($this); $oESearch->sCriterias = $sSearchCriterias; $oESearch->aReturn = $aSearchReturn; $oESearch->bUid = $bReturnUid; $oESearch->sLimit = $sLimit; $oESearch->sCharset = $sCharset; return $oESearch->SendRequestGetResponse() ->getSimpleESearchOrESortResult($this->getCurrentTag(), $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 Requests\SORT($this); $oSort->sCriterias = $sSearchCriterias; $oSort->bUid = $bReturnUid; $oSort->aSortTypes = $aSortTypes; $oSort->aReturn = $aSearchReturn ?: ['ALL']; $oSort->sLimit = $sLimit; return $oSort->SendRequestGetResponse() ->getSimpleESearchOrESortResult($this->getCurrentTag(), $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 && Enumerations\ResponseType::CONTINUATION === $oItem->ResponseType) { $aParts = explode("\r\n", $sCont); foreach ($aParts as $sLine) { $this->sendRaw($sLine); $oResult = $this->getResponse(); $oItem = $oResult->getLast(); if ($oItem && Enumerations\ResponseType::CONTINUATION === $oItem->ResponseType) { continue; } } } } return $oResult->getMessageSimpleSearchResult($bReturnUid); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageSimpleThread(string $sSearchCriterias = 'ALL', bool $bReturnUid = true, string $sCharset = \MailSo\Base\Enumerations\Charset::UTF_8) : array { $oThread = new Requests\THREAD($this); $oThread->sCriterias = $sSearchCriterias; $oThread->sCharset = $sCharset; $oThread->bUid = $bReturnUid; return $oThread->SendRequestGetResponse() ->getMessageSimpleThreadResult($bReturnUid); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageCopy(string $sToFolder, string $sIndexRange, bool $bIndexIsUid) : self { if (!\strlen($sIndexRange)) { $this->writeLogException( new \MailSo\Base\Exceptions\InvalidArgumentException, \MailSo\Log\Enumerations\Type::ERROR, true); } $this->SendRequestGetResponse($bIndexIsUid ? 'UID COPY' : 'COPY', array($sIndexRange, $this->EscapeString($sToFolder))); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageMove(string $sToFolder, string $sIndexRange, bool $bIndexIsUid) : self { if (!\strlen($sIndexRange)) { $this->writeLogException( new \MailSo\Base\Exceptions\InvalidArgumentException, \MailSo\Log\Enumerations\Type::ERROR, true); } if (!$this->IsSupported('MOVE')) { $this->writeLogException( new Exceptions\RuntimeException('Move is not supported'), \MailSo\Log\Enumerations\Type::ERROR, true); } $this->SendRequestGetResponse($bIndexIsUid ? 'UID MOVE' : 'MOVE', array($sIndexRange, $this->EscapeString($sToFolder))); return $this; } /** * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageExpunge(string $sUidRangeIfSupported = '', bool $bForceUidExpunge = false, bool $bExpungeAll = false) : self { $sUidRangeIfSupported = \trim($sUidRangeIfSupported); $sCmd = 'EXPUNGE'; $aArguments = array(); if (!$bExpungeAll && $bForceUidExpunge && \strlen($sUidRangeIfSupported) && $this->IsSupported('UIDPLUS')) { $sCmd = 'UID '.$sCmd; $aArguments = array($sUidRangeIfSupported); } $this->SendRequestGetResponse($sCmd, $aArguments); return $this; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function MessageStoreFlag(string $sIndexRange, bool $bIndexIsUid, array $aInputStoreItems, string $sStoreAction) : self { if (!\strlen(\trim($sIndexRange)) || !\strlen(\trim($sStoreAction)) || !\count($aInputStoreItems)) { return false; } /** * TODO: * https://datatracker.ietf.org/doc/html/rfc4551#section-3.2 * $sStoreAction[] = (UNCHANGEDSINCE $modsequence) */ $this->SendRequestGetResponse( $bIndexIsUid ? 'UID STORE' : 'STORE', array($sIndexRange, $sStoreAction, $aInputStoreItems) ); return $this; } /** * @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 $aAppendFlags = null, int &$iUid = null, int $iDateTime = 0) : self { $aData = array($this->EscapeString($sFolderName), $aAppendFlags); if (0 < $iDateTime) { $aData[] = $this->EscapeString(\gmdate('d-M-Y H:i:s', $iDateTime).' +0000'); } $aData[] = '{'.$iStreamSize.'}'; $this->SendRequestGetResponse('APPEND', $aData); $this->writeLog('Write to connection stream', \MailSo\Log\Enumerations\Type::NOTE); \MailSo\Base\Utils::MultipleStreamWriter($rMessageAppendStream, array($this->ConnectionResource())); $this->sendRaw(''); $oResponse = $this->getResponse(); if (null !== $iUid) { $oLast = $oResponse->getLast(); if ($oLast && Enumerations\ResponseType::TAGGED === $oLast->ResponseType && \is_array($oLast->OptionalResponse)) { if (\strlen($oLast->OptionalResponse[0]) && \strlen($oLast->OptionalResponse[2]) && 'APPENDUID' === strtoupper($oLast->OptionalResponse[0]) && \is_numeric($oLast->OptionalResponse[2]) ) { $iUid = (int) $oLast->OptionalResponse[2]; } } } return $this; } public function FolderCurrentInformation() : ?FolderInformation { return $this->oCurrentFolderInfo; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception */ public function SendRequest(string $sCommand, array $aParams = array(), bool $bBreakOnLiteral = false) : string { $sCommand = \trim($sCommand); if (!\strlen($sCommand)) { $this->writeLogException( new \MailSo\Base\Exceptions\InvalidArgumentException, \MailSo\Log\Enumerations\Type::ERROR, true); } $this->IsConnected(true); $sTag = $this->getNewTag(); $sRealCommand = $sTag.' '.$sCommand.$this->prepareParamLine($aParams); $sFakeCommand = ''; if ($aFakeParams = $this->secureRequestParams($sCommand, $aParams)) { $sFakeCommand = $sTag.' '.$sCommand.$this->prepareParamLine($aFakeParams); } // $this->lastCommand = $sFakeCommand ?: $sRealCommand; $this->aTagTimeouts[$sTag] = \microtime(true); if ($bBreakOnLiteral && !\preg_match('/\d\+\}\r\n/', $sRealCommand)) { $iPos = \strpos($sRealCommand, "}\r\n"); if (false !== $iPos) { $iFakePos = \strpos($sFakeCommand, "}\r\n"); $this->sendRaw(\substr($sRealCommand, 0, $iPos + 1), true, false !== $iFakePos ? \substr($sFakeCommand, 0, $iFakePos + 3) : ''); return \substr($sRealCommand, $iPos + 3); } } $this->sendRaw($sRealCommand, true, $sFakeCommand); return ''; } private function secureRequestParams(string $sCommand, array $aParams) : ?array { if ('LOGIN' === $sCommand && isset($aParams[1])) { $aParams[1] = '"********"'; return $aParams; } return null; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ public function SendRequestGetResponse(string $sCommand, array $aParams = array()) : ResponseCollection { $this->SendRequest($sCommand, $aParams); return $this->getResponse(); } private function getResponseValue(ResponseCollection $oResponseCollection, int $type = 0) : string { $oResponse = $oResponseCollection->getLast(); if ($oResponse && (!$type || $type === $oResponse->ResponseType)) { $sResult = $oResponse->ResponseList[1] ?? null; if ($sResult) { return $sResult; } $this->writeLogException( new Exceptions\LoginException, \MailSo\Log\Enumerations\Type::NOTICE, true); } $this->writeLogException( new Exceptions\LoginException, \MailSo\Log\Enumerations\Type::NOTICE, true); } /** * @throws \MailSo\Imap\Exceptions\ResponseNotFoundException * @throws \MailSo\Imap\Exceptions\InvalidResponseException * @throws \MailSo\Imap\Exceptions\NegativeResponseException */ private function getResponse(string $sEndTag = null) : ResponseCollection { try { $oResult = new ResponseCollection; if (\is_resource($this->ConnectionResource())) { $sEndTag = $sEndTag ?: $this->getCurrentTag(); while (true) { $oResponse = $this->partialParseResponse(); $oResult->append($oResponse); // RFC 5530 if ($sEndTag === $oResponse->Tag && \is_array($oResponse->OptionalResponse) && 'CLIENTBUG' === $oResponse->OptionalResponse[0]) { // The server has detected a client bug. // \error_log("IMAP {$oResponse->OptionalResponse[0]}: {$this->lastCommand}"); } if ($sEndTag === $oResponse->Tag || Enumerations\ResponseType::CONTINUATION === $oResponse->ResponseType) { if (isset($this->aTagTimeouts[$sEndTag])) { $this->writeLog((\microtime(true) - $this->aTagTimeouts[$sEndTag]).' ('.$sEndTag.')', \MailSo\Log\Enumerations\Type::TIME); unset($this->aTagTimeouts[$sEndTag]); } break; } } } $oResult->validate(); } catch (\Throwable $e) { $this->writeLogException($e, \MailSo\Log\Enumerations\Type::WARNING); throw $e; } return $oResult; } private function prepareParamLine(array $aParams = array()) : string { $sReturn = ''; foreach ($aParams as $mParamItem) { if (\is_array($mParamItem) && \count($mParamItem)) { $sReturn .= ' ('.\trim($this->prepareParamLine($mParamItem)).')'; } else if (\is_string($mParamItem)) { $sReturn .= ' '.$mParamItem; } } return $sReturn; } private function getNewTag() : string { ++$this->iTagCount; return $this->getCurrentTag(); } private function getCurrentTag() : string { return self::TAG_PREFIX.$this->iTagCount; } public function EscapeString(?string $sStringForEscape) : string { if (null === $sStringForEscape) { return 'NIL'; } /* // literal-string if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) { return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape); } */ // quoted-string return '"' . \addcslashes($sStringForEscape, '\\"') . '"'; } public function getLogName() : string { return 'IMAP'; } /** * RFC 2971 * Don't have to be logged in to call this command */ public function ServerID() : string { if ($this->IsSupported('ID')) { foreach ($this->SendRequestGetResponse('ID', [null]) as $oResponse) { if ('ID' === $oResponse->ResponseList[1] && \is_array($oResponse->ResponseList[2])) { $c = \count($oResponse->ResponseList[2]); $aResult = []; for ($i = 0; $i < $c; $i += 2) { $aResult[] = $oResponse->ResponseList[2][$i] . '=' . $oResponse->ResponseList[2][$i+1]; } return \implode(' ', $aResult); } } } return 'UNKNOWN'; } }