diff --git a/composer.json b/composer.json index 59f920e..75dd48c 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "require": { "tinymce/tinymce": "*", "phpmailer/phpmailer": "dev-master", - "php-imap/php-imap": "^2.0" + "php-imap/php-imap": "5.0" }, "minimum-stability": "dev" } diff --git a/composer.lock b/composer.lock index 1bbfba5..8caae0b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,34 +4,54 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "273d2592d3c90db120bdd9644b0975a0", + "content-hash": "28af27dc1d7f1cf3b23cc196c9a13301", "packages": [ { "name": "php-imap/php-imap", - "version": "2.0.3", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/barbushin/php-imap.git", - "reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b" + "reference": "13bdfa9a6f541798253e24e2d8f44332c8be098c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barbushin/php-imap/zipball/cc1a49a3f68090db182183c59ffbc09055d59f5b", - "reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b", + "url": "https://api.github.com/repos/barbushin/php-imap/zipball/13bdfa9a6f541798253e24e2d8f44332c8be098c", + "reference": "13bdfa9a6f541798253e24e2d8f44332c8be098c", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-fileinfo": "*", + "ext-iconv": "*", + "ext-imap": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "maglnet/composer-require-checker": "^2.0|^3.2", + "nikic/php-parser": "^4.3,<4.7|^4.10", + "paragonie/hidden-string": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "^8.5|^9.5", + "povils/phpmnd": "^2.2", + "psalm/plugin-phpunit": "^0.10.0|^0.15.1", + "roave/security-advisories": "dev-master", + "sebastian/phpcpd": "^4.1|^6.0" + }, + "suggest": { + "ext-fileinfo": "To facilitate IncomingMailAttachment::getFileInfo() auto-detection" }, "type": "library", "autoload": { - "psr-0": { - "PhpImap": "src/" + "psr-4": { + "PhpImap\\": "src/PhpImap" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD 3-Clause" + "MIT" ], "authors": [ { @@ -40,18 +60,21 @@ "homepage": "http://linkedin.com/in/barbushin" } ], - "description": "PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension", + "description": "Manage mailboxes, filter/get/delete emails in PHP (supports IMAP/POP3/NNTP)", "homepage": "https://github.com/barbushin/php-imap", "keywords": [ "imap", "mail", - "php" + "mailbox", + "php", + "pop3", + "receive emails" ], "support": { "issues": "https://github.com/barbushin/php-imap/issues", - "source": "https://github.com/barbushin/php-imap/tree/master" + "source": "https://github.com/barbushin/php-imap/tree/5.0.0" }, - "time": "2015-09-16T07:40:39+00:00" + "time": "2022-03-12T14:39:59+00:00" }, { "name": "phpmailer/phpmailer", diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php index e8a5b7e..15a2ff3 100644 --- a/vendor/composer/autoload_namespaces.php +++ b/vendor/composer/autoload_namespaces.php @@ -6,5 +6,4 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( - 'PhpImap' => array($vendorDir . '/php-imap/php-imap/src'), ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 28567a0..f45d4f6 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -6,5 +6,6 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + 'PhpImap\\' => array($vendorDir . '/php-imap/php-imap/src/PhpImap'), 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index f53de54..00513ac 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -9,27 +9,22 @@ class ComposerStaticInitb3a3dfb766a515d49b7f8665bad574b3 public static $prefixLengthsPsr4 = array ( 'P' => array ( + 'PhpImap\\' => 8, 'PHPMailer\\PHPMailer\\' => 20, ), ); public static $prefixDirsPsr4 = array ( + 'PhpImap\\' => + array ( + 0 => __DIR__ . '/..' . '/php-imap/php-imap/src/PhpImap', + ), 'PHPMailer\\PHPMailer\\' => array ( 0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src', ), ); - public static $prefixesPsr0 = array ( - 'P' => - array ( - 'PhpImap' => - array ( - 0 => __DIR__ . '/..' . '/php-imap/php-imap/src', - ), - ), - ); - public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', ); @@ -39,7 +34,6 @@ class ComposerStaticInitb3a3dfb766a515d49b7f8665bad574b3 return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitb3a3dfb766a515d49b7f8665bad574b3::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitb3a3dfb766a515d49b7f8665bad574b3::$prefixDirsPsr4; - $loader->prefixesPsr0 = ComposerStaticInitb3a3dfb766a515d49b7f8665bad574b3::$prefixesPsr0; $loader->classMap = ComposerStaticInitb3a3dfb766a515d49b7f8665bad574b3::$classMap; }, null, ClassLoader::class); diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 03ea825..ec7418b 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2,33 +2,53 @@ "packages": [ { "name": "php-imap/php-imap", - "version": "2.0.3", - "version_normalized": "2.0.3.0", + "version": "5.0.0", + "version_normalized": "5.0.0.0", "source": { "type": "git", "url": "https://github.com/barbushin/php-imap.git", - "reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b" + "reference": "13bdfa9a6f541798253e24e2d8f44332c8be098c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barbushin/php-imap/zipball/cc1a49a3f68090db182183c59ffbc09055d59f5b", - "reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b", + "url": "https://api.github.com/repos/barbushin/php-imap/zipball/13bdfa9a6f541798253e24e2d8f44332c8be098c", + "reference": "13bdfa9a6f541798253e24e2d8f44332c8be098c", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-fileinfo": "*", + "ext-iconv": "*", + "ext-imap": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" }, - "time": "2015-09-16T07:40:39+00:00", + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "maglnet/composer-require-checker": "^2.0|^3.2", + "nikic/php-parser": "^4.3,<4.7|^4.10", + "paragonie/hidden-string": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "^8.5|^9.5", + "povils/phpmnd": "^2.2", + "psalm/plugin-phpunit": "^0.10.0|^0.15.1", + "roave/security-advisories": "dev-master", + "sebastian/phpcpd": "^4.1|^6.0" + }, + "suggest": { + "ext-fileinfo": "To facilitate IncomingMailAttachment::getFileInfo() auto-detection" + }, + "time": "2022-03-12T14:39:59+00:00", "type": "library", "installation-source": "dist", "autoload": { - "psr-0": { - "PhpImap": "src/" + "psr-4": { + "PhpImap\\": "src/PhpImap" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD 3-Clause" + "MIT" ], "authors": [ { @@ -37,16 +57,19 @@ "homepage": "http://linkedin.com/in/barbushin" } ], - "description": "PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension", + "description": "Manage mailboxes, filter/get/delete emails in PHP (supports IMAP/POP3/NNTP)", "homepage": "https://github.com/barbushin/php-imap", "keywords": [ "imap", "mail", - "php" + "mailbox", + "php", + "pop3", + "receive emails" ], "support": { "issues": "https://github.com/barbushin/php-imap/issues", - "source": "https://github.com/barbushin/php-imap/tree/master" + "source": "https://github.com/barbushin/php-imap/tree/5.0.0" }, "install-path": "../php-imap/php-imap" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 9ff148f..b98670e 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1,9 +1,9 @@ array( 'name' => '__root__', - 'pretty_version' => '1.0.0+no-version-set', - 'version' => '1.0.0.0', - 'reference' => NULL, + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '1c6f08b77a9f8f7d91de74616da0b18dfccef4e8', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -11,18 +11,18 @@ ), 'versions' => array( '__root__' => array( - 'pretty_version' => '1.0.0+no-version-set', - 'version' => '1.0.0.0', - 'reference' => NULL, + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '1c6f08b77a9f8f7d91de74616da0b18dfccef4e8', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), 'php-imap/php-imap' => array( - 'pretty_version' => '2.0.3', - 'version' => '2.0.3.0', - 'reference' => 'cc1a49a3f68090db182183c59ffbc09055d59f5b', + 'pretty_version' => '5.0.0', + 'version' => '5.0.0.0', + 'reference' => '13bdfa9a6f541798253e24e2d8f44332c8be098c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-imap/php-imap', 'aliases' => array(), diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 454eefd..580fa96 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 50500)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 5.5.0". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 70400)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { diff --git a/vendor/php-imap/php-imap/.codeclimate.yml b/vendor/php-imap/php-imap/.codeclimate.yml new file mode 100644 index 0000000..651c187 --- /dev/null +++ b/vendor/php-imap/php-imap/.codeclimate.yml @@ -0,0 +1,36 @@ +# Advanced Configuration for CodeClimate: https://docs.codeclimate.com/docs +languages: + PHP: true +exclude_paths: ["tests/*", "examples/*"] +version: "2" # required to adjust maintainability checks +checks: + argument-count: + config: + threshold: 5 + complex-logic: + config: + threshold: 4 + file-lines: + config: + threshold: 250 + method-complexity: + config: + threshold: 5 + method-count: + config: + threshold: 20 + method-lines: + config: + threshold: 40 + nested-control-flow: + config: + threshold: 4 + return-statements: + config: + threshold: 4 + similar-code: + config: + threshold: # language-specific defaults. an override will affect all languages. + identical-code: + config: + threshold: # language-specific defaults. an override will affect all languages. diff --git a/vendor/php-imap/php-imap/.editorconfig b/vendor/php-imap/php-imap/.editorconfig new file mode 100644 index 0000000..b06b4db --- /dev/null +++ b/vendor/php-imap/php-imap/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_size = 2 + +[composer.json] +indent_style = space diff --git a/vendor/php-imap/php-imap/.gitattributes b/vendor/php-imap/php-imap/.gitattributes deleted file mode 100644 index 412eeda..0000000 --- a/vendor/php-imap/php-imap/.gitattributes +++ /dev/null @@ -1,22 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto - -# Custom for Visual Studio -*.cs diff=csharp -*.sln merge=union -*.csproj merge=union -*.vbproj merge=union -*.fsproj merge=union -*.dbproj merge=union - -# Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain diff --git a/vendor/php-imap/php-imap/.github/CONTRIBUTING/contributing.md b/vendor/php-imap/php-imap/.github/CONTRIBUTING/contributing.md new file mode 100644 index 0000000..9751018 --- /dev/null +++ b/vendor/php-imap/php-imap/.github/CONTRIBUTING/contributing.md @@ -0,0 +1,21 @@ +# How to contribute + +First of all, thanks for taking the time to contribute! + +Every contribution, being it pull requests, bug reports or feature requests, will help to improve this library! + +## Ways to contribute + +* Found a bug? Want a feature? Or just having question? [Open an +issue!](https://github.com/barbushin/php-imap/issues/new/choose) +* Add a feature or fix a bug: + * Check for existing issue or create a new one. + * Fork the repo, make your changes. + * Create a pull request, and reference the issue. +* Add examples, tests, or improve documentation. + +## Test + +When committing code, please make sure to test before creating a pull request. + +We use PHPUnit for testing, feel free to add new tests. This is not a requirement, but helps us maintain code coverage. diff --git a/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/bug_report.md b/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..10cb27e --- /dev/null +++ b/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,46 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] A short description of what the bug is" +labels: needs investigation +assignees: '' + +--- + +**Environment (please complete the following information):** + - PHP IMAP version: [e.g. 3.0.11] + - PHP Version: [e.g. 7.1.26] + - Type of execution: [e.g. Daemon / CLI or Web Server] + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +The used code: +```php +$mailbox = new Mailbox(... +``` + +The headers of the parsed email, if required and possible (only, if it's NOT confidential): +``` +Received: from BN3NAM04HT142.eop-NAM04.prod.protection.outlook.com +(2603:10a6:209:2a::30) by AM6PR05MB6294.eurprd05.prod.outlook.com with HTTPS +via AM6PR07CA0017.EURPRD07.PROD.OUTLOOK.COM; Sun, 5 May 2019 12:29:42 +0000 +Received: from BN3NAM04FT054.eop-NAM04.prod.protection.outlook.com +(10.152.92.54) by BN3NAM04HT142.eop-NAM04.prod.protection.outlook.com +(10.152.92.244) with Microsoft SMTP Server (version=TLS1_2, +cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id 15.20.1835.13; Sun, 5 May +2019 12:29:41 +0000 +... +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots / Outputs** +If applicable, add screenshots or outputs of your script to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/feature_request.md b/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..508eeef --- /dev/null +++ b/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature Request] A short description of what you want to happen" +labels: needs investigation +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/other-issue.md b/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/other-issue.md new file mode 100644 index 0000000..af8fbe6 --- /dev/null +++ b/vendor/php-imap/php-imap/.github/ISSUE_TEMPLATE/other-issue.md @@ -0,0 +1,16 @@ +--- +name: Other Issue +about: Ask your question, if it's not a bug or feature request +title: '' +labels: '' +assignees: '' + +--- + +**Environment (please complete the following information):** + - PHP IMAP version: [e.g. 3.0.11] + - PHP Version: [e.g. 7.1.26] + - Type of execution: [e.g. Daemon / CLI or Web Server] + +**Your Text** +A clear and concise description of what you want. diff --git a/vendor/php-imap/php-imap/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/vendor/php-imap/php-imap/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..256e2e6 --- /dev/null +++ b/vendor/php-imap/php-imap/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,16 @@ +**Describe the change(s)** + +A clear and concise description of what you changed and why you changed it. + +**Example code to use new / updated methods** + +```php +$mailbox = new Mailbox(... +``` + +--- +Make sure, that these boxes are checked before creating your pull request: + +- [ ] A short description of this change is provided +- [ ] A short example code is provided, if possible +- [ ] PHPUnit tests for your changes exists diff --git a/vendor/php-imap/php-imap/.github/workflows/php_code_coverage.yml b/vendor/php-imap/php-imap/.github/workflows/php_code_coverage.yml new file mode 100644 index 0000000..9b21626 --- /dev/null +++ b/vendor/php-imap/php-imap/.github/workflows/php_code_coverage.yml @@ -0,0 +1,53 @@ +name: PHP Code Coverage + +on: + push: + branches: + - master + pull_request: + +jobs: + coverage: + name: Coverage + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-20.04'] + php-versions: ['8.1'] + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: xdebug + + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Files + uses: actions/cache@v2 + with: + path: | + ${{ steps.composer-cache.outputs.dir }} + **/.php_cs.cache + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run tests + uses: paambaati/codeclimate-action@v2.7.2 + env: + CC_TEST_REPORTER_ID: "945dfb58a832d233a3caeb84e3e6d3be212e8c7abcb48117fce63b9adcb43647" + with: + coverageCommand: ./vendor/bin/phpunit --testdox --stop-on-failure --coverage-clover=clover.xml diff --git a/vendor/php-imap/php-imap/.github/workflows/php_static_analysis.yml b/vendor/php-imap/php-imap/.github/workflows/php_static_analysis.yml new file mode 100644 index 0000000..eb68eca --- /dev/null +++ b/vendor/php-imap/php-imap/.github/workflows/php_static_analysis.yml @@ -0,0 +1,50 @@ +name: PHP Static Analysis & Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + static-analysis: + name: Static Analysis PHP ${{ matrix.php-versions }} + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-20.04'] + php-versions: ['7.4', '8.0', '8.1'] + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Files + uses: actions/cache@v2 + with: + path: | + ${{ steps.composer-cache.outputs.dir }} + **/.php_cs.cache + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run static analysis + run: composer run static-analysis diff --git a/vendor/php-imap/php-imap/.github/workflows/php_unit_tests.yml b/vendor/php-imap/php-imap/.github/workflows/php_unit_tests.yml new file mode 100644 index 0000000..fe1023f --- /dev/null +++ b/vendor/php-imap/php-imap/.github/workflows/php_unit_tests.yml @@ -0,0 +1,50 @@ +name: PHP Unit Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + phpunit: + name: PHP ${{ matrix.php-versions }} Unit Tests + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-20.04'] + php-versions: ['7.4', '8.0', '8.1'] + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Files + uses: actions/cache@v2 + with: + path: | + ${{ steps.composer-cache.outputs.dir }} + **/.php_cs.cache + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run tests + run: ./vendor/bin/phpunit --testdox --stop-on-failure diff --git a/vendor/php-imap/php-imap/.gitignore b/vendor/php-imap/php-imap/.gitignore index a4fed97..fb9818c 100644 --- a/vendor/php-imap/php-imap/.gitignore +++ b/vendor/php-imap/php-imap/.gitignore @@ -1,3 +1,11 @@ +# All of the vendor directory should be installed by running 'composer install' +vendor/ +composer.lock +*.phar +.php-cs-fixer.cache +coverage/ +psalm/cache/ + ################# ## Eclipse ################# @@ -153,6 +161,7 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +clover.xml #Translations *.mo @@ -162,3 +171,9 @@ pip-log.txt # Mac crap .DS_Store + +# NetBeans IDE +nbproject/ + +# Visual Studio Code +.vscode/ diff --git a/vendor/php-imap/php-imap/.php-cs-fixer.dist.php b/vendor/php-imap/php-imap/.php-cs-fixer.dist.php new file mode 100644 index 0000000..924761f --- /dev/null +++ b/vendor/php-imap/php-imap/.php-cs-fixer.dist.php @@ -0,0 +1,39 @@ +setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHP71Migration' => true, // @PHP72Migration does not exist + '@PHP71Migration:risky' => true, // @PHP72Migration:risky does not exist + 'array_syntax' => ['syntax' => 'short'], + 'declare_strict_types' => true, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => false, + ], + 'native_constant_invocation' => true, + 'native_function_invocation' => [ + 'strict' => false, + 'include' => ['@compiler_optimized'], + ], + 'no_superfluous_phpdoc_tags' => true, + 'ordered_class_elements' => true, + 'ordered_imports' => true, + 'php_unit_dedicate_assert' => ['target' => 'newest'], + 'php_unit_method_casing' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], + 'phpdoc_to_comment' => false, + 'void_return' => true, + ]) + ->setFinder(PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in(__DIR__) + ) +; diff --git a/vendor/php-imap/php-imap/LICENSE b/vendor/php-imap/php-imap/LICENSE index 551e727..df016aa 100644 --- a/vendor/php-imap/php-imap/LICENSE +++ b/vendor/php-imap/php-imap/LICENSE @@ -1,32 +1,21 @@ -ImapMailbox +The MIT License (MIT) -Copyright (c) 2012 by Barbushin Sergey . -All rights reserved. +Copyright (c) 2012 Sergey Barbushin -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/vendor/php-imap/php-imap/README.md b/vendor/php-imap/php-imap/README.md index 6ee45ce..ad978ba 100644 --- a/vendor/php-imap/php-imap/README.md +++ b/vendor/php-imap/php-imap/README.md @@ -1,50 +1,190 @@ -ImapMailbox is PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension +# PHP IMAP + +[![GitHub release](https://img.shields.io/github/release/barbushin/php-imap.svg?style=flat-square)](https://packagist.org/packages/php-imap/php-imap) +[![Supported PHP Version](https://img.shields.io/packagist/php-v/php-imap/php-imap.svg)](README.md) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Packagist](https://img.shields.io/packagist/dt/php-imap/php-imap.svg?style=flat-square)](https://packagist.org/packages/php-imap/php-imap) + +[![CI PHP Unit Tests](https://github.com/barbushin/php-imap/actions/workflows/php_unit_tests.yml/badge.svg?branch=master)](https://github.com/barbushin/php-imap/actions/workflows/php_unit_tests.yml) +[![CI PHP Static Analysis](https://github.com/barbushin/php-imap/actions/workflows/php_static_analysis.yml/badge.svg?branch=master)](https://github.com/barbushin/php-imap/actions/workflows/php_static_analysis.yml) +[![CI PHP Code Coverage](https://github.com/barbushin/php-imap/actions/workflows/php_code_coverage.yml/badge.svg?branch=master)](https://github.com/barbushin/php-imap/actions/workflows/php_code_coverage.yml) + +[![Maintainability](https://api.codeclimate.com/v1/badges/02f72a4fd695cb7e2976/maintainability)](https://codeclimate.com/github/barbushin/php-imap/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/02f72a4fd695cb7e2976/test_coverage)](https://codeclimate.com/github/barbushin/php-imap/test_coverage) +[![Type Coverage](https://shepherd.dev/github/barbushin/php-imap/coverage.svg)](https://shepherd.dev/github/barbushin/php-imap) + +Initially released in December 2012, the PHP IMAP Mailbox is a powerful and open source library to connect to a mailbox by POP3, IMAP and NNTP using the PHP IMAP extension. This library allows you to fetch emails from your email server. Extend the functionality or create powerful web applications to handle your incoming emails. ### Features -* Connect to mailbox by POP3/IMAP/NNTP (see [imap_open](http://php.net/imap_open)) -* Get mailbox status (see [imap_check](http://php.net/imap_check)) -* Receive emails (+attachments, +html body images) -* Search emails by custom criteria (see [imap_search](http://php.net/imap_search)) -* Change email status (see [imap_setflag_full](http://php.net/imap_setflag_full)) -* Delete email +* Connect to mailbox by POP3/IMAP/NNTP, using [PHP IMAP extension](http://php.net/manual/book.imap.php) +* Get emails with attachments and inline images +* Get emails filtered or sorted by custom criteria +* Mark emails as seen/unseen +* Delete emails +* Manage mailbox folders + +### Requirements + +| PHP Version | php-imap Version | php-imap status | +| ------------- | ------------- | ------------- | +| 5.6 | 3.x | End of life | +| 7.0 | 3.x | End of life | +| 7.1 | 3.x | End of life | +| 7.2 | 3.x, 4.x | End of life | +| 7.3 | 3.x, 4.x | End of life | +| 7.4 | >3.0.33, 4.x, 5.x | Active support | +| 8.0 | >3.0.33, 4.x, 5.x | Active support | +| 8.1 | >4.3.0, 5.x | Active support | + +* PHP `fileinfo` extension must be present; so make sure this line is active in your php.ini: `extension=php_fileinfo.dll` +* PHP `iconv` extension must be present; so make sure this line is active in your php.ini: `extension=php_iconv.dll` +* PHP `imap` extension must be present; so make sure this line is active in your php.ini: `extension=php_imap.dll` +* PHP `mbstring` extension must be present; so make sure this line is active in your php.ini: `extension=php_mbstring.dll` +* PHP `json` extension must be present; so make sure this line is active in your php.ini: `extension=json.dll` ### Installation by Composer - { - "require": { - "php-imap/php-imap": "~2.0" - } - } +Install the [latest available release](https://github.com/barbushin/php-imap/releases): -Or + $ composer require php-imap/php-imap - $ composer require php-imap/php-imap ~2.0 +Install the latest available and stable source code from `master`, which is may not released / tagged yet: -### Migration from `v1.*` to `v2.*` + $ composer require php-imap/php-imap:dev-master -Just add following code in the head of your script: +### Run Tests - use PhpImap\Mailbox as ImapMailbox; - use PhpImap\IncomingMail; - use PhpImap\IncomingMailAttachment; +Before you can run the any tests you may need to run `composer install` to install all (development) dependencies. -### [Usage example](https://github.com/barbushin/php-imap/blob/master/example/index.php) +#### Run all tests + +You can run all available tests by running the following command (inside of the installed `php-imap` directory): `composer run tests` + +#### Run only PHPUnit tests + +You can run all PHPUnit tests by running the following command (inside of the installed `php-imap` directory): `php vendor/bin/phpunit --testdox` + +### Integration with frameworks + +* Symfony - https://github.com/secit-pl/imap-bundle + +### Getting Started Example + +Below, you'll find an example code how you can use this library. For further information and other examples, you may take a look at the [wiki](https://github.com/barbushin/php-imap/wiki). + +By default, this library uses random filenames for attachments as identical file names from other emails would overwrite other attachments. If you want to keep the original file name, you can set the attachment filename mode to ``true``, but then you also need to ensure, that those files don't get overwritten by other emails for example. ```php -$mailbox = new PhpImap\Mailbox('{imap.gmail.com:993/imap/ssl}INBOX', 'some@gmail.com', '*********', __DIR__); -$mails = array(); +// Create PhpImap\Mailbox instance for all further actions +$mailbox = new PhpImap\Mailbox( + '{imap.gmail.com:993/imap/ssl}INBOX', // IMAP server and mailbox folder + 'some@gmail.com', // Username for the before configured mailbox + '*********', // Password for the before configured username + __DIR__, // Directory, where attachments will be saved (optional) + 'UTF-8', // Server encoding (optional) + true, // Trim leading/ending whitespaces of IMAP path (optional) + false // Attachment filename mode (optional; false = random filename; true = original filename) +); -$mailsIds = $mailbox->searchMailBox('ALL'); +// set some connection arguments (if appropriate) +$mailbox->setConnectionArgs( + CL_EXPUNGE // expunge deleted mails upon mailbox close + | OP_SECURE // don't do non-secure authentication +); + +try { + // Get all emails (messages) + // PHP.net imap_search criteria: http://php.net/manual/en/function.imap-search.php + $mailsIds = $mailbox->searchMailbox('ALL'); +} catch(PhpImap\Exceptions\ConnectionException $ex) { + echo "IMAP connection failed: " . implode(",", $ex->getErrors('all')); + die(); +} + +// If $mailsIds is empty, no emails could be found if(!$mailsIds) { die('Mailbox is empty'); } -$mailId = reset($mailsIds); -$mail = $mailbox->getMail($mailId); +// Get the first message +// If '__DIR__' was defined in the first line, it will automatically +// save all attachments to the specified directory +$mail = $mailbox->getMail($mailsIds[0]); -var_dump($mail); -var_dump($mail->getAttachments()); +// Show, if $mail has one or more attachments +echo "\nMail has attachments? "; +if($mail->hasAttachments()) { + echo "Yes\n"; +} else { + echo "No\n"; +} + +// Print all information of $mail +print_r($mail); + +// Print all attachements of $mail +echo "\n\nAttachments:\n"; +print_r($mail->getAttachments()); +``` + +Method `imap()` allows to call any [PHP IMAP function](https://www.php.net/manual/ref.imap.php) in a context of the instance. Example: + +```php +// Call imap_check() - see http://php.net/manual/function.imap-check.php +$info = $mailbox->imap('check'); + + +// Show current time for the mailbox +$currentServerTime = isset($info->Date) && $info->Date ? date('Y-m-d H:i:s', strtotime($info->Date)) : 'Unknown'; + +echo $currentServerTime; +``` + +Some request require much time and resources: + +```php +// If you don't need to grab attachments you can significantly increase performance of your application +$mailbox->setAttachmentsIgnore(true); + +// get the list of folders/mailboxes +$folders = $mailbox->getMailboxes('*'); + +// loop through mailboxs +foreach($folders as $folder) { + + // switch to particular mailbox + $mailbox->switchMailbox($folder['fullpath']); + + // search in particular mailbox + $mails_ids[$folder['fullpath']] = $mailbox->searchMailbox('SINCE "1 Jan 2018" BEFORE "28 Jan 2018"'); +} + +print_r($mails_ids); +``` + +### Upgrading from 3.x + +Prior to 3.1, `Mailbox` used a "magic" method (`Mailbox::imap()`), with the +class `Imap` now performing it's purpose to call many `imap_*` functions with +automated string encoding/decoding of arguments and return values: + +Before: + +```php + public function checkMailbox() + { + return $this->imap('check'); + } +``` + +After: + +```php + public function checkMailbox(): object + { + return Imap::check($this->getImapStream()); + } ``` ### Recommended diff --git a/vendor/php-imap/php-imap/composer-require-checker.config.json b/vendor/php-imap/php-imap/composer-require-checker.config.json new file mode 100644 index 0000000..9020d1d --- /dev/null +++ b/vendor/php-imap/php-imap/composer-require-checker.config.json @@ -0,0 +1,18 @@ +{ + "symbol-whitelist" : [ + "null", "true", "false", + "static", "self", "parent", + "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed", "never", + "IMAP\\Connection" + ], + "php-core-extensions" : [ + "Core", + "date", + "pcre", + "Phar", + "Reflection", + "SPL", + "standard" + ], + "scan-files" : [] +} diff --git a/vendor/php-imap/php-imap/composer.json b/vendor/php-imap/php-imap/composer.json index dfbec7b..9b44d8e 100644 --- a/vendor/php-imap/php-imap/composer.json +++ b/vendor/php-imap/php-imap/composer.json @@ -1,28 +1,72 @@ { - "name": "php-imap/php-imap", - "description": "PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension", - "keywords": [ - "PHP", - "IMAP", - "mail" - ], - "homepage": "https://github.com/barbushin/php-imap", - "license": "BSD 3-Clause", - "type": "library", - "authors": [ - { - "name": "Sergey Barbushin", - "homepage": "http://linkedin.com/in/barbushin", - "email": "barbushin@gmail.com" - } - ], - "require": { - "php": ">=5.3.0" - }, - "autoload": { - "psr-0": { - "PhpImap": "src/" - } - }, - "minimum-stability": "stable" + "name": "php-imap/php-imap", + "description": "Manage mailboxes, filter/get/delete emails in PHP (supports IMAP/POP3/NNTP)", + "keywords": [ + "PHP", + "mail", + "IMAP", + "POP3", + "mailbox", + "receive emails" + ], + "homepage": "https://github.com/barbushin/php-imap", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Sergey Barbushin", + "homepage": "http://linkedin.com/in/barbushin", + "email": "barbushin@gmail.com" + } + ], + "config": { + "sort-packages": true + }, + "require": { + "php": "^7.4 || ^8.0", + "ext-fileinfo": "*", + "ext-iconv": "*", + "ext-imap": "*", + "ext-mbstring": "*", + "ext-json": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "maglnet/composer-require-checker": "^2.0|^3.2", + "nikic/php-parser": "^4.3,<4.7|^4.10", + "paragonie/hidden-string": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "^8.5|^9.5", + "povils/phpmnd": "^2.2", + "psalm/plugin-phpunit": "^0.10.0|^0.15.1", + "roave/security-advisories": "dev-master", + "sebastian/phpcpd": "^4.1|^6.0" + }, + "scripts": { + "static-analysis": [ + "parallel-lint .php-cs-fixer.dist.php src tests examples", + "phpcpd src tests", + "composer-require-checker check --config-file=composer-require-checker.config.json ./composer.json", + "phpmnd ./ --exclude=./.github/ --exclude=./examples/ --exclude=./vendor/ --non-zero-exit-on-violation --hint", + "php-cs-fixer fix --allow-risky=yes --no-interaction --dry-run -v", + "psalm --show-info=false" + ], + "tests": [ + "@static-analysis", + "phpunit --testdox" + ] + }, + "suggest": { + "ext-fileinfo": "To facilitate IncomingMailAttachment::getFileInfo() auto-detection" + }, + "autoload-dev": { + "psr-4": { + "PhpImap\\": "tests/unit" + } + }, + "autoload": { + "psr-4": { + "PhpImap\\": "src/PhpImap" + } + } } diff --git a/vendor/php-imap/php-imap/examples/get_and_parse_all_emails_with_matching_subject.php b/vendor/php-imap/php-imap/examples/get_and_parse_all_emails_with_matching_subject.php new file mode 100644 index 0000000..ccd3d3d --- /dev/null +++ b/vendor/php-imap/php-imap/examples/get_and_parse_all_emails_with_matching_subject.php @@ -0,0 +1,74 @@ + + */ + declare(strict_types=1); + + require_once __DIR__.'/../vendor/autoload.php'; + + use PhpImap\Exceptions\ConnectionException; + use PhpImap\Mailbox; + + $mailbox = new Mailbox( + '{imap.gmail.com:993/imap/ssl}INBOX', // IMAP server and mailbox folder + 'some@gmail.com', // Username for the before configured mailbox + '*********', // Password for the before configured username + __DIR__, // Directory, where attachments will be saved (optional) + 'US-ASCII' // Server encoding (optional) + ); + + try { + $mail_ids = $mailbox->searchMailbox('SUBJECT "part of the subject"'); + } catch (ConnectionException $ex) { + exit('IMAP connection failed: '.$ex->getMessage()); + } catch (Exception $ex) { + exit('An error occured: '.$ex->getMessage()); + } + + foreach ($mail_ids as $mail_id) { + echo "+------ P A R S I N G ------+\n"; + + $email = $mailbox->getMail( + $mail_id, // ID of the email, you want to get + false // Do NOT mark emails as seen (optional) + ); + + echo 'from-name: '.(string) ($email->fromName ?? $email->fromAddress)."\n"; + echo 'from-email: '.(string) $email->fromAddress."\n"; + echo 'to: '.(string) $email->toString."\n"; + echo 'subject: '.(string) $email->subject."\n"; + echo 'message_id: '.(string) $email->messageId."\n"; + + echo 'mail has attachments? '; + if ($email->hasAttachments()) { + echo "Yes\n"; + } else { + echo "No\n"; + } + + if (!empty($email->getAttachments())) { + echo \count($email->getAttachments())." attachements\n"; + } + if ($email->textHtml) { + echo "Message HTML:\n".$email->textHtml; + } else { + echo "Message Plain:\n".$email->textPlain; + } + + if (!empty($email->autoSubmitted)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Auto-Reply ------+\n"; + } + + if (!empty($email_content->precedence)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Non-Delivery Report/Receipt ------+\n"; + } + } + + $mailbox->disconnect(); diff --git a/vendor/php-imap/php-imap/examples/get_and_parse_all_emails_without_saving_attachments.php b/vendor/php-imap/php-imap/examples/get_and_parse_all_emails_without_saving_attachments.php new file mode 100644 index 0000000..258129e --- /dev/null +++ b/vendor/php-imap/php-imap/examples/get_and_parse_all_emails_without_saving_attachments.php @@ -0,0 +1,84 @@ + + */ + declare(strict_types=1); + + require_once __DIR__.'/../vendor/autoload.php'; + + use PhpImap\Exceptions\ConnectionException; + use PhpImap\Mailbox; + + $mailbox = new Mailbox( + '{imap.gmail.com:993/imap/ssl}INBOX', // IMAP server and mailbox folder + 'some@gmail.com', // Username for the before configured mailbox + '*********', // Password for the before configured username + null, // Directory, where attachments will be saved (optional) + 'US-ASCII' // Server encoding (optional) + ); + + // OR + $mailbox = new Mailbox( + '{imap.gmail.com:993/imap/ssl}INBOX', // IMAP server and mailbox folder + 'some@gmail.com', // Username for the before configured mailbox + '*********' // Password for the before configured username + ); + + // If you haven't defined the server encoding (charset) in 'new Mailbox()', you can change it any time + $mailbox->setServerEncoding('US-ASCII'); + + try { + $mail_ids = $mailbox->searchMailbox('UNSEEN'); + } catch (ConnectionException $ex) { + exit('IMAP connection failed: '.$ex->getMessage()); + } catch (Exception $ex) { + exit('An error occured: '.$ex->getMessage()); + } + + foreach ($mail_ids as $mail_id) { + echo "+------ P A R S I N G ------+\n"; + + $email = $mailbox->getMail( + $mail_id, // ID of the email, you want to get + false // Do NOT mark emails as seen (optional) + ); + + echo 'from-name: '.(string) ($email->fromName ?? $email->fromAddress)."\n"; + echo 'from-email: '.(string) $email->fromAddress."\n"; + echo 'to: '.(string) $email->toString."\n"; + echo 'subject: '.(string) $email->subject."\n"; + echo 'message_id: '.(string) $email->messageId."\n"; + + echo 'mail has attachments? '; + if ($email->hasAttachments()) { + echo "Yes\n"; + } else { + echo "No\n"; + } + + if (!empty($email->getAttachments())) { + echo \count($email->getAttachments())." attachements\n"; + } + if ($email->textHtml) { + echo "Message HTML:\n".$email->textHtml; + } else { + echo "Message Plain:\n".$email->textPlain; + } + + if (!empty($email->autoSubmitted)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Auto-Reply ------+\n"; + } + + if (!empty($email_content->precedence)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Non-Delivery Report/Receipt ------+\n"; + } + } + + $mailbox->disconnect(); diff --git a/vendor/php-imap/php-imap/examples/get_and_parse_unseen_emails.php b/vendor/php-imap/php-imap/examples/get_and_parse_unseen_emails.php new file mode 100644 index 0000000..12eb8bb --- /dev/null +++ b/vendor/php-imap/php-imap/examples/get_and_parse_unseen_emails.php @@ -0,0 +1,74 @@ + + */ + declare(strict_types=1); + + require_once __DIR__.'/../vendor/autoload.php'; + + use PhpImap\Exceptions\ConnectionException; + use PhpImap\Mailbox; + + $mailbox = new Mailbox( + '{imap.gmail.com:993/imap/ssl}INBOX', // IMAP server and mailbox folder + 'some@gmail.com', // Username for the before configured mailbox + '*********', // Password for the before configured username + __DIR__, // Directory, where attachments will be saved (optional) + 'US-ASCII' // Server encoding (optional) + ); + + try { + $mail_ids = $mailbox->searchMailbox('UNSEEN'); + } catch (ConnectionException $ex) { + exit('IMAP connection failed: '.$ex->getErrors('first')); + } catch (Exception $ex) { + exit('An error occured: '.$ex->getMessage()); + } + + foreach ($mail_ids as $mail_id) { + echo "+------ P A R S I N G ------+\n"; + + $email = $mailbox->getMail( + $mail_id, // ID of the email, you want to get + false // Do NOT mark emails as seen (optional) + ); + + echo 'from-name: '.(string) ($email->fromName ?? $email->fromAddress)."\n"; + echo 'from-email: '.(string) $email->fromAddress."\n"; + echo 'to: '.(string) $email->toString."\n"; + echo 'subject: '.(string) $email->subject."\n"; + echo 'message_id: '.(string) $email->messageId."\n"; + + echo 'mail has attachments? '; + if ($email->hasAttachments()) { + echo "Yes\n"; + } else { + echo "No\n"; + } + + if (!empty($email->getAttachments())) { + echo \count($email->getAttachments())." attachements\n"; + } + if ($email->textHtml) { + echo "Message HTML:\n".$email->textHtml; + } else { + echo "Message Plain:\n".$email->textPlain; + } + + if (!empty($email->autoSubmitted)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Auto-Reply ------+\n"; + } + + if (!empty($email_content->precedence)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Non-Delivery Report/Receipt ------+\n"; + } + } + + $mailbox->disconnect(); diff --git a/vendor/php-imap/php-imap/examples/get_and_parse_unseen_emails_save_attachments_one_by_one.php b/vendor/php-imap/php-imap/examples/get_and_parse_unseen_emails_save_attachments_one_by_one.php new file mode 100644 index 0000000..54a4899 --- /dev/null +++ b/vendor/php-imap/php-imap/examples/get_and_parse_unseen_emails_save_attachments_one_by_one.php @@ -0,0 +1,92 @@ + + */ + declare(strict_types=1); + + require_once __DIR__.'/../vendor/autoload.php'; + + use PhpImap\Exceptions\ConnectionException; + use PhpImap\Mailbox; + + $mailbox = new Mailbox( + '{imap.gmail.com:993/imap/ssl}INBOX', // IMAP server and mailbox folder + 'some@gmail.com', // Username for the before configured mailbox + '*********' // Password for the before configured username + ); + + try { + $mail_ids = $mailbox->searchMailbox('UNSEEN'); + } catch (ConnectionException $ex) { + exit('IMAP connection failed: '.$ex->getMessage()); + } catch (Exception $ex) { + exit('An error occured: '.$ex->getMessage()); + } + + foreach ($mail_ids as $mail_id) { + echo "+------ P A R S I N G ------+\n"; + + $email = $mailbox->getMail( + $mail_id, // ID of the email, you want to get + false // Do NOT mark emails as seen (optional) + ); + + echo 'from-name: '.(string) ($email->fromName ?? $email->fromAddress)."\n"; + echo 'from-email: '.(string) $email->fromAddress."\n"; + echo 'to: '.(string) $email->toString."\n"; + echo 'subject: '.(string) $email->subject."\n"; + echo 'message_id: '.(string) $email->messageId."\n"; + + echo 'mail has attachments? '; + if ($email->hasAttachments()) { + echo "Yes\n"; + } else { + echo "No\n"; + } + + if (!empty($email->getAttachments())) { + echo \count($email->getAttachments())." attachements\n"; + } + + // Save attachments one by one + if (!$mailbox->getAttachmentsIgnore()) { + $attachments = $email->getAttachments(); + + foreach ($attachments as $attachment) { + echo '--> Saving '.(string) $attachment->name.'...'; + + // Set individually filePath for each single attachment + // In this case, every file will get the current Unix timestamp + $attachment->setFilePath(__DIR__.'/files/'.\time()); + + if ($attachment->saveToDisk()) { + echo "OK, saved!\n"; + } else { + echo "ERROR, could not save!\n"; + } + } + } + + if ($email->textHtml) { + echo "Message HTML:\n".$email->textHtml; + } else { + echo "Message Plain:\n".$email->textPlain; + } + + if (!empty($email->autoSubmitted)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Auto-Reply ------+\n"; + } + + if (!empty($email_content->precedence)) { + // Mark email as "read" / "seen" + $mailbox->markMailAsRead($mail_id); + echo "+------ IGNORING: Non-Delivery Report/Receipt ------+\n"; + } + } + + $mailbox->disconnect(); diff --git a/vendor/php-imap/php-imap/phpunit.xml b/vendor/php-imap/php-imap/phpunit.xml new file mode 100644 index 0000000..6e985a5 --- /dev/null +++ b/vendor/php-imap/php-imap/phpunit.xml @@ -0,0 +1,27 @@ + + + + + src + + + + + tests + + + diff --git a/vendor/php-imap/php-imap/psalm.baseline.xml b/vendor/php-imap/php-imap/psalm.baseline.xml new file mode 100644 index 0000000..12cc8fb --- /dev/null +++ b/vendor/php-imap/php-imap/psalm.baseline.xml @@ -0,0 +1,57 @@ + + + + + $mailbox + + + + + \is_int($section) + !\is_string($section) && !\is_int($section) + \is_resource($maybe) + + + + + \in_array($imapSearchOption, $supported_options, true) + \in_array($key, $supported_params, true) + + + $element->charset + $element->text + $element->text + + + setConnectionRetry + setConnectionRetryDelay + setExpungeOnDisconnect + renameMailbox + getListingFolders + searchMailboxFrom + searchMailboxFromDisableServerEncoding + searchMailboxMergeResults + searchMailboxMergeResultsDisableServerEncoding + saveMail + moveMail + copyMail + markMailAsUnread + markMailAsImportant + markMailsAsRead + markMailsAsUnread + markMailsAsImportant + getMailboxHeaders + getMailboxInfo + getQuotaLimit + getQuotaUsage + getSubscribedMailboxes + subscribeMailbox + unsubscribeMailbox + + + + + self::ANYTHING + + + diff --git a/vendor/php-imap/php-imap/psalm.xml b/vendor/php-imap/php-imap/psalm.xml new file mode 100644 index 0000000..dc5084c --- /dev/null +++ b/vendor/php-imap/php-imap/psalm.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/vendor/php-imap/php-imap/src/PhpImap/DataPartInfo.php b/vendor/php-imap/php-imap/src/PhpImap/DataPartInfo.php new file mode 100644 index 0000000..6884101 --- /dev/null +++ b/vendor/php-imap/php-imap/src/PhpImap/DataPartInfo.php @@ -0,0 +1,126 @@ +mail = $mail; + $this->id = $id; + $this->part = $part; + $this->encoding = $encoding; + $this->options = $options; + } + + public function fetch(): string + { + if (0 === $this->part) { + $this->data = Imap::body($this->mail->getImapStream(), $this->id, $this->options); + } else { + if (null !== $this->data) { + return $this->data; + } + $this->data = Imap::fetchbody($this->mail->getImapStream(), $this->id, $this->part, $this->options); + } + + return $this->decodeAfterFetch($this->data); + } + + public function decodeAfterFetch(string $data): string + { + switch ($this->encoding) { + case ENC8BIT: + $this->data = \imap_utf8((string) $data); + break; + case ENCBINARY: + $this->data = \imap_binary((string) $data); + break; + case ENCBASE64: + $this->data = \base64_decode((string) $data, false); + break; + case ENCQUOTEDPRINTABLE: + $this->data = \quoted_printable_decode((string) $data); + break; + } + + return $this->convertEncodingAfterFetch(); + } + + protected function convertEncodingAfterFetch(): string + { + if (isset($this->charset) && !empty(\trim($this->charset))) { + $this->data = $this->mail->decodeMimeStr( + (string) $this->data // Data to convert + ); + + $this->data = $this->mail->convertToUtf8( + $this->data, + $this->charset + ); + } + + return (null === $this->data) ? '' : $this->data; + } +} diff --git a/vendor/php-imap/php-imap/src/PhpImap/Exceptions/ConnectionException.php b/vendor/php-imap/php-imap/src/PhpImap/Exceptions/ConnectionException.php new file mode 100644 index 0000000..fb64497 --- /dev/null +++ b/vendor/php-imap/php-imap/src/PhpImap/Exceptions/ConnectionException.php @@ -0,0 +1,42 @@ +getMessage(); + + switch (strtolower($select)) { + case 'all': + return json_decode($message); + break; + default: + case 'first': + $message = json_decode($message); + + return $message[0]; + break; + case 'last': + $message = json_decode($message); + + return $message[\count($message) - 1]; + break; + } + } +} diff --git a/vendor/php-imap/php-imap/src/PhpImap/Exceptions/InvalidParameterException.php b/vendor/php-imap/php-imap/src/PhpImap/Exceptions/InvalidParameterException.php new file mode 100644 index 0000000..f91e302 --- /dev/null +++ b/vendor/php-imap/php-imap/src/PhpImap/Exceptions/InvalidParameterException.php @@ -0,0 +1,16 @@ +, + * type:int, + * subtype:string + * } + */ +final class Imap +{ + /** @psalm-var list */ + public const SORT_CRITERIA = [ + SORTARRIVAL, + SORTCC, + SORTDATE, + SORTFROM, + SORTSIZE, + SORTSUBJECT, + SORTTO, + ]; + + /** @psalm-var list */ + public const TIMEOUT_TYPES = [ + IMAP_CLOSETIMEOUT, + IMAP_OPENTIMEOUT, + IMAP_READTIMEOUT, + IMAP_WRITETIMEOUT, + ]; + + /** @psalm-var list */ + 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 $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 + */ + 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 */ + 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 + */ + 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 */ + return $result; + } + + /** + * @param resource|false $imap_stream + * + * @return object[] + * + * @psalm-return list + */ + 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 */ + 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 + */ + 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 $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 $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 + */ + 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 */ + 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 $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 + */ + 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 */ + 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 $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; + } +} diff --git a/vendor/php-imap/php-imap/src/PhpImap/IncomingMail.php b/vendor/php-imap/php-imap/src/PhpImap/IncomingMail.php index 7512eee..d955261 100644 --- a/vendor/php-imap/php-imap/src/PhpImap/IncomingMail.php +++ b/vendor/php-imap/php-imap/src/PhpImap/IncomingMail.php @@ -1,65 +1,247 @@ -, 1:list} + */ + protected $dataInfo = [[], []]; - public $to = array(); - public $toString; - public $cc = array(); - public $replyTo = array(); + /** @var string|null */ + private $textPlain; - public $messageId; + /** @var string|null */ + private $textHtml; - public $textPlain; - public $textHtml; - /** @var IncomingMailAttachment[] */ - protected $attachments = array(); + /** + * __get() is utilized for reading data from inaccessible (protected + * or private) or non-existing properties. + * + * @param string $name Name of the property (eg. textPlain) + * + * @return string Value of the property (eg. Plain text message) + */ + public function __get(string $name): string + { + $type = false; + if ('textPlain' == $name) { + $type = DataPartInfo::TEXT_PLAIN; + } + if ('textHtml' == $name) { + $type = DataPartInfo::TEXT_HTML; + } + if (('textPlain' === $name || 'textHtml' === $name) && isset($this->$name)) { + return (string) $this->$name; + } + if (false === $type) { + \trigger_error("Undefined property: IncomingMail::$name"); + } + if (!isset($this->$name)) { + $this->$name = ''; + } + foreach ($this->dataInfo[$type] as $data) { + $this->$name .= \trim($data->fetch()); + } - public function addAttachment(IncomingMailAttachment $attachment) { - $this->attachments[$attachment->id] = $attachment; - } + /** @var string */ + return $this->$name; + } - /** - * @return IncomingMailAttachment[] - */ - public function getAttachments() { - return $this->attachments; - } + /** + * The method __isset() is triggered by calling isset() or empty() + * on inaccessible (protected or private) or non-existing properties. + * + * @param string $name Name of the property (eg. textPlain) + * + * @return bool True, if property is set or empty + */ + public function __isset(string $name): bool + { + self::__get($name); - /** - * Get array of internal HTML links placeholders - * @return array attachmentId => link placeholder - */ - public function getInternalLinksPlaceholders() { - return preg_match_all('/=["\'](ci?d:([\w\.%*@-]+))["\']/i', $this->textHtml, $matches) ? array_combine($matches[2], $matches[1]) : array(); + return isset($this->$name); + } - } + public function setHeader(IncomingMailHeader $header): void + { + /** @psalm-var array */ + $array = \get_object_vars($header); + foreach ($array as $property => $value) { + $this->$property = $value; + } + } - public function replaceInternalLinks($baseUri) { - $baseUri = rtrim($baseUri, '\\/') . '/'; - $fetchedHtml = $this->textHtml; - foreach($this->getInternalLinksPlaceholders() as $attachmentId => $placeholder) { - if(isset($this->attachments[$attachmentId])) { - $fetchedHtml = str_replace($placeholder, $baseUri . basename($this->attachments[$attachmentId]->filePath), $fetchedHtml); - } - } - return $fetchedHtml; - } -} - -class IncomingMailAttachment { - - public $id; - public $name; - public $filePath; + /** + * @param DataPartInfo::TEXT_PLAIN|DataPartInfo::TEXT_HTML $type + */ + public function addDataPartInfo(DataPartInfo $dataInfo, int $type): void + { + $this->dataInfo[$type][] = $dataInfo; + } + + public function addAttachment(IncomingMailAttachment $attachment): void + { + if (!\is_string($attachment->id)) { + throw new InvalidArgumentException('Argument 1 passed to '.__METHOD__.'() does not have an id specified!'); + } + $this->attachments[$attachment->id] = $attachment; + + $this->setHasAttachments(true); + } + + /** + * Sets property $hasAttachments. + * + * @param bool $hasAttachments True, if IncomingMail[] has one or more attachments + */ + public function setHasAttachments(bool $hasAttachments): void + { + $this->hasAttachments = $hasAttachments; + } + + /** + * Returns, if the mail has attachments or not. + * + * @return bool true or false + */ + public function hasAttachments(): bool + { + return $this->hasAttachments; + } + + /** + * @return IncomingMailAttachment[] + */ + public function getAttachments(): array + { + return $this->attachments; + } + + /** + * @param string $id The attachment id + */ + public function removeAttachment(string $id): bool + { + if (!isset($this->attachments[$id])) { + return false; + } + + unset($this->attachments[$id]); + + $this->setHasAttachments([] !== $this->attachments); + + return true; + } + + /** + * Get array of internal HTML links placeholders. + * + * @return array attachmentId => link placeholder + * + * @psalm-return array + */ + public function getInternalLinksPlaceholders(): array + { + $fetchedHtml = $this->__get('textHtml'); + + $match = \preg_match_all('/=["\'](ci?d:([\w\.%*@-]+))["\']/i', $fetchedHtml, $matches); + + /** @psalm-var array{1:list, 2:list} */ + $matches = $matches; + + return $match ? \array_combine($matches[2], $matches[1]) : []; + } + + public function replaceInternalLinks(string $baseUri): string + { + $baseUri = \rtrim($baseUri, '\\/').'/'; + $fetchedHtml = $this->textHtml; + $search = []; + $replace = []; + foreach ($this->getInternalLinksPlaceholders() as $attachmentId => $placeholder) { + foreach ($this->attachments as $attachment) { + if ($attachment->contentId == $attachmentId) { + if (!\is_string($attachment->id)) { + throw new InvalidArgumentException('Argument 1 passed to '.__METHOD__.'() does not have an id specified!'); + } + $search[] = $placeholder; + $replace[] = $baseUri.\basename($this->attachments[$attachment->id]->filePath); + } + } + } + + /** @psalm-var string */ + return \str_replace($search, $replace, $fetchedHtml); + } + + /** + * Embed inline image attachments as base64 to allow for + * email html to display inline images automatically. + */ + public function embedImageAttachments(): void + { + $fetchedHtml = $this->__get('textHtml'); + + \preg_match_all("/\bcid:[^'\"\s]{1,256}/mi", $fetchedHtml, $matches); + + if (isset($matches[0]) && \is_array($matches[0]) && \count($matches[0])) { + /** @var list */ + $matches = $matches[0]; + $attachments = $this->getAttachments(); + foreach ($matches as $match) { + $cid = \str_replace('cid:', '', $match); + + foreach ($attachments as $attachment) { + /** + * Inline images can contain a "Content-Disposition: inline", but only a "Content-ID" is also enough. + * See https://github.com/barbushin/php-imap/issues/569. + */ + if ($attachment->contentId == $cid || 'inline' == \mb_strtolower((string) $attachment->disposition)) { + $contents = $attachment->getContents(); + $contentType = $attachment->getFileInfo(FILEINFO_MIME_TYPE); + + if (!\strstr($contentType, 'image')) { + continue; + } elseif (!\is_string($attachment->id)) { + throw new InvalidArgumentException('Argument 1 passed to '.__METHOD__.'() does not have an id specified!'); + } + + $base64encoded = \base64_encode($contents); + $replacement = 'data:'.$contentType.';base64, '.$base64encoded; + + $this->textHtml = \str_replace($match, $replacement, $this->textHtml); + + $this->removeAttachment($attachment->id); + } + } + } + } + } } diff --git a/vendor/php-imap/php-imap/src/PhpImap/IncomingMailAttachment.php b/vendor/php-imap/php-imap/src/PhpImap/IncomingMailAttachment.php new file mode 100644 index 0000000..2e29ce8 --- /dev/null +++ b/vendor/php-imap/php-imap/src/PhpImap/IncomingMailAttachment.php @@ -0,0 +1,169 @@ +file_path)) { + return false; + } + + $this->filePath = $this->file_path; + + if (@\file_exists($this->file_path)) { + return $this->filePath; + } + + return $this->filePath; + } + + /** + * Sets the file path. + * + * @param string $filePath File path incl. file name and optional extension + */ + public function setFilePath(string $filePath): void + { + $this->file_path = $filePath; + } + + /** + * Sets the data part info. + * + * @param DataPartInfo $dataInfo Date info (file content) + */ + public function addDataPartInfo(DataPartInfo $dataInfo): void + { + $this->dataInfo = $dataInfo; + } + + /** + * Gets information about a file. + * + * @param int $fileinfo_const Any predefined constant. See https://www.php.net/manual/en/fileinfo.constants.php + * + * @psalm-param fileinfoconst $fileinfo_const + */ + public function getFileInfo(int $fileinfo_const = FILEINFO_NONE): string + { + $finfo = new finfo($fileinfo_const); + + return $finfo->buffer($this->getContents()); + } + + /** + * Gets the file content. + */ + public function getContents(): string + { + if (null === $this->dataInfo) { + throw new UnexpectedValueException(static::class.'::$dataInfo has not been set by calling '.self::class.'::addDataPartInfo()'); + } + + return $this->dataInfo->fetch(); + } + + /** + * Saves the attachment object on the disk. + * + * @return bool True, if it could save the attachment on the disk + */ + public function saveToDisk(): bool + { + if (null === $this->dataInfo) { + return false; + } + + if (false === \file_put_contents($this->__get('filePath'), $this->dataInfo->fetch())) { + unset($this->filePath, $this->file_path); + + return false; + } + + return true; + } +} diff --git a/vendor/php-imap/php-imap/src/PhpImap/IncomingMailHeader.php b/vendor/php-imap/php-imap/src/PhpImap/IncomingMailHeader.php new file mode 100644 index 0000000..908c4f3 --- /dev/null +++ b/vendor/php-imap/php-imap/src/PhpImap/IncomingMailHeader.php @@ -0,0 +1,149 @@ + + */ + public $to = []; + + /** @var string|null */ + public $toString; + + /** + * @var (string|null)[] + * + * @psalm-var array + */ + public $cc = []; + + /** @var string|null */ + public $ccString; + + /** + * @var (string|null)[] + * + * @psalm-var array + */ + public $bcc = []; + + /** + * @var (string|null)[] + * + * @psalm-var array + */ + public $replyTo = []; + + /** @var string|null */ + public $messageId; +} diff --git a/vendor/php-imap/php-imap/src/PhpImap/Mailbox.php b/vendor/php-imap/php-imap/src/PhpImap/Mailbox.php index 2d9a7b8..8e1803b 100644 --- a/vendor/php-imap/php-imap/src/PhpImap/Mailbox.php +++ b/vendor/php-imap/php-imap/src/PhpImap/Mailbox.php @@ -1,611 +1,2069 @@ -, + * type:int, + * subtype:string + * } + * @psalm-type HOSTNAMEANDADDRESS_ENTRY = object{host?:string, personal?:string, mailbox:string} + * @psalm-type HOSTNAMEANDADDRESS = array{0:HOSTNAMEANDADDRESS_ENTRY, 1?:HOSTNAMEANDADDRESS_ENTRY} + * @psalm-type COMPOSE_ENVELOPE = array{ + * subject?:string + * } + * @psalm-type COMPOSE_BODY = list + * + * @todo see @todo of Imap::mail_compose() */ -class Mailbox { +class Mailbox +{ + public const EXPECTED_SIZE_OF_MESSAGE_AS_ARRAY = 2; - protected $imapPath; - protected $imapLogin; - protected $imapPassword; - protected $imapOptions = 0; - protected $imapRetriesNum = 0; - protected $imapParams = array(); - protected $serverEncoding; - protected $attachmentsDir; + public const MAX_LENGTH_FILEPATH = 255; - public function __construct($imapPath, $login, $password, $attachmentsDir = null, $serverEncoding = 'UTF-8') { - $this->imapPath = $imapPath; - $this->imapLogin = $login; - $this->imapPassword = $password; - $this->serverEncoding = strtoupper($serverEncoding); - if($attachmentsDir) { - if(!is_dir($attachmentsDir)) { - throw new Exception('Directory "' . $attachmentsDir . '" not found'); - } - $this->attachmentsDir = rtrim(realpath($attachmentsDir), '\\/'); - } - } + public const PART_TYPE_TWO = 2; - /** - * Set custom connection arguments of imap_open method. See http://php.net/imap_open - * @param int $options - * @param int $retriesNum - * @param array $params - */ - public function setConnectionArgs($options = 0, $retriesNum = 0, array $params = null) { - $this->imapOptions = $options; - $this->imapRetriesNum = $retriesNum; - $this->imapParams = $params; - } + public const IMAP_OPTIONS_SUPPORTED_VALUES = + OP_READONLY // 2 + | OP_ANONYMOUS // 4 + | OP_HALFOPEN // 64 + | CL_EXPUNGE // 32768 + | OP_DEBUG // 1 + | OP_SHORTCACHE // 8 + | OP_SILENT // 16 + | OP_PROTOTYPE // 32 + | OP_SECURE // 256 + ; - /** - * Get IMAP mailbox connection stream - * @param bool $forceConnection Initialize connection if it's not initialized - * @return null|resource - */ - public function getImapStream($forceConnection = true) { - static $imapStream; - if($forceConnection) { - if($imapStream && (!is_resource($imapStream) || !imap_ping($imapStream))) { - $this->disconnect(); - $imapStream = null; - } - if(!$imapStream) { - $imapStream = $this->initImapStream(); - } - } - return $imapStream; - } + /** @var string */ + public $decodeMimeStrDefaultCharset = 'default'; - protected function initImapStream() { - $imapStream = @imap_open($this->imapPath, $this->imapLogin, $this->imapPassword, $this->imapOptions, $this->imapRetriesNum, $this->imapParams); - if(!$imapStream) { - throw new Exception('Connection error: ' . imap_last_error()); - } - return $imapStream; - } + /** @var string */ + protected $imapPath; - protected function disconnect() { - $imapStream = $this->getImapStream(false); - if($imapStream && is_resource($imapStream)) { - imap_close($imapStream, CL_EXPUNGE); - } - } + /** @var string */ + protected $imapLogin; - /** - * Get information about the current mailbox. - * - * Returns the information in an object with following properties: - * Date - current system time formatted according to RFC2822 - * Driver - protocol used to access this mailbox: POP3, IMAP, NNTP - * Mailbox - the mailbox name - * Nmsgs - number of mails in the mailbox - * Recent - number of recent mails in the mailbox - * - * @return stdClass - */ - public function checkMailbox() { - return imap_check($this->getImapStream()); - } + /** @var string */ + protected $imapPassword; - /** - * Creates a new mailbox specified by mailbox. - * - * @return bool - */ + /** @var int */ + protected $imapSearchOption = SE_UID; - public function createMailbox() { - return imap_createmailbox($this->getImapStream(), imap_utf7_encode($this->imapPath)); - } + /** @var int */ + protected $connectionRetry = 0; - /** - * Gets status information about the given mailbox. - * - * This function returns an object containing status information. - * The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity. - * - * @return stdClass if the box doesn't exist - */ + /** @var int */ + protected $connectionRetryDelay = 100; - public function statusMailbox() { - return imap_status($this->getImapStream(), $this->imapPath, SA_ALL); - } + /** @var int */ + protected $imapOptions = 0; + /** @var int */ + protected $imapRetriesNum = 0; - /** - * Gets listing the folders - * - * This function returns an object containing listing the folders. - * The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity. - * - * @return array listing the folders - */ + /** @psalm-var array{DISABLE_AUTHENTICATOR?:string} */ + protected $imapParams = []; - public function getListingFolders() { - $folders = imap_list($this->getImapStream(), $this->imapPath, "*"); - foreach ($folders as $key => $folder) - { - $folder = str_replace($this->imapPath, "", imap_utf7_decode($folder)); - $folders[$key] = $folder; - } - return $folders; - } + /** @var string */ + protected $serverEncoding = 'UTF-8'; + /** @var string|null */ + protected $attachmentsDir = null; - /** - * This function performs a search on the mailbox currently opened in the given IMAP stream. - * For example, to match all unanswered mails sent by Mom, you'd use: "UNANSWERED FROM mom". - * Searches appear to be case insensitive. This list of criteria is from a reading of the UW - * c-client source code and may be incomplete or inaccurate (see also RFC2060, section 6.4.4). - * - * @param string $criteria String, delimited by spaces, in which the following keywords are allowed. Any multi-word arguments (e.g. FROM "joey smith") must be quoted. Results will match all criteria entries. - * ALL - return all mails matching the rest of the criteria - * ANSWERED - match mails with the \\ANSWERED flag set - * BCC "string" - match mails with "string" in the Bcc: field - * BEFORE "date" - match mails with Date: before "date" - * BODY "string" - match mails with "string" in the body of the mail - * CC "string" - match mails with "string" in the Cc: field - * DELETED - match deleted mails - * FLAGGED - match mails with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set - * FROM "string" - match mails with "string" in the From: field - * KEYWORD "string" - match mails with "string" as a keyword - * NEW - match new mails - * OLD - match old mails - * ON "date" - match mails with Date: matching "date" - * RECENT - match mails with the \\RECENT flag set - * SEEN - match mails that have been read (the \\SEEN flag is set) - * SINCE "date" - match mails with Date: after "date" - * SUBJECT "string" - match mails with "string" in the Subject: - * TEXT "string" - match mails with text "string" - * TO "string" - match mails with "string" in the To: - * UNANSWERED - match mails that have not been answered - * UNDELETED - match mails that are not deleted - * UNFLAGGED - match mails that are not flagged - * UNKEYWORD "string" - match mails that do not have the keyword "string" - * UNSEEN - match mails which have not been read yet - * - * @return array Mails ids - */ - public function searchMailbox($criteria = 'ALL') { - $mailsIds = imap_search($this->getImapStream(), $criteria, SE_UID, $this->serverEncoding); - return $mailsIds ? $mailsIds : array(); - } - - /** - * Save mail body. - * @return bool - */ - public function saveMail($mailId, $filename = 'email.eml') { - return imap_savebody($this->getImapStream(), $filename, $mailId, "", FT_UID); - } - - /** - * Marks mails listed in mailId for deletion. - * @return bool - */ - public function deleteMail($mailId) { - return imap_delete($this->getImapStream(), $mailId, FT_UID); - } - - public function moveMail($mailId, $mailBox) { - return imap_mail_move($this->getImapStream(), $mailId, $mailBox, CP_UID) && $this->expungeDeletedMails(); - } - - /** - * Deletes all the mails marked for deletion by imap_delete(), imap_mail_move(), or imap_setflag_full(). - * @return bool - */ - public function expungeDeletedMails() { - return imap_expunge($this->getImapStream()); - } - - /** - * Add the flag \Seen to a mail. - * @return bool - */ - public function markMailAsRead($mailId) { - return $this->setFlag(array($mailId), '\\Seen'); - } - - /** - * Remove the flag \Seen from a mail. - * @return bool - */ - public function markMailAsUnread($mailId) { - return $this->clearFlag(array($mailId), '\\Seen'); - } - - /** - * Add the flag \Flagged to a mail. - * @return bool - */ - public function markMailAsImportant($mailId) { - return $this->setFlag(array($mailId), '\\Flagged'); - } - - /** - * Add the flag \Seen to a mails. - * @return bool - */ - public function markMailsAsRead(array $mailId) { - return $this->setFlag($mailId, '\\Seen'); - } - - /** - * Remove the flag \Seen from some mails. - * @return bool - */ - public function markMailsAsUnread(array $mailId) { - return $this->clearFlag($mailId, '\\Seen'); - } - - /** - * Add the flag \Flagged to some mails. - * @return bool - */ - public function markMailsAsImportant(array $mailId) { - return $this->setFlag($mailId, '\\Flagged'); - } - - /** - * Causes a store to add the specified flag to the flags set for the mails in the specified sequence. - * - * @param array $mailsIds - * @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060. - * @return bool - */ - public function setFlag(array $mailsIds, $flag) { - return imap_setflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID); - } - - /** - * Cause a store to delete the specified flag to the flags set for the mails in the specified sequence. - * - * @param array $mailsIds - * @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060. - * @return bool - */ - public function clearFlag(array $mailsIds, $flag) { - return imap_clearflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID); - } - - /** - * Fetch mail headers for listed mails ids - * - * Returns an array of objects describing one mail header each. The object will only define a property if it exists. The possible properties are: - * subject - the mails subject - * from - who sent it - * to - recipient - * date - when was it sent - * message_id - Mail-ID - * references - is a reference to this mail id - * in_reply_to - is a reply to this mail id - * size - size in bytes - * uid - UID the mail has in the mailbox - * msgno - mail sequence number in the mailbox - * recent - this mail is flagged as recent - * flagged - this mail is flagged - * answered - this mail is flagged as answered - * deleted - this mail is flagged for deletion - * seen - this mail is flagged as already read - * draft - this mail is flagged as being a draft - * - * @param array $mailsIds - * @return array - */ - public function getMailsInfo(array $mailsIds) { - $mails = imap_fetch_overview($this->getImapStream(), implode(',', $mailsIds), FT_UID); - if(is_array($mails) && count($mails)) - { - foreach($mails as &$mail) - { - if(isset($mail->subject)) { - $mail->subject = $this->decodeMimeStr($mail->subject, $this->serverEncoding); - } - if(isset($mail->from)) { - $mail->from = $this->decodeMimeStr($mail->from, $this->serverEncoding); - } - if(isset($mail->to)) { - $mail->to = $this->decodeMimeStr($mail->to, $this->serverEncoding); - } - } - } - return $mails; - } - - /** - * Get information about the current mailbox. - * - * Returns an object with following properties: - * Date - last change (current datetime) - * Driver - driver - * Mailbox - name of the mailbox - * Nmsgs - number of messages - * Recent - number of recent messages - * Unread - number of unread messages - * Deleted - number of deleted messages - * Size - mailbox size - * - * @return object Object with info | FALSE on failure - */ - - public function getMailboxInfo() { - return imap_mailboxmsginfo($this->getImapStream()); - } - - /** - * Gets mails ids sorted by some criteria - * - * Criteria can be one (and only one) of the following constants: - * SORTDATE - mail Date - * SORTARRIVAL - arrival date (default) - * SORTFROM - mailbox in first From address - * SORTSUBJECT - mail subject - * SORTTO - mailbox in first To address - * SORTCC - mailbox in first cc address - * SORTSIZE - size of mail in octets - * - * @param int $criteria - * @param bool $reverse - * @return array Mails ids - */ - public function sortMails($criteria = SORTARRIVAL, $reverse = true) { - return imap_sort($this->getImapStream(), $criteria, $reverse, SE_UID); - } - - /** - * Get mails count in mail box - * @return int - */ - public function countMails() { - return imap_num_msg($this->getImapStream()); - } - - /** - * Retrieve the quota settings per user - * @return array - FALSE in the case of call failure - */ - protected function getQuota() { - return imap_get_quotaroot($this->getImapStream(), 'INBOX'); - } - - /** - * Return quota limit in KB - * @return int - FALSE in the case of call failure - */ - public function getQuotaLimit() { - $quota = $this->getQuota(); - if(is_array($quota)) { - $quota = $quota['STORAGE']['limit']; - } - return $quota; - } - - /** - * Return quota usage in KB - * @return int - FALSE in the case of call failure - */ - public function getQuotaUsage() { - $quota = $this->getQuota(); - if(is_array($quota)) { - $quota = $quota['STORAGE']['usage']; - } - return $quota; - } + /** @var bool */ + protected $expungeOnDisconnect = true; /** - * Get mail data + * @var int[] * - * @param $mailId - * @param bool $markAsSeen - * @return IncomingMail + * @psalm-var array{1?:int, 2?:int, 3?:int, 4?:int} */ - public function getMail($mailId, $markAsSeen = true) { - $head = imap_rfc822_parse_headers(imap_fetchheader($this->getImapStream(), $mailId, FT_UID)); + protected $timeouts = []; - $mail = new IncomingMail(); - $mail->id = $mailId; - $mail->date = date('Y-m-d H:i:s', isset($head->date) ? strtotime(preg_replace('/\(.*?\)/', '', $head->date)) : time()); - $mail->subject = isset($head->subject) ? $this->decodeMimeStr($head->subject, $this->serverEncoding) : null; - $mail->fromName = isset($head->from[0]->personal) ? $this->decodeMimeStr($head->from[0]->personal, $this->serverEncoding) : null; - $mail->fromAddress = strtolower($head->from[0]->mailbox . '@' . $head->from[0]->host); + /** @var bool */ + protected $attachmentsIgnore = false; - if(isset($head->to)) { - $toStrings = array(); - foreach($head->to as $to) { - if(!empty($to->mailbox) && !empty($to->host)) { - $toEmail = strtolower($to->mailbox . '@' . $to->host); - $toName = isset($to->personal) ? $this->decodeMimeStr($to->personal, $this->serverEncoding) : null; - $toStrings[] = $toName ? "$toName <$toEmail>" : $toEmail; - $mail->to[$toEmail] = $toName; - } - } - $mail->toString = implode(', ', $toStrings); - } + /** @var string */ + protected $pathDelimiter = '.'; - if(isset($head->cc)) { - foreach($head->cc as $cc) { - $mail->cc[strtolower($cc->mailbox . '@' . $cc->host)] = isset($cc->personal) ? $this->decodeMimeStr($cc->personal, $this->serverEncoding) : null; - } - } + /** @var string */ + protected $mailboxFolder; - if(isset($head->reply_to)) { - foreach($head->reply_to as $replyTo) { - $mail->replyTo[strtolower($replyTo->mailbox . '@' . $replyTo->host)] = isset($replyTo->personal) ? $this->decodeMimeStr($replyTo->personal, $this->serverEncoding) : null; - } - } + /** @var bool|false */ + protected $attachmentFilenameMode = false; - if(isset($head->message_id)) { - $mail->messageId = $head->message_id; - } + /** @var resource|null */ + private $imapStream; - $mailStructure = imap_fetchstructure($this->getImapStream(), $mailId, FT_UID); + /** + * @throws InvalidParameterException + */ + public function __construct(string $imapPath, string $login, string $password, string $attachmentsDir = null, string $serverEncoding = 'UTF-8', bool $trimImapPath = true, bool $attachmentFilenameMode = false) + { + $this->imapPath = (true == $trimImapPath) ? \trim($imapPath) : $imapPath; + $this->imapLogin = \trim($login); + $this->imapPassword = $password; + $this->setServerEncoding($serverEncoding); + if (null != $attachmentsDir) { + $this->setAttachmentsDir($attachmentsDir); + } + $this->setAttachmentFilenameMode($attachmentFilenameMode); - if(empty($mailStructure->parts)) { - $this->initMailPart($mail, $mailStructure, 0, $markAsSeen); - } - else { - foreach($mailStructure->parts as $partNum => $partStructure) { - $this->initMailPart($mail, $partStructure, $partNum + 1, $markAsSeen); - } - } + $this->setMailboxFolder(); + } - return $mail; - } + /** + * Disconnects from the IMAP server / mailbox. + */ + public function __destruct() + { + $this->disconnect(); + } - protected function initMailPart(IncomingMail $mail, $partStructure, $partNum, $markAsSeen = true) { - $options = FT_UID; - if(!$markAsSeen) { + /** + * Sets / Changes the path delimiter character (Supported values: '.', '/'). + * + * @param string $delimiter Path delimiter + * + * @throws InvalidParameterException + */ + public function setPathDelimiter(string $delimiter): void + { + if (!$this->validatePathDelimiter($delimiter)) { + throw new InvalidParameterException('setPathDelimiter() can only set the delimiter to these characters: ".", "/"'); + } + + $this->pathDelimiter = $delimiter; + } + + /** + * Returns the current set path delimiter character. + * + * @return string Path delimiter + */ + public function getPathDelimiter(): string + { + return $this->pathDelimiter; + } + + /** + * Validates the given path delimiter character. + * + * @param string $delimiter Path delimiter + * + * @return bool true (supported) or false (unsupported) + * + * @psalm-pure + */ + public function validatePathDelimiter(string $delimiter): bool + { + $supported_delimiters = ['.', '/']; + + if (!\in_array($delimiter, $supported_delimiters)) { + return false; + } + + return true; + } + + /** + * Returns the current set server encoding. + * + * @return string Server encoding (eg. 'UTF-8') + */ + public function getServerEncoding(): string + { + return $this->serverEncoding; + } + + /** + * Sets / Changes the server encoding. + * + * @param string $serverEncoding Server encoding (eg. 'UTF-8') + * + * @throws InvalidParameterException + */ + public function setServerEncoding(string $serverEncoding): void + { + $serverEncoding = \strtoupper(\trim($serverEncoding)); + + $supported_encodings = \array_map('strtoupper', \mb_list_encodings()); + + if (!\in_array($serverEncoding, $supported_encodings) && 'US-ASCII' != $serverEncoding) { + throw new InvalidParameterException('"'.$serverEncoding.'" is not supported by setServerEncoding(). Your system only supports these encodings: US-ASCII, '.\implode(', ', $supported_encodings)); + } + + $this->serverEncoding = $serverEncoding; + } + + /** + * Returns the current set attachment filename mode. + * + * @return bool Attachment filename mode (e.g. true) + */ + public function getAttachmentFilenameMode(): bool + { + return $this->attachmentFilenameMode; + } + + /** + * Sets / Changes the attachment filename mode. + * + * @param bool $attachmentFilenameMode Attachment filename mode (e.g. false) + * + * @throws InvalidParameterException + */ + public function setAttachmentFilenameMode(bool $attachmentFilenameMode): void + { + if (!\is_bool($attachmentFilenameMode)) { + throw new InvalidParameterException('"'.$attachmentFilenameMode.'" is not supported by setOriginalAttachmentFilename(). Only boolean values are allowed: true (use original filename), false (use random generated filename)'); + } + + $this->attachmentFilenameMode = $attachmentFilenameMode; + } + + /** + * Returns the current set IMAP search option. + * + * @return int IMAP search option (eg. 'SE_UID') + */ + public function getImapSearchOption(): int + { + return $this->imapSearchOption; + } + + /** + * Sets / Changes the IMAP search option. + * + * @param int $imapSearchOption IMAP search option (eg. 'SE_UID') + * + * @psalm-param 1|2 $imapSearchOption + * + * @throws InvalidParameterException + */ + public function setImapSearchOption(int $imapSearchOption): void + { + $supported_options = [SE_FREE, SE_UID]; + + if (!\in_array($imapSearchOption, $supported_options, true)) { + throw new InvalidParameterException('"'.$imapSearchOption.'" is not supported by setImapSearchOption(). Supported options are SE_FREE and SE_UID.'); + } + + $this->imapSearchOption = $imapSearchOption; + } + + /** + * Set $this->attachmentsIgnore param. Allow to ignore attachments when they are not required and boost performance. + */ + public function setAttachmentsIgnore(bool $attachmentsIgnore): void + { + $this->attachmentsIgnore = $attachmentsIgnore; + } + + /** + * Get $this->attachmentsIgnore param. + * + * @return bool $attachmentsIgnore + */ + public function getAttachmentsIgnore(): bool + { + return $this->attachmentsIgnore; + } + + /** + * Sets the timeout of all or one specific type. + * + * @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(int $timeout, array $types = [IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT]): void + { + $supported_types = [IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT]; + + $found_types = \array_intersect($types, $supported_types); + + if (\count($types) != \count($found_types)) { + 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); + } + + /** + * Returns the IMAP login (usually an email address). + * + * @return string IMAP login + */ + public function getLogin(): string + { + return $this->imapLogin; + } + + /** + * Set custom connection arguments of imap_open method. See http://php.net/imap_open. + * + * @param string[]|null $params + * + * @psalm-param array{DISABLE_AUTHENTICATOR?:string}|array|null $params + * + * @throws InvalidParameterException + */ + public function setConnectionArgs(int $options = 0, int $retriesNum = 0, array $params = null): void + { + if (0 !== $options) { + if (($options & self::IMAP_OPTIONS_SUPPORTED_VALUES) !== $options) { + throw new InvalidParameterException('Please check your option for setConnectionArgs()! Unsupported option "'.$options.'". Available options: https://www.php.net/manual/de/function.imap-open.php'); + } + $this->imapOptions = $options; + } + + if (0 != $retriesNum) { + if ($retriesNum < 0) { + throw new InvalidParameterException('Invalid number of retries provided for setConnectionArgs()! It must be a positive integer. (eg. 1 or 3)'); + } + $this->imapRetriesNum = $retriesNum; + } + + if (\is_array($params) && \count($params) > 0) { + $supported_params = ['DISABLE_AUTHENTICATOR']; + + foreach (\array_keys($params) as $key) { + if (!\in_array($key, $supported_params, true)) { + throw new InvalidParameterException('Invalid array key of params provided for setConnectionArgs()! Only DISABLE_AUTHENTICATOR is currently valid.'); + } + } + + $this->imapParams = $params; + } + } + + /** + * Set custom folder for attachments in case you want to have tree of folders for each email + * i.e. a/1 b/1 c/1 where a,b,c - senders, i.e. john@smith.com. + * + * @param string $attachmentsDir Folder where to save attachments + * + * @throws InvalidParameterException + */ + public function setAttachmentsDir(string $attachmentsDir): void + { + if (empty(\trim($attachmentsDir))) { + throw new InvalidParameterException('setAttachmentsDir() expects a string as first parameter!'); + } + if (!\is_dir($attachmentsDir)) { + throw new InvalidParameterException('Directory "'.$attachmentsDir.'" not found'); + } + $this->attachmentsDir = \rtrim(\realpath($attachmentsDir), '\\/'); + } + + /** + * Get current saving folder for attachments. + * + * @return string|null Attachments dir + */ + public function getAttachmentsDir(): ?string + { + return $this->attachmentsDir; + } + + /** + * Sets / Changes the attempts / retries to connect. + */ + public function setConnectionRetry(int $maxAttempts): void + { + $this->connectionRetry = $maxAttempts; + } + + /** + * Sets / Changes the delay between each attempt / retry to connect. + */ + public function setConnectionRetryDelay(int $milliseconds): void + { + $this->connectionRetryDelay = $milliseconds; + } + + /** + * Get IMAP mailbox connection stream. + * + * @param bool $forceConnection Initialize connection if it's not initialized + * + * @return resource + */ + public function getImapStream(bool $forceConnection = true) + { + if ($forceConnection) { + $this->pingOrDisconnect(); + if (!$this->imapStream) { + $this->imapStream = $this->initImapStreamWithRetry(); + } + } + + /** @var resource */ + return $this->imapStream; + } + + public function hasImapStream(): bool + { + return (\is_resource($this->imapStream) || $this->imapStream instanceof \IMAP\Connection) && \imap_ping($this->imapStream); + } + + /** + * Returns the provided string in UTF7-IMAP encoded format. + * + * @return string $str UTF-7 encoded string + * + * @psalm-pure + */ + public function encodeStringToUtf7Imap(string $str): string + { + return imap_utf7_encode($str); + } + + /** + * Returns the provided string in UTF-8 encoded format. + * + * @return string $str UTF-7 encoded string or same as before, when it's no string + * + * @psalm-pure + */ + public function decodeStringFromUtf7ImapToUtf8(string $str): string + { + $out = imap_utf7_decode($str); + + if (!\is_string($out)) { + throw new UnexpectedValueException('mb_convert_encoding($str, \'UTF-8\', \'UTF7-IMAP\') could not convert $str'); + } + + return $out; + } + + /** + * Sets the folder of the current mailbox. + */ + public function setMailboxFolder(): void + { + $imapPathParts = \explode('}', $this->imapPath); + $this->mailboxFolder = (!empty($imapPathParts[1])) ? $imapPathParts[1] : 'INBOX'; + } + + /** + * Switch mailbox without opening a new connection. + * + * @throws Exception + */ + public function switchMailbox(string $imapPath, bool $absolute = true): void + { + if (\strpos($imapPath, '}') > 0) { + $this->imapPath = $imapPath; + } else { + $this->imapPath = $this->getCombinedPath($imapPath, $absolute); + } + + $this->setMailboxFolder(); + + Imap::reopen($this->getImapStream(), $this->imapPath); + } + + /** + * Disconnects from IMAP server / mailbox. + */ + public function disconnect(): void + { + if ($this->hasImapStream()) { + Imap::close($this->getImapStream(false), $this->expungeOnDisconnect ? CL_EXPUNGE : 0); + } + } + + /** + * Sets 'expunge on disconnect' parameter. + */ + public function setExpungeOnDisconnect(bool $isEnabled): void + { + $this->expungeOnDisconnect = $isEnabled; + } + + /** + * Get information about the current mailbox. + * + * Returns the information in an object with following properties: + * Date - current system time formatted according to RFC2822 + * Driver - protocol used to access this mailbox: POP3, IMAP, NNTP + * Mailbox - the mailbox name + * Nmsgs - number of mails in the mailbox + * Recent - number of recent mails in the mailbox + * + * @see imap_check + */ + public function checkMailbox(): object + { + return Imap::check($this->getImapStream()); + } + + /** + * Creates a new mailbox. + * + * @param string $name Name of new mailbox (eg. 'PhpImap') + * + * @see imap_createmailbox() + */ + public function createMailbox(string $name): void + { + Imap::createmailbox($this->getImapStream(), $this->getCombinedPath($name)); + } + + /** + * Deletes a specific mailbox. + * + * @param string $name Name of mailbox, which you want to delete (eg. 'PhpImap') + * + * @see imap_deletemailbox() + */ + public function deleteMailbox(string $name, bool $absolute = false): bool + { + return Imap::deletemailbox($this->getImapStream(), $this->getCombinedPath($name, $absolute)); + } + + /** + * Rename an existing mailbox from $oldName to $newName. + * + * @param string $oldName Current name of mailbox, which you want to rename (eg. 'PhpImap') + * @param string $newName New name of mailbox, to which you want to rename it (eg. 'PhpImapTests') + */ + public function renameMailbox(string $oldName, string $newName): void + { + Imap::renamemailbox($this->getImapStream(), $this->getCombinedPath($oldName), $this->getCombinedPath($newName)); + } + + /** + * Gets status information about the given mailbox. + * + * This function returns an object containing status information. + * The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity. + */ + public function statusMailbox(): stdClass + { + return Imap::status($this->getImapStream(), $this->imapPath, SA_ALL); + } + + /** + * Gets listing the folders. + * + * This function returns an object containing listing the folders. + * The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity. + * + * @return string[] listing the folders + * + * @psalm-return list + */ + public function getListingFolders(string $pattern = '*'): array + { + return Imap::listOfMailboxes($this->getImapStream(), $this->imapPath, $pattern); + } + + /** + * This function uses imap_search() to perform a search on the mailbox currently opened in the given IMAP stream. + * For example, to match all unanswered mails sent by Mom, you'd use: "UNANSWERED FROM mom". + * + * @param string $criteria See http://php.net/imap_search for a complete list of available criteria + * @param bool $disableServerEncoding Disables server encoding while searching for mails (can be useful on Exchange servers) + * + * @return int[] mailsIds (or empty array) + * + * @psalm-return list + */ + public function searchMailbox(string $criteria = 'ALL', bool $disableServerEncoding = false): array + { + if ($disableServerEncoding) { + /** @psalm-var list */ + return Imap::search($this->getImapStream(), $criteria, $this->imapSearchOption); + } + + /** @psalm-var list */ + return Imap::search($this->getImapStream(), $criteria, $this->imapSearchOption, $this->getServerEncoding()); + } + + /** + * Search the mailbox for emails from multiple, specific senders. + * + * @see Mailbox::searchMailboxFromWithOrWithoutDisablingServerEncoding() + * + * @return int[] + * + * @psalm-return list + */ + public function searchMailboxFrom(string $criteria, string $sender, string ...$senders): array + { + return $this->searchMailboxFromWithOrWithoutDisablingServerEncoding($criteria, false, $sender, ...$senders); + } + + /** + * Search the mailbox for emails from multiple, specific senders whilst not using server encoding. + * + * @see Mailbox::searchMailboxFromWithOrWithoutDisablingServerEncoding() + * + * @return int[] + * + * @psalm-return list + */ + public function searchMailboxFromDisableServerEncoding(string $criteria, string $sender, string ...$senders): array + { + return $this->searchMailboxFromWithOrWithoutDisablingServerEncoding($criteria, true, $sender, ...$senders); + } + + /** + * Search the mailbox using multiple criteria merging the results. + * + * @param string $single_criteria + * @param string ...$criteria + * + * @return int[] + * + * @psalm-return list + */ + public function searchMailboxMergeResults($single_criteria, ...$criteria) + { + return $this->searchMailboxMergeResultsWithOrWithoutDisablingServerEncoding(false, $single_criteria, ...$criteria); + } + + /** + * Search the mailbox using multiple criteria merging the results. + * + * @param string $single_criteria + * @param string ...$criteria + * + * @return int[] + * + * @psalm-return list + */ + public function searchMailboxMergeResultsDisableServerEncoding($single_criteria, ...$criteria) + { + return $this->searchMailboxMergeResultsWithOrWithoutDisablingServerEncoding(false, $single_criteria, ...$criteria); + } + + /** + * Save a specific body section to a file. + * + * @param int $mailId message number + * + * @see imap_savebody() + */ + public function saveMail(int $mailId, string $filename = 'email.eml'): void + { + Imap::savebody($this->getImapStream(), $filename, $mailId, '', (SE_UID === $this->imapSearchOption) ? FT_UID : 0); + } + + /** + * Marks mails listed in mailId for deletion. + * + * @param int $mailId message number + * + * @see imap_delete() + */ + public function deleteMail(int $mailId): void + { + Imap::delete($this->getImapStream(), $mailId, (SE_UID === $this->imapSearchOption) ? FT_UID : 0); + } + + /** + * Moves mails listed in mailId into new mailbox. + * + * @param string|int $mailId a range or message number + * @param string $mailBox Mailbox name + * + * @see imap_mail_move() + */ + public function moveMail($mailId, string $mailBox): void + { + Imap::mail_move($this->getImapStream(), $mailId, $mailBox, CP_UID); + $this->expungeDeletedMails(); + } + + /** + * Copies mails listed in mailId into new mailbox. + * + * @param string|int $mailId a range or message number + * @param string $mailBox Mailbox name + * + * @see imap_mail_copy() + */ + public function copyMail($mailId, string $mailBox): void + { + Imap::mail_copy($this->getImapStream(), $mailId, $mailBox, CP_UID); + $this->expungeDeletedMails(); + } + + /** + * Deletes all the mails marked for deletion by imap_delete(), imap_mail_move(), or imap_setflag_full(). + * + * @see imap_expunge() + */ + public function expungeDeletedMails(): void + { + Imap::expunge($this->getImapStream()); + } + + /** + * Add the flag \Seen to a mail. + */ + public function markMailAsRead(int $mailId): void + { + $this->setFlag([$mailId], '\\Seen'); + } + + /** + * Remove the flag \Seen from a mail. + */ + public function markMailAsUnread(int $mailId): void + { + $this->clearFlag([$mailId], '\\Seen'); + } + + /** + * Add the flag \Flagged to a mail. + */ + public function markMailAsImportant(int $mailId): void + { + $this->setFlag([$mailId], '\\Flagged'); + } + + /** + * Add the flag \Seen to a mails. + * + * @param int[] $mailId + * + * @psalm-param list $mailId + */ + public function markMailsAsRead(array $mailId): void + { + $this->setFlag($mailId, '\\Seen'); + } + + /** + * Remove the flag \Seen from some mails. + * + * @param int[] $mailId + * + * @psalm-param list $mailId + */ + public function markMailsAsUnread(array $mailId): void + { + $this->clearFlag($mailId, '\\Seen'); + } + + /** + * Add the flag \Flagged to some mails. + * + * @param int[] $mailId + * + * @psalm-param list $mailId + */ + public function markMailsAsImportant(array $mailId): void + { + $this->setFlag($mailId, '\\Flagged'); + } + + /** + * Check, if the specified flag for the mail is set or not. + * + * @param int $mailId A single mail ID + * @param string $flag Which you can get are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060 + * + * @return bool True, when the flag is set, false when not + * + * @psalm-param int $mailId + */ + public function flagIsSet(int $mailId, string $flag): bool + { + $flag = str_replace('\\', '', strtolower($flag)); + + $overview = Imap::fetch_overview($this->getImapStream(), $mailId, ST_UID); + + if ($overview[0]->$flag == 1) { + return true; + } + + return false; + } + + /** + * Causes a store to add the specified flag to the flags set for the mails in the specified sequence. + * + * @param array $mailsIds Array of mail IDs + * @param string $flag Which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060 + * + * @psalm-param list $mailsIds + */ + public function setFlag(array $mailsIds, string $flag): void + { + Imap::setflag_full($this->getImapStream(), \implode(',', $mailsIds), $flag, ST_UID); + } + + /** + * Causes a store to delete the specified flag to the flags set for the mails in the specified sequence. + * + * @param array $mailsIds Array of mail IDs + * @param string $flag Which you can delete are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060 + */ + public function clearFlag(array $mailsIds, string $flag): void + { + Imap::clearflag_full($this->getImapStream(), \implode(',', $mailsIds), $flag, ST_UID); + } + + /** + * Fetch mail headers for listed mails ids. + * + * Returns an array of objects describing one mail header each. The object will only define a property if it exists. The possible properties are: + * subject - the mails subject + * from - who sent it + * sender - who sent it + * to - recipient + * date - when was it sent + * message_id - Mail-ID + * references - is a reference to this mail id + * in_reply_to - is a reply to this mail id + * size - size in bytes + * uid - UID the mail has in the mailbox + * msgno - mail sequence number in the mailbox + * recent - this mail is flagged as recent + * flagged - this mail is flagged + * answered - this mail is flagged as answered + * deleted - this mail is flagged for deletion + * seen - this mail is flagged as already read + * draft - this mail is flagged as being a draft + * + * @return array $mailsIds Array of mail IDs + * + * @psalm-return list + * + * @todo adjust types & conditionals pending resolution of https://github.com/vimeo/psalm/issues/2619 + */ + public function getMailsInfo(array $mailsIds): array + { + $mails = Imap::fetch_overview( + $this->getImapStream(), + \implode(',', $mailsIds), + (SE_UID === $this->imapSearchOption) ? FT_UID : 0 + ); + if (\count($mails)) { + foreach ($mails as $index => &$mail) { + if (isset($mail->subject) && !\is_string($mail->subject)) { + throw new UnexpectedValueException('subject property at index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() was not a string!'); + } + if (isset($mail->from) && !\is_string($mail->from)) { + throw new UnexpectedValueException('from property at index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() was not a string!'); + } + if (isset($mail->sender) && !\is_string($mail->sender)) { + throw new UnexpectedValueException('sender property at index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() was not a string!'); + } + if (isset($mail->to) && !\is_string($mail->to)) { + throw new UnexpectedValueException('to property at index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() was not a string!'); + } + + if (isset($mail->subject) && !empty(\trim($mail->subject))) { + $mail->subject = $this->decodeMimeStr($mail->subject); + } + if (isset($mail->from) && !empty(\trim($mail->from))) { + $mail->from = $this->decodeMimeStr($mail->from); + } + if (isset($mail->sender) && !empty(\trim($mail->sender))) { + $mail->sender = $this->decodeMimeStr($mail->sender); + } + if (isset($mail->to) && !empty(\trim($mail->to))) { + $mail->to = $this->decodeMimeStr($mail->to); + } + } + } + + /** @var list */ + return $mails; + } + + /** + * Get headers for all messages in the defined mailbox, + * returns an array of string formatted with header info, + * one element per mail message. + * + * @see imap_headers() + */ + public function getMailboxHeaders(): array + { + return Imap::headers($this->getImapStream()); + } + + /** + * Get information about the current mailbox. + * + * Returns an object with following properties: + * Date - last change (current datetime) + * Driver - driver + * Mailbox - name of the mailbox + * Nmsgs - number of messages + * Recent - number of recent messages + * Unread - number of unread messages + * Deleted - number of deleted messages + * Size - mailbox size + * + * @return stdClass Object with info + * + * @see mailboxmsginfo + */ + public function getMailboxInfo(): stdClass + { + return Imap::mailboxmsginfo($this->getImapStream()); + } + + /** + * Gets mails ids sorted by some criteria. + * + * Criteria can be one (and only one) of the following constants: + * SORTDATE - mail Date + * SORTARRIVAL - arrival date (default) + * SORTFROM - mailbox in first From address + * SORTSUBJECT - mail subject + * SORTTO - mailbox in first To address + * SORTCC - mailbox in first cc address + * SORTSIZE - size of mail in octets + * + * @param int $criteria Sorting criteria (eg. SORTARRIVAL) + * @param bool $reverse Sort reverse or not + * @param string|null $searchCriteria See http://php.net/imap_search for a complete list of available criteria + * + * @psalm-param value-of $criteria + * + * @return int[] Mails ids + * + * @psalm-return list + */ + public function sortMails( + int $criteria = SORTARRIVAL, + bool $reverse = true, + ?string $searchCriteria = 'ALL', + string $charset = null + ): array { + return Imap::sort( + $this->getImapStream(), + $criteria, + $reverse, + $this->imapSearchOption, + $searchCriteria, + $charset + ); + } + + /** + * Get mails count in mail box. + * + * @see imap_num_msg() + */ + public function countMails(): int + { + return Imap::num_msg($this->getImapStream()); + } + + /** + * Return quota limit in KB. + * + * @param string $quota_root Should normally be in the form of which mailbox (i.e. INBOX) + */ + public function getQuotaLimit(string $quota_root = 'INBOX'): int + { + $quota = $this->getQuota($quota_root); + + /** @var int */ + return $quota['STORAGE']['limit'] ?? 0; + } + + /** + * Return quota usage in KB. + * + * @param string $quota_root Should normally be in the form of which mailbox (i.e. INBOX) + * + * @return int|false FALSE in the case of call failure + */ + public function getQuotaUsage(string $quota_root = 'INBOX') + { + $quota = $this->getQuota($quota_root); + + /** @var int|false */ + return $quota['STORAGE']['usage'] ?? 0; + } + + /** + * Get raw mail data. + * + * @param int $msgId ID of the message + * @param bool $markAsSeen Mark the email as seen, when set to true + * + * @return string Message of the fetched body + */ + public function getRawMail(int $msgId, bool $markAsSeen = true): string + { + $options = (SE_UID == $this->imapSearchOption) ? FT_UID : 0; + if (!$markAsSeen) { $options |= FT_PEEK; } - $data = $partNum ? imap_fetchbody($this->getImapStream(), $mail->id, $partNum, $options) : imap_body($this->getImapStream(), $mail->id, $options); - if($partStructure->encoding == 1) { - $data = imap_utf8($data); - } - elseif($partStructure->encoding == 2) { - $data = imap_binary($data); - } - elseif($partStructure->encoding == 3) { - $data = preg_replace('~[^a-zA-Z0-9+=/]+~s', '', $data); // https://github.com/barbushin/php-imap/issues/88 - $data = imap_base64($data); - } - elseif($partStructure->encoding == 4) { - $data = quoted_printable_decode($data); - } + return Imap::fetchbody($this->getImapStream(), $msgId, '', $options); + } - $params = array(); - if(!empty($partStructure->parameters)) { - foreach($partStructure->parameters as $param) { - $params[strtolower($param->attribute)] = $param->value; - } - } - if(!empty($partStructure->dparameters)) { - foreach($partStructure->dparameters as $param) { - $paramName = strtolower(preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute); - if(isset($params[$paramName])) { - $params[$paramName] .= $param->value; - } - else { - $params[$paramName] = $param->value; - } - } - } + /** + * Get mail header field value. + * + * @param string $headersRaw RAW headers as single string + * @param string $header_field_name Name of the required header field + * + * @return string Value of the header field + */ + public function getMailHeaderFieldValue(string $headersRaw, string $header_field_name): string + { + $header_field_value = ''; - // attachments - $attachmentId = $partStructure->ifid - ? trim($partStructure->id, " <>") - : (isset($params['filename']) || isset($params['name']) ? mt_rand() . mt_rand() : null); + if (\preg_match("/$header_field_name\:(.*)/i", $headersRaw, $matches)) { + if (isset($matches[1])) { + return \trim($matches[1]); + } + } - if($attachmentId) { - if(empty($params['filename']) && empty($params['name'])) { - $fileName = $attachmentId . '.' . strtolower($partStructure->subtype); - } - else { - $fileName = !empty($params['filename']) ? $params['filename'] : $params['name']; - $fileName = $this->decodeMimeStr($fileName, $this->serverEncoding); - $fileName = $this->decodeRFC2231($fileName, $this->serverEncoding); - } - $attachment = new IncomingMailAttachment(); - $attachment->id = $attachmentId; - $attachment->name = $fileName; - if($this->attachmentsDir) { - $replace = array( - '/\s/' => '_', - '/[^0-9a-zа-яіїє_\.]/iu' => '', - '/_+/' => '_', - '/(^_)|(_$)/' => '', - ); - $fileSysName = preg_replace('~[\\\\/]~', '', $mail->id . '_' . $attachmentId . '_' . preg_replace(array_keys($replace), $replace, $fileName)); - $attachment->filePath = $this->attachmentsDir . DIRECTORY_SEPARATOR . $fileSysName; - file_put_contents($attachment->filePath, $data); - } - $mail->addAttachment($attachment); - } - else { - if(!empty($params['charset'])) { - $data = $this->convertStringEncoding($data, $params['charset'], $this->serverEncoding); - } - if($partStructure->type == 0 && $data) { - if(strtolower($partStructure->subtype) == 'plain') { - $mail->textPlain .= $data; - } - else { - $mail->textHtml .= $data; - } - } - elseif($partStructure->type == 2 && $data) { - $mail->textPlain .= trim($data); - } - } - if(!empty($partStructure->parts)) { - foreach($partStructure->parts as $subPartNum => $subPartStructure) { - if($partStructure->type == 2 && $partStructure->subtype == 'RFC822') { - $this->initMailPart($mail, $subPartStructure, $partNum, $markAsSeen); - } - else { - $this->initMailPart($mail, $subPartStructure, $partNum . '.' . ($subPartNum + 1), $markAsSeen); - } - } - } - } + return $header_field_value; + } - protected function decodeMimeStr($string, $charset = 'utf-8') { - $newString = ''; - $elements = imap_mime_header_decode($string); - for($i = 0; $i < count($elements); $i++) { - if($elements[$i]->charset == 'default') { - $elements[$i]->charset = 'iso-8859-1'; - } - $newString .= $this->convertStringEncoding($elements[$i]->text, $elements[$i]->charset, $charset); - } - return $newString; - } + /** + * Get mail header. + * + * @param int $mailId ID of the message + * + * @throws Exception + * + * @todo update type checking pending resolution of https://github.com/vimeo/psalm/issues/2619 + */ + public function getMailHeader(int $mailId): IncomingMailHeader + { + $headersRaw = Imap::fetchheader( + $this->getImapStream(), + $mailId, + (SE_UID === $this->imapSearchOption) ? FT_UID : 0 + ); - function isUrlEncoded($string) { - $hasInvalidChars = preg_match( '#[^%a-zA-Z0-9\-_\.\+]#', $string ); - $hasEscapedChars = preg_match( '#%[a-zA-Z0-9]{2}#', $string ); - return !$hasInvalidChars && $hasEscapedChars; - } + /** @var object{ + * date?:scalar, + * Date?:scalar, + * subject?:scalar, + * from?:HOSTNAMEANDADDRESS, + * to?:HOSTNAMEANDADDRESS, + * cc?:HOSTNAMEANDADDRESS, + * bcc?:HOSTNAMEANDADDRESS, + * reply_to?:HOSTNAMEANDADDRESS, + * sender?:HOSTNAMEANDADDRESS + * } + */ + $head = \imap_rfc822_parse_headers($headersRaw); - protected function decodeRFC2231($string, $charset = 'utf-8') { - if(preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) { - $encoding = $matches[1]; - $data = $matches[2]; - if($this->isUrlEncoded($data)) { - $string = $this->convertStringEncoding(urldecode($data), $encoding, $charset); - } - } - return $string; - } + if (isset($head->date) && !\is_string($head->date)) { + throw new UnexpectedValueException('date property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not a string!'); + } + if (isset($head->Date) && !\is_string($head->Date)) { + throw new UnexpectedValueException('Date property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not a string!'); + } + if (isset($head->subject) && !\is_string($head->subject)) { + throw new UnexpectedValueException('subject property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not a string!'); + } + if (isset($head->from) && !\is_array($head->from)) { + throw new UnexpectedValueException('from property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not an array!'); + } + if (isset($head->sender) && !\is_array($head->sender)) { + throw new UnexpectedValueException('sender property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not an array!'); + } + if (isset($head->to) && !\is_array($head->to)) { + throw new UnexpectedValueException('to property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not an array!'); + } + if (isset($head->cc) && !\is_array($head->cc)) { + throw new UnexpectedValueException('cc property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not an array!'); + } + if (isset($head->bcc) && !\is_array($head->bcc)) { + throw new UnexpectedValueException('bcc property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not an array!'); + } + if (isset($head->reply_to) && !\is_array($head->reply_to)) { + throw new UnexpectedValueException('reply_to property of parsed headers corresponding to argument 1 passed to '.__METHOD__.'() was present but not an array!'); + } - /** - * Converts a string from one encoding to another. - * @param string $string - * @param string $fromEncoding - * @param string $toEncoding - * @return string Converted string if conversion was successful, or the original string if not - */ - protected function convertStringEncoding($string, $fromEncoding, $toEncoding) { - $convertedString = null; - if($string && $fromEncoding != $toEncoding) { - $convertedString = @iconv($fromEncoding, $toEncoding . '//IGNORE', $string); - if(!$convertedString && extension_loaded('mbstring')) { - $convertedString = @mb_convert_encoding($string, $toEncoding, $fromEncoding); - } - } - return $convertedString ?: $string; - } - - public function __destruct() { - $this->disconnect(); - } -} - -class Exception extends \Exception { + $header = new IncomingMailHeader(); + $header->headersRaw = $headersRaw; + $header->headers = $head; + $header->id = $mailId; + $header->imapPath = $this->imapPath; + $header->mailboxFolder = $this->mailboxFolder; + $header->isSeen = ($this->flagIsSet($mailId, '\Seen')) ? true : false; + $header->isAnswered = ($this->flagIsSet($mailId, '\Answered')) ? true : false; + $header->isRecent = ($this->flagIsSet($mailId, '\Recent')) ? true : false; + $header->isFlagged = ($this->flagIsSet($mailId, '\Flagged')) ? true : false; + $header->isDeleted = ($this->flagIsSet($mailId, '\Deleted')) ? true : false; + $header->isDraft = ($this->flagIsSet($mailId, '\Draft')) ? true : false; + $header->mimeVersion = $this->getMailHeaderFieldValue($headersRaw, 'MIME-Version'); + $header->xVirusScanned = $this->getMailHeaderFieldValue($headersRaw, 'X-Virus-Scanned'); + $header->organization = $this->getMailHeaderFieldValue($headersRaw, 'Organization'); + $header->contentType = $this->getMailHeaderFieldValue($headersRaw, 'Content-Type'); + $header->xMailer = $this->getMailHeaderFieldValue($headersRaw, 'X-Mailer'); + $header->contentLanguage = $this->getMailHeaderFieldValue($headersRaw, 'Content-Language'); + $header->xSenderIp = $this->getMailHeaderFieldValue($headersRaw, 'X-Sender-IP'); + $header->priority = $this->getMailHeaderFieldValue($headersRaw, 'Priority'); + $header->importance = $this->getMailHeaderFieldValue($headersRaw, 'Importance'); + $header->sensitivity = $this->getMailHeaderFieldValue($headersRaw, 'Sensitivity'); + $header->autoSubmitted = $this->getMailHeaderFieldValue($headersRaw, 'Auto-Submitted'); + $header->precedence = $this->getMailHeaderFieldValue($headersRaw, 'Precedence'); + $header->failedRecipients = $this->getMailHeaderFieldValue($headersRaw, 'Failed-Recipients'); + $header->xOriginalTo = $this->getMailHeaderFieldValue($headersRaw, 'X-Original-To'); + if (isset($head->date) && !empty(\trim($head->date))) { + $header->date = self::parseDateTime($head->date); + } elseif (isset($head->Date) && !empty(\trim($head->Date))) { + $header->date = self::parseDateTime($head->Date); + } else { + $now = new DateTime(); + $header->date = self::parseDateTime($now->format('Y-m-d H:i:s')); + } + + $header->subject = (isset($head->subject) && !empty(\trim($head->subject))) ? $this->decodeMimeStr($head->subject) : null; + if (isset($head->from) && !empty($head->from)) { + [$header->fromHost, $header->fromName, $header->fromAddress] = $this->possiblyGetHostNameAndAddress($head->from); + } elseif (\preg_match('/smtp.mailfrom=[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+.[a-zA-Z]{2,4}/', $headersRaw, $matches)) { + $header->fromAddress = \substr($matches[0], 14); + } + if (isset($head->sender) && !empty($head->sender)) { + [$header->senderHost, $header->senderName, $header->senderAddress] = $this->possiblyGetHostNameAndAddress($head->sender); + } + if (isset($head->to)) { + $toStrings = []; + foreach ($head->to as $to) { + $to_parsed = $this->possiblyGetEmailAndNameFromRecipient($to); + if ($to_parsed) { + [$toEmail, $toName] = $to_parsed; + $toStrings[] = $toName ? "$toName <$toEmail>" : $toEmail; + $header->to[$toEmail] = $toName; + } + } + $header->toString = \implode(', ', $toStrings); + } + + if (isset($head->cc)) { + $ccStrings = []; + foreach ($head->cc as $cc) { + $cc_parsed = $this->possiblyGetEmailAndNameFromRecipient($cc); + if ($cc_parsed) { + [$ccEmail, $ccName] = $cc_parsed; + $ccStrings[] = $ccName ? "$ccName <$ccEmail>" : $ccEmail; + $header->cc[$ccEmail] = $ccName; + } + } + $header->ccString = \implode(', ', $ccStrings); + } + + if (isset($head->bcc)) { + foreach ($head->bcc as $bcc) { + $bcc_parsed = $this->possiblyGetEmailAndNameFromRecipient($bcc); + if ($bcc_parsed) { + $header->bcc[$bcc_parsed[0]] = $bcc_parsed[1]; + } + } + } + + if (isset($head->reply_to)) { + foreach ($head->reply_to as $replyTo) { + $replyTo_parsed = $this->possiblyGetEmailAndNameFromRecipient($replyTo); + if ($replyTo_parsed) { + $header->replyTo[$replyTo_parsed[0]] = $replyTo_parsed[1]; + } + } + } + + if (isset($head->message_id)) { + if (!\is_string($head->message_id)) { + throw new UnexpectedValueException('Message ID was expected to be a string, '.\gettype($head->message_id).' found!'); + } + $header->messageId = $head->message_id; + } + + return $header; + } + + /** + * taken from https://www.electrictoolbox.com/php-imap-message-parts/. + * + * @param stdClass[] $messageParts + * @param stdClass[] $flattenedParts + * + * @psalm-param array $flattenedParts + * + * @return stdClass[] + * + * @psalm-return array + */ + public function flattenParts(array $messageParts, array $flattenedParts = [], string $prefix = '', int $index = 1, bool $fullPrefix = true): array + { + foreach ($messageParts as $part) { + $flattenedParts[$prefix.$index] = $part; + if (isset($part->parts)) { + /** @var stdClass[] */ + $part_parts = $part->parts; + + if (self::PART_TYPE_TWO == $part->type) { + $flattenedParts = $this->flattenParts($part_parts, $flattenedParts, $prefix.$index.'.', 0, false); + } elseif ($fullPrefix) { + $flattenedParts = $this->flattenParts($part_parts, $flattenedParts, $prefix.$index.'.'); + } else { + $flattenedParts = $this->flattenParts($part_parts, $flattenedParts, $prefix); + } + unset($flattenedParts[$prefix.$index]->parts); + } + ++$index; + } + + /** @var array */ + return $flattenedParts; + } + + /** + * Get mail data. + * + * @param int $mailId ID of the mail + * @param bool $markAsSeen Mark the email as seen, when set to true + */ + public function getMail(int $mailId, bool $markAsSeen = true): IncomingMail + { + $mail = new IncomingMail(); + $mail->setHeader($this->getMailHeader($mailId)); + + $mailStructure = Imap::fetchstructure( + $this->getImapStream(), + $mailId, + (SE_UID === $this->imapSearchOption) ? FT_UID : 0 + ); + + if (empty($mailStructure->parts)) { + $this->initMailPart($mail, $mailStructure, 0, $markAsSeen); + } else { + /** @var array */ + $parts = $mailStructure->parts; + foreach ($this->flattenParts($parts) as $partNum => $partStructure) { + $this->initMailPart($mail, $partStructure, $partNum, $markAsSeen); + } + } + + return $mail; + } + + /** + * Download attachment. + * + * @param array $params Array of params of mail + * @param object $partStructure Part of mail + * @param bool $emlOrigin True, if it indicates, that the attachment comes from an EML (mail) file + * + * @psalm-param array $params + * @psalm-param PARTSTRUCTURE $partStructure + * + * @return IncomingMailAttachment $attachment + */ + public function downloadAttachment(DataPartInfo $dataInfo, array $params, object $partStructure, bool $emlOrigin = false): IncomingMailAttachment + { + if ('RFC822' == $partStructure->subtype && isset($partStructure->disposition) && 'attachment' == $partStructure->disposition) { + $fileName = \strtolower($partStructure->subtype).'.eml'; + } elseif ('ALTERNATIVE' == $partStructure->subtype) { + $fileName = \strtolower($partStructure->subtype).'.eml'; + } elseif ((!isset($params['filename']) || empty(\trim($params['filename']))) && (!isset($params['name']) || empty(\trim($params['name'])))) { + $fileName = \strtolower($partStructure->subtype); + } else { + $fileName = (isset($params['filename']) && !empty(\trim($params['filename']))) ? $params['filename'] : $params['name']; + $fileName = $this->decodeMimeStr($fileName); + $fileName = $this->decodeRFC2231($fileName); + } + + /** @var scalar|array|object|null */ + $sizeInBytes = $partStructure->bytes ?? null; + + /** @var scalar|array|object|null */ + $encoding = $partStructure->encoding ?? null; + + if (null !== $sizeInBytes && !\is_int($sizeInBytes)) { + throw new UnexpectedValueException('Supplied part structure specifies a non-integer, non-null bytes header!'); + } + if (null !== $encoding && !\is_int($encoding)) { + throw new UnexpectedValueException('Supplied part structure specifies a non-integer, non-null encoding header!'); + } + if (isset($partStructure->type) && !\is_int($partStructure->type)) { + throw new UnexpectedValueException('Supplied part structure specifies a non-integer, non-null type header!'); + } + + $partStructure_id = ($partStructure->ifid && isset($partStructure->id)) ? \trim($partStructure->id) : null; + + $attachment = new IncomingMailAttachment(); + $attachment->id = \bin2hex(\random_bytes(20)); + $attachment->contentId = isset($partStructure_id) ? \trim($partStructure_id, ' <>') : null; + if (isset($partStructure->type)) { + $attachment->type = $partStructure->type; + } + $attachment->encoding = $encoding; + $attachment->subtype = ($partStructure->ifsubtype && isset($partStructure->subtype)) ? \trim($partStructure->subtype) : null; + $attachment->description = ($partStructure->ifdescription && isset($partStructure->description)) ? \trim((string) $partStructure->description) : null; + $attachment->name = $fileName; + $attachment->sizeInBytes = $sizeInBytes; + $attachment->disposition = (isset($partStructure->disposition) && \is_string($partStructure->disposition)) ? $partStructure->disposition : null; + + /** @var scalar|array|object|resource|null */ + $charset = $params['charset'] ?? null; + + if (isset($charset) && !\is_string($charset)) { + throw new InvalidArgumentException('Argument 2 passed to '.__METHOD__.'() must specify charset as a string when specified!'); + } + $attachment->charset = (isset($charset) && !empty(\trim($charset))) ? $charset : null; + $attachment->emlOrigin = $emlOrigin; + + $attachment->addDataPartInfo($dataInfo); + + $attachment->fileInfoRaw = $attachment->getFileInfo(FILEINFO_RAW); + $attachment->fileInfo = $attachment->getFileInfo(FILEINFO_NONE); + $attachment->mime = $attachment->getFileInfo(FILEINFO_MIME); + $attachment->mimeType = $attachment->getFileInfo(FILEINFO_MIME_TYPE); + $attachment->mimeEncoding = $attachment->getFileInfo(FILEINFO_MIME_ENCODING); + $attachment->fileExtension = $attachment->getFileInfo(FILEINFO_EXTENSION); + + $attachmentsDir = $this->getAttachmentsDir(); + + if (null != $attachmentsDir) { + if (true == $this->getAttachmentFilenameMode()) { + $fileSysName = $attachment->name; + } else { + $fileSysName = \bin2hex(\random_bytes(16)).'.bin'; + } + + $filePath = $attachmentsDir.DIRECTORY_SEPARATOR.$fileSysName; + + if (\strlen($filePath) > self::MAX_LENGTH_FILEPATH) { + $ext = \pathinfo($filePath, PATHINFO_EXTENSION); + $filePath = \substr($filePath, 0, self::MAX_LENGTH_FILEPATH - 1 - \strlen($ext)).'.'.$ext; + } + + $attachment->setFilePath($filePath); + $attachment->saveToDisk(); + } + + return $attachment; + } + + /** + * Converts a string to UTF-8. + * + * @param string $string MIME string to decode + * @param string $fromCharset Charset to convert from + * + * @return string Converted string if conversion was successful, or the original string if not + */ + public function convertToUtf8(string $string, string $fromCharset): string + { + $fromCharset = mb_strtolower($fromCharset); + $newString = ''; + + if ('default' === $fromCharset) { + $fromCharset = $this->decodeMimeStrDefaultCharset; + } + + switch ($fromCharset) { + case 'default': // Charset default is already ASCII (not encoded) + case 'utf-8': // Charset UTF-8 is OK + $newString .= $string; + break; + default: + // If charset exists in mb_list_encodings(), convert using mb_convert function + if (\in_array($fromCharset, $this->lowercase_mb_list_encodings(), true)) { + $newString .= \mb_convert_encoding($string, 'UTF-8', $fromCharset); + } else { + // Fallback: Try to convert with iconv() + $iconv_converted_string = @\iconv($fromCharset, 'UTF-8', $string); + if (!$iconv_converted_string) { + // If iconv() could also not convert, return string as it is + // (unknown charset) + $newString .= $string; + } else { + $newString .= $iconv_converted_string; + } + } + break; + } + + return $newString; + } + + /** + * Decodes a mime string. + * + * @param string $string MIME string to decode + * + * @return string Converted string if conversion was successful, or the original string if not + * + * @throws Exception + * + * @todo update implementation pending resolution of https://github.com/vimeo/psalm/issues/2619 & https://github.com/vimeo/psalm/issues/2620 + */ + public function decodeMimeStr(string $string): string + { + $newString = ''; + /** @var list|false */ + $elements = \imap_mime_header_decode($string); + + if (false === $elements) { + return $string; + } + + foreach ($elements as $element) { + $newString .= $this->convertToUtf8($element->text, $element->charset); + } + + return $newString; + } + + /** + * @psalm-pure + */ + public function isUrlEncoded(string $string): bool + { + $hasInvalidChars = \preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $string); + $hasEscapedChars = \preg_match('#%[a-zA-Z0-9]{2}#', $string); + + return !$hasInvalidChars && $hasEscapedChars; + } + + /** + * Converts the datetime to a RFC 3339 compliant format. + * + * @param string $dateHeader Header datetime + * + * @return string RFC 3339 compliant format or original (unchanged) format, + * if conversation is not possible + * + * @psalm-pure + */ + public function parseDateTime(string $dateHeader): string + { + if (empty(\trim($dateHeader))) { + throw new InvalidParameterException('parseDateTime() expects parameter 1 to be a parsable string datetime'); + } + + $dateHeaderUnixtimestamp = \strtotime($dateHeader); + + if (!$dateHeaderUnixtimestamp) { + return $dateHeader; + } + + $dateHeaderRfc3339 = \date(DATE_RFC3339, $dateHeaderUnixtimestamp); + + if (!$dateHeaderRfc3339) { + return $dateHeader; + } + + return $dateHeaderRfc3339; + } + + /** + * Gets IMAP path. + */ + public function getImapPath(): string + { + return $this->imapPath; + } + + /** + * Get message in MBOX format. + * + * @param int $mailId message number + */ + public function getMailMboxFormat(int $mailId): string + { + $option = (SE_UID == $this->imapSearchOption) ? FT_UID : 0; + + return Imap::fetchheader($this->getImapStream(), $mailId, $option | FT_PREFETCHTEXT).Imap::body($this->getImapStream(), $mailId, $option); + } + + /** + * Get folders list. + * + * @return (false|mixed|string)[][] + * + * @psalm-return list + */ + public function getMailboxes(string $search = '*'): array + { + /** @psalm-var array */ + $mailboxes = Imap::getmailboxes($this->getImapStream(), $this->imapPath, $search); + + return $this->possiblyGetMailboxes($mailboxes); + } + + /** + * Get folders list. + * + * @return (false|mixed|string)[][] + * + * @psalm-return list + */ + public function getSubscribedMailboxes(string $search = '*'): array + { + /** @psalm-var array */ + $mailboxes = Imap::getsubscribed($this->getImapStream(), $this->imapPath, $search); + + return $this->possiblyGetMailboxes($mailboxes); + } + + /** + * Subscribe to a mailbox. + * + * @throws Exception + */ + public function subscribeMailbox(string $mailbox): void + { + Imap::subscribe( + $this->getImapStream(), + $this->getCombinedPath($mailbox) + ); + } + + /** + * Unsubscribe from a mailbox. + * + * @throws Exception + */ + public function unsubscribeMailbox(string $mailbox): void + { + Imap::unsubscribe( + $this->getImapStream(), + $this->getCombinedPath($mailbox) + ); + } + + /** + * Appends $message to $mailbox. + * + * @param string|array $message + * + * @psalm-param string|array{0:COMPOSE_ENVELOPE, 1:COMPOSE_BODY} $message + * + * @return true + * + * @see Imap::append() + */ + public function appendMessageToMailbox( + $message, + string $mailbox = '', + string $options = null, + string $internal_date = null + ): bool { + if ( + \is_array($message) && + self::EXPECTED_SIZE_OF_MESSAGE_AS_ARRAY === \count($message) && + isset($message[0], $message[1]) + ) { + $message = Imap::mail_compose($message[0], $message[1]); + } + + if (!\is_string($message)) { + throw new InvalidArgumentException('Argument 1 passed to '.__METHOD__.' must be a string or envelope/body pair.'); + } + + return Imap::append( + $this->getImapStream(), + $this->getCombinedPath($mailbox), + $message, + $options, + $internal_date + ); + } + + /** + * Returns the list of available encodings in lower case. + * + * @return string[] + * + * @psalm-return list + */ + protected function lowercase_mb_list_encodings(): array + { + $lowercase_encodings = []; + $encodings = \mb_list_encodings(); + foreach ($encodings as $encoding) { + $lowercase_encodings[] = \strtolower($encoding); + } + + return $lowercase_encodings; + } + + /** @return resource */ + protected function initImapStreamWithRetry() + { + $retry = $this->connectionRetry; + + do { + try { + return $this->initImapStream(); + } catch (ConnectionException $exception) { + } + } while (--$retry > 0 && (!$this->connectionRetryDelay || !\usleep((int) $this->connectionRetryDelay * 1000))); + + throw $exception; + } + + /** + * Retrieve the quota settings per user. + * + * @param string $quota_root Should normally be in the form of which mailbox (i.e. INBOX) + * + * @see imap_get_quotaroot() + */ + protected function getQuota(string $quota_root = 'INBOX'): array + { + return Imap::get_quotaroot($this->getImapStream(), $quota_root); + } + + /** + * Open an IMAP stream to a mailbox. + * + * @throws Exception if an error occured + * + * @return resource IMAP stream on success + */ + protected function initImapStream() + { + foreach ($this->timeouts as $type => $timeout) { + Imap::timeout($type, $timeout); + } + + $imapStream = Imap::open( + $this->imapPath, + $this->imapLogin, + $this->imapPassword, + $this->imapOptions, + $this->imapRetriesNum, + $this->imapParams + ); + + return $imapStream; + } + + /** + * @param string|0 $partNum + * + * @psalm-param PARTSTRUCTURE $partStructure + * @psalm-suppress InvalidArgument + * + * @todo refactor type checking pending resolution of https://github.com/vimeo/psalm/issues/2619 + */ + protected function initMailPart(IncomingMail $mail, object $partStructure, $partNum, bool $markAsSeen = true, bool $emlParse = false): void + { + if (!isset($mail->id)) { + throw new InvalidArgumentException('Argument 1 passeed to '.__METHOD__.'() did not have the id property set!'); + } + + $options = (SE_UID === $this->imapSearchOption) ? FT_UID : 0; + + if (!$markAsSeen) { + $options |= FT_PEEK; + } + $dataInfo = new DataPartInfo($this, $mail->id, $partNum, $partStructure->encoding, $options); + + /** @var array */ + $params = []; + if (!empty($partStructure->parameters)) { + foreach ($partStructure->parameters as $param) { + $params[\strtolower($param->attribute)] = ''; + $value = $param->value ?? null; + if (isset($value) && '' !== \trim($value)) { + $params[\strtolower($param->attribute)] = $this->decodeMimeStr($value); + } + } + } + if (!empty($partStructure->dparameters)) { + foreach ($partStructure->dparameters as $param) { + $paramName = \strtolower(\preg_match('~^(.*?)\*~', $param->attribute, $matches) ? (!isset($matches[1]) ?: $matches[1]) : $param->attribute); + if (isset($params[$paramName])) { + $params[$paramName] .= $param->value; + } else { + $params[$paramName] = $param->value; + } + } + } + + $isAttachment = isset($params['filename']) || isset($params['name']) || isset($partStructure->id); + + $dispositionAttachment = (isset($partStructure->disposition) && + \is_string($partStructure->disposition) && + 'attachment' === \mb_strtolower($partStructure->disposition)); + + // ignore contentId on body when mail isn't multipart (https://github.com/barbushin/php-imap/issues/71) + if ( + !$partNum && + TYPETEXT === $partStructure->type && + !$dispositionAttachment + ) { + $isAttachment = false; + } + + if ($isAttachment) { + $mail->setHasAttachments(true); + } + + // check if the part is a subpart of another attachment part (RFC822) + if ('RFC822' === $partStructure->subtype && isset($partStructure->disposition) && 'attachment' === $partStructure->disposition) { + // Although we are downloading each part separately, we are going to download the EML to a single file + //incase someone wants to process or parse in another process + $attachment = self::downloadAttachment($dataInfo, $params, $partStructure, false); + $mail->addAttachment($attachment); + } + + // If it comes from an EML file it is an attachment + if ($emlParse) { + $isAttachment = true; + } + + // Do NOT parse attachments, when getAttachmentsIgnore() is true + if ( + $this->getAttachmentsIgnore() + && (TYPEMULTIPART !== $partStructure->type + && (TYPETEXT !== $partStructure->type || !\in_array(\mb_strtolower($partStructure->subtype), ['plain', 'html'], true))) + ) { + return; + } + + if ($isAttachment) { + $attachment = self::downloadAttachment($dataInfo, $params, $partStructure, $emlParse); + $mail->addAttachment($attachment); + } else { + if (isset($params['charset']) && !empty(\trim($params['charset']))) { + $dataInfo->charset = $params['charset']; + } + } + + if (!empty($partStructure->parts)) { + foreach ($partStructure->parts as $subPartNum => $subPartStructure) { + $not_attachment = (!isset($partStructure->disposition) || 'attachment' !== $partStructure->disposition); + + if (TYPEMESSAGE === $partStructure->type && 'RFC822' === $partStructure->subtype && $not_attachment) { + $this->initMailPart($mail, $subPartStructure, $partNum, $markAsSeen); + } elseif (TYPEMULTIPART === $partStructure->type && 'ALTERNATIVE' === $partStructure->subtype && $not_attachment) { + // https://github.com/barbushin/php-imap/issues/198 + $this->initMailPart($mail, $subPartStructure, $partNum, $markAsSeen); + } elseif ('RFC822' === $partStructure->subtype && isset($partStructure->disposition) && 'attachment' === $partStructure->disposition) { + //If it comes from am EML attachment, download each part separately as a file + $this->initMailPart($mail, $subPartStructure, $partNum.'.'.($subPartNum + 1), $markAsSeen, true); + } else { + $this->initMailPart($mail, $subPartStructure, $partNum.'.'.($subPartNum + 1), $markAsSeen); + } + } + } else { + if (TYPETEXT === $partStructure->type) { + if ('plain' === \mb_strtolower($partStructure->subtype)) { + if ($dispositionAttachment) { + return; + } + + $mail->addDataPartInfo($dataInfo, DataPartInfo::TEXT_PLAIN); + } elseif (!$partStructure->ifdisposition) { + $mail->addDataPartInfo($dataInfo, DataPartInfo::TEXT_HTML); + } elseif (!\is_string($partStructure->disposition)) { + throw new InvalidArgumentException('disposition property of object passed as argument 2 to '.__METHOD__.'() was present but not a string!'); + } elseif (!$dispositionAttachment) { + $mail->addDataPartInfo($dataInfo, DataPartInfo::TEXT_HTML); + } + } elseif (TYPEMESSAGE === $partStructure->type) { + $mail->addDataPartInfo($dataInfo, DataPartInfo::TEXT_PLAIN); + } + } + } + + protected function decodeRFC2231(string $string): string + { + if (\preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) { + $data = $matches[2] ?? ''; + if ($this->isUrlEncoded($data)) { + $string = $this->decodeMimeStr(\urldecode($data)); + } + } + + return $string; + } + + /** + * Combine Subfolder or Folder to the connection. + * Have the imapPath a folder added to the connection info, then will the $folder added as subfolder. + * If the parameter $absolute TRUE, then will the connection new builded only with this folder as root element. + * + * @param string $folder Folder, the will added to the path + * @param bool $absolute Add folder as root element to the connection and remove all other from this + * + * @return string Return the new path + */ + protected function getCombinedPath(string $folder, bool $absolute = false): string + { + if (empty(\trim($folder))) { + return $this->imapPath; + } elseif ('}' === \substr($this->imapPath, -1)) { + return $this->imapPath.$folder; + } elseif (true === $absolute) { + $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; + } + + return $this->imapPath.$this->getPathDelimiter().$folder; + } + + /** + * @psalm-return array{0: string, 1: null|string}|null + * + * @return (null|string)[]|null + */ + protected function possiblyGetEmailAndNameFromRecipient(object $recipient): ?array + { + if (isset($recipient->mailbox, $recipient->host)) { + /** @var string */ + $recipientMailbox = $recipient->mailbox; + /** @var string */ + $recipientHost = $recipient->host; + /** @var string|null */ + $recipientPersonal = $recipient->personal ?? null; + + if (!\is_string($recipientMailbox)) { + throw new UnexpectedValueException('mailbox was present on argument 1 passed to '.__METHOD__.'() but was not a string!'); + } elseif (!\is_string($recipientHost)) { + throw new UnexpectedValueException('host was present on argument 1 passed to '.__METHOD__.'() but was not a string!'); + } elseif (null !== $recipientPersonal && !\is_string($recipientPersonal)) { + throw new UnexpectedValueException('personal was present on argument 1 passed to '.__METHOD__.'() but was not a string!'); + } + + if ('' !== \trim($recipientMailbox) && '' !== \trim($recipientHost)) { + $recipientEmail = \strtolower($recipientMailbox.'@'.$recipientHost); + $recipientName = (\is_string($recipientPersonal) && '' !== \trim($recipientPersonal)) ? $this->decodeMimeStr($recipientPersonal) : null; + + return [ + $recipientEmail, + $recipientName, + ]; + } + } + + return null; + } + + /** + * @psalm-param array $t + * + * @todo revisit implementation pending resolution of https://github.com/vimeo/psalm/issues/2619 + * + * @return (false|mixed|string)[][] + * + * @psalm-return list + */ + protected function possiblyGetMailboxes(array $t): array + { + $arr = []; + if ($t) { + foreach ($t as $index => $item) { + if (!\is_object($item)) { + throw new UnexpectedValueException('Index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() corresponds to a non-object value, '.\gettype($item).' given!'); + } + /** @var scalar|array|object|resource|null */ + $item_name = $item->name ?? null; + + if (!isset($item->name, $item->attributes, $item->delimiter)) { + throw new UnexpectedValueException('The object at index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() was missing one or more of the required properties "name", "attributes", "delimiter"!'); + } elseif (!\is_string($item_name)) { + throw new UnexpectedValueException('The object at index '.(string) $index.' of argument 1 passed to '.__METHOD__.'() has a non-string value for the name property!'); + } + + // https://github.com/barbushin/php-imap/issues/339 + $name = $this->decodeStringFromUtf7ImapToUtf8($item_name); + $name_pos = \strpos($name, '}'); + if (false === $name_pos) { + throw new UnexpectedValueException('Expected token "}" not found in subscription name!'); + } + $arr[] = [ + 'fullpath' => $name, + 'attributes' => $item->attributes, + 'delimiter' => $item->delimiter, + 'shortpath' => \substr($name, $name_pos + 1), + ]; + } + } + + return $arr; + } + + /** + * @psalm-param HOSTNAMEANDADDRESS $t + * + * @psalm-return array{0:string|null, 1:string|null, 2:string} + */ + protected function possiblyGetHostNameAndAddress(array $t): array + { + $out = [ + $t[0]->host ?? (isset($t[1], $t[1]->host) ? $t[1]->host : null), + 1 => null, + ]; + foreach ([0, 1] as $index) { + $maybe = isset($t[$index], $t[$index]->personal) ? $t[$index]->personal : null; + if (\is_string($maybe) && '' !== \trim($maybe)) { + $out[1] = $this->decodeMimeStr($maybe); + + break; + } + } + + /** @var string */ + $out[] = \strtolower($t[0]->mailbox.'@'.(string) $out[0]); + + /** @var array{0:string|null, 1:string|null, 2:string} */ + return $out; + } + + /** + * @todo revisit redundant condition issues pending fix of https://github.com/vimeo/psalm/issues/2626 + */ + protected function pingOrDisconnect(): void + { + if ($this->imapStream && !Imap::ping($this->imapStream)) { + $this->disconnect(); + $this->imapStream = null; + } + } + + /** + * Search the mailbox for emails from multiple, specific senders. + * + * This function wraps Mailbox::searchMailbox() to overcome a shortcoming in ext-imap + * + * @return int[] + * + * @psalm-return list + */ + protected function searchMailboxFromWithOrWithoutDisablingServerEncoding(string $criteria, bool $disableServerEncoding, string $sender, string ...$senders): array + { + \array_unshift($senders, $sender); + + $senders = \array_values(\array_unique(\array_map( + /** + * @param string $sender + * + * @return string + */ + static function ($sender) use ($criteria): string { + return $criteria.' FROM '.\mb_strtolower($sender); + }, + $senders + ))); + + return $this->searchMailboxMergeResultsWithOrWithoutDisablingServerEncoding( + $disableServerEncoding, + ...$senders + ); + } + + /** + * Search the mailbox using different criteria, then merge the results. + * + * @param bool $disableServerEncoding + * @param string $single_criteria + * @param string ...$criteria + * + * @return int[] + * + * @psalm-return list + */ + protected function searchMailboxMergeResultsWithOrWithoutDisablingServerEncoding($disableServerEncoding, $single_criteria, ...$criteria) + { + \array_unshift($criteria, $single_criteria); + + $criteria = \array_values(\array_unique($criteria)); + + $out = []; + + foreach ($criteria as $criterion) { + $out = \array_merge($out, $this->searchMailbox($criterion, $disableServerEncoding)); + } + + /** @psalm-var list */ + return \array_values(\array_unique($out, SORT_NUMERIC)); + } } diff --git a/vendor/php-imap/php-imap/src/PhpImap/__autoload.php b/vendor/php-imap/php-imap/src/PhpImap/__autoload.php deleted file mode 100644 index 5ca245a..0000000 --- a/vendor/php-imap/php-imap/src/PhpImap/__autoload.php +++ /dev/null @@ -1,8 +0,0 @@ - + */ +abstract class AbstractLiveMailboxTest extends TestCase +{ + use LiveMailboxTestingTrait; + + /** + * @psalm-return Generator + */ + public function ComposeProvider(): Generator + { + yield from []; + } + + /** + * @psalm-return Generator + */ + public function AppendProvider(): Generator + { + foreach ($this->MailBoxProvider() as $mailbox_args) { + foreach ($this->ComposeProvider() as $compose_args) { + [$envelope, $body, $expected_compose_result] = $compose_args; + + yield [$mailbox_args, $envelope, $body, $expected_compose_result, false]; + } + + foreach ($this->ComposeProvider() as $compose_args) { + [$envelope, $body, $expected_compose_result] = $compose_args; + + yield [$mailbox_args, $envelope, $body, $expected_compose_result, true]; + } + } + } + + /** + * @dataProvider AppendProvider + * + * @group live + * + * @depends testGetImapStream + * @depends testMailCompose + * + * @psalm-param MAILBOX_ARGS $mailbox_args + * @psalm-param COMPOSE_ENVELOPE $envelope + * @psalm-param COMPOSE_BODY $body + */ + public function testAppend( + array $mailbox_args, + array $envelope, + array $body, + string $_expected_compose_result, + bool $pre_compose + ): void { + if ($this->MaybeSkipAppendTest($envelope)) { + return; + } + + [$search_criteria] = $this->SubjectSearchCriteriaAndSubject($envelope); + + [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( + $mailbox_args + ); + + /** @var Throwable|null */ + $exception = null; + + $mailboxDeleted = false; + + try { + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $message = [$envelope, $body]; + + if ($pre_compose) { + $message = Imap::mail_compose($envelope, $body); + } + + $mailbox->appendMessageToMailbox($message); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $mailbox->deleteMail($search[0]); + + $mailbox->expungeDeletedMails(); + + $mailbox->switchMailbox($path->getString()); + $mailbox->deleteMailbox($remove_mailbox); + $mailboxDeleted = true; + + $this->assertCount( + 0, + $mailbox->searchMailbox($search_criteria), + ( + 'If a subject was found,'. + ' then the message is was not expunged as requested.' + ) + ); + } catch (Throwable $ex) { + $exception = $ex; + } finally { + $mailbox->switchMailbox($path->getString()); + if (!$mailboxDeleted) { + $mailbox->deleteMailbox($remove_mailbox); + } + $mailbox->disconnect(); + } + + if (null !== $exception) { + throw $exception; + } + } + + /** + * Get subject search criteria and subject. + * + * @psalm-param array{subject?:mixed} $envelope + * + * @psalm-return array{0:string, 1:string} + */ + protected function SubjectSearchCriteriaAndSubject(array $envelope): array + { + /** @var string|null */ + $subject = $envelope['subject'] ?? null; + + $this->assertIsString($subject); + + $search_criteria = \sprintf('SUBJECT "%s"', $subject); + + /** @psalm-var array{0:string, 1:string} */ + return [$search_criteria, $subject]; + } + + protected function MaybeSkipAppendTest(array $envelope): bool + { + if (!isset($envelope['subject'])) { + $this->markTestSkipped( + 'Cannot search for message by subject, no subject specified!' + ); + + return true; + } + + return false; + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/Fixtures/DataPartInfo.php b/vendor/php-imap/php-imap/tests/unit/Fixtures/DataPartInfo.php new file mode 100644 index 0000000..7f01282 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/Fixtures/DataPartInfo.php @@ -0,0 +1,23 @@ +decodeAfterFetch($this->data); + } + + public function setData(string $data = null): void + { + $this->data = $data; + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/Fixtures/IncomingMailAttachment.php b/vendor/php-imap/php-imap/tests/unit/Fixtures/IncomingMailAttachment.php new file mode 100644 index 0000000..65c31ac --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/Fixtures/IncomingMailAttachment.php @@ -0,0 +1,27 @@ +override_getFileInfo_mime_type) + ) { + return $this->override_getFileInfo_mime_type; + } + + return parent::getFileInfo($fileinfo_const); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/Fixtures/Mailbox.php b/vendor/php-imap/php-imap/tests/unit/Fixtures/Mailbox.php new file mode 100644 index 0000000..0e9fa45 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/Fixtures/Mailbox.php @@ -0,0 +1,20 @@ +imapPassword; + } + + public function getImapOptions(): int + { + return $this->imapOptions; + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/Fixtures/rgbkw5x1.png b/vendor/php-imap/php-imap/tests/unit/Fixtures/rgbkw5x1.png new file mode 100644 index 0000000..aebc850 Binary files /dev/null and b/vendor/php-imap/php-imap/tests/unit/Fixtures/rgbkw5x1.png differ diff --git a/vendor/php-imap/php-imap/tests/unit/Fixtures/rgbkw5x1.webp b/vendor/php-imap/php-imap/tests/unit/Fixtures/rgbkw5x1.webp new file mode 100644 index 0000000..80f2621 Binary files /dev/null and b/vendor/php-imap/php-imap/tests/unit/Fixtures/rgbkw5x1.webp differ diff --git a/vendor/php-imap/php-imap/tests/unit/ImapTest.php b/vendor/php-imap/php-imap/tests/unit/ImapTest.php new file mode 100644 index 0000000..d5cd6fc --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/ImapTest.php @@ -0,0 +1,152 @@ + + * } $args + */ +class ImapTest extends Base +{ + use LiveMailboxTestingTrait; + + /** + * @psalm-return Generator<'CI ENV with invalid password'|'empty mailbox/username/password', array{0: ConnectionException::class, 1: '/^[AUTHENTICATIONFAILED]/'|'Can't open mailbox : no such mailbox', 2: array{0: HiddenString, 1: HiddenString, 2: HiddenString, 3: 0, 4: 0, 5: array}, 3?: true}, mixed, void> + */ + public function OpenFailure(): Generator + { + yield 'empty mailbox/username/password' => [ + ConnectionException::class, + 'Can\'t open mailbox : no such mailbox', + [ + new HiddenString(''), + new HiddenString(''), + new HiddenString(''), + 0, + 0, + [], + ], + ]; + + $imapPath = \getenv('PHPIMAP_IMAP_PATH'); + $login = \getenv('PHPIMAP_LOGIN'); + $password = \getenv('PHPIMAP_PASSWORD'); + + if (\is_string($imapPath) && \is_string($login) && \is_string($password)) { + yield 'CI ENV with invalid password' => [ + ConnectionException::class, + '/^\[AUTHENTICATIONFAILED\].*/', + [ + new HiddenString($imapPath, true, true), + new HiddenString($login, true, true), + new HiddenString(\strrev($password), true, true), + 0, + 0, + [], + ], + true, + ]; + } + } + + /** + * @dataProvider OpenFailure + * + * @psalm-param class-string $exception + * @psalm-param PSALM_OPEN_ARGS $args + */ + public function testOpenFailure( + string $exception, + string $message, + array $args, + bool $message_as_regex = false + ): void { + $this->expectException($exception); + + if ($message_as_regex) { + $this->expectExceptionMessageMatches($message); + } else { + $this->expectExceptionMessage($message); + } + + Imap::open( + $args[0]->getString(), + $args[1]->getString(), + $args[2]->getString(), + $args[3], + $args[4], + $args[5] + ); + } + + /** + * @dataProvider MailBoxProvider + * + * @group live + */ + public function testSortEmpty( + HiddenString $path, + HiddenString $login, + HiddenString $password + ): void { + [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs([ + $path, + $login, + $password, + \sys_get_temp_dir(), + ]); + + /** @var Throwable|null */ + $exception = null; + + $mailboxDeleted = false; + + try { + $this->assertSame( + [], + Imap::sort( + $mailbox->getImapStream(), + SORTARRIVAL, + false, + 0 + ) + ); + } catch (Throwable $ex) { + $exception = $ex; + } finally { + $mailbox->switchMailbox($path->getString()); + if (!$mailboxDeleted) { + $mailbox->deleteMailbox($remove_mailbox); + } + $mailbox->disconnect(); + } + + if (null !== $exception) { + throw $exception; + } + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/IncomingMailTest.php b/vendor/php-imap/php-imap/tests/unit/IncomingMailTest.php new file mode 100644 index 0000000..a43ca77 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/IncomingMailTest.php @@ -0,0 +1,93 @@ +id = 1; + $header->id = 2; + + $mail->isDraft = true; + $header->isDraft = false; + + $mail->date = \date(DATE_RFC3339, 0); + $header->date = \date(DATE_RFC3339, 60 * 60 * 24); + + $mail->setHeader($header); + + foreach ( + [ + 'id', + 'isDraft', + 'date', + ] as $property + ) { + /** @var scalar|array|object|resource|null */ + $headerPropertyValue = $header->$property; + $this->assertSame($headerPropertyValue, $mail->$property); + } + } + + public function testDataPartInfo(): void + { + $mail = new IncomingMail(); + $mailbox = new Mailbox('', '', ''); + + $data_part = new Fixtures\DataPartInfo($mailbox, 1, 0, ENCOTHER, 0); + $data_part->setData('foo'); + + $this->assertSame('foo', $data_part->fetch()); + + $mail->addDataPartInfo($data_part, DataPartInfo::TEXT_PLAIN); + + $this->assertSame('foo', $mail->textPlain); + + $this->assertTrue($mail->__isset('textPlain')); + } + + public function testAttachments(): void + { + $mail = new IncomingMail(); + + $this->assertFalse($mail->hasAttachments()); + $this->assertSame([], $mail->getAttachments()); + + $attachments = [ + new IncomingMailAttachment(), + ]; + + foreach ($attachments as $i => $attachment) { + $attachment->id = (string) $i; + $mail->addAttachment($attachment); + } + + $this->assertTrue($mail->hasAttachments()); + $this->assertSame($attachments, $mail->getAttachments()); + + foreach ($attachments as $attachment) { + $this->assertIsString($attachment->id); + $this->assertTrue($mail->removeAttachment($attachment->id)); + } + + $this->assertFalse($mail->hasAttachments()); + $this->assertSame([], $mail->getAttachments()); + + foreach ($attachments as $attachment) { + $this->assertIsString($attachment->id); + $this->assertFalse($mail->removeAttachment($attachment->id)); + } + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/Issue509Test.php b/vendor/php-imap/php-imap/tests/unit/Issue509Test.php new file mode 100644 index 0000000..0d5aa9b --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/Issue509Test.php @@ -0,0 +1,56 @@ +decodeMimeStrDefaultCharset = 'EUC-KR'; + $decoded = $mailbox->decodeMimeStr(\base64_decode(self::base64)); + + $this->assertSame(self::sha256, \hash('sha256', $decoded)); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/Issue519Test.php b/vendor/php-imap/php-imap/tests/unit/Issue519Test.php new file mode 100644 index 0000000..215bb15 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/Issue519Test.php @@ -0,0 +1,184 @@ +'; + + public const MIME_TYPE = 'image/jpeg'; + + public const EXPECTED_ATTACHMENT_COUNT = 1; + + public const EXPECTED_ATTACHMENT_COUNT_AFTER_EMBED = 0; + + /** + * @psalm-return array + * + * @return string[][] + */ + public function provider(): array + { + $out = []; + + foreach (self::HEADER_VALUES as $value) { + $out[$value] = [$value]; + } + + return $out; + } + + /** + * @dataProvider provider + */ + public function test(string $header_value): void + { + $mailbox = new Mailbox('', '', ''); + $mail = new IncomingMail(); + $attachment = new Fixtures\IncomingMailAttachment(); + $part = new Fixtures\DataPartInfo( + $mailbox, + 0, + 0, + ENCBASE64, + 0 + ); + + $html = new Fixtures\DataPartInfo( + $mailbox, + 0, + 0, + ENC8BIT, + 0 + ); + + $html_string = ''; + + $html->setData($html_string); + $part->setData(''); + + $attachment->id = self::ID; + $attachment->contentId = self::ID; + $attachment->type = TYPEIMAGE; + $attachment->encoding = ENCBASE64; + $attachment->subtype = self::SUBTYPE; + $attachment->description = self::ID; + $attachment->name = self::ID; + $attachment->sizeInBytes = self::SIZE_IN_BYTES; + $attachment->disposition = $header_value; + $attachment->override_getFileInfo_mime_type = self::MIME_TYPE; + + $attachment->addDataPartInfo($part); + + $mail->addDataPartInfo($html, DataPartInfo::TEXT_HTML); + $mail->addAttachment($attachment); + + $this->assertTrue($mail->hasAttachments()); + + $this->assertCount( + self::EXPECTED_ATTACHMENT_COUNT, + $mail->getAttachments() + ); + + $this->assertSame($html_string, $mail->textHtml); + + $mail->embedImageAttachments(); + + $this->assertCount( + self::EXPECTED_ATTACHMENT_COUNT_AFTER_EMBED, + $mail->getAttachments() + ); + + $this->assertSame(self::HTML_EMBED, $mail->textHtml); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue250Test.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue250Test.php new file mode 100644 index 0000000..b66a4cc --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue250Test.php @@ -0,0 +1,89 @@ + + */ +class LiveMailboxIssue250Test extends AbstractLiveMailboxTest +{ + /** + * @psalm-return Generator + */ + public function ComposeProvider(): Generator + { + $random_subject = 'barbushin/php-imap#250 测试: '.\bin2hex(\random_bytes(16)); + + yield [ + ['subject' => $random_subject], + [ + [ + 'type' => TYPETEXT, + 'contents.data' => 'test', + ], + ], + ( + 'Subject: '.$random_subject."\r\n". + 'MIME-Version: 1.0'."\r\n". + 'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII'."\r\n". + "\r\n". + 'test'."\r\n" + ), + ]; + } + + /** + * @dataProvider AppendProvider + * + * @group live + * @group live-issue-250 + * + * @psalm-param MAILBOX_ARGS $mailbox_args + * @psalm-param COMPOSE_ENVELOPE $envelope + * @psalm-param COMPOSE_BODY $body + */ + public function testAppend( + array $mailbox_args, + array $envelope, + array $body, + string $expected_compose_result, + bool $pre_compose + ): void { + parent::testAppend( + $mailbox_args, + $envelope, + $body, + $expected_compose_result, + $pre_compose + ); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue490Test.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue490Test.php new file mode 100644 index 0000000..695bc93 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue490Test.php @@ -0,0 +1,141 @@ +getMailbox( + $imapPath, + $login, + $password, + $attachmentsDir, + $serverEncoding + ); + + $exception = null; + + try { + $envelope = [ + 'subject' => 'barbushin/php-imap#501: '.\bin2hex(\random_bytes(16)), + ]; + + [$search_criteria] = $this->SubjectSearchCriteriaAndSubject( + $envelope + ); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $message = Imap::mail_compose( + $envelope, + [ + [ + 'type' => TYPEMULTIPART, + ], + [ + 'type' => TYPETEXT, + 'contents.data' => 'foo', + ], + [ + 'type' => TYPEMULTIPART, + 'subtype' => 'plain', + 'description' => 'bar.txt', + 'disposition.type' => 'attachment', + 'disposition' => ['filename' => 'bar.txt'], + 'type.parameters' => ['name' => 'bar.txt'], + 'contents.data' => 'bar', + ], + [ + 'type' => TYPEMULTIPART, + 'subtype' => 'plain', + 'description' => 'baz.txt', + 'disposition.type' => 'attachment', + 'disposition' => ['filename' => 'baz.txt'], + 'type.parameters' => ['name' => 'baz.txt'], + 'contents.data' => 'baz', + ], + ] + ); + + $mailbox->appendMessageToMailbox($message); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $mail = $mailbox->getMail($search[0], false); + + $this->assertSame('foo', $mail->textPlain); + + $attachments = $mail->getAttachments(); + $keys = \array_keys($attachments); + + $this->assertCount(2, $attachments); + + $this->assertSame('bar', $attachments[$keys[0]]->getContents()); + $this->assertSame('baz', $attachments[$keys[1]]->getContents()); + } catch (Exception $ex) { + $exception = $ex; + } finally { + $mailbox->switchMailbox($imapPath->getString()); + $mailbox->deleteMailbox($remove_mailbox); + $mailbox->disconnect(); + } + + if (null !== $exception) { + throw $exception; + } + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue501Test.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue501Test.php new file mode 100644 index 0000000..34ebc9e --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue501Test.php @@ -0,0 +1,130 @@ +assertSame([], \imap_mime_header_decode('')); + + // example credentials nabbed from MailboxTest::testConstructorTrimsPossibleVariables() + $imapPath = ' {imap.example.com:993/imap/ssl}INBOX '; + $login = ' php-imap@example.com'; + $password = ' v3rY!53cEt&P4sSWöRd$'; + // directory names can contain spaces before AND after on Linux/Unix systems. Windows trims these spaces automatically. + $attachmentsDir = '.'; + $serverEncoding = 'UTF-8 '; + + $mailbox = new Mailbox($imapPath, $login, $password, $attachmentsDir, $serverEncoding); + + $this->assertSame('', $mailbox->decodeMimeStr('')); + } + + /** + * @dataProvider MailBoxProvider + * + * @group live + * @group live-issue-501 + */ + public function testGetEmptyBody( + HiddenString $imapPath, + HiddenString $login, + HiddenString $password, + string $attachmentsDir, + string $serverEncoding = 'UTF-8' + ): void { + [$mailbox, $remove_mailbox] = $this->getMailbox( + $imapPath, + $login, + $password, + $attachmentsDir, + $serverEncoding + ); + + $exception = null; + + try { + $envelope = [ + 'subject' => 'barbushin/php-imap#501: '.\bin2hex(\random_bytes(16)), + ]; + + [$search_criteria] = $this->SubjectSearchCriteriaAndSubject( + $envelope + ); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $mailbox->appendMessageToMailbox(Imap::mail_compose( + $envelope, + [ + [ + 'type' => TYPETEXT, + 'contents.data' => '', + ], + ] + )); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $mail = $mailbox->getMail($search[0], false); + + $this->assertSame('', $mail->textPlain); + } catch (Exception $ex) { + $exception = $ex; + } finally { + $mailbox->switchMailbox($imapPath->getString()); + $mailbox->deleteMailbox($remove_mailbox); + $mailbox->disconnect(); + } + + if (null !== $exception) { + throw $exception; + } + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue514Test.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue514Test.php new file mode 100644 index 0000000..1c8edba --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxIssue514Test.php @@ -0,0 +1,258 @@ + 'barbushin/php-imap#514--'.\bin2hex(\random_bytes(16)), + ]; + + [$search_criteria] = $this->SubjectSearchCriteriaAndSubject($envelope); + + $body = [ + [ + 'type' => TYPEMULTIPART, + ], + [ + 'type' => TYPETEXT, + 'subtype' => 'plain', + 'contents.data' => 'foo', + ], + [ + 'type' => TYPETEXT, + 'subtype' => 'html', + 'contents.data' => \implode('', [ + 'png', + 'webp', + ]), + ], + [ + 'type' => TYPEIMAGE, + 'subtype' => 'png', + 'encoding' => ENCBASE64, + 'id' => 'foo.png', + 'description' => 'foo.png', + 'disposition' => ['filename' => 'foo.png'], + 'disposition.type' => 'inline', + 'type.parameters' => ['name' => 'foo.png'], + 'contents.data' => \base64_encode( + \file_get_contents(__DIR__.'/Fixtures/rgbkw5x1.png') + ), + ], + [ + 'type' => TYPEIMAGE, + 'subtype' => 'webp', + 'encoding' => ENCBASE64, + 'id' => 'foo.webp', + 'description' => 'foo.webp', + 'disposition' => ['filename' => 'foo.webp'], + 'disposition.type' => 'inline', + 'type.parameters' => ['name' => 'foo.webp'], + 'contents.data' => \base64_encode( + \file_get_contents(__DIR__.'/Fixtures/rgbkw5x1.webp') + ), + ], + ]; + + $message = Imap::mail_compose( + $envelope, + $body + ); + + [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs([ + $imapPath, + $login, + $password, + $attachmentsDir, + $serverEncoding, + ]); + + $result = null; + + try { + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $mailbox->appendMessageToMailbox($message); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $result = $mailbox->getMail($search[0], false); + + /** @var array */ + $counts = []; + + foreach ($result->getAttachments() as $attachment) { + if (!isset($counts[(string) $attachment->contentId])) { + $counts[(string) $attachment->contentId] = 0; + } + + ++$counts[(string) $attachment->contentId]; + } + + $this->assertCount( + 2, + $counts, + ( + 'counts should only contain foo.png and foo.webp, found: '. + \implode( + ', ', + \array_keys($counts) + ) + ) + ); + + foreach ($counts as $cid => $count) { + $this->assertSame( + 1, + $count, + $cid.' had '.(string) $count.', expected 1.' + ); + } + + $this->assertSame( + 'foo', + $result->textPlain, + 'plain text body did not match expected result!' + ); + + $embedded = \implode('', [ + 'png', + 'webp', + ]); + + $this->assertSame( + [ + 'foo.png' => 'cid:foo.png', + 'foo.webp' => 'cid:foo.webp', + ], + $result->getInternalLinksPlaceholders(), + 'Internal link placeholders did not match expected result!' + ); + + $replaced = \implode('', [ + 'png', + 'webp', + ]); + + foreach ($result->getAttachments() as $attachment) { + if ('foo.png' === $attachment->contentId) { + $replaced = \str_replace( + 'foo.png', + '/'.\basename($attachment->filePath), + $replaced + ); + } elseif ('foo.webp' === $attachment->contentId) { + $replaced = \str_replace( + 'foo.webp', + '/'.\basename($attachment->filePath), + $replaced + ); + } + } + + $this->assertSame( + $replaced, + $result->replaceInternalLinks(''), + 'replaced html body did not match expected result!' + ); + + $this->assertSame( + $body[2]['contents.data'], + $result->textHtml, + 'unembeded html body did not match expected result!' + ); + + $result->embedImageAttachments(); + + $this->assertSame( + $embedded, + $result->textHtml, + 'embeded html body did not match expected result!' + ); + + $mailbox->deleteMail($search[0]); + } catch (Throwable $ex) { + $exception = $ex; + } finally { + $mailbox->switchMailbox($path->getString()); + + if (!$mailboxDeleted) { + $mailbox->deleteMailbox($remove_mailbox); + } + + $mailbox->disconnect(); + } + + if (null !== $exception) { + throw $exception; + } + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxStringDecodingConvertingTest.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxStringDecodingConvertingTest.php new file mode 100644 index 0000000..5025a4a --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxStringDecodingConvertingTest.php @@ -0,0 +1,91 @@ + [ + ENCQUOTEDPRINTABLE, + 'iso-8859-1', + 'mountainguan', + 'mountainguan', + 'e94a37111edb29a8d3f6078dc4810953964f19562613cf2bd15e21b69d30822a', + ]; + + yield 'Issue #250 utf-7' => [ + ENCQUOTEDPRINTABLE, + 'utf-7', + '+bUuL1Q-', + '测试', + '6aa8f49cc992dfd75a114269ed26de0ad6d4e7d7a70d9c8afb3d7a57a88a73ed', + ]; + + yield 'Issue #250 utf-7 with chinese' => [ + ENCQUOTEDPRINTABLE, + 'utf-7', + 'mountainguan+bUuL1Q-', + 'mountainguan测试', + '62a5022b682b7e02bda8d18424fa06501cdd71cce2832e95129673f63da2e177', + ]; + + yield 'Issue #250 utf-8 with chinese' => [ + ENCQUOTEDPRINTABLE, + 'utf-8', + 'mountainguan=E6=B5=8B=E8=AF=95', + 'mountainguan测试', + '62a5022b682b7e02bda8d18424fa06501cdd71cce2832e95129673f63da2e177', + ]; + + yield 'Issue #657' => [ + ENCQUOTEDPRINTABLE, + 'iso-8859-2', + '=EC=B9=E8=F8=BE=FD=E1=ED=E9', + 'ěščřžýáíé', + 'a05e42c7e14de716cd501e135f3f5e49545f71069de316a1e9f7bb153f9a7356', + ]; + + yield 'Emoji utf-8' => [ + ENCQUOTEDPRINTABLE, + 'utf-8', + 'Some subject here =F0=9F=98=98', + 'Some subject here 😘', + 'da66c62e7e82316b8b543f52f1ecc4415c4dc93bc87e2239ee5f98bdf00a8c50', + ]; + } + + /** + * Test that string decoding and converting works as expected. + * + * @dataProvider stringDecodeProvider + */ + public function testStringDecode(int $encoding, string $charset, string $iso_8859_2, string $utf8, string $sha256): void + { + $mailbox = new Mailbox('', '', ''); + + $dataInfo = new DataPartInfo($mailbox, 1337, '', $encoding, 0); + $dataInfo->charset = $charset; + + $decoded = $dataInfo->decodeAfterFetch($iso_8859_2); + + $this->assertSame($utf8, $decoded); + + $this->assertSame($sha256, \hash('sha256', $decoded)); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxTest.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxTest.php new file mode 100644 index 0000000..f27de94 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxTest.php @@ -0,0 +1,678 @@ + + * + * @todo see @todo of Imap::mail_compose() + */ +class LiveMailboxTest extends AbstractLiveMailboxTest +{ + public const RANDOM_MAILBOX_SAMPLE_SIZE = 3; + + public const ISSUE_EXPECTED_ATTACHMENT_COUNT = [ + 448 => 1, + 391 => 2, + ]; + + /** + * @dataProvider MailBoxProvider + * + * @group live + */ + public function testGetImapStream(HiddenString $imapPath, HiddenString $login, HiddenString $password, string $attachmentsDir, string $serverEncoding = 'UTF-8'): void + { + [$mailbox, $remove_mailbox] = $this->getMailbox( + $imapPath, + $login, + $password, + $attachmentsDir, + $serverEncoding + ); + + /** @var Throwable|null */ + $exception = null; + + try { + $mailbox->getImapStream(); + $this->assertTrue($mailbox->hasImapStream()); + + $mailboxes = $mailbox->getMailboxes(); + \shuffle($mailboxes); + + $mailboxes = \array_values($mailboxes); + + $limit = \min(\count($mailboxes), self::RANDOM_MAILBOX_SAMPLE_SIZE); + + for ($i = 0; $i < $limit; ++$i) { + $this->assertIsArray($mailboxes[$i]); + $this->assertTrue(isset($mailboxes[$i]['shortpath'])); + $this->assertIsString($mailboxes[$i]['shortpath']); + $mailbox->switchMailbox($mailboxes[$i]['shortpath']); + + $check = $mailbox->checkMailbox(); + + foreach ([ + 'Date', + 'Driver', + 'Mailbox', + 'Nmsgs', + 'Recent', + ] as $expectedProperty) { + $this->assertTrue(\property_exists($check, $expectedProperty)); + } + + $this->assertIsString($check->Date, 'Date property of Mailbox::checkMailbox() result was not a string!'); + + $unix = \strtotime($check->Date); + + if (false === $unix && \preg_match('/[+-]\d{1,2}:?\d{2} \([^\)]+\)$/', $check->Date)) { + /** @var int */ + $pos = \strrpos($check->Date, '('); + + // Although the date property is likely RFC2822-compliant, it will not be parsed by strtotime() + $unix = \strtotime(\substr($check->Date, 0, $pos)); + } + + $this->assertIsInt($unix, 'Date property of Mailbox::checkMailbox() result was not a valid date!'); + $this->assertTrue(\in_array($check->Driver, ['POP3', 'IMAP', 'NNTP', 'pop3', 'imap', 'nntp'], true), 'Driver property of Mailbox::checkMailbox() result was not of an expected value!'); + $this->assertIsInt($check->Nmsgs, 'Nmsgs property of Mailbox::checkMailbox() result was not of an expected type!'); + $this->assertIsInt($check->Recent, 'Recent property of Mailbox::checkMailbox() result was not of an expected type!'); + + $status = $mailbox->statusMailbox(); + + foreach ([ + 'messages', + 'recent', + 'unseen', + 'uidnext', + 'uidvalidity', + ] as $expectedProperty) { + $this->assertTrue(\property_exists($status, $expectedProperty)); + } + + $this->assertSame($check->Nmsgs, $mailbox->countMails(), 'Mailbox::checkMailbox()->Nmsgs did not match Mailbox::countMails()!'); + } + } catch (Throwable $ex) { + $exception = $ex; + } finally { + $mailbox->switchMailbox($imapPath->getString()); + $mailbox->deleteMailbox($remove_mailbox); + $mailbox->disconnect(); + } + + if (null !== $exception) { + throw $exception; + } + } + + /** + * @psalm-return Generator + */ + public function ComposeProvider(): Generator + { + $random_subject = 'test: '.\bin2hex(\random_bytes(16)); + + yield [ + ['subject' => $random_subject], + [ + [ + 'type' => TYPETEXT, + 'contents.data' => 'test', + ], + ], + ( + 'Subject: '.$random_subject."\r\n". + 'MIME-Version: 1.0'."\r\n". + 'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII'."\r\n". + "\r\n". + 'test'."\r\n" + ), + ]; + + $random_subject = 'barbushin/php-imap#448: dot first:'.\bin2hex(\random_bytes(16)); + + yield [ + ['subject' => $random_subject], + [ + [ + 'type' => TYPEAPPLICATION, + 'encoding' => ENCBASE64, + 'subtype' => 'octet-stream', + 'description' => '.gitignore', + 'disposition.type' => 'attachment', + 'disposition' => ['filename' => '.gitignore'], + 'type.parameters' => ['name' => '.gitignore'], + 'contents.data' => \base64_encode( + \file_get_contents(__DIR__.'/../../.gitignore') + ), + ], + ], + ( + 'Subject: '.$random_subject."\r\n". + 'MIME-Version: 1.0'."\r\n". + 'Content-Type: APPLICATION/octet-stream; name=.gitignore'."\r\n". + 'Content-Transfer-Encoding: BASE64'."\r\n". + 'Content-Description: .gitignore'."\r\n". + 'Content-Disposition: attachment; filename=.gitignore'."\r\n". + "\r\n". + \base64_encode( + \file_get_contents(__DIR__.'/../../.gitignore') + )."\r\n" + ), + ]; + + $random_subject = 'barbushin/php-imap#448: dot last: '.\bin2hex(\random_bytes(16)); + + yield [ + ['subject' => $random_subject], + [ + [ + 'type' => TYPEAPPLICATION, + 'encoding' => ENCBASE64, + 'subtype' => 'octet-stream', + 'description' => 'gitignore.', + 'disposition.type' => 'attachment', + 'disposition' => ['filename' => 'gitignore.'], + 'type.parameters' => ['name' => 'gitignore.'], + 'contents.data' => \base64_encode( + \file_get_contents(__DIR__.'/../../.gitignore') + ), + ], + ], + ( + 'Subject: '.$random_subject."\r\n". + 'MIME-Version: 1.0'."\r\n". + 'Content-Type: APPLICATION/octet-stream; name=gitignore.'."\r\n". + 'Content-Transfer-Encoding: BASE64'."\r\n". + 'Content-Description: gitignore.'."\r\n". + 'Content-Disposition: attachment; filename=gitignore.'."\r\n". + "\r\n". + \base64_encode( + \file_get_contents(__DIR__.'/../../.gitignore') + )."\r\n" + ), + ]; + + $random_subject = 'barbushin/php-imap#391: '.\bin2hex(\random_bytes(16)); + + $random_attachment_a = \base64_encode(\random_bytes(16)); + $random_attachment_b = \base64_encode(\random_bytes(16)); + + yield [ + ['subject' => $random_subject], + [ + [ + 'type' => TYPEMULTIPART, + ], + [ + 'type' => TYPETEXT, + 'contents.data' => 'test', + ], + [ + 'type' => TYPEAPPLICATION, + 'encoding' => ENCBASE64, + 'subtype' => 'octet-stream', + 'description' => 'foo.bin', + 'disposition.type' => 'attachment', + 'disposition' => ['filename' => 'foo.bin'], + 'type.parameters' => ['name' => 'foo.bin'], + 'contents.data' => $random_attachment_a, + ], + [ + 'type' => TYPEAPPLICATION, + 'encoding' => ENCBASE64, + 'subtype' => 'octet-stream', + 'description' => 'foo.bin', + 'disposition.type' => 'attachment', + 'disposition' => ['filename' => 'foo.bin'], + 'type.parameters' => ['name' => 'foo.bin'], + 'contents.data' => $random_attachment_b, + ], + ], + ( + 'Subject: '.$random_subject."\r\n". + 'MIME-Version: 1.0'."\r\n". + 'Content-Type: MULTIPART/MIXED; BOUNDARY="{{REPLACE_BOUNDARY_HERE}}"'."\r\n". + "\r\n". + '--{{REPLACE_BOUNDARY_HERE}}'."\r\n". + 'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII'."\r\n". + "\r\n". + 'test'."\r\n". + '--{{REPLACE_BOUNDARY_HERE}}'."\r\n". + 'Content-Type: APPLICATION/octet-stream; name=foo.bin'."\r\n". + 'Content-Transfer-Encoding: BASE64'."\r\n". + 'Content-Description: foo.bin'."\r\n". + 'Content-Disposition: attachment; filename=foo.bin'."\r\n". + "\r\n". + $random_attachment_a."\r\n". + '--{{REPLACE_BOUNDARY_HERE}}'."\r\n". + 'Content-Type: APPLICATION/octet-stream; name=foo.bin'."\r\n". + 'Content-Transfer-Encoding: BASE64'."\r\n". + 'Content-Description: foo.bin'."\r\n". + 'Content-Disposition: attachment; filename=foo.bin'."\r\n". + "\r\n". + $random_attachment_b."\r\n". + '--{{REPLACE_BOUNDARY_HERE}}--'."\r\n" + ), + ]; + } + + /** + * @dataProvider ComposeProvider + * + * @group compose + * + * @psalm-param COMPOSE_ENVELOPE $envelope + * @psalm-param COMPOSE_BODY $body + */ + public function testMailCompose(array $envelope, array $body, string $expected_result): void + { + $actual_result = Imap::mail_compose($envelope, $body); + + $expected_result = $this->ReplaceBoundaryHere( + $expected_result, + $actual_result + ); + + $this->assertSame($expected_result, $actual_result); + } + + /** + * @dataProvider AppendProvider + * + * @group live + * + * @depends testAppend + * + * @psalm-param MAILBOX_ARGS $mailbox_args + * @psalm-param COMPOSE_ENVELOPE $envelope + * @psalm-param COMPOSE_BODY $body + */ + public function testAppendNudgesMailboxCount( + array $mailbox_args, + array $envelope, + array $body, + string $_expected_compose_result, + bool $pre_compose + ): void { + if ($this->MaybeSkipAppendTest($envelope)) { + return; + } + + [$search_criteria] = $this->SubjectSearchCriteriaAndSubject($envelope); + + [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( + $mailbox_args + ); + + $count = $mailbox->countMails(); + + $message = [$envelope, $body]; + + if ($pre_compose) { + $message = Imap::mail_compose($envelope, $body); + } + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $mailbox->appendMessageToMailbox($message); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $this->assertSame( + $count + 1, + $mailbox->countMails(), + ( + 'If the message count did not increase'. + ' then either the message was not appended,'. + ' or a mesage was removed while the test was running.' + ) + ); + + $mailbox->deleteMail($search[0]); + + $mailbox->expungeDeletedMails(); + + $mailbox->switchMailbox($path->getString()); + $mailbox->deleteMailbox($remove_mailbox); + + $this->assertCount( + 0, + $mailbox->searchMailbox($search_criteria), + ( + 'If a subject was found,'. + ' then the message is was not expunged as requested.' + ) + ); + } + + /** + * @dataProvider AppendProvider + * + * @group live + * + * @depends testAppend + * + * @psalm-param MAILBOX_ARGS $mailbox_args + * @psalm-param COMPOSE_ENVELOPE $envelope + * @psalm-param COMPOSE_BODY $body + */ + public function testAppendSingleSearchMatchesSort( + array $mailbox_args, + array $envelope, + array $body, + string $_expected_compose_result, + bool $pre_compose + ): void { + if ($this->MaybeSkipAppendTest($envelope)) { + return; + } + + [$search_criteria] = $this->SubjectSearchCriteriaAndSubject($envelope); + + [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( + $mailbox_args + ); + + $message = [$envelope, $body]; + + if ($pre_compose) { + $message = Imap::mail_compose($envelope, $body); + } + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $mailbox->appendMessageToMailbox($message); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $this->assertSame( + $search, + $mailbox->sortMails(SORTARRIVAL, true, $search_criteria) + ); + + $this->assertSame( + $search, + $mailbox->sortMails(SORTARRIVAL, false, $search_criteria) + ); + + $this->assertSame( + $search, + $mailbox->sortMails(SORTARRIVAL, false, $search_criteria, 'UTF-8') + ); + + $this->assertTrue(\in_array( + $search[0], + $mailbox->sortMails(SORTARRIVAL, false, null), + true + )); + + $mailbox->deleteMail($search[0]); + + $mailbox->expungeDeletedMails(); + + $mailbox->switchMailbox($path->getString()); + $mailbox->deleteMailbox($remove_mailbox); + + $this->assertCount( + 0, + $mailbox->searchMailbox($search_criteria), + ( + 'If a subject was found,'. + ' then the message is was not expunged as requested.' + ) + ); + } + + /** + * @dataProvider AppendProvider + * + * @group live + * + * @depends testAppend + * + * @psalm-param MAILBOX_ARGS $mailbox_args + * @psalm-param COMPOSE_ENVELOPE $envelope + * @psalm-param COMPOSE_BODY $body + */ + public function testAppendRetrievalMatchesExpected( + array $mailbox_args, + array $envelope, + array $body, + string $expected_compose_result, + bool $pre_compose + ): void { + if ($this->MaybeSkipAppendTest($envelope)) { + return; + } + + [$search_criteria, $search_subject] = $this->SubjectSearchCriteriaAndSubject($envelope); + + [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( + $mailbox_args + ); + + $message = [$envelope, $body]; + + if ($pre_compose) { + $message = Imap::mail_compose($envelope, $body); + } + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 0, + $search, + ( + 'If a subject was found,'. + ' then the message is insufficiently unique to assert that'. + ' a newly-appended message was actually created.' + ) + ); + + $mailbox->appendMessageToMailbox($message); + + $search = $mailbox->searchMailbox($search_criteria); + + $this->assertCount( + 1, + $search, + ( + 'If a subject was not found, '. + ' then Mailbox::appendMessageToMailbox() failed'. + ' despite not throwing an exception.' + ) + ); + + $actual_result = $mailbox->getMailMboxFormat($search[0]); + + $this->assertSame( + $this->ReplaceBoundaryHere( + $expected_compose_result, + $actual_result + ), + $actual_result + ); + + $actual_result = $mailbox->getRawMail($search[0]); + + $this->assertSame( + $this->ReplaceBoundaryHere( + $expected_compose_result, + $actual_result + ), + $actual_result + ); + + $mail = $mailbox->getMail($search[0], false); + + $this->assertSame( + $search_subject, + $mail->subject, + ( + 'If a retrieved mail did not have a matching subject'. + ' despite being found via search,'. + ' then something has gone wrong.' + ) + ); + + $info = $mailbox->getMailsInfo($search); + + $this->assertCount(1, $info); + + $this->assertSame( + $search_subject, + $info[0]->subject, + ( + 'If a retrieved mail did not have a matching subject'. + ' despite being found via search,'. + ' then something has gone wrong.' + ) + ); + + if (1 === \preg_match( + '/^barbushin\/php-imap#(448|391):/', + $envelope['subject'] ?? '', + $matches + )) { + $this->assertTrue($mail->hasAttachments()); + + $attachments = $mail->getAttachments(); + + $this->assertCount(self::ISSUE_EXPECTED_ATTACHMENT_COUNT[ + (int) $matches[1]], + $attachments + ); + + if ('448' === $matches[1]) { + $this->assertSame( + \file_get_contents(__DIR__.'/../../.gitignore'), + \current($attachments)->getContents() + ); + } + } + + $mailbox->deleteMail($search[0]); + + $mailbox->expungeDeletedMails(); + + $mailbox->switchMailbox($path->getString()); + $mailbox->deleteMailbox($remove_mailbox); + + $this->assertCount( + 0, + $mailbox->searchMailbox($search_criteria), + ( + 'If a subject was found,'. + ' then the message is was not expunged as requested.' + ) + ); + } + + /** + * @param string $expected_result + * @param string $actual_result + * + * @return string + * + * @psalm-pure + */ + protected function ReplaceBoundaryHere( + $expected_result, + $actual_result + ) { + if ( + 1 === \preg_match('/{{REPLACE_BOUNDARY_HERE}}/', $expected_result) && + 1 === \preg_match( + '/Content-Type: MULTIPART\/MIXED; BOUNDARY="([^"]+)"/', + $actual_result, + $matches + ) + ) { + $expected_result = \str_replace( + '{{REPLACE_BOUNDARY_HERE}}', + $matches[1], + $expected_result + ); + } + + return $expected_result; + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxTestingTrait.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxTestingTrait.php new file mode 100644 index 0000000..3c08434 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxTestingTrait.php @@ -0,0 +1,90 @@ +getString(), $login->getString(), $password->getString(), $attachmentsDir, $serverEncoding); + + $random = 'test-box-'.\date('c').\bin2hex(\random_bytes(4)); + + $mailbox->createMailbox($random); + + $mailbox->switchMailbox($random, false); + + return [$mailbox, $random, $imapPath]; + } + + /** + * @psalm-param MAILBOX_ARGS $mailbox_args + * + * @return mixed[] + * + * @psalm-return array{0:Mailbox, 1:string, 2:HiddenString} + */ + protected function getMailboxFromArgs(array $mailbox_args): array + { + [$path, $username, $password, $attachments_dir] = $mailbox_args; + + return $this->getMailbox( + $path, + $username, + $password, + $attachments_dir, + $mailbox_args[4] ?? 'UTF-8' + ); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/LiveMailboxWithManualSetupTest.php b/vendor/php-imap/php-imap/tests/unit/LiveMailboxWithManualSetupTest.php new file mode 100644 index 0000000..ba3ce9a --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/LiveMailboxWithManualSetupTest.php @@ -0,0 +1,70 @@ + + */ + public function RelativeToRootPathProvider(): Generator + { + yield [ + '.issue-499.Éléments envoyés', + ]; + } + + /** + * @psalm-return Generator + */ + public function statusProviderAbsolutePath(): Generator + { + foreach ($this->RelativeToRootPathProvider() as $path_args) { + foreach ($this->MailBoxProvider() as $args) { + $args[0] = new HiddenString($args[0]->getString().$path_args[0]); + + yield [$args]; + } + } + } + + /** + * Tests the status of an absolute mailbox path set from the Mailbox constructor. + * + * @dataProvider statusProviderAbsolutePath + * + * @group live + * @group live-manual + * + * @psalm-param MAILBOX_ARGS $mailbox_args + */ + public function testAbsolutePathStatusFromConstruction( + array $mailbox_args + ): void { + [$mailbox] = $this->getMailboxFromArgs($mailbox_args); + + $mailbox->statusMailbox(); + + $this->assertTrue(true); + } +} diff --git a/vendor/php-imap/php-imap/tests/unit/MailboxTest.php b/vendor/php-imap/php-imap/tests/unit/MailboxTest.php new file mode 100644 index 0000000..507ff59 --- /dev/null +++ b/vendor/php-imap/php-imap/tests/unit/MailboxTest.php @@ -0,0 +1,814 @@ + + */ +declare(strict_types=1); + +namespace PhpImap; + +use const CL_EXPUNGE; +use DateTime; +use Generator; +use const IMAP_CLOSETIMEOUT; +use const IMAP_OPENTIMEOUT; +use const IMAP_READTIMEOUT; +use const IMAP_WRITETIMEOUT; +use const OP_ANONYMOUS; +use const OP_DEBUG; +use const OP_HALFOPEN; +use const OP_PROTOTYPE; +use const OP_READONLY; +use const OP_SECURE; +use const OP_SHORTCACHE; +use const OP_SILENT; +use PhpImap\Exceptions\InvalidParameterException; +use PHPUnit\Framework\TestCase; +use const SE_FREE; +use const SE_UID; + +final class MailboxTest extends TestCase +{ + public const ANYTHING = 0; + + /** + * Holds the imap path. + * + * @var string + */ + private $imapPath = '{imap.example.com:993/imap/ssl/novalidate-cert}INBOX'; + + /** + * Holds the imap username. + * + * @var string|email + * + * @psalm-var string + */ + private $login = 'php-imap@example.com'; + + /** + * Holds the imap user password. + * + * @var string + */ + private $password = 'v3rY!53cEt&P4sSWöRd$'; + + /** + * Holds the relative name of the directory, where email attachments will be saved. + * + * @var string + */ + private $attachmentsDir = '.'; + + /** + * Holds the server encoding setting. + * + * @var string + */ + private $serverEncoding = 'UTF-8'; + + /** + * Test, that the constructor trims possible variables + * Leading and ending spaces are not even possible in some variables. + */ + public function testConstructorTrimsPossibleVariables(): void + { + $imapPath = ' {imap.example.com:993/imap/ssl}INBOX '; + $login = ' php-imap@example.com'; + $password = ' v3rY!53cEt&P4sSWöRd$'; + // directory names can contain spaces before AND after on Linux/Unix systems. Windows trims these spaces automatically. + $attachmentsDir = '.'; + $serverEncoding = 'UTF-8 '; + + $mailbox = new Fixtures\Mailbox($imapPath, $login, $password, $attachmentsDir, $serverEncoding); + + $this->assertSame('{imap.example.com:993/imap/ssl}INBOX', $mailbox->getImapPath()); + $this->assertSame('php-imap@example.com', $mailbox->getLogin()); + $this->assertSame(' v3rY!53cEt&P4sSWöRd$', $mailbox->getImapPassword()); + $this->assertSame(\realpath('.'), $mailbox->getAttachmentsDir()); + $this->assertSame('UTF-8', $mailbox->getServerEncoding()); + } + + /** + * @psalm-return non-empty-list + * + * @return string[][] + */ + public function SetAndGetServerEncodingProvider(): array + { + $data = [ + ['UTF-8'], + ]; + + $supported = \mb_list_encodings(); + + foreach ( + [ + 'Windows-1251', + 'Windows-1252', + ] as $perhaps + ) { + if ( + \in_array(\trim($perhaps), $supported, true) || + \in_array(\strtoupper(\trim($perhaps)), $supported, true) + ) { + $data[] = [$perhaps]; + } + } + + return $data; + } + + /** + * Test, that the server encoding can be set. + * + * @dataProvider SetAndGetServerEncodingProvider + */ + public function testSetAndGetServerEncoding(string $encoding): void + { + $mailbox = $this->getMailbox(); + + $mailbox->setServerEncoding($encoding); + + $encoding = \strtoupper(\trim($encoding)); + + $this->assertEquals($mailbox->getServerEncoding(), $encoding); + } + + /** + * Test, that server encoding is set to a default value. + */ + public function testServerEncodingHasDefaultSetting(): void + { + // Default character encoding should be set + $mailbox = new Mailbox($this->imapPath, $this->login, $this->password, $this->attachmentsDir); + $this->assertSame('UTF-8', $mailbox->getServerEncoding()); + } + + /** + * Test, that server encoding that all functions uppers the server encoding setting. + */ + public function testServerEncodingUppersSetting(): void + { + // Server encoding should be always upper formatted + $mailbox = new Mailbox($this->imapPath, $this->login, $this->password, $this->attachmentsDir, 'utf-8'); + $this->assertSame('UTF-8', $mailbox->getServerEncoding()); + + $mailbox = new Mailbox($this->imapPath, $this->login, $this->password, $this->attachmentsDir, 'UTF7-IMAP'); + $mailbox->setServerEncoding('uTf-8'); + $this->assertSame('UTF-8', $mailbox->getServerEncoding()); + } + + /** + * Provides test data for testing server encodings. + * + * @return (bool|string)[][] + * + * @psalm-return array{UTF-7: array{0: true, 1: 'UTF-7'}, UTF7-IMAP: array{0: true, 1: 'UTF7-IMAP'}, UTF-8: array{0: true, 1: 'UTF-8'}, ASCII: array{0: true, 1: 'ASCII'}, US-ASCII: array{0: true, 1: 'US-ASCII'}, ISO-8859-1: array{0: true, 1: 'ISO-8859-1'}, UTF7: array{0: false, 1: 'UTF7'}, UTF-7-IMAP: array{0: false, 1: 'UTF-7-IMAP'}, UTF-7IMAP: array{0: false, 1: 'UTF-7IMAP'}, UTF8: array{0: false, 1: 'UTF8'}, USASCII: array{0: false, 1: 'USASCII'}, ASC11: array{0: false, 1: 'ASC11'}, ISO-8859-0: array{0: false, 1: 'ISO-8859-0'}, ISO-8855-1: array{0: false, 1: 'ISO-8855-1'}, ISO-8859: array{0: false, 1: 'ISO-8859'}} + */ + public function serverEncodingProvider(): array + { + return [ + // Supported encodings + 'UTF-7' => [true, 'UTF-7'], + 'UTF7-IMAP' => [true, 'UTF7-IMAP'], + 'UTF-8' => [true, 'UTF-8'], + 'ASCII' => [true, 'ASCII'], + 'US-ASCII' => [true, 'US-ASCII'], + 'ISO-8859-1' => [true, 'ISO-8859-1'], + // NOT supported encodings + 'UTF7' => [false, 'UTF7'], + 'UTF-7-IMAP' => [false, 'UTF-7-IMAP'], + 'UTF-7IMAP' => [false, 'UTF-7IMAP'], + 'UTF8' => [false, 'UTF8'], + 'USASCII' => [false, 'USASCII'], + 'ASC11' => [false, 'ASC11'], + 'ISO-8859-0' => [false, 'ISO-8859-0'], + 'ISO-8855-1' => [false, 'ISO-8855-1'], + 'ISO-8859' => [false, 'ISO-8859'], + ]; + } + + /** + * Test, that server encoding only can use supported character encodings. + * + * @dataProvider serverEncodingProvider + */ + public function testServerEncodingOnlyUseSupportedSettings(bool $bool, string $encoding): void + { + $mailbox = $this->getMailbox(); + + if ($bool) { + $mailbox->setServerEncoding($encoding); + $this->assertEquals($encoding, $mailbox->getServerEncoding()); + } else { + $this->expectException(InvalidParameterException::class); + $mailbox->setServerEncoding($encoding); + $this->assertNotEquals($encoding, $mailbox->getServerEncoding()); + } + } + + /** + * Test, that the IMAP search option has a default value + * 1 => SE_UID + * 2 => SE_FREE. + */ + public function testImapSearchOptionHasADefault(): void + { + $this->assertEquals($this->getMailbox()->getImapSearchOption(), 1); + } + + /** + * Test, that the IMAP search option can be changed + * 1 => SE_UID + * 2 => SE_FREE. + */ + public function testSetAndGetImapSearchOption(): void + { + $mailbox = $this->getMailbox(); + + $mailbox->setImapSearchOption(SE_FREE); + $this->assertEquals($mailbox->getImapSearchOption(), 2); + + $this->expectException(InvalidParameterException::class); + $mailbox->setImapSearchOption(self::ANYTHING); + + $mailbox->setImapSearchOption(SE_UID); + $this->assertEquals($mailbox->getImapSearchOption(), 1); + } + + /** + * Test, that the imap login can be retrieved. + */ + public function testGetLogin(): void + { + $this->assertEquals($this->getMailbox()->getLogin(), 'php-imap@example.com'); + } + + /** + * Test, that the path delimiter has a default value. + */ + public function testPathDelimiterHasADefault(): void + { + $this->assertNotEmpty($this->getMailbox()->getPathDelimiter()); + } + + /** + * Provides test data for testing path delimiter. + * + * @psalm-return array{0: array{0: '0'}, 1: array{0: '1'}, 2: array{0: '2'}, 3: array{0: '3'}, 4: array{0: '4'}, 5: array{0: '5'}, 6: array{0: '6'}, 7: array{0: '7'}, 8: array{0: '8'}, 9: array{0: '9'}, a: array{0: 'a'}, b: array{0: 'b'}, c: array{0: 'c'}, d: array{0: 'd'}, e: array{0: 'e'}, f: array{0: 'f'}, g: array{0: 'g'}, h: array{0: 'h'}, i: array{0: 'i'}, j: array{0: 'j'}, k: array{0: 'k'}, l: array{0: 'l'}, m: array{0: 'm'}, n: array{0: 'n'}, o: array{0: 'o'}, p: array{0: 'p'}, q: array{0: 'q'}, r: array{0: 'r'}, s: array{0: 's'}, t: array{0: 't'}, u: array{0: 'u'}, v: array{0: 'v'}, w: array{0: 'w'}, x: array{0: 'x'}, y: array{0: 'y'}, z: array{0: 'z'}, !: array{0: '!'}, '\\': array{0: '\'}, $: array{0: '$'}, %: array{0: '%'}, §: array{0: '§'}, &: array{0: '&'}, /: array{0: '/'}, (: array{0: '('}, ): array{0: ')'}, =: array{0: '='}, #: array{0: '#'}, ~: array{0: '~'}, *: array{0: '*'}, +: array{0: '+'}, ,: array{0: ','}, ;: array{0: ';'}, '.': array{0: '.'}, ':': array{0: ':'}, <: array{0: '<'}, >: array{0: '>'}, |: array{0: '|'}, _: array{0: '_'}} + * + * @return string[][] + */ + public function pathDelimiterProvider(): array + { + return [ + '0' => ['0'], + '1' => ['1'], + '2' => ['2'], + '3' => ['3'], + '4' => ['4'], + '5' => ['5'], + '6' => ['6'], + '7' => ['7'], + '8' => ['8'], + '9' => ['9'], + 'a' => ['a'], + 'b' => ['b'], + 'c' => ['c'], + 'd' => ['d'], + 'e' => ['e'], + 'f' => ['f'], + 'g' => ['g'], + 'h' => ['h'], + 'i' => ['i'], + 'j' => ['j'], + 'k' => ['k'], + 'l' => ['l'], + 'm' => ['m'], + 'n' => ['n'], + 'o' => ['o'], + 'p' => ['p'], + 'q' => ['q'], + 'r' => ['r'], + 's' => ['s'], + 't' => ['t'], + 'u' => ['u'], + 'v' => ['v'], + 'w' => ['w'], + 'x' => ['x'], + 'y' => ['y'], + 'z' => ['z'], + '!' => ['!'], + '\\' => ['\\'], + '$' => ['$'], + '%' => ['%'], + '§' => ['§'], + '&' => ['&'], + '/' => ['/'], + '(' => ['('], + ')' => [')'], + '=' => ['='], + '#' => ['#'], + '~' => ['~'], + '*' => ['*'], + '+' => ['+'], + ',' => [','], + ';' => [';'], + '.' => ['.'], + ':' => [':'], + '<' => ['<'], + '>' => ['>'], + '|' => ['|'], + '_' => ['_'], + ]; + } + + /** + * Test, that the path delimiter is checked for supported chars. + * + * @dataProvider pathDelimiterProvider + */ + public function testPathDelimiterIsBeingChecked(string $str): void + { + $supported_delimiters = ['.', '/']; + + $mailbox = $this->getMailbox(); + + if (\in_array($str, $supported_delimiters)) { + $this->assertTrue($mailbox->validatePathDelimiter($str)); + } else { + $this->expectException(InvalidParameterException::class); + $mailbox->setPathDelimiter($str); + } + } + + /** + * Test, that the path delimiter can be set. + */ + public function testSetAndGetPathDelimiter(): void + { + $mailbox = $this->getMailbox(); + + $mailbox->setPathDelimiter('.'); + $this->assertEquals($mailbox->getPathDelimiter(), '.'); + + $mailbox->setPathDelimiter('/'); + $this->assertEquals($mailbox->getPathDelimiter(), '/'); + } + + /** + * Test, that the attachments are not ignored by default. + */ + public function testGetAttachmentsAreNotIgnoredByDefault(): void + { + $this->assertEquals($this->getMailbox()->getAttachmentsIgnore(), false); + } + + /** + * Provides test data for testing attachments ignore. + * + * @psalm-return array + */ + public function attachmentsIgnoreProvider(): array + { + /** @psalm-var array */ + return [ + 'true' => [true], + 'false' => [false], + ]; + } + + /** + * Test, that attachments can be ignored and only valid values are accepted. + * + * @dataProvider attachmentsIgnoreProvider + */ + public function testSetAttachmentsIgnore(bool $paramValue): void + { + $mailbox = $this->getMailbox(); + $mailbox->setAttachmentsIgnore($paramValue); + $this->assertEquals($mailbox->getAttachmentsIgnore(), $paramValue); + } + + /** + * Provides test data for testing encoding. + * + * @psalm-return array{Avañe’ẽ: array{0: 'Avañe’ẽ'}, azərbaycanca: array{0: 'azərbaycanca'}, Bokmål: array{0: 'Bokmål'}, chiCheŵa: array{0: 'chiCheŵa'}, Deutsch: array{0: 'Deutsch'}, 'U.S. English': array{0: 'U.S. English'}, français: array{0: 'français'}, 'Éléments envoyés': array{0: 'Éléments envoyés'}, føroyskt: array{0: 'føroyskt'}, Kĩmĩrũ: array{0: 'Kĩmĩrũ'}, Kɨlaangi: array{0: 'Kɨlaangi'}, oʼzbekcha: array{0: 'oʼzbekcha'}, Plattdüütsch: array{0: 'Plattdüütsch'}, română: array{0: 'română'}, Sängö: array{0: 'Sängö'}, 'Tiếng Việt': array{0: 'Tiếng Việt'}, ɔl-Maa: array{0: 'ɔl-Maa'}, Ελληνικά: array{0: 'Ελληνικά'}, Ўзбек: array{0: 'Ўзбек'}, Азәрбајҹан: array{0: 'Азәрбајҹан'}, Српски: array{0: 'Српски'}, русский: array{0: 'русский'}, 'ѩзыкъ словѣньскъ': array{0: 'ѩзыкъ словѣньскъ'}, العربية: array{0: 'العربية'}, नेपाली: array{0: 'नेपाली'}, 日本語: array{0: '日本語'}, 简体中文: array{0: '简体中文'}, 繁體中文: array{0: '繁體中文'}, 한국어: array{0: '한국어'}, ąčęėįšųūžĄČĘĖĮŠŲŪŽ: array{0: 'ąčęėįšųūžĄČĘĖĮŠŲŪŽ'}} + * + * @return string[][] + */ + public function encodingTestStringsProvider(): array + { + return [ + 'Avañe’ẽ' => ['Avañe’ẽ'], // Guaraní + 'azərbaycanca' => ['azərbaycanca'], // Azerbaijani (Latin) + 'Bokmål' => ['Bokmål'], // Norwegian Bokmål + 'chiCheŵa' => ['chiCheŵa'], // Chewa + 'Deutsch' => ['Deutsch'], // German + 'U.S. English' => ['U.S. English'], // U.S. English + 'français' => ['français'], // French + 'Éléments envoyés' => ['Éléments envoyés'], // issue 499 + 'føroyskt' => ['føroyskt'], // Faroese + 'Kĩmĩrũ' => ['Kĩmĩrũ'], // Kimîîru + 'Kɨlaangi' => ['Kɨlaangi'], // Langi + 'oʼzbekcha' => ['oʼzbekcha'], // Uzbek (Latin) + 'Plattdüütsch' => ['Plattdüütsch'], // Low German + 'română' => ['română'], // Romanian + 'Sängö' => ['Sängö'], // Sango + 'Tiếng Việt' => ['Tiếng Việt'], // Vietnamese + 'ɔl-Maa' => ['ɔl-Maa'], // Masai + 'Ελληνικά' => ['Ελληνικά'], // Greek + 'Ўзбек' => ['Ўзбек'], // Uzbek (Cyrillic) + 'Азәрбајҹан' => ['Азәрбајҹан'], // Azerbaijani (Cyrillic) + 'Српски' => ['Српски'], // Serbian (Cyrillic) + 'русский' => ['русский'], // Russian + 'ѩзыкъ словѣньскъ' => ['ѩзыкъ словѣньскъ'], // Church Slavic + 'العربية' => ['العربية'], // Arabic + 'नेपाली' => ['नेपाली'], // Nepali + '日本語' => ['日本語'], // Japanese + '简体中文' => ['简体中文'], // Chinese (Simplified) + '繁體中文' => ['繁體中文'], // Chinese (Traditional) + '한국어' => ['한국어'], // Korean + 'ąčęėįšųūžĄČĘĖĮŠŲŪŽ' => ['ąčęėįšųūžĄČĘĖĮŠŲŪŽ'], // Lithuanian letters + ]; + } + + /** + * Test, that strings encoded to UTF-7 can be decoded back to UTF-8. + * + * @dataProvider encodingTestStringsProvider + */ + public function testEncodingToUtf7DecodeBackToUtf8(string $str): void + { + $mailbox = $this->getMailbox(); + + $utf7_encoded_str = $mailbox->encodeStringToUtf7Imap($str); + $utf8_decoded_str = $mailbox->decodeStringFromUtf7ImapToUtf8($utf7_encoded_str); + + $this->assertEquals($utf8_decoded_str, $str); + } + + /** + * Test, that strings encoded to UTF-7 can be decoded back to UTF-8. + * + * @dataProvider encodingTestStringsProvider + */ + public function testMimeDecodingReturnsCorrectValues(string $str): void + { + $this->assertEquals($this->getMailbox()->decodeMimeStr($str), $str); + } + + /** + * Provides test data for testing parsing datetimes. + * + * @psalm-return array{'Sun, 14 Aug 2005 16:13:03 +0000 (CEST)': array{0: '2005-08-14T16:13:03+00:00', 1: 1124035983}, 'Sun, 14 Aug 2005 16:13:03 +0000': array{0: '2005-08-14T16:13:03+00:00', 1: 1124035983}, 'Sun, 14 Aug 2005 16:13:03 +1000 (CEST)': array{0: '2005-08-14T06:13:03+00:00', 1: 1123999983}, 'Sun, 14 Aug 2005 16:13:03 +1000': array{0: '2005-08-14T06:13:03+00:00', 1: 1123999983}, 'Sun, 14 Aug 2005 16:13:03 -1000': array{0: '2005-08-15T02:13:03+00:00', 1: 1124071983}, 'Sun, 14 Aug 2005 16:13:03 +1100 (CEST)': array{0: '2005-08-14T05:13:03+00:00', 1: 1123996383}, 'Sun, 14 Aug 2005 16:13:03 +1100': array{0: '2005-08-14T05:13:03+00:00', 1: 1123996383}, 'Sun, 14 Aug 2005 16:13:03 -1100': array{0: '2005-08-15T03:13:03+00:00', 1: 1124075583}, '14 Aug 2005 16:13:03 +1000 (CEST)': array{0: '2005-08-14T06:13:03+00:00', 1: 1123999983}, '14 Aug 2005 16:13:03 +1000': array{0: '2005-08-14T06:13:03+00:00', 1: 1123999983}, '14 Aug 2005 16:13:03 -1000': array{0: '2005-08-15T02:13:03+00:00', 1: 1124071983}} + * + * @return (int|string)[][] + */ + public function datetimeProvider(): array + { + return [ + 'Sun, 14 Aug 2005 16:13:03 +0000 (CEST)' => ['2005-08-14T16:13:03+00:00', 1124035983], + 'Sun, 14 Aug 2005 16:13:03 +0000' => ['2005-08-14T16:13:03+00:00', 1124035983], + + 'Sun, 14 Aug 2005 16:13:03 +1000 (CEST)' => ['2005-08-14T06:13:03+00:00', 1123999983], + 'Sun, 14 Aug 2005 16:13:03 +1000' => ['2005-08-14T06:13:03+00:00', 1123999983], + 'Sun, 14 Aug 2005 16:13:03 -1000' => ['2005-08-15T02:13:03+00:00', 1124071983], + + 'Sun, 14 Aug 2005 16:13:03 +1100 (CEST)' => ['2005-08-14T05:13:03+00:00', 1123996383], + 'Sun, 14 Aug 2005 16:13:03 +1100' => ['2005-08-14T05:13:03+00:00', 1123996383], + 'Sun, 14 Aug 2005 16:13:03 -1100' => ['2005-08-15T03:13:03+00:00', 1124075583], + + '14 Aug 2005 16:13:03 +1000 (CEST)' => ['2005-08-14T06:13:03+00:00', 1123999983], + '14 Aug 2005 16:13:03 +1000' => ['2005-08-14T06:13:03+00:00', 1123999983], + '14 Aug 2005 16:13:03 -1000' => ['2005-08-15T02:13:03+00:00', 1124071983], + ]; + } + + /** + * Test, different datetimes conversions using differents timezones. + * + * @dataProvider datetimeProvider + */ + public function testParsedDateDifferentTimeZones(string $dateToParse, int $epochToCompare): void + { + $parsedDt = $this->getMailbox()->parseDateTime($dateToParse); + $parsedDateTime = new DateTime($parsedDt); + $this->assertEquals((int) $parsedDateTime->format('U'), $epochToCompare); + } + + /** + * Provides test data for testing parsing invalid / unparseable datetimes. + * + * @psalm-return array{'Sun, 14 Aug 2005 16:13:03 +9000 (CEST)': array{0: 'Sun, 14 Aug 2005 16:13:03 +9000 (CEST)'}, 'Sun, 14 Aug 2005 16:13:03 +9000': array{0: 'Sun, 14 Aug 2005 16:13:03 +9000'}, 'Sun, 14 Aug 2005 16:13:03 -9000': array{0: 'Sun, 14 Aug 2005 16:13:03 -9000'}} + * + * @return string[][] + */ + public function invalidDatetimeProvider(): array + { + return [ + 'Sun, 14 Aug 2005 16:13:03 +9000 (CEST)' => ['Sun, 14 Aug 2005 16:13:03 +9000 (CEST)'], + 'Sun, 14 Aug 2005 16:13:03 +9000' => ['Sun, 14 Aug 2005 16:13:03 +9000'], + 'Sun, 14 Aug 2005 16:13:03 -9000' => ['Sun, 14 Aug 2005 16:13:03 -9000'], + ]; + } + + /** + * Test, different invalid / unparseable datetimes conversions. + * + * @dataProvider invalidDatetimeProvider + */ + public function testParsedDateWithUnparseableDateTime(string $dateToParse): void + { + $parsedDt = $this->getMailbox()->parseDateTime($dateToParse); + $this->assertEquals($parsedDt, $dateToParse); + } + + /** + * Test, parsed datetime being emtpy the header date. + */ + public function testParsedDateTimeWithEmptyHeaderDate(): void + { + $this->expectException(InvalidParameterException::class); + $this->getMailbox()->parseDateTime(''); + } + + /** + * Provides test data for testing mime encoding. + * + * @return string[][] + * + * @psalm-return array{0: array{0: '=?iso-8859-1?Q?Sebastian_Kr=E4tzig?= ', 1: 'Sebastian Krätzig '}, 1: array{0: '=?iso-8859-1?Q?Sebastian_Kr=E4tzig?=', 1: 'Sebastian Krätzig'}, 2: array{0: 'sebastian.kraetzig', 1: 'sebastian.kraetzig'}, 3: array{0: '=?US-ASCII?Q?Keith_Moore?= ', 1: 'Keith Moore '}, 4: array{0: ' ', 1: ' '}, 5: array{0: '=?ISO-8859-1?Q?Max_J=F8rn_Simsen?= ', 1: 'Max Jørn Simsen '}, 6: array{0: '=?ISO-8859-1?Q?Andr=E9?= Muster ', 1: 'André Muster '}, 7: array{0: '=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?= =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=', 1: 'If you can read this you understand the example.'}, 8: array{0: '', 1: ''}} + */ + public function mimeEncodingProvider(): array + { + return [ + ['=?iso-8859-1?Q?Sebastian_Kr=E4tzig?= ', 'Sebastian Krätzig '], + ['=?iso-8859-1?Q?Sebastian_Kr=E4tzig?=', 'Sebastian Krätzig'], + ['sebastian.kraetzig', 'sebastian.kraetzig'], + ['=?US-ASCII?Q?Keith_Moore?= ', 'Keith Moore '], + [' ', ' '], + ['=?ISO-8859-1?Q?Max_J=F8rn_Simsen?= ', 'Max Jørn Simsen '], + ['=?ISO-8859-1?Q?Andr=E9?= Muster ', 'André Muster '], + ['=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?= =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=', 'If you can read this you understand the example.'], + ['', ''], // barbushin/php-imap#501 + ]; + } + + /** + * Test, that mime encoding returns correct strings. + * + * @dataProvider mimeEncodingProvider + */ + public function testMimeEncoding(string $str, string $expected): void + { + $mailbox = $this->getMailbox(); + + $this->assertEquals($mailbox->decodeMimeStr($str), $expected); + } + + /** + * Provides test data for testing timeouts. + * + * @psalm-return array}> + */ + public function timeoutsProvider(): array + { + /** @psalm-var array}> */ + return [ + 'array(IMAP_OPENTIMEOUT)' => ['assertNull', 1, [IMAP_OPENTIMEOUT]], + 'array(IMAP_READTIMEOUT)' => ['assertNull', 1, [IMAP_READTIMEOUT]], + 'array(IMAP_WRITETIMEOUT)' => ['assertNull', 1, [IMAP_WRITETIMEOUT]], + 'array(IMAP_CLOSETIMEOUT)' => ['assertNull', 1, [IMAP_CLOSETIMEOUT]], + 'array(IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT)' => ['assertNull', 1, [IMAP_OPENTIMEOUT, IMAP_READTIMEOUT, IMAP_WRITETIMEOUT, IMAP_CLOSETIMEOUT]], + ]; + } + + /** + * Test, that only supported timeouts can be set. + * + * @dataProvider timeoutsProvider + * + * @param int[] $types + * + * @psalm-param 'assertNull'|'expectException' $assertMethod + * @psalm-param list<1|2|3|4> $types + */ + public function testSetTimeouts(string $assertMethod, int $timeout, array $types): void + { + $mailbox = $this->getMailbox(); + + if ('expectException' == $assertMethod) { + $this->expectException(InvalidParameterException::class); + $mailbox->setTimeouts($timeout, $types); + } else { + $this->assertNull($mailbox->setTimeouts($timeout, $types)); + } + } + + /** + * Provides test data for testing connection args. + * + * @psalm-return Generator}, mixed, void> + */ + public function connectionArgsProvider(): Generator + { + yield from [ + 'readonly, disable gssapi' => ['assertNull', OP_READONLY, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'anonymous, disable gssapi' => ['assertNull', OP_ANONYMOUS, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'half open, disable gssapi' => ['assertNull', OP_HALFOPEN, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'expunge on close, disable gssapi' => ['assertNull', CL_EXPUNGE, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'debug, disable gssapi' => ['assertNull', OP_DEBUG, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'short cache, disable gssapi' => ['assertNull', OP_SHORTCACHE, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'silent, disable gssapi' => ['assertNull', OP_SILENT, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'return driver prototype, disable gssapi' => ['assertNull', OP_PROTOTYPE, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'don\'t do non-secure authentication, disable gssapi' => ['assertNull', OP_SECURE, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, disable gssapi, 1 retry' => ['assertNull', OP_READONLY, 1, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, disable gssapi, 3 retries' => ['assertNull', OP_READONLY, 3, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, disable gssapi, 12 retries' => ['assertNull', OP_READONLY, 12, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly debug, disable gssapi' => ['assertNull', OP_READONLY | OP_DEBUG, 0, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, -1 retries' => ['expectException', OP_READONLY, -1, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, -3 retries' => ['expectException', OP_READONLY, -3, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, -12 retries' => ['expectException', OP_READONLY, -12, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']], + 'readonly, null options' => ['expectException', OP_READONLY, 0, [null]], + ]; + + /** @psalm-var list */ + $options = [ + [OP_DEBUG, 'debug'], // 1 + [OP_READONLY, 'readonly'], // 2 + [OP_ANONYMOUS, 'anonymous'], // 4 + [OP_SHORTCACHE, 'short cache'], // 8 + [OP_SILENT, 'silent'], // 16 + [OP_PROTOTYPE, 'return driver prototype'], // 32 + [OP_HALFOPEN, 'half-open'], // 64 + [OP_SECURE, 'don\'t do non-secure authnetication'], // 256 + [CL_EXPUNGE, 'expunge on close'], // 32768 + ]; + + foreach ($options as $i => $option) { + $value = $option[0]; + + for ($j = $i + 1; $j < \count($options); ++$j) { + $value |= $options[$j][0]; + + $fields = []; + + foreach ($options as $option) { + if (0 !== ($value & $option[0])) { + $fields[] = $option[1]; + } + } + + $key = \implode(', ', $fields); + + yield $key => ['assertNull', $value, 0, []]; + yield ('INVALID + '.$key) => ['expectException', $value | 128, 0, []]; + } + } + } + + /** + * Test, that only supported and valid connection args can be set. + * + * @dataProvider connectionArgsProvider + * + * @psalm-param array{DISABLE_AUTHENTICATOR?:string}|array $param + */ + public function testSetConnectionArgs(string $assertMethod, int $option, int $retriesNum, array $param = null): void + { + $mailbox = $this->getMailbox(); + + if ('expectException' == $assertMethod) { + $this->expectException(InvalidParameterException::class); + $mailbox->setConnectionArgs($option, $retriesNum, $param); + $this->assertSame($option, $mailbox->getImapOptions()); + } elseif ('assertNull' == $assertMethod) { + $this->assertNull($mailbox->setConnectionArgs($option, $retriesNum, $param)); + } + + $mailbox->disconnect(); + } + + /** + * Provides test data for testing mime string decoding. + * + * @psalm-return array{'': array{0: '', 1: ''}, '': array{0: '', 1: ''}, '': array{0: '', 1: ''}, '': array{0: '', 1: ''}, 'Some subject here 😘': array{0: '=?UTF-8?q?Some_subject_here_?= =?UTF-8?q?=F0=9F=98=98?=', 1: 'Some subject here 😘'}, mountainguan测试: array{0: '=?UTF-8?Q?mountainguan=E6=B5=8B=E8=AF=95?=', 1: 'mountainguan测试'}, 'This is the Euro symbol \'\'.': array{0: 'This is the Euro symbol ''.', 1: 'This is the Euro symbol ''.'}, 'Some subject here 😘 US-ASCII': array{0: '=?UTF-8?q?Some_subject_here_?= =?UTF-8?q?=F0=9F=98=98?=', 1: 'Some subject here 😘', 2: 'US-ASCII'}, 'mountainguan测试 US-ASCII': array{0: '=?UTF-8?Q?mountainguan=E6=B5=8B=E8=AF=95?=', 1: 'mountainguan测试', 2: 'US-ASCII'}, 'مقتطفات من: صن تزو. \"فن الحرب\". كتب أبل. Something': array{0: 'مقتطفات من: صن تزو. "فن الحرب". كتب أبل. Something', 1: 'مقتطفات من: صن تزو. "فن الحرب". كتب أبل. Something'}, '(事件单编号:TESTA-111111)(通报)入口有陌生人': array{0: '=?utf-8?b?KOS6i+S7tuWNlee8luWPtzpURVNUQS0xMTExMTEpKOmAmuaKpSnl?= =?utf-8?b?haXlj6PmnInpmYznlJ/kuro=?=', 1: '(事件单编号:TESTA-111111)(通报)入口有陌生人'}} + * + * @return string[][] + */ + public function mimeStrDecodingProvider(): array + { + return [ + '' => ['', ''], + '' => ['', ''], + '' => ['', ''], + '' => ['', ''], + 'Some subject here 😘' => ['=?UTF-8?q?Some_subject_here_?= =?UTF-8?q?=F0=9F=98=98?=', 'Some subject here 😘'], + 'mountainguan测试' => ['=?UTF-8?Q?mountainguan=E6=B5=8B=E8=AF=95?=', 'mountainguan测试'], + "This is the Euro symbol ''." => ["This is the Euro symbol ''.", "This is the Euro symbol ''."], + 'Some subject here 😘 US-ASCII' => ['=?UTF-8?q?Some_subject_here_?= =?UTF-8?q?=F0=9F=98=98?=', 'Some subject here 😘', 'US-ASCII'], + 'mountainguan测试 US-ASCII' => ['=?UTF-8?Q?mountainguan=E6=B5=8B=E8=AF=95?=', 'mountainguan测试', 'US-ASCII'], + 'مقتطفات من: صن تزو. "فن الحرب". كتب أبل. Something' => ['مقتطفات من: صن تزو. "فن الحرب". كتب أبل. Something', 'مقتطفات من: صن تزو. "فن الحرب". كتب أبل. Something'], + '(事件单编号:TESTA-111111)(通报)入口有陌生人' => ['=?utf-8?b?KOS6i+S7tuWNlee8luWPtzpURVNUQS0xMTExMTEpKOmAmuaKpSnl?= =?utf-8?b?haXlj6PmnInpmYznlJ/kuro=?=', '(事件单编号:TESTA-111111)(通报)入口有陌生人'], + ]; + } + + /** + * Test, that decoding mime strings return unchanged / not broken strings. + * + * @dataProvider mimeStrDecodingProvider + */ + public function testDecodeMimeStr(string $str, string $expectedStr, string $serverEncoding = 'utf-8'): void + { + $mailbox = $this->getMailbox(); + + $mailbox->setServerEncoding($serverEncoding); + $this->assertEquals($mailbox->decodeMimeStr($str), $expectedStr); + } + + /** + * Provides test data for testing base64 string decoding. + * + * @psalm-return array{0: array{0: 'bm8tcmVwbHlAZXhhbXBsZS5jb20=', 1: 'no-reply@example.com'}, 1: array{0: 'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=', 1: 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'}, 2: array{0: 'SSBjYW4gZWF0IGdsYXNzIGFuZCBpdCBkb2VzIG5vdCBodXJ0IG1lLg==', 1: 'I can eat glass and it does not hurt me.'}, 3: array{0: '77u/4KSV4KS+4KSa4KSCIOCktuCkleCljeCkqOCli+CkruCljeCkr+CkpOCljeCkpOClgeCkruCljSDgpaQg4KSo4KWL4KSq4KS54KS/4KSo4KS44KWN4KSk4KS/IOCkruCkvuCkruCljSDgpaU=', 1: 'काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥'}, 4: array{0: 'SmUgcGV1eCBtYW5nZXIgZHUgdmVycmUsIMOnYSBuZSBtZSBmYWl0IHBhcyBtYWwu', 1: 'Je peux manger du verre, ça ne me fait pas mal.'}, 5: array{0: 'UG90IHPEgyBtxINuw6JuYyBzdGljbMSDIMiZaSBlYSBudSBtxIMgcsSDbmXImXRlLg==', 1: 'Pot să mănânc sticlă și ea nu mă rănește.'}, 6: array{0: '5oiR6IO95ZCe5LiL546755KD6ICM5LiN5YK36Lqr6auU44CC', 1: '我能吞下玻璃而不傷身體。'}} + * + * @return string[][] + */ + public function Base64DecodeProvider(): array + { + return [ + ['bm8tcmVwbHlAZXhhbXBsZS5jb20=', 'no-reply@example.com'], + ['TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=', 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'], + ['SSBjYW4gZWF0IGdsYXNzIGFuZCBpdCBkb2VzIG5vdCBodXJ0IG1lLg==', 'I can eat glass and it does not hurt me.'], + ['77u/4KSV4KS+4KSa4KSCIOCktuCkleCljeCkqOCli+CkruCljeCkr+CkpOCljeCkpOClgeCkruCljSDgpaQg4KSo4KWL4KSq4KS54KS/4KSo4KS44KWN4KSk4KS/IOCkruCkvuCkruCljSDgpaU=', 'काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥'], + ['SmUgcGV1eCBtYW5nZXIgZHUgdmVycmUsIMOnYSBuZSBtZSBmYWl0IHBhcyBtYWwu', 'Je peux manger du verre, ça ne me fait pas mal.'], + ['UG90IHPEgyBtxINuw6JuYyBzdGljbMSDIMiZaSBlYSBudSBtxIMgcsSDbmXImXRlLg==', 'Pot să mănânc sticlă și ea nu mă rănește.'], + ['5oiR6IO95ZCe5LiL546755KD6ICM5LiN5YK36Lqr6auU44CC', '我能吞下玻璃而不傷身體。'], + ]; + } + + /** + * @dataProvider Base64DecodeProvider + */ + public function testBase64Decode(string $input, string $expected): void + { + $this->assertSame($expected, \imap_base64(\preg_replace('~[^a-zA-Z0-9+=/]+~s', '', $input))); + $this->assertSame($expected, \base64_decode($input, false)); + } + + /** + * @psalm-return array{0: array{0: string, 1: '', 2: Exceptions\InvalidParameterException::class, 3: 'setAttachmentsDir() expects a string as first parameter!'}, 1: array{0: string, 1: ' ', 2: Exceptions\InvalidParameterException::class, 3: 'setAttachmentsDir() expects a string as first parameter!'}, 2: array{0: string, 1: string, 2: Exceptions\InvalidParameterException::class, 3: string}} + * + * @return string[][] + */ + public function attachmentDirFailureProvider(): array + { + return [ + [ + __DIR__, + '', + InvalidParameterException::class, + 'setAttachmentsDir() expects a string as first parameter!', + ], + [ + __DIR__, + ' ', + InvalidParameterException::class, + 'setAttachmentsDir() expects a string as first parameter!', + ], + [ + __DIR__, + __FILE__, + InvalidParameterException::class, + 'Directory "'.__FILE__.'" not found', + ], + ]; + } + + /** + * Test that setting the attachments directory fails when expected. + * + * @dataProvider attachmentDirFailureProvider + * + * @psalm-param class-string<\Exception> $expectedException + */ + public function testAttachmentDirFailure(string $initialDir, string $attachmentsDir, string $expectedException, string $expectedExceptionMessage): void + { + $mailbox = new Mailbox('', '', '', $initialDir); + + $this->assertSame(\trim($initialDir), $mailbox->getAttachmentsDir()); + + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $mailbox->setAttachmentsDir($attachmentsDir); + } + + protected function getMailbox(): Fixtures\Mailbox + { + return new Fixtures\Mailbox($this->imapPath, $this->login, $this->password, $this->attachmentsDir, $this->serverEncoding); + } +}