barbushin/php-imap

View on GitHub
src/PhpImap/Imap.php

Summary

Maintainability
F
5 days
Test Coverage
<?php
/**
 * @author Barbushin Sergey http://linkedin.com/in/barbushin
 * @author BAPCLTD-Marv
 */
declare(strict_types=1);

namespace PhpImap;

use const CL_EXPUNGE;
use const IMAP_CLOSETIMEOUT;
use const IMAP_OPENTIMEOUT;
use const IMAP_READTIMEOUT;
use const IMAP_WRITETIMEOUT;
use InvalidArgumentException;
use const NIL;
use const PHP_MAJOR_VERSION;
use PhpImap\Exceptions\ConnectionException;
use const SE_FREE;
use const SORTARRIVAL;
use const SORTCC;
use const SORTDATE;
use const SORTFROM;
use const SORTSIZE;
use const SORTSUBJECT;
use const SORTTO;
use stdClass;
use Throwable;
use UnexpectedValueException;

/**
 * @psalm-type PARTSTRUCTURE_PARAM = object{attribute:string, value?:string}
 * @psalm-type PARTSTRUCTURE = object{
 *  id?:string,
 *  encoding:int|mixed,
 *  partStructure:object[],
 *  parameters:PARTSTRUCTURE_PARAM[],
 *  dparameters:object{attribute:string, value:string}[],
 *  parts:array<int, object{disposition?:string}>,
 *  type:int,
 *  subtype:string
 * }
 */
final class Imap
{
    /** @psalm-var list<int> */
    public const SORT_CRITERIA = [
        SORTARRIVAL,
        SORTCC,
        SORTDATE,
        SORTFROM,
        SORTSIZE,
        SORTSUBJECT,
        SORTTO,
    ];

    /** @psalm-var list<int> */
    public const TIMEOUT_TYPES = [
        IMAP_CLOSETIMEOUT,
        IMAP_OPENTIMEOUT,
        IMAP_READTIMEOUT,
        IMAP_WRITETIMEOUT,
    ];

    /** @psalm-var list<int> */
    public const CLOSE_FLAGS = [
        0,
        CL_EXPUNGE,
    ];

    /**
     * @param resource|false $imap_stream
     *
     * @return true
     *
     * @see imap_append()
     */
    public static function append(
        $imap_stream,
        string $mailbox,
        string $message,
        string $options = null,
        string $internal_date = null
    ): bool {
        \imap_errors(); // flush errors

        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        if (null !== $options && null !== $internal_date) {
            $result = \imap_append(
                $imap_stream,
                $mailbox,
                $message,
                $options,
                $internal_date
            );
        } elseif (null !== $options) {
            $result = \imap_append($imap_stream, $mailbox, $message, $options);
        } else {
            $result = \imap_append($imap_stream, $mailbox, $message);
        }

        if (false === $result) {
            throw new UnexpectedValueException('Could not append message to mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_append'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function body(
        $imap_stream,
        int $msg_number,
        int $options = 0
    ): string {
        \imap_errors(); // flush errors

        $result = \imap_body(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $msg_number,
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch message body from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_body'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function check($imap_stream): object
    {
        \imap_errors(); // flush errors

        $result = \imap_check(self::EnsureConnection($imap_stream, __METHOD__, 1));

        if (false === $result) {
            throw new UnexpectedValueException('Could not check imap mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_check'));
        }

        /** @var object */
        return $result;
    }

    /**
     * @param false|resource $imap_stream
     * @param int|string     $sequence
     *
     * @return true
     */
    public static function clearflag_full(
        $imap_stream,
        $sequence,
        string $flag,
        int $options = 0
    ): bool {
        \imap_errors(); // flush errors

        $result = \imap_clearflag_full(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            self::encodeStringToUtf7Imap(static::EnsureRange(
                $sequence,
                __METHOD__,
                2,
                true
            )),
            self::encodeStringToUtf7Imap($flag),
            $options
        );

        if (!$result) {
            throw new UnexpectedValueException('Could not clear flag on messages!', 0, self::HandleErrors(\imap_errors(), 'imap_clearflag_full'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @psalm-param value-of<self::CLOSE_FLAGS> $flag
     *
     * @return true
     */
    public static function close($imap_stream, int $flag = 0): bool
    {
        \imap_errors(); // flush errors

        /** @var int */
        $flag = $flag;

        $result = \imap_close(self::EnsureConnection($imap_stream, __METHOD__, 1), $flag);

        if (false === $result) {
            $message = 'Could not close imap connection';

            if (CL_EXPUNGE === ($flag & CL_EXPUNGE)) {
                $message .= ', messages may not have been expunged';
            }

            $message .= '!';
            throw new UnexpectedValueException($message, 0, self::HandleErrors(\imap_errors(), 'imap_close'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return true
     */
    public static function createmailbox($imap_stream, string $mailbox): bool
    {
        \imap_errors(); // flush errors

        $result = \imap_createmailbox(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            static::encodeStringToUtf7Imap($mailbox)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not create mailbox!', 0, self::HandleErrors(\imap_errors(), 'createmailbox'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     * @param string|int     $msg_number
     *
     * @return true
     */
    public static function delete(
        $imap_stream,
        $msg_number,
        int $options = 0
    ): bool {
        /**
         * @var int
         *
         * @todo remove docblock pending resolution of https://github.com/vimeo/psalm/issues/2620
         */
        $msg_number = self::encodeStringToUtf7Imap(self::EnsureRange(
            $msg_number,
            __METHOD__,
            1
        ));

        \imap_errors(); // flush errors

        $result = \imap_delete(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $msg_number,
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not delete message from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_delete'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return true
     */
    public static function deletemailbox($imap_stream, string $mailbox): bool
    {
        \imap_errors(); // flush errors

        $result = \imap_deletemailbox(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            static::encodeStringToUtf7Imap($mailbox)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not delete mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_deletemailbox'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return true
     */
    public static function expunge($imap_stream): bool
    {
        \imap_errors(); // flush errors

        $result = \imap_expunge(
            self::EnsureConnection($imap_stream, __METHOD__, 1)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not expunge messages from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_expunge'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     * @param int|string     $sequence
     *
     * @return object[]
     *
     * @psalm-return list<object>
     */
    public static function fetch_overview(
        $imap_stream,
        $sequence,
        int $options = 0
    ): array {
        \imap_errors(); // flush errors

        $result = \imap_fetch_overview(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            self::encodeStringToUtf7Imap(self::EnsureRange(
                $sequence,
                __METHOD__,
                1,
                true
            )),
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch overview for message from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_fetch_overview'));
        }

        /** @psalm-var list<object> */
        return $result;
    }

    /**
     * @param false|resource $imap_stream
     * @param string|int     $section
     */
    public static function fetchbody(
        $imap_stream,
        int $msg_number,
        $section,
        int $options = 0
    ): string {
        if (!\is_string($section) && !\is_int($section)) {
            throw new InvalidArgumentException('Argument 3 passed to '.__METHOD__.'() must be a string or integer, '.\gettype($section).' given!');
        }

        \imap_errors(); // flush errors

        $result = \imap_fetchbody(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $msg_number,
            self::encodeStringToUtf7Imap((string) $section),
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch message body from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_fetchbody'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function fetchheader(
        $imap_stream,
        int $msg_number,
        int $options = 0
    ): string {
        \imap_errors(); // flush errors

        $result = \imap_fetchheader(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $msg_number,
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch message header from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_fetchheader'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @psalm-return PARTSTRUCTURE
     */
    public static function fetchstructure(
        $imap_stream,
        int $msg_number,
        int $options = 0
    ): object {
        \imap_errors(); // flush errors

        $result = \imap_fetchstructure(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $msg_number,
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch message structure from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_fetchstructure'));
        }

        /** @psalm-var PARTSTRUCTURE */
        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @todo add return array shape pending resolution of https://github.com/vimeo/psalm/issues/2620
     */
    public static function get_quotaroot(
        $imap_stream,
        string $quota_root
    ): array {
        \imap_errors(); // flush errors

        $result = \imap_get_quotaroot(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            self::encodeStringToUtf7Imap($quota_root)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not quota for mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_get_quotaroot'));
        }

        return $result;
    }

    /**
     * @param resource|false $imap_stream
     *
     * @return object[]
     *
     * @psalm-return list<object>
     */
    public static function getmailboxes(
        $imap_stream,
        string $ref,
        string $pattern
    ): array {
        \imap_errors(); // flush errors

        $result = \imap_getmailboxes(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $ref,
            $pattern
        );

        if (false === $result) {
            $errors = \imap_errors();

            if (false === $errors) {
                /*
                * if there were no errors then there were no mailboxes,
                *  rather than a failure to get mailboxes.
                */
                return [];
            }

            throw new UnexpectedValueException('Call to imap_getmailboxes() with supplied arguments returned false, not array!', 0, self::HandleErrors(\imap_errors(), 'imap_getmailboxes'));
        }

        /** @psalm-var list<object> */
        return $result;
    }

    /**
     * @param resource|false $imap_stream
     *
     * @return object[]
     *
     * @psalm-return list<object>
     */
    public static function getsubscribed(
        $imap_stream,
        string $ref,
        string $pattern
    ): array {
        \imap_errors(); // flush errors

        $result = \imap_getsubscribed(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            $ref,
            $pattern
        );

        if (false === $result) {
            throw new UnexpectedValueException('Call to imap_getsubscribed() with supplied arguments returned false, not array!', 0, self::HandleErrors(\imap_errors(), 'imap_getsubscribed'));
        }

        /** @psalm-var list<object> */
        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function headers($imap_stream): array
    {
        \imap_errors(); // flush errors

        $result = \imap_headers(
            self::EnsureConnection($imap_stream, __METHOD__, 1)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch headers from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_headers'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return string[]
     *
     * @psalm-return list<string>
     */
    public static function listOfMailboxes($imap_stream, string $ref, string $pattern): array
    {
        \imap_errors(); // flush errors

        $result = \imap_list(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            static::encodeStringToUtf7Imap($ref),
            static::encodeStringToUtf7Imap($pattern)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not list folders mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_list'));
        }

        return \array_values(\array_map(
            static function (string $folder): string {
                return static::decodeStringFromUtf7ImapToUtf8($folder);
            },
            $result
        ));
    }

    /**
     * @param mixed[] An associative array of headers fields
     * @param mixed[] An indexed array of bodies
     *
     * @psalm-param array{
     *    subject?:string
     * } $envelope An associative array of headers fields (docblock is not complete)
     * @psalm-param list<array{
     *    type?:int,
     *    encoding?:int,
     *    charset?:string,
     *    subtype?:string,
     *    description?:string,
     *    disposition?:array{filename:string}
     * }> $body An indexed array of bodies (docblock is not complete)
     *
     * @todo flesh out array shape pending resolution of https://github.com/vimeo/psalm/issues/1518
     *
     * @psalm-pure
     */
    public static function mail_compose(array $envelope, array $body): string
    {
        return \imap_mail_compose($envelope, $body);
    }

    /**
     * @param false|resource $imap_stream
     * @param int|string     $msglist
     *
     * @return true
     */
    public static function mail_copy(
        $imap_stream,
        $msglist,
        string $mailbox,
        int $options = 0
    ): bool {
        \imap_errors(); // flush errors

        $result = \imap_mail_copy(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            static::encodeStringToUtf7Imap(self::EnsureRange(
                $msglist,
                __METHOD__,
                2,
                true
            )),
            static::encodeStringToUtf7Imap($mailbox),
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not copy messages!', 0, self::HandleErrors(\imap_errors(), 'imap_mail_copy'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     * @param int|string     $msglist
     *
     * @return true
     */
    public static function mail_move(
        $imap_stream,
        $msglist,
        string $mailbox,
        int $options = 0
    ): bool {
        \imap_errors(); // flush errors

        $result = \imap_mail_move(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            static::encodeStringToUtf7Imap(self::EnsureRange(
                $msglist,
                __METHOD__,
                2,
                true
            )),
            static::encodeStringToUtf7Imap($mailbox),
            $options
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not move messages!', 0, self::HandleErrors(\imap_errors(), 'imap_mail_move'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function mailboxmsginfo($imap_stream): stdClass
    {
        \imap_errors(); // flush errors

        $result = \imap_mailboxmsginfo(
            self::EnsureConnection($imap_stream, __METHOD__, 1)
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not fetch mailboxmsginfo from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_mailboxmsginfo'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function num_msg($imap_stream): int
    {
        \imap_errors(); // flush errors

        $result = \imap_num_msg(self::EnsureConnection($imap_stream, __METHOD__, 1));

        if (false === $result) {
            throw new UnexpectedValueException('Could not get the number of messages in the mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_num_msg'));
        }

        return $result;
    }

    /**
     * @psalm-param array{DISABLE_AUTHENTICATOR:string}|array<empty, empty> $params
     *
     * @return resource
     */
    public static function open(
        string $mailbox,
        string $username,
        string $password,
        int $options = 0,
        int $n_retries = 0,
        array $params = []
    ) {
        if (\preg_match("/^\{.*\}(.*)$/", $mailbox, $matches)) {
            $mailbox_name = $matches[1] ?? '';

            if (!\mb_detect_encoding($mailbox_name, 'ASCII', true)) {
                $mailbox = static::encodeStringToUtf7Imap($mailbox);
            }
        }

        \imap_errors(); // flush errors

        $result = @\imap_open($mailbox, $username, $password, $options, $n_retries, $params);

        if (!$result) {
            throw new ConnectionException(\imap_errors() ?: []);
        }

        return $result;
    }

    /**
     * @param resource|false $imap_stream
     *
     * @psalm-pure
     */
    public static function ping($imap_stream): bool
    {
        return (\is_resource($imap_stream) || $imap_stream instanceof \IMAP\Connection) && \imap_ping($imap_stream);
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return true
     */
    public static function renamemailbox(
        $imap_stream,
        string $old_mbox,
        string $new_mbox
    ): bool {
        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        $old_mbox = static::encodeStringToUtf7Imap($old_mbox);
        $new_mbox = static::encodeStringToUtf7Imap($new_mbox);

        \imap_errors(); // flush errors

        $result = \imap_renamemailbox($imap_stream, $old_mbox, $new_mbox);

        if (!$result) {
            throw new UnexpectedValueException('Could not rename mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_renamemailbox'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return true
     */
    public static function reopen(
        $imap_stream,
        string $mailbox,
        int $options = 0,
        int $n_retries = 0
    ): bool {
        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        $mailbox = static::encodeStringToUtf7Imap($mailbox);

        \imap_errors(); // flush errors

        $result = \imap_reopen($imap_stream, $mailbox, $options, $n_retries);

        if (!$result) {
            throw new UnexpectedValueException('Could not reopen mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_reopen'));
        }

        return $result;
    }

    /**
     * @param false|resource        $imap_stream
     * @param string|false|resource $file
     *
     * @return true
     */
    public static function savebody(
        $imap_stream,
        $file,
        int $msg_number,
        string $part_number = '',
        int $options = 0
    ): bool {
        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);
        $file = \is_string($file) ? $file : self::EnsureResource($file, __METHOD__, 2);
        $part_number = self::encodeStringToUtf7Imap($part_number);

        \imap_errors(); // flush errors

        $result = \imap_savebody($imap_stream, $file, $msg_number, $part_number, $options);

        if (!$result) {
            throw new UnexpectedValueException('Could not reopen mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_savebody'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @return int[]
     *
     * @psalm-return list<int>
     */
    public static function search(
        $imap_stream,
        string $criteria,
        int $options = SE_FREE,
        string $charset = null,
        bool $encodeCriteriaAsUtf7Imap = false
    ): array {
        \imap_errors(); // flush errors

        $imap_stream = static::EnsureConnection($imap_stream, __METHOD__, 1);

        if ($encodeCriteriaAsUtf7Imap) {
            $criteria = static::encodeStringToUtf7Imap($criteria);
        }

        if (\is_string($charset)) {
            $result = \imap_search(
                $imap_stream,
                $criteria,
                $options,
                static::encodeStringToUtf7Imap($charset)
            );
        } else {
            $result = \imap_search($imap_stream, $criteria, $options);
        }

        if (!$result) {
            $errors = \imap_errors();

            if (false === $errors) {
                /*
                * if there were no errors then there were no matches,
                *  rather than a failure to parse criteria.
                */
                return [];
            }

            throw new UnexpectedValueException('Could not search mailbox!', 0, self::HandleErrors($errors, 'imap_search'));
        }

        /** @psalm-var list<int> */
        return $result;
    }

    /**
     * @param false|resource $imap_stream
     * @param int|string     $sequence
     *
     * @return true
     */
    public static function setflag_full(
        $imap_stream,
        $sequence,
        string $flag,
        int $options = NIL
    ): bool {
        \imap_errors(); // flush errors

        $result = \imap_setflag_full(
            self::EnsureConnection($imap_stream, __METHOD__, 1),
            self::encodeStringToUtf7Imap(static::EnsureRange(
                $sequence,
                __METHOD__,
                2,
                true
            )),
            self::encodeStringToUtf7Imap($flag),
            $options
        );

        if (!$result) {
            throw new UnexpectedValueException('Could not set flag on messages!', 0, self::HandleErrors(\imap_errors(), 'imap_setflag_full'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @psalm-param value-of<self::SORT_CRITERIA> $criteria
     * @psalm-suppress InvalidArgument
     *
     * @todo InvalidArgument, although it's correct: Argument 3 of imap_sort expects int, bool provided https://www.php.net/manual/de/function.imap-sort.php
     *
     * @return int[]
     *
     * @psalm-return list<int>
     */
    public static function sort(
        $imap_stream,
        int $criteria,
        bool $reverse,
        int $options,
        string $search_criteria = null,
        string $charset = null
    ): array {
        \imap_errors(); // flush errors

        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        /** @var int */
        $criteria = $criteria;

        if (PHP_MAJOR_VERSION < 8) {
            /** @var int */
            $reverse = (int) $reverse;
        } else {
            /** @var bool */
            $reverse = $reverse;
        }

        if (null !== $search_criteria && null !== $charset) {
            $result = \imap_sort(
                $imap_stream,
                $criteria,
                $reverse,
                $options,
                self::encodeStringToUtf7Imap($search_criteria),
                self::encodeStringToUtf7Imap($charset)
            );
        } elseif (null !== $search_criteria) {
            $result = \imap_sort(
                $imap_stream,
                $criteria,
                $reverse,
                $options,
                self::encodeStringToUtf7Imap($search_criteria)
            );
        } else {
            $result = \imap_sort(
                $imap_stream,
                $criteria,
                $reverse,
                $options
            );
        }

        if (false === $result) {
            throw new UnexpectedValueException('Could not sort messages!', 0, self::HandleErrors(\imap_errors(), 'imap_sort'));
        }

        /** @psalm-var list<int> */
        return $result;
    }

    /**
     * @param false|resource $imap_stream
     *
     * @psalm-param SA_MESSAGES|SA_RECENT|SA_UNSEEN|SA_UIDNEXT|SA_UIDVALIDITY|SA_ALL $flags
     */
    public static function status($imap_stream, string $mailbox, int $options): stdClass
    {
        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        $mailbox = static::encodeStringToUtf7Imap($mailbox);

        \imap_errors(); // flush errors

        $result = \imap_status($imap_stream, $mailbox, $options);

        if (!$result) {
            throw new UnexpectedValueException('Could not get status of mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_status'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function subscribe($imap_stream, string $mailbox): void
    {
        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        $mailbox = static::encodeStringToUtf7Imap($mailbox);

        \imap_errors(); // flush errors

        $result = \imap_subscribe($imap_stream, $mailbox);

        if (false === $result) {
            throw new UnexpectedValueException('Could not subscribe to mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_subscribe'));
        }
    }

    /**
     * @psalm-param value-of<self::TIMEOUT_TYPES> $timeout_type
     *
     * @return true|int
     */
    public static function timeout(
        int $timeout_type,
        int $timeout = -1
    ) {
        \imap_errors(); // flush errors

        /** @var int */
        $timeout_type = $timeout_type;

        $result = \imap_timeout(
            $timeout_type,
            $timeout
        );

        if (false === $result) {
            throw new UnexpectedValueException('Could not get/set connection timeout!', 0, self::HandleErrors(\imap_errors(), 'imap_timeout'));
        }

        return $result;
    }

    /**
     * @param false|resource $imap_stream
     */
    public static function unsubscribe(
        $imap_stream,
        string $mailbox
    ): void {
        $imap_stream = self::EnsureConnection($imap_stream, __METHOD__, 1);

        $mailbox = static::encodeStringToUtf7Imap($mailbox);

        \imap_errors(); // flush errors

        $result = \imap_unsubscribe($imap_stream, $mailbox);

        if (false === $result) {
            throw new UnexpectedValueException('Could not unsubscribe from mailbox!', 0, self::HandleErrors(\imap_errors(), 'imap_unsubscribe'));
        }
    }

    /**
     * Returns the provided string in UTF7-IMAP encoded format.
     *
     * @return string $str UTF-7 encoded string
     *
     * @psalm-pure
     */
    public static function encodeStringToUtf7Imap(string $str): string
    {
        $out = \mb_convert_encoding($str, 'UTF7-IMAP', \mb_detect_encoding($str, 'UTF-8, ISO-8859-1, ISO-8859-15', true));

        if (!\is_string($out)) {
            throw new UnexpectedValueException('mb_convert_encoding($str, \'UTF-8\', {detected}) could not convert $str');
        }

        return $out;
    }

    /**
     * Returns the provided string in UTF-8 encoded format.
     *
     * @return string $str, but UTF-8 encoded
     *
     * @psalm-pure
     */
    public static function decodeStringFromUtf7ImapToUtf8(string $str): string
    {
        $out = \mb_convert_encoding($str, 'UTF-8', 'UTF7-IMAP');

        if (!\is_string($out)) {
            throw new UnexpectedValueException('mb_convert_encoding($str, \'UTF-8\', \'UTF7-IMAP\') could not convert $str');
        }

        return $out;
    }

    /**
     * @param false|resource $maybe
     *
     * @throws InvalidArgumentException if $maybe is not a valid resource
     *
     * @return resource
     *
     * @psalm-pure
     */
    private static function EnsureResource($maybe, string $method, int $argument)
    {
        if (!$maybe || (!\is_resource($maybe) && !$maybe instanceof \IMAP\Connection)) {
            throw new InvalidArgumentException('Argument '.(string) $argument.' passed to '.$method.' must be a valid resource!');
        }

        /** @var resource */
        return $maybe;
    }

    /**
     * @param false|resource $maybe
     *
     * @throws Exceptions\ConnectionException if $maybe is not a valid resource
     *
     * @return resource
     */
    private static function EnsureConnection($maybe, string $method, int $argument)
    {
        try {
            return self::EnsureResource($maybe, $method, $argument);
        } catch (Throwable $e) {
            throw new Exceptions\ConnectionException('Argument '.(string) $argument.' passed to '.$method.' must be valid resource!', 0, $e);
        }
    }

    /**
     * @param array|false $errors
     *
     * @psalm-pure
     */
    private static function HandleErrors($errors, string $method): UnexpectedValueException
    {
        if ($errors) {
            return new UnexpectedValueException('IMAP method '.$method.'() failed with error: '.\implode('. ', $errors));
        }

        return new UnexpectedValueException('IMAP method '.$method.'() failed!');
    }

    /**
     * @param scalar $msg_number
     *
     * @psalm-pure
     */
    private static function EnsureRange(
        $msg_number,
        string $method,
        int $argument,
        bool $allow_sequence = false
    ): string {
        if (!\is_int($msg_number) && !\is_string($msg_number)) {
            throw new InvalidArgumentException('Argument 1 passed to '.__METHOD__.'() must be an integer or a string!');
        }

        $regex = '/^\d+:\d+$/';
        $suffix = '() did not appear to be a valid message id range!';

        if ($allow_sequence) {
            $regex = '/^\d+(?:(?:,\d+)+|:\d+)$/';
            $suffix = '() did not appear to be a valid message id range or sequence!';
        }

        if (\is_int($msg_number) || \preg_match('/^\d+$/', $msg_number)) {
            return \sprintf('%1$s:%1$s', $msg_number);
        } elseif (1 !== \preg_match($regex, $msg_number)) {
            throw new InvalidArgumentException('Argument '.(string) $argument.' passed to '.$method.$suffix);
        }

        return $msg_number;
    }
}