diff --git a/psalm.baseline.xml b/psalm.baseline.xml index f20869f2..2bcc9e83 100644 --- a/psalm.baseline.xml +++ b/psalm.baseline.xml @@ -27,91 +27,22 @@ \is_int($retriesNum) \is_array($params) - - $dataInfo - $attachment - $dataInfo - $attachment - "imap_$methodShortName" - - - $imapStream - - - object - $mail->id $mail->id $option && FT_PREFETCHTEXT - - new $throwExceptionClass("IMAP method imap_$methodShortName() failed with error: ".implode('. ', $errors)) - - - throw new $throwExceptionClass("IMAP method imap_$methodShortName() failed with error: ".implode('. ', $errors)); - - - string - - - $delimiter - $serverEncoding - $imapSearchOption - $maxAttempts - $milliseconds + $str $str $mailId $mailId $mailId - $prefix - $index - $fullPrefix - $partStructure - $markAsSeen - $emlParse - $toCharset - $string - $string - $charset - - $imapPath - $imapLogin - $imapPassword - $imapOAuthAccessToken - $imapSearchOption - $connectionRetry - $connectionRetryDelay - $imapOptions - $imapRetriesNum - $imapParams - $serverEncoding - $expungeOnDisconnect - $timeouts - $attachmentsIgnore - $pathDelimiter - $imapStream - - - initImapStreamWithRetry + initMailPart - isUrlEncoded - decodeRFC2231 - imap - - $this->imapStream - $serverEncoding - $imapSearchOption - $this->connectionRetryDelay * 1000 - $this->imapPath - $this->imapLogin - $this->imapPassword - $this->imapOptions - $this->imapRetriesNum - $this->imapParams + $mail->subject $mail->subject $mail->from @@ -155,10 +86,11 @@ $param->attribute $param->attribute preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute - $partStructure $partStructure->subtype - $partStructure - $emlParse + $subPartStructure + $subPartStructure + $subPartStructure + $subPartStructure $partStructure->subtype $partStructure->disposition $partStructure->subtype @@ -172,22 +104,7 @@ $partStructure->id $element->charset $element->text - $fromCharset - $toCharset - $string - $string - $string - $charset - $this->imapPath - $this->imapPath - $this->imapPath - $this->imapPath - $this->imapPath - - $types - $args - $head->from[0] $head->from[1] @@ -200,20 +117,13 @@ $head->sender[1] $head->sender[0] - - $mailbox_info + $value - $retry - $timeout - $folders - $folder - $mails $mail $to $cc $bcc $replyTo - $mailStructure $param $param $params[$paramName] @@ -221,47 +131,17 @@ $fileName $fileName $fileName - $fileName $element - $fromCharset - $toCharset $item - $item - $arg - $result - - string - string - string - string - bool + string string - resource|null - string - string - stdClass - stdClass - array - array - array - array - object - array - int - array int - int - string - IncomingMailAttachment[] - string - string + int|false + IncomingMailAttachment - - $retry - $this->connectionRetryDelay - $this->imapPath + $head->from[0]->mailbox $head->sender[0]->mailbox $to->mailbox @@ -272,18 +152,11 @@ $bcc->host $replyTo->mailbox $replyTo->host - $prefix - $prefix - $prefix - $prefix - $index $params[$paramName] $subPartNum $subPartNum $fileName $fileExt - $this->imapPath - $this->imapPath $mail @@ -291,7 +164,7 @@ $mail $mail - + $mail->subject $mail->from $mail->sender @@ -322,40 +195,13 @@ $replyTo->mailbox $replyTo->host $replyTo->personal - $mailStructure->parts - $mailStructure->parts - $partStructure->encoding - $partStructure->parameters - $partStructure->parameters $param->value $param->value $param->attribute - $partStructure->dparameters - $partStructure->dparameters $param->attribute $param->attribute $param->value $param->value - $partStructure->type - $partStructure->subtype - $partStructure->disposition - $partStructure->type - $partStructure->subtype - $partStructure->parts - $partStructure->parts - $partStructure->type - $partStructure->subtype - $partStructure->disposition - $partStructure->type - $partStructure->subtype - $partStructure->disposition - $partStructure->subtype - $partStructure->disposition - $partStructure->type - $partStructure->subtype - $partStructure->ifdisposition - $partStructure->disposition - $partStructure->type $element->text $element->charset $element->charset @@ -364,48 +210,19 @@ $item->name $item->attributes $item->delimiter - $item->name - $item->attributes - $item->delimiter - - $this->imapOAuthAccessToken - $this->pathDelimiter - $this->serverEncoding - $this->imapSearchOption - $this->attachmentsIgnore - $this->imapLogin - $this->imapStream + $str $str - $this->imap('check') - $this->imap('status', [$this->imapPath, SA_ALL]) - $folders - $this->imap('search', [$criteria, $this->imapSearchOption]) ?: [] - $this->imap('search', [$criteria, $this->imapSearchOption, $this->getServerEncoding()]) ?: [] - $mails - $this->imap('headers') - $this->imap('mailboxmsginfo') - $this->imap('sort', [$criteria, $reverse, $this->imapSearchOption, $searchCriteria]) - $this->imap('num_msg') - $this->imap('get_quotaroot', $quota_root) isset($quota['STORAGE']['limit']) ? $quota['STORAGE']['limit'] : 0 isset($quota['STORAGE']['usage']) ? $quota['STORAGE']['usage'] : 0 - $this->imap('fetchbody', [$msgId, '', $options]) - $this->imapPath - $this->imapPath - - strpos($name, '}') - strpos($name, '}') - $posConnectionDefinitionEnd - - - $this->getImapStream() - $this->getImapStream() - $this->getImapStream() - $this->getImapStream() - + + $mailStructure + + + $mailStructure->parts + $ccStrings $bccStrings @@ -454,17 +271,19 @@ subscribeMailbox unsubscribeMailbox - + + $dataInfo $mailId $emlOrigin - + !\is_int($retriesNum) or $retriesNum < 0 \is_int($retriesNum) null != $params and !empty($params) - \is_resource($imapStream) - $imapStream && \is_resource($imapStream) + + list<scalar|array|object|resource|null>|string + $value diff --git a/src/PhpImap/Mailbox.php b/src/PhpImap/Mailbox.php index 4ce883e4..9e3e3094 100644 --- a/src/PhpImap/Mailbox.php +++ b/src/PhpImap/Mailbox.php @@ -19,23 +19,59 @@ */ class Mailbox { + /** @var string */ protected $imapPath; + + /** @var string */ protected $imapLogin; + + /** @var string */ protected $imapPassword; + + /** @var string|null */ protected $imapOAuthAccessToken = null; + + /** @var int */ protected $imapSearchOption = SE_UID; + + /** @var int */ protected $connectionRetry = 0; + + /** @var int */ protected $connectionRetryDelay = 100; + + /** @var int */ protected $imapOptions = 0; + + /** @var int */ protected $imapRetriesNum = 0; + + /** @var array */ protected $imapParams = []; + + /** @var string */ protected $serverEncoding = 'UTF-8'; + /** @var string|null */ protected $attachmentsDir = null; + + /** @var bool */ protected $expungeOnDisconnect = true; + + /** + * @var int[] + * + * @psalm-var array{1?:int, 2?:int, 3?:int, 4?:int} + */ protected $timeouts = []; + + /** @var bool */ protected $attachmentsIgnore = false; + + /** @var string */ protected $pathDelimiter = '.'; + + /** @var resource|null */ private $imapStream; /** @@ -79,7 +115,7 @@ protected function _oauthAuthentication() { $oauth_command = 'A AUTHENTICATE XOAUTH2 '.$this->_constructAuthString(); - $oauth_result = fwrite($this->imapStream, $oauth_command); + $oauth_result = fwrite($this->getImapStream(), $oauth_command); if (false === $oauth_result) { throw new Exception('Could not authenticate using OAuth!'); @@ -124,7 +160,7 @@ public function setOAuthToken($access_token) /** * Gets the OAuth Token for the authentication. * - * @return string $access_token OAuth Access Token + * @return string|null $access_token OAuth Access Token */ public function getOAuthToken() { @@ -160,7 +196,7 @@ public function getPathDelimiter() /** * Validates the given path delimiter character. * - * @param string Path delimiter + * @param string $delimiter Path delimiter * * @return bool true (supported) or false (unsupported) */ @@ -188,7 +224,7 @@ public function getServerEncoding() /** * Sets / Changes the server encoding. * - * @param string Server encoding (eg. 'UTF-8') + * @param string $serverEncoding Server encoding (eg. 'UTF-8') * * @throws InvalidParameterException */ @@ -208,7 +244,7 @@ public function setServerEncoding($serverEncoding) /** * Returns the current set IMAP search option. * - * @return string IMAP search option (eg. 'SE_UID') + * @return int IMAP search option (eg. 'SE_UID') */ public function getImapSearchOption() { @@ -218,17 +254,17 @@ public function getImapSearchOption() /** * Sets / Changes the IMAP search option. * - * @param string IMAP search option (eg. 'SE_UID') + * @param int $imapSearchOption IMAP search option (eg. 'SE_UID') + * + * @psalm-param 1|2 $imapSearchOptions * * @throws InvalidParameterException */ public function setImapSearchOption($imapSearchOption) { - $imapSearchOption = strtoupper(trim($imapSearchOption)); - $supported_options = [SE_FREE, SE_UID]; - if (!\in_array($imapSearchOption, $supported_options)) { + if (!\in_array($imapSearchOption, $supported_options, true)) { throw new InvalidParameterException('"'.$imapSearchOption.'" is not supported by setImapSearchOption(). Supported options are SE_FREE and SE_UID.'); } @@ -269,6 +305,8 @@ public function getAttachmentsIgnore() * @param int $timeout Timeout in seconds * @param array $types One of the following: IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT * + * @psalm-param list<1|2|3|4> $types + * * @throws InvalidParameterException */ public function setTimeouts($timeout, $types = [IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT]) @@ -281,6 +319,7 @@ public function setTimeouts($timeout, $types = [IMAP_OPENTIMEOUT, IMAP_READTIMEO throw new InvalidParameterException('You have provided at least one unsupported timeout type. Supported types are: IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT'); } + /** @var array{1?:int, 2?:int, 3?:int, 4?:int} */ $this->timeouts = array_fill_keys($types, $timeout); } @@ -367,7 +406,7 @@ public function getAttachmentsDir() return $this->attachmentsDir; } - /* + /** * Sets / Changes the attempts / retries to connect * @param int $maxAttempts * @return void @@ -377,7 +416,7 @@ public function setConnectionRetry($maxAttempts) $this->connectionRetry = $maxAttempts; } - /* + /** * Sets / Changes the delay between each attempt / retry to connect * @param int $milliseconds * @return void @@ -392,7 +431,7 @@ public function setConnectionRetryDelay($milliseconds) * * @param bool $forceConnection Initialize connection if it's not initialized * - * @return resource|null + * @return resource */ public function getImapStream($forceConnection = true) { @@ -403,9 +442,15 @@ public function getImapStream($forceConnection = true) } } + /** @var resource */ return $this->imapStream; } + public function hasImapStream() : bool + { + return \is_resource($this->imapStream) && imap_ping($this->imapStream); + } + /** * Returns the provided string in UTF7-IMAP encoded format. * @@ -454,6 +499,7 @@ public function switchMailbox($imapPath) $this->imap('reopen', $this->imapPath); } + /** @return resource */ protected function initImapStreamWithRetry() { $retry = $this->connectionRetry; @@ -471,7 +517,7 @@ protected function initImapStreamWithRetry() /** * Open an IMAP stream to a mailbox. * - * @return object IMAP stream on success + * @return resource IMAP stream on success * * @throws Exception if an error occured */ @@ -507,9 +553,8 @@ protected function initImapStream() */ public function disconnect() { - $imapStream = $this->getImapStream(false); - if ($imapStream && \is_resource($imapStream)) { - $this->imap('close', [$imapStream, $this->expungeOnDisconnect ? CL_EXPUNGE : 0], false, null); + if ($this->hasImapStream()) { + $this->imap('close', [$this->getImapStream(false), $this->expungeOnDisconnect ? CL_EXPUNGE : 0], false, null); } } @@ -539,6 +584,7 @@ public function setExpungeOnDisconnect($isEnabled) */ public function checkMailbox() { + /** @var stdClass */ return $this->imap('check'); } @@ -559,11 +605,14 @@ public function createMailbox($name) * * @param string $name Name of mailbox, which you want to delete (eg. 'PhpImap') * + * @return bool + * * @see imap_deletemailbox() */ public function deleteMailbox($name) { - $this->imap('deletemailbox', $this->getCombinedPath($name)); + /** @var bool */ + return $this->imap('deletemailbox', $this->getCombinedPath($name)); } /** @@ -587,6 +636,7 @@ public function renameMailbox($oldName, $newName) */ public function statusMailbox() { + /** @var stdClass */ return $this->imap('status', [$this->imapPath, SA_ALL]); } @@ -602,12 +652,10 @@ public function statusMailbox() */ public function getListingFolders($pattern = '*') { + /** @var string[] */ $folders = $this->imap('list', [$this->imapPath, $pattern]) ?: []; - foreach ($folders as &$folder) { - $folder = $this->decodeStringFromUtf7ImapToUtf8($folder); - } - return $folders; + return array_map([$this, 'decodeStringFromUtf7ImapToUtf8'], $folders); } /** @@ -622,9 +670,11 @@ public function getListingFolders($pattern = '*') public function searchMailbox($criteria = 'ALL', $disableServerEncoding = false) { if ($disableServerEncoding) { + /** @var array */ return $this->imap('search', [$criteria, $this->imapSearchOption]) ?: []; } + /** @var array */ return $this->imap('search', [$criteria, $this->imapSearchOption, $this->getServerEncoding()]) ?: []; } @@ -811,6 +861,7 @@ public function getMailsInfo(array $mailsIds) } } + /** @var array */ return $mails; } @@ -825,6 +876,7 @@ public function getMailsInfo(array $mailsIds) */ public function getMailboxHeaders() { + /** @var array */ return $this->imap('headers'); } @@ -847,6 +899,7 @@ public function getMailboxHeaders() */ public function getMailboxInfo() { + /** @var object */ return $this->imap('mailboxmsginfo'); } @@ -870,6 +923,7 @@ public function getMailboxInfo() */ public function sortMails($criteria = SORTARRIVAL, $reverse = true, $searchCriteria = 'ALL') { + /** @var array */ return $this->imap('sort', [$criteria, $reverse, $this->imapSearchOption, $searchCriteria]); } @@ -882,6 +936,7 @@ public function sortMails($criteria = SORTARRIVAL, $reverse = true, $searchCrite */ public function countMails() { + /** @var int */ return $this->imap('num_msg'); } @@ -896,6 +951,7 @@ public function countMails() */ protected function getQuota($quota_root = 'INBOX') { + /** @var array */ return $this->imap('get_quotaroot', $quota_root); } @@ -918,7 +974,7 @@ public function getQuotaLimit($quota_root = 'INBOX') * * @param string $quota_root Should normally be in the form of which mailbox (i.e. INBOX) * - * @return int FALSE in the case of call failure + * @return int|false FALSE in the case of call failure */ public function getQuotaUsage($quota_root = 'INBOX') { @@ -942,6 +998,7 @@ public function getRawMail($msgId, $markAsSeen = true) $options |= FT_PEEK; } + /** @var string */ return $this->imap('fetchbody', [$msgId, '', $options]); } @@ -1050,6 +1107,9 @@ public function getMailHeader($mailId) * * @param \stdClass[] $messageParts * @param \stdClass[] $flattenedParts + * @param string $prefix + * @param int $index + * @param bool $fullPrefix * * @psalm-param array $flattenedParts * @@ -1111,7 +1171,12 @@ public function getMail($mailId, $markAsSeen = true) } /** + * @param object $partStructure * @param string|int $partNum + * @param bool $markAsSeen + * @param bool $emlParse + * + * @todo flesh out shape of $partStructure */ protected function initMailPart(IncomingMail $mail, $partStructure, $partNum, $markAsSeen = true, $emlParse = false) { @@ -1218,13 +1283,13 @@ protected function initMailPart(IncomingMail $mail, $partStructure, $partNum, $m /** * Download attachment. * - * @param string $dataInfo + * @param DataPartInfo $dataInfo * @param array $params Array of params of mail * @param object $partStructure Part of mail * @param int $mailId ID of mail * @param bool $emlOrigin True, if it indicates, that the attachment comes from an EML (mail) file * - * @return IncomingMailAttachment[] $attachment + * @return IncomingMailAttachment $attachment */ public function downloadAttachment(DataPartInfo $dataInfo, $params, $partStructure, $mailId, $emlOrigin = false) { @@ -1275,6 +1340,7 @@ public function downloadAttachment(DataPartInfo $dataInfo, $params, $partStructu * Decodes a mime string. * * @param string $string MIME string to decode + * @param string $toCharset * * @return string Converted string if conversion was successful, or the original string if not * @@ -1289,6 +1355,7 @@ public function decodeMimeStr($string, $toCharset = 'utf-8') $newString = ''; foreach (imap_mime_header_decode($string) as $element) { if (isset($element->text)) { + /** @var string */ $fromCharset = !isset($element->charset) ? 'iso-8859-1' : $element->charset; // Convert to UTF-8, if string has UTF-8 characters to avoid broken strings. See https://github.com/barbushin/php-imap/issues/232 $toCharset = isset($element->charset) && preg_match('/(UTF\-8)|(default)/i', $element->charset) ? 'UTF-8' : $toCharset; @@ -1299,6 +1366,11 @@ public function decodeMimeStr($string, $toCharset = 'utf-8') return $newString; } + /** + * @param string $string + * + * @return bool + */ public function isUrlEncoded($string) { $hasInvalidChars = preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $string); @@ -1307,6 +1379,12 @@ public function isUrlEncoded($string) return !$hasInvalidChars && $hasEscapedChars; } + /** + * @param string $string + * @param string $charset + * + * @return string + */ protected function decodeRFC2231($string, $charset = 'utf-8') { if (preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) { @@ -1450,6 +1528,8 @@ public function subscribeMailbox($mailbox) * * @param string $mailbox * + * @return void + * * @throws Exception */ public function unsubscribeMailbox($mailbox) @@ -1465,10 +1545,19 @@ public function unsubscribeMailbox($mailbox) * @param bool $prependConnectionAsFirstArg Add 'resource $imap_stream' as first argument, if set to true * @param string|null $throwExceptionClass Name of exception class, which will be thrown in case of errors * + * @psalm-param list|string $args + * @psalm-param class-string|null $throwExceptionClass + * + * @return scalar|array|object|resource|null + * * @throws Exception */ public function imap($methodShortName, $args = [], $prependConnectionAsFirstArg = true, $throwExceptionClass = Exception::class) { + $method_name = 'imap_'.$methodShortName; + if (!\function_exists($method_name)) { + throw new InvalidArgumentException('Argument 1 passed to '.__METHOD__.'() did not correspond to a known imap_* function'); + } if (!\is_array($args)) { $args = [$args]; } @@ -1497,13 +1586,15 @@ public function imap($methodShortName, $args = [], $prependConnectionAsFirstArg } imap_errors(); // flush errors - $result = @\call_user_func_array("imap_$methodShortName", $args); + + /** @var scalar|array|object|resource|null */ + $result = @\call_user_func_array($method_name, $args); if (!$result) { $errors = imap_errors(); if ($errors) { if ($throwExceptionClass) { - throw new $throwExceptionClass("IMAP method imap_$methodShortName() failed with error: ".implode('. ', $errors)); + throw new $throwExceptionClass("IMAP method $method_name() failed with error: ".implode('. ', $errors)); } else { return false; } @@ -1533,6 +1624,10 @@ protected function getCombinedPath($folder, $absolute = false) $folder = ('/' === $folder) ? '' : $folder; $posConnectionDefinitionEnd = strpos($this->imapPath, '}'); + if (false === $posConnectionDefinitionEnd) { + throw new UnexpectedValueException('"}" was not present in IMAP path!'); + } + return substr($this->imapPath, 0, $posConnectionDefinitionEnd + 1).$folder; } diff --git a/tests/unit/MailboxTest.php b/tests/unit/MailboxTest.php index a59a1abc..b33d9df2 100644 --- a/tests/unit/MailboxTest.php +++ b/tests/unit/MailboxTest.php @@ -213,9 +213,6 @@ public function testSetAndGetImapSearchOption() $this->mailbox->setImapSearchOption(SE_FREE); $this->assertEquals($this->mailbox->getImapSearchOption(), 2); - $this->expectException(InvalidParameterException::class); - $this->mailbox->setImapSearchOption('SE_FREE'); - $this->expectException(InvalidParameterException::class); $this->mailbox->setImapSearchOption(ANYTHING); @@ -594,7 +591,7 @@ public function testMimeEncoding($str, $expected) /** * Provides test data for testing timeouts. * - * @psalm-return array}> + * @psalm-return array}> */ public function timeoutsProvider() { @@ -623,7 +620,7 @@ public function timeoutsProvider() * @param int[] $types * * @psalm-param 'assertNull'|'expectException' $assertMethod - * @psalm-param list $types + * @psalm-param list<1|2|3|4> $types * * @return void */