diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5e5b915 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = tab +tab_width = 4 + +[*.{json,yaml,yml,md}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 4f8bb68..aad8529 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,10 @@ +.docs export-ignore +.editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore .travis.yml export-ignore -composer.lock export-ignore -tests/ export-ignore -*.md export-ignore \ No newline at end of file +Makefile export-ignore +README.md export-ignore +phpstan.neon export-ignore +ruleset.xml export-ignore +tests export-ignore diff --git a/.github/workflows/codesniffer.yml b/.github/workflows/codesniffer.yml new file mode 100644 index 0000000..a58ac4f --- /dev/null +++ b/.github/workflows/codesniffer.yml @@ -0,0 +1,18 @@ +name: "Codesniffer" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 8 * * 1" + +jobs: + codesniffer: + name: "Codesniffer" + uses: contributte/.github/.github/workflows/codesniffer.yml@master + with: + php: "8.2" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..fac01f8 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,18 @@ +name: "Coverage" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 9 * * 1" + +jobs: + coverage: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester-coverage-v2.yml@master + with: + php: "8.2" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..13ceb07 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,18 @@ +name: "Phpstan" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 10 * * 1" + +jobs: + phpstan: + name: "Phpstan" + uses: contributte/.github/.github/workflows/phpstan.yml@master + with: + php: "8.2" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ef17693 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,43 @@ +name: "Nette Tester" + +on: + pull_request: + workflow_dispatch: + + push: + branches: ["*"] + + schedule: + - cron: "0 10 * * 1" + +jobs: + test85: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.5" + + test84: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.4" + + test83: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.3" + + test82: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.2" + + testlower: + name: "Nette Tester" + uses: contributte/.github/.github/workflows/nette-tester.yml@master + with: + php: "8.2" + composer: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable --prefer-lowest" diff --git a/.gitignore b/.gitignore index d72acbc..f0b3670 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,14 @@ -# Folders +# IDE +/.idea + +# Composer /vendor -# Files /composer.lock -/tests/*.log + +# Tests +/tests/tmp +/coverage.* +/tests/**/*.log +/tests/**/*.html +/tests/**/*.expected +/tests/**/*.actual diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..33bc117 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +.PHONY: install +install: + composer update + +.PHONY: qa +qa: phpstan cs + +.PHONY: cs +cs: +ifdef GITHUB_ACTION + vendor/bin/phpcs --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp -q --report=checkstyle src tests | cs2pr +else + vendor/bin/phpcs --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp src tests +endif + +.PHONY: csf +csf: + vendor/bin/phpcbf --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp src tests + +.PHONY: phpstan +phpstan: + vendor/bin/phpstan analyse -c phpstan.neon + +.PHONY: tests +tests: + vendor/bin/tester -s -p php --colors 1 -C tests/Cases + +.PHONY: coverage +coverage: +ifdef GITHUB_ACTION + vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage coverage.xml --coverage-src src tests/Cases +else + vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage coverage.html --coverage-src src tests/Cases +endif diff --git a/composer.json b/composer.json index 5068dc7..18dc891 100644 --- a/composer.json +++ b/composer.json @@ -7,13 +7,33 @@ "authors": [ { "name": "Milan Felix Sulc", - "homepage": "http://jfx.cz" + "homepage": "https://f3l1x.io" } ], + "require": { + "php": ">=8.2" + }, "require-dev": { - "nette/tester": "~1.3.0" + "contributte/phpstan": "~0.2.0", + "contributte/qa": "~0.4.0", + "contributte/tester": "~0.3.0" }, "autoload": { - "classmap": ["src/"] + "psr-4": { + "Minetro\\Imap\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5011892 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +includes: + - vendor/contributte/phpstan/phpstan.neon + +parameters: + level: 9 + phpVersion: 80200 + + scanDirectories: + - src + + fileExtensions: + - php + + paths: + - src diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..3da188c --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + /tests/tmp + diff --git a/src/ImapMessage.php b/src/ImapMessage.php index d3349d9..f0932ab 100644 --- a/src/ImapMessage.php +++ b/src/ImapMessage.php @@ -1,196 +1,170 @@ - */ class ImapMessage { - // Message flags - const FLAG_SEEN = '\\Seen'; - const FLAG_ANSWERED = '\\Answered'; - const FLAG_FLAGGED = '\\Flagged'; - const FLAG_DELETED = '\\Deleted'; - const FLAG_DRAFT = '\\Draft'; - - /** @var int */ - private $number; - - /** @var stdClass */ - private $headers; - - /** @var stdClass */ - private $structure; - - /** @var array */ - private $body = []; - - /** - * @param int $number - * @param stdClass $headers - * @param stdClass $structure - * @param array $body - */ - function __construct($number, $headers, $structure, array $body) - { - $this->number = $number; - - // Convert all headers to UTF-8 - $this->headers = $this->utf8($headers); - - $this->structure = $structure; - $this->body = $body; - } - - /** - * GETTERS ***************************************************************** - * ************************************************************************* - */ - - /** - * @return int - */ - public function getNumber() - { - return $this->number; - } - - /** - * @return stdClass - */ - public function getHeaders() - { - return $this->headers; - } - - /** - * @return stdClass - */ - public function getStructure() - { - return $this->structure; - } - - /** - * @return array - */ - public function getBody() - { - return $this->body; - } - - /** - * @param int $section - * @return mixed - * @throws InvalidArgumentException - */ - public function getBodySection($section) - { - if ($section > count($this->body) || !isset($this->body[$section])) { - throw new InvalidArgumentException('Section #' . $section . ' not found.'); - } - - return $this->body[$section]; - } - - /** - * Returns text of the e-mail converted to utf-8. - * - * @param int $section - * @param int $encoding [optional] - * @return string - */ - public function getBodySectionText($section, $encoding = NULL) - { - $text = $this->getBodySection($section); - $encoding = $encoding ? $encoding : (isset($this->structure->parts[$section]) ? $this->structure->parts[$section]->encoding : $this->structure->encoding); - - switch ($encoding) { - # 7BIT - case 0: - $etext = $text; - break; - # 8BIT - case 1: - $etext = quoted_printable_decode(imap_8bit($text)); - break; - # BINARY - case 2: - $etext = imap_binary($text); - break; - # BASE64 - case 3: - $etext = imap_base64($text); - break; - # QUOTED-PRINTABLE - case 4: - $etext = quoted_printable_decode($text); - break; - # OTHER - case 5: - $etext = $text; - break; - # UNKNOWN - default: - $etext = $text; - } - - $charset = $this->getBodyCharset($section); - $charset = $charset ?: mb_detect_encoding($etext, mb_detect_order(), TRUE); - if ($charset === FALSE) { - return $etext; - } else { - return iconv($charset, "UTF-8//TRANSLIT", $etext); - } - } - - /** - * Returns charset defined in e-mail headers. - * - * @return string|NULL - */ - public function getBodyCharset() - { - foreach ($this->structure->parameters as $pair) { - if (isset($pair->attribute) && $pair->attribute == 'charset') { - return $pair->value; - } - } - return NULL; - } - - /** - * @return int - */ - public function countBodies() - { - return count($this->body); - } - - - /** - * HELPERS ***************************************************************** - * ************************************************************************* - */ - - /** - * @param mixed $data - * @return stdClass - */ - private function utf8($data) - { - $array = json_decode(json_encode($data), TRUE); - array_walk_recursive($array, function ($v, $k) { - return is_array($v) ? $v : imap_utf8($v); - }); - - return json_decode(json_encode($array)); - } -} \ No newline at end of file + // Message flags + public const FLAG_SEEN = '\\Seen'; + public const FLAG_ANSWERED = '\\Answered'; + public const FLAG_FLAGGED = '\\Flagged'; + public const FLAG_DELETED = '\\Deleted'; + public const FLAG_DRAFT = '\\Draft'; + + private int $number; + + private stdClass $headers; + + private stdClass $structure; + + /** @var array */ + private array $body = []; + + /** + * @param array $body + */ + public function __construct(int $number, stdClass $headers, stdClass $structure, array $body) + { + $this->number = $number; + $this->headers = $this->utf8($headers); + $this->structure = $structure; + $this->body = $body; + } + + public function getNumber(): int + { + return $this->number; + } + + public function getHeaders(): stdClass + { + return $this->headers; + } + + public function getStructure(): stdClass + { + return $this->structure; + } + + /** + * @return array + */ + public function getBody(): array + { + return $this->body; + } + + /** + * @throws InvalidArgumentException + */ + public function getBodySection(int $section): string|false + { + if ($section > count($this->body) || !isset($this->body[$section])) { + throw new InvalidArgumentException('Section #' . $section . ' not found.'); + } + + return $this->body[$section]; + } + + /** + * Returns text of the e-mail converted to utf-8. + */ + public function getBodySectionText(int $section, ?int $encoding = null): string + { + $text = (string) $this->getBodySection($section); + $encoding ??= (int) (isset($this->structure->parts[$section]) ? $this->structure->parts[$section]->encoding : $this->structure->encoding); + + $etext = match ($encoding) { + 0 => $text, // 7BIT + 1 => quoted_printable_decode((string) imap_8bit($text)), // 8BIT + 2 => (string) imap_binary($text), // BINARY + 3 => (string) imap_base64($text), // BASE64 + 4 => quoted_printable_decode($text), // QUOTED-PRINTABLE + default => $text, // OTHER / UNKNOWN + }; + + $charset = $this->getBodyCharset(); + + if ($charset === null) { + $detected = mb_detect_encoding($etext, mb_detect_order(), true); + $charset = $detected !== false ? $detected : null; + } + + if ($charset === null) { + return $etext; + } + + $result = iconv($charset, 'UTF-8//TRANSLIT', $etext); + + return $result !== false ? $result : $etext; + } + + /** + * Returns charset defined in e-mail headers. + */ + public function getBodyCharset(): ?string + { + foreach ($this->structure->parameters as $pair) { + if (isset($pair->attribute) && $pair->attribute === 'charset') { + return $pair->value; // @phpstan-ignore property.notFound + } + } + + return null; + } + + public function countBodies(): int + { + return count($this->body); + } + + private function utf8(stdClass $data): stdClass + { + $json = json_encode($data); + + if ($json === false) { + return $data; + } + + /** @var array $array */ + $array = json_decode($json, true); + $array = $this->utf8Recursive($array); + + $result = json_decode((string) json_encode($array)); + + if ($result instanceof stdClass) { + return $result; + } + + return $data; + } + + /** + * @param array $array + * @return array + */ + private function utf8Recursive(array $array): array + { + $result = []; + + foreach ($array as $key => $value) { + if (is_array($value)) { + $result[$key] = $this->utf8Recursive($value); + } elseif (is_string($value)) { + $result[$key] = imap_utf8($value); + } else { + $result[$key] = $value; + } + } + + return $result; + } + +} diff --git a/src/ImapReader.php b/src/ImapReader.php index f1010f7..6ad75a5 100644 --- a/src/ImapReader.php +++ b/src/ImapReader.php @@ -1,205 +1,177 @@ - */ final class ImapReader { - // Message criteria - const CRITERIA_ALL = 'ALL'; // Return all messages matching the rest of the criteria - const CRITERIA_ANSWERED = 'ANSWERED'; // Match messages with the \\ANSWERED flag set - const CRITERIA_BCC = 'BCC'; // Match messages with "string" in the Bcc: field - const CRITERIA_BEFORE = 'BEFORE'; // Match messages with Date: before "date" - const CRITERIA_BODY = 'BODY'; // Match messages with "string" in the body of the message - const CRITERIA_CC = 'CC'; // Match messages with "string" in the Cc: field - const CRITERIA_DELETED = 'DELETED'; // Match deleted messages - const CRITERIA_FLAGGED = 'FLAGGED'; // Match messages with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set - const CRITERIA_FROM = 'FROM'; // Match messages with "string" in the From: field - const CRITERIA_KEYWORD = 'KEYWORD'; // Match messages with "string" as a keyword - const CRITERIA_NEW = 'NEW'; // Match new messages - const CRITERIA_OLD = 'OLD'; // Match old messages - const CRITERIA_ON = 'ON'; // Match messages with Date: matching "date" - const CRITERIA_RECENT = 'RECENT'; // Match messages with the \\RECENT flag set - const CRITERIA_SEEN = 'SEEN'; // Match messages that have been read (the \\SEEN flag is set) - const CRITERIA_SINCE = 'SINCE'; // Match messages with Date: after "date" - const CRITERIA_SUBJECT = 'SUBJECT'; // Match messages with "string" in the Subject: - const CRITERIA_TEXT = 'TEXT'; // Match messages with teconst CRITERIA_t = 't'; "string" - const CRITERIA_TO = 'TO'; // Match messages with "string" in the To: - const CRITERIA_UNANSWERED = 'UNANSWERED'; // Match messages that have not been answered - const CRITERIA_UNDELETED = 'UNDELETED'; // Match messages that are not deleted - const CRITERIA_UNFLAGGED = 'UNFLAGGED'; // Match messages that are not flagged - const CRITERIA_UNKEYWORD = 'UNKEYWORD'; // Match messages that do not have the keyword "string" - const CRITERIA_UNSEEN = 'UNSEEN'; // Match messages which have not been read yet - - /** @var resource */ - private $imap; - - /** @var string */ - private $mailbox; - - /** @var string */ - private $username; - - /** @var string */ - private $password; - - /** - * Create IMAP resource - * - * @param string $mailbox - * @param string $username - * @param string $password - */ - function __construct($mailbox, $username, $password) - { - $this->mailbox = $mailbox; - $this->username = $username; - $this->password = $password; - - $this->connect($mailbox, $username, $password); - } - - /** - * Close IMAP resource - * - * @return void - */ - function __destruct() - { - $this->disconnect(); - } - - /** - * API ********************************************************************* - * ************************************************************************* - */ - - /** - * @return bool - */ - public function isAlive() - { - return (bool)@imap_ping($this->imap); - } - - /** - * @param string $criteria - * @param int $options - * @return ImapMessage[] - */ - public function read($criteria = 'ALL', $options = SE_FREE) - { - $mails = imap_search($this->imap, $criteria, $options); - if (!$mails) return []; - - $emails = []; - foreach ($mails as $mailnum) { - $structure = imap_fetchstructure($this->imap, $mailnum); - $headers = imap_headerinfo($this->imap, $mailnum); - if (!$structure || !$headers) continue; - - $sections = []; - if (isset($structure->parts)) { - foreach ($structure->parts as $partnum => $part) { - $sections[] = imap_fetchbody($this->imap, $mailnum, $partnum); - } - } else { - $sections[] = imap_body($this->imap, $mailnum); - } - - $emails[] = new ImapMessage($mailnum, $headers, $structure, $sections); - } - - return $emails; - } - - /** - * Change IMAP reading folder - * - * @param string $folder - * @return void - */ - public function folder($folder) - { - // @todo - } - - /** - * Set message flag - * - * @param string|array $sequence Message numbers - * @param string|array $flag Message flag - * @return bool - */ - public function flag($sequence, $flag) - { - $sequence = is_array($sequence) ? implode(',', $sequence) : $sequence; - $flag = is_array($flag) ? implode(' ', $flag) : $flag; - return imap_setflag_full($this->imap, $sequence, $flag); - } - - /** - * Unset message flag - * - * @param string|array $sequence Message numbers - * @param string|array $flag Message flag - * @return bool - */ - public function unflag($sequence, $flag) - { - $sequence = is_array($sequence) ? implode(',', $sequence) : $sequence; - $flag = is_array($flag) ? implode(' ', $flag) : $flag; - return imap_clearflag_full($this->imap, $sequence, $flag); - } - - /** - * MAGIC ******************************************************************* - * ************************************************************************* - */ - - /** - * @param string $name - * @param array $arguments - * @return mixed - */ - function __call($name, $arguments) - { - $args = $arguments; - array_unshift($args, $this->imap); - return call_user_func_array('imap_' . $name, $args); - } - - /** - * HELPERS ***************************************************************** - * ************************************************************************* - */ - - /** - * Connect to IMAP mailbox - * - * @return void - */ - protected function connect() - { - $this->imap = imap_open($this->mailbox, $this->username, $this->password); - } - - /** - * Disconnect to IMAP mailbox - * - * @return void - */ - protected function disconnect() - { - // Flush all messages out - imap_errors(); - imap_alerts(); - - imap_close($this->imap); - } -} \ No newline at end of file + // Message criteria + public const CRITERIA_ALL = 'ALL'; + public const CRITERIA_ANSWERED = 'ANSWERED'; + public const CRITERIA_BCC = 'BCC'; + public const CRITERIA_BEFORE = 'BEFORE'; + public const CRITERIA_BODY = 'BODY'; + public const CRITERIA_CC = 'CC'; + public const CRITERIA_DELETED = 'DELETED'; + public const CRITERIA_FLAGGED = 'FLAGGED'; + public const CRITERIA_FROM = 'FROM'; + public const CRITERIA_KEYWORD = 'KEYWORD'; + public const CRITERIA_NEW = 'NEW'; + public const CRITERIA_OLD = 'OLD'; + public const CRITERIA_ON = 'ON'; + public const CRITERIA_RECENT = 'RECENT'; + public const CRITERIA_SEEN = 'SEEN'; + public const CRITERIA_SINCE = 'SINCE'; + public const CRITERIA_SUBJECT = 'SUBJECT'; + public const CRITERIA_TEXT = 'TEXT'; + public const CRITERIA_TO = 'TO'; + public const CRITERIA_UNANSWERED = 'UNANSWERED'; + public const CRITERIA_UNDELETED = 'UNDELETED'; + public const CRITERIA_UNFLAGGED = 'UNFLAGGED'; + public const CRITERIA_UNKEYWORD = 'UNKEYWORD'; + public const CRITERIA_UNSEEN = 'UNSEEN'; + + private Connection $imap; + + private string $mailbox; + + private string $username; + + private string $password; + + public function __construct(string $mailbox, string $username, string $password) + { + $this->mailbox = $mailbox; + $this->username = $username; + $this->password = $password; + + $this->connect(); + } + + public function __destruct() + { + $this->disconnect(); + } + + public function isAlive(): bool + { + return @imap_ping($this->imap); + } + + /** + * @return ImapMessage[] + */ + public function read(string $criteria = 'ALL', int $options = SE_FREE): array + { + $mails = imap_search($this->imap, $criteria, $options); + + if ($mails === false) { + return []; + } + + $emails = []; + + foreach ($mails as $mailnum) { + $structure = imap_fetchstructure($this->imap, $mailnum); + $headers = imap_headerinfo($this->imap, $mailnum); + + if ($structure === false || $headers === false) { + continue; + } + + $sections = []; + + if (isset($structure->parts)) { + foreach ($structure->parts as $partnum => $part) { + $sections[] = imap_fetchbody($this->imap, $mailnum, (string) $partnum); + } + } else { + $sections[] = imap_body($this->imap, $mailnum); + } + + $emails[] = new ImapMessage($mailnum, $headers, $structure, $sections); + } + + return $emails; + } + + /** + * Change IMAP reading folder + */ + public function folder(string $folder): void + { + // @todo + } + + /** + * Set message flag + * + * @param string|array $sequence Message numbers + * @param string|array $flag Message flag + */ + public function flag(string|array $sequence, string|array $flag): bool + { + $sequence = is_array($sequence) ? implode(',', $sequence) : $sequence; + $flag = is_array($flag) ? implode(' ', $flag) : $flag; + + return imap_setflag_full($this->imap, $sequence, $flag); + } + + /** + * Unset message flag + * + * @param string|array $sequence Message numbers + * @param string|array $flag Message flag + */ + public function unflag(string|array $sequence, string|array $flag): bool + { + $sequence = is_array($sequence) ? implode(',', $sequence) : $sequence; + $flag = is_array($flag) ? implode(' ', $flag) : $flag; + + return imap_clearflag_full($this->imap, $sequence, $flag); + } + + /** + * Connect to IMAP mailbox + */ + protected function connect(): void + { + $connection = imap_open($this->mailbox, $this->username, $this->password); + + if ($connection === false) { + throw new RuntimeException('Failed to connect to IMAP mailbox'); + } + + $this->imap = $connection; + } + + /** + * Disconnect from IMAP mailbox + */ + protected function disconnect(): void + { + // Flush all messages out + imap_errors(); + imap_alerts(); + + imap_close($this->imap); + } + + /** + * @param array $arguments + */ + public function __call(string $name, array $arguments): mixed + { + $args = $arguments; + array_unshift($args, $this->imap); + + return call_user_func_array('imap_' . $name, $args); // @phpstan-ignore argument.type + } + +} diff --git a/tests/Cases/ImapMessageTest.phpt b/tests/Cases/ImapMessageTest.phpt new file mode 100644 index 0000000..e9a0cff --- /dev/null +++ b/tests/Cases/ImapMessageTest.phpt @@ -0,0 +1,108 @@ +attribute = 'charset'; + $param->value = $charset; + + $structure = new stdClass(); + $structure->parameters = [$param]; + $structure->encoding = $encoding; + + return $structure; +} + +function createHeaders(): stdClass +{ + $headers = new stdClass(); + $headers->subject = 'Test Subject'; + $headers->from = 'sender@example.com'; + $headers->to = 'recipient@example.com'; + + return $headers; +} + +// Tests + +Toolkit::test(function (): void { + $headers = createHeaders(); + $structure = createStructure(); + $body = ['Section 0', 'Section 1']; + + $message = new ImapMessage(1, $headers, $structure, $body); + + Assert::same(1, $message->getNumber()); + Assert::type(stdClass::class, $message->getHeaders()); + Assert::same($structure, $message->getStructure()); + Assert::same($body, $message->getBody()); + Assert::same(2, $message->countBodies()); +}); + +Toolkit::test(function (): void { + $message = new ImapMessage(42, createHeaders(), createStructure(), ['body0', 'body1', 'body2']); + + Assert::same('body0', $message->getBodySection(0)); + Assert::same('body1', $message->getBodySection(1)); + Assert::same('body2', $message->getBodySection(2)); +}); + +Toolkit::test(function (): void { + $message = new ImapMessage(1, createHeaders(), createStructure(), ['only']); + + Assert::exception(function () use ($message): void { + $message->getBodySection(5); + }, InvalidArgumentException::class, 'Section #5 not found.'); +}); + +Toolkit::test(function (): void { + $message = new ImapMessage(1, createHeaders(), createStructure('UTF-8'), []); + + Assert::same('UTF-8', $message->getBodyCharset()); +}); + +Toolkit::test(function (): void { + $structure = new stdClass(); + $structure->parameters = []; + $structure->encoding = 0; + + $message = new ImapMessage(1, createHeaders(), $structure, []); + + Assert::null($message->getBodyCharset()); +}); + +Toolkit::test(function (): void { + $structure = createStructure('UTF-8', 0); + $message = new ImapMessage(1, createHeaders(), $structure, ['Hello World']); + + Assert::same('Hello World', $message->getBodySectionText(0, 0)); +}); + +Toolkit::test(function (): void { + $message = new ImapMessage(1, createHeaders(), createStructure(), []); + + Assert::same(0, $message->countBodies()); +}); + +Toolkit::test(function (): void { + Assert::same('\\Seen', ImapMessage::FLAG_SEEN); + Assert::same('\\Answered', ImapMessage::FLAG_ANSWERED); + Assert::same('\\Flagged', ImapMessage::FLAG_FLAGGED); + Assert::same('\\Deleted', ImapMessage::FLAG_DELETED); + Assert::same('\\Draft', ImapMessage::FLAG_DRAFT); +}); diff --git a/tests/Cases/ImapReaderTest.phpt b/tests/Cases/ImapReaderTest.phpt new file mode 100644 index 0000000..6aef024 --- /dev/null +++ b/tests/Cases/ImapReaderTest.phpt @@ -0,0 +1,41 @@ +