diff --git a/.github/linter.yml b/.github/linter.yml deleted file mode 100644 index 506dfd9..0000000 --- a/.github/linter.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Linter -on: - push: - branches: [test2] - -jobs: - build: - name: Linter - runs-on: ubuntu-latest - - steps: - - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: github/super-linter@v4 - env: - VALIDATE_ALL_CODEBASE: false - DEFAULT_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/gmail-test.yml b/.github/workflows/gmail-test.yml deleted file mode 100644 index 6c30153..0000000 --- a/.github/workflows/gmail-test.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: Gmail Test -on: - push: - branches: [main] - -env: - IMAP_MAILBOX: "{imap.gmail.com:993/ssl/novalidate-cert}" - IMAP_USERNAME: ${{ secrets.GOOGLE_USERNAME }} - IMAP_PASSWORD: ${{ secrets.GOOGLE_PASSWORD }} - IMAP_ACCESS_TOKEN: ${{ secrets.GOOGLE_ACCESS_TOKEN }} - -permissions: write-all - -jobs: - run: - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: - - 'ubuntu-latest' - #- 'windows-latest' - #- 'macos-latest' - php-versions: - - '7.0' - #- '7.4' - #- '8.0' - #- '8.1' - phpunit-versions: - - '6.5' - #include: - # - operating-system: 'ubuntu-latest' - # php-versions: '7.2' - # phpunit-versions: '8.5.21' - steps: - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl - ini-values: post_max_size=256M, max_execution_time=180 - coverage: xdebug - tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }} - - name: Git checkout - uses: actions/checkout@v3 - - name: Install dependencies - run: composer install --prefer-dist - - name: Test - run: phpunit \ No newline at end of file diff --git a/.github/workflows/refresh-access-token.yml.0 b/.github/workflows/refresh-access-token.yml.0 deleted file mode 100644 index 9e0cd9a..0000000 --- a/.github/workflows/refresh-access-token.yml.0 +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: refresh-access-token - -on: - schedule: - - cron: '*/30 * * * *' - workflow_dispatch: - -env: - GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} - GOOGLE_ACCESS_TOKEN: ${{ secrets.GOOGLE_ACCESS_TOKEN }} - GOOGLE_REFRESH_TOKEN: ${{ secrets.GOOGLE_REFRESH_TOKEN }} - -permissions: write-all - -jobs: - build: - runs-on: ubuntu-latest - steps: - #- run: echo ${{ secrets.GOOGLE_ACCESS_TOKEN }} | sed 's/./& /g' - - run: curl -sL https://raw.githubusercontent.com/javanile/php-imap2/${GITHUB_REF##*/}/contrib/refresh-access-token.sh | bash - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..82b2a71 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: Tests + +on: [push, pull_request] + +jobs: + php-tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + php: [8.4, 8.3, 8.2] + dependency-version: [prefer-stable] + os: [ubuntu-latest, windows-latest] + + name: ${{ matrix.os }} - PHP${{ matrix.php }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, intl, pdo_sqlite + coverage: none + + - name: Install dependencies + run: | + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit + diff --git a/.gitignore b/.gitignore index 055de1a..b2ebefa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ composer.lock coverage.xml .env +.php-cs-fixer.cache +.phpunit.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..139664d --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,15 @@ +setFinder(PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in(__DIR__.'/src') + ->in(__DIR__.'/Tests') + ); diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d58cc32..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,53 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - - -## [v0.1.10](https://github.com/javanile/php-imap2/compare/0.1.9...0.1.10) - 2022-10-25 - -### Changed - -- Updated imap2_fetchstructure message with support over 2000 emails ([#10](https://github.com/javanile/php-imap2/issues/10)) - -### Fixed - -- Fixed imap2_headers missing un-flagged emails ([#17](https://github.com/javanile/php-imap2/issues/17)) - - - -## [v0.1.9](https://github.com/javanile/php-imap2/compare/0.1.8...0.1.9) - 2022-09-15 - -### Changed - -- Updated imap2_last_error message ([#3](https://github.com/javanile/php-imap2/issues/3)) - -### Fixed - -- Fixed imap2_fetchmime unexpected behaviour ([#10](https://github.com/javanile/php-imap2/issues/10)) - - - -## [v0.1.8](https://github.com/javanile/php-imap2/compare/0.1.7...0.1.8) - 2022-09-05 - -### Changed - -- New reopen error message ([#3](https://github.com/javanile/php-imap2/issues/3)) - -### Fixed - -- Fixed reopen functions ([#3](https://github.com/javanile/php-imap2/issues/3)) - - - -## [v0.1.7](https://github.com/javanile/php-imap2/compare/0.1.0...0.1.7) - 2022-01-01 - -### Changed - -- Documentation amends ([#3](https://github.com/javanile/php-imap2/issues/3)) - -### Fixed - -- Generic imap2_open bugs ([#3](https://github.com/javanile/php-imap2/issues/3)) diff --git a/Makefile b/Makefile deleted file mode 100644 index 81f1e44..0000000 --- a/Makefile +++ /dev/null @@ -1,181 +0,0 @@ - -bash: - @docker-compose run --rm php bash - -build: - @docker-compose build - -install: - @docker-compose run --rm composer install - -update: - @docker-compose run --rm composer update - -dump-autoload: - @docker-compose run --rm composer dump-autoload - -imap2-coverage: - @docker-compose run --rm imap2 ./vendor/bin/phpunit tests --coverage-html docs/coverage - -imap2-test: - @docker-compose run --rm imap2 ./vendor/bin/phpunit tests --stop-on-failure - -coverage: - @docker-compose run --rm php ./vendor/bin/phpunit tests/ErrorsTest.php --coverage-html docs/coverage - -release: - git add . - git commit -am "Test CI" - git push - - -## ======= -## Develop -## ======= -google-access-token-link: - @open "https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground&prompt=consent&response_type=code&client_id=407408718192.apps.googleusercontent.com&scope=https%3A%2F%2Fmail.google.com%2F&access_type=offline&flowName=GeneralOAuthFlow" - -google-access-token: - @bash contrib/google-access-token.sh - -refresh-access-token: - @bash contrib/refresh-access-token.sh - -## ===== -## Tests -## ===== -test: - @docker-compose run --rm phpunit tests --stop-on-failure --verbose - -test-open: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testOpenAndClose - -test-alerts: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testAlerts - -test-last-error: - @docker-compose run --rm phpunit tests --filter ErrorsTest::testLastError - -test-check: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testCheck - -test-status: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testStatus - -test-mailbox-msg-info: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testMailboxMsgInfo - -test-append: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testAppend - -test-list: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testList - -test-delete: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testDelete - -test-fetch-body: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testFetchBody - -test-fetch-overview: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testFetchOverview - -test-uid: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testUid - -test-create-mailbox: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testCreateMailbox - -test-copy: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testCopy - -test-move: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testMove - -test-fetch-header: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testFetchHeader - -test-fetch-structure: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testFetchStructure - -test-header-info: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testHeaderInfo - -test-headers: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testHeaders - -test-num-msg: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testNumMsg - -test-reopen: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testReopen - -test-fetch-mime: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testFetchMime - -test-ping: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testPing - -test-get-mailboxes: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testGetMailboxes - -test-delete-mailbox: - @docker-compose run --rm phpunit tests --filter CompatibilityTest::testDeleteMailbox - -test-body-structure: - @docker-compose run --rm phpunit tests --filter BodyStructureTest::testFetchStructure - -test-search: - @docker-compose run --rm phpunit tests --filter SearchTest - -test-timeout: - @docker-compose run --rm phpunit tests --filter XoauthTest::testTimeout - -test-xoauth: - @docker-compose run --rm phpunit tests --filter XoauthTest - -test-signatures: - @docker-compose run --rm phpunit tests --filter SignaturesTest - -test-polyfill: - @docker-compose run --rm phpunit tests --filter PolyfillTest - -test-parse-headers: - @docker-compose run --rm phpunit tests --filter PolyfillTest::testRfc822ParseHeaders - -test-parse-adrlist: - @docker-compose run --rm phpunit tests --filter PolyfillTest::testRfc822ParseAdrList - -test-special: - @docker-compose run --rm phpunit tests --filter HeaderInfoTest::testSanitizeAddress - -test-minimal: - @docker-compose run --rm phpunit tests --filter MinimalTest - -test-cleaning: - @docker-compose run --rm phpunit tests --filter CleaningTest - -test-retrofit: - @docker-compose run --rm phpunit tests --filter RetrofitTest - -test-errors: - @docker-compose run --rm phpunit tests --filter ErrorsTest - -## ====== -## Legacy -## ====== -legacy-last-error: - @docker-compose run --rm php -f tests/legacy/last-error.php - -## ==== -## Diff -## ==== -diff-last-error: - @docker-compose run --rm php bash -c "php -f tests/legacy/last-error.php > tests/legacy/last-error.1.txt 2>&1" - @docker-compose run --rm imap2 bash -c "php -f tests/legacy/last-error.php > tests/legacy/last-error.2.txt 2>&1" - @docker-compose run --rm imap2 bash -c "chmod 777 -R tests/legacy" - -diff-fetch-body-error: - @docker-compose run --rm php bash -c "php -f tests/legacy/fetch-body-error.php > tests/legacy/fetch-body-error.1.txt 2>&1" - @docker-compose run --rm imap2 bash -c "php -f tests/legacy/fetch-body-error.php > tests/legacy/fetch-body-error.2.txt 2>&1" - @docker-compose run --rm imap2 bash -c "chmod 777 -R tests/legacy" diff --git a/tests/.env.examples b/OldTests/.env.examples similarity index 100% rename from tests/.env.examples rename to OldTests/.env.examples diff --git a/tests/BodyStructureTest.php b/OldTests/BodyStructureTest.php similarity index 100% rename from tests/BodyStructureTest.php rename to OldTests/BodyStructureTest.php diff --git a/tests/CleaningTest.php b/OldTests/CleaningTest.php similarity index 100% rename from tests/CleaningTest.php rename to OldTests/CleaningTest.php diff --git a/tests/CompatibilityTest.php b/OldTests/CompatibilityTest.php similarity index 100% rename from tests/CompatibilityTest.php rename to OldTests/CompatibilityTest.php diff --git a/tests/ErrorsTest.php b/OldTests/ErrorsTest.php similarity index 100% rename from tests/ErrorsTest.php rename to OldTests/ErrorsTest.php diff --git a/tests/HeaderInfoTest.php b/OldTests/HeaderInfoTest.php similarity index 100% rename from tests/HeaderInfoTest.php rename to OldTests/HeaderInfoTest.php diff --git a/tests/ImapTestCase.php b/OldTests/ImapTestCase.php similarity index 100% rename from tests/ImapTestCase.php rename to OldTests/ImapTestCase.php diff --git a/tests/MinimalTest.php b/OldTests/MinimalTest.php similarity index 100% rename from tests/MinimalTest.php rename to OldTests/MinimalTest.php diff --git a/tests/PolyfillTest.php b/OldTests/PolyfillTest.php similarity index 100% rename from tests/PolyfillTest.php rename to OldTests/PolyfillTest.php diff --git a/tests/RetrofitTest.php b/OldTests/RetrofitTest.php similarity index 100% rename from tests/RetrofitTest.php rename to OldTests/RetrofitTest.php diff --git a/tests/SearchTest.php b/OldTests/SearchTest.php similarity index 100% rename from tests/SearchTest.php rename to OldTests/SearchTest.php diff --git a/tests/SignaturesTest.php b/OldTests/SignaturesTest.php similarity index 100% rename from tests/SignaturesTest.php rename to OldTests/SignaturesTest.php diff --git a/tests/XoauthTest.php b/OldTests/XoauthTest.php similarity index 100% rename from tests/XoauthTest.php rename to OldTests/XoauthTest.php diff --git a/OldTests/bootstrap.php b/OldTests/bootstrap.php new file mode 100644 index 0000000..6653e0a --- /dev/null +++ b/OldTests/bootstrap.php @@ -0,0 +1,15 @@ + +# PHP IMAP Drop in Replacement for PHP 8.4 -# ⭐ Give me a star, please! ⭐ - - - - - ---- - -
- -![PHP IMAP2](docs/logo.png) - -# PHP IMAP2 - -[![Linter](https://github.com/javanile/php-imap2/actions/workflows/linter.yml/badge.svg)](https://github.com/javanile/php-imap2/actions/workflows/linter.yml) - -
- -## Requirements - -- PHP >= 7.0 - -## Install - -```shell -composer require javanile/php-imap2 -``` - -or - -Download latest release +A drop in PHP replacement for the imap_ functions removed from PHP 8.4. If imap_ functions are detected, then the built in functions are used. So you can install this on PHP 8.2 or 8.3 and when you upgrade to PHP 8.4, it will continue to work. ## Usage ```php -$mbh = imap2_open($server, $username, $token, OP_XOAUTH2); -if (! $mbh) { - error_log(imap2_last_error()); - throw new \RuntimeException('Unable to open the INBOX'); -} +include 'vendor/phpfui/php-imap2/src/Imap2/IMAPStubs.php +// continue to use imap_ functions. Example: +//$mbh = \imap_open($server, $username, $token, OP_XOAUTH2); ``` -## Gmail OAuth2 - -Scope: https://mail.google.com/ - -## Sandbox +## This package is based on arbor-education/php-imap2 and jaavanile/php-imap2 -- [Gmail Demo](https://replit.com/@frabik/PHP-IMAP2-Google-Demo?v=1#main.php) -- Outlook Demo - **COMING SOON** +It has been updated to modern PHP 8 standards and simplified. Since this a just a wrapper around the imap_ functions, it can easily be updated to a better IMAP library if needed. +## Unimplemented functions -## Contributors +The following functions were not implemented in the original source and will be undefined: -- [dicode-nl](https://github.com/dicode-nl) -- [glensc](https://github.com/glensc) -- [bago](https://github.com/bago) +* function imap_mail(string $to, string $subject, string $message, ?string $additional_headers = null, ?string $cc = null, ?string $bcc = null, ?string $return_path = null) : bool +* function imap_mail_compose(array $envelope, array $bodies) : string|false +* function imap_mime_header_decode(string $string) : array +* function imap_set_quota(IMAP\Connection $imap,string $quota_root): array|false -## Other links +The following functions have no effect: -- +* function imap_mutf7_to_utf8(string $string) : string -## Reference +## Testing -### Microsoft Outlook +Due to the complexities of setting up and running an actual IMAP server in GitHub Actions, test is confined to testing on live sites. -- +Anyone interested in contributing tests should look into updating the original tests in javanile/php-imap2 and get them to run under GitHub Actions. PRs welcome. -### IMAP & OAUTH +The following functions seem to work on live sites: -- -- -- +* imap_close() +* imap_delete() +* imap_errors() +* imap_num_msg() +* imap_open() +* imap_savebody() diff --git a/Tests/ErrorsTest.php b/Tests/ErrorsTest.php new file mode 100644 index 0000000..2920ac3 --- /dev/null +++ b/Tests/ErrorsTest.php @@ -0,0 +1,32 @@ +mailbox, $this->username, $this->accessToken, OP_SECURE); + + if ($imap) + { + $body = \imap_fetchbody($imap, 9999, null); + $this->assertFalse($body); + $this->assertFalse(\imap_last_error()); + } + else + $this->assertEquals("Can't open mailbox : no such mailbox", \imap_last_error()); + } + + public function testLastError1() : void + { + @\imap_open('...', '...', '...', OP_SECURE); + $this->assertEquals('Can\'t open mailbox ...: no such mailbox', \imap_last_error()); + } + + public function testLastError2() : void + { + @\imap_open('{imap.gmail.com:993/imap/ssl}', 'wrong-username', 'wrong-password', OP_SECURE); + $this->assertEquals('Can not authenticate to IMAP server: LOGIN: Invalid credentials (Failure)', \imap_last_error()); + } + } diff --git a/Tests/HeaderInfoTest.php b/Tests/HeaderInfoTest.php new file mode 100644 index 0000000..0506f4e --- /dev/null +++ b/Tests/HeaderInfoTest.php @@ -0,0 +1,24 @@ +' => 'TeamViewer Sign In Confirmation ', + '"Aruba.it" ' => 'Aruba.it ', + 'Aruba.it ' => 'Aruba.it ', + "'Aruba.it' " => "'Aruba.it' ", + "Aruba'i " => "Aruba'i ", + ]; + + foreach ($inputs as $input => $output) + { + $this->assertEquals($output, Functions::sanitizeAddress($input, 'localhost')); + } + } + } diff --git a/Tests/ImapTestCase.php b/Tests/ImapTestCase.php new file mode 100644 index 0000000..a5d2e3d --- /dev/null +++ b/Tests/ImapTestCase.php @@ -0,0 +1,148 @@ +mailbox = \getenv('IMAP_MAILBOX'); + $this->username = \getenv('IMAP_USERNAME'); + $this->password = \getenv('IMAP_PASSWORD'); + $this->accessToken = \getenv('IMAP_ACCESS_TOKEN'); + + $this->altMailbox = \getenv('IMAP_ALT_MAILBOX'); + $this->altUsername = \getenv('IMAP_ALT_USERNAME'); + $this->altPassword = \getenv('IMAP_ALT_PASSWORD'); + $this->altAccessToken = \getenv('IMAP_ALT_ACCESS_TOKEN'); + + $this->message = \file_get_contents('tests/fixtures/message.eml'); + } + + public function captureError() : void + { + $this->errorMessage = null; + $this->errorNumber = null; + $errorMessage = &$this->errorMessage; + $errorNumber = &$this->errorNumber; + \set_error_handler(static function($nr, $message) use (&$errorMessage, &$errorNumber) : void { + $errorMessage = $message; + $errorNumber = $nr; + }); + } + + public function retrieveError() : string + { + \restore_error_handler(); + + if (empty($this->errorMessage)) { + return null; + } + + // $errorNumber = $this->errorNumber; + + return $this->errorMessage; + } +} diff --git a/Tests/Unit/Rfc822ParseAdrlistTest.php b/Tests/Unit/Rfc822ParseAdrlistTest.php new file mode 100644 index 0000000..471a356 --- /dev/null +++ b/Tests/Unit/Rfc822ParseAdrlistTest.php @@ -0,0 +1,24 @@ +, postmaster@example.com, root'; + $address_array = \imap_rfc822_parse_adrlist($address_string, 'example.com'); + + $this->assertIsArray($address_array); + + $this->assertEquals('doe', $address_array[0]->mailbox); + $this->assertEquals('example.com', $address_array[0]->host); + $this->assertEquals('Joe Doe', $address_array[0]->personal); + + $this->assertEquals('postmaster', $address_array[1]->mailbox); + $this->assertEquals('example.com', $address_array[1]->host); + + $this->assertEquals('root', $address_array[2]->mailbox); + $this->assertEquals('example.com', $address_array[2]->host); + } + } diff --git a/Tests/Unit/Rfc822WriteAddressTest.php b/Tests/Unit/Rfc822WriteAddressTest.php new file mode 100644 index 0000000..15cd5d4 --- /dev/null +++ b/Tests/Unit/Rfc822WriteAddressTest.php @@ -0,0 +1,15 @@ +assertEquals('Hartmut Holzgraefe ', $address); + + $address = \imap_rfc822_write_address('hartmut', 'example.com', ''); + $this->assertEquals('hartmut@example.com', $address); + } + } diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php new file mode 100644 index 0000000..6653e0a --- /dev/null +++ b/Tests/bootstrap.php @@ -0,0 +1,15 @@ + -Received: from AUS01-ME3-obe.outbound.protection.outlook.com (mail-me3aus01on2042.outbound.protection.outlook.com. [40.107.108.42]) - by mx.google.com with ESMTPS id u20-20020a170903309400b0016362d70cf1si15490323plc.50.2022.09.14.16.30.09 - for - (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); - Wed, 14 Sep 2022 16:30:10 -0700 (PDT) -Received-SPF: pass (google.com: domain of chris.walker@canfigure.net designates 40.107.108.42 as permitted sender) client-ip=40.107.108.42; -Authentication-Results: mx.google.com; - dkim=temperror (no key for signature) header.i=@canfigurenet.onmicrosoft.com header.s=selector1-canfigurenet-onmicrosoft-com header.b=E0uBNnB2; - arc=pass (i=1 spf=pass spfdomain=canfigure.net dkim=pass dkdomain=canfigure.net dmarc=pass fromdomain=canfigure.net); - spf=pass (google.com: domain of chris.walker@canfigure.net designates 40.107.108.42 as permitted sender) smtp.mailfrom=chris.walker@canfigure.net; - dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=canfigure.net -ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; - b=clOMbK0x4yoU7VoeFpYib3EAHvmL6yluTjNm6w9r/NJij3hfI0/bxppfdrXXkU8e1WkJOxjKxkFSTvJgJ++y6VF8fKIPJStwbWH+QaAuWCuYdqM+ZoyOKRuqV05JsFqLA/6SQSUpQ+t9wcqTo25v9ufOpAw8iVrwr3UG0OY04+4ZnbdSthtfCho5U94riMRR2EQBnHicM/xUBPm0JPG0TVQPZ0PEdkKLEVke/pwuwot5JvUfSCDZW3HiQreE11S/jpQliD/AhPx8J+a2qemOGm56G/mjHAizxEzv8iAQPdJyWDTKZa+EHf73Xvc92nLGWoAmuPypvUyOscOYTZ7rNw== -ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; - s=arcselector9901; - h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; - bh=k/fVPgVh0+nxcbloFstaf0qPo+Hy5R3TQrRu48j3sgA=; - b=hlDMVnDjDgI8fFpDABvgGRN9g/0M//ycAnlEKU0JaAgSSAY+1YdSZvluENmafOQURCRFS4/B83ZbnBVgP0CLDyXnF44/zq8oW5TPAUit5UyRsOE0R6KmpHVcSqbrFnbPEPDrB2AiYLgyrLnoO6lK3a7N/6sxl7YUinhVXNAsDM8R8mvYcGcV7RFfSdTcSBjgJJTMW2biqwoeeSTq8jcUrhrscS6jCoEsWDfRHdSqWTSAbfVSBdE5D34Cgv6doD97AjE+OCN+xHgzHc7NNDkqDVl6dkt+ZZnJLrWdNe7OLlbvNqJoypyeJvL4BKYSJa82U7GBMT3By8USotYxX4UxXQ== -ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass - smtp.mailfrom=canfigure.net; dmarc=pass action=none - header.from=canfigure.net; dkim=pass header.d=canfigure.net; arc=none -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; - d=canfigurenet.onmicrosoft.com; s=selector1-canfigurenet-onmicrosoft-com; - h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; - bh=k/fVPgVh0+nxcbloFstaf0qPo+Hy5R3TQrRu48j3sgA=; - b=E0uBNnB2DVHiTTotTK/yworetpJm4vY7Rwn/X6+jx0/vtE7/SVAz6oHKuT29VY4clI79Nr3Eg0ytKyaujfwRd1suA2d6Zn5elzhV7rVZjqX3GImvBMTX6P0Kr8RpJlPq9IzKF+woZbwKIXMBUoa78aQFGJRrimdWoaiVYp+VWNk= -Received: from SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM (2603:10c6:10:ce::9) by - SY4P282MB2949.AUSP282.PROD.OUTLOOK.COM (2603:10c6:10:15c::7) with Microsoft - SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id - 15.20.5612.14; Wed, 14 Sep 2022 23:30:06 +0000 -Received: from SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM - ([fe80::89d6:57de:a343:678]) by SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM - ([fe80::89d6:57de:a343:678%4]) with mapi id 15.20.5612.022; Wed, 14 Sep 2022 - 23:30:06 +0000 -From: Chris Walker -To: "javanile.develop@gmail.com" -Subject: Encoding issue: E-mail with image -Thread-Topic: Encoding issue: E-mail with image -Thread-Index: AQHYyJHX784APQm8LECAPNRK8QCiiQ== -Date: Wed, 14 Sep 2022 23:30:06 +0000 -Message-ID: - -Accept-Language: en-US -Content-Language: en-US -X-MS-Has-Attach: yes -X-MS-TNEF-Correlator: -msip_labels: -authentication-results: dkim=none (message not signed) - header.d=none;dmarc=none action=none header.from=canfigure.net; -x-ms-publictraffictype: Email -x-ms-traffictypediagnostic: SY4P282MB1739:EE_|SY4P282MB2949:EE_ -x-ms-office365-filtering-correlation-id: 3be4be49-bfce-4a48-8756-08da96a90ea6 -x-ms-exchange-senderadcheck: 1 -x-ms-exchange-antispam-relay: 0 -x-microsoft-antispam: BCL:0; -x-microsoft-antispam-message-info: - 1uAwklekO4bwL64gyMVZt8mlhSIS0X1RHaKdRVdv2iW51jr8zxH5aeihMrl1aBnhUXalNwgyhAU42T5jPWJGPWKQ5uNfkdlnlEBMPIFigREHVNPiY9uCrdMJScPzzF9BAwsCQvEz+Dho0bf/12O/OZQwT53RRhAwoyy6jDhc9bddpYN12voNPbcF46KG9TZu91B+QWyU2kDfwINDfWk/ptS0gtf8tbPtOQ1PsWRe9Ixg9WJOgpgGrP4yX8pBYY1O3klw9Iqn24+o/yMGPw5R0rWKj0cHibYaJiccFC3TdYv5ahDVwQ1VxkFUp6v58YKZX+f0D50flhmUBetZkMwhHYosd7jrqqLvDfXQZZNRGJKNzXXK5K0/hFtg1Sgavs+TqXvwOMa6CKlQxbVuLCkJGif1vUfgoz6LQKG/Z2L6GlSkw4YewsScVKB2tDXX35vlT3c7DGns3rVtRipuFUfCFjr5lASZPxgRSeMIElrYxsDuB4DS7YsFdJagDei8gmox -x-forefront-antispam-report: - CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230022)(346002)(376002)(366004)(39830400003)(396003)(136003)(451199015)(9686003)(6506007)(7696005)(55016003)(26005)(33656002)(41300700001)(316002)(6916009)(71200400001)(478600001)(122000001)(38100700002)(38070700005)(99936003)(73894004)(186003)(4270600006)(86362001)(52536014)(2906002)(8936002)(44832011)(19627405001)(66946007)(66556008)(66476007)(66446008)(8676002)(5660300002)(64756008)(76116006);DIR:OUT;SFP:1101; -x-ms-exchange-antispam-messagedata-chunkcount: 1 -x-ms-exchange-antispam-messagedata-0: - =?iso-8859-1?Q?PuU7EPnUP9drcq13nAV7ausSWw1AK5bfTIOUXxu5HPoJxNv2T6w9gIsiqp?= - =?iso-8859-1?Q?+habGtj8OR0RVBn84cfs2yUS+uivxUYZwue9Al5b0kPt1M2LBOdl+1Ks50?= - =?iso-8859-1?Q?/0MEFeuVXyji/q73y71pUod8h81s182w/TOlLByj9Jd9rM2sKCieqr0pBE?= - =?iso-8859-1?Q?Rd3lnKI43t/YOtCHuGXsRYORvEoQoijjDj6PJrmkKC4JUqpDy2HjhCBrpi?= - =?iso-8859-1?Q?bQOWJYIO4xr+31oCEZ14APa5H/eycEcGW6wPQBXgi1o4tcxU26FchCpugo?= - =?iso-8859-1?Q?yd1RYtAJROIEc3GlZxL91Zm0CV1d6vM9TdSOXr1SuQMB4dhBS/PKgCLP1Y?= - =?iso-8859-1?Q?yKNy7/r8AbJtYOL1zpv3yFrgBgHU2R4yS1s3uiCscTVHXi5rAyAABbDOI9?= - =?iso-8859-1?Q?E1wUyXIkl6rt70AZNwaahyiyp27Slf/jarhfIye2O4YurOl9UX8k/a0uOB?= - =?iso-8859-1?Q?72e5blWtQowkHLJD7A3Hi7rVGAXrOSY3FRfKlbTKQU6zwN4l8Jx+S5farb?= - =?iso-8859-1?Q?MK3dFolImnzTrEe7Vl+PH0faskqsJufaK/qy1c5ZTacgbDrF3gYmDy2MxZ?= - =?iso-8859-1?Q?88QgfE53D3KXdMmjDGb/fyUZnKyJleIt7bnm3FzLg2uC+hSQHxqyVxceyO?= - =?iso-8859-1?Q?W7YAXZDi4yPWj8pTEQWjfutGLzyOPwLED5NF/rj9Xr0kexDfZOLfe3i0qq?= - =?iso-8859-1?Q?ubSntK5Bgaj9LEFTsFAd5fe9NBJHtkGwYMkhwcjPy4JxPh6Cw2eDULPwqI?= - =?iso-8859-1?Q?dPapDKtoL2XxKURn2K6oMEP8JOGuTeTt85QY0o/gdN+DFbEMDLVCA282jT?= - =?iso-8859-1?Q?ut9awkRL7hiGi8LvEHBEIVP1I0xLit/SJxa0pWCMsSiS+O3ehcZJA4MOvm?= - =?iso-8859-1?Q?g6Vn6aAjSMR9Ohs9EzY3zf1JbSfZ/3w3SFEbYYY0Z6sCLCK3+g2zGOQSey?= - =?iso-8859-1?Q?n25zMMCIxoeeDfnXn4IE81v8PkiCEGasSnZZHDUuHspMm/+5AEYr3Z3nBM?= - =?iso-8859-1?Q?Jx4zeh8f+aRzzIlYzfw8hDmIn3qsuJ81nRUK8HxPUjxfKzk0pPHNC9j+lT?= - =?iso-8859-1?Q?n6PdHO8WB+ZUDkVQ1poh+yq1sEjMJk2V/RhqXHqCsm7oE79eqcMTf0iA0u?= - =?iso-8859-1?Q?h514aEfseAC6lwWZ7RhA0qmbW3OoYRZJ7wn7XHGnYrdCyWRsMM5GQJHrS7?= - =?iso-8859-1?Q?30lGkdS4XtYisaAHPqJPfKICg0K2KWptw/vJ9d+Uy8r/2iWfY5e2JecLn3?= - =?iso-8859-1?Q?oFgWJNaPLZ6yzrM/4IoUq5N9lHGn2KAA8KvU/F0k7WvpB/PpoKjiH6Ekyf?= - =?iso-8859-1?Q?Cuyixj1j+KLfhhtruBTj0lzyoxevceSnorcNoMi5gWG6DseS0CpvnwAqIm?= - =?iso-8859-1?Q?iiy7Hs/6vDZ8BvPApZW7djFCnOIt90RU+2XgmU6XzDV+rzPl5IFcanpV+W?= - =?iso-8859-1?Q?etTPO1FR0/sNsOL/c/p8GhbgADNekS0q+O4nJ6mi/Xha8iBaAfjCo5WQEV?= - =?iso-8859-1?Q?XZyQS4Xt1AC7K/WsoYCJrING3wtNpCs3IRMCPpRt6ydP2/NOd3clC81VMi?= - =?iso-8859-1?Q?R4FCe9OdNioVIGaTmKhBVJvYLcsW7faN0mJ4pjuy5cXNHOUT0gjm/xIEQY?= - =?iso-8859-1?Q?IQ6DM5BQ/1GaJluRj39dyKwXX9chLkRyZI?= -Content-Type: multipart/mixed; - boundary="_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_" -MIME-Version: 1.0 -X-OriginatorOrg: canfigure.net -X-MS-Exchange-CrossTenant-AuthAs: Internal -X-MS-Exchange-CrossTenant-AuthSource: SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM -X-MS-Exchange-CrossTenant-Network-Message-Id: 3be4be49-bfce-4a48-8756-08da96a90ea6 -X-MS-Exchange-CrossTenant-originalarrivaltime: 14 Sep 2022 23:30:06.5468 - (UTC) -X-MS-Exchange-CrossTenant-fromentityheader: Hosted -X-MS-Exchange-CrossTenant-id: 0c994c59-c4af-4ffa-935d-dd1b2aa2cb0f -X-MS-Exchange-CrossTenant-mailboxtype: HOSTED -X-MS-Exchange-CrossTenant-userprincipalname: rziFXcK04PyB2UP5egyeVrT1sxaUAw31eRFUO256k7AG6bt68NNcnXp4idas6VHm1gqy7scPA22fY0gu3oQSTAIW8Mrsmd8utIXjsORJJI0= -X-MS-Exchange-Transport-CrossTenantHeadersStamped: SY4P282MB2949 - ---_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ -Content-Type: multipart/alternative; - boundary="_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_" - ---_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ -Content-Type: text/plain; charset="iso-8859-1" -Content-Transfer-Encoding: quoted-printable - - - ---_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ -Content-Type: text/html; charset="iso-8859-1" -Content-Transfer-Encoding: quoted-printable - - - - - - - -
-
-
- - - ---_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_-- - ---_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ -Content-Type: image/png; name="boy.png" -Content-Description: boy.png -Content-Disposition: attachment; filename="boy.png"; size=92256; - creation-date="Wed, 14 Sep 2022 23:29:40 GMT"; - modification-date="Wed, 14 Sep 2022 23:30:02 GMT" -Content-Transfer-Encoding: base64 - -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 -Nb/+9S/5q1/8nN/78R/wBz/9xzz+7HMIArpYbTZCEvKTn/yEuvGkfEwjFkJS9LSWCNqB6+trvvrq -K16+fMn19Q03NzdcX19zt7vn/GLJ9e077m4O5ARVk2ec9on9XmpLf/iTH/M//Q//Iz/72c+4efOW -3/76b9mei2ZzJuHVFfTO4TAa1OqQeDOfnFmTQ8QlCJUTfvcc8HEgVZEw1NRVJEZx92qns97iIBCv -0Os0G0vEUNULfFABc47aeeoVUjuqaurFktVmzWK1pKmXY2peWKsli5ZSJgyBrhNXfL/b0e53pG6g -aw8MM3R/msU5WIGRGYxmID3e18rzWJGt4Xx7qdnGGmyZBTGtSWMXY/G3DDwsNTtJexeeSpnjnjLk -Ea0T8UxF5SKsxQLOC8yp1FPNJDCQp+beMXEwP0qz6MzimWNEiK8qNhvZzHd3d9ze3uIV8QEza1g+ -cQYakM8RivohKR1EzDgn61jqllXVsFpuWG1kv11dXfH8+QvOH11hmpp6taZpGsWkFmoJSRUVhZCe -PmW1Wk0g/k6ery4PvL2+5t2bl/z1LwSE8fm7O7ZXV9i6kVno1uJr74Xc1Boqm6kXFc6uZsxNOvLV -etq2Zb/fa8Np4Obmhq+++opXb9/w8s1rfvmbX/Pv/uqvefnyJft9S3foIYNzcLay/Mmf/Al//ud/ -zn/4k98nhY6qtmzP14riSCOOwVkZ0C5uTZQa3Iz0p/IelwXZWHuBfnVDr1hFvQE5k2NQopmkw+Gl -5pUUAdL3PU3XMfR7nElYbzD398pAJszQ2Tq88Xz/xWc8evyUs7NzqqbReQRyk2NMR1avbVtaVVRh -SMQkjbXZGlzd0JipIbUdBszQs1iswGbK5FXnZYO4WrjsF6sldd1QLxZ4r63/Wu90VqjnpsLnVCiW -njEBYBtXyFmPN3surqU9Flbr3fh+57S7wGiwBzMByBgdIjIXpPl3zK3UaBnLGWd5bblcst1u2Ww2 -NI3QKBYA/RFGNU3MY+W1r7/+emzVEdo9S+MbFppUevL8E1arFecXV5yfn7Pdbjm7OOfy4hGrszMG -a8ehoNbakeuzZKzruub8/Jz1YnnUOdO3Ur+1JvHu3Ttevb7m7e0df/Xzv+D6+pqf/OP/iP/g9/+A -1AepPVsMbdcKDEkhK97XY7EwxkiKkRg6vDNcXpyNrw3Dc374w8/Y7/e8un7Ll19+yd/++Ef8+te/ -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 - ---_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_-- +Delivered-To: javanile.develop@gmail.com +Received: by 2002:a17:906:899e:b0:779:b701:86b8 with SMTP id gg30csp19141ejc; + Wed, 14 Sep 2022 16:30:11 -0700 (PDT) +X-Google-Smtp-Source: AA6agR69Z1/U7+33XmDCfhjcYHAqK+8RAcodE4+W7RsmoVD28K3eBiE3qCBpHT3zOZOIeQ5bG07M +X-Received: by 2002:a63:2ace:0:b0:434:ebd5:e18e with SMTP id q197-20020a632ace000000b00434ebd5e18emr32177118pgq.79.1663198210573; + Wed, 14 Sep 2022 16:30:10 -0700 (PDT) +ARC-Seal: i=2; a=rsa-sha256; t=1663198210; cv=pass; + d=google.com; s=arc-20160816; + b=WjuRETPblcwrIFT3Ijdswfpa8zuOlhzYvBgZp5jt52VXXKMpU/5+nxnEoMSYY2dvAz + R2+H/JDjzgUExb9F51PuvI1BuJP4WIf0vd6kZ+3/zX8HBOCn1WZUBNvMDGYTV4PLNxAa + 9S+605LPCK/83e0N140dN06247ohafX6E4mL4xXjBvTPJxDBz695nsoDh3k58u1Ap+UT + 59RZNCDeoucDdTdDtGDUgKF1AoSaj+GYvQv1G6eJ08ofWgyPz0spToGjdRjesXBr1K4Y + VtJybsrjCPEYOxamKnLC/hdmEMy9impjk98Qc/hB03dUVTxT0BfSw0R0SXRjS9tpjbuW + 5UZw== +ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; + h=mime-version:msip_labels:content-language:accept-language + :message-id:date:thread-index:thread-topic:subject:to:from + :dkim-signature; + bh=k/fVPgVh0+nxcbloFstaf0qPo+Hy5R3TQrRu48j3sgA=; + b=b/ZMA8hAU0GSkO53Wl9AcsnvMEfig9/BFpEg1sUS6Wx/zqVZ76O2eYCTOqbVK+WD11 + x3icV6iNhZsQqo1w3LPuQHTQPDtcskOuayIcI9e/GB6Vnk0HGB2QX84/o2HB316yChhU + eTu2DHJKXs7KsuP42fplGHmmr4aK2GNNf+oL/oO/BGsi/6AJlam7Z4BAYZAePOPDEohG + YZwTmZfpzLopCEh4O0MkrgQdSx43o46Sz/QSrJQsiB2BEagPeU7y4kIn+4jptGpQ35L1 + x0+TXfgPjmL5JeVu2SsHC9ocyJ0GRHMLMjOqlYeXnuubEYl7DkfOeiElzF4akVLoRNg2 + SjwA== +ARC-Authentication-Results: i=2; mx.google.com; + dkim=temperror (no key for signature) header.i=@canfigurenet.onmicrosoft.com header.s=selector1-canfigurenet-onmicrosoft-com header.b=E0uBNnB2; + arc=pass (i=1 spf=pass spfdomain=canfigure.net dkim=pass dkdomain=canfigure.net dmarc=pass fromdomain=canfigure.net); + spf=pass (google.com: domain of chris.walker@canfigure.net designates 40.107.108.42 as permitted sender) smtp.mailfrom=chris.walker@canfigure.net; + dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=canfigure.net +Return-Path: +Received: from AUS01-ME3-obe.outbound.protection.outlook.com (mail-me3aus01on2042.outbound.protection.outlook.com. [40.107.108.42]) + by mx.google.com with ESMTPS id u20-20020a170903309400b0016362d70cf1si15490323plc.50.2022.09.14.16.30.09 + for + (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); + Wed, 14 Sep 2022 16:30:10 -0700 (PDT) +Received-SPF: pass (google.com: domain of chris.walker@canfigure.net designates 40.107.108.42 as permitted sender) client-ip=40.107.108.42; +Authentication-Results: mx.google.com; + dkim=temperror (no key for signature) header.i=@canfigurenet.onmicrosoft.com header.s=selector1-canfigurenet-onmicrosoft-com header.b=E0uBNnB2; + arc=pass (i=1 spf=pass spfdomain=canfigure.net dkim=pass dkdomain=canfigure.net dmarc=pass fromdomain=canfigure.net); + spf=pass (google.com: domain of chris.walker@canfigure.net designates 40.107.108.42 as permitted sender) smtp.mailfrom=chris.walker@canfigure.net; + dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=canfigure.net +ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; + b=clOMbK0x4yoU7VoeFpYib3EAHvmL6yluTjNm6w9r/NJij3hfI0/bxppfdrXXkU8e1WkJOxjKxkFSTvJgJ++y6VF8fKIPJStwbWH+QaAuWCuYdqM+ZoyOKRuqV05JsFqLA/6SQSUpQ+t9wcqTo25v9ufOpAw8iVrwr3UG0OY04+4ZnbdSthtfCho5U94riMRR2EQBnHicM/xUBPm0JPG0TVQPZ0PEdkKLEVke/pwuwot5JvUfSCDZW3HiQreE11S/jpQliD/AhPx8J+a2qemOGm56G/mjHAizxEzv8iAQPdJyWDTKZa+EHf73Xvc92nLGWoAmuPypvUyOscOYTZ7rNw== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; + s=arcselector9901; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; + bh=k/fVPgVh0+nxcbloFstaf0qPo+Hy5R3TQrRu48j3sgA=; + b=hlDMVnDjDgI8fFpDABvgGRN9g/0M//ycAnlEKU0JaAgSSAY+1YdSZvluENmafOQURCRFS4/B83ZbnBVgP0CLDyXnF44/zq8oW5TPAUit5UyRsOE0R6KmpHVcSqbrFnbPEPDrB2AiYLgyrLnoO6lK3a7N/6sxl7YUinhVXNAsDM8R8mvYcGcV7RFfSdTcSBjgJJTMW2biqwoeeSTq8jcUrhrscS6jCoEsWDfRHdSqWTSAbfVSBdE5D34Cgv6doD97AjE+OCN+xHgzHc7NNDkqDVl6dkt+ZZnJLrWdNe7OLlbvNqJoypyeJvL4BKYSJa82U7GBMT3By8USotYxX4UxXQ== +ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass + smtp.mailfrom=canfigure.net; dmarc=pass action=none + header.from=canfigure.net; dkim=pass header.d=canfigure.net; arc=none +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=canfigurenet.onmicrosoft.com; s=selector1-canfigurenet-onmicrosoft-com; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; + bh=k/fVPgVh0+nxcbloFstaf0qPo+Hy5R3TQrRu48j3sgA=; + b=E0uBNnB2DVHiTTotTK/yworetpJm4vY7Rwn/X6+jx0/vtE7/SVAz6oHKuT29VY4clI79Nr3Eg0ytKyaujfwRd1suA2d6Zn5elzhV7rVZjqX3GImvBMTX6P0Kr8RpJlPq9IzKF+woZbwKIXMBUoa78aQFGJRrimdWoaiVYp+VWNk= +Received: from SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM (2603:10c6:10:ce::9) by + SY4P282MB2949.AUSP282.PROD.OUTLOOK.COM (2603:10c6:10:15c::7) with Microsoft + SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id + 15.20.5612.14; Wed, 14 Sep 2022 23:30:06 +0000 +Received: from SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM + ([fe80::89d6:57de:a343:678]) by SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM + ([fe80::89d6:57de:a343:678%4]) with mapi id 15.20.5612.022; Wed, 14 Sep 2022 + 23:30:06 +0000 +From: Chris Walker +To: "javanile.develop@gmail.com" +Subject: Encoding issue: E-mail with image +Thread-Topic: Encoding issue: E-mail with image +Thread-Index: AQHYyJHX784APQm8LECAPNRK8QCiiQ== +Date: Wed, 14 Sep 2022 23:30:06 +0000 +Message-ID: + +Accept-Language: en-US +Content-Language: en-US +X-MS-Has-Attach: yes +X-MS-TNEF-Correlator: +msip_labels: +authentication-results: dkim=none (message not signed) + header.d=none;dmarc=none action=none header.from=canfigure.net; +x-ms-publictraffictype: Email +x-ms-traffictypediagnostic: SY4P282MB1739:EE_|SY4P282MB2949:EE_ +x-ms-office365-filtering-correlation-id: 3be4be49-bfce-4a48-8756-08da96a90ea6 +x-ms-exchange-senderadcheck: 1 +x-ms-exchange-antispam-relay: 0 +x-microsoft-antispam: BCL:0; +x-microsoft-antispam-message-info: + 1uAwklekO4bwL64gyMVZt8mlhSIS0X1RHaKdRVdv2iW51jr8zxH5aeihMrl1aBnhUXalNwgyhAU42T5jPWJGPWKQ5uNfkdlnlEBMPIFigREHVNPiY9uCrdMJScPzzF9BAwsCQvEz+Dho0bf/12O/OZQwT53RRhAwoyy6jDhc9bddpYN12voNPbcF46KG9TZu91B+QWyU2kDfwINDfWk/ptS0gtf8tbPtOQ1PsWRe9Ixg9WJOgpgGrP4yX8pBYY1O3klw9Iqn24+o/yMGPw5R0rWKj0cHibYaJiccFC3TdYv5ahDVwQ1VxkFUp6v58YKZX+f0D50flhmUBetZkMwhHYosd7jrqqLvDfXQZZNRGJKNzXXK5K0/hFtg1Sgavs+TqXvwOMa6CKlQxbVuLCkJGif1vUfgoz6LQKG/Z2L6GlSkw4YewsScVKB2tDXX35vlT3c7DGns3rVtRipuFUfCFjr5lASZPxgRSeMIElrYxsDuB4DS7YsFdJagDei8gmox +x-forefront-antispam-report: + CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230022)(346002)(376002)(366004)(39830400003)(396003)(136003)(451199015)(9686003)(6506007)(7696005)(55016003)(26005)(33656002)(41300700001)(316002)(6916009)(71200400001)(478600001)(122000001)(38100700002)(38070700005)(99936003)(73894004)(186003)(4270600006)(86362001)(52536014)(2906002)(8936002)(44832011)(19627405001)(66946007)(66556008)(66476007)(66446008)(8676002)(5660300002)(64756008)(76116006);DIR:OUT;SFP:1101; +x-ms-exchange-antispam-messagedata-chunkcount: 1 +x-ms-exchange-antispam-messagedata-0: + =?iso-8859-1?Q?PuU7EPnUP9drcq13nAV7ausSWw1AK5bfTIOUXxu5HPoJxNv2T6w9gIsiqp?= + =?iso-8859-1?Q?+habGtj8OR0RVBn84cfs2yUS+uivxUYZwue9Al5b0kPt1M2LBOdl+1Ks50?= + =?iso-8859-1?Q?/0MEFeuVXyji/q73y71pUod8h81s182w/TOlLByj9Jd9rM2sKCieqr0pBE?= + =?iso-8859-1?Q?Rd3lnKI43t/YOtCHuGXsRYORvEoQoijjDj6PJrmkKC4JUqpDy2HjhCBrpi?= + =?iso-8859-1?Q?bQOWJYIO4xr+31oCEZ14APa5H/eycEcGW6wPQBXgi1o4tcxU26FchCpugo?= + =?iso-8859-1?Q?yd1RYtAJROIEc3GlZxL91Zm0CV1d6vM9TdSOXr1SuQMB4dhBS/PKgCLP1Y?= + =?iso-8859-1?Q?yKNy7/r8AbJtYOL1zpv3yFrgBgHU2R4yS1s3uiCscTVHXi5rAyAABbDOI9?= + =?iso-8859-1?Q?E1wUyXIkl6rt70AZNwaahyiyp27Slf/jarhfIye2O4YurOl9UX8k/a0uOB?= + =?iso-8859-1?Q?72e5blWtQowkHLJD7A3Hi7rVGAXrOSY3FRfKlbTKQU6zwN4l8Jx+S5farb?= + =?iso-8859-1?Q?MK3dFolImnzTrEe7Vl+PH0faskqsJufaK/qy1c5ZTacgbDrF3gYmDy2MxZ?= + =?iso-8859-1?Q?88QgfE53D3KXdMmjDGb/fyUZnKyJleIt7bnm3FzLg2uC+hSQHxqyVxceyO?= + =?iso-8859-1?Q?W7YAXZDi4yPWj8pTEQWjfutGLzyOPwLED5NF/rj9Xr0kexDfZOLfe3i0qq?= + =?iso-8859-1?Q?ubSntK5Bgaj9LEFTsFAd5fe9NBJHtkGwYMkhwcjPy4JxPh6Cw2eDULPwqI?= + =?iso-8859-1?Q?dPapDKtoL2XxKURn2K6oMEP8JOGuTeTt85QY0o/gdN+DFbEMDLVCA282jT?= + =?iso-8859-1?Q?ut9awkRL7hiGi8LvEHBEIVP1I0xLit/SJxa0pWCMsSiS+O3ehcZJA4MOvm?= + =?iso-8859-1?Q?g6Vn6aAjSMR9Ohs9EzY3zf1JbSfZ/3w3SFEbYYY0Z6sCLCK3+g2zGOQSey?= + =?iso-8859-1?Q?n25zMMCIxoeeDfnXn4IE81v8PkiCEGasSnZZHDUuHspMm/+5AEYr3Z3nBM?= + =?iso-8859-1?Q?Jx4zeh8f+aRzzIlYzfw8hDmIn3qsuJ81nRUK8HxPUjxfKzk0pPHNC9j+lT?= + =?iso-8859-1?Q?n6PdHO8WB+ZUDkVQ1poh+yq1sEjMJk2V/RhqXHqCsm7oE79eqcMTf0iA0u?= + =?iso-8859-1?Q?h514aEfseAC6lwWZ7RhA0qmbW3OoYRZJ7wn7XHGnYrdCyWRsMM5GQJHrS7?= + =?iso-8859-1?Q?30lGkdS4XtYisaAHPqJPfKICg0K2KWptw/vJ9d+Uy8r/2iWfY5e2JecLn3?= + =?iso-8859-1?Q?oFgWJNaPLZ6yzrM/4IoUq5N9lHGn2KAA8KvU/F0k7WvpB/PpoKjiH6Ekyf?= + =?iso-8859-1?Q?Cuyixj1j+KLfhhtruBTj0lzyoxevceSnorcNoMi5gWG6DseS0CpvnwAqIm?= + =?iso-8859-1?Q?iiy7Hs/6vDZ8BvPApZW7djFCnOIt90RU+2XgmU6XzDV+rzPl5IFcanpV+W?= + =?iso-8859-1?Q?etTPO1FR0/sNsOL/c/p8GhbgADNekS0q+O4nJ6mi/Xha8iBaAfjCo5WQEV?= + =?iso-8859-1?Q?XZyQS4Xt1AC7K/WsoYCJrING3wtNpCs3IRMCPpRt6ydP2/NOd3clC81VMi?= + =?iso-8859-1?Q?R4FCe9OdNioVIGaTmKhBVJvYLcsW7faN0mJ4pjuy5cXNHOUT0gjm/xIEQY?= + =?iso-8859-1?Q?IQ6DM5BQ/1GaJluRj39dyKwXX9chLkRyZI?= +Content-Type: multipart/mixed; + boundary="_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_" +MIME-Version: 1.0 +X-OriginatorOrg: canfigure.net +X-MS-Exchange-CrossTenant-AuthAs: Internal +X-MS-Exchange-CrossTenant-AuthSource: SY4P282MB1739.AUSP282.PROD.OUTLOOK.COM +X-MS-Exchange-CrossTenant-Network-Message-Id: 3be4be49-bfce-4a48-8756-08da96a90ea6 +X-MS-Exchange-CrossTenant-originalarrivaltime: 14 Sep 2022 23:30:06.5468 + (UTC) +X-MS-Exchange-CrossTenant-fromentityheader: Hosted +X-MS-Exchange-CrossTenant-id: 0c994c59-c4af-4ffa-935d-dd1b2aa2cb0f +X-MS-Exchange-CrossTenant-mailboxtype: HOSTED +X-MS-Exchange-CrossTenant-userprincipalname: rziFXcK04PyB2UP5egyeVrT1sxaUAw31eRFUO256k7AG6bt68NNcnXp4idas6VHm1gqy7scPA22fY0gu3oQSTAIW8Mrsmd8utIXjsORJJI0= +X-MS-Exchange-Transport-CrossTenantHeadersStamped: SY4P282MB2949 + +--_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ +Content-Type: multipart/alternative; + boundary="_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_" + +--_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +--_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + +
+
+
+ + + +--_000_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_-- + +--_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_ +Content-Type: image/png; name="boy.png" +Content-Description: boy.png +Content-Disposition: attachment; filename="boy.png"; size=92256; + creation-date="Wed, 14 Sep 2022 23:29:40 GMT"; + modification-date="Wed, 14 Sep 2022 23:30:02 GMT" +Content-Transfer-Encoding: base64 + +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 + +--_004_SY4P282MB173913A2B375BF08F7BF730786469SY4P282MB1739AUSP_-- diff --git a/tests/fixtures/embedded_email.eml b/Tests/fixtures/embedded_email.eml similarity index 100% rename from tests/fixtures/embedded_email.eml rename to Tests/fixtures/embedded_email.eml diff --git a/tests/fixtures/embedded_email_without_content_disposition.eml b/Tests/fixtures/embedded_email_without_content_disposition.eml similarity index 100% rename from tests/fixtures/embedded_email_without_content_disposition.eml rename to Tests/fixtures/embedded_email_without_content_disposition.eml diff --git a/tests/fixtures/four_nested_emails.eml b/Tests/fixtures/four_nested_emails.eml similarity index 100% rename from tests/fixtures/four_nested_emails.eml rename to Tests/fixtures/four_nested_emails.eml diff --git a/tests/fixtures/gbk_charset.eml b/Tests/fixtures/gbk_charset.eml similarity index 100% rename from tests/fixtures/gbk_charset.eml rename to Tests/fixtures/gbk_charset.eml diff --git a/tests/fixtures/html_only.eml b/Tests/fixtures/html_only.eml similarity index 100% rename from tests/fixtures/html_only.eml rename to Tests/fixtures/html_only.eml diff --git a/tests/fixtures/imap_mime_header_decode_returns_false.eml b/Tests/fixtures/imap_mime_header_decode_returns_false.eml similarity index 100% rename from tests/fixtures/imap_mime_header_decode_returns_false.eml rename to Tests/fixtures/imap_mime_header_decode_returns_false.eml diff --git a/tests/fixtures/inline_attachment.eml b/Tests/fixtures/inline_attachment.eml similarity index 100% rename from tests/fixtures/inline_attachment.eml rename to Tests/fixtures/inline_attachment.eml diff --git a/tests/fixtures/inputs.json b/Tests/fixtures/inputs.json similarity index 100% rename from tests/fixtures/inputs.json rename to Tests/fixtures/inputs.json diff --git a/tests/fixtures/issue_142.eml b/Tests/fixtures/issue_142.eml similarity index 100% rename from tests/fixtures/issue_142.eml rename to Tests/fixtures/issue_142.eml diff --git a/tests/fixtures/ks_c_5601-1987_headers.eml b/Tests/fixtures/ks_c_5601-1987_headers.eml similarity index 100% rename from tests/fixtures/ks_c_5601-1987_headers.eml rename to Tests/fixtures/ks_c_5601-1987_headers.eml diff --git a/tests/fixtures/mail_that_is_attachment.eml b/Tests/fixtures/mail_that_is_attachment.eml similarity index 100% rename from tests/fixtures/mail_that_is_attachment.eml rename to Tests/fixtures/mail_that_is_attachment.eml diff --git a/tests/fixtures/message.eml b/Tests/fixtures/message.eml similarity index 100% rename from tests/fixtures/message.eml rename to Tests/fixtures/message.eml diff --git a/tests/fixtures/missing_date.eml b/Tests/fixtures/missing_date.eml similarity index 100% rename from tests/fixtures/missing_date.eml rename to Tests/fixtures/missing_date.eml diff --git a/tests/fixtures/missing_from.eml b/Tests/fixtures/missing_from.eml similarity index 100% rename from tests/fixtures/missing_from.eml rename to Tests/fixtures/missing_from.eml diff --git a/tests/fixtures/mixed_filename.eml b/Tests/fixtures/mixed_filename.eml similarity index 100% rename from tests/fixtures/mixed_filename.eml rename to Tests/fixtures/mixed_filename.eml diff --git a/tests/fixtures/multiple_nested_attachments.eml b/Tests/fixtures/multiple_nested_attachments.eml similarity index 100% rename from tests/fixtures/multiple_nested_attachments.eml rename to Tests/fixtures/multiple_nested_attachments.eml diff --git a/tests/fixtures/nestes_embedded_with_attachment.eml b/Tests/fixtures/nestes_embedded_with_attachment.eml similarity index 100% rename from tests/fixtures/nestes_embedded_with_attachment.eml rename to Tests/fixtures/nestes_embedded_with_attachment.eml diff --git a/tests/fixtures/null_content_charset.eml b/Tests/fixtures/null_content_charset.eml similarity index 100% rename from tests/fixtures/null_content_charset.eml rename to Tests/fixtures/null_content_charset.eml diff --git a/tests/fixtures/pec.eml b/Tests/fixtures/pec.eml similarity index 100% rename from tests/fixtures/pec.eml rename to Tests/fixtures/pec.eml diff --git a/tests/fixtures/plain_only.eml b/Tests/fixtures/plain_only.eml similarity index 100% rename from tests/fixtures/plain_only.eml rename to Tests/fixtures/plain_only.eml diff --git a/tests/fixtures/plain_text_attachment.eml b/Tests/fixtures/plain_text_attachment.eml similarity index 100% rename from tests/fixtures/plain_text_attachment.eml rename to Tests/fixtures/plain_text_attachment.eml diff --git a/tests/fixtures/references.eml b/Tests/fixtures/references.eml similarity index 100% rename from tests/fixtures/references.eml rename to Tests/fixtures/references.eml diff --git a/tests/fixtures/simple_multipart.eml b/Tests/fixtures/simple_multipart.eml similarity index 100% rename from tests/fixtures/simple_multipart.eml rename to Tests/fixtures/simple_multipart.eml diff --git a/tests/fixtures/structured_with_attachment.eml b/Tests/fixtures/structured_with_attachment.eml similarity index 100% rename from tests/fixtures/structured_with_attachment.eml rename to Tests/fixtures/structured_with_attachment.eml diff --git a/tests/fixtures/thread/my_topic.eml b/Tests/fixtures/thread/my_topic.eml similarity index 100% rename from tests/fixtures/thread/my_topic.eml rename to Tests/fixtures/thread/my_topic.eml diff --git a/tests/fixtures/thread/re_my_topic.eml b/Tests/fixtures/thread/re_my_topic.eml similarity index 100% rename from tests/fixtures/thread/re_my_topic.eml rename to Tests/fixtures/thread/re_my_topic.eml diff --git a/tests/fixtures/thread/unrelated.eml b/Tests/fixtures/thread/unrelated.eml similarity index 100% rename from tests/fixtures/thread/unrelated.eml rename to Tests/fixtures/thread/unrelated.eml diff --git a/tests/fixtures/undisclosed-recipients/minus.eml b/Tests/fixtures/undisclosed-recipients/minus.eml similarity index 100% rename from tests/fixtures/undisclosed-recipients/minus.eml rename to Tests/fixtures/undisclosed-recipients/minus.eml diff --git a/tests/fixtures/undisclosed-recipients/space.eml b/Tests/fixtures/undisclosed-recipients/space.eml similarity index 100% rename from tests/fixtures/undisclosed-recipients/space.eml rename to Tests/fixtures/undisclosed-recipients/space.eml diff --git a/tests/fixtures/unknown_encoding.eml b/Tests/fixtures/unknown_encoding.eml similarity index 100% rename from tests/fixtures/unknown_encoding.eml rename to Tests/fixtures/unknown_encoding.eml diff --git a/tests/fixtures/without_charset_plain_only.eml b/Tests/fixtures/without_charset_plain_only.eml similarity index 100% rename from tests/fixtures/without_charset_plain_only.eml rename to Tests/fixtures/without_charset_plain_only.eml diff --git a/tests/fixtures/without_charset_simple_multipart.eml b/Tests/fixtures/without_charset_simple_multipart.eml similarity index 100% rename from tests/fixtures/without_charset_simple_multipart.eml rename to Tests/fixtures/without_charset_simple_multipart.eml diff --git a/bootstrap.php b/bootstrap.php deleted file mode 100644 index f3a9fd0..0000000 --- a/bootstrap.php +++ /dev/null @@ -1,1628 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Javanile\Imap2\Connection; -use Javanile\Imap2\Errors; -use Javanile\Imap2\Mail; -use Javanile\Imap2\Mailbox; -use Javanile\Imap2\Message; -use Javanile\Imap2\Thread; -use Javanile\Imap2\Polyfill; -use Javanile\Imap2\Timeout; -use Javanile\Imap2\Functions; - -define('IMAP2_CHARSET', 'UTF-8'); -define('IMAP2_RETROFIT_MODE', function_exists('imap_open')); - -if (!defined('NIL')) { - define('NIL', 0); -} -if (!defined('OP_DEBUG')) { - define('OP_DEBUG', 1); -} -if (!defined('OP_READONLY')) { - define('OP_READONLY', 2); -} -if (!defined('OP_ANONYMOUS')) { - define('OP_ANONYMOUS', 4); -} -if (!defined('OP_SHORTCACHE')) { - define('OP_SHORTCACHE', 8); -} -if (!defined('OP_SILENT')) { - define('OP_SILENT', 16); -} -if (!defined('OP_PROTOTYPE')) { - define('OP_PROTOTYPE', 32); -} -if (!defined('OP_HALFOPEN')) { - define('OP_HALFOPEN', 64); -} -if (!defined('OP_EXPUNGE')) { - define('OP_EXPUNGE', 128); -} -if (!defined('OP_SECURE')) { - define('OP_SECURE', 256); -} -if (!defined('OP_XOAUTH2')) { - define('OP_XOAUTH2', 512); -} -if (!defined('CL_EXPUNGE')) { - define('CL_EXPUNGE', 32768); -} -if (!defined('FT_UID')) { - define('FT_UID', 1); -} -if (!defined('FT_PEEK')) { - define('FT_PEEK', 2); -} -if (!defined('FT_NOT')) { - define('FT_NOT', 4); -} -if (!defined('FT_INTERNAL')) { - define('FT_INTERNAL', 8); -} -if (!defined('FT_PREFETCHTEXT')) { - define('FT_PREFETCHTEXT', 32); -} -if (!defined('ST_UID')) { - define('ST_UID', 1); -} -if (!defined('ST_SILENT')) { - define('ST_SILENT', 2); -} -if (!defined('ST_SET')) { - define('ST_SET', 4); -} -if (!defined('CP_UID')) { - define('CP_UID', 1); -} -if (!defined('CP_MOVE')) { - define('CP_MOVE', 2); -} -if (!defined('SE_UID')) { - define('SE_UID', 1); -} -if (!defined('SE_FREE')) { - define('SE_FREE', 2); -} -if (!defined('SE_NOPREFETCH')) { - define('SE_NOPREFETCH', 4); -} -if (!defined('SO_FREE')) { - define('SO_FREE', 8); -} -if (!defined('SO_NOSERVER')) { - define('SO_NOSERVER', 16); -} -if (!defined('SA_MESSAGES')) { - define('SA_MESSAGES', 1); -} -if (!defined('SA_RECENT')) { - define('SA_RECENT', 2); -} -if (!defined('SA_UNSEEN')) { - define('SA_UNSEEN', 4); -} -if (!defined('SA_UIDNEXT')) { - define('SA_UIDNEXT', 8); -} -if (!defined('SA_UIDVALIDITY')) { - define('SA_UIDVALIDITY', 16); -} -if (!defined('SA_ALL')) { - define('SA_ALL', 31); -} -if (!defined('LATT_NOINFERIORS')) { - define('LATT_NOINFERIORS', 1); -} -if (!defined('LATT_NOSELECT')) { - define('LATT_NOSELECT', 2); -} -if (!defined('LATT_MARKED')) { - define('LATT_MARKED', 4); -} -if (!defined('LATT_UNMARKED')) { - define('LATT_UNMARKED', 8); -} -if (!defined('LATT_REFERRAL')) { - define('LATT_REFERRAL', 16); -} -if (!defined('LATT_HASCHILDREN')) { - define('LATT_HASCHILDREN', 32); -} -if (!defined('LATT_HASNOCHILDREN')) { - define('LATT_HASNOCHILDREN', 64); -} -if (!defined('SORTDATE')) { - define('SORTDATE', 0); -} -if (!defined('SORTARRIVAL')) { - define('SORTARRIVAL', 1); -} -if (!defined('SORTFROM')) { - define('SORTFROM', 2); -} -if (!defined('SORTSUBJECT')) { - define('SORTSUBJECT', 3); -} -if (!defined('SORTTO')) { - define('SORTTO', 4); -} -if (!defined('SORTCC')) { - define('SORTCC', 5); -} -if (!defined('SORTSIZE')) { - define('SORTSIZE', 6); -} -if (!defined('TYPETEXT')) { - define('TYPETEXT', 0); -} -if (!defined('TYPEMULTIPART')) { - define('TYPEMULTIPART', 1); -} -if (!defined('TYPEMESSAGE')) { - define('TYPEMESSAGE', 2); -} -if (!defined('TYPEAPPLICATION')) { - define('TYPEAPPLICATION', 3); -} -if (!defined('TYPEAUDIO')) { - define('TYPEAUDIO', 4); -} -if (!defined('TYPEIMAGE')) { - define('TYPEIMAGE', 5); -} -if (!defined('TYPEVIDEO')) { - define('TYPEVIDEO', 6); -} -if (!defined('TYPEMODEL')) { - define('TYPEMODEL', 7); -} -if (!defined('TYPEOTHER')) { - define('TYPEOTHER', 8); -} -if (!defined('ENC7BIT')) { - define('ENC7BIT', 0); -} -if (!defined('ENC8BIT')) { - define('ENC8BIT', 1); -} -if (!defined('ENCBINARY')) { - define('ENCBINARY', 2); -} -if (!defined('ENCBASE64')) { - define('ENCBASE64', 3); -} -if (!defined('ENCQUOTEDPRINTABLE')) { - define('ENCQUOTEDPRINTABLE', 4); -} -if (!defined('ENCOTHER')) { - define('ENCOTHER', 5); -} -if (!defined('IMAP_OPENTIMEOUT')) { - define('IMAP_OPENTIMEOUT', 1); -} -if (!defined('IMAP_READTIMEOUT')) { - define('IMAP_READTIMEOUT', 2); -} -if (!defined('IMAP_WRITETIMEOUT')) { - define('IMAP_WRITETIMEOUT', 3); -} -if (!defined('IMAP_CLOSETIMEOUT')) { - define('IMAP_CLOSETIMEOUT', 4); -} -if (!defined('IMAP_GC_ELT')) { - define('IMAP_GC_ELT', 1); -} -if (!defined('IMAP_GC_ENV')) { - define('IMAP_GC_ENV', 2); -} -if (!defined('IMAP_GC_TEXTS')) { - define('IMAP_GC_TEXTS', 4); -} - -/** - * imap2_open - */ -if (!function_exists('imap_open')) { - /** @codeCoverageIgnore */ - function imap_open($mailbox, $user, $password, $flags = 0, $retries = 0, $options = []) - { - return imap2_open($mailbox, $user, $password, $flags, $retries, $options); - } -} -if (!function_exists('imap2_open')) { - function imap2_open($mailbox, $user, $password, $flags = 0, $retries = 0, $options = []) - { - if (IMAP2_RETROFIT_MODE && !($flags & OP_XOAUTH2)) { - return imap_open($mailbox, $user, $password, $flags, $retries, $options); - } - - return Connection::open($mailbox, $user, $password, $flags, $retries, $options); - } -} - -/** - * imap2_reopen - */ -if (!function_exists('imap_reopen')) { - /** @codeCoverageIgnore */ - function imap_reopen($imap, $mailbox, $flags = 0, $retries = 0) - { - return imap2_reopen($imap, $mailbox, $flags, $retries); - } -} -if (!function_exists('imap2_reopen')) { - function imap2_reopen($imap, $mailbox, $flags = 0, $retries = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_reopen($imap, $mailbox, $flags, $retries); - } - - return Connection::reopen($imap, $mailbox, $flags, $retries); - } -} - -/** - * imap2_ping - */ -if (!function_exists('imap_ping')) { - /** @codeCoverageIgnore */ - function imap_ping($imap) - { - return Connection::ping($imap); - } -} -if (!function_exists('imap2_ping')) { - function imap2_ping($imap) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_ping($imap); - } - - return Connection::ping($imap); - } -} - -/** - * imap2_close - */ -if (!function_exists('imap_close')) { - /** @codeCoverageIgnore */ - function imap_close($imap, $flags = 0) - { - return Connection::close($imap, $flags); - } -} -if (!function_exists('imap2_close')) { - function imap2_close($imap, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_close($imap, $flags); - } - - return Connection::close($imap, $flags); - } -} - -/** - * imap2_timeout - */ -if (!function_exists('imap_timeout')) { - /** @codeCoverageIgnore */ - function imap_timeout($timeoutType, $timeout = -1) - { - return imap2_timeout($timeoutType, $timeout); - } -} -if (!function_exists('imap2_timeout')) { - function imap2_timeout($timeoutType, $timeout = -1) - { - if (IMAP2_RETROFIT_MODE) { - imap_timeout($timeoutType, $timeout); - } - - return Timeout::set($timeoutType, $timeout); - } -} - -/** - * imap2_check - */ -if (!function_exists('imap_check')) { - function imap_check($imap) - { - return Mailbox::check($imap); - } -} -if (!function_exists('imap2_check')) { - function imap2_check($imap) - { - return Mailbox::check($imap); - } -} - -/** - * imap2_status - */ -if (!function_exists('imap_status')) { - function imap_status($imap, $mailbox, $flags) - { - return imap2_status($imap, $mailbox, $flags); - } -} -if (!function_exists('imap2_status')) { - function imap2_status($imap, $mailbox, $flags) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_status($imap, $mailbox, $flags); - } - - return Mailbox::status($imap, $mailbox, $flags); - } -} - -/** - * imap2_num_msg - */ -if (!function_exists('imap_num_msg')) { - /** @codeCoverageIgnore */ - function imap_num_msg($imap) - { - return imap2_num_msg($imap); - } -} -if (!function_exists('imap2_num_msg')) { - function imap2_num_msg($imap) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_num_msg($imap); - } - - return Mailbox::numMsg($imap); - } -} - -/** - * imap2_num_recent - */ -if (!function_exists('imap_num_recent')) { - function imap_num_recent($imap) - { - return imap2_num_recent($imap); - } -} -if (!function_exists('imap2_num_recent')) { - function imap2_num_recent($imap) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_num_recent($imap); - } - - return Mailbox::numRecent($imap); - } -} - -/** - * imap2_list - */ -if (!function_exists('imap_list')) { - /** @codeCoverageIgnore */ - function imap_list($imap, $reference, $pattern) - { - return imap2_list($imap, $reference, $pattern); - } -} -if (!function_exists('imap2_list')) { - function imap2_list($imap, $reference, $pattern) - { - return Mailbox::list($imap, $reference, $pattern); - } -} - -/** - * imap2_listmailbox - */ -if (!function_exists('imap_listmailbox')) { - /** @codeCoverageIgnore */ - function imap_listmailbox($imap, $reference, $pattern) - { - return imap2_listmailbox($imap, $reference, $pattern); - } -} -if (!function_exists('imap2_listmailbox')) { - function imap2_listmailbox($imap, $reference, $pattern) - { - return Mailbox::list($imap, $reference, $pattern); - } -} - -/** - * imap2_listscan - */ -if (!function_exists('imap_listscan')) { - /** @codeCoverageIgnore */ - function imap_listscan($imap, $reference, $pattern, $content) - { - return imap2_listscan($imap, $reference, $pattern, $content); - } -} -if (!function_exists('imap2_listscan')) { - function imap2_listscan($imap, $reference, $pattern, $content) - { - return Mailbox::listScan($imap, $reference, $pattern, $content); - } -} - -/** - * imap2_scan - */ -if (!function_exists('imap_scan')) { - /** @codeCoverageIgnore */ - function imap_scan($imap, $reference, $pattern, $content) - { - return imap2_scan($imap, $reference, $pattern, $content); - } -} -if (!function_exists('imap2_scan')) { - function imap2_scan($imap, $reference, $pattern, $content) - { - return Mailbox::listScan($imap, $reference, $pattern, $content); - } -} - -/** - * imap2_scanmailbox - */ -if (!function_exists('imap_scanmailbox')) { - /** @codeCoverageIgnore */ - function imap_scanmailbox($imap, $reference, $pattern, $content) - { - return imap2_scanmailbox($imap, $reference, $pattern, $content); - } -} -if (!function_exists('imap2_scanmailbox')) { - function imap2_scanmailbox($imap, $reference, $pattern, $content) - { - return Mailbox::listScan($imap, $reference, $pattern, $content); - } -} - -/** - * imap2_getmailboxes - */ -if (!function_exists('imap_getmailboxes')) { - /** @codeCoverageIgnore */ - function imap_getmailboxes($imap, $reference, $pattern) - { - return imap2_getmailboxes($imap, $reference, $pattern); - } -} -if (!function_exists('imap2_getmailboxes')) { - function imap2_getmailboxes($imap, $reference, $pattern) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_getmailboxes($imap, $reference, $pattern); - } - - return Mailbox::getMailboxes($imap, $reference, $pattern); - } -} - -/** - * imap2_listsubscribed - */ -if (!function_exists('imap_listsubscribed')) { - /** @codeCoverageIgnore */ - function imap_listsubscribed($imap, $reference, $pattern) - { - return imap2_listsubscribed($imap, $reference, $pattern); - } -} -if (!function_exists('imap2_listsubscribed')) { - function imap2_listsubscribed($imap, $reference, $pattern) - { - return Mailbox::listSubscribed($imap, $reference, $pattern); - } -} - -/** - * imap2_lsub - */ -if (!function_exists('imap_lsub')) { - /** @codeCoverageIgnore */ - function imap_lsub($imap, $reference, $pattern) - { - return imap2_lsub($imap, $reference, $pattern); - } -} -if (!function_exists('imap2_lsub')) { - function imap2_lsub($imap, $reference, $pattern) - { - return Mailbox::listSubscribed($imap, $reference, $pattern); - } -} - -/** - * imap2_getsubscribed - */ -if (!function_exists('imap_getsubscribed')) { - /** @codeCoverageIgnore */ - function imap_getsubscribed($imap, $reference, $pattern) - { - return imap2_getsubscribed($imap, $reference, $pattern); - } -} -if (!function_exists('imap2_getsubscribed')) { - function imap2_getsubscribed($imap, $reference, $pattern) - { - return Mailbox::getSubscribed($imap, $reference, $pattern); - } -} - -/** - * imap2_subscribe - */ -if (!function_exists('imap_subscribe')) { - /** @codeCoverageIgnore */ - function imap_subscribe($imap, $mailbox) - { - return imap2_subscribe($imap, $mailbox); - } -} -if (!function_exists('imap2_subscribe')) { - function imap2_subscribe($imap, $mailbox) - { - return Mailbox::subscribe($imap, $mailbox); - } -} - -/** - * imap2_unsubscribe - */ -if (!function_exists('imap_unsubscribe')) { - function imap_unsubscribe($imap, $mailbox) - { - return imap2_unsubscribe($imap, $mailbox); - } -} -if (!function_exists('imap2_unsubscribe')) { - function imap2_unsubscribe($imap, $mailbox) - { - return Mailbox::unsubscribe($imap, $mailbox); - } -} - -/** - * imap2_createmailbox - */ -if (!function_exists('imap_createmailbox')) { - function imap_createmailbox($imap, $mailbox) - { - return imap2_createmailbox($imap, $mailbox); - } -} -if (!function_exists('imap2_createmailbox')) { - function imap2_createmailbox($imap, $mailbox) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_createmailbox($imap, $mailbox); - } - - return Mailbox::createMailbox($imap, $mailbox); - } -} - -/** - * imap2_create - */ -if (!function_exists('imap_create')) { - function imap_create($imap, $mailbox) - { - return imap2_create($imap, $mailbox); - } -} -if (!function_exists('imap2_create')) { - function imap2_create($imap, $mailbox) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_create($imap, $mailbox); - } - - return Mailbox::createMailbox($imap, $mailbox); - } -} - -/** - * imap2_deletemailbox - */ -if (!function_exists('imap_deletemailbox')) { - function imap_deletemailbox($imap, $mailbox) - { - return imap2_deletemailbox($imap, $mailbox); - } -} -if (!function_exists('imap2_deletemailbox')) { - function imap2_deletemailbox($imap, $mailbox) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_deletemailbox($imap, $mailbox); - } - - return Mailbox::deleteMailbox($imap, $mailbox); - } -} - -/** - * imap2_renamemailbox - */ -if (!function_exists('imap_renamemailbox')) { - function imap_renamemailbox($imap, $from, $to) - { - return imap2_renamemailbox($imap, $from, $to); - } -} -if (!function_exists('imap2_renamemailbox')) { - function imap2_renamemailbox($imap, $from, $to) - { - return Mailbox::renameMailbox($imap, $from, $to); - } -} - -/** - * imap2_rename - */ -if (!function_exists('imap_rename')) { - function imap_rename($imap, $from, $to) - { - return imap2_rename($imap, $from, $to); - } -} -if (!function_exists('imap2_rename')) { - function imap2_rename($imap, $from, $to) - { - return Mailbox::renameMailbox($imap, $from, $to); - } -} - -/** - * imap2_mailboxmsginfo - */ -if (!function_exists('imap_mailboxmsginfo')) { - function imap_mailboxmsginfo($imap) - { - return imap2_mailboxmsginfo($imap); - } -} -if (!function_exists('imap2_mailboxmsginfo')) { - function imap2_mailboxmsginfo($imap) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_mailboxmsginfo($imap); - } - - return Mailbox::mailboxMsgInfo($imap); - } -} - -/** - * imap_search - */ -if (!function_exists('imap_search')) { - function imap_search($imap, $criteria, $flags = SE_FREE, $charset = "") - { - return imap2_search($imap, $criteria, $flags, $charset); - } -} -if (!function_exists('imap2_search')) { - function imap2_search($imap, $criteria, $flags = SE_FREE, $charset = "") - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - if (empty($charset)) { - return imap_search($imap, $criteria, $flags); - } else { - return imap_search($imap, $criteria, $flags, $charset); - } - } - - return Message::search($imap, $criteria, $flags, $charset); - } -} - -/** - * imap2_headers - */ -if (!function_exists('imap_headers')) { - function imap_headers($imap) - { - return imap2_headers($imap); - } -} -if (!function_exists('imap2_headers')) { - function imap2_headers($imap) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_headers($imap); - } - - return Message::headers($imap); - } -} - -/** - * imap2_msgno - */ -if (!function_exists('imap_msgno')) { - function imap_msgno($imap, $messageUid) - { - return imap2_msgno($imap, $messageUid); - } -} -if (!function_exists('imap2_msgno')) { - function imap2_msgno($imap, $messageUid) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_msgno($imap, $messageUid); - } - - return Message::msgNo($imap, $messageUid); - } -} - -/** - * imap2_uid - */ -if (!function_exists('imap_uid')) { - function imap_uid($imap, $messageNum) - { - return imap2_uid($imap, $messageNum); - } -} -if (!function_exists('imap2_uid')) { - function imap2_uid($imap, $messageNum) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_uid($imap, $messageNum); - } - - return Message::uid($imap, $messageNum); - } -} - -/** - * imap2_sort - */ -if (!function_exists('imap_sort')) { - function imap_sort($imap, $criteria, $reverse, $flags = 0, $searchCriteria = null, $charset = null) - { - return imap2_sort($imap, $criteria, $reverse, $flags, $searchCriteria, $charset); - } -} -if (!function_exists('imap2_sort')) { - function imap2_sort($imap, $criteria, $reverse, $flags = 0, $searchCriteria = null, $charset = null) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - if (empty($charset)) { - return imap_sort($imap, $criteria, $reverse, $flags, $searchCriteria); - } else { - return imap_sort($imap, $criteria, $reverse, $flags, $searchCriteria, $charset); - } - } - - return Message::sort($imap, $criteria, $reverse, $flags, $searchCriteria, $charset); - } -} - -/** - * - */ -if (!function_exists('imap_append')) { - function imap_append($imap, $folder, $message, $options = null, $internalDate = null) - { - return imap2_append($imap, $folder, $message, $options, $internalDate); - } -} -if (!function_exists('imap2_append')) { - function imap2_append($imap, $folder, $message, $options = null, $internalDate = null) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_append($imap, $folder, $message, $options, $internalDate); - } - - return Mailbox::append($imap, $folder, $message, $options, $internalDate); - } -} - -/** - * imap2_headerinfo - */ -if (!function_exists('imap_headerinfo')) { - function imap_headerinfo($imap, $messageNum, $fromLength = 0, $subjectLength = 0, $defaultHost = null) - { - return imap2_headerinfo($imap, $messageNum, $fromLength, $subjectLength, $defaultHost); - } -} -if (!function_exists('imap2_headerinfo')) { - function imap2_headerinfo($imap, $messageNum, $fromLength = 0, $subjectLength = 0, $defaultHost = null) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - if(PHP_MAJOR_VERSION < 8) { - return imap_headerinfo($imap, $messageNum, $fromLength, $subjectLength, $defaultHost); - } - - return imap_headerinfo($imap, $messageNum, $fromLength, $subjectLength); - } - - return Message::headerInfo($imap, $messageNum, $fromLength, $subjectLength, $defaultHost); - } -} - -/** - * imap2_header - */ -if (!function_exists('imap_header')) { - function imap_header($imap, $messageNum, $fromLength = 0, $subjectLength = 0, $defaultHost = null) - { - return imap2_header($imap, $messageNum, $fromLength, $subjectLength, $defaultHost); - } -} -if (!function_exists('imap2_header')) { - function imap2_header($imap, $messageNum, $fromLength = 0, $subjectLength = 0, $defaultHost = null) - { - return Message::headerInfo($imap, $messageNum, $fromLength, $subjectLength, $defaultHost); - } -} - -/** - * imap2_body - */ -if (!function_exists('imap_body')) { - function imap_body($imap, $messageNum, $flags = 0) - { - return imap2_body($imap, $messageNum, $flags); - } -} -if (!function_exists('imap2_body')) { - function imap2_body($imap, $messageNum, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_body($imap, $messageNum, $flags); - } - - return Message::body($imap, $messageNum, $flags); - } -} - -/** - * imap2_fetchtext - */ -if (!function_exists('imap_fetchtext')) { - function imap_fetchtext($imap, $messageNum, $flags = 0) - { - return imap2_fetchtext($imap, $messageNum, $flags); - } -} -if (!function_exists('imap2_fetchtext')) { - function imap2_fetchtext($imap, $messageNum, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_fetchtext($imap, $messageNum, $flags); - } - - return Message::body($imap, $messageNum, $flags); - } -} - -/** - * imap2_fetchbody - */ -if (!function_exists('imap_fetchbody')) { - function imap_fetchbody($imap, $messageNum, $section, $flags = 0) - { - return imap2_fetchbody($imap, $messageNum, $section, $flags); - } -} -if (!function_exists('imap2_fetchbody')) { - function imap2_fetchbody($imap, $messageNum, $section, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_fetchbody($imap, $messageNum, $section, $flags); - } - - return Message::fetchBody($imap, $messageNum, $section, $flags); - } -} - -/** - * imap2_bodystruct - */ -if (!function_exists('imap_bodystruct')) { - function imap_bodystruct($imap, $messageNum, $section) - { - return imap2_bodystruct($imap, $messageNum, $section); - } -} -if (!function_exists('imap2_bodystruct')) { - function imap2_bodystruct($imap, $messageNum, $section) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_bodystruct($imap, $messageNum, $section); - } - - return Message::bodyStruct($imap, $messageNum, $section); - } -} - -/** - * imap2_savebody - */ -if (!function_exists('imap_savebody')) { - function imap_savebody($imap, $file, $messageNum, $section = "", $flags = 0) - { - return imap2_savebody($imap, $file, $messageNum, $section, $flags); - } -} -if (!function_exists('imap2_savebody')) { - function imap2_savebody($imap, $file, $messageNum, $section = "", $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_savebody($imap, $file, $messageNum, $section, $flags); - } - - return Message::saveBody($imap, $file, $messageNum, $section, $flags); - } -} - -/** - * imap2_fetchstructure - */ -if (!function_exists('imap_fetchstructure')) { - function imap_fetchstructure($imap, $messageNum, $flags = 0) - { - return imap2_fetchstructure($imap, $messageNum, $flags); - } -} -if (!function_exists('imap2_fetchstructure')) { - function imap2_fetchstructure($imap, $messageNum, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_fetchstructure($imap, $messageNum, $flags); - } - - return Message::fetchStructure($imap, $messageNum, $flags); - } -} - -/** - * imap2_fetchheader - */ -if (!function_exists('imap_fetchheader')) { - function imap_fetchheader($imap, $messageNum, $flags = 0) - { - return imap2_fetchheader($imap, $messageNum, $flags); - } -} -if (!function_exists('imap2_fetchheader')) { - function imap2_fetchheader($imap, $messageNum, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_fetchheader($imap, $messageNum, $flags); - } - - return Message::fetchHeader($imap, $messageNum, $flags); - } -} - -/** - * imap2_fetch_overview - */ -if (!function_exists('imap_fetch_overview')) { - function imap_fetch_overview($imap, $sequence, $flags = 0) - { - return imap2_fetch_overview($imap, $sequence, $flags); - } -} -if (!function_exists('imap2_fetch_overview')) { - function imap2_fetch_overview($imap, $sequence, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_fetch_overview($imap, $sequence, $flags); - } - - return Message::fetchOverview($imap, $sequence, $flags); - } -} - -/** - * imap2_fetchmime - */ -if (!function_exists('imap_fetchmime')) { - function imap_fetchmime($imap, $messageNum, $section, $flags = 0) - { - return imap2_fetchmime($imap, $messageNum, $section, $flags); - } -} -if (!function_exists('imap2_fetchmime')) { - function imap2_fetchmime($imap, $messageNum, $section, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_fetchmime($imap, $messageNum, $section, $flags); - } - - return Message::fetchMime($imap, $messageNum, $section, $flags); - } -} - -/** - * imap2_delete - */ -if (!function_exists('imap_delete')) { - function imap_delete($imap, $messageNums, $flags = 0) - { - return imap2_delete($imap, $messageNums, $flags); - } -} -if (!function_exists('imap2_delete')) { - function imap2_delete($imap, $messageNums, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_delete($imap, $messageNums, $flags); - } - - return Message::delete($imap, $messageNums, $flags); - } -} - -/** - * imap2_undelete - */ -if (!function_exists('imap_undelete')) { - function imap_undelete($imap, $messageNums, $flags = 0) - { - return imap2_undelete($imap, $messageNums, $flags); - } -} -if (!function_exists('imap2_undelete')) { - function imap2_undelete($imap, $messageNums, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_undelete($imap, $messageNums, $flags); - } - - return Message::undelete($imap, $messageNums, $flags); - } -} - -/** - * imap2_clearflag_full - */ -if (!function_exists('imap_clearflag_full')) { - function imap_clearflag_full($imap, $sequence, $flag, $options = 0) - { - return imap2_clearflag_full($imap, $sequence, $flag, $options); - } -} -if (!function_exists('imap2_clearflag_full')) { - function imap2_clearflag_full($imap, $sequence, $flag, $options = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_clearflag_full($imap, $sequence, $flag, $options); - } - - return Message::clearFlagFull($imap, $sequence, $flag, $options); - } -} - -/** - * imap2_setflag_full - */ -if (!function_exists('imap_setflag_full')) { - function imap_setflag_full($imap, $sequence, $flag, $options = 0) - { - return imap2_setflag_full($imap, $sequence, $flag, $options); - } -} -if (!function_exists('imap2_setflag_full')) { - function imap2_setflag_full($imap, $sequence, $flag, $options = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_setflag_full($imap, $sequence, $flag, $options); - } - - return Message::setFlagFull($imap, $sequence, $flag, $options); - } -} - -/** - * imap2_mail_compose - */ -if (!function_exists('imap_mail_compose')) { - function imap_mail_compose($envelope, $bodies) - { - return imap2_mail_compose($envelope, $bodies); - } -} -if (!function_exists('imap2_mail_compose')) { - function imap2_mail_compose($envelope, $bodies) - { - return Polyfill::mailCompose($envelope, $bodies); - } -} - -/** - * imap2_mail_copy - */ -if (!function_exists('imap_mail_copy')) { - function imap_mail_copy($imap, $messageNums, $mailbox, $flags = 0) - { - return imap2_mail_copy($imap, $messageNums, $mailbox, $flags); - } -} -if (!function_exists('imap2_mail_copy')) { - function imap2_mail_copy($imap, $messageNums, $mailbox, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_mail_copy($imap, $messageNums, $mailbox, $flags); - } - - return Mail::copy($imap, $messageNums, $mailbox, $flags); - } -} - -/** - * imap2_mail_move - */ -if (!function_exists('imap_mail_move')) { - function imap_mail_move($imap, $messageNums, $mailbox, $flags = 0) - { - return imap2_mail_move($imap, $messageNums, $mailbox, $flags); - } -} -if (!function_exists('imap2_mail_move')) { - function imap2_mail_move($imap, $messageNums, $mailbox, $flags = 0) - { - if (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_mail_move($imap, $messageNums, $mailbox, $flags); - } - - return Mail::move($imap, $messageNums, $mailbox, $flags); - } -} - -/** - * imap2_mail - */ -if (!function_exists('imap_mail')) { - function imap_mail($to, $subject, $message, $additionalHeaders = null, $cc = null, $bcc = null, $returnPath = null) - { - return imap2_mail($to, $subject, $message, $additionalHeaders, $cc, $bcc, $returnPath); - } -} -if (!function_exists('imap2_mail')) { - function imap2_mail($to, $subject, $message, $additionalHeaders = null, $cc = null, $bcc = null, $returnPath = null) - { - if (IMAP2_RETROFIT_MODE) { - return imap_mail($to, $subject, $message, $additionalHeaders, $cc, $bcc, $returnPath); - } - - return Mail::send($to, $subject, $message, $additionalHeaders, $cc, $bcc, $returnPath); - } -} - -/** - * imap2_expunge - */ -if (!function_exists('imap_expunge')) { - function imap_expunge($imap) - { - return imap2_expunge($imap); - } -} -if (!function_exists('imap2_expunge')) { - function imap2_expunge($imap) - { - return Message::expunge($imap); - } -} - -/** - * imap2_gc - */ -if (!function_exists('imap_gc')) { - function imap_gc($imap, $flags) - { - return imap2_gc($imap, $flags); - } -} -if (!function_exists('imap2_gc')) { - function imap2_gc($imap, $flags) - { - return Message::expunge($imap, $flags); - } -} - -/** - * imap2_get_quota - */ -if (!function_exists('imap_get_quota')) { - function imap_get_quota($imap, $quotaRoot) - { - return imap2_get_quota($imap, $quotaRoot); - } -} -if (!function_exists('imap2_get_quota')) { - function imap2_get_quota($imap, $quotaRoot) - { - return Message::expunge($imap, $quotaRoot); - } -} - -/** - * imap2_set_quota - */ -if (!function_exists('imap_set_quota')) { - function imap_set_quota($imap, $quotaRoot, $mailboxSize) - { - return imap2_set_quota($imap, $quotaRoot, $mailboxSize); - } -} -if (!function_exists('imap2_set_quota')) { - function imap2_set_quota($imap, $quotaRoot, $mailboxSize) - { - return Message::expunge($imap, $quotaRoot, $mailboxSize); - } -} - -/** - * imap2_get_quotaroot - */ -if (!function_exists('imap_get_quotaroot')) { - function imap_get_quotaroot($imap, $mailbox) - { - return imap2_get_quotaroot($imap, $mailbox); - } -} -if (!function_exists('imap2_get_quotaroot')) { - function imap2_get_quotaroot($imap, $mailbox) - { - return Message::expunge($imap, $mailbox); - } -} - -/** - * imap2_getacl - */ -if (!function_exists('imap_getacl')) { - function imap_getacl($imap, $mailbox) - { - return imap2_getacl($imap, $mailbox); - } -} -if (!function_exists('imap2_getacl')) { - function imap2_getacl($imap, $mailbox) - { - return Message::expunge($imap, $mailbox); - } -} - -/** - * imap2_setacl - */ -if (!function_exists('imap_setacl')) { - function imap_setacl($imap, $mailbox, $userId, $rights) - { - return imap2_setacl($imap, $mailbox, $userId, $rights); - } -} -if (!function_exists('imap2_setacl')) { - function imap2_setacl($imap, $mailbox, $userId, $rights) - { - return Message::expunge($imap, $mailbox, $userId, $rights); - } -} - -/** - * imap2_thread - */ -if (!function_exists('imap_thread')) { - function imap_thread($imap, $flags = SE_FREE) - { - return imap2_thread($imap, $flags); - } -} -if (!function_exists('imap2_thread')) { - function imap2_thread($imap, $flags = SE_FREE) - { - return Thread::thread($imap, $flags); - } -} - -/** - * imap2_errors - */ -if (!function_exists('imap_errors')) { - function imap_errors() - { - return imap2_errors(); - } -} -if (!function_exists('imap2_errors')) { - function imap2_errors() - { - if (IMAP2_RETROFIT_MODE) { - $errors = imap_errors(); - if ($errors) { - return $errors; - } - } - - return Errors::errors(); - } -} - -/** - * imap2_last_error - */ -if (!function_exists('imap_last_error')) { - function imap_last_error() - { - return imap2_last_error(); - } -} -if (!function_exists('imap2_last_error')) { - function imap2_last_error() - { - return Errors::lastError(); - } -} - -/** - * imap2_alerts - */ -if (!function_exists('imap_alerts')) { - function imap_alerts() - { - return imap2_alerts(); - } -} -if (!function_exists('imap2_alerts')) { - function imap2_alerts() - { - return Errors::alerts(); - } -} - -/** - * imap2_8bit - */ -if (!function_exists('imap_8bit')) { - function imap_8bit($string) - { - return imap2_8bit($string); - } -} -if (!function_exists('imap2_8bit')) { - function imap2_8bit($string) - { - return function_exists('imap_8bit') ? imap_8bit($string) : Polyfill::convert8bit($string); - } -} - -/** - * imap2_base64 - */ -if (!function_exists('imap_base64')) { - function imap_base64($string) - { - return imap2_base64($string); - } -} -if (!function_exists('imap2_base64')) { - function imap2_base64($string) - { - if (IMAP2_RETROFIT_MODE) { - return imap_base64($string); - } - - // please check: https://www.php.net/manual/en/function.imap-base64.php#102766 - return base64_decode($string, true); - } -} - -/** - * imap2_binary - */ -if (!function_exists('imap_binary')) { - function imap_binary($string) - { - return imap2_binary($string); - } -} -if (!function_exists('imap2_binary')) { - function imap2_binary($string) - { - if (IMAP2_RETROFIT_MODE) { - return imap_binary($string); - } - - // please check: https://www.php.net/manual/en/function.base64-encode.php#111942 - return base64_encode($string); - } -} - -/** - * imap2_mime_header_decode - */ -if (!function_exists('imap_mime_header_decode')) { - function imap_mime_header_decode($string) - { - return imap2_mime_header_decode($string); - } -} -if (!function_exists('imap2_mime_header_decode')) { - function imap2_mime_header_decode($string) - { - if (IMAP2_RETROFIT_MODE) { - return imap_mime_header_decode($string); - } - - return Polyfill::mimeHeaderDecode($string); - } -} - -/** - * imap2_mutf7_to_utf8 - */ -if (!function_exists('imap_mutf7_to_utf8')) { - function imap_mutf7_to_utf8($string) - { - return imap2_mutf7_to_utf8($string); - } -} -if (!function_exists('imap2_mutf7_to_utf8')) { - function imap2_mutf7_to_utf8($string) - { - if (IMAP2_RETROFIT_MODE) { - return imap_mutf7_to_utf8($string); - } - - return Polyfill::mutf7ToUtf8($string); - } -} - -/** - * imap2_qprint - */ -if (!function_exists('imap_qprint')) { - function imap_qprint($string) - { - return Polyfill::qPrint($string); - } -} -if (!function_exists('imap2_qprint')) { - function imap2_qprint($string) - { - return imap_qprint($string); - } -} - -/** - * imap2_rfc822_parse_adrlist - */ -if (!function_exists('imap_rfc822_parse_adrlist')) { - function imap_rfc822_parse_adrlist($string, $defaultHostname) - { - return Polyfill::rfc822ParseAdrList($string, $defaultHostname); - } -} -if (!function_exists('imap2_rfc822_parse_adrlist')) { - function imap2_rfc822_parse_adrlist($string, $defaultHostname) - { - return imap_rfc822_parse_adrlist($string, $defaultHostname); - } -} - -/** - * imap2_rfc822_parse_headers - */ -if (!function_exists('imap_rfc822_parse_headers')) { - function imap_rfc822_parse_headers($headers, $defaultHostname = 'UNKNOWN') - { - return Polyfill::rfc822ParseHeaders($headers, $defaultHostname); - } -} -if (!function_exists('imap2_rfc822_parse_headers')) { - function imap2_rfc822_parse_headers($headers, $defaultHostname = 'UNKNOWN') - { - return imap_rfc822_parse_headers($headers, $defaultHostname); - } -} - -/** - * imap2_rfc822_write_address - */ -if (!function_exists('imap_rfc822_write_address')) { - function imap_rfc822_write_address($mailbox, $hostname, $personal) - { - return Polyfill::rfc822WriteHeaders($mailbox, $hostname, $personal); - } -} -if (!function_exists('imap2_rfc822_write_address')) { - function imap2_rfc822_write_address($mailbox, $hostname, $personal) - { - return imap_rfc822_write_address($mailbox, $hostname, $personal); - } -} - -/** - * imap_utf7_decode - */ -if (!function_exists('imap_utf7_decode')) { - function imap_utf7_decode($string) - { - return Polyfill::utf7Decode($string); - } -} -if (!function_exists('imap2_utf7_decode')) { - function imap2_utf7_decode($string) - { - return imap_utf7_decode($string); - } -} - -/** - * imap_utf7_encode - */ -if (!function_exists('imap_utf7_encode')) { - function imap_utf7_encode($string) - { - return Polyfill::utf7Encode($string); - } -} -if (!function_exists('imap2_utf7_encode')) { - function imap2_utf7_encode($string) - { - return imap_utf7_encode($string); - } -} - -/** - * imap2_utf8_to_mutf7 - */ -if (!function_exists('imap_utf8_to_mutf7')) { - function imap_utf8_to_mutf7($string) - { - return Polyfill::utf8ToMutf7($string); - } -} -if (!function_exists('imap2_utf8_to_mutf7')) { - function imap2_utf8_to_mutf7($string) - { - return imap_utf8_to_mutf7($string); - } -} - -/** - * imap2_utf8 - */ -if (!function_exists('imap_utf8')) { - function imap_utf8($string) - { - return Polyfill::utf8($string); - } -} -if (!function_exists('imap2_utf8')) { - function imap2_utf8(string $string) - { - return imap_utf8($string); - } -} diff --git a/composer.json b/composer.json index b7f9a58..6600fd7 100755 --- a/composer.json +++ b/composer.json @@ -1,54 +1,47 @@ { - "name": "javanile/php-imap2", + "name": "phpfui/php-imap", "type": "library", - "description": "PHP IMAP with OAUTH2", + "description": "PHP IMAP with OAUTH2 for drop in replacement in PHP 8.4", "keywords": [ "imap", "oauth", - "oauth2", - "google", - "microsoft", - "gmail", - "outlook", - "exchange" + "oauth2" ], - "homepage": "https://github.com/javanile/php-imap2", + "homepage": "https://github.com/phpfui/php-imap2", "license": "GPL-3.0-or-later", "authors": [ { "name": "Francesco Bianco", "homepage": "https://github.com/francescobianco" + }, + { + "name": "Bruce Wells", + "homepage": "https://github.com/phpfui" } ], "require": { - "php": ">=7.0", - "zbateson/mail-mime-parser": "^2.2" + "php": ">=8.2", + "zbateson/mail-mime-parser": "^3.0", + "pear/auth_sasl": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^6.5" - }, - "suggest": { - "ext-mbstring": "For best support/performance", - "ext-iconv": "For best support/performance" + "roave/security-advisories": "dev-latest", + "rector/rector": "^2.0", + "phpunit/phpunit": "<=12.0", + "phpfui/phpunit-syntax-coverage": "^1.0", + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*" }, "autoload": { "psr-4": { - "Javanile\\Imap2\\": "src" - }, - "files": [ - "bootstrap.php" - ] - }, - "autoload-dev": { - "psr-4": { - "Javanile\\Imap2\\Tests\\": "tests" - } - }, - "minimum-stability": "dev", - "extra": { - "thanks": { - "name": "Roundcube", - "url": "https://github.com/roundcube" + "PHPFUI\\Imap2\\": "src/Imap2/", + "IMAP\\": "src/IMAP/" } - } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "Tests/" + } + }, + "minimum-stability": "stable" } diff --git a/contrib/google-access-token.php b/contrib/google-access-token.php deleted file mode 100644 index 56b2e9d..0000000 --- a/contrib/google-access-token.php +++ /dev/null @@ -1,39 +0,0 @@ -'; -var_dump($response); -echo ''; \ No newline at end of file diff --git a/contrib/google-access-token.sh b/contrib/google-access-token.sh deleted file mode 100644 index 9b9a54a..0000000 --- a/contrib/google-access-token.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -docker run \ - --rm -ti \ - -p 8080:8080 \ - -v $PWD/contrib/google-access-token.php:/router.php \ - --env-file .env \ - php -S 0.0.0.0:8080 /router.php diff --git a/contrib/refresh-access-token.sh b/contrib/refresh-access-token.sh deleted file mode 100644 index e6f6f49..0000000 --- a/contrib/refresh-access-token.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -## Load environment for local testing -[ -f .env ] && source .env - -## ====== -## Google -## ====== - -## Refresh the access token -echo "Refresh the access token" -refresh_request=$(curl -s \ - -X POST \ - -d "client_id=$GOOGLE_CLIENT_ID" \ - -d "client_secret=$GOOGLE_CLIENT_SECRET" \ - -d "refresh_token=$GOOGLE_REFRESH_TOKEN" \ - -d 'grant_type=refresh_token' \ - https://accounts.google.com/o/oauth2/token) -access_token=$(echo "$refresh_request" | grep "access_token" | cut -d'"' -f4) - -## Store newly access token -if [ "${GOOGLE_ACCESS_TOKEN}" = "$access_token" ]; then - echo "Access token not was changed" -elif [ -n "$access_token" ]; then - echo "Update access token secret" - gh secret set GOOGLE_ACCESS_TOKEN --body $access_token --repo javanile/php-imap2 -else - echo "Problem on refresh access token: $refresh_request" - exit 1 -fi diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4b71fae..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: '2' - -services: - - php: - build: ./tests/docker/retrofit - volumes: ['./:/app'] - env_file: ['.env'] - - imap2: - build: ./tests/docker/imap2 - volumes: ['./:/app'] - env_file: ['.env'] - - composer: - extends: { service: php } - entrypoint: ['composer'] - - phpunit: - extends: { service: php } - entrypoint: ['./vendor/bin/phpunit'] diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index a0aa3fa..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -php-imap2.javanile.org \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 0c5fb05..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,4 +0,0 @@ -title: "PHP IMAP2" -remote_theme: pmarsceill/just-the-docs - -logo: /logo.png diff --git a/docs/compatibility.md b/docs/compatibility.md deleted file mode 100644 index edd2ba8..0000000 --- a/docs/compatibility.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -layout: default -title: Compatibility -nav_order: 3 ---- - -## Compatibility - -This page is intended to track the compatibility of all standard PHP [IMAP functions](https://www.php.net/manual/en/ref.imap.php) over the ones provided by [PHP-IMAP2](https://php-imap2.javanile.org/functions.html) -For each function is reported a list of issues born around problems on real use cases. Please add new issue if your have some concerns with specific function. -All below checked functions are tested with complex tests and will be considered an exact equivalent of the original one. -If your function is not checked, no problem, it works but can be effected by a small difference from the original one. Ask for support and create a new issue if your have special needs or mandatory behaviour to fix. - -- [x] [imap_alerts](https://github.com/javanile/php-imap2/issues?q=label%3Aalerts) -- [x] [imap_append](https://github.com/javanile/php-imap2/issues?q=label%3Aappend) -- [x] [imap_check](https://github.com/javanile/php-imap2/issues?q=label%3Acheck) -- [ ] [imap_clearflag_full](https://github.com/javanile/php-imap2/issues?q=label%3Aclearflag_full) -- [x] [imap_close](https://github.com/javanile/php-imap2/issues?q=label%3Aclose) -- [ ] [imap_createmailbox](https://github.com/javanile/php-imap2/issues?q=label%3Acreatemailbox) -- [ ] [imap_delete](https://github.com/javanile/php-imap2/issues?q=label%3Adelete) -- [ ] [imap_deletemailbox](https://github.com/javanile/php-imap2/issues?q=label%3Adeletemailbox) -- [ ] [imap_errors](https://github.com/javanile/php-imap2/issues?q=label%3Aerrors) -- [ ] [imap_expunge](https://github.com/javanile/php-imap2/issues?q=label%3Aexpunge) -- [ ] [imap_fetchbody](https://github.com/javanile/php-imap2/issues?q=label%3Afetchbody) -- [x] [imap_fetchheader](https://github.com/javanile/php-imap2/issues?q=label%3Afetchheader) -- [ ] [imap_fetch_overview](https://github.com/javanile/php-imap2/issues?q=label%3Afetch_overview) -- [x] [imap_fetchstructure](https://github.com/javanile/php-imap2/issues?q=label%3Afetchstructure) -- [ ] [imap_getmailboxes](https://github.com/javanile/php-imap2/issues?q=label%3Agetmailboxes) -- [ ] [imap_get_quotaroot](https://github.com/javanile/php-imap2/issues?q=label%3Aget_quotaroot) -- [x] [imap_headerinfo](https://github.com/javanile/php-imap2/issues?q=label%3Aheaderinfo) -- [x] [imap_last_error](https://github.com/javanile/php-imap2/issues?q=label%3Alast_error) -- [x] [imap_mail_copy](https://github.com/javanile/php-imap2/issues?q=label%3Amail_move) -- [x] [imap_mail_move](https://github.com/javanile/php-imap2/issues?q=label%3Afetchbody) -- [x] [imap_mime_header_decode](https://github.com/javanile/php-imap2/issues?q=label%3Amime_header_decode) -- [x] [imap_msgno](https://github.com/javanile/php-imap2/issues?q=label%3Amsgno) -- [x] [imap_num_msg](https://github.com/javanile/php-imap2/issues?q=label%3Anum_msg) -- [ ] [imap_open](https://github.com/javanile/php-imap2/issues?q=label%3Aopen) -- [x] [imap_ping](https://github.com/javanile/php-imap2/issues?q=label%3Aping) -- [ ] [imap_reopen](https://github.com/javanile/php-imap2/issues?q=label%3Areopen) -- [ ] [imap_savebody](https://github.com/javanile/php-imap2/issues?q=label%3Asavebody) -- [ ] [imap_search](https://github.com/javanile/php-imap2/issues?q=label%3Asearch) -- [ ] [imap_setflag_full](https://github.com/javanile/php-imap2/issues?q=label%3Asetflag_full) -- [ ] [imap_sort](https://github.com/javanile/php-imap2/issues?q=label%3Asort) -- [ ] [imap_status](https://github.com/javanile/php-imap2/issues?q=label%3Astatus) -- [ ] [imap_thread](https://github.com/javanile/php-imap2/issues?q=label%3Athread) -- [ ] [imap_timeout](https://github.com/javanile/php-imap2/issues?q=label%3Atimeout) -- [ ] [imap_undelete](https://github.com/javanile/php-imap2/issues?q=label%3Aundelete) - -Your function not is on the list. [Submit an issue for it.](https://github.com/javanile/php-imap2/issues/new) diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index c64aab8..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -layout: default -title: Examples -nav_order: 4 ---- - -# Examples - -## Gmail - -## Outlook - -```php - - - - -

If you are not redirected in five seconds, click here.

- diff --git a/docs/index.md b/docs/index.md deleted file mode 100755 index ed8012f..0000000 --- a/docs/index.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -layout: default -title: PHP IMAP2 -nav_order: 1 ---- - -# PHP IMAP2 - -Welcome, please, get you a chance to PHP-IMAP2 -This is a FULL implementation of standard PHP IMAP functions like (imap_open, imap_getmailboxes, imap_*, etc...) - -This was full tested, every input to imap2_* functions get back the same output of imap_* equivalent - -- - - -> The IMAP2 works well with OAUTH - -- - -This libray can be installed with composer - -```shell -composer require javanile/php-imap2 -``` - -This library introduce a easy way to replace the old PHP-IMAP with new one: - -> JUST replace all imap_(...) functions with imap2_(...) - -** NO OTHER AMENDS are required. ** - -> Please give me the opportunity to make my sacrifices useful to the community. diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100755 index c2980fb..0000000 Binary files a/docs/logo.png and /dev/null differ diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..a4135b5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 5 + errorFormat: raw + editorUrl: '%%file%% %%line%% %%column%%: %%error%%' + paths: + - src diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index e61da00..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - ./src/ - - - - - ./tests/ - - - - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..1f12644 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + ./Tests/ + + + diff --git a/src/Acl.php b/src/Acl.php deleted file mode 100644 index b5d8dd8..0000000 --- a/src/Acl.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Acl -{ - -} diff --git a/src/BodyStructure.php b/src/BodyStructure.php deleted file mode 100644 index ad2396b..0000000 --- a/src/BodyStructure.php +++ /dev/null @@ -1,286 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class BodyStructure -{ - - const TYPETEXT = 0; /* unformatted text */ - const TYPEMULTIPART = 1; /* multiple part */ - const TYPEMESSAGE = 2; /* encapsulated message */ - const TYPEAPPLICATION = 3; /* application data */ - const TYPEAUDIO = 4; /* audio */ - const TYPEIMAGE = 5; /* static image */ - const TYPEVIDEO = 6; /* video */ - const TYPEMODEL = 7; /* model */ - const TYPEOTHER = 8; /* unknown */ - - const ENC7BIT = 0; /* 7 bit SMTP semantic data */ - const ENC8BIT = 1; /* 8 bit SMTP semantic data */ - const ENCBINARY = 2; /* 8 bit binary data */ - const ENCBASE64 = 3; /* base-64 encoded data */ - const ENCQUOTEDPRINTABLE = 4; /* human-readable 8-as-7 bit data */ - const ENCOTHER = 5; /* unknown */ - - protected static $body_types = [ - self::TYPETEXT => "TEXT", - self::TYPEMULTIPART => "MULTIPART", - self::TYPEMESSAGE => "MESSAGE", - self::TYPEAPPLICATION => "APPLICATION", - self::TYPEAUDIO => "AUDIO", - self::TYPEIMAGE => "IMAGE", - self::TYPEVIDEO => "VIDEO", - self::TYPEMODEL => "MODEL", - self::TYPEOTHER => "X-UNKNOWN" - ]; - - protected static $body_encodings = [ - self::ENC7BIT => "7BIT", - self::ENC8BIT => "8BIT", - self::ENCBINARY => "BINARY", - self::ENCBASE64 => "BASE64", - self::ENCQUOTEDPRINTABLE => "QUOTED-PRINTABLE", - self::ENCOTHER => "X-UNKNOWN" - ]; - - public static function fromMessage($message) - { - return self::extractBodyStructure($message->bodystructure); - } - - protected static function extractBodyStructure($structure) - { - - /* if NIL */ - if ( is_null($structure) ) - return null; - - /* body structure list */ - if ( ! $length = count($structure) ) - return null; - - $body = (object)[ - "type" => self::TYPEOTHER, - "encoding" => self::ENC7BIT, - "ifsubtype" => 0, - "subtype" => null, - "ifdescription" => 0, - "description" => null, - "ifid" => 0, - "id" => null, - "lines" => null, - "bytes" => null, - "ifdisposition" => 0, - "disposition" => null, - "ifdparameters" => 0, - "dparameters" => null, - "ifparameters" => 0, - "parameters" => null - ]; - - /* multipart body? */ - if ( is_array($structure[0]) ) { - - /* yes, set its type */ - $body->type = self::TYPEMULTIPART; - - $index = 0; - $parts = []; - - /* for each body part */ - while( is_array($structure[$index]) ) - $parts[] = self::extractBodyStructure( $structure[$index++] ); - - /* parse subtype */ - if ( $body->subtype = strtoupper($structure[$index++]) ) - $body->ifsubtype = 1; - - /* multipart parameters */ - if ( $index < $length ) { - if ( count( $body->parameters = self::extractParameters($structure[$index++], []) ) ) - $body->ifparameters = 1; - else - $body->parameters = (object)[]; - } - - /* disposition */ - if ( $index < $length ) { - if ( is_array($disposition = $structure[$index++]) ) { - $body->disposition = $disposition[0]; - $body->ifdisposition = 1; - - if ( count( $body->dparameters = self::extractParameters($disposition[1], []) ) ) - $body->ifdparameters = 1; - else { - $body->dparameters = null; - } - } - } - - /* location */ - if ( $index < $length ) { - ++$index; - } - - while( $index < $length ) { - //parse_extension - ++$index; - } - - $body->parts = $parts; - - } - /* not multipart, parse type name */ - else { - - /* empty body? */ - if ( ! $length ) return (object)[]; - - /* assume unknown type */ - $body->type = self::TYPEOTHER; - /* and unknown encoding */ - $body->encoding = self::ENCOTHER; - - /* parse type */ - if ( ($type = array_search(strtoupper($structure[0]), self::$body_types)) !== false ) - $body->type = $type; - - /* encoding always gets uppercase form */ - if ( ($encoding = array_search(strtoupper($structure[5]), self::$body_encodings)) !== false ) - $body->encoding = $encoding; - - /* parse subtype */ - if ( $body->subtype = strtoupper($structure[1]) ) - $body->ifsubtype = 1; - - $body->ifdescription = 0; - if ( ! empty($structure[4]) ) { - $body->ifdescription = 1; - $body->description = $structure[4]; - } - - $body->ifid = 0; - if ( ! empty($structure[3]) ) { - $body->ifid = 1; - $body->id = $structure[3]; - } - - /* parse size of contents in bytes */ - $body->bytes = intval($structure[6]); - - $index = 7; - - /* possible extra stuff */ - switch ( $body->type ) { - - /* message envelope and body */ - case self::TYPEMESSAGE: - /* non MESSAGE/RFC822 is basic type */ - if ( strcmp($body->subtype, "RFC822") ) break; - - /* make certain server sends an envelope */ - ++$index; - - $body->parts[] = self::extractBodyStructure( $structure[$index++] ); - - /* drop into text case */ - - /* size in lines */ - case self::TYPETEXT: - $body->lines = intval($structure[$index++]); - break; - - /* otherwise nothing special */ - default: - break; - - } - - /* extension data - md5 */ - if ( $index < $length ) - ++$index; - - /* disposition */ - if ( $index < $length ) { - if ( is_array($disposition = $structure[$index++]) ) { - $body->disposition = $disposition[0]; - $body->ifdisposition = 1; - - if ( count( $body->dparameters = self::extractParameters($disposition[1], []) ) ) - $body->ifdparameters = 1; - else { - $body->dparameters = null; - } - } - } - - /* language */ - if ( $index < $length ) { - //parse_language - ++$index; - } - - /* location */ - if ( $index < $length ) { - ++$index; - } - - while( $index < $length ) { - //parse_extension - ++$index; - } - - if ( count( $body->parameters = self::extractParameters($structure[2], []) ) ) - $body->ifparameters = 1; - else - $body->parameters = (object)[]; - - } - - if ( is_null($body->description) ) unset($body->description); - if ( is_null($body->id) ) unset($body->id); - if ( is_null($body->disposition) ) unset($body->disposition); - if ( is_null($body->dparameters) ) unset($body->dparameters); - if ( is_null($body->parameters) ) unset($body->parameters); - - if ( ! $body->bytes ) unset($body->bytes); - if ( ! $body->lines ) unset($body->lines); - - return $body; - - } - - protected static function extractParameters($attributes, $parameters) - { - if (empty($attributes)) { - return []; - } - - $attribute = null; - - foreach ($attributes as $value) { - if (empty($attribute)) { - $attribute = [ - 'attribute' => $value, - 'value' => null, - ]; - } else { - $attribute['value'] = $value; - $parameters[] = (object) $attribute; - $attribute = null; - } - } - - return $parameters; - } - -} diff --git a/src/Connection.php b/src/Connection.php deleted file mode 100644 index 726063c..0000000 --- a/src/Connection.php +++ /dev/null @@ -1,292 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -use Javanile\Imap2\Roundcube\ImapClient; - -class Connection -{ - protected $mailbox; - protected $user; - protected $password; - protected $flags; - protected $retries; - protected $options; - protected $client; - protected $host; - protected $port; - protected $sslMode; - protected $currentMailbox; - protected $connected; - protected $registry; - - /** - * - */ - public function __construct($mailbox, $user, $password, $flags = 0, $retries = 0, $options = []) - { - $this->user = $user; - $this->password = $password; - $this->flags = $flags; - $this->retries = $retries; - $this->options = $options; - - $this->openMailbox($mailbox); - - $this->client = new ImapClient(); - } - - public function openMailbox($mailbox) - { - $this->mailbox = $mailbox; - - $mailboxParts = Functions::parseMailboxString($mailbox); - - $this->host = Functions::getHostFromMailbox($mailboxParts); - $this->port = @$mailboxParts['port']; - $this->sslMode = Functions::getSslModeFromMailbox($mailboxParts); - $this->currentMailbox = $mailboxParts['mailbox']; - } - - /** - * Open an IMAP stream to a mailbox. - * - * @param $mailbox - * @param $user - * @param $password - * @param int $flags - * @param int $retries - * @param array $options - * - * @return void - */ - public static function open($mailbox, $user, $password, $flags = 0, $retries = 0, $options = []) - { - $connection = new Connection($mailbox, $user, $password, $flags, $retries, $options); - - $success = $connection->connect(); - - if (empty($success)) { - Errors::appendErrorCanNotOpen($connection->getMailbox(), $connection->getLastError()); - - trigger_error(Errors::couldNotOpenStream($connection->getMailbox(), debug_backtrace(), 1), E_USER_WARNING); - - return false; - } - - return $connection; - } - - public static function reopen($imap, $mailbox, $flags = 0, $retries = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, null); - } - - $imap->openMailbox($mailbox); - - $success = $imap->connect(); - - if (empty($success)) { - trigger_error('imap2_reopen(): Couldn\'t re-open stream', E_USER_WARNING); - - return false; - } - - $imap->selectMailbox(); - - return true; - } - - public static function ping($imap) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, null); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - $status = $client->status($imap->getMailboxName(), ['UIDNEXT']); - - return isset($status['UIDNEXT']) && $status['UIDNEXT'] > 0; - } - - /** - * - */ - protected function connect() - { - $this->connected = false; - $client = $this->getClient(); - #$client->setDebug(true); - - $success = $client->connect($this->host, $this->user, $this->password, [ - 'port' => $this->port, - 'ssl_mode' => $this->sslMode, - 'auth_type' => $this->flags & OP_XOAUTH2 ? 'XOAUTH2' : 'IMAP', - 'timeout' => -1, - 'force_caps' => false, - ]); - - if (empty($success)) { - return false; - } - - if (empty($this->currentMailbox)) { - $mailboxes = $this->client->listMailboxes('', '*'); - if (false === $mailboxes) { - return false; - } - - if (in_array('INBOX', $mailboxes)) { - $this->currentMailbox = 'INBOX'; - $this->mailbox .= 'INBOX'; - } - } - - $this->rewriteMailbox(); - - $this->connected = true; - - return $this; - } - - /** - * - */ - protected function rewriteMailbox($forceMailbox = null) - { - $mailboxParts = Functions::parseMailboxString($this->mailbox); - - // '{imap.gmail.com:993/imap/notls/ssl/user="javanile.develop@gmail.com"}INBOX' - $params = []; - - $params[] = 'imap'; - if ($this->sslMode == 'ssl') { - $params[] = 'notls'; - $params[] = 'ssl'; - } - $params[] = 'user="'.$this->user.'"'; - - $mailboxName = $forceMailbox ? $forceMailbox : $mailboxParts['mailbox']; - - $updatedMailbox = '{'.$mailboxParts['host'].':'.$mailboxParts['port'].'/'.implode('/', $params).'}'.$mailboxName; - - $this->mailbox = $updatedMailbox; - } - - /** - * - */ - public function getClient() - { - return $this->client; - } - - /** - * - */ - public function getMailbox() - { - return $this->mailbox; - } - - /** - * - */ - public function getMailboxName() - { - return $this->currentMailbox; - } - - /** - * - */ - public function getHost() - { - return $this->host; - } - - /** - * - */ - public function selectMailbox() - { - $success = $this->client->select($this->currentMailbox); - - if (empty($success)) { - $this->rewriteMailbox(''); - } - } - - /** - * - */ - public static function close($imap, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - if ($client->close()) { - return true; - } - - $client->closeConnection(); - - return true; - } - - public function isConnected() - { - return boolval($this->connected); - } - - public static function isValid($imap) - { - return is_a($imap, Connection::class) && $imap->isConnected(); - } - - public function setRegistryValue($space, $item, $key, $value) - { - if (empty($this->registry)) { - $this->registry = []; - } - - if (empty($this->registry[$space])) { - $this->registry[$space] = []; - } - - if (empty($this->registry[$space][$item])) { - $this->registry[$space][$item] = []; - } - - $this->registry[$space][$item][$key] = $value; - } - - public function getRegistryValue($space, $item, $key) - { - if (isset($this->registry[$space][$item][$key])) { - return $this->registry[$space][$item][$key]; - } - - return false; - } - - public function getLastError() - { - $client = $this->getClient(); - - return $client->error; - } -} diff --git a/src/Errors.php b/src/Errors.php deleted file mode 100644 index 8c9187c..0000000 --- a/src/Errors.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Errors -{ - protected static $alerts = []; - - protected static $errors = []; - - protected static $lastError; - - public static function appendAlert($alert) - { - self::$alerts[] = $alert; - } - - public static function appendError($error) - { - self::$lastError = $error; - - self::$errors[] = $error; - } - - public static function alerts() - { - if (empty(self::$alerts)) { - return false; - } - - $return = self::$alerts; - - self::$alerts = []; - - return $return; - } - - public static function errors() - { - if (empty(self::$errors)) { - return false; - } - - $return = self::$errors; - - self::$errors = []; - - return $return; - } - - public static function lastError() - { - if (empty(self::$lastError)) { - return false; - } - - return self::$lastError; - } - - public static function raiseWarning($warning, $backtrace, $depth) - { - $message = $warning . ' in '.$backtrace[$depth]['file']. ' on line '.$backtrace[$depth]['line']; - - trigger_error($message, E_USER_WARNING); - } - - public static function invalidImapConnection($backtrace, $depth, $return) - { - $warning = 'Invalid IMAP connection parameter for '.$backtrace[$depth]['function'].'() ' - . 'at '.$backtrace[$depth]['file']. ' on line '.$backtrace[$depth]['line'].'. Source code'; - - trigger_error($warning, E_USER_WARNING); - - return $return; - } - - public static function couldNotOpenStream($mailbox, $backtrace, $depth) - { - if (isset($backtrace[$depth + 1]['function']) && $backtrace[$depth + 1]['function'] == 'imap_open') { - $depth++; - } - - return $backtrace[$depth]['function'].'(): Couldn\'t open stream '.$mailbox - . ' in '.$backtrace[$depth]['file']. ' on line '.$backtrace[$depth]['line'].'. Source code'; - } - - public static function badMessageNumber($backtrace, $depth) - { - if (Functions::isBackportCall($backtrace, $depth)) { - $depth++; - } - - return $backtrace[$depth]['function'].'(): Bad message number in ' - . $backtrace[$depth]['file']. ' on line '.$backtrace[$depth]['line'].'. Source code'; - } - - public static function appendErrorCanNotOpen($mailbox, $error) - { - $mailbox = (string) $mailbox; - - if ($mailbox[0] == '{') { - $error = preg_replace("/^AUTHENTICATE [A-Z]+\d*:\s/i", '', $error); - //$error = preg_replace("/^([A-Z]+\d+ )(OK|NO|BAD|BYE|PREAUTH)?\s/i", '', $error); - $error = 'Can not authenticate to IMAP server: '.$error; - } else { - $error = "Can't open mailbox {$mailbox}: no such mailbox"; - } - - self::appendError($error); - } -} diff --git a/src/Functions.php b/src/Functions.php deleted file mode 100644 index 292d9c6..0000000 --- a/src/Functions.php +++ /dev/null @@ -1,216 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Functions -{ - public static function parseMailboxString($mailbox) - { - $mailboxParts = explode('}', $mailbox); - $mailboxParts[0] = substr($mailboxParts[0], 1); - - $values = parse_url($mailboxParts[0]); - - $values['mailbox'] = $mailboxParts[1] ?? ''; - $values['path'] = explode('/', $values['path']); - - return $values; - } - - public static function getHostFromMailbox($mailbox) - { - $mailboxParts = is_array($mailbox) ? $mailbox : self::parseMailboxString($mailbox); - - return @$mailboxParts['host']; - } - - public static function getSslModeFromMailbox($mailbox) - { - $mailboxParts = is_array($mailbox) ? $mailbox : self::parseMailboxString($mailbox); - - if (in_array('ssl', $mailboxParts['path'])) { - return 'ssl'; - } - - return false; - } - - public static function expectedNumberOfMessages($sequence) - { - if (strpos($sequence, ',') > 0) { - return count(explode(',', $sequence)); - } elseif (strpos($sequence, ':') > 0) { - $range = explode(':', $sequence); - return (int) $range[1] - (int) $range[0]; - } else { - return 1; - } - } - - public static function unique() - { - return md5(microtime(). time() . rand(1000, 9999)); - } - - /** - * Get name from full mailbox string. - * - * @param $mailbox - * - * @return mixed|string - */ - public static function getMailboxName($mailbox) - { - $mailboxParts = explode('}', $mailbox, 2); - - return empty($mailboxParts[1]) ? 'INBOX' : $mailboxParts[1]; - } - - /** - * - * @param $address - * @param $defaultHost - * - * @return string - */ - public static function sanitizeAddress($address, $defaultHost = 'UNKNOWN') - { - $addressList = imap_rfc822_parse_adrlist($address, $defaultHost); - - $sanitizedAddress = []; - foreach ($addressList as $addressEntry) { - $sanitizedAddress[] = imap_rfc822_write_address($addressEntry->mailbox, $addressEntry->host, $addressEntry->personal); - } - - return implode(', ', $sanitizedAddress); - } - - /** - * - */ - public static function writeAddressFromEnvelope($addressList) - { - if (empty($addressList)) { - return null; - } - - $sanitizedAddress = []; - foreach ($addressList as $addressEntry) { - $parsedAddressEntry = imap_rfc822_write_address($addressEntry[2], $addressEntry[3], $addressEntry[0]); - if (substr($parsedAddressEntry, -3) == '@""') { - $parsedAddressEntry = substr($parsedAddressEntry, 0, strlen($parsedAddressEntry) - 3).': '; - } - $sanitizedAddress[] = $parsedAddressEntry; - } - - return implode(', ', $sanitizedAddress); - } - - /** - * - */ - public static function getListAttributesValue($attributes) - { - $attributesValue = 0; - - foreach ($attributes as $attribute) { - switch ($attribute) { - case '\\NoInferiors': - $attributesValue |= LATT_NOINFERIORS; - break; - case '\\NoSelect': - $attributesValue |= LATT_NOSELECT; - break; - case '\\Marked': - $attributesValue |= LATT_MARKED; - break; - case '\\UnMarked': - $attributesValue |= LATT_UNMARKED; - break; - case '\\Referral': - $attributesValue |= LATT_REFERRAL; - break; - case '\\HasChildren': - $attributesValue |= LATT_HASCHILDREN; - break; - case '\\HasNoChildren': - $attributesValue |= LATT_HASNOCHILDREN; - break; - } - } - - return $attributesValue; - } - - public static function isValidImap1Connection($imap) - { - return self::isRetrofitResource($imap); - } - - public static function isValidImap2Connection($imap) - { - return Connection::isValid($imap); - } - - public static function getAddressObjectList($addressList, $defaultHost = 'UNKNOWN') - { - $addressObjectList = []; - foreach ($addressList as $toAddress) { - $email = explode('@', $toAddress->getEmail()); - - $addressObject = (object) [ - 'mailbox' => $email[0], - 'host' => $email[1] ?? $defaultHost, - ]; - - $personal = $toAddress->getName(); - if ($personal) { - $addressObject->personal = $personal; - } - - $addressObjectList[] = $addressObject; - } - - return $addressObjectList; - } - - public static function isBackportCall($backtrace, $depth) - { - return isset($backtrace[$depth + 1]['function']) - && preg_match('/^imap_/', $backtrace[$depth + 1]['function']) - && preg_match('/^imap2_/', $backtrace[$depth]['function']) - && substr($backtrace[$depth + 1]['function'], 4) == substr($backtrace[$depth]['function'], 5); - } - - public static function isRetrofitResource($imap) - { - return (class_exists('\IMAP\Connection') && $imap instanceof \IMAP\Connection) || (is_resource($imap) && get_resource_type($imap) == 'imap'); - } - - public static function keyBy(string $name, array $list): array - { - $keyBy = []; - foreach ($list as $item) { - if (!isset($item->$name)) { - trigger_error('keyBy: key "' . $name . '" not found!', E_USER_WARNING); - continue; - } - if (isset($keyBy[$item->$name])) { - trigger_error('keyBy: duplicate key "' . $name . '" = "' . $item->$name . '"', E_USER_WARNING); - continue; - } - $keyBy[$item->$name] = $item; - } - - return $keyBy; - } -} diff --git a/src/HeaderInfo.php b/src/HeaderInfo.php deleted file mode 100644 index 7784cf4..0000000 --- a/src/HeaderInfo.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class HeaderInfo -{ - - public static function fromMessage($message, $defaultHost) - { - #file_put_contents('t3.json', json_encode($message, JSON_PRETTY_PRINT)); - - $to = Functions::writeAddressFromEnvelope($message->envelope[5]); - $cc = Functions::writeAddressFromEnvelope($message->envelope[6]); - $from = Functions::writeAddressFromEnvelope($message->envelope[2]); - $sender = Functions::writeAddressFromEnvelope($message->envelope[3]); - - if (empty($message->replyto)) { - $replyTo = $from; - } else { - $replyTo = Functions::writeAddressFromEnvelope($message->envelope[4]); - } - - $headerInfo = [ - 'date' => $message->envelope[0], - 'Date' => $message->envelope[0], - 'subject' => $message->envelope[1], - 'Subject' => $message->envelope[1], - 'in_reply_to' => $message->envelope[8], - 'message_id' => $message->envelope[9], - 'references' => $message->references, - 'toaddress' => $to, - 'to' => self::parseAddressList($to, $defaultHost), - 'fromaddress' => $from, - 'from' => self::parseAddressList($from, $defaultHost), - 'ccaddress' => $cc, - 'cc' => self::parseAddressList($cc, $defaultHost), - 'reply_toaddress' => $replyTo, - 'reply_to' => self::parseAddressList($replyTo, $defaultHost), - 'senderaddress' => $sender, - 'sender' => self::parseAddressList($sender, $defaultHost), - 'Recent' => ' ', - 'Unseen' => isset($message->flags['SEEN']) && $message->flags['SEEN'] ? ' ' : 'U', - 'Flagged' => empty($message->flags['FLAGGED']) ? ' ' : 'F', - 'Answered' => empty($message->flags['ANSWERED']) ? ' ' : 'A', - 'Deleted' => empty($message->flags['DELETED']) ? ' ' : 'X', - 'Draft' => empty($message->flags['DRAFT']) ? ' ' : 'D', - 'Msgno' => str_pad($message->id, 4, ' ', STR_PAD_LEFT), - 'MailDate' => self::sanitizeMailDate($message->internaldate), - 'Size' => strval($message->size), - 'udate' => strtotime($message->internaldate) - ]; - - if (empty($headerInfo['subject'])) { - unset($headerInfo['subject']); - unset($headerInfo['Subject']); - } - - if (empty($headerInfo['in_reply_to'])) { - unset($headerInfo['in_reply_to']); - } - - if (empty($headerInfo['references'])) { - unset($headerInfo['references']); - } - - if (empty($headerInfo['to'])) { - unset($headerInfo['toaddress']); - unset($headerInfo['to']); - } - - if (empty($headerInfo['cc'])) { - unset($headerInfo['ccaddress']); - unset($headerInfo['cc']); - } - - return (object) $headerInfo; - } - - protected static function parseAddressList($address, $defaultHost) - { - $addressList = imap_rfc822_parse_adrlist($address, $defaultHost); - $customAddressList = []; - - foreach ($addressList as $objectEntry) { - $addressEntry = (object) [ - 'personal' => $objectEntry->personal ?? null, - 'mailbox' => @$objectEntry->mailbox, - 'host' => @$objectEntry->host, - ]; - - if (empty($addressEntry->personal)) { - unset($addressEntry->personal); - } - - if (empty($addressEntry->host)) { - unset($addressEntry->host); - } - - if (empty($addressEntry->mailbox)) { - continue; - } - - $customAddressList[] = $addressEntry; - } - - return $customAddressList; - } - - public static function sanitizeMailDate($mailDate) - { - if ($mailDate[0] == '0') { - $mailDate = ' '.substr($mailDate, 1); - } - - return $mailDate; - } -} - diff --git a/src/IMAP/Connection.php b/src/IMAP/Connection.php new file mode 100644 index 0000000..b2e9f58 --- /dev/null +++ b/src/IMAP/Connection.php @@ -0,0 +1,254 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace IMAP; + +class Connection + { + protected \PHPFUI\Imap2\Roundcube\ImapClient $client; + + protected bool $connected; + + protected string $currentMailbox; + + protected string $host; + + protected int $port; + + protected array $registry; + + protected string $sslMode; + + public function __construct(protected $mailbox, protected string $user, protected string $password, protected int $flags = 0, protected int $retries = 0, protected array $options = []) + { + $this->openMailbox($mailbox); + + $this->client = new \PHPFUI\Imap2\Roundcube\ImapClient(); + } + + public static function close(\IMAP\Connection $imap, int $flags = 0) : true + { + $client = $imap->getClient(); + + if ($client->close()) + { + return true; + } + + $client->closeConnection(); + + return true; + } + + public function getClient() : \PHPFUI\Imap2\Roundcube\ImapClient + { + return $this->client; + } + + public function getHost() : string + { + return $this->host; + } + + public function getLastError() : string + { + $client = $this->getClient(); + + return $client->error; + } + + public function getMailbox() : string + { + return $this->mailbox; + } + + public function getMailboxName() : string + { + return $this->currentMailbox; + } + + public function getRegistryValue(string $space, string $item, $key) : string | false + { + if (isset($this->registry[$space][$item][$key])) + { + return $this->registry[$space][$item][$key]; + } + + return false; + } + + public function isConnected() : bool + { + return $this->connected; + } + + public static function isValid(\IMAP\Connection $imap) : bool + { + return $imap->isConnected(); + } + + /** + * Open an IMAP stream to a mailbox. + */ + public static function open(string $mailbox, string $user, string $password, int $flags = 0, int $retries = 0, array $options = []) : Connection | false + { + if (! ($flags & 4095) && ! ($flags & CL_EXPUNGE)) + { + throw new \ValueError('imap_open(): Argument #4 ($flags) must be a bitmask of the OP_* constants, and CL_EXPUNGE'); + } + + $connection = new Connection($mailbox, $user, $password, $flags, $retries, $options); + + $success = $connection->connect(); + + if (empty($success)) + { + \PHPFUI\Imap2\Errors::appendErrorCanNotOpen($connection->getMailbox(), $connection->getLastError()); + + \trigger_error(\PHPFUI\Imap2\Errors::couldNotOpenStream($connection->getMailbox()), E_USER_WARNING); + + return false; + } + + return $connection; + } + + public function openMailbox(string $mailbox) : void + { + $this->mailbox = $mailbox; + + $mailboxParts = \PHPFUI\Imap2\Functions::parseMailboxString($mailbox); + + $this->host = \PHPFUI\Imap2\Functions::getHostFromMailbox($mailboxParts); + $this->port = $mailboxParts['port'] ?? 0; + $this->sslMode = \PHPFUI\Imap2\Functions::getSslModeFromMailbox($mailboxParts); + $this->currentMailbox = $mailboxParts['mailbox']; + } + + public static function ping(\IMAP\Connection $imap) : bool + { + $client = $imap->getClient(); + $status = $client->status($imap->getMailboxName(), ['UIDNEXT']); + + return isset($status['UIDNEXT']) && $status['UIDNEXT'] > 0; + } + + public static function reopen(\IMAP\Connection $imap, string $mailbox, int $flags = 0, int $retries = 0) : bool + { + $imap->openMailbox($mailbox); + + $success = $imap->connect(); + + if (empty($success)) + { + \trigger_error('imap_reopen(): Couldn\'t re-open stream', E_USER_WARNING); + + return false; + } + + $imap->selectMailbox(); + + return true; + } + + public function selectMailbox() : void + { + $success = $this->client->select($this->currentMailbox); + + if (empty($success)) + { + $this->rewriteMailbox(''); + } + } + + public function setRegistryValue($space, $item, $key, $value) : void + { + if (empty($this->registry)) + { + $this->registry = []; + } + + if (empty($this->registry[$space])) + { + $this->registry[$space] = []; + } + + if (empty($this->registry[$space][$item])) + { + $this->registry[$space][$item] = []; + } + + $this->registry[$space][$item][$key] = $value; + } + + protected function connect() : bool|static + { + $this->connected = false; + $client = $this->getClient(); + + $success = $client->connect($this->host, $this->user, $this->password, [ + 'port' => $this->port, + 'ssl_mode' => $this->sslMode, + 'auth_type' => $this->flags & OP_XOAUTH2 ? 'XOAUTH2' : 'IMAP', + 'timeout' => -1, + 'force_caps' => false, + ]); + + if (empty($success)) + { + return false; + } + + if (empty($this->currentMailbox)) + { + $mailboxes = $this->client->listMailboxes('', '*'); + + if (false === $mailboxes) + { + return false; + } + + if (\in_array('INBOX', $mailboxes)) + { + $this->currentMailbox = 'INBOX'; + $this->mailbox .= 'INBOX'; + } + } + + $this->rewriteMailbox(); + + $this->connected = true; + + return $this; + } + + protected function rewriteMailbox(?string $forceMailbox = null) : void + { + $mailboxParts = \PHPFUI\Imap2\Functions::parseMailboxString($this->mailbox); + + $params = []; + + $params[] = 'imap'; + + if ('ssl' == $this->sslMode) + { + $params[] = 'notls'; + $params[] = 'ssl'; + } + $params[] = 'user="' . $this->user . '"'; + + $mailboxName = $forceMailbox ?: $mailboxParts['mailbox']; + + $updatedMailbox = '{' . $mailboxParts['host'] . ':' . $mailboxParts['port'] . '/' . \implode('/', $params) . '}' . $mailboxName; + + $this->mailbox = $updatedMailbox; + } + } diff --git a/src/Imap2/BodyStructure.php b/src/Imap2/BodyStructure.php new file mode 100644 index 0000000..052c1aa --- /dev/null +++ b/src/Imap2/BodyStructure.php @@ -0,0 +1,345 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class BodyStructure + { + public const int ENC7BIT = 0; // 7 bit SMTP semantic data + + public const int ENC8BIT = 1; // 8 bit SMTP semantic data + + public const int ENCBASE64 = 3; // base-64 encoded data + + public const int ENCBINARY = 2; // 8 bit binary data + + public const int ENCOTHER = 5; // unknown + + public const int ENCQUOTEDPRINTABLE = 4; // human-readable 8-as-7 bit data + + public const int TYPEAPPLICATION = 3; // application data + + public const int TYPEAUDIO = 4; // audio + + public const int TYPEIMAGE = 5; // static image + + public const int TYPEMESSAGE = 2; // encapsulated message + + public const int TYPEMODEL = 7; // model + + public const int TYPEMULTIPART = 1; // multiple part + + public const int TYPEOTHER = 8; // unknown + + public const int TYPETEXT = 0; // unformatted text + + public const int TYPEVIDEO = 6; // video + + protected static array $body_encodings = [ + self::ENC7BIT => '7BIT', + self::ENC8BIT => '8BIT', + self::ENCBINARY => 'BINARY', + self::ENCBASE64 => 'BASE64', + self::ENCQUOTEDPRINTABLE => 'QUOTED-PRINTABLE', + self::ENCOTHER => 'X-UNKNOWN' + ]; + + protected static array $body_types = [ + self::TYPETEXT => 'TEXT', + self::TYPEMULTIPART => 'MULTIPART', + self::TYPEMESSAGE => 'MESSAGE', + self::TYPEAPPLICATION => 'APPLICATION', + self::TYPEAUDIO => 'AUDIO', + self::TYPEIMAGE => 'IMAGE', + self::TYPEVIDEO => 'VIDEO', + self::TYPEMODEL => 'MODEL', + self::TYPEOTHER => 'X-UNKNOWN' + ]; + + public static function fromMessage($message) : \stdClass | false + { + return self::extractBodyStructure($message->bodystructure); + } + + protected static function extractBodyStructure($structure) : \stdClass | false + { + // if NIL + if ( null === $structure ) + { + return false; + } + + // body structure list + $length = \count($structure); + + if ( ! $length) + { + return false; + } + + $body = (object)[ + 'type' => self::TYPEOTHER, + 'encoding' => self::ENC7BIT, + 'ifsubtype' => 0, + 'subtype' => null, + 'ifdescription' => 0, + 'description' => null, + 'ifid' => 0, + 'id' => null, + 'lines' => null, + 'bytes' => null, + 'ifdisposition' => 0, + 'disposition' => null, + 'ifdparameters' => 0, + 'dparameters' => null, + 'ifparameters' => 0, + 'parameters' => null + ]; + + // multipart body? + if ( \is_array($structure[0]) ) + { + // yes, set its type + $body->type = self::TYPEMULTIPART; + + $index = 0; + $parts = []; + + // for each body part + while( \is_array($structure[$index]) ) + { + $parts[] = self::extractBodyStructure( $structure[$index++] ); + } + + // parse subtype + if ( $body->subtype = \strtoupper($structure[$index++]) ) + { + $body->ifsubtype = 1; + } + + // multipart parameters + if ( $index < $length ) + { + if ( \count( $body->parameters = self::extractParameters($structure[$index++], []) ) ) + { + $body->ifparameters = 1; + } + else + { + $body->parameters = (object)[]; + } + } + + // disposition + if ( $index < $length ) + { + if ( \is_array($disposition = $structure[$index++]) ) + { + $body->disposition = $disposition[0]; + $body->ifdisposition = 1; + + if ( \count( $body->dparameters = self::extractParameters($disposition[1], []) )) + { + $body->ifdparameters = 1; + } + else + { + $body->dparameters = null; + } + } + } + + // location + if ( $index < $length ) + { + ++$index; + } + + while( $index < $length ) + { + //parse_extension + ++$index; + } + + $body->parts = $parts; + + } + // not multipart, parse type name + else + { + // assume unknown type + $body->type = self::TYPEOTHER; + // and unknown encoding + $body->encoding = self::ENCOTHER; + + // parse type + if ( ($type = \array_search(\strtoupper($structure[0]), self::$body_types)) !== false ) + { + $body->type = $type; + } + + // encoding always gets uppercase form + if ( ($encoding = \array_search(\strtoupper($structure[5]), self::$body_encodings)) !== false ) + { + $body->encoding = $encoding; + } + + // parse subtype + if ( $body->subtype = \strtoupper($structure[1]) ) + { + $body->ifsubtype = 1; + } + + $body->ifdescription = 0; + + if ( ! empty($structure[4]) ) + { + $body->ifdescription = 1; + $body->description = $structure[4]; + } + + $body->ifid = 0; + + if ( ! empty($structure[3]) ) + { + $body->ifid = 1; + $body->id = $structure[3]; + } + + // parse size of contents in bytes + $body->bytes = (int)($structure[6]); + + $index = 7; + + // possible extra stuff + switch ( $body->type ) + { + // message envelope and body + case self::TYPEMESSAGE: + // non MESSAGE/RFC822 is basic type + if ( \strcmp($body->subtype, 'RFC822') ) break; + + // make certain server sends an envelope + ++$index; + + $body->parts[] = self::extractBodyStructure( $structure[$index++] ); + + // drop into text case + + // size in lines + // Intentionally fall through + case self::TYPETEXT: + $body->lines = (int)($structure[$index++]); + + break; + + // otherwise nothing special + default: + break; + + } + + // extension data - md5 + if ( $index < $length ) + { + ++$index; + } + + // disposition + if ( $index < $length ) + { + if ( \is_array($disposition = $structure[$index++]) ) + { + $body->disposition = $disposition[0]; + $body->ifdisposition = 1; + + if ( \count( $body->dparameters = self::extractParameters($disposition[1], []) ) ) + { + $body->ifdparameters = 1; + } + else + { + $body->dparameters = null; + } + } + } + + // language + if ( $index < $length ) + { + //parse_language + ++$index; + } + + // location + if ( $index < $length ) + { + ++$index; + } + + while( $index < $length ) + { + //parse_extension + ++$index; + } + + if ( \count( $body->parameters = self::extractParameters($structure[2], []) ) ) + { + $body->ifparameters = 1; + } + else + { + $body->parameters = (object)[]; + } + } + + if ( null === $body->description ) $body->description = null; + + if ( null === $body->id ) $body->id = null; + + if ( null === $body->disposition ) $body->disposition = null; + + if ( null === $body->dparameters ) $body->dparameters = null; + + if ( null === $body->parameters ) $body->parameters = null; + + return $body; + } + + protected static function extractParameters(array $attributes, array $parameters) : array + { + if (empty($attributes)) + { + return []; + } + + $attribute = null; + + foreach ($attributes as $value) + { + if (empty($attribute)) + { + $attribute = [ + 'attribute' => $value, + 'value' => null, + ]; + } + else + { + $attribute['value'] = $value; + $parameters[] = (object)$attribute; + $attribute = null; + } + } + + return $parameters; + } + } diff --git a/src/Imap2/Errors.php b/src/Imap2/Errors.php new file mode 100644 index 0000000..6a9883f --- /dev/null +++ b/src/Imap2/Errors.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Errors + { + protected static array $alerts = []; + + protected static array | false $errors = []; + + protected static string | bool $lastError = false; + + public static function alerts() : array + { + $return = self::$alerts; + + self::$alerts = []; + + return $return; + } + + public static function appendError(string $error) : string + { + self::$lastError = $error; + + if (! self::$errors) + { + self::$errors = []; + } + self::$errors[] = $error; + + return $error; + } + + public static function appendErrorCanNotOpen(string $mailbox, string $error) : string + { + if (\strlen($mailbox) && '{' == $mailbox[0]) + { + $error = \preg_replace("/^AUTHENTICATE [A-Z]+\d*:\s/i", '', $error); + $error = 'Can not authenticate to IMAP server: ' . $error; + } + else + { + $error = "Can't open mailbox {$mailbox}: no such mailbox"; + } + + return self::appendError($error); + } + + public static function badMessageNumber() : string + { + $backtrace = \debug_backtrace(); + $error = $backtrace[1]['function'] . '(): Bad message number in ' + . $backtrace[1]['file'] . ' on line ' . $backtrace[1]['line']; + + return self::appendError($error); + } + + public static function couldNotOpenStream(string $mailbox) : string + { + $backtrace = \debug_backtrace(); + $depth = 1; + + if (isset($backtrace[$depth + 1]['function']) && 'imap_open' == $backtrace[$depth + 1]['function']) + { + $depth++; + } + + return $backtrace[$depth]['function'] . '(): Couldn\'t open stream ' . $mailbox + . ' in ' . $backtrace[$depth]['file'] . ' on line ' . $backtrace[$depth]['line']; + } + + public static function errors() : array | false + { + $return = self::$errors; + + self::$errors = false; + + return $return; + } + + public static function lastError() : string | false + { + return self::$lastError; + } + } diff --git a/src/Imap2/Functions.php b/src/Imap2/Functions.php new file mode 100644 index 0000000..1123430 --- /dev/null +++ b/src/Imap2/Functions.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Functions + { + public static function expectedNumberOfMessages(string $sequence) : int + { + if (\strpos($sequence, ',') > 0) + { + return \count(\explode(',', $sequence)); + } + elseif (\strpos($sequence, ':') > 0) + { + $range = \explode(':', $sequence); + + return (int)$range[1] - (int)$range[0]; + } + + return 1; + } + + public static function getAddressObjectList(array $addressList, string $defaultHost = 'UNKNOWN') : array + { + $addressObjectList = []; + + foreach ($addressList as $toAddress) + { + $email = \explode('@', $toAddress->getEmail()); + + $addressObject = (object)[ + 'mailbox' => $email[0], + 'host' => $email[1] ?? $defaultHost, + ]; + + $personal = $toAddress->getName(); + + if ($personal) + { + $addressObject->personal = $personal; + } + + $addressObjectList[] = $addressObject; + } + + return $addressObjectList; + } + + public static function getHostFromMailbox(string|array $mailbox) : string + { + $mailboxParts = \is_array($mailbox) ? $mailbox : self::parseMailboxString($mailbox); + + return $mailboxParts['host'] ?? ''; + } + + public static function getListAttributesValue(array $attributes) : int + { + $attributesValue = 0; + + foreach ($attributes as $attribute) + { + switch ($attribute) { + case '\\NoInferiors': + $attributesValue |= LATT_NOINFERIORS; + + break; + + case '\\NoSelect': + $attributesValue |= LATT_NOSELECT; + + break; + + case '\\Marked': + $attributesValue |= LATT_MARKED; + + break; + + case '\\UnMarked': + $attributesValue |= LATT_UNMARKED; + + break; + + case '\\Referral': + $attributesValue |= LATT_REFERRAL; + + break; + + case '\\HasChildren': + $attributesValue |= LATT_HASCHILDREN; + + break; + + case '\\HasNoChildren': + $attributesValue |= LATT_HASNOCHILDREN; + + break; + } + } + + return $attributesValue; + } + + /** + * Get name from full mailbox string. + */ + public static function getMailboxName(string $mailbox) : string + { + $mailboxParts = \explode('}', $mailbox, 2); + + return empty($mailboxParts[1]) ? 'INBOX' : $mailboxParts[1]; + } + + public static function getSslModeFromMailbox(array|string $mailbox) : string | bool + { + $mailboxParts = \is_array($mailbox) ? $mailbox : self::parseMailboxString($mailbox); + + if (\in_array('ssl', $mailboxParts['path'])) + { + return 'ssl'; + } + + return false; + } + + public static function keyBy(string $name, array $list) : array + { + $keyBy = []; + + foreach ($list as $item) + { + if (! isset($item->{$name})) + { + \trigger_error('keyBy: key "' . $name . '" not found!', E_USER_WARNING); + + continue; + } + + if (isset($keyBy[$item->{$name}])) + { + \trigger_error('keyBy: duplicate key "' . $name . '" = "' . $item->{$name} . '"', E_USER_WARNING); + + continue; + } + $keyBy[$item->{$name}] = $item; + } + + return $keyBy; + } + + public static function parseMailboxString(string $mailbox) : array + { + $mailboxParts = \explode('}', $mailbox); + $mailboxParts[0] = \substr($mailboxParts[0], 1); + + $values = \parse_url($mailboxParts[0]); + + $values['mailbox'] = $mailboxParts[1] ?? ''; + $values['path'] = \explode('/', $values['path']); + + return $values; + } + + public static function sanitizeAddress(string $address, string $defaultHost = 'UNKNOWN') : string + { + $addressList = \imap_rfc822_parse_adrlist($address, $defaultHost); + + $sanitizedAddress = []; + + foreach ($addressList as $addressEntry) + { + $sanitizedAddress[] = \imap_rfc822_write_address($addressEntry->mailbox, $addressEntry->host, $addressEntry->personal); + } + + return \implode(', ', $sanitizedAddress); + } + + public static function writeAddressFromEnvelope(array $addressList) : string + { + if (empty($addressList)) + { + return ''; + } + + $sanitizedAddress = []; + + foreach ($addressList as $addressEntry) + { + $parsedAddressEntry = \imap_rfc822_write_address($addressEntry[2], $addressEntry[3], $addressEntry[0]); + + if ('@""' == \substr($parsedAddressEntry, -3)) + { + $parsedAddressEntry = \substr($parsedAddressEntry, 0, \strlen($parsedAddressEntry) - 3) . ': '; + } + $sanitizedAddress[] = $parsedAddressEntry; + } + + return \implode(', ', $sanitizedAddress); + } + } diff --git a/src/Imap2/HeaderInfo.php b/src/Imap2/HeaderInfo.php new file mode 100644 index 0000000..3836227 --- /dev/null +++ b/src/Imap2/HeaderInfo.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class HeaderInfo + { + public static function fromMessage($message, $defaultHost) : \stdClass + { + $to = Functions::writeAddressFromEnvelope($message->envelope[5]); + $cc = Functions::writeAddressFromEnvelope($message->envelope[6]); + $from = Functions::writeAddressFromEnvelope($message->envelope[2]); + $sender = Functions::writeAddressFromEnvelope($message->envelope[3]); + + if (empty($message->replyto)) + { + $replyTo = $from; + } + else + { + $replyTo = Functions::writeAddressFromEnvelope($message->envelope[4]); + } + + $headerInfo = [ + 'date' => $message->envelope[0], + 'Date' => $message->envelope[0], + 'subject' => $message->envelope[1], + 'Subject' => $message->envelope[1], + 'in_reply_to' => $message->envelope[8], + 'message_id' => $message->envelope[9], + 'references' => $message->references, + 'toaddress' => $to, + 'to' => self::parseAddressList($to, $defaultHost), + 'fromaddress' => $from, + 'from' => self::parseAddressList($from, $defaultHost), + 'ccaddress' => $cc, + 'cc' => self::parseAddressList($cc, $defaultHost), + 'reply_toaddress' => $replyTo, + 'reply_to' => self::parseAddressList($replyTo, $defaultHost), + 'senderaddress' => $sender, + 'sender' => self::parseAddressList($sender, $defaultHost), + 'Recent' => ' ', + 'Unseen' => isset($message->flags['SEEN']) && $message->flags['SEEN'] ? ' ' : 'U', + 'Flagged' => empty($message->flags['FLAGGED']) ? ' ' : 'F', + 'Answered' => empty($message->flags['ANSWERED']) ? ' ' : 'A', + 'Deleted' => empty($message->flags['DELETED']) ? ' ' : 'X', + 'Draft' => empty($message->flags['DRAFT']) ? ' ' : 'D', + 'Msgno' => \str_pad($message->id, 4, ' ', STR_PAD_LEFT), + 'MailDate' => self::sanitizeMailDate($message->internaldate), + 'Size' => (string)($message->size), + 'udate' => \strtotime($message->internaldate) + ]; + + if (empty($headerInfo['subject'])) + { + unset($headerInfo['subject'], $headerInfo['Subject']); + } + + if (empty($headerInfo['in_reply_to'])) + { + unset($headerInfo['in_reply_to']); + } + + if (empty($headerInfo['references'])) + { + unset($headerInfo['references']); + } + + if (empty($headerInfo['to'])) + { + unset($headerInfo['toaddress'], $headerInfo['to']); + } + + if (empty($headerInfo['cc'])) + { + unset($headerInfo['ccaddress'], $headerInfo['cc']); + } + + return (object)$headerInfo; + } + + public static function sanitizeMailDate(string $mailDate) : string + { + if ('0' == $mailDate[0]) + { + $mailDate = ' ' . \substr($mailDate, 1); + } + + return $mailDate; + } + + /** + * @return array<\stdClass> + */ + protected static function parseAddressList(string $address, string $defaultHost) : array + { + $addressList = \imap_rfc822_parse_adrlist($address, $defaultHost); + $customAddressList = []; + + foreach ($addressList as $objectEntry) + { + $addressEntry = (object)[ + 'personal' => $objectEntry->personal ?? null, + 'mailbox' => @$objectEntry->mailbox, + 'host' => @$objectEntry->host, + ]; + + if (empty($addressEntry->personal)) + { + $addressEntry->personal = null; + } + + if (empty($addressEntry->host)) + { + $addressEntry->host = null; + } + + if (empty($addressEntry->mailbox)) + { + continue; + } + + $customAddressList[] = $addressEntry; + } + + return $customAddressList; + } + } diff --git a/src/Imap2/IMAPStubs.php b/src/Imap2/IMAPStubs.php new file mode 100644 index 0000000..e89421c --- /dev/null +++ b/src/Imap2/IMAPStubs.php @@ -0,0 +1,942 @@ +isConnected()) + { + return false; + } + + $client = $imap->getClient(); + + return $client->getQuota(); + } + } + +if (! \function_exists('imap_get_quotaroot')) + { + function imap_get_quotaroot(\IMAP\Connection $imap, string $mailbox) : array | false + { + if (! $imap->isConnected()) + { + return false; + } + + $client = $imap->getClient(); + + return $client->getQuota($mailbox); + } + } + +if (! \function_exists('imap_getacl')) + { + function imap_getacl(\IMAP\Connection $imap, string $mailbox) : array | false + { + if (! $imap->isConnected()) + { + return false; + } + + $client = $imap->getClient(); + + return $client->getACL($mailbox); + } + } + +if (! \function_exists('imap_getmailboxes')) + { + function imap_getmailboxes(\IMAP\Connection $imap, string $reference, string $pattern) : array|false + { + return \PHPFUI\Imap2\Mailbox::getMailboxes($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_getsubscribed')) + { + function imap_getsubscribed(\IMAP\Connection $imap, string $reference, string $pattern) : array|false + { + return \PHPFUI\Imap2\Mailbox::getMailboxes($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_header')) + { + function imap_header(\IMAP\Connection $imap, int $message_num, int $from_length = 0, int $subject_length = 0) : \stdClass|false + { + return \PHPFUI\Imap2\Message::headerInfo($imap, $message_num, $from_length, $subject_length); + } + } + +if (! \function_exists('imap_headerinfo')) + { + function imap_headerinfo(\IMAP\Connection $imap, int $message_num, int $from_length = 0, int $subject_length = 0) : \stdClass|false + { + return \PHPFUI\Imap2\Message::headerInfo($imap, $message_num, $from_length, $subject_length); + } + } + +if (! \function_exists('imap_headers')) + { + function imap_headers(\IMAP\Connection $imap) : array|false + { + return \PHPFUI\Imap2\Message::headers($imap); + } + } + +if (! \function_exists('imap_last_error')) + { + function imap_last_error() : string | false + { + return \PHPFUI\Imap2\Errors::lastError(); + } + } + +if (! \function_exists('imap_list')) + { + function imap_list(\IMAP\Connection $imap, string $reference, string $pattern) : array|false + { + return \PHPFUI\Imap2\Mailbox::list($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_listmailbox')) + { + function imap_listmailbox(\IMAP\Connection $imap, string $reference, string $pattern) : array|false + { + return \PHPFUI\Imap2\Mailbox::list($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_listscan')) + { + function imap_listscan(\IMAP\Connection $imap, string $reference, string $pattern, string $content) : array|false + { + return \PHPFUI\Imap2\Mailbox::listScan($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_listsubscribed')) + { + function imap_listsubscribed(\IMAP\Connection $imap, string $reference, string $pattern) : array|false + { + return \PHPFUI\Imap2\Mailbox::listSubscribed($imap, $reference); + } + } + +if (! \function_exists('imap_lsub')) + { + function imap_lsub(\IMAP\Connection $imap, string $reference, string $pattern) : array|false + { + return \PHPFUI\Imap2\Mailbox::listSubscribed($imap, $reference); + } + } + +//if (! \function_exists('imap_mail')) +// { +// function imap_mail(string $to, string $subject, string $message, ?string $additional_headers = null, ?string $cc = null, ?string $bcc = null, ?string $return_path = null) : bool +// { +// throw new \Exception('imap_mail is not implemented'); +// } +// } +// +//if (! \function_exists('imap_mail_compose')) +// { +// function imap_mail_compose(array $envelope, array $bodies) : string|false +// { +// throw new \Exception('imap_mail_compose is not implemented'); +// } +// } + +if (! \function_exists('imap_mail_copy')) + { + function imap_mail_copy(\IMAP\Connection $imap, string $message_nums, string $mailbox, int $flags = 0) : bool + { + return \PHPFUI\Imap2\Mail::copy($imap, $message_nums, $mailbox, $flags); + } + } + +if (! \function_exists('imap_mail_move')) + { + function imap_mail_move(\IMAP\Connection $imap, string $message_nums, string $mailbox, int $flags = 0) : bool + { + return \PHPFUI\Imap2\Mail::move($imap, $message_nums, $mailbox, $flags); + } + } + +if (! \function_exists('imap_mailboxmsginfo')) + { + function imap_mailboxmsginfo(\IMAP\Connection $imap) : stdclass + { + return \PHPFUI\Imap2\Mailbox::mailboxMsgInfo($imap); + } + } + +//if (! \function_exists('imap_mime_header_decode')) +// { +// function imap_mime_header_decode(string $string) : array +// { +// throw new \Exception(__FUNCTION__ . ' is not implemented.'); +// } +// } + +if (! \function_exists('imap_msgno')) + { + function imap_msgno(\IMAP\Connection $imap, int $message_uid) : int + { + return \PHPFUI\Imap2\Message::msgNo($imap, $message_uid); + } + } + +if (! \function_exists('imap_mutf7_to_utf8')) + { + function imap_mutf7_to_utf8(string $string) : string + { + return $string; + } + } + +if (! \function_exists('imap_num_msg')) + { + function imap_num_msg(\IMAP\Connection $imap) : int|false + { + return \PHPFUI\Imap2\Mailbox::numMsg($imap); + } + } + +if (! \function_exists('imap_num_recent')) + { + function imap_num_recent(\IMAP\Connection $imap) : int + { + return \PHPFUI\Imap2\Mailbox::numRecent($imap); + } + } + +if (! \function_exists('imap_open')) + { + function imap_open(string $mailbox, string $user, string $password, int $flags = 0, int $retries = 0, array $options = []) : \IMAP\Connection|false + { + return \IMAP\Connection::open($mailbox, $user, $password, $flags, $retries, $options); + } + } + +if (! \function_exists('imap_ping')) + { + function imap_ping(\IMAP\Connection $imap) : bool + { + return \IMAP\Connection::ping($imap); + } + } + +if (! \function_exists('imap_qprint')) + { + function imap_qprint(string $string) : string + { + return \quoted_printable_decode($string); + } + } + +if (! \function_exists('imap_rename')) + { + function imap_rename(\IMAP\Connection $imap, string $from, string $to) : bool + { + return \PHPFUI\Imap2\Mailbox::renameMailbox($imap, $from, $to); + } + } + +if (! \function_exists('imap_renamemailbox')) + { + function imap_renamemailbox(\IMAP\Connection $imap, string $from, string $to) : bool + { + return \PHPFUI\Imap2\Mailbox::renameMailbox($imap, $from, $to); + } + } + +if (! \function_exists('imap_reopen')) + { + function imap_reopen(\IMAP\Connection $imap, string $mailbox, int $flags = 0, int $retries = 0) : bool + { + return \IMAP\Connection::reopen($imap, $mailbox, $flags, $retries); + } + } + +if (! \function_exists('imap_rfc822_parse_adrlist')) + { + function imap_rfc822_parse_adrlist(string $string, string $default_hostname) : array + { + $message = \ZBateson\MailMimeParser\Message::from('To: ' . $string, false); + + return \PHPFUI\Imap2\Functions::getAddressObjectList( + $message->getHeader(\ZBateson\MailMimeParser\Header\HeaderConsts::TO)->getAddresses(), // @phpstan-ignore-line + $default_hostname + ); + } + } + +if (! \function_exists('imap_rfc822_parse_headers')) + { + function imap_rfc822_parse_headers(string $headers, string $default_hostname = 'UNKNOWN') : \stdClass + { + $message = \ZBateson\MailMimeParser\Message::from($headers, false); + + $date = $message->getHeaderValue(\ZBateson\MailMimeParser\Header\HeaderConsts::DATE); + $subject = $message->getHeaderValue(\ZBateson\MailMimeParser\Header\HeaderConsts::SUBJECT); + + $hasReplyTo = null !== $message->getHeader(\ZBateson\MailMimeParser\Header\HeaderConsts::REPLY_TO); + $hasSender = null !== $message->getHeader(\ZBateson\MailMimeParser\Header\HeaderConsts::SENDER); + + return (object)[ + 'date' => $date, + 'Date' => $date, + 'subject' => $subject, + 'Subject' => $subject, + 'message_id' => '<' . $message->getHeaderValue(\ZBateson\MailMimeParser\Header\HeaderConsts::MESSAGE_ID) . '>', + 'toaddress' => $message->getHeaderValue(\ZBateson\MailMimeParser\Header\HeaderConsts::TO), + 'to' => \PHPFUI\Imap2\Functions::getAddressObjectList($message->getHeader(\ZBateson\MailMimeParser\Header\HeaderConsts::TO)->getAddresses()), // @phpstan-ignore-line + 'fromaddress' => $message->getHeaderValue(\ZBateson\MailMimeParser\Header\HeaderConsts::FROM), + 'from' => \PHPFUI\Imap2\Functions::getAddressObjectList($message->getHeader(\ZBateson\MailMimeParser\Header\HeaderConsts::FROM)->getAddresses()), // @phpstan-ignore-line + 'reply_toaddress' => $message->getHeaderValue($hasReplyTo ? \ZBateson\MailMimeParser\Header\HeaderConsts::REPLY_TO : \ZBateson\MailMimeParser\Header\HeaderConsts::FROM), + 'reply_to' => \PHPFUI\Imap2\Functions::getAddressObjectList($message->getHeader($hasReplyTo ? \ZBateson\MailMimeParser\Header\HeaderConsts::REPLY_TO : \ZBateson\MailMimeParser\Header\HeaderConsts::FROM)->getAddresses()), // @phpstan-ignore-line + 'senderaddress' => $message->getHeaderValue($hasSender ? \ZBateson\MailMimeParser\Header\HeaderConsts::SENDER : \ZBateson\MailMimeParser\Header\HeaderConsts::FROM), + 'sender' => \PHPFUI\Imap2\Functions::getAddressObjectList($message->getHeader($hasSender ? \ZBateson\MailMimeParser\Header\HeaderConsts::SENDER : \ZBateson\MailMimeParser\Header\HeaderConsts::FROM)->getAddresses()), // @phpstan-ignore-line + ]; + } + } + +if (! \function_exists('imap_rfc822_write_address')) + { + function imap_rfc822_write_address(string $mailbox, string $hostname, string $personal) : string + { + $ret = $mailbox; + + if (! \filter_var($mailbox, FILTER_VALIDATE_EMAIL) && ! empty($hostname)) + { + $ret .= '@' . $hostname; + } + + if (! empty($personal)) + { + $ret = \mb_encode_mimeheader($personal, 'UTF-8') . ' <' . $ret . '>'; + } + + return $ret; + } + } + +if (! \function_exists('imap_savebody')) + { + function imap_savebody(\IMAP\Connection $imap, mixed $file, int $message_num, string $section = '', int $flags = 0) : bool + { + return \PHPFUI\Imap2\Message::saveBody($imap, $file, $message_num, $section, $flags); + } + } + +if (! \function_exists('imap_scan')) + { + function imap_scan(\IMAP\Connection $imap, string $reference, string $pattern, string $content) : array|false + { + return \PHPFUI\Imap2\Mailbox::listScan($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_scanmailbox')) + { + function imap_scanmailbox(\IMAP\Connection $imap, string $reference, string $pattern, string $content) : array|false + { + return \PHPFUI\Imap2\Mailbox::listScan($imap, $reference, $pattern); + } + } + +if (! \function_exists('imap_search')) + { + function imap_search(\IMAP\Connection $imap, string $criteria, int $flags = SE_FREE, string $charset = '') : array|false + { + return \PHPFUI\Imap2\Message::search($imap, $criteria, $flags, $charset); + } + } + +//if (! \function_exists('imap_set_quota')) +// { +// function imap_set_quota(\IMAP\Connection $imap, string $quota_root) : array|false +// { +// throw new \Exception(__FUNCTION__ . ' is not implemented.'); +// } +// } + +if (! \function_exists('imap_setacl')) + { + function imap_setacl(\IMAP\Connection $imap, string $mailbox, string $userId, string $rights) : void + { + $client = $imap->getClient(); + $client->setACL($mailbox, $userId, $rights); + } + } + +if (! \function_exists('imap_setflag_full')) + { + function imap_setflag_full(\IMAP\Connection $imap, string $sequence, string $flag, int $options = 0) : bool + { + return \PHPFUI\Imap2\Message::setFlagFull($imap, $sequence, $flag, $options); + } + } + +if (! \function_exists('imap_sort')) + { + function imap_sort(\IMAP\Connection $imap, int $criteria, bool $reverse, int $flags = 0, ?string $search_criteria = null, ?string $charset = null) : array|false + { + return \PHPFUI\Imap2\Message::sort($imap, $criteria, $reverse, $flags, $search_criteria, $charset); + } + } + +if (! \function_exists('imap_status')) + { + function imap_status(\IMAP\Connection $imap, string $mailbox, int $flags) : \stdClass|false + { + return \PHPFUI\Imap2\Mailbox::status($imap, $mailbox, $flags); + } + } + +if (! \function_exists('imap_subscribe')) + { + function imap_subscribe(\IMAP\Connection $imap, string $mailbox) : bool + { + return \PHPFUI\Imap2\Mailbox::subscribe($imap, $mailbox); + } + } + +if (! \function_exists('imap_thread')) + { + function imap_thread(\IMAP\Connection $imap, int $flags = SE_FREE) : array|false + { + return \PHPFUI\Imap2\Thread::thread($imap, $flags); + } + } + +if (! \function_exists('imap_timeout')) + { + function imap_timeout(int $timeout_type, int $timeout = -1) : int|bool + { + return \PHPFUI\Imap2\Timeout::set($timeout_type, $timeout); + } + } + +if (! \function_exists('imap_uid')) + { + function imap_uid(\IMAP\Connection $imap, int $message_num) : int + { + return \PHPFUI\Imap2\Message::uid($imap, $message_num); + } + } + +if (! \function_exists('imap_undelete')) + { + function imap_undelete(\IMAP\Connection $imap, string $message_nums, int $flags = 0) : true + { + return \PHPFUI\Imap2\Message::undelete($imap, $message_nums, $flags); + } + } + +if (! \function_exists('imap_unsubscribe')) + { + function imap_unsubscribe(\IMAP\Connection $imap, string $mailbox) : bool + { + return \PHPFUI\Imap2\Mailbox::unsubscribe($imap, $mailbox); + } + } + +if (! \function_exists('imap_utf7_decode')) + { + function imap_utf7_decode(string $string) : string + { + return \mb_convert_encoding($string, 'UTF7-IMAP', 'ISO-8859-1'); + } + } + +if (! \function_exists('imap_utf7_encode')) + { + function imap_utf7_encode(string $string) : string + { + return \mb_convert_encoding($string, 'ISO-8859-1', 'UTF7-IMAP'); + } + } + +if (! \function_exists('imap_utf8')) + { + function imap_utf8(string $string) : string + { + return \iconv_mime_decode($string, 0, 'UTF-8'); + } + } + +if (! \function_exists('imap_utf8_to_mutf7')) + { + function imap_utf8_to_mutf7(string $string) : string + { + return \mb_convert_encoding($string, 'UTF8', 'UTF7-IMAP'); + } + } diff --git a/src/Imap2/Mail.php b/src/Imap2/Mail.php new file mode 100644 index 0000000..ca0a42f --- /dev/null +++ b/src/Imap2/Mail.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Mail + { + /** + * Copy specified messages to a mailbox. + */ + public static function copy(\IMAP\Connection $imap, $messageNums, string $mailbox, int $flags = 0) : bool + { + if ($flags & CP_MOVE) + { + return Mail::move($imap, $messageNums, $mailbox, $flags); + } + + $client = $imap->getClient(); + + if (! ($flags & CP_UID)) + { + $messageNums = \PHPFUI\Imap2\Message::idToUid($imap, $messageNums); + } + + $from = $imap->getMailboxName(); + $to = $mailbox; + + return $client->copy($messageNums, $from, $to); + } + + /** + * Move specified messages to a mailbox. + */ + public static function move(\IMAP\Connection $imap, string $messageNums, string $mailbox, int $flags = 0) : bool + { + $client = $imap->getClient(); + + if (! ($flags & CP_UID)) + { + $messageNums = \PHPFUI\Imap2\Message::idToUid($imap, $messageNums); + } + + return $client->move($messageNums, $imap->getMailboxName(), $mailbox); + } + } diff --git a/src/Imap2/Mailbox.php b/src/Imap2/Mailbox.php new file mode 100644 index 0000000..9a5b5fe --- /dev/null +++ b/src/Imap2/Mailbox.php @@ -0,0 +1,287 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Mailbox + { + /** + * Append a string message to a specified mailbox. + */ + public static function append(\IMAP\Connection $imap, $folder, $message, $options = null, $internalDate = null) : bool + { + $folderParts = \explode('}', $folder); + $client = $imap->getClient(); + + $mailbox = empty($folderParts[1]) ? 'INBOX' : $folderParts[1]; + + $success = $client->append($mailbox, $message); + + return (bool)$success; + } + + public static function check(\IMAP\Connection $imap) : \stdClass + { + $imap->selectMailbox(); + + $client = $imap->getClient(); + $status = $client->status($imap->getMailboxName(), ['MESSAGES', 'RECENT']); + + return (object)[ + 'Date' => \date('D, j M Y G:i:s') . ' +0000 (UTC)', + 'Driver' => 'imap', + 'Mailbox' => $imap->getMailbox(), + 'Nmsgs' => (int)($status['MESSAGES']), + 'Recent' => (int)($status['RECENT']), + ]; + } + + public static function createMailbox(\IMAP\Connection $imap, string $mailbox) : bool + { + $client = $imap->getClient(); + + if ('{' == $mailbox[0]) + { + $mailbox = (string)\preg_replace('/^{.+}/', '', $mailbox); + } + + $success = $client->createFolder($mailbox); + + if (! $success) + { + \PHPFUI\Imap2\Errors::appendError($client->getRawLastLine()); + } + + return $success; + } + + public static function deleteMailbox(\IMAP\Connection $imap, string $mailbox) + { + $client = $imap->getClient(); + + if ('{' == $mailbox[0]) + { + $mailbox = (string)\preg_replace('/^{.+}/', '', $mailbox); + } + + $result = $client->execute('DELETE', [$client->escape($mailbox)], \PHPFUI\Imap2\Roundcube\ImapClient::COMMAND_RAW_LASTLINE); + + $success = \PHPFUI\Imap2\Roundcube\ImapClient::ERROR_OK == $result[0]; + + if (! $success && $imap->getRegistryValue('mailbox', $mailbox, 'deleted')) + { + \PHPFUI\Imap2\Errors::appendError($result[1]); + } + elseif (! $success) + { + \PHPFUI\Imap2\Errors::appendError("Can't delete mailbox {$mailbox}: no such mailbox"); + } + else + { + $imap->setRegistryValue('mailbox', $mailbox, 'deleted', true); + } + + return $success; + } + + public static function getMailboxes(\IMAP\Connection $imap, $reference, $pattern) : array | false + { + if (! $imap->isConnected()) + { + return false; + } + + $referenceParts = \explode('}', $reference); + $client = $imap->getClient(); + $return = []; + $delimiter = $client->getHierarchyDelimiter(); + $mailboxes = $client->listMailboxes($referenceParts[1], $pattern); + + foreach ($mailboxes as $mailbox) + { + $attributesValue = Functions::getListAttributesValue($client->data['LIST'][$mailbox]); + + if ('[Gmail]' == $mailbox && 'imap.gmail.com' == $imap->getHost()) + { + $attributesValue = 34; + } + $return[] = (object)[ + 'name' => $referenceParts[0] . '}' . $mailbox, + 'attributes' => $attributesValue, + 'delimiter' => $delimiter, + ]; + } + + return $return; + } + + public static function list(\IMAP\Connection $imap, $reference, $pattern) : array + { + $referenceParts = \explode('}', $reference); + $client = $imap->getClient(); + $return = []; + $mailboxes = $client->listMailboxes($referenceParts[1], $pattern); + + foreach ($mailboxes as $mailbox) + { + if (\in_array('\\Noselect', $client->data['LIST'][$mailbox])) + { + continue; + } + $return[] = $referenceParts[0] . '}' . $mailbox; + } + + return $return; + } + + public static function listScan(\IMAP\Connection $imap, $reference, $pattern) : array + { + $referenceParts = \explode('}', $reference); + $client = $imap->getClient(); + $return = []; + $mailboxes = $client->listMailboxes($referenceParts[1], $pattern); + + foreach ($mailboxes as $mailbox) + { + if (\in_array('\\Noselect', $client->data['LIST'][$mailbox])) + { + continue; + } + $return[] = $referenceParts[0] . '}' . $mailbox; + } + + return $return; + } + + public static function listSubscribed(\IMAP\Connection $imap, $mailbox) : bool + { + } + + public static function mailboxMsgInfo(\IMAP\Connection $imap) : \stdClass + { + $client = $imap->getClient(); + + $imap->selectMailbox(); + $mailboxName = $imap->getMailboxName(); + + $status = $client->status($mailboxName, [ + 'MESSAGES', + 'UNSEEN', + 'RECENT', + 'UIDNEXT', + 'UIDVALIDITY' + ]); + + $mailboxInfo = [ + 'Unread' => (int)($status['UNSEEN']), + 'Deleted' => 0, + 'Nmsgs' => (int)($status['MESSAGES']), + 'Size' => 0, + 'Date' => \date('D, j M Y G:i:s') . ' +0000 (UTC)', + 'Driver' => 'imap', + 'Mailbox' => $imap->getMailbox(), + 'Recent' => (int)($status['RECENT']) + ]; + + return (object)$mailboxInfo; + } + + public static function numMsg(\IMAP\Connection $imap) : int + { + $imap->selectMailbox(); + $client = $imap->getClient(); + + $status = $client->status($imap->getMailboxName(), ['MESSAGES']); + + return (int)($status['MESSAGES']); + } + + public static function numRecent(\IMAP\Connection $imap) : int + { + $client = $imap->getClient(); + $imap->selectMailbox(); + + return (int)$client->data['RECENT']; + } + + public static function renameMailbox(\IMAP\Connection $imap, string $from, string $to) : bool + { + return $imap->getClient()->renameFolder($from, $to); + } + + public static function status(\IMAP\Connection $imap, string $mailbox, int $flags) : \stdClass | false + { + $mailboxName = Functions::getMailboxName($mailbox); + + $client = $imap->getClient(); + + $items = []; + + $statusKeys = [ + 'MESSAGES' => 'messages', + 'UNSEEN' => 'unseen', + 'RECENT' => 'recent', + 'UIDNEXT' => 'uidnext', + 'UIDVALIDITY' => 'uidvalidity', + ]; + + if ($flags & SA_MESSAGES || $flags & SA_ALL) + { + $items[] = 'MESSAGES'; + } + + if ($flags & SA_RECENT || $flags & SA_ALL) + { + $items[] = 'RECENT'; + } + + if ($flags & SA_UNSEEN || $flags & SA_ALL) + { + $items[] = 'UNSEEN'; + } + + if ($flags & SA_UIDNEXT || $flags & SA_ALL) + { + $items[] = 'UIDNEXT'; + } + + if ($flags & SA_UIDVALIDITY || $flags & SA_ALL) + { + $items[] = 'UIDVALIDITY'; + } + + $status = $client->status($mailboxName, $items); + + if (empty($status)) + { + return false; + } + + $returnStatus = []; + + foreach ($status as $key => $value) + { + $returnStatus[$statusKeys[$key]] = \is_numeric($value) ? (int)$value : $value; + } + + return (object)$returnStatus; + } + + public static function subscribe(\IMAP\Connection $imap, string $mailbox) : bool + { + return $imap->getClient()->deleteFolder($mailbox); + } + + public static function unsubscribe(\IMAP\Connection $imap, string $mailbox) : bool + { + return $imap->getClient()->deleteFolder($mailbox); + } + } diff --git a/src/Imap2/Message.php b/src/Imap2/Message.php new file mode 100644 index 0000000..b608e4d --- /dev/null +++ b/src/Imap2/Message.php @@ -0,0 +1,537 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Message + { + public static function body(\IMAP\Connection $imap, int $messageNum, int $flags = 0) + { + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + + /** + * TODO + * The information on whether the fetch operation will be performed in PEEK mode should be obtained + * from the $flags parameter. The $flags parameter needs to be moved to where the fetch() function is called, + * and the PEEK mode needs to be decided whether the $flags variable contains the FT_PEEK value. + */ + $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK[TEXT]']); + + if ($isUid && \is_array($messages)) + { + $messages = \PHPFUI\Imap2\Functions::keyBy('uid', $messages); + } + + return $messages[$messageNum]->bodypart['TEXT']; + } + + public static function bodyStruct(\IMAP\Connection $imap, int $messageNum, $section) + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageNum, false, ['BODY.PEEK[' . $section . ']']); + + if ($section) + { + return $messages[$messageNum]->bodypart[$section]; + } + + return $messages[$messageNum]->body; + } + + /** + * Clears cache + */ + public static function clearCache(\IMAP\Connection $imap, int $flags) : true + { + $client = $imap->getClient(); + + $client->clear_mailbox_cache(); + $client->clear_status_cache(); + + return true; + } + + /** + * Clears flags on messages. + */ + public static function clearFlagFull(\IMAP\Connection $imap, $sequence, $flag, $options = 0) : string | false + { + $client = $imap->getClient(); + + if (! ($options & ST_UID)) + { + $messages = $client->fetch($imap->getMailboxName(), $sequence, false, ['UID']); + + $uid = []; + + foreach ($messages as $message) + { + $uid[] = $message->uid; + } + + $sequence = \implode(',', $uid); + } + + $client->unflag($imap->getMailboxName(), $sequence, \strtoupper(\substr($flag, 1))); + + return false; + } + + public static function delete(\IMAP\Connection $imap, string $messageNums, int $flags = 0) + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageNums, false, ['UID']); + + $uid = []; + + foreach ($messages as $message) + { + $uid[] = $message->uid; + } + + $client->flag($imap->getMailboxName(), \implode(',', $uid), $client->flags['DELETED']); + + return true; + } + + public static function expunge(\IMAP\Connection $imap) + { + return $imap->getClient()->expunge($imap->getMailboxName()); + } + + public static function fetchBody(\IMAP\Connection $imap, int $messageNum, $section, int $flags = 0) + { + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK[' . $section . ']']); + + if (empty($messages)) + { + \trigger_error(\PHPFUI\Imap2\Errors::badMessageNumber(), E_USER_WARNING); + + return false; + } + + if ($isUid && \is_array($messages)) + { + $messages = \PHPFUI\Imap2\Functions::keyBy('uid', $messages); + } + + if ($section) + { + return $messages[$messageNum]->bodypart[$section]; + } + + return $messages[$messageNum]->body; + } + + public static function fetchHeader(\IMAP\Connection $imap, int $messageNum, int $flags = 0) + { + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + + $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK[HEADER]']); + + if (empty($messages)) + { + return false; + } + + foreach ($messages as $message) + { + return $message->bodypart['HEADER'] ?? false; + } + } + + public static function fetchMime(\IMAP\Connection $imap, int $messageNum, $section, int $flags = 0) : string | false + { + if ($messageNum <= 0) + { + \trigger_error(\PHPFUI\Imap2\Errors::badMessageNumber(), E_USER_WARNING); + + return false; + } + + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + + $sectionKey = $section . '.MIME'; + $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK[' . $sectionKey . ']']); + + if (empty($messages)) + { + return false; + } + + if ($isUid && \is_array($messages)) + { + $messages = \PHPFUI\Imap2\Functions::keyBy('uid', $messages); + } + + if ($section && isset($messages[$messageNum]->bodypart[$sectionKey])) + { + return $messages[$messageNum]->bodypart[$sectionKey]; + } + + return $messages[$messageNum]->body; + } + + public static function fetchOverview(\IMAP\Connection $imap, $sequence, int $flags = 0) : array | false + { + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + $messages = $client->fetch($imap->getMailboxName(), $sequence, $isUid, [ + 'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO CC REPLYTO MESSAGEID DATE SIZE REFERENCES)]', + 'UID', + 'FLAGS', + 'INTERNALDATE', + 'RFC822.SIZE', + 'ENVELOPE', + 'RFC822.HEADER' + ]); + + + if (false === $messages) + { + return false; + } + + if ('*' != $sequence && \count($messages) < \PHPFUI\Imap2\Functions::expectedNumberOfMessages($sequence)) + { + return false; + } + + $overview = []; + + foreach ($messages as $message) + { + $messageEntry = (object)[ + 'subject' => $message->envelope[1], + 'from' => \PHPFUI\Imap2\Functions::writeAddressFromEnvelope($message->envelope[2]), + 'to' => $message->get('to'), + 'date' => $message->envelope[0], + 'message_id' => $message->envelope[9], + 'references' => $message->references, + 'in_reply_to' => $message->envelope[8], + 'size' => $message->size, + 'uid' => $message->uid, + 'msgno' => $message->id, + 'recent' => (int)($message->flags['RECENT'] ?? 0), + 'flagged' => (int)($message->flags['FLAGGED'] ?? 0), + 'answered' => (int)($message->flags['ANSWERED'] ?? 0), + 'deleted' => (int)($message->flags['DELETED'] ?? 0), + 'seen' => (int)($message->flags['SEEN'] ?? 0), + 'draft' => (int)($message->flags['DRAFT'] ?? 0), + 'udate' => \strtotime($message->internaldate), + ]; + + if (empty($messageEntry->subject)) + { + $messageEntry->subject = null; + } + + if (empty($messageEntry->references)) + { + $messageEntry->references = null; + } + + if (empty($messageEntry->in_reply_to)) + { + $messageEntry->in_reply_to = null; + } + + if (empty($messageEntry->to)) + { + $messageEntry->to = null; + } + + $overview[] = $messageEntry; + } + + return $overview; + } + + public static function fetchStructure(\IMAP\Connection $imap, int $messageNum, int $flags = 0) : \stdClass | false + { + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + + $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODYSTRUCTURE']); + + if (empty($messages)) + { + return false; + } + + foreach ($messages as $message) + { + return \PHPFUI\Imap2\BodyStructure::fromMessage($message); + } + + return false; + } + + public static function fetchUids(\IMAP\Connection $imap, string $sequence, int $flags = 0) : array | false + { + $client = $imap->getClient(); + + $isUid = (bool)($flags & FT_UID); + $messages = $client->fetch($imap->getMailboxName(), $sequence, $isUid, ['UID']); + + if ('*' != $sequence && \count($messages) < \PHPFUI\Imap2\Functions::expectedNumberOfMessages($sequence)) + { + return false; + } + + return $messages; + } + + public static function headerInfo(\IMAP\Connection $imap, int $messageNum, int $fromLength = 0, int $subjectLength = 0, ?string $defaultHost = null) : \stdClass | false + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageNum, false, [ + 'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO CC REPLY-TO DATE SIZE REFERENCES)]', + 'ENVELOPE', + 'INTERNALDATE', + 'UID', + 'FLAGS', + 'RFC822.SIZE', + 'RFC822.HEADER' + ]); + + if (empty($messages)) + { + return false; + } + + foreach ($messages as $message) + { + return \PHPFUI\Imap2\HeaderInfo::fromMessage($message, $defaultHost); + } + + return false; + } + + public static function headers(\IMAP\Connection $imap) : array + { + $client = $imap->getClient(); + + $status = $client->status($imap->getMailboxName(), ['MESSAGES']); + + if (empty($status['MESSAGES'])) + { + return []; + } + + $sequence = '1:' . (int)($status['MESSAGES']); + $messages = $client->fetch($imap->getMailboxName(), $sequence, false, [ + 'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO CC REPLYTO MESSAGEID DATE SIZE REFERENCES)]', + //'UID', + 'FLAGS', + 'INTERNALDATE', + 'RFC822.SIZE', + //'ENVELOPE', + 'RFC822.HEADER' + ]); + + if (empty($messages)) + { + return []; + } + + $headers = []; + + foreach ($messages as $message) + { + $from = ' '; + + if ('no_host' != $message->from) + { + $from = \imap_rfc822_parse_adrlist($message->from, 'no_host'); + $from = $from[0]->personal ?? $message->from; + } + + $date = \explode(' ', $message->internaldate); + $subject = empty($message->subject) ? ' ' : $message->subject; + $unseen = empty($message->flags['SEEN']) ? 'U' : ' '; + $flagged = empty($message->flags['FLAGGED']) ? ' ' : 'F'; + $answered = empty($message->flags['ANSWERED']) ? ' ' : 'A'; + $draft = empty($message->flags['DRAFT']) ? ' ' : 'D'; + $deleted = empty($message->flags['DELETED']) ? ' ' : 'X'; + + $header = ' ' . $unseen . $flagged . $answered . $draft . $deleted . ' ' + . \str_pad($message->id, 3, ' ', STR_PAD_LEFT) . ')' . $date[0] . ' ' . \str_pad($from, 20, ' ') . ' ' + . \substr($subject, 0, 25) . ' (' . $message->size . ' chars)'; + + $headers[] = $header; + } + + return $headers; + } + + /** + * Convert a string contain a sequence of message id to and equivalent with uid. + */ + public static function idToUid(\IMAP\Connection $imap, $messageNums) : string + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageNums, false, ['UID']); + + $uid = []; + + foreach ($messages as $message) + { + $uid[] = $message->uid; + } + + return \implode(',', $uid); + } + + public static function msgno(\IMAP\Connection $imap, $messageUid) + { + $client = $imap->getClient(); + + $msgNo = self::uidtoid($imap, $messageUid); + + return \is_numeric($msgNo) ? (int)$msgNo : $msgNo; + } + + public static function saveBody(\IMAP\Connection $imap, $file, $messageNum, $section = '', $flags = 0) + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageNum, false, ['BODY.PEEK[' . $section . ']']); + + $body = $section ? $messages[$messageNum]->bodypart[$section] : $messages[$messageNum]->body; + + return \file_put_contents($file, $body); + } + + /** + * Returns an array of messages matching the given search criteria. + */ + public static function search(\IMAP\Connection $imap, $criteria, int $flags = SE_FREE, string $charset = '') : array | false + { + $client = $imap->getClient(); + + $result = $client->search($imap->getMailboxName(), $criteria, $flags & SE_UID); + + if (empty($result->count())) + { + return false; + } + + $messages = $result->get(); + + foreach ($messages as &$message) + { + $message = \is_numeric($message) ? (int)$message : $message; + } + + return $messages; + } + + /** + * Sets flags on messages. + */ + public static function setFlagFull(\IMAP\Connection $imap, $sequence, string $flag, int $options = 0) : bool + { + $client = $imap->getClient(); + + if (! ($options & ST_UID)) + { + $messages = $client->fetch($imap->getMailboxName(), $sequence, false, ['UID']); + + $uid = []; + + foreach ($messages as $message) + { + $uid[] = $message->uid; + } + + $sequence = \implode(',', $uid); + } + + return $client->flag($imap->getMailboxName(), $sequence, \strtoupper(\substr($flag, 1))); + } + + public static function sort(\IMAP\Connection $imap, $criteria, $reverse, int $flags = 0, $searchCriteria = null, ?string $charset = null) : string | false + { + $client = $imap->getClient(); + + $result = $client->search($imap->getMailboxName(), $criteria, $flags & SE_UID); + + if (empty($result->count())) + { + return false; + } + + $messages = $result->get(); + + foreach ($messages as &$message) + { + $message = \is_numeric($message) ? (int)$message : $message; + } + + return $messages; + } + + public static function uid(\IMAP\Connection $imap, int $messageNum) : int + { + $uid = self::idToUid($imap, $messageNum); + + return \is_numeric($uid) ? (int)$uid : $uid; + } + + /** + * Convert a string contain a sequence of uid(s) to an equivalent with id(s). + */ + public static function uidToId(\IMAP\Connection $imap, $messageUid) : string + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageUid, true, ['UID']); + + $id = []; + + foreach ($messages as $message) + { + $id[] = $message->id; + } + + return \implode(',', $id); + } + + public static function undelete(\IMAP\Connection $imap, string $messageNums, int $flags = 0) + { + $client = $imap->getClient(); + + $messages = $client->fetch($imap->getMailboxName(), $messageNums, false, ['UID']); + + foreach ($messages as $message) + { + $client->unflag($imap->getMailboxName(), $message->uid, $client->flags['DELETED']); + } + + return true; + } + } diff --git a/src/Imap2/Roundcube/Charset.php b/src/Imap2/Roundcube/Charset.php new file mode 100644 index 0000000..aa57968 --- /dev/null +++ b/src/Imap2/Roundcube/Charset.php @@ -0,0 +1,806 @@ + | + * | | + * | Licensed under the GNU General Public License version 3 or | + * | any later version with exceptions for skins & plugins. | + * | See the README file for a full license statement. | + * | | + * | PURPOSE: | + * | Provide charset conversion functionality | + * +-----------------------------------------------------------------------+ + * | Author: Thomas Bruederli | + * | Author: Aleksander Machniak | + * | Author: Edmund Grimley Evans | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +/** + * Character sets conversion functionality + * + * @package Framework + * @subpackage Core + */ +class Charset +{ + // Aliases: some of them from HTML5 spec. + public static $aliases = [ + 'USASCII' => 'WINDOWS-1252', + 'ANSIX31101983' => 'WINDOWS-1252', + 'ANSIX341968' => 'WINDOWS-1252', + 'UNKNOWN8BIT' => 'ISO-8859-15', + 'UNKNOWN' => 'ISO-8859-15', + 'USERDEFINED' => 'ISO-8859-15', + 'KSC56011987' => 'EUC-KR', + 'GB2312' => 'GBK', + 'GB231280' => 'GBK', + 'UNICODE' => 'UTF-8', + 'UTF7IMAP' => 'UTF7-IMAP', + 'TIS620' => 'WINDOWS-874', + 'ISO88599' => 'WINDOWS-1254', + 'ISO885911' => 'WINDOWS-874', + 'MACROMAN' => 'MACINTOSH', + '77' => 'MAC', + '128' => 'SHIFT-JIS', + '129' => 'CP949', + '130' => 'CP1361', + '134' => 'GBK', + '136' => 'BIG5', + '161' => 'WINDOWS-1253', + '162' => 'WINDOWS-1254', + '163' => 'WINDOWS-1258', + '177' => 'WINDOWS-1255', + '178' => 'WINDOWS-1256', + '186' => 'WINDOWS-1257', + '204' => 'WINDOWS-1251', + '222' => 'WINDOWS-874', + '238' => 'WINDOWS-1250', + 'MS950' => 'CP950', + 'WINDOWS949' => 'UHC', + ]; + + /** + * Windows codepages + * + * @var array + */ + public static $windows_codepages = [ + 37 => 'IBM037', // IBM EBCDIC US-Canada + 437 => 'IBM437', // OEM United States + 500 => 'IBM500', // IBM EBCDIC International + 708 => 'ASMO-708', // Arabic (ASMO 708) + 720 => 'DOS-720', // Arabic (Transparent ASMO); Arabic (DOS) + 737 => 'IBM737', // OEM Greek (formerly 437G); Greek (DOS) + 775 => 'IBM775', // OEM Baltic; Baltic (DOS) + 850 => 'IBM850', // OEM Multilingual Latin 1; Western European (DOS) + 852 => 'IBM852', // OEM Latin 2; Central European (DOS) + 855 => 'IBM855', // OEM Cyrillic (primarily Russian) + 857 => 'IBM857', // OEM Turkish; Turkish (DOS) + 858 => 'IBM00858', // OEM Multilingual Latin 1 + Euro symbol + 860 => 'IBM860', // OEM Portuguese; Portuguese (DOS) + 861 => 'IBM861', // OEM Icelandic; Icelandic (DOS) + 862 => 'DOS-862', // OEM Hebrew; Hebrew (DOS) + 863 => 'IBM863', // OEM French Canadian; French Canadian (DOS) + 864 => 'IBM864', // OEM Arabic; Arabic (864) + 865 => 'IBM865', // OEM Nordic; Nordic (DOS) + 866 => 'cp866', // OEM Russian; Cyrillic (DOS) + 869 => 'IBM869', // OEM Modern Greek; Greek, Modern (DOS) + 870 => 'IBM870', // IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2 + 874 => 'windows-874', // ANSI/OEM Thai (ISO 8859-11); Thai (Windows) + 875 => 'cp875', // IBM EBCDIC Greek Modern + 932 => 'shift_jis', // ANSI/OEM Japanese; Japanese (Shift-JIS) + 936 => 'gb2312', // ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312) + 950 => 'big5', // ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5) + 1026 => 'IBM1026', // IBM EBCDIC Turkish (Latin 5) + 1047 => 'IBM01047', // IBM EBCDIC Latin 1/Open System + 1140 => 'IBM01140', // IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro) + 1141 => 'IBM01141', // IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro) + 1142 => 'IBM01142', // IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro) + 1143 => 'IBM01143', // IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro) + 1144 => 'IBM01144', // IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro) + 1145 => 'IBM01145', // IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro) + 1146 => 'IBM01146', // IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro) + 1147 => 'IBM01147', // IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro) + 1148 => 'IBM01148', // IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro) + 1149 => 'IBM01149', // IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro) + 1200 => 'UTF-16', // Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications + 1201 => 'UTF-16BE', // Unicode UTF-16, big endian byte order; available only to managed applications + 1250 => 'windows-1250', // ANSI Central European; Central European (Windows) + 1251 => 'windows-1251', // ANSI Cyrillic; Cyrillic (Windows) + 1252 => 'windows-1252', // ANSI Latin 1; Western European (Windows) + 1253 => 'windows-1253', // ANSI Greek; Greek (Windows) + 1254 => 'windows-1254', // ANSI Turkish; Turkish (Windows) + 1255 => 'windows-1255', // ANSI Hebrew; Hebrew (Windows) + 1256 => 'windows-1256', // ANSI Arabic; Arabic (Windows) + 1257 => 'windows-1257', // ANSI Baltic; Baltic (Windows) + 1258 => 'windows-1258', // ANSI/OEM Vietnamese; Vietnamese (Windows) + 10000 => 'macintosh', // MAC Roman; Western European (Mac) + 12000 => 'UTF-32', // Unicode UTF-32, little endian byte order; available only to managed applications + 12001 => 'UTF-32BE', // Unicode UTF-32, big endian byte order; available only to managed applications + 20127 => 'US-ASCII', // US-ASCII (7-bit) + 20273 => 'IBM273', // IBM EBCDIC Germany + 20277 => 'IBM277', // IBM EBCDIC Denmark-Norway + 20278 => 'IBM278', // IBM EBCDIC Finland-Sweden + 20280 => 'IBM280', // IBM EBCDIC Italy + 20284 => 'IBM284', // IBM EBCDIC Latin America-Spain + 20285 => 'IBM285', // IBM EBCDIC United Kingdom + 20290 => 'IBM290', // IBM EBCDIC Japanese Katakana Extended + 20297 => 'IBM297', // IBM EBCDIC France + 20420 => 'IBM420', // IBM EBCDIC Arabic + 20423 => 'IBM423', // IBM EBCDIC Greek + 20424 => 'IBM424', // IBM EBCDIC Hebrew + 20838 => 'IBM-Thai', // IBM EBCDIC Thai + 20866 => 'koi8-r', // Russian (KOI8-R); Cyrillic (KOI8-R) + 20871 => 'IBM871', // IBM EBCDIC Icelandic + 20880 => 'IBM880', // IBM EBCDIC Cyrillic Russian + 20905 => 'IBM905', // IBM EBCDIC Turkish + 20924 => 'IBM00924', // IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) + 20932 => 'EUC-JP', // Japanese (JIS 0208-1990 and 0212-1990) + 20936 => 'cp20936', // Simplified Chinese (GB2312); Chinese Simplified (GB2312-80) + 20949 => 'cp20949', // Korean Wansung + 21025 => 'cp1025', // IBM EBCDIC Cyrillic Serbian-Bulgarian + 21866 => 'koi8-u', // Ukrainian (KOI8-U); Cyrillic (KOI8-U) + 28591 => 'iso-8859-1', // ISO 8859-1 Latin 1; Western European (ISO) + 28592 => 'iso-8859-2', // ISO 8859-2 Central European; Central European (ISO) + 28593 => 'iso-8859-3', // ISO 8859-3 Latin 3 + 28594 => 'iso-8859-4', // ISO 8859-4 Baltic + 28595 => 'iso-8859-5', // ISO 8859-5 Cyrillic + 28596 => 'iso-8859-6', // ISO 8859-6 Arabic + 28597 => 'iso-8859-7', // ISO 8859-7 Greek + 28598 => 'iso-8859-8', // ISO 8859-8 Hebrew; Hebrew (ISO-Visual) + 28599 => 'iso-8859-9', // ISO 8859-9 Turkish + 28603 => 'iso-8859-13', // ISO 8859-13 Estonian + 28605 => 'iso-8859-15', // ISO 8859-15 Latin 9 + 38598 => 'iso-8859-8-i', // ISO 8859-8 Hebrew; Hebrew (ISO-Logical) + 50220 => 'iso-2022-jp', // ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS) + 50221 => 'csISO2022JP', // ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana) + 50222 => 'iso-2022-jp', // ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI) + 50225 => 'iso-2022-kr', // ISO 2022 Korean + 51932 => 'EUC-JP', // EUC Japanese + 51936 => 'EUC-CN', // EUC Simplified Chinese; Chinese Simplified (EUC) + 51949 => 'EUC-KR', // EUC Korean + 52936 => 'hz-gb-2312', // HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ) + 54936 => 'GB18030', // Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030) + 65000 => 'UTF-7', + 65001 => 'UTF-8', + ]; + + /** + * Removes non-unicode characters from input. + */ + public static function clean(string | array $input) : string | array + { + // handle input of type array + if (\is_array($input)) { + foreach ($input as $idx => $val) { + $input[$idx] = self::clean($val); + } + + return $input; + } + + if (! \is_string($input) || '' == $input) { + return $input; + } + + // iconv/mbstring are much faster (especially with long strings) + if (\function_exists('mb_convert_encoding')) { + $msch = \mb_substitute_character(); + \mb_substitute_character('none'); + $res = \mb_convert_encoding($input, 'UTF-8', 'UTF-8'); + \mb_substitute_character($msch); + + if (false !== $res) { // @phpstan-ignore-line + return $res; + } + } + + if (\function_exists('iconv')) { + if (($res = @\iconv('UTF-8', 'UTF-8//IGNORE', $input)) !== false) { + return $res; + } + } + + $seq = ''; + $out = ''; + $regexp = '/^(' . +// '[\x00-\x7F]'. // UTF8-1 + '|[\xC2-\xDF][\x80-\xBF]' . // UTF8-2 + '|\xE0[\xA0-\xBF][\x80-\xBF]' . // UTF8-3 + '|[\xE1-\xEC][\x80-\xBF][\x80-\xBF]' . // UTF8-3 + '|\xED[\x80-\x9F][\x80-\xBF]' . // UTF8-3 + '|[\xEE-\xEF][\x80-\xBF][\x80-\xBF]' . // UTF8-3 + '|\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]' . // UTF8-4 + '|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]' . // UTF8-4 + '|\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]' . // UTF8-4 + ')$/'; + + for ($i = 0, $len = \strlen($input); $i < $len; $i++) { + $chr = $input[$i]; + $ord = \ord($chr); + + // 1-byte character + if ($ord <= 0x7F) { + if ('' !== $seq) { + $out .= \preg_match($regexp, $seq) ? $seq : ''; + $seq = ''; + } + + $out .= $chr; + } + // first byte of multibyte sequence + elseif ($ord >= 0xC0) { + if ('' !== $seq) { + $out .= \preg_match($regexp, $seq) ? $seq : ''; + $seq = ''; + } + + $seq = $chr; + } + // next byte of multibyte sequence + elseif ('' !== $seq) { + $seq .= $chr; + } + } + + if ('' !== $seq) { + $out .= \preg_match($regexp, $seq) ? $seq : ''; + } + + return $out; + } + + /** + * Convert a string from one charset to another. + * Uses mbstring and iconv functions if possible + * + * @param string $str Input string + * @param string $from Suspected charset of the input string + * @param string $to Target charset to convert to; defaults to RCUBE_CHARSET + * + * @return string Converted string + */ + public static function convert(string $str, string $from, ?string $to = null) : string + { + static $iconv_options = null; + static $mbstring_sc = null; + + $to = empty($to) ? IMAP2_CHARSET : \strtoupper($to); + $from = self::parse_charset($from); + + // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) + // In that case we can just skip the conversion (use UTF-8) + if ('UTF-16' == $from && ! \preg_match('/[^\x00-\x7F]/', $str)) { + $from = 'UTF-8'; + } + + if ($from == $to || empty($str) || empty($from)) { + return $str; + } + + if (null === $iconv_options) { + if (\function_exists('iconv')) { + // ignore characters not available in output charset + $iconv_options = '//IGNORE'; + + if (false === \iconv('', $iconv_options, '')) { + // iconv implementation does not support options + $iconv_options = ''; + } + } + else { + $iconv_options = false; + } + } + + // convert charset using iconv module + if (false !== $iconv_options && 'UTF7-IMAP' != $from && 'UTF7-IMAP' != $to + && 'ISO-2022-JP' !== $from + ) { + // throw an exception if iconv reports an illegal character in input + // it means that input string has been truncated + try { + $out = \iconv($from, $to . $iconv_options, $str); + } + catch (\Exception $e) { + $out = false; + } + + if (false !== $out) { + return $out; + } + } + + if (null === $mbstring_sc) { + $mbstring_sc = \extension_loaded('mbstring') ? \mb_substitute_character() : false; + } + + // convert charset using mbstring module + if (false !== $mbstring_sc) { + $aliases = [ + 'WINDOWS-1257' => 'ISO-8859-13', + 'US-ASCII' => 'ASCII', + 'ISO-2022-JP' => 'ISO-2022-JP-MS', + ]; + + $mb_from = $aliases[$from] ?? $from; + $mb_to = $aliases[$to] ?? $to; + + // Do the same as //IGNORE with iconv + \mb_substitute_character('none'); + + // throw an exception if mbstring reports an illegal character in input + // using mb_check_encoding() is much slower + try { + $out = \mb_convert_encoding($str, $mb_to, $mb_from); + } + catch (\Exception $e) { + $out = false; + } + + \mb_substitute_character($mbstring_sc); + + if (false !== $out) { + return $out; + } + } + + // convert charset using bundled classes/functions + if ('UTF-8' == $to) { + if ('UTF7-IMAP' == $from) { + if ($out = self::utf7imap_to_utf8($str)) { + return $out; + } + } + elseif ('UTF-7' == $from) { + if ($out = self::utf7_to_utf8($str)) { + return $out; + } + } + } + + // encode string for output + if ('UTF-8' == $from) { + // @TODO: we need a function for UTF-7 (RFC2152) conversion + if ('UTF7-IMAP' == $to || 'UTF-7' == $to) { + if ($out = self::utf8_to_utf7imap($str)) { + return $out; + } + } + } + + if (! isset($out)) { + \trigger_error("No suitable function found for '{$from}' to '{$to}' conversion"); + } + + // return original string + return $str; + } + + /** + * Parse and validate charset name string. + * Sometimes charset string is malformed, there are also charset aliases, + * but we need strict names for charset conversion (specially utf8 class) + * + * @param string $input Input charset name + * + * @return ?string The validated charset name + */ + public static function parse_charset($input) : ?string + { + static $charsets = []; + $charset = \strtoupper($input); + + if (isset($charsets[$input])) { + return $charsets[$input]; + } + + $charset = \preg_replace([ + '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO + '/\$.*$/', // e.g. _ISO-8859-JP$SIO + '/UNICODE-1-1-*/', // RFC1641/1642 + '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8) + '/\*.*$/' // lang code according to RFC 2231.5 + ], '', $charset); + + if ('BINARY' == $charset) { + return $charsets[$input] = null; + } + + // allow A-Z and 0-9 only + $str = \preg_replace('/[^A-Z0-9]/', '', $charset); + + if (isset(self::$aliases[$str])) { + $result = self::$aliases[$str]; + } + // UTF + elseif (\preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) { + $result = 'UTF-' . $m[1] . @$m[2]; + } + // ISO-8859 + elseif (\preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) { + $iso = 'ISO-8859-' . ($m[1] ?: 1); + // some clients sends windows-1252 text as latin1, + // it is safe to use windows-1252 for all latin1 + $result = 'ISO-8859-1' == $iso ? 'WINDOWS-1252' : $iso; + } + // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE + elseif (\preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) { + $result = 'WINDOWS-' . $m[2]; + } + // LATIN + elseif (\preg_match('/LATIN(.*)/', $str, $m)) { + $aliases = ['2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10, + '7' => 13, '8' => 14, '9' => 15, '10' => 16, + 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8 + ]; + + // some clients sends windows-1252 text as latin1, + // it is safe to use windows-1252 for all latin1 + if (1 == $m[1]) { + $result = 'WINDOWS-1252'; + } + // if iconv is not supported we need ISO labels, it's also safe for iconv + elseif (! empty($aliases[$m[1]])) { + $result = 'ISO-8859-' . $aliases[$m[1]]; + } + // iconv requires conversion of e.g. LATIN-1 to LATIN1 + else { + $result = $str; + } + } + else { + $result = $charset; + } + + $charsets[$input] = $result; + + return $result; + } + + /** + * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion) + * + * @param string $str Input string + * + * @return string The converted string + */ + public static function utf16_to_utf8(string $str) : string + { + $len = \strlen($str); + $dec = ''; + + for ($i = 0; $i < $len; $i += 2) { + $c = \ord($str[$i]) << 8 | \ord($str[$i + 1]); + + if ($c >= 0x0001 && $c <= 0x007F) { + $dec .= \chr($c); + } + elseif ($c > 0x07FF) { + $dec .= \chr(0xE0 | (($c >> 12) & 0x0F)); + $dec .= \chr(0x80 | (($c >> 6) & 0x3F)); + $dec .= \chr(0x80 | (($c >> 0) & 0x3F)); + } + else { + $dec .= \chr(0xC0 | (($c >> 6) & 0x1F)); + $dec .= \chr(0x80 | (($c >> 0) & 0x3F)); + } + } + + return $dec; + } + + /** + * Converts string from standard UTF-7 (RFC 2152) to UTF-8. + * + * @param string $str Input string (UTF-7) + * + * @return string Converted string (UTF-8) + */ + public static function utf7_to_utf8(string $str) : string + { + $Index_64 = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + ]; + + $u7len = \strlen($str); + $str = (string)$str; + $res = ''; + + for ($i = 0; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + + if ('+' == $u7) { + $i++; + $u7len--; + $ch = ''; + + for (; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + + if (! $Index_64[\ord($u7)]) { + break; + } + + $ch .= $u7; + } + + if ('' == $ch) { + if ('-' == $u7) { + $res .= '+'; + } + + continue; + } + + $res .= self::utf16_to_utf8(\base64_decode($ch)); + } + else { + $res .= $u7; + } + } + + return $res; + } + + /** + * Convert the data ($str) from RFC 2060's UTF-7 to UTF-8. + * If input data is invalid, return the original input string. + * RFC 2060 obviously intends the encoding to be unique (see + * point 5 in section 5.1.3), so we reject any non-canonical + * form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead + * of &AMAAwA-). + * + * Translated from C to PHP by Thomas Bruederli + * + * @param string $str Input string (UTF7-IMAP) + * + * @return string Output string (UTF-8) + */ + public static function utf7imap_to_utf8(string $str) : string + { + $Index_64 = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, 63, -1, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 + ]; + + $u7len = \strlen($str); + $str = (string)$str; + $p = ''; + $err = ''; + + for ($i = 0; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + + if ('&' == $u7) { + $i++; + $u7len--; + $u7 = $str[$i]; + + if ($u7len && '-' == $u7) { + $p .= '&'; + + continue; + } + + $ch = 0; + $k = 10; + + for (; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + + if ((\ord($u7) & 0x80) || ($b = $Index_64[\ord($u7)]) == -1) { + break; + } + + if ($k > 0) { + $ch |= $b << $k; + $k -= 6; + } + else { + $ch |= $b >> (-$k); + + if ($ch < 0x80) { + // Printable US-ASCII + if (0x20 <= $ch && $ch < 0x7f) { + return $err; + } + $p .= \chr($ch); + } + elseif ($ch < 0x800) { + $p .= \chr(0xc0 | ($ch >> 6)); + $p .= \chr(0x80 | ($ch & 0x3f)); + } + else { + $p .= \chr(0xe0 | ($ch >> 12)); + $p .= \chr(0x80 | (($ch >> 6) & 0x3f)); + $p .= \chr(0x80 | ($ch & 0x3f)); + } + + $ch = ($b << (16 + $k)) & 0xffff; + $k += 10; + } + } + + // Non-zero or too many extra bits + if ($ch || $k < 6) { + return $err; + } + + // BASE64 not properly terminated + if (! $u7len || '-' != $u7) { + return $err; + } + + // Adjacent BASE64 sections + if ($u7len > 2 && '&' == $str[$i + 1] && '-' != $str[$i + 2]) { + return $err; + } + } + // Not printable US-ASCII + elseif (\ord($u7) < 0x20 || \ord($u7) >= 0x7f) { + return $err; + } + else { + $p .= $u7; + } + } + + return $p; + } + + /** + * Convert the data ($str) from UTF-8 to RFC 2060's UTF-7. + * Unicode characters above U+FFFF are replaced by U+FFFE. + * If input data is invalid, return an empty string. + * + * Translated from C to PHP by Thomas Bruederli + * + * @param string $str Input string (UTF-8) + * + * @return string Output string (UTF7-IMAP) + */ + public static function utf8_to_utf7imap(string $str) : string + { + $B64Chars = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '+', ',' + ]; + + $u8len = \strlen($str); + $base64 = 0; + $i = $b = $k = 0; + $p = ''; + $err = ''; + + while ($u8len) { + $u8 = $str[$i]; + $c = \ord($u8); + + if ($c < 0x80) { + $ch = $c; + $n = 0; + } + elseif ($c < 0xc2) { + return $err; + } + elseif ($c < 0xe0) { + $ch = $c & 0x1f; + $n = 1; + } + elseif ($c < 0xf0) { + $ch = $c & 0x0f; + $n = 2; + } + elseif ($c < 0xf8) { + $ch = $c & 0x07; + $n = 3; + } + elseif ($c < 0xfc) { + $ch = $c & 0x03; + $n = 4; + } + elseif ($c < 0xfe) { + $ch = $c & 0x01; + $n = 5; + } + else { + return $err; + } + + $i++; + $u8len--; + + if ($n > $u8len) { + return $err; + } + + for ($j = 0; $j < $n; $j++) { + $o = \ord($str[$i + $j]); + + if (($o & 0xc0) != 0x80) { + return $err; + } + $ch = ($ch << 6) | ($o & 0x3f); + } + + if ($n > 1 && ! ($ch >> ($n * 5 + 1))) { + return $err; + } + + $i += $n; + $u8len -= $n; + + if ($ch < 0x20 || $ch >= 0x7f) { + if (! $base64) { + $p .= '&'; + $base64 = 1; + $b = 0; + $k = 10; + } + + if ($ch & ~0xffff) { + $ch = 0xfffe; + } + + $p .= $B64Chars[($b | $ch >> $k)]; + $k -= 6; + + for (; $k >= 0; $k -= 6) { + $p .= $B64Chars[(($ch >> $k) & 0x3f)]; + } + + $b = ($ch << (-$k)) & 0x3f; + $k += 16; + } + else { + if ($base64) { + if ($k > 10) { + $p .= $B64Chars[$b]; + } + $p .= '-'; + $base64 = 0; + } + + $p .= \chr($ch); + + if ('&' == \chr($ch)) { + $p .= '-'; + } + } + } + + if ($base64) { + if ($k > 10) { + $p .= $B64Chars[$b]; + } + $p .= '-'; + } + + return $p; + } +} diff --git a/src/Imap2/Roundcube/ImapClient.php b/src/Imap2/Roundcube/ImapClient.php new file mode 100644 index 0000000..18a0624 --- /dev/null +++ b/src/Imap2/Roundcube/ImapClient.php @@ -0,0 +1,4458 @@ + | + * | Author: Ryo Chijiiwa | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +/** + * PHP based wrapper class to connect to an IMAP server + * + * @package Framework + * @subpackage Storage + */ +class ImapClient +{ + public const int COMMAND_ANONYMIZED = 8; + + public const int COMMAND_CAPABILITY = 2; + + public const int COMMAND_LASTLINE = 4; + + public const int COMMAND_NORESPONSE = 1; + + public const int COMMAND_RAW_LASTLINE = 16; + + public const int DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n + + public const int ERROR_BAD = -2; + + public const int ERROR_BYE = -3; + + public const int ERROR_COMMAND = -5; + + public const int ERROR_NO = -1; + + public const int ERROR_OK = 0; + + public const int ERROR_READONLY = -6; + + public const int ERROR_UNKNOWN = -4; + + public array $data = []; + + public string $error = ''; + + public int $errornum = self::ERROR_OK; + + public array $extensions_enabled = []; + + public array $flags = [ + 'SEEN' => '\\Seen', + 'DELETED' => '\\Deleted', + 'ANSWERED' => '\\Answered', + 'DRAFT' => '\\Draft', + 'FLAGGED' => '\\Flagged', + 'FORWARDED' => '$Forwarded', + 'MDNSENT' => '$MDNSent', + '*' => '\\*', + ]; + + public string $result = ''; + + public string $resultcode = ''; + + public string $selected = ''; + + protected array $capability = []; + + protected bool $capability_readed = false; + + protected int $cmd_num = 0; + + protected string $cmd_tag = ''; + + protected bool $debug = false; + + protected $debug_handler = null; + + protected $fp = null; + + protected string $host = ''; + + protected bool $logged = false; + + protected array $prefs = []; + + protected string $rawLastLine; + + protected string $resourceid = ''; + + protected string $user = ''; + + /** + * Handler for IMAP APPEND command + * + * @param string $mailbox Mailbox name + * @param string|array $message The message source string or array (of strings and file pointers) + * @param array $flags Message flags + * @param string $date Message internal date + * @param bool $binary Enable BINARY append (RFC3516) + * + * @return string|bool On success APPENDUID response (if available) or True, False on failure + */ + public function append(?string $mailbox, string|array &$message, array $flags = [], ?string $date = null, bool $binary = false) : string | bool + { + unset($this->data['APPENDUID']); + + if (empty($mailbox)) { + return false; + } + + $binary = $binary && $this->getCapability('BINARY'); + $literal_plus = ! $binary && isset($this->prefs['literal+']) && $this->prefs['literal+']; + $len = 0; + $msg = \is_array($message) ? $message : [&$message]; + $chunk_size = 512000; + + for ($i = 0, $cnt = \count($msg); $i < $cnt; $i++) { + if (\is_resource($msg[$i])) { + $stat = \fstat($msg[$i]); + + if (false === $stat) { + return false; + } + $len += $stat['size']; + } + else { + if (! $binary) { + $msg[$i] = \str_replace("\r", '', $msg[$i]); + $msg[$i] = \str_replace("\n", "\r\n", $msg[$i]); + } + + $len += \strlen($msg[$i]); + } + } + + if (! $len) { + return false; + } + + // build APPEND command + $key = $this->nextTag(); + $request = "{$key} APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; + + if (! empty($date)) { + $request .= ' ' . $this->escape($date); + } + $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}'; + + // send APPEND command + if (! $this->putLine($request)) { + $this->setError(self::ERROR_COMMAND, 'Failed to send APPEND command'); + + return false; + } + + // Do not wait when LITERAL+ is supported + if (! $literal_plus) { + $line = $this->readReply(); + + if ('+' != $line[0]) { + $this->parseResult($line, 'APPEND: '); + + return false; + } + } + + foreach ($msg as $msg_part) { + // file pointer + if (\is_resource($msg_part)) { + \rewind($msg_part); + + while (! \feof($msg_part) && $this->fp) { + $buffer = \fread($msg_part, $chunk_size); + $this->putLine($buffer, false); + } + \fclose($msg_part); + } + // string + else { + $size = \strlen($msg_part); + + // Break up the data by sending one chunk (up to 512k) at a time. + // This approach reduces our peak memory usage + for ($offset = 0; $offset < $size; $offset += $chunk_size) { + $chunk = \substr($msg_part, $offset, $chunk_size); + + if (! $this->putLine($chunk, false)) { + return false; + } + } + } + } + + if (! $this->putLine('')) { // \r\n + return false; + } + + do { + $line = $this->readLine(); + } while (! $this->startsWith($line, $key, true, true)); + + // Clear internal status cache + unset($this->data['STATUS:' . $mailbox]); + + if (self::ERROR_OK != $this->parseResult($line, 'APPEND: ')) { + return false; + } + + if (! empty($this->data['APPENDUID'])) { + return $this->data['APPENDUID']; + } + + return true; + } + + /** + * Handler for IMAP APPEND command. + * + * @param string $mailbox Mailbox name + * @param string $path Path to the file with message body + * @param string $headers Message headers + * @param array $flags Message flags + * @param string $date Message internal date + * @param bool $binary Enable BINARY append (RFC3516) + * + * @return string|bool On success APPENDUID response (if available) or True, False on failure + */ + public function appendFromFile(string $mailbox, string $path, ?string $headers = null, array $flags = [], ?string $date = null, bool $binary = false) : string | bool + { + // open message file + $fp = null; + + if (\file_exists(\realpath($path))) { + $fp = \fopen($path, 'r'); + } + + if (! $fp) { + $this->setError(self::ERROR_UNKNOWN, "Couldn't open {$path} for reading"); + + return false; + } + + $message = []; + + if ($headers) { + $message[] = \trim($headers, "\r\n") . "\r\n\r\n"; + } + $message[] = $fp; + + return $this->append($mailbox, $message, $flags, $date, $binary); + } + + /** + * Clears detected server capabilities + */ + public function clearCapability() : void + { + $this->capability = []; + $this->capability_readed = false; + } + + /** + * Removes all messages in a folder + * + * @param string $mailbox Mailbox name + * + * @return bool True on success, False on error + */ + public function clearFolder(string $mailbox) : bool + { + $res = null; + + if ($this->countMessages($mailbox) > 0) + { + $res = $this->flag($mailbox, '1:*', 'DELETED'); + } + + if ($res) + { + if ($this->selected === $mailbox) + { + $res = $this->close(); + } + else + { + $res = $this->expunge($mailbox); + } + } + + return $res; + } + + /** + * Executes CLOSE command + */ + public function close() : bool + { + $result = $this->execute('CLOSE', [], self::COMMAND_NORESPONSE); + + if (self::ERROR_OK == $result) + { + $this->selected = ''; + + return true; + } + + return false; + } + + /** + * Closes connection with logout. + */ + public function closeConnection() : void + { + if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) + { + $this->readReply(); + } + + $this->closeSocket(); + } + + /** + * Converts message identifiers array into sequence-set syntax + * + * @param array $messages Message identifiers + * @param bool $force Forces compression of any size + * + * @return string Compressed sequence-set + */ + public static function compressMessageSet(string|array $messages, bool $force = false) : string + { + // given a comma delimited list of independent mid's, + // compresses by grouping sequences together + if (! \is_array($messages)) + { + // if less than 255 bytes long, let's not bother + if (! $force && \strlen($messages) < 255) + { + return \preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages; + } + + // see if it's already been compressed + if (false !== \strpos($messages, ':')) + { + return \preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages; + } + + // separate, then sort + $messages = \explode(',', $messages); + } + + \sort($messages); + + $result = []; + $start = $prev = $messages[0]; + + foreach ($messages as $id) + { + $incr = $id - $prev; + + if ($incr > 1) + { // found a gap + if ($start == $prev) + { + $result[] = $prev; // push single id + } + else + { + $result[] = $start . ':' . $prev; // push sequence as start_id:end_id + } + $start = $id; // start of new sequence + } + $prev = $id; + } + + // handle the last sequence/id + if ($start == $prev) + { + $result[] = $prev; + } + else + { + $result[] = $start . ':' . $prev; + } + + // return as comma separated string + $result = \implode(',', $result); + + return \preg_match('/[^0-9:,*]/', $result) ? 'INVALID' : $result; + } + + /** + * Connects to IMAP server and authenticates. + * + * @param string $host Server hostname or IP + * @param string $user User name + * @param string $password Password + * @param array $options Connection and class options + */ + public function connect(string $host, string $user, string $password, array $options = []) : bool + { + // configure + $this->set_prefs($options); + + $this->host = $host; + $this->user = $user; + $this->logged = false; + $this->selected = ''; + + // check input + if (empty($host)) + { + $this->setError(self::ERROR_BAD, 'Empty host'); + + return false; + } + + if (empty($user)) + { + $this->setError(self::ERROR_NO, 'Empty user'); + + return false; + } + + if (empty($password) && empty($options['gssapi_cn'])) + { + $this->setError(self::ERROR_NO, 'Empty password'); + + return false; + } + + // Connect + if (! $this->_connect($host)) + { + return false; + } + + // Send ID info + if (! empty($this->prefs['ident']) && $this->getCapability('ID')) + { + $this->data['ID'] = $this->id($this->prefs['ident']); + } + + $auth_method = $this->prefs['auth_type']; + + // Switch to XOAUTH2 if password is a JWT token + $token = \json_decode(\base64_decode(\str_replace('_', '/', \str_replace('-', '+', @\explode('.', $password)[0]))), true); + + if (\is_array($token) && isset($token['typ']) && 'JWT' == $token['typ']) + { + $auth_method = 'XOAUTH2'; + } + + $auth_methods = []; + $result = null; + + // check for supported auth methods + if (! $auth_method || 'CHECK' == $auth_method) + { + if ($auth_caps = $this->getCapability('AUTH')) + { + $auth_methods = $auth_caps; + } + + // Use best (for security) supported authentication method + $all_methods = ['DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN', 'XOAUTH2']; + + if (! empty($this->prefs['gssapi_cn'])) + { + \array_unshift($all_methods, 'GSSAPI'); + } + + foreach ($all_methods as $auth_method) + { + if (\in_array($auth_method, $auth_methods)) + { + break; + } + } + + // Prefer LOGIN over AUTHENTICATE LOGIN for performance reasons + if ('LOGIN' == $auth_method && ! $this->getCapability('LOGINDISABLED')) + { + $auth_method = 'IMAP'; + } + } + + // pre-login capabilities can be not complete + $this->capability_readed = false; + + // Authenticate + switch ($auth_method) + { + case 'CRAM_MD5': + $auth_method = 'CRAM-MD5'; + + // Intentionally fall through + case 'CRAM-MD5': + case 'DIGEST-MD5': + case 'GSSAPI': + case 'PLAIN': + case 'LOGIN': + case 'XOAUTH2': + $result = $this->authenticate($user, $password, $auth_method); + + break; + + case 'IMAP': + $result = $this->login($user, $password); + + break; + + default: + $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: {$auth_method}"); + } + + // Connected and authenticated + if (\is_resource($result)) + { + if ($this->prefs['force_caps']) + { + $this->clearCapability(); + } + $this->logged = true; + + return true; + } + + $this->closeConnection(); + + return false; + } + + /** + * Checks connection status + * + * @return bool True if connection is active and user is logged in, False otherwise. + */ + public function connected() : bool + { + return $this->fp && $this->logged; + } + + /** + * Copies message(s) from one folder to another + * + * @param string|array $messages Message UID(s) + * @param string $from Mailbox name + * @param string $to Destination mailbox name + */ + public function copy(string|array $messages, string $from, string $to) : bool + { + // Clear last COPYUID data + unset($this->data['COPYUID']); + + if (! $this->select($from)) + { + return false; + } + + // Clear internal status cache + unset($this->data['STATUS:' . $to]); + + $result = $this->execute( + 'UID COPY', + [ + $this->compressMessageSet($messages), $this->escape($to)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Returns count of all messages in a folder + * + * @param string $mailbox Mailbox name + * + * @return int Number of messages, False on error + */ + public function countMessages(string $mailbox) : int | false + { + if ($this->selected === $mailbox && isset($this->data['EXISTS'])) + { + return $this->data['EXISTS']; + } + + // Check internal cache + $cache = $this->data['STATUS:' . $mailbox]; + + if (! empty($cache) && isset($cache['MESSAGES'])) + { + return (int)$cache['MESSAGES']; + } + + // Try STATUS (should be faster than SELECT) + $counts = $this->status($mailbox); + + return (int)$counts['MESSAGES']; + } + + /** + * Returns count of messages with \Recent flag in a folder + * + * @param string $mailbox Mailbox name + * + * @return int Number of messages, False on error + */ + public function countRecent(string $mailbox) : int | false + { + if ($this->selected === $mailbox && isset($this->data['RECENT'])) + { + return $this->data['RECENT']; + } + + // Check internal cache + $cache = $this->data['STATUS:' . $mailbox]; + + if (! empty($cache) && isset($cache['RECENT'])) + { + return (int)$cache['RECENT']; + } + + // Try STATUS (should be faster than SELECT) + $counts = $this->status($mailbox, ['RECENT']); + + return (int)$counts['RECENT']; + } + + /** + * Returns count of messages without \Seen flag in a specified folder + * + * @param string $mailbox Mailbox name + * + * @return int Number of messages, False on error + */ + public function countUnseen(string $mailbox) : int | false + { + // Check internal cache + $cache = $this->data['STATUS:' . $mailbox]; + + if (! empty($cache) && isset($cache['UNSEEN'])) + { + return (int)$cache['UNSEEN']; + } + + // Try STATUS (should be faster than SELECT+SEARCH) + $counts = $this->status($mailbox); + + if (\is_array($counts)) + { + return (int)$counts['UNSEEN']; + } + + // Invoke SEARCH as a fallback + $index = $this->search($mailbox, 'ALL UNSEEN', false, ['COUNT']); + + if (! $index->is_error()) + { + return $index->count(); + } + + return false; + } + + /** + * Folder creation (CREATE) + * + * @param string $mailbox Mailbox name + * @param array $types Optional folder types (RFC 6154) + */ + public function createFolder(string $mailbox, ?array $types = null) : bool + { + $args = [$this->escape($mailbox)]; + + // RFC 6154: CREATE-SPECIAL-USE + if (! empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) + { + $args[] = '(USE (' . \implode(' ', $types) . '))'; + } + + $result = $this->execute('CREATE', $args, self::COMMAND_RAW_LASTLINE); + + $this->rawLastLine = $result[1]; + + return self::ERROR_OK == $result[0]; + } + + /** + * Send the DELETEACL command (RFC4314) + * + * @param string $mailbox Mailbox name + * @param string $user User name + */ + public function deleteACL(string $mailbox, string $user) : bool + { + $result = $this->execute( + 'DELETEACL', + [ + $this->escape($mailbox), $this->escape($user)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Executes DELETE command + * + * @param string $mailbox Mailbox name + */ + public function deleteFolder(string $mailbox) : bool + { + $result = $this->execute( + 'DELETE', + [$this->escape($mailbox)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Send the SETMETADATA command with NIL values (RFC5464) + * + * @param string $mailbox Mailbox name + * @param array $entries Entry names array + */ + public function deleteMetadata(string $mailbox, array | string $entries) : bool + { + if (! \is_array($entries) && ! empty($entries)) + { + $entries = \explode(' ', $entries); + } + + if (empty($entries)) + { + $this->setError(self::ERROR_COMMAND, 'Wrong argument for SETMETADATA command'); + + return false; + } + + foreach ($entries as $entry) + { + $data[$entry] = null; + } + + return $this->setMetadata($mailbox, $data); + } + + /** + * Executes ENABLE command (RFC5161) + * + * @param mixed $extension Extension name to enable (or array of names) + * + * @return array|bool List of enabled extensions, False on error + */ + public function enable($extension) : array | bool + { + if (empty($extension)) + { + return false; + } + + if (! $this->hasCapability('ENABLE')) + { + return false; + } + + if (! \is_array($extension)) + { + $extension = [$extension]; + } + + if (! empty($this->extensions_enabled)) + { + // check if all extensions are already enabled + $diff = \array_diff($extension, $this->extensions_enabled); + + if (empty($diff)) + { + return $extension; + } + + // Make sure the mailbox isn't selected, before enabling extension(s) + if ($this->selected) + { + $this->close(); + } + } + + [$code, $response] = $this->execute('ENABLE', $extension, 0, '/^\* ENABLED /i'); + + if (self::ERROR_OK == $code && $response) + { + $response = \substr($response, 10); // remove prefix "* ENABLED " + $result = (array)$this->tokenizeResponse($response); + + $this->extensions_enabled = \array_unique(\array_merge((array)$this->extensions_enabled, $result)); + + return $this->extensions_enabled; + } + + return false; + } + + /** + * Escapes a string when it contains special characters (RFC3501) + * + * @param ?string $string IMAP string + * @param bool $force_quotes Forces string quoting (for atoms) + * + * @return string String atom, quoted-string or string literal + */ + public static function escape(?string $string, bool $force_quotes = false) : string + { + if (null === $string) { + return 'NIL'; + } + + if ('' === $string) { + return '""'; + } + + // atom-string (only safe characters) + if (! $force_quotes && ! \preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) { + return $string; + } + + // quoted-string + if (! \preg_match('/[\r\n\x00\x80-\xFF]/', $string)) { + return '"' . \addcslashes($string, '\\"') . '"'; + } + + // literal-string + return \sprintf("{%d}\r\n%s", \strlen($string), $string); + } + + /** + * Sends IMAP command and parses result + * + * @param string $command IMAP command + * @param array $arguments Command arguments + * @param int $options Execution options + * @param string $filter Line filter (regexp) + * + * @return mixed Response code or list of response code and data + */ + public function execute(string $command, ?array $arguments = [], int $options = 0, ?string $filter = null) + { + $tag = $this->nextTag(); + $query = $tag . ' ' . $command; + $noresp = ($options & self::COMMAND_NORESPONSE); + $response = $noresp ? null : ''; + + if (! empty($arguments)) { + foreach ($arguments as $arg) { + $query .= ' ' . self::r_implode($arg); + } + } + + // Send command + if (! $this->putLineC($query, true, (bool)($options & self::COMMAND_ANONYMIZED))) { + \preg_match('/^[A-Z0-9]+ ((UID )?[A-Z]+)/', $query, $matches); + $cmd = $matches[1] ?: 'UNKNOWN'; + $this->setError(self::ERROR_COMMAND, "Failed to send {$cmd} command"); + + return $noresp ? self::ERROR_COMMAND : [self::ERROR_COMMAND, '']; + } + + // Parse response + do { + $line = $this->readFullLine(4096); + + if (null !== $response) { + if (! $filter || \preg_match($filter, $line)) { + $response .= $line; + } + } + + // parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851) + if ($line && 'UID MOVE' == $command) { + if (\preg_match("/^\* OK \[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $line, $m)) { + $this->data['COPYUID'] = [$m[1], $m[2]]; + } + } + } + while (! $this->startsWith($line, $tag . ' ', true, true)); + + $code = $this->parseResult($line, $command . ': '); + + // Remove last line from response + if ($response) { + if (! $filter) { + $line_len = \min(\strlen($response), \strlen($line)); + $response = \substr($response, 0, -$line_len); + } + + $response = \rtrim($response, "\r\n"); + } + + // optional CAPABILITY response + if (($options & self::COMMAND_CAPABILITY) && self::ERROR_OK == $code + && \preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches) + ) { + $this->parseCapability($matches[1], true); + } + + // return last line only (without command tag, result and response code) + if ($line && ($options & self::COMMAND_LASTLINE)) { + $response = \preg_replace("/^{$tag} (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', \trim($line)); + } + + // return raw last line only (without command tag, result and response code) + if ($line && ($options & self::COMMAND_RAW_LASTLINE)) { + $response = \preg_replace("/^{$tag} (OK|NO|BAD|BYE|PREAUTH)?\s*/i", '', \trim($line)); + } + + return $noresp ? $code : [$code, $response]; + } + + /** + * Executes EXPUNGE command + * + * @param string $mailbox Mailbox name + * @param string|array $messages Message UIDs to expunge + */ + public function expunge(string $mailbox, string | array | null $messages = null) : bool + { + if (! $this->select($mailbox)) { + return false; + } + + if (! $this->data['READ-WRITE']) { + $this->setError(self::ERROR_READONLY, 'Mailbox is read-only'); + + return false; + } + + // Clear internal status cache + $this->clear_status_cache($mailbox); + + if (! empty($messages) && '*' != $messages && $this->hasCapability('UIDPLUS')) { + $messages = self::compressMessageSet($messages); + $result = $this->execute('UID EXPUNGE', [$messages], self::COMMAND_NORESPONSE); + } + else { + $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE); + } + + if (self::ERROR_OK == $result) { + $this->selected = ''; // state has changed, need to reselect + + return true; + } + + return false; + } + + /** + * FETCH command (RFC3501) + * + * @param string $mailbox Mailbox name + * @param array|string $message_set Message(s) sequence identifier(s) or UID(s) + * @param bool $is_uid True if $message_set contains UIDs + * @param array $query_items FETCH command data items + * @param ?string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query + * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query + * + * @return array List of rcube_message_header elements, False on error + */ + public function fetch( + string $mailbox, + array | string $message_set, + bool $is_uid = false, + array $query_items = [], + ?string $mod_seq = null, + bool $vanished = false + ) : array | false + { + if (! $this->select($mailbox)) { + return false; + } + + $message_set = $this->compressMessageSet($message_set); + $result = []; + + $key = $this->nextTag(); + $cmd = ($is_uid ? 'UID ' : '') . 'FETCH'; + $request = "{$key} {$cmd} {$message_set} (" . \implode(' ', $query_items) . ')'; + + if (null !== $mod_seq && $this->hasCapability('CONDSTORE')) { + $request .= " (CHANGEDSINCE {$mod_seq}" . ($vanished ? ' VANISHED' : '') . ')'; + } + + if (! $this->putLine($request)) { + $this->setError(self::ERROR_COMMAND, "Failed to send {$cmd} command"); + + return false; + } + + do { + $line = $this->readFullLine(4096); + + if (! $line) { + break; + } + + // Sample reply line: + // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen) + // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...) + // BODY[HEADER.FIELDS ... + + if (\preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { + + if ($is_uid && \preg_match('/UID ([0-9]+)/', $line, $m2)) { + $id = (int)($m2[1]); + } else { + $id = (int)($m[1]); + } + + $message_id = (int)($m[1]); + + $result[$id] = new MessageHeader(); + $result[$id]->id = $message_id; + $result[$id]->subject = ''; + $result[$id]->messageID = 'mid:' . $message_id; + + $headers = null; + $lines = []; + $line = \substr($line, \strlen($m[0]) + 2); + $ln = 0; + + // Tokenize response and assign to object properties + $tokens = []; + + while ($tokens = $this->tokenizeResponse($line, 2)) { + $name = $tokens[0] ?? ''; + $value = $tokens[1] ?? ''; + + if ('UID' == $name) { + $result[$id]->uid = (int)$value; + } + elseif ('RFC822.SIZE' == $name) { + $result[$id]->size = (int)$value; + } + elseif ('RFC822.TEXT' == $name) { + $result[$id]->body = $value; + } + elseif ('INTERNALDATE' == $name) { + $result[$id]->internaldate = $value; + $result[$id]->date = $value; + $result[$id]->timestamp = Utils::strtotime($value); + } + elseif ('FLAGS' == $name) { + if (! empty($value)) { + foreach ((array)$value as $flag) { + $flag = \str_replace(['$', '\\'], '', $flag); + $flag = \strtoupper($flag); + + $result[$id]->flags[$flag] = true; + } + } + } + elseif ('MODSEQ' == $name) { + $result[$id]->modseq = $value[0]; + } + elseif ('ENVELOPE' == $name) { + $result[$id]->envelope = $value; + } + elseif ('BODYSTRUCTURE' == $name || ('BODY' == $name && \count($value) > 2)) { + if (! \is_array($value[0]) && ('message' == \strtolower($value[0]) && 'rfc822' == \strtolower($value[1]))) { + $value = [$value]; + } + $result[$id]->bodystructure = $value; + } + elseif ('RFC822' == $name) { + $result[$id]->body = $value; + } + elseif (0 === \stripos($name, 'BODY[')) { + $name = \str_replace(']', '', \substr($name, 5)); + + if ('HEADER.FIELDS' == $name) { + // skip ']' after headers list + $this->tokenizeResponse($line, 1); + $headers = $this->tokenizeResponse($line, 1); + } + elseif (\strlen($name)) { + $result[$id]->bodypart[$name] = $value; + } + else { + $result[$id]->body = $value; + } + } + } + + // create array with header field:data + if (! empty($headers)) { + $headers = \explode("\n", \trim($headers)); + + foreach ($headers as $resln) { + if (\ord($resln[0]) <= 32) { + $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . \trim($resln); + } + else { + $lines[++$ln] = \trim($resln); + } + } + + foreach ($lines as $str) { + [$field, $string] = \explode(':', $str, 2); + + $field = \strtolower($field); + $string = \preg_replace('/\n[\t\s]*/', ' ', \trim($string)); + + switch ($field) { + case 'date': + $string = \substr($string, 0, 128); + $result[$id]->date = $string; + $result[$id]->timestamp = Utils::strtotime($string); + + break; + + case 'to': + $result[$id]->to = \preg_replace('/undisclosed-recipients:[;,]*/', '', $string); + + break; + + case 'from': + case 'subject': + $string = \substr($string, 0, 2048); + + // Intentionally fall through + case 'cc': + case 'bcc': + case 'references': + $result[$id]->{$field} = $string; + + break; + + case 'reply-to': + $result[$id]->replyto = $string; + + break; + + case 'content-transfer-encoding': + $result[$id]->encoding = \substr($string, 0, 32); + + break; + + case 'content-type': + $ctype_parts = \preg_split('/[; ]+/', $string); + $result[$id]->ctype = \strtolower(\array_shift($ctype_parts)); + + if (\preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) { + $result[$id]->charset = $regs[1]; + } + + break; + + case 'in-reply-to': + $result[$id]->in_reply_to = \str_replace(["\n", '<', '>'], '', $string); + + break; + + case 'return-receipt-to': + case 'disposition-notification-to': + case 'x-confirm-reading-to': + $result[$id]->mdn_to = \substr($string, 0, 2048); + + break; + + case 'message-id': + $result[$id]->messageID = \substr($string, 0, 2048); + + break; + + case 'x-priority': + if (\preg_match('/^(\d+)/', $string, $matches)) { + $result[$id]->priority = (int)($matches[1]); + } + + break; + + default: + if (\strlen($field) < 3) { + break; + } + + if ($result[$id]->others[$field]) { + $string = \array_merge((array)$result[$id]->others[$field], (array)$string); + } + $result[$id]->others[$field] = $string; + } + } + } + } + // VANISHED response (QRESYNC RFC5162) + // Sample: * VANISHED (EARLIER) 300:310,405,411 + elseif (\preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { + $line = \substr($line, \strlen($match[0])); + $v_data = $this->tokenizeResponse($line, 1); + + $this->data['VANISHED'] = $v_data; + } + } + while (! $this->startsWith($line, $key, true)); + + return $result; + } + + /** + * Returns message data (flags, headers, etc.) + * + * @param string $mailbox Mailbox name + * @param int $id Message sequence identifier or UID + * @param bool $is_uid True if $id is an UID + * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result + * @param array $add_headers List of additional headers + * + * @return \PHPFUI\Imap2\Roundcube\MessageHeader Message data, False on error + */ + public function fetchHeader(string $mailbox, int $id, bool $is_uid = false, bool $bodystr = false, array $add_headers = []) : bool | \PHPFUI\Imap2\Roundcube\MessageHeader + { + $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); + + if (\is_array($a)) { + return \array_shift($a); + } + + return false; + } + + /** + * Fetches specified header/data value for a set of messages. + * + * @param string $mailbox Mailbox name + * @param string|array $message_set Searching criteria (list of messages to return) + * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) + * @param bool $skip_deleted Makes that DELETED messages will be skipped + * @param bool $uidfetch Enables UID FETCH usage + * @param bool $return_uid Enables returning UIDs instead of IDs + * + * @return array|bool List of header values or False on failure + */ + public function fetchHeaderIndex( + string $mailbox, + array | string $message_set, + string $index_field = '', + bool $skip_deleted = true, + bool $uidfetch = false, + bool $return_uid = false + ) + { + if (\is_array($message_set)) { + if (! ($message_set = $this->compressMessageSet($message_set))) { + return false; + } + } + else { + [$from_idx, $to_idx] = \explode(':', $message_set); + + if (empty($message_set) || + ('*' != $to_idx && (int)$from_idx > (int)$to_idx) + ) { + return false; + } + } + + $index_field = empty($index_field) ? 'DATE' : \strtoupper($index_field); + + $fields_a['DATE'] = 1; + $fields_a['INTERNALDATE'] = 4; + $fields_a['ARRIVAL'] = 4; + $fields_a['FROM'] = 1; + $fields_a['REPLY-TO'] = 1; + $fields_a['SENDER'] = 1; + $fields_a['TO'] = 1; + $fields_a['CC'] = 1; + $fields_a['SUBJECT'] = 1; + $fields_a['UID'] = 2; + $fields_a['SIZE'] = 2; + $fields_a['SEEN'] = 3; + $fields_a['RECENT'] = 3; + $fields_a['DELETED'] = 3; + + if (! ($mode = $fields_a[$index_field])) { + return false; + } + + // Select the mailbox + if (! $this->select($mailbox)) { + return false; + } + + // build FETCH command string + $key = $this->nextTag(); + $cmd = $uidfetch ? 'UID FETCH' : 'FETCH'; + $fields = []; + + if ($return_uid) { + $fields[] = 'UID'; + } + + if ($skip_deleted) { + $fields[] = 'FLAGS'; + } + + if (1 == $mode) { + if ('DATE' == $index_field) { + $fields[] = 'INTERNALDATE'; + } + $fields[] = "BODY.PEEK[HEADER.FIELDS ({$index_field})]"; + } + elseif (2 == $mode) { + if ('SIZE' == $index_field) { + $fields[] = 'RFC822.SIZE'; + } + elseif (! $return_uid || 'UID' != $index_field) { + $fields[] = $index_field; + } + } + elseif (3 == $mode && ! $skip_deleted) { + $fields[] = 'FLAGS'; + } + elseif (4 == $mode) { + $fields[] = 'INTERNALDATE'; + } + + $request = "{$key} {$cmd} {$message_set} (" . \implode(' ', $fields) . ')'; + + if (! $this->putLine($request)) { + $this->setError(self::ERROR_COMMAND, "Failed to send {$cmd} command"); + + return false; + } + + $result = []; + + do { + $line = \rtrim($this->readLine(200)); + $line = $this->multLine($line); + + if (\preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { + $id = $m[1]; + $flags = null; + + if ($return_uid) { + if (\preg_match('/UID ([0-9]+)/', $line, $matches)) { + $id = (int)$matches[1]; + } + else { + continue; + } + } + + if ($skip_deleted && \preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { + $flags = \explode(' ', \strtoupper($matches[1])); + + if (\in_array('\\DELETED', $flags)) { + continue; + } + } + + if (1 == $mode && 'DATE' == $index_field) { + if (\preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) { + $value = \preg_replace(['/^"*[a-z]+:/i'], '', $matches[1]); + $value = \trim($value); + $result[$id] = Utils::strtotime($value); + } + + // non-existent/empty Date: header, use INTERNALDATE + if (empty($result[$id])) { + if (\preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { + $result[$id] = Utils::strtotime($matches[1]); + } + else { + $result[$id] = 0; + } + } + } + elseif (1 == $mode) { + if (\preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) { + $value = \preg_replace(['/^"*[a-z]+:/i', '/\s+$/sm'], ['', ''], $matches[2]); + $result[$id] = \trim($value); + } + else { + $result[$id] = ''; + } + } + elseif (2 == $mode) { + if (\preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) { + $result[$id] = \trim($matches[1]); + } + else { + $result[$id] = 0; + } + } + elseif (3 == $mode) { + if (! $flags && \preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { + $flags = \explode(' ', $matches[1]); + } + $result[$id] = \in_array('\\' . $index_field, (array)$flags) ? 1 : 0; + } + elseif (4 == $mode) { + if (\preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { + $result[$id] = Utils::strtotime($matches[1]); + } + else { + $result[$id] = 0; + } + } + } + } + while (! $this->startsWith($line, $key, true, true)); + + return $result; + } + + /** + * Returns message(s) data (flags, headers, etc.) + * + * @param string $mailbox Mailbox name + * @param string | array $message_set Message(s) sequence identifier(s) or UID(s) + * @param bool $is_uid True if $message_set contains UIDs + * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result + * @param array $add_headers List of additional headers + * + * @return bool|array<\PHPFUI\Imap2\Roundcube\MessageHeader> List of rcube_message_header elements, False on error + */ + public function fetchHeaders(string $mailbox, int | string | array $message_set, bool $is_uid = false, bool $bodystr = false, array $add_headers = []) : array | bool + { + $query_items = ['UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE']; + $headers = ['DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', + 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY']; + + if (! empty($add_headers)) { + $add_headers = \array_map('strtoupper', $add_headers); + $headers = \array_unique(\array_merge($headers, $add_headers)); + } + + if ($bodystr) { + $query_items[] = 'BODYSTRUCTURE'; + } + + $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . \implode(' ', $headers) . ')]'; + + return $this->fetch($mailbox, $message_set, $is_uid, $query_items); + } + + /** + * Fetch MIME headers of specified message parts + * + * @param string $mailbox Mailbox name + * @param int $uid Message UID + * @param array $parts Message part identifiers + * @param bool $mime Use MIME instad of HEADER + * + * @return array|bool Array containing headers string for each specified body + * False on failure. + */ + public function fetchMIMEHeaders(string $mailbox, int $uid, array $parts, bool $mime = true) : array | bool + { + if (! $this->select($mailbox)) { + return false; + } + + $result = false; + $parts = (array)$parts; + $key = $this->nextTag(); + $peeks = []; + $type = $mime ? 'MIME' : 'HEADER'; + + // format request + foreach ($parts as $part) { + $peeks[] = "BODY.PEEK[{$part}.{$type}]"; + } + + $request = "{$key} UID FETCH {$uid} (" . \implode(' ', $peeks) . ')'; + + // send request + if (! $this->putLine($request)) { + $this->setError(self::ERROR_COMMAND, 'Failed to send UID FETCH command'); + + return false; + } + + do { + $line = $this->readLine(1024); + + if (\preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+/', $line, $m)) { + $line = \ltrim(\substr($line, \strlen($m[0]))); + + while (\preg_match('/^BODY\[([0-9\.]+)\.' . $type . '\]/', $line, $matches)) { + $line = \substr($line, \strlen($matches[0])); + $result[$matches[1]] = \trim($this->multLine($line)); + $line = $this->readLine(1024); + } + } + } + while (! $this->startsWith($line, $key, true)); + + return $result; + } + + /** + * Fetches message part header + */ + public function fetchPartHeader(string $mailbox, int $id, bool $is_uid = false, ?string $part = null) + { + $part = empty($part) ? 'HEADER' : $part . '.MIME'; + + return $this->handlePartBody($mailbox, $id, $is_uid, $part); + } + + /** + * Sets flag of the message(s) + * + * @param string $mailbox Mailbox name + * @param string|array $messages Message UID(s) + * @param string $flag Flag name + */ + public function flag(string $mailbox, string | array $messages, string $flag) : bool + { + return $this->modFlag($mailbox, $messages, $flag, '+'); + } + + /** + * Send the GETACL command (RFC4314) + * + * @param string $mailbox Mailbox name + * + * @return array User-rights array on success, NULL on error + */ + public function getACL(string $mailbox) : array + { + [$code, $response] = $this->execute('GETACL', [$this->escape($mailbox)], 0, '/^\* ACL /i'); + + if (self::ERROR_OK == $code && $response) { + // Parse server response (remove "* ACL ") + $response = \substr($response, 6); + $ret = $this->tokenizeResponse($response); + $mbox = \array_shift($ret); + $size = \count($ret); + + // Create user-rights hash array + // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1 + // so we could return only standard rights defined in RFC4314, + // excluding 'c' and 'd' defined in RFC2086. + if (0 == $size % 2) { + for ($i = 0; $i < $size; $i++) { + $ret[$ret[$i]] = \str_split($ret[++$i]); + unset($ret[$i - 1], $ret[$i]); + + } + + return $ret; + } + + $this->setError(self::ERROR_COMMAND, 'Incomplete ACL response'); + } + + return []; + } + + /** + * Send the GETANNOTATION command (draft-daboo-imap-annotatemore) + * + * @param string $mailbox Mailbox name + * @param array $entries Entries names + * @param array $attribs Attribs names + * + * @return array Annotations result on success, NULL on error + */ + public function getAnnotation(string $mailbox, string | array $entries, string | array $attribs) : ?array + { + $result = null; + + if (! \is_array($entries)) { + $entries = [$entries]; + } + + $last_entry = ''; + + // create entries string + // ANNOTATEMORE drafts before version 08 require quoted parameters + foreach ($entries as $idx => $name) { + $entries[$idx] = $this->escape($name, true); + } + $entries = '(' . \implode(' ', $entries) . ')'; + + if (! \is_array($attribs)) { + $attribs = [$attribs]; + } + + // create attributes string + foreach ($attribs as $idx => $name) { + $attribs[$idx] = $this->escape($name, true); + } + $attribs = '(' . \implode(' ', $attribs) . ')'; + + [$code, $response] = $this->execute('GETANNOTATION', [ + $this->escape($mailbox), $entries, $attribs]); + + if (self::ERROR_OK == $code) { + $data = $this->tokenizeResponse($response); + + // Here we returns only data compatible with METADATA result format + if (! empty($data) && ($size = \count($data))) { + for ($i = 0; $i < $size; $i++) { + $entry = $data[$i]; + + if (isset($mbox) && \is_array($entry)) { + $attribs = $entry; + $entry = $last_entry; + } + elseif ('*' == $entry) { + if ('ANNOTATION' == $data[$i + 1]) { + $mbox = $data[$i + 2]; + unset($data[$i], $data[++$i], $data[++$i]); // "*" + // "ANNOTATION" + // Mailbox + } + // get rid of other untagged responses + else { + unset($mbox, $data[$i]); + + } + + continue; + } + elseif (isset($mbox)) { + $attribs = $data[++$i]; + } + else { + unset($data[$i]); + + continue; + } + + if (! empty($attribs)) { + for ($x = 0, $len = \count($attribs); $x < $len;) { + $attr = $attribs[$x++]; + $value = $attribs[$x++]; + + if ('value.priv' == $attr && null !== $value) { + $result[$mbox]['/private' . $entry] = $value; + } + elseif ('value.shared' == $attr && null !== $value) { + $result[$mbox]['/shared' . $entry] = $value; + } + } + } + $last_entry = $entry; + unset($data[$i]); + } + } + + } + + return $result; + } + + /** + * Capabilities checker + * + * @param string $name Capability name + * + * @return mixed Capability values array for key=value pairs, true/false for others + */ + public function getCapability(string $name) + { + $result = $this->hasCapability($name); + + if (! empty($result)) { + return $result; + } + elseif ($this->capability_readed) { + return false; + } + + // get capabilities (only once) because initial + // optional CAPABILITY response may differ + $result = $this->execute('CAPABILITY'); + + if (self::ERROR_OK == $result[0]) { + $this->parseCapability($result[1]); + } + + $this->capability_readed = true; + + return $this->hasCapability($name); + } + + /** + * Detects hierarchy delimiter + * + * @return string The delimiter + */ + public function getHierarchyDelimiter() : string + { + if (isset($this->prefs['delimiter']) && $this->prefs['delimiter']) { + return $this->prefs['delimiter']; + } + + // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8) + [$code, $response] = $this->execute( + 'LIST', + [$this->escape(''), $this->escape('')] + ); + + if (self::ERROR_OK == $code) { + $args = $this->tokenizeResponse($response, 4); + $delimiter = $args[3]; + + if (\strlen($delimiter) > 0) { + return $this->prefs['delimiter'] = $delimiter; + } + } + + return ''; + } + + /** + * Send the GETMETADATA command (RFC5464) + * + * @param string $mailbox Mailbox name + * @param array $entries Entries + * @param array $options Command options (with MAXSIZE and DEPTH keys) + * + * @return array GETMETADATA result on success, NULL on error + */ + public function getMetadata(string $mailbox, string | array $entries, array $options = []) : array + { + $result = []; + + if (! \is_array($entries)) { + $entries = [$entries]; + } + + // create entries string + foreach ($entries as $idx => $name) { + $entries[$idx] = $this->escape($name); + } + + $optlist = ''; + $entlist = '(' . \implode(' ', $entries) . ')'; + + // create options string + if (\count($options)) { + $options = \array_change_key_case($options, CASE_UPPER); + $opts = []; + + if (! empty($options['MAXSIZE'])) { + $opts[] = 'MAXSIZE ' . (int)($options['MAXSIZE']); + } + + if (! empty($options['DEPTH'])) { + $opts[] = 'DEPTH ' . (int)($options['DEPTH']); + } + + if ($opts) { + $optlist = '(' . \implode(' ', $opts) . ')'; + } + } + + $optlist .= ($optlist ? ' ' : '') . $entlist; + + [$code, $response] = $this->execute('GETMETADATA', [ + $this->escape($mailbox), $optlist]); + + if (self::ERROR_OK == $code) { + $result = []; + $data = $this->tokenizeResponse($response); + + // The METADATA response can contain multiple entries in a single + // response or multiple responses for each entry or group of entries + for ($i = 0, $size = \count($data); $i < $size; $i++) { + if ('*' === $data[$i] + && 'METADATA' === $data[++$i] + && \is_string($mbox = $data[++$i]) + && \is_array($data[++$i]) + ) { + for ($x = 0, $size2 = \count($data[$i]); $x < $size2; $x += 2) { + if (null !== $data[$i][$x + 1]) { + $result[$mbox][$data[$i][$x]] = $data[$i][$x + 1]; + } + } + } + } + + } + + return $result; + } + + /** + * NAMESPACE handler (RFC 2342) + * + * @return array Namespace data hash (personal, other, shared) + */ + public function getNamespace() : array | int + { + $data = null; + + if (\array_key_exists('namespace', $this->prefs)) { + return $this->prefs['namespace']; + } + + if (! $this->getCapability('NAMESPACE')) { + return self::ERROR_BAD; + } + + [$code, $response] = $this->execute('NAMESPACE'); + + if (self::ERROR_OK == $code && \preg_match('/^\* NAMESPACE /', $response)) { + $response = \substr($response, 11); + $data = $this->tokenizeResponse($response); + } + + if (! \is_array($data)) { + return $code; + } + + $this->prefs['namespace'] = [ + 'personal' => $data[0], + 'other' => $data[1], + 'shared' => $data[2], + ]; + + return $this->prefs['namespace']; + } + + /** + * Returns QUOTA information + * + * @param string $mailbox Mailbox name + * + * @return array Quota information + */ + public function getQuota(?string $mailbox = null) : array + { + if (null === $mailbox || '' === $mailbox) { + $mailbox = 'INBOX'; + } + + // a0001 GETQUOTAROOT INBOX + // * QUOTAROOT INBOX user/sample + // * QUOTA user/sample (STORAGE 654 9765) + // a0001 OK Completed + + [$code, $response] = $this->execute('GETQUOTAROOT', [$this->escape($mailbox)], 0, '/^\* QUOTA /i'); + + $result = false; + $min_free = PHP_INT_MAX; + $all = []; + + if (self::ERROR_OK == $code) { + foreach (\explode("\n", $response) as $line) { + [, , $quota_root] = $this->tokenizeResponse($line, 3); + + $quotas = $this->tokenizeResponse($line, 1); + + if (empty($quotas)) { + continue; + } + + foreach (\array_chunk($quotas, 3) as $quota) { + [$type, $used, $total] = $quota; + $type = \strtolower($type); + + if ($type && $total) { + $all[$quota_root][$type]['used'] = (int)$used; + $all[$quota_root][$type]['total'] = (int)$total; + } + } + + if (empty($all[$quota_root]['storage'])) { + continue; + } + + $used = $all[$quota_root]['storage']['used']; + $total = $all[$quota_root]['storage']['total']; + $free = $total - $used; + + // calculate lowest available space from all storage quotas + if ($free < $min_free) { + $min_free = $free; + $result['used'] = $used; + $result['total'] = $total; + $result['percent'] = \min(100, \round(($used / \max(1, $total)) * 100)); + $result['free'] = 100 - $result['percent']; + } + } + } + + if (! empty($result)) { + $result['all'] = $all; + } + + return $result; + } + + public function getRawLastLine() + { + return $this->rawLastLine; + } + + /** + * Returns BODYSTRUCTURE for the specified message. + * + * @param string $mailbox Folder name + * @param int $id Message sequence number or UID + * @param bool $is_uid True if $id is an UID + * + * @return array|bool Body structure array or False on error. + */ + public function getStructure(string $mailbox, int $id, bool $is_uid = false) : array | bool + { + $result = $this->fetch($mailbox, "{$id}", $is_uid, ['BODYSTRUCTURE']); + + if (\is_array($result)) { + $result = \array_shift($result); + + return $result->bodystructure; + } + + return false; + } + + public static function getStructurePartArray($a, $part) + { + if (! \is_array($a)) { + return false; + } + + if (empty($part)) { + return $a; + } + + $ctype = \is_string($a[0]) && \is_string($a[1]) ? $a[0] . '/' . $a[1] : ''; + + if (0 == \strcasecmp($ctype, 'message/rfc822')) { + $a = $a[8]; + } + + if (\strpos($part, '.') > 0) { + $orig_part = $part; + $pos = \strpos($part, '.'); + $rest = \substr($orig_part, $pos + 1); + $part = (int)\substr($orig_part, 0, $pos); + + return self::getStructurePartArray($a[$part - 1], $rest); + } + elseif ($part > 0) { + return (\is_array($a[$part - 1])) ? $a[$part - 1] : $a; + } + } + + /** + * Returns data of a message part according to specified structure. + * + * @param array $structure Message structure (getStructure() result) + * @param string $part Message part identifier + * + * @return array Part data as hash array (type, encoding, charset, size) + */ + public static function getStructurePartData($structure, $part) : array + { + $part_a = self::getStructurePartArray($structure, $part); + $data = []; + + if (empty($part_a)) { + return $data; + } + + // content-type + if (\is_array($part_a[0])) { + $data['type'] = 'multipart'; + } + else { + $data['type'] = \strtolower($part_a[0]); + $data['subtype'] = \strtolower($part_a[1]); + $data['encoding'] = \strtolower($part_a[5]); + + // charset + if (\is_array($part_a[2])) { + foreach ($part_a[2] as $key => $val) { + if (0 == \strcasecmp($val, 'charset')) { + $data['charset'] = $part_a[2][$key + 1]; + + break; + } + } + } + } + + // size + $data['size'] = (int)($part_a[6]); + + return $data; + } + + /** + * Fetches body of the specified message part + * + * @param ?resource $file + */ + public function handlePartBody(string $mailbox, $id, bool $is_uid = false, string $part = '', ?string $encoding = null, bool $print = false, $file = null, bool $formatted = false, int $max_bytes = 0) + { + if (! $this->select($mailbox)) { + return false; + } + + $binary = true; + $key = ''; + $result = $initiated = $found = false; + $mode = 0; + + do { + if (! $initiated) { + switch ($encoding) { + case 'base64': + $mode = 1; + + break; + + case 'quoted-printable': + $mode = 2; + + break; + + case 'x-uuencode': + case 'x-uue': + case 'uue': + case 'uuencode': + $mode = 3; + + break; + + default: + $mode = 0; + } + + // Use BINARY extension when possible (and safe) + $binary = $binary && $mode && \preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); + $fetch_mode = $binary ? 'BINARY' : 'BODY'; + $partial = $max_bytes ? \sprintf('<0.%d>', $max_bytes) : ''; + + // format request + $key = $this->nextTag(); + $cmd = ($is_uid ? 'UID ' : '') . 'FETCH'; + $request = "{$key} {$cmd} {$id} ({$fetch_mode}.PEEK[{$part}]{$partial})"; + $result = false; + $found = false; + $initiated = true; + + // send request + if (! $this->putLine($request)) { + $this->setError(self::ERROR_COMMAND, "Failed to send {$cmd} command"); + + return false; + } + + if ($binary) { + // WARNING: Use $formatted argument with care, this may break binary data stream + $mode = -1; + } + } + + $line = \trim($this->readLine(1024)); + + if (! $line) { + break; + } + + // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request + if ($binary && ! $found && \preg_match('/^' . $key . ' NO \[(UNKNOWN-CTE|PARSE)\]/i', $line)) { + $binary = $initiated = false; + + continue; + } + + // skip irrelevant untagged responses (we have a result already) + if ($found || ! \preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { + continue; + } + + $line = $m[2]; + + // handle one line response + if ('(' == $line[0] && ')' == \substr($line, -1)) { + // tokenize content inside brackets + // the content can be e.g.: (UID 9844 BODY[2.4] NIL) + $replacedLine = \preg_replace('/(^\(|\)$)/', '', $line); + $tokens = $this->tokenizeResponse($replacedLine); + + for ($i = 0; $i < \count($tokens); $i += 2) { + if (\preg_match('/^(BODY|BINARY)/i', $tokens[$i])) { + $result = $tokens[$i + 1]; + $found = true; + + break; + } + } + + if (false !== $result) { + if (1 == $mode) { + $result = \base64_decode($result); + } + elseif (2 == $mode) { + $result = \quoted_printable_decode($result); + } + elseif (3 == $mode) { + $result = \convert_uudecode($result); + } + } + } + // response with string literal + elseif (\preg_match('/\{([0-9]+)\}$/', $line, $m)) { + $bytes = (int)$m[1]; + $prev = ''; + $found = true; + + // empty body + if (! $bytes) { + $result = ''; + } + else while ($bytes > 0) { + $line = $this->readLine(8192); + + if (null === $line) { + break; + } + + $len = \strlen($line); + + if ($len > $bytes) { + $line = \substr($line, 0, $bytes); + $len = \strlen($line); + } + $bytes -= $len; + + // BASE64 + if (1 == $mode) { + $line = \preg_replace('|[^a-zA-Z0-9+=/]|', '', $line); + // create chunks with proper length for base64 decoding + $line = $prev . $line; + $length = \strlen($line); + + if ($length % 4) { + $length = (int)(\floor($length / 4) * 4); + $prev = \substr($line, $length); + $line = \substr($line, 0, $length); + } + else { + $prev = ''; + } + $line = \base64_decode($line); + } + // QUOTED-PRINTABLE + elseif (2 == $mode) { + $line = \rtrim($line, "\t\r\0\x0B"); + $line = \quoted_printable_decode($line); + } + // UUENCODE + elseif (3 == $mode) { + $line = \rtrim($line, "\t\r\n\0\x0B"); + + if ('end' == $line || \preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) { + continue; + } + $line = \convert_uudecode($line); + } + // default + elseif ($formatted) { + $line = \rtrim($line, "\t\r\n\0\x0B") . "\n"; + } + + if ($file) { + if (false === \fwrite($file, $line)) { + break; + } + } + elseif ($print) { + echo $line; + } + else { + $result .= $line; + } + } + } + } + while (! $this->startsWith($line, $key, true) || ! $initiated); + + if (false !== $result) { + if ($file) { + return \fwrite($file, $result); + } + elseif ($print) { + echo $result; + + return true; + } + + return $result; + } + + return false; + } + + /** + * Executes ID command (RFC2971) + * + * @param array $items Client identification information key/value hash + * + * @return array|false Server identification information key/value hash, False on error + */ + public function id(array $items = []) : array | false + { + if (! empty($items)) { + foreach ($items as $key => $value) { + $args[] = $this->escape($key, true); + $args[] = $this->escape($value, true); + } + } + + [$code, $response] = $this->execute( + 'ID', + [! empty($args) ? '(' . \implode(' ', (array)$args) . ')' : $this->escape(null)], + 0, + '/^\* ID /i' + ); + + if (self::ERROR_OK == $code && $response) { + $response = \substr($response, 5); // remove prefix "* ID " + $items = $this->tokenizeResponse($response, 1); + $result = []; + + if (\is_array($items)) { + for ($i = 0, $len = \count($items); $i < $len; $i += 2) { + $result[$items[$i]] = $items[$i + 1]; + } + } + + return $result; + } + + return false; + } + + /** + * Returns message unique identifier (UID) + * + * @param string $mailbox Mailbox name + * + * @return int Message unique identifier + */ + public function ID2UID(string $mailbox, $id) : ?int + { + if (empty($id) || $id < 0) { + return null; + } + + if (! $this->select($mailbox)) { + return null; + } + + if ($uid = $this->data['UID-MAP'][$id]) { + return $uid; + } + + if (isset($this->data['EXISTS']) && $id > $this->data['EXISTS']) { + return null; + } + + $index = $this->search($mailbox, $id, true); + + if (1 == $index->count()) { + $arr = $index->get(); + + return $this->data['UID-MAP'][$id] = (int)$arr[0]; + } + + return null; + } + + /** + * Simulates SORT command by using FETCH and sorting. + * + * @param string $mailbox Mailbox name + * @param string|array $message_set Searching criteria (list of messages to return) + * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) + * @param bool $skip_deleted Makes that DELETED messages will be skipped + * @param bool $uidfetch Enables UID FETCH usage + * @param bool $return_uid Enables returning UIDs instead of IDs + * + * @return ResultIndex Response data + */ + public function index( + string $mailbox, + string | array $message_set, + string $index_field = '', + bool $skip_deleted = true, + bool $uidfetch = false, + bool $return_uid = false + ) : ResultIndex + { + $msg_index = $this->fetchHeaderIndex( + $mailbox, + $message_set, + $index_field, + $skip_deleted, + $uidfetch, + $return_uid + ); + + if (! empty($msg_index)) { + \asort($msg_index); // ASC + $msg_index = \array_keys($msg_index); + $msg_index = '* SEARCH ' . \implode(' ', $msg_index); + } + else { + $msg_index = \is_array($msg_index) ? '* SEARCH' : null; + } + + return new ResultIndex($mailbox, $msg_index); + } + + /** + * Returns list of mailboxes + * + * @param string $ref Reference name + * @param string $mailbox Mailbox name + * @param array $return_opts (see self::_listMailboxes) + * @param array $select_opts (see self::_listMailboxes) + * + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. + */ + public function listMailboxes(string $ref, string $mailbox, array $return_opts = [], array $select_opts = []) + { + return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts); + } + + /** + * Send the LISTRIGHTS command (RFC4314) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * + * @return array List of user rights + */ + public function listRights(string $mailbox, string $user) : array + { + [$code, $response] = $this->execute( + 'LISTRIGHTS', + [$this->escape($mailbox), $this->escape($user)], + 0, + '/^\* LISTRIGHTS /i' + ); + + if (self::ERROR_OK == $code && $response) { + // Parse server response (remove "* LISTRIGHTS ") + $response = \substr($response, 13); + + $ret_mbox = $this->tokenizeResponse($response, 1); + $ret_user = $this->tokenizeResponse($response, 1); + $granted = $this->tokenizeResponse($response, 1); + $optional = \trim($response); + + return [ + 'granted' => \str_split($granted), + 'optional' => \explode(' ', $optional), + ]; + } + + return []; + } + + /** + * Returns list of subscribed mailboxes + * + * @param string $ref Reference name + * @param string $mailbox Mailbox name + * @param array $return_opts (see self::_listMailboxes) + * + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. + */ + public function listSubscribed(string $ref, string $mailbox, array $return_opts = []) + { + return $this->_listMailboxes($ref, $mailbox, true, $return_opts, []); + } + + /** + * Moves message(s) from one folder to another. + * + * @param string|array $messages Message UID(s) + * @param string $from Mailbox name + * @param string $to Destination mailbox name + * + * @return bool True on success, False on failure + */ + public function move($messages, string $from, string $to) : bool + { + if (! $this->select($from)) + { + return false; + } + + if (! $this->data['READ-WRITE']) + { + $this->setError(self::ERROR_READONLY, 'Mailbox is read-only'); + + return false; + } + + // use MOVE command (RFC 6851) + if ($this->hasCapability('MOVE')) + { + // Clear last COPYUID data + unset($this->data['COPYUID'], $this->data['STATUS:' . $to]); + + // Clear internal status cache + + $this->clear_status_cache($from); + + $result = $this->execute( + 'UID MOVE', + [ + $this->compressMessageSet($messages), $this->escape($to)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE + $result = $this->copy($messages, $from, $to); + + if ($result) + { + // Clear internal status cache + unset($this->data['STATUS:' . $from]); + + $result = $this->flag($from, $messages, 'DELETED'); + + if ('*' == $messages) + { + // CLOSE+SELECT should be faster than EXPUNGE + $this->close(); + } + else + { + $this->expunge($from, $messages); + } + } + + return $result; + } + + /** + * Send the MYRIGHTS command (RFC4314) + * + * @param string $mailbox Mailbox name + * + * @return array MYRIGHTS response on success, NULL on error + */ + public function myRights(string $mailbox) : array + { + [$code, $response] = $this->execute('MYRIGHTS', [$this->escape($mailbox)], 0, '/^\* MYRIGHTS /i'); + + if (self::ERROR_OK == $code && $response) { + // Parse server response (remove "* MYRIGHTS ") + $response = \substr($response, 11); + + $ret_mbox = $this->tokenizeResponse($response, 1); + $rights = $this->tokenizeResponse($response, 1); + + return \str_split($rights); + } + + return []; + } + + /** + * Creates next command identifier (tag) + * + * @return string Command identifier + */ + public function nextTag() : string + { + $this->cmd_num++; + $this->cmd_tag = \sprintf('A%04d', $this->cmd_num); + + return $this->cmd_tag; + } + + /** + * Folder renaming (RENAME) + * + * + * @return bool True on success, False on error + */ + public function renameFolder($from, $to) : bool + { + $result = $this->execute( + 'RENAME', + [$this->escape($from), $this->escape($to)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Executes SEARCH command + * + * @param string $mailbox Mailbox name + * @param string $criteria Searching criteria + * @param bool $return_uid Enable UID in result instead of sequence ID + * @param array $items Return items (MIN, MAX, COUNT, ALL) + * + * @return ResultIndex Result data + */ + public function search(string $mailbox, string $criteria, bool $return_uid = false, array $items = []) + { + $old_sel = $this->selected; + + if (! $this->select($mailbox)) { + return new ResultIndex($mailbox); + } + + // return empty result when folder is empty and we're just after SELECT + if ($old_sel != $mailbox && ! $this->data['EXISTS']) { + return new ResultIndex($mailbox, '* SEARCH'); + } + + // If ESEARCH is supported always use ALL + // but not when items are specified or using simple id2uid search + if (empty($items) && \preg_match('/[^0-9]/', $criteria)) { + $items = ['ALL']; + } + + $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); + $criteria = \trim($criteria); + $params = ''; + + // RFC4731: ESEARCH + if (! empty($items) && $esearch) { + $params .= 'RETURN (' . \implode(' ', $items) . ')'; + } + + if (! empty($criteria)) { + $params .= ($params ? ' ' : '') . $criteria; + } + else { + $params .= 'ALL'; + } + + [$code, $response] = $this->execute( + $return_uid ? 'UID SEARCH' : 'SEARCH', + [$params] + ); + + if (self::ERROR_OK != $code) { + $response = null; + } + + return new ResultIndex($mailbox, $response); + } + + /** + * Executes SELECT command (if mailbox is already not in selected state) + * + * @param string $mailbox Mailbox name + * @param array $qresync_data QRESYNC data (RFC5162) + * + * @return bool True on success, false on error + */ + public function select(string $mailbox, ?array $qresync_data = null) + { + if (! \strlen($mailbox)) { + return false; + } + + if ($this->selected === $mailbox) { + return true; + } + + $params = [$this->escape($mailbox)]; + + // QRESYNC data items + // 0. the last known UIDVALIDITY, + // 1. the last known modification sequence, + // 2. the optional set of known UIDs, and + // 3. an optional parenthesized list of known sequence ranges and their + // corresponding UIDs. + if (! empty($qresync_data)) { + if (! empty($qresync_data[2])) { + $qresync_data[2] = self::compressMessageSet($qresync_data[2]); + } + + $params[] = ['QRESYNC', $qresync_data]; + } + + [$code, $response] = $this->execute('SELECT', $params); + + if (self::ERROR_OK == $code) { + $this->clear_mailbox_cache(); + + $response = \explode("\r\n", $response); + + foreach ($response as $line) { + if (\preg_match('/^\* OK \[/i', $line)) { + $pos = \strcspn($line, ' ]', 6); + $token = \strtoupper(\substr($line, 6, $pos)); + $pos += 7; + + switch ($token) { + case 'UIDNEXT': + case 'UIDVALIDITY': + case 'UNSEEN': + if ($len = \strspn($line, '0123456789', $pos)) { + $this->data[$token] = (int)\substr($line, $pos, $len); + } + + break; + + case 'HIGHESTMODSEQ': + if ($len = \strspn($line, '0123456789', $pos)) { + $this->data[$token] = (string)\substr($line, $pos, $len); + } + + break; + + case 'NOMODSEQ': + $this->data[$token] = true; + + break; + + case 'PERMANENTFLAGS': + $start = \strpos($line, '(', $pos); + $end = \strrpos($line, ')'); + + if ($start && $end) { + $flags = \substr($line, $start + 1, $end - $start - 1); + $this->data[$token] = \explode(' ', $flags); + } + + break; + } + } + elseif (\preg_match('/^\* ([0-9]+) (EXISTS|RECENT|FETCH)/i', $line, $match)) { + $token = \strtoupper($match[2]); + + switch ($token) { + case 'EXISTS': + case 'RECENT': + $this->data[$token] = (int)$match[1]; + + break; + + case 'FETCH': + // QRESYNC FETCH response (RFC5162) + $line = \substr($line, \strlen($match[0])); + $fetch_data = $this->tokenizeResponse($line, 1); + $data = ['id' => $match[1]]; + + for ($i = 0, $size = \count($fetch_data); $i < $size; $i += 2) { + $data[\strtolower($fetch_data[$i])] = $fetch_data[$i + 1]; + } + + $this->data['QRESYNC'][$data['uid']] = $data; + + break; + } + } + // QRESYNC VANISHED response (RFC5162) + elseif (\preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { + $line = \substr($line, \strlen($match[0])); + $v_data = $this->tokenizeResponse($line, 1); + + $this->data['VANISHED'] = $v_data; + } + } + + $this->data['READ-WRITE'] = 'READ-ONLY' != $this->resultcode; + $this->selected = $mailbox; + + return true; + } + + return false; + } + + /** + * Send the SETACL command (RFC4314) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * @param string | array $acl ACL string or array + * + * @return bool True on success, False on failure + */ + public function setACL(string $mailbox, string $user, array | string $acl) : bool + { + if (\is_array($acl)) { + $acl = \implode('', $acl); + } + + $result = $this->execute( + 'SETACL', + [ + $this->escape($mailbox), $this->escape($user), \strtolower($acl)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Send the SETANNOTATION command (draft-daboo-imap-annotatemore) + * + * @param string $mailbox Mailbox name + * @param array $data Data array where each item is an array with + * three elements: entry name, attribute name, value + * + * @return bool True on success, False on failure + */ + public function setAnnotation(string $mailbox, array $data) : bool + { + if (empty($data)) { + $this->setError(self::ERROR_COMMAND, 'Wrong argument for SETANNOTATION command'); + + return false; + } + + foreach ($data as $entry) { + // ANNOTATEMORE drafts before version 08 require quoted parameters + $entries[] = \sprintf( + '%s (%s %s)', + $this->escape($entry[0], true), + $this->escape($entry[1], true), + $this->escape($entry[2], true) + ); + } + + $entries = \implode(' ', $entries); + $result = $this->execute('SETANNOTATION', [ + $this->escape($mailbox), $entries], self::COMMAND_NORESPONSE); + + return self::ERROR_OK == $result; + } + + /** + * Set the value of the debugging flag. + * + * @param bool $debug New value for the debugging flag. + * @param callable $handler Logging handler function + */ + public function setDebug($debug, ?callable $handler = null) : void + { + $this->debug = $debug; + $this->debug_handler = $handler; + } + + /** + * Error code/message setter. + */ + public function setError(int $code, string $msg = '') : int + { + $this->errornum = $code; + $this->error = $msg; + + return $code; + } + + /** + * Send the SETMETADATA command (RFC5464) + * + * @param string $mailbox Mailbox name + * @param array $entries Entry-value array (use NULL value as NIL) + * + * @return bool True on success, False on failure + */ + public function setMetadata(string $mailbox, array $entries) : bool + { + if (empty($entries)) { + $this->setError(self::ERROR_COMMAND, 'Wrong argument for SETMETADATA command'); + + return false; + } + + foreach ($entries as $name => $value) { + $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true); + } + + $entries = \implode(' ', $entries); + $result = $this->execute( + 'SETMETADATA', + [ + $this->escape($mailbox), '(' . $entries . ')'], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Executes SORT command + * + * @param string $mailbox Mailbox name + * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) + * @param string $criteria Searching criteria + * @param bool $return_uid Enables UID SORT usage + * @param string $encoding Character set + * + * @return ResultIndex Response data + */ + public function sort(string $mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII') + { + $old_sel = $this->selected; + $supported = ['ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO']; + $field = \strtoupper($field); + + if ('INTERNALDATE' == $field) { + $field = 'ARRIVAL'; + } + + if (! \in_array($field, $supported)) { + return new ResultIndex($mailbox); + } + + if (! $this->select($mailbox)) { + return new ResultIndex($mailbox); + } + + // return empty result when folder is empty and we're just after SELECT + if ($old_sel != $mailbox && ! $this->data['EXISTS']) { + return new ResultIndex($mailbox, '* SORT'); + } + + // RFC 5957: SORT=DISPLAY + if (('FROM' == $field || 'TO' == $field) && $this->getCapability('SORT=DISPLAY')) { + $field = 'DISPLAY' . $field; + } + + $encoding = $encoding ? \trim($encoding) : 'US-ASCII'; + $criteria = $criteria ? 'ALL ' . \trim($criteria) : 'ALL'; + + [$code, $response] = $this->execute( + $return_uid ? 'UID SORT' : 'SORT', + ["({$field})", $encoding, $criteria] + ); + + if (self::ERROR_OK != $code) { + $response = null; + } + + return new ResultIndex($mailbox, $response); + } + + /** + * Sort messages by specified header field + * + * @param array $messages Array of rcube_message_header objects + * @param string $field Name of the property to sort by + * @param string $flag Sorting order (ASC|DESC) + * + * @return array Sorted input array + */ + public static function sortHeaders($messages, $field, $flag) + { + $field = empty($field) ? 'uid' : \strtolower($field); + $order = empty($flag) ? 'ASC' : \strtoupper($flag); + $index = []; + + \reset($messages); + + // Create an index + foreach ($messages as $key => $headers) { + switch ($field) { + case 'arrival': + $field = 'internaldate'; + + // no-break + // Intentionally fall through + case 'date': + case 'internaldate': + case 'timestamp': + $value = Utils::strtotime($headers->{$field}); + + if (! $value && 'timestamp' != $field) { + $value = $headers->timestamp; + } + + break; + + default: + // @TODO: decode header value, convert to UTF-8 + $value = $headers->{$field}; + + if (\is_string($value)) { + $value = \str_replace('"', '', $value); + + if ('subject' == $field) { + $value = \preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value); + } + } + } + + $index[$key] = $value; + } + + $sort_order = 'ASC' == $flag ? SORT_ASC : SORT_DESC; + $sort_flags = SORT_STRING | SORT_FLAG_CASE; + + if (\in_array($field, ['arrival', 'date', 'internaldate', 'timestamp'])) { + $sort_flags = SORT_NUMERIC; + } + + \array_multisort($index, $sort_order, $sort_flags, $messages); + + return $messages; + } + + /** + * Executes STATUS command + * + * @param string $mailbox Mailbox name + * @param array $items Additional requested item names. By default + * MESSAGES and UNSEEN are requested. Other defined + * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT + * + * @return array Status item-value hash + */ + public function status(string $mailbox, array $items = []) : array | false + { + if (! \strlen($mailbox)) { + return false; + } + + if (! \in_array('MESSAGES', $items)) { + $items[] = 'MESSAGES'; + } + + if (! \in_array('UNSEEN', $items)) { + $items[] = 'UNSEEN'; + } + + [$code, $response] = $this->execute( + 'STATUS', + [$this->escape($mailbox), '(' . \implode(' ', $items) . ')'], + 0, + '/^\* STATUS /i' + ); + + if (self::ERROR_OK == $code && $response) { + $result = []; + $response = \substr($response, 9); // remove prefix "* STATUS " + + [$mbox, $items] = $this->tokenizeResponse($response, 2); + + // Fix for #1487859. Some buggy server returns not quoted + // folder name with spaces. Let's try to handle this situation + if (! \is_array($items) && ($pos = \strpos($response, '(')) !== false) { + $response = \substr($response, $pos); + $items = $this->tokenizeResponse($response, 1); + } + + if (! \is_array($items)) { + return $result; + } + + for ($i = 0, $len = \count($items); $i < $len; $i += 2) { + $result[$items[$i]] = $items[$i + 1]; + } + + $this->data['STATUS:' . $mailbox] = $result; + + return $result; + } + + return false; + } + + /** + * Folder subscription (SUBSCRIBE) + * + * @param string $mailbox Mailbox name + * + * @return bool True on success, False on error + */ + public function subscribe($mailbox) + { + $result = $this->execute( + 'SUBSCRIBE', + [$this->escape($mailbox)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Executes THREAD command + * + * @param string $mailbox Mailbox name + * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS) + * @param string $criteria Searching criteria + * @param bool $return_uid Enables UIDs in result instead of sequence numbers + * @param string $encoding Character set + */ + public function thread(string $mailbox, string $algorithm = 'REFERENCES', string $criteria = '', bool $return_uid = false, string $encoding = 'US-ASCII') : \PHPFUI\Imap2\Roundcube\ResultThread + { + $old_sel = $this->selected; + + if (! $this->select($mailbox)) { + return new \PHPFUI\Imap2\Roundcube\ResultThread($mailbox); + } + + // return empty result when folder is empty and we're just after SELECT + if ($old_sel != $mailbox && ! $this->data['EXISTS']) { + return new \PHPFUI\Imap2\Roundcube\ResultThread($mailbox, '* THREAD'); + } + + $encoding = $encoding ? \trim($encoding) : 'US-ASCII'; + $algorithm = $algorithm ? \trim($algorithm) : 'REFERENCES'; + $criteria = $criteria ? 'ALL ' . \trim($criteria) : 'ALL'; + + [$code, $response] = $this->execute( + $return_uid ? 'UID THREAD' : 'THREAD', + [$algorithm, $encoding, $criteria] + ); + + if (self::ERROR_OK != $code) { + $response = null; + } + + return new \PHPFUI\Imap2\Roundcube\ResultThread($mailbox, $response); + } + + /** + * Splits IMAP response into string tokens + * + * @param string &$str The IMAP's server response + * @param int $num Number of tokens to return + * + * @return mixed Tokens array or string if $num=1 + */ + public static function tokenizeResponse(&$str, $num = 0) + { + $result = []; + + while (! $num || \count($result) < $num) { + // remove spaces from the beginning of the string + $str = \ltrim($str); + + switch ($str[0]) { + + // String literal + case '{': + if (($epos = \strpos($str, "}\r\n", 1)) == false) { + // error + } + + if (! \is_numeric(($bytes = \substr($str, 1, $epos - 1)))) { + // error + } + + $bytes = (int)$bytes; + $result[] = $bytes ? \substr($str, $epos + 3, $bytes) : ''; + $str = \substr($str, $epos + 3 + $bytes); + + break; + + // Quoted string + case '"': + $len = \strlen($str); + + for ($pos = 1; $pos < $len; $pos++) { + if ('"' == $str[$pos]) { + break; + } + + if ('\\' == $str[$pos]) { + if ('"' == $str[$pos + 1] || '\\' == $str[$pos + 1]) { + $pos++; + } + } + } + + // we need to strip slashes for a quoted string + $result[] = \stripslashes(\substr($str, 1, $pos - 1)); + $str = \substr($str, $pos + 1); + + break; + + // Parenthesized list + case '(': + $str = \substr($str, 1); + $result[] = self::tokenizeResponse($str); + + break; + + case ')': + $str = \substr($str, 1); + + return $result; + + // String atom, number, astring, NIL, *, % + default: + // empty string + if (empty($str)) + { + break 2; + } + + // excluded chars: SP, CTL, ), DEL + // we do not exclude [ and ] (#1489223) + if (\preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) { + $result[] = 'NIL' == $m[1] ? null : $m[1]; + $str = \substr($str, \strlen($m[1])); + } + + break; + } + } + + return 1 == $num ? $result[0] : $result; + } + + /** + * Returns message sequence identifier + * + * @param string $mailbox Mailbox name + * @param int $uid Message unique identifier (UID) + * + * @return int Message sequence identifier + */ + public function UID2ID(string $mailbox, int $uid) : int + { + if ($uid > 0) { + $index = $this->search($mailbox, "UID {$uid}"); + + if (1 == $index->count()) { + $arr = $index->get(); + + return (int)$arr[0]; + } + } + + return $uid; + } + + /** + * Converts message sequence-set into array + * + * @param string $messages Message identifiers + * + * @return array List of message identifiers + */ + public static function uncompressMessageSet($messages) + { + if (empty($messages)) { + return []; + } + + $result = []; + $messages = \explode(',', $messages); + + foreach ($messages as $idx => $part) { + $items = \explode(':', $part); + $max = \max($items[0], $items[1] ?? $items[0]); + + for ($x = $items[0]; $x <= $max; $x++) { + $result[] = (int)$x; + } + unset($messages[$idx]); + } + + return $result; + } + + /** + * Unsets flag of the message(s) + * + * @param string $mailbox Mailbox name + * @param string|array $messages Message UID(s) + * @param string $flag Flag name + * + * @return bool True on success, False on failure + */ + public function unflag(string $mailbox, $messages, $flag) + { + return $this->modFlag($mailbox, $messages, $flag, '-'); + } + + /** + * Folder unsubscription (UNSUBSCRIBE) + * + * @param string $mailbox Mailbox name + * + * @return bool True on success, False on error + */ + public function unsubscribe($mailbox) + { + $result = $this->execute( + 'UNSUBSCRIBE', + [$this->escape($mailbox)], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Connects to IMAP server. + * + * @param string $host Server hostname or IP + * + * @return bool True on success, False on failure + */ + protected function _connect($host) + { + // initialize connection + $this->error = ''; + $this->errornum = self::ERROR_OK; + + if (! $this->prefs['port']) { + $this->prefs['port'] = 143; + } + + // check for SSL + if ($this->prefs['ssl_mode'] && 'tls' != $this->prefs['ssl_mode']) { + $host = $this->prefs['ssl_mode'] . '://' . $host; + } + + if ($this->prefs['timeout'] <= 0) { + $this->prefs['timeout'] = \max(60, (int)(\ini_get('default_socket_timeout'))); + } + + if ($this->debug) { + // set connection identifier for debug output + $this->resourceid = \strtoupper(\substr(\md5(\microtime() . $host . $this->user), 0, 4)); + + $_host = ('tls' == $this->prefs['ssl_mode'] ? 'tls://' : '') . $host . ':' . $this->prefs['port']; + $this->debug("Connecting to {$_host}..."); + } + + if (! empty($this->prefs['socket_options'])) { + $context = \stream_context_create($this->prefs['socket_options']); + $this->fp = \stream_socket_client( + $host . ':' . $this->prefs['port'], + $errno, + $errstr, + $this->prefs['timeout'], + STREAM_CLIENT_CONNECT, + $context + ); + } + else { + $this->fp = @\fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); + } + + if (! $this->fp) { + $this->setError(self::ERROR_BAD, \sprintf( + 'Could not connect to %s:%d: %s', + $host, + $this->prefs['port'], + $errstr ?: 'Unknown reason' + )); + + return false; + } + + if ($this->prefs['timeout'] > 0) { + \stream_set_timeout($this->fp, $this->prefs['timeout']); + } + + $line = \trim(\fgets($this->fp, 8192)); + + if ($this->debug && $line) { + $this->debug('S: ' . $line); + } + + // Connected to wrong port or connection error? + if (! \preg_match('/^\* (OK|PREAUTH)/i', $line)) { + if ($line) + $error = \sprintf('Wrong startup greeting (%s:%d): %s', $host, $this->prefs['port'], $line); + else + $error = \sprintf('Empty startup greeting (%s:%d)', $host, $this->prefs['port']); + + $this->setError(self::ERROR_BAD, $error); + $this->closeConnection(); + + return false; + } + + $this->data['GREETING'] = \trim(\preg_replace('/\[[^\]]+\]\s*/', '', $line)); + + // RFC3501 [7.1] optional CAPABILITY response + if (\preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { + $this->parseCapability($matches[1], true); + } + + // TLS connection + if ('tls' == $this->prefs['ssl_mode'] && $this->getCapability('STARTTLS')) { + $res = $this->execute('STARTTLS'); + + if (self::ERROR_OK != $res[0]) { + $this->closeConnection(); + + return false; + } + + if (isset($this->prefs['socket_options']['ssl']['crypto_method'])) { + $crypto_method = $this->prefs['socket_options']['ssl']['crypto_method']; + } + else { + // There is no flag to enable all TLS methods. Net_SMTP + // handles enabling TLS similarly. + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT + | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } + + if (! \stream_socket_enable_crypto($this->fp, true, $crypto_method)) { + $this->setError(self::ERROR_BAD, 'Unable to negotiate TLS'); + $this->closeConnection(); + + return false; + } + + // Now we're secure, capabilities need to be reread + $this->clearCapability(); + } + + return true; + } + + /** + * IMAP LIST/LSUB command + * + * @param string $ref Reference name + * @param string $mailbox Mailbox name + * @param bool $subscribed Enables returning subscribed mailboxes only + * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED) + * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN, + * MYRIGHTS, SUBSCRIBED, CHILDREN + * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED) + * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE, + * SPECIAL-USE (RFC6154) + * + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. + */ + protected function _listMailboxes( + string $ref, + string $mailbox, + bool $subscribed = false, + array $return_opts = [], + array $select_opts = [] + ) + { + if (! \strlen($mailbox)) { + $mailbox = '*'; + } + + $args = []; + $rets = []; + + if (! empty($select_opts) && $this->getCapability('LIST-EXTENDED')) { + $select_opts = (array)$select_opts; + + $args[] = '(' . \implode(' ', $select_opts) . ')'; + } + + $args[] = $this->escape($ref); + $args[] = $this->escape($mailbox); + + if (! empty($return_opts) && $this->getCapability('LIST-EXTENDED')) { + $ext_opts = ['SUBSCRIBED', 'CHILDREN']; + $rets = \array_intersect($return_opts, $ext_opts); + $return_opts = \array_diff($return_opts, $rets); + } + + if (! empty($return_opts) && $this->getCapability('LIST-STATUS')) { + $lstatus = true; + $status_opts = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN']; + $opts = \array_diff($return_opts, $status_opts); + $status_opts = \array_diff($return_opts, $opts); + + if (! empty($status_opts)) { + $rets[] = 'STATUS (' . \implode(' ', $status_opts) . ')'; + } + + if (! empty($opts)) { + $rets = \array_merge($rets, $opts); + } + } else { + $lstatus = false; + } + + if (! empty($rets)) { + $args[] = 'RETURN (' . \implode(' ', $rets) . ')'; + } + + [$code, $response] = $this->execute($subscribed ? 'LSUB' : 'LIST', $args); + + if (self::ERROR_OK == $code) { + $folders = []; + $last = 0; + $pos = 0; + $response .= "\r\n"; + + while ($pos = \strpos($response, "\r\n", $pos + 1)) { + // literal string, not real end-of-command-line + if ('}' == $response[$pos - 1]) { + continue; + } + + $line = \substr($response, $last, $pos - $last); + $last = $pos + 2; + + if (! \preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) { + continue; + } + + $cmd = \strtoupper($m[1]); + $line = \substr($line, \strlen($m[0])); + + // * LIST () + if ('LIST' == $cmd || 'LSUB' == $cmd) { + [$opts, $delim, $mailbox] = $this->tokenizeResponse($line, 3); + + // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879) + if ($delim) { + $mailbox = \rtrim($mailbox, $delim); + } + + // Add to result array + if (! $lstatus) { + $folders[] = $mailbox; + } + else { + $folders[$mailbox] = []; + } + + // store folder options + if ('LIST' == $cmd) { + // Add to options array + if (empty($this->data['LIST'][$mailbox])) { + $this->data['LIST'][$mailbox] = $opts; + } + elseif (! empty($opts)) { + $this->data['LIST'][$mailbox] = \array_unique(\array_merge( + $this->data['LIST'][$mailbox], + $opts + )); + } + } + } + elseif ($lstatus) { + // * STATUS () + if ('STATUS' == $cmd) { + [$mailbox, $status] = $this->tokenizeResponse($line, 2); + + for ($i = 0, $len = \count($status); $i < $len; $i += 2) { + [$name, $value] = $this->tokenizeResponse($status, 2); + $folders[$mailbox][$name] = $value; + } + } + // * MYRIGHTS + elseif ('MYRIGHTS' == $cmd) { + [$mailbox, $acl] = $this->tokenizeResponse($line, 2); + $folders[$mailbox]['MYRIGHTS'] = $acl; + } + } + } + + return $folders; + } + + return false; + } + + /** + * DIGEST-MD5/CRAM-MD5/PLAIN Authentication + * + * @param string $user Username + * @param string $pass Password + * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5) + * + * @return \IMAP\Connection resourse on success, error code on error + */ + protected function authenticate(string $user, string $pass, string $type = 'PLAIN') : \IMAP\Connection | int + { + $result = $line = ''; + + if ('CRAM-MD5' == $type || 'DIGEST-MD5' == $type) { + if ('DIGEST-MD5' == $type && ! \class_exists('Auth_SASL')) { + return $this->setError( + self::ERROR_BYE, + 'The Auth_SASL package is required for DIGEST-MD5 authentication' + ); + } + + $this->putLine($this->nextTag() . " AUTHENTICATE {$type}"); + $line = \trim($this->readReply()); + + if ('+' == $line[0]) { + $challenge = \substr($line, 2); + } + else { + return $this->parseResult($line); + } + + if ('CRAM-MD5' == $type) { + // RFC2195: CRAM-MD5 + $ipad = ''; + $opad = ''; + $xor = static function($str1, $str2) { + $result = ''; + $size = \strlen($str1); + + for ($i = 0; $i < $size; $i++) { + $result .= \chr(\ord($str1[$i]) ^ \ord($str2[$i])); + } + + return $result; + }; + + // initialize ipad, opad + for ($i = 0; $i < 64; $i++) { + $ipad .= \chr(0x36); + $opad .= \chr(0x5C); + } + + // pad $pass so it's 64 bytes + $pass = \str_pad($pass, 64, \chr(0)); + + // generate hash + $hash = \md5($xor($pass, $opad) . \pack( + 'H*', + \md5($xor($pass, $ipad) . \base64_decode($challenge)) + )); + $reply = \base64_encode($user . ' ' . $hash); + + // send result + $this->putLine($reply, true, true); + } + else { + // RFC2831: DIGEST-MD5 + // proxy authorization + if (! empty($this->prefs['auth_cid'])) { + $authc = $this->prefs['auth_cid']; + $pass = $this->prefs['auth_pw']; + } + else { + $authc = $user; + $user = ''; + } + + $auth_sasl = new \Auth_SASL(); + $auth_sasl = $auth_sasl->factory('digestmd5'); + $reply = \base64_encode($auth_sasl->getResponse( + $authc, + $pass, + \base64_decode($challenge), + $this->host, + 'imap', + $user + )); + + // send result + $this->putLine($reply, true, true); + $line = \trim($this->readReply()); + + if ('+' != $line[0]) { + return $this->parseResult($line); + } + + // check response + $challenge = \substr($line, 2); + $challenge = \base64_decode($challenge); + + if (false === \strpos($challenge, 'rspauth=')) { + return $this->setError( + self::ERROR_BAD, + 'Unexpected response from server to DIGEST-MD5 response' + ); + } + + $this->putLine(''); + } + + $line = $this->readReply(); + $result = $this->parseResult($line); + } + //else if ($type == 'GSSAPI') { + // if (!extension_loaded('krb5')) { + // return $this->setError(self::ERROR_BYE, + // "The krb5 extension is required for GSSAPI authentication"); + // } + // + // if (empty($this->prefs['gssapi_cn'])) { + // return $this->setError(self::ERROR_BYE, + // "The gssapi_cn parameter is required for GSSAPI authentication"); + // } + // + // if (empty($this->prefs['gssapi_context'])) { + // return $this->setError(self::ERROR_BYE, + // "The gssapi_context parameter is required for GSSAPI authentication"); + // } + // + // putenv('KRB5CCNAME=' . $this->prefs['gssapi_cn']); + // + // try { + // $ccache = new \KRB5CCache(); + // $ccache->open($this->prefs['gssapi_cn']); + // $gssapicontext = new \GSSAPIContext(); + // $gssapicontext->acquireCredentials($ccache); + // + // $token = ''; + // $success = $gssapicontext->initSecContext($this->prefs['gssapi_context'], null, null, null, $token); + // $token = base64_encode($token); + // } + // catch (\Exception $e) { + // trigger_error($e->getMessage(), E_USER_WARNING); + // return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed"); + // } + // + // $this->putLine($this->nextTag() . " AUTHENTICATE GSSAPI " . $token); + // $line = trim($this->readReply()); + // + // if ($line[0] != '+') { + // return $this->parseResult($line); + // } + // + // try { + // $itoken = base64_decode(substr($line, 2)); + // + // if (!$gssapicontext->unwrap($itoken, $itoken)) { + // throw new \Exception("GSSAPI SASL input token unwrap failed"); + // } + // + // if (strlen($itoken) < 4) { + // throw new \Exception("GSSAPI SASL input token invalid"); + // } + // + // // Integrity/encryption layers are not supported. The first bit + // // indicates that the server supports "no security layers". + // // 0x00 should not occur, but support broken implementations. + // $server_layers = ord($itoken[0]); + // if ($server_layers && ($server_layers & 0x1) != 0x1) { + // throw new \Exception("Server requires GSSAPI SASL integrity/encryption"); + // } + // + // // Construct output token. 0x01 in the first octet = SASL layer "none", + // // zero in the following three octets = no data follows. + // // See https://github.com/cyrusimap/cyrus-sasl/blob/e41cfb986c1b1935770de554872247453fdbb079/plugins/gssapi.c#L1284 + // if (!$gssapicontext->wrap(pack("CCCC", 0x1, 0, 0, 0), $itoken, true)) { + // throw new \Exception("GSSAPI SASL output token wrap failed"); + // } + // } + // catch (\Exception $e) { + // trigger_error($e->getMessage(), E_USER_WARNING); + // return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed"); + // } + // + // $this->putLine(base64_encode($itoken)); + // + // $line = $this->readReply(); + // $result = $this->parseResult($line); + //} + elseif ('PLAIN' == $type) { + // proxy authorization + if (! empty($this->prefs['auth_cid'])) { + $authc = $this->prefs['auth_cid']; + $pass = $this->prefs['auth_pw']; + } + else { + $authc = $user; + $user = ''; + } + + $reply = \base64_encode($user . \chr(0) . $authc . \chr(0) . $pass); + + // RFC 4959 (SASL-IR): save one round trip + if ($this->getCapability('SASL-IR')) { + [$result, $line] = $this->execute( + 'AUTHENTICATE PLAIN', + [$reply], + self::COMMAND_RAW_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED + ); + } + else { + $this->putLine($this->nextTag() . ' AUTHENTICATE PLAIN'); + $line = \trim($this->readReply()); + + if ('+' != $line[0]) { + return $this->parseResult($line); + } + + // send result, get reply and process it + $this->putLine($reply, true, true); + $line = $this->readReply(); + $result = $this->parseResult($line); + } + } + elseif ('LOGIN' == $type) { + $this->putLine($this->nextTag() . ' AUTHENTICATE LOGIN'); + + $line = \trim($this->readReply()); + + if ('+' != $line[0]) { + return $this->parseResult($line); + } + + $this->putLine(\base64_encode($user), true, true); + + $line = \trim($this->readReply()); + + if ('+' != $line[0]) { + return $this->parseResult($line); + } + + // send result, get reply and process it + $this->putLine(\base64_encode($pass), true, true); + + $line = $this->readReply(); + $result = $this->parseResult($line); + } + elseif ('XOAUTH2' == $type) { + $auth = \base64_encode("user={$user}\1auth=Bearer {$pass}\1\1"); + $this->putLine($this->nextTag() . " AUTHENTICATE XOAUTH2 {$auth}", true, true); + + $line = \trim($this->readReply()); + + if ('+' == $line[0]) { + // send empty line + $this->putLine('', true, true); + $line = $this->readReply(); + } + + $result = $this->parseResult($line); + } + + if (self::ERROR_OK === $result) { + // optional CAPABILITY response + if ($line && \preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { + $this->parseCapability($matches[1], true); + } + + return $this->fp; + } + + return $this->setError($result, "AUTHENTICATE {$type}: {$line}"); + } + + /** + * Clear internal cache of the current mailbox + */ + protected function clear_mailbox_cache() : void + { + $this->clear_status_cache($this->selected); + + $keys = ['UIDNEXT', 'UIDVALIDITY', 'HIGHESTMODSEQ', 'NOMODSEQ', + 'PERMANENTFLAGS', 'QRESYNC', 'VANISHED', 'READ-WRITE']; + + foreach ($keys as $key) + { + unset($this->data[$key]); + } + } + + /** + * Clear internal status cache + */ + protected function clear_status_cache(string $mailbox = '') : void + { + unset($this->data['STATUS:' . $mailbox]); + + $keys = ['EXISTS', 'RECENT', 'UNSEEN', 'UID-MAP']; + + foreach ($keys as $key) + { + unset($this->data[$key]); + } + } + + /** + * Closes connection stream. + */ + protected function closeSocket() : void + { + @\fclose($this->fp); + $this->fp = null; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug message text. + */ + protected function debug(string $message) : void + { + if (($len = \strlen($message)) > self::DEBUG_LINE_LENGTH) + { + $diff = $len - self::DEBUG_LINE_LENGTH; + $message = \substr($message, 0, self::DEBUG_LINE_LENGTH) + . "... [truncated {$diff} bytes]"; + } + + if ($this->resourceid) + { + $message = \sprintf('[%s] %s', $this->resourceid, $message); + } + + if ($this->debug_handler) + { + \call_user_func_array($this->debug_handler, [&$this, $message]); + } + else + { + echo "DEBUG: {$message}\n"; + } + } + + /** + * Checks connection stream state. + * + * @return bool True if connection is closed + */ + protected function eof() : bool + { + if (! \is_resource($this->fp)) + { + return true; + } + + // If a connection opened by fsockopen() wasn't closed + // by the server, feof() will hang. + $start = \microtime(true); + + if (\feof($this->fp) || ($this->prefs['timeout'] && (\microtime(true) - $start > $this->prefs['timeout']))) + { + $this->closeSocket(); + + return true; + } + + return false; + } + + /** + * Converts flags array into string for inclusion in IMAP command + * + * @param array $flags Flags (see self::flags) + * + * @return string Space-separated list of flags + */ + protected function flagsToStr(array $flags) : string + { + foreach ((array)$flags as $idx => $flag) + { + if ($flag = $this->flags[\strtoupper($flag)]) + { + $flags[$idx] = $flag; + } + } + + return \implode(' ', (array)$flags); + } + + /** + * Capabilities checker + */ + protected function hasCapability(string $name) : bool + { + if (empty($this->capability) || '' == $name) { + return false; + } + + if (\in_array($name, $this->capability)) { + return true; + } + elseif (\strpos($name, '=')) { + return false; + } + + $result = []; + + foreach ($this->capability as $cap) { + $entry = \explode('=', $cap); + + if ($entry[0] == $name) { + $result[] = $entry[1]; + } + } + + return $result ?: false; + } + + /** + * LOGIN Authentication + * + * @param string $user Username + * + * @return resource Connection resourse on success, error code on error + */ + protected function login(string $user, string $password) + { + // Prevent from sending credentials in plain text when connection is not secure + if ($this->getCapability('LOGINDISABLED')) { + return $this->setError(self::ERROR_BAD, 'Login disabled by IMAP server'); + } + + [$code, $response] = $this->execute('LOGIN', [ + $this->escape($user), $this->escape($password)], self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); + + // re-set capabilities list if untagged CAPABILITY response provided + if (\preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) { + $this->parseCapability($matches[1], true); + } + + if (self::ERROR_OK == $code) { + return $this->fp; + } + + return $code; + } + + /** + * Changes flag of the message(s) + * + * @param string $mailbox Mailbox name + * @param string|array $messages Message UID(s) + * @param string $flag Flag name + * @param string $mod Modifier [+|-]. Default: "+". + * + * @return bool True on success, False on failure + */ + protected function modFlag(string $mailbox, string | array $messages, string $flag, string $mod = '+') : bool + { + if (! $flag) { + return false; + } + + if (! $this->select($mailbox)) { + return false; + } + + if (! $this->data['READ-WRITE']) { + $this->setError(self::ERROR_READONLY, 'Mailbox is read-only'); + + return false; + } + + if (isset($this->flags[\strtoupper($flag)]) && $this->flags[\strtoupper($flag)]) { + $flag = $this->flags[\strtoupper($flag)]; + } + + // if PERMANENTFLAGS is not specified all flags are allowed + if (! empty($this->data['PERMANENTFLAGS']) + && ! \in_array($flag, (array)$this->data['PERMANENTFLAGS']) + && ! \in_array('\\*', (array)$this->data['PERMANENTFLAGS']) + ) { + return false; + } + + // Clear internal status cache + if ('SEEN' == $flag) { + unset($this->data['STATUS:' . $mailbox]['UNSEEN']); + } + + if ('+' != $mod && '-' != $mod) { + $mod = '+'; + } + + $result = $this->execute( + 'UID STORE', + [ + $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "({$flag})"], + self::COMMAND_NORESPONSE + ); + + return self::ERROR_OK == $result; + } + + /** + * Reads more data from the connection stream when provided + * data contain string literal + * + * @param string $line Response text + * @param bool $escape Enables escaping + * + * @return string Line of text response + */ + protected function multLine(string $line, bool $escape = false) : string + { + $line = \rtrim($line); + + if (\preg_match('/\{([0-9]+)\}$/', $line, $m)) { + $out = ''; + $str = \substr($line, 0, -\strlen($m[0])); + $bytes = (int)$m[1]; + + while (\strlen($out) < $bytes) { + $line = $this->readBytes($bytes); + + if (null === $line) { + break; + } + + $out .= $line; + } + + $line = $str . ($escape ? $this->escape($out) : $out); + } + + return $line; + } + + /** + * CAPABILITY response parser + */ + protected function parseCapability(string $str, bool $trusted = false) : void + { + $str = \preg_replace('/^\* CAPABILITY /i', '', $str); + + $this->capability = \explode(' ', \strtoupper($str)); + + if (! empty($this->prefs['disabled_caps'])) { + $this->capability = \array_diff($this->capability, $this->prefs['disabled_caps']); + } + + if (! isset($this->prefs['literal+']) && \in_array('LITERAL+', $this->capability)) { + $this->prefs['literal+'] = true; + } + + if ($trusted) { + $this->capability_readed = true; + } + } + + /** + * Response parser. + * + * @param string $string Response text + * @param string $err_prefix Error message prefix + * + * @return int Response status + */ + protected function parseResult(string $string, string $err_prefix = '') : int + { + if (\preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', \trim($string), $matches)) { + $res = \strtoupper($matches[1]); + $str = \trim($matches[2]); + + if ('OK' == $res) { + $this->errornum = self::ERROR_OK; + } + elseif ('NO' == $res) { + $this->errornum = self::ERROR_NO; + } + elseif ('BAD' == $res) { + $this->errornum = self::ERROR_BAD; + } + elseif ('BYE' == $res) { + $this->closeSocket(); + $this->errornum = self::ERROR_BYE; + } + + if ($str) { + $str = \trim($str); + + // get response string and code (RFC5530) + if (\preg_match("/^\[([a-z-]+)\]/i", $str, $m)) { + $this->resultcode = \strtoupper($m[1]); + $str = \trim(\substr($str, \strlen($m[1]) + 2)); + } + else { + $this->resultcode = ''; + + // parse response for [APPENDUID 1204196876 3456] + if (\preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) { + $this->data['APPENDUID'] = $m[1]; + } + // parse response for [COPYUID 1204196876 3456:3457 123:124] + elseif (\preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) { + $this->data['COPYUID'] = [$m[1], $m[2]]; + } + } + + $this->result = $str; + + if (self::ERROR_OK != $this->errornum) { + $this->error = $err_prefix ? $err_prefix . $str : $str; + } + } + + return $this->errornum; + } + + return self::ERROR_UNKNOWN; + } + + /** + * Send simple (one line) command to the connection stream + * + * @param string $string Command string + * @param bool $endln True if CRLF need to be added at the end of command + * @param bool $anonymized Don't write the given data to log but a placeholder + * + * @return int | bool Number of bytes sent, False on error + */ + protected function putLine(string $string, bool $endln = true, bool $anonymized = false) : int | bool + { + if (! $this->fp) { + return false; + } + + if ($this->debug) { + // anonymize the sent command for logging + $cut = $endln ? 2 : 0; + + if ($anonymized && \preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) { + $log = $m[1] . \sprintf('****** [%d]', \strlen($m[2]) - $cut); + } + elseif ($anonymized) { + $log = \sprintf('****** [%d]', \strlen($string) - $cut); + } + else { + $log = \rtrim($string); + } + + $this->debug('C: ' . $log); + } + + if ($endln) { + $string .= "\r\n"; + } + + $res = \fwrite($this->fp, $string); + + if (false === $res) { + $this->closeSocket(); + } + + return $res; + } + + /** + * Send command to the connection stream with Command Continuation + * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support + * + * @param string $string Command string + * @param bool $endln True if CRLF need to be added at the end of command + * @param bool $anonymized Don't write the given data to log but a placeholder + * + * @return int|bool Number of bytes sent, False on error + */ + protected function putLineC(string $string, bool $endln = true, bool $anonymized = false) : bool | int + { + if (! $this->fp) { + return false; + } + + if ($endln) { + $string .= "\r\n"; + } + + $res = 0; + + if ($parts = \preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) { + for ($i = 0, $cnt = \count($parts); $i < $cnt; $i++) { + if (isset($parts[$i + 1]) && \preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i + 1], $matches)) { + // LITERAL+ support + if ($this->prefs['literal+']) { + $parts[$i + 1] = \sprintf("{%d+}\r\n", $matches[1]); + } + + $bytes = $this->putLine($parts[$i] . $parts[$i + 1], false, $anonymized); + + if (false === $bytes) { + return false; + } + + $res += $bytes; + + // don't wait if server supports LITERAL+ capability + if (! $this->prefs['literal+']) { + $line = $this->readLine(1000); + + // handle error in command + if ('+' != $line[0]) { + return false; + } + } + + $i++; + } + else { + $bytes = $this->putLine($parts[$i], false, $anonymized); + + if (false === $bytes) { + return false; + } + + $res += $bytes; + } + } + } + + return $res; + } + + /** + * Joins IMAP command line elements (recursively) + */ + protected static function r_implode(string | array $element) : string + { + $string = ''; + + if (\is_array($element)) { + \reset($element); + + foreach ($element as $value) { + $string .= ' ' . self::r_implode($value); + } + } + else { + return $element; + } + + return '(' . \trim($string) . ')'; + } + + /** + * Reads specified number of bytes from the connection stream + * + * @param int $bytes Number of bytes to get + * + * @return string Response text + */ + protected function readBytes(int $bytes) : string + { + $data = ''; + $len = 0; + + while ($len < $bytes && ! $this->eof()) { + $d = \fread($this->fp, $bytes - $len); + + if ($this->debug) { + $this->debug('S: ' . $d); + } + $data .= $d; + $data_len = \strlen($data); + + if ($len == $data_len) { + break; // nothing was read -> exit to avoid apache lockups + } + $len = $data_len; + } + + return $data; + } + + /** + * Reads a line of data from the connection stream inluding all + * string continuation literals. + * + * @param int $size Buffer size + * + * @return string Line of text response + */ + protected function readFullLine(int $size = 1024) : string + { + $line = $this->readLine($size); + + // include all string literels untile the real end of "line" + while (\preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) { + $bytes = (int)$m[1]; + $out = ''; + + while (\strlen($out) < $bytes) { + $out = $this->readBytes($bytes); + + if (null === $out) { + break; + } + + $line .= $out; + } + + $line .= $this->readLine($size); + } + + return $line; + } + + /** + * Reads line from the connection stream + * + * @param int $size Buffer size + * + * @return string Line of text response + */ + protected function readLine(int $size = 1024) : ?string + { + $line = ''; + + if (! $size) { + $size = 1024; + } + + do { + if ($this->eof()) { + return $line ?: null; + } + + $buffer = \fgets($this->fp, $size); + + if (false === $buffer) { + $this->closeSocket(); + + break; + } + + if ($this->debug) { + $this->debug('S: ' . \rtrim($buffer)); + } + + $line .= $buffer; + } + while ("\n" != \substr($buffer, -1)); + + return $line; + } + + /** + * Reads complete response to the IMAP command + * + * @param array $untagged Will be filled with untagged response lines + * + * @return string Response text + */ + protected function readReply(string|array|null &$untagged = null) : string + { + do + { + $line = \trim($this->readLine(1024)); + + // store untagged response lines + if ('*' == $line[0]) + { + $untagged[] = $line; + } + } + while ('*' == $line[0]); + + if (\is_array($untagged)) + { + $untagged = \implode("\n", $untagged); + } + + return $line; + } + + /** + * Initializes environment + */ + protected function set_prefs($prefs) : void + { + // set preferences + if (\is_array($prefs)) { + $this->prefs = $prefs; + } + + // set auth method + if (! empty($this->prefs['auth_type'])) { + $this->prefs['auth_type'] = \strtoupper($this->prefs['auth_type']); + } + else { + $this->prefs['auth_type'] = 'CHECK'; + } + + // disabled capabilities + if (! empty($this->prefs['disabled_caps'])) { + $this->prefs['disabled_caps'] = \array_map('strtoupper', (array)$this->prefs['disabled_caps']); + } + + // additional message flags + if (! empty($this->prefs['message_flags'])) { + $this->flags = \array_merge($this->flags, $this->prefs['message_flags']); + unset($this->prefs['message_flags']); + } + } + + /** + * Checks response status. + * Checks if command response line starts with specified prefix (or * BYE/BAD) + * + * @param string $string Response text + * @param string $match Prefix to match with (case-sensitive) + * @param bool $error Enables BYE/BAD checking + * @param bool $nonempty Enables empty response checking + * + * @return bool True any check is true or connection is closed. + */ + protected function startsWith(string $string, string $match, bool $error = false, bool $nonempty = false) : bool + { + if (! $this->fp) { + return true; + } + + if (0 == \strncmp($string, $match, \strlen($match))) { + return true; + } + + if ($error && \preg_match('/^\* (BYE|BAD) /i', $string, $m)) { + if ('BYE' == \strtoupper($m[1])) { + $this->closeSocket(); + } + + return true; + } + + return (bool)($nonempty && ! \strlen($string)); + } +} diff --git a/src/Imap2/Roundcube/MessageHeader.php b/src/Imap2/Roundcube/MessageHeader.php new file mode 100644 index 0000000..b261404 --- /dev/null +++ b/src/Imap2/Roundcube/MessageHeader.php @@ -0,0 +1,226 @@ + | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +/** + * Struct representing an e-mail message header + * + * @package Framework + * @subpackage Storage + */ +class MessageHeader +{ + /** + * Message additional recipients (bCc) + */ + public string $bcc; + + public $body; + + public $bodypart; + + /** + * IMAP bodystructure string + */ + public string $bodystructure; + + /** + * Message additional recipients (Cc) + */ + public string $cc; + + /** + * Message charset + */ + public string $charset; + + /** + * Message Content-type + */ + public string $ctype; + + /** + * Message date (Date) + */ + public string $date; + + /** + * Message encoding + */ + public string $encoding; + + public $envelope; + + /** + * Message flags + */ + public array $flags = []; + + /** + * IMAP folder this message is stored in + */ + public string $folder; + + /** + * Message sender (From) + */ + public string $from; + + /** + * Message sequence number + */ + public int $id; + + /** + * Message In-Reply-To header + */ + public string $in_reply_to; + + /** + * IMAP internal date + */ + public string $internaldate; + + /** + * Message receipt recipient + */ + public string $mdn_to; + + /** + * Message identifier (Message-ID) + */ + public string $messageID; + + public $modseq; + + /** + * Other message headers + */ + public array $others = []; + + /** + * Message priority (X-Priority) + */ + public int $priority; + + /** + * Message References header + */ + public string $references; + + /** + * Message Reply-To header + */ + public string $replyto; + + /** + * Message size + */ + public int $size; + + /** + * Message subject + */ + public string $subject; + + /** + * Message timestamp (based on message date) + */ + public int $timestamp; + + /** + * Message recipient (To) + */ + public string $to; + + /** + * Message unique identifier + */ + public int $uid; + + // map header to rcube_message_header object property + private $obj_headers = [ + 'date' => 'date', + 'from' => 'from', + 'to' => 'to', + 'subject' => 'subject', + 'reply-to' => 'replyto', + 'cc' => 'cc', + 'bcc' => 'bcc', + 'mbox' => 'folder', + 'folder' => 'folder', + 'content-transfer-encoding' => 'encoding', + 'in-reply-to' => 'in_reply_to', + 'content-type' => 'ctype', + 'charset' => 'charset', + 'references' => 'references', + 'return-receipt-to' => 'mdn_to', + 'disposition-notification-to' => 'mdn_to', + 'x-confirm-reading-to' => 'mdn_to', + 'message-id' => 'messageID', + 'x-priority' => 'priority', + ]; + + /** + * Returns header value + */ + public function get(string $name, bool $decode = true) + { + $name = \strtolower($name); + + if (isset($this->obj_headers[$name])) { + $value = $this->{$this->obj_headers[$name]}; + } + else { + $value = $this->others[$name]; + } + + if ($decode) { + if (\is_array($value)) { + foreach ($value as $key => $val) { + $val = Mime::decode_header($val, $this->charset); + $value[$key] = Charset::clean($val); + } + } + else { + $value = Mime::decode_header($value, $this->charset); + $value = Charset::clean($value); + } + } + + return $value; + } + + /** + * Sets header value + */ + public function set(string $name, $value) : void + { + $name = \strtolower($name); + + if (isset($this->obj_headers[$name])) { + $this->{$this->obj_headers[$name]} = $value; + } + else { + $this->others[$name] = $value; + } + } +} diff --git a/src/Imap2/Roundcube/Mime.php b/src/Imap2/Roundcube/Mime.php new file mode 100644 index 0000000..089cb63 --- /dev/null +++ b/src/Imap2/Roundcube/Mime.php @@ -0,0 +1,303 @@ + | + * | Author: Aleksander Machniak | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +/** + * Class for parsing MIME messages + * + * @package Framework + * @subpackage Storage + */ +class Mime +{ + private static string $default_charset; + + /** + * Object constructor. + */ + public function __construct($default_charset = null) + { + self::$default_charset = $default_charset; + } + + /** + * Decode a mime part + * + * @param string $input Input string + * @param string $encoding Part encoding + * + * @return string Decoded string + */ + public static function decode(string $input, string $encoding = '7bit') : string + { + switch (\strtolower($encoding)) { + case 'quoted-printable': + return \quoted_printable_decode($input); + + case 'base64': + return \base64_decode($input); + + case 'x-uuencode': + case 'x-uue': + case 'uue': + case 'uuencode': + return \convert_uudecode($input); + + case '7bit': + default: + return $input; + } + } + + /** + * Decode a message header value + * + * @param string $input Header value + * @param string $fallback Fallback charset if none specified + * + * @return string Decoded string + */ + public static function decode_header($input, $fallback = null) + { + $str = self::decode_mime_string((string)$input, $fallback); + + return $str; + } + + /** + * Decode a mime-encoded string to internal charset + * + * @param string $input Header value + * @param string $fallback Fallback charset if none specified + * + * @return string Decoded string + */ + public static function decode_mime_string($input, $fallback = null) + { + $default_charset = $fallback ?: self::get_charset(); + + // rfc: all line breaks or other characters not found + // in the Base64 Alphabet must be ignored by decoding software + // delete all blanks between MIME-lines, differently we can + // receive unnecessary blanks and broken utf-8 symbols + $input = \preg_replace("/\?=\s+=\?/", '?==?', $input); + + // encoded-word regexp + $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/'; + + // Find all RFC2047's encoded words + if (\preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + // Initialize variables + $tmp = []; + $out = ''; + $start = 0; + + foreach ($matches as $idx => $m) { + $pos = $m[0][1]; + $charset = $m[1][0]; + $encoding = $m[2][0]; + $text = $m[3][0]; + $length = \strlen($m[0][0]); + + // Append everything that is before the text to be decoded + if ($start != $pos) { + $substr = \substr($input, $start, $pos - $start); + $out .= Charset::convert($substr, $default_charset); + $start = $pos; + } + $start += $length; + + // Per RFC2047, each string part "MUST represent an integral number + // of characters . A multi-octet character may not be split across + // adjacent encoded-words." However, some mailers break this, so we + // try to handle characters spanned across parts anyway by iterating + // through and aggregating sequential encoded parts with the same + // character set and encoding, then perform the decoding on the + // aggregation as a whole. + + $tmp[] = $text; + + if (isset($matches[$idx + 1]) && ($next_match = $matches[$idx + 1])) { + if ($next_match[0][1] == $start + && $next_match[1][0] == $charset + && $next_match[2][0] == $encoding + ) { + continue; + } + } + + $count = \count($tmp); + $text = ''; + + // Decode and join encoded-word's chunks + if ('B' == $encoding || 'b' == $encoding) { + $rest = ''; + + // base64 must be decoded a segment at a time. + // However, there are broken implementations that continue + // in the following word, we'll handle that (#6048) + for ($i = 0; $i < $count; $i++) { + $chunk = $rest . $tmp[$i]; + $length = \strlen($chunk); + + if ($length % 4) { + $length = (int)(\floor($length / 4) * 4); + $rest = \substr($chunk, $length); + $chunk = \substr($chunk, 0, $length); + } + + $text .= \base64_decode($chunk); + } + } + else { //if ($encoding == 'Q' || $encoding == 'q') { + // quoted printable can be combined and processed at once + for ($i = 0; $i < $count; $i++) + $text .= $tmp[$i]; + + $text = \str_replace('_', ' ', $text); + $text = \quoted_printable_decode($text); + } + + $out .= Charset::convert($text, $charset); + $tmp = []; + } + + // add the last part of the input string + if ($start != \strlen($input)) { + $out .= Charset::convert(\substr($input, $start), $default_charset); + } + + // return the results + return $out; + } + + // no encoding information, use fallback + return Charset::convert($input, $default_charset); + } + + /** + * Explodes header (e.g. address-list) string into array of strings + * using specified separator characters with proper handling + * of quoted-strings and comments (RFC2822) + * + * @param string $separator String containing separator characters + * @param string $str Header string + * @param bool $remove_comments Enable to remove comments + * + * @return array Header items + */ + public static function explode_header_string($separator, $str, $remove_comments = false) + { + $length = \strlen($str); + $result = []; + $quoted = false; + $comment = 0; + $out = ''; + + for ($i = 0; $i < $length; $i++) { + // we're inside a quoted string + if ($quoted) { + if ('"' == $str[$i]) { + $quoted = false; + } + elseif ('\\' == $str[$i]) { + if ($comment <= 0) { + $out .= '\\'; + } + $i++; + } + } + // we are inside a comment string + elseif ($comment > 0) { + if (')' == $str[$i]) { + $comment--; + } + elseif ('(' == $str[$i]) { + $comment++; + } + elseif ('\\' == $str[$i]) { + $i++; + } + + continue; + } + // separator, add to result array + elseif (false !== \strpos($separator, $str[$i])) { + if ($out) { + $result[] = $out; + } + $out = ''; + + continue; + } + // start of quoted string + elseif ('"' == $str[$i]) { + $quoted = true; + } + // start of comment + elseif ($remove_comments && '(' == $str[$i]) { + $comment++; + } + + if ($comment <= 0) { + $out .= $str[$i]; + } + } + + if ($out && $comment <= 0) { + $result[] = $out; + } + + return $result; + } + + /** + * Try to fix invalid email addresses + */ + public static function fix_email($email) + { + $parts = Utils::explode_quoted_string('@', $email); + + foreach ($parts as $idx => $part) { + // remove redundant quoting (#1490040) + if ('"' == $part[0] && \preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) { + $parts[$idx] = $m[1]; + } + } + + return \implode('@', $parts); + } + + /** + * Returns message/object character set name + * + * @return string Character set name + */ + public static function get_charset() : string + { + if (self::$default_charset) { + return self::$default_charset; + } + + return IMAP2_CHARSET; + } +} diff --git a/src/Imap2/Roundcube/ResultIndex.php b/src/Imap2/Roundcube/ResultIndex.php new file mode 100644 index 0000000..e26a48f --- /dev/null +++ b/src/Imap2/Roundcube/ResultIndex.php @@ -0,0 +1,284 @@ + | + * | Author: Aleksander Machniak | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +/** + * Class for accessing IMAP's SORT/SEARCH/ESEARCH result + * + * @package Framework + * @subpackage Storage + */ +class ResultIndex +{ + public const SEPARATOR_ELEMENT = ' '; + + public $incomplete = false; + + protected $mailbox; + + protected $meta = []; + + protected $order = 'ASC'; + + protected $params = []; + + protected $raw_data; + + /** + * Object constructor. + */ + public function __construct($mailbox = null, $data = null, $order = null) + { + $this->mailbox = $mailbox; + $this->order = 'DESC' == $order ? 'DESC' : 'ASC'; + $this->init($data); + } + + /** + * Returns number of elements in the result + * + * @return int Number of elements + */ + public function count() + { + if (! empty($this->meta['count'])) + { + return $this->meta['count']; + } + + if (empty($this->raw_data)) + { + $this->meta['count'] = 0; + $this->meta['length'] = 0; + } + else + { + $this->meta['count'] = 1 + \substr_count($this->raw_data, self::SEPARATOR_ELEMENT); + } + + return $this->meta['count']; + } + + /** + * Returns number of elements in the result. + * Alias for count() for compatibility with \PHPFUI\Imap2\Roundcube\ResultThread + * + * @return int Number of elements + */ + public function count_messages() : int + { + return $this->count(); + } + + /** + * Filters data set. Removes elements not listed in $ids list. + * + * @param array $ids List of IDs to remove. + */ + public function filter(array $ids = []) : void + { + $data = $this->get(); + $data = \array_intersect($data, $ids); + + $this->meta = []; + $this->meta['count'] = \count($data); + $this->raw_data = \implode(self::SEPARATOR_ELEMENT, $data); + } + + /** + * Return all messages in the result. + * + * @return array List of message IDs + */ + public function get() : array + { + if (empty($this->raw_data)) + { + return []; + } + + return \explode(self::SEPARATOR_ELEMENT, $this->raw_data); + } + + /** + * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ + * or internal data e.g. MAILBOX, ORDER + * + * @param string $param Parameter name + * + * @return array|string Response parameters or parameter value + */ + public function get_parameters(?string $param = null) : array|string + { + $params = $this->params; + $params['MAILBOX'] = $this->mailbox; + $params['ORDER'] = $this->order; + + if (null !== $param) + { + return $params[$param]; + } + + return $params; + } + + /** + * Initializes object with SORT command response + * + * @param string $data IMAP response string + */ + public function init(?string $data = null) : void + { + $this->meta = []; + + $data = \explode('*', (string)$data); + + // ...skip unilateral untagged server responses + for ($i = 0, $len = \count($data); $i < $len; $i++) { + $data_item = &$data[$i]; + + if (\preg_match('/^ SORT/i', $data_item)) { + // valid response, initialize raw_data for is_error() + $this->raw_data = ''; + $data_item = \substr($data_item, 5); + + break; + } + elseif (\preg_match('/^ (E?SEARCH)/i', $data_item, $m)) { + // valid response, initialize raw_data for is_error() + $this->raw_data = ''; + $data_item = \substr($data_item, \strlen($m[0])); + + if ('ESEARCH' == \strtoupper($m[1])) { + $data_item = \trim($data_item); + + // remove MODSEQ response + if (\preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) { + $data_item = \substr($data_item, 0, -\strlen($m[0])); + $this->params['MODSEQ'] = $m[1]; + } + + // remove TAG response part + if (\preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) { + $data_item = \substr($data_item, \strlen($m[0])); + } + // remove UID + $data_item = \preg_replace('/^UID\s*/i', '', $data_item); + + // ESEARCH parameters + while (\preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) { + $param = \strtoupper($m[1]); + $value = $m[2]; + + $this->params[$param] = $value; + $data_item = \substr($data_item, \strlen($m[0])); + + if (\in_array($param, ['COUNT', 'MIN', 'MAX'])) { + $this->meta[\strtolower($param)] = (int)$value; + } + } + +// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ? +// @TODO: work with compressed result?! + if (isset($this->params['ALL'])) { + $data_item = \implode( + self::SEPARATOR_ELEMENT, + ImapClient::uncompressMessageSet($this->params['ALL']) + ); + } + } + + break; + } + + unset($data[$i]); + } + + $data = \array_filter($data); + + if (empty($data)) { + return; + } + + $data = \array_shift($data); + $data = \trim($data); + $data = \preg_replace('/[\r\n]/', '', $data); + $data = \preg_replace('/\s+/', ' ', $data); + + $this->raw_data = $data; + } + + /** + * Checks the result from IMAP command + * + * @return bool True if the result is an error, False otherwise + */ + public function is_error() : bool + { + return null === $this->raw_data; + } + + /** + * Returns maximal message identifier in the result + * + * @return int Maximal message identifier + */ + public function max() : int + { + if (! isset($this->meta['max'])) + { + $this->meta['max'] = (int)@\max($this->get()); + } + + return $this->meta['max']; + } + + /** + * Returns minimal message identifier in the result + * + * @return int Minimal message identifier + */ + public function min() : int + { + if (! isset($this->meta['min'])) + { + $this->meta['min'] = (int)@\min($this->get()); + } + + return $this->meta['min']; + } + + /** + * Returns length of internal data representation + * + * @return int Data length + */ + protected function length() : int + { + if (! isset($this->meta['length'])) + { + $this->meta['length'] = \strlen($this->raw_data); + } + + return $this->meta['length']; + } + } diff --git a/src/Imap2/Roundcube/ResultThread.php b/src/Imap2/Roundcube/ResultThread.php new file mode 100644 index 0000000..9a3da99 --- /dev/null +++ b/src/Imap2/Roundcube/ResultThread.php @@ -0,0 +1,474 @@ + | + * | Author: Aleksander Machniak | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +/** + * Class for accessing IMAP's THREAD result + * + * @package Framework + * @subpackage Storage + */ +class ResultThread +{ + public const SEPARATOR_ELEMENT = ' '; + + public const SEPARATOR_ITEM = '~'; + + public const SEPARATOR_LEVEL = ':'; + + protected array $meta = []; + + protected string $order = 'ASC'; + + protected string $raw_data = ''; + + /** + * Object constructor. + */ + public function __construct(protected ?string $mailbox = null, ?string $data = null) + { + $this->init($data); + } + + /** + * Returns number of elements (threads) in the result + * + * @return int Number of elements + */ + public function count() : int + { + if (isset($this->meta['count']) && null !== $this->meta['count']) + return (int)$this->meta['count']; + + if (empty($this->raw_data)) { + $this->meta['count'] = 0; + } + else { + $this->meta['count'] = 1 + \substr_count($this->raw_data, self::SEPARATOR_ELEMENT); + } + + if (! $this->meta['count']) + $this->meta['messages'] = 0; + + return (int)$this->meta['count']; + } + + /** + * Returns number of all messages in the result + * + * @return int Number of elements + */ + public function count_messages() : int + { + if (null !== $this->meta['messages']) + return $this->meta['messages']; + + if (empty($this->raw_data)) { + $this->meta['messages'] = 0; + } + else { + $this->meta['messages'] = 1 + + \substr_count($this->raw_data, self::SEPARATOR_ELEMENT) + + \substr_count($this->raw_data, self::SEPARATOR_ITEM); + } + + if (0 == $this->meta['messages'] || 1 == $this->meta['messages']) + $this->meta['count'] = $this->meta['messages']; + + return $this->meta['messages']; + } + + /** + * Filters data set. Removes threads not listed in $roots list. + * + * @param array $roots List of IDs of thread roots. + */ + public function filter(array $roots) : void + { + $datalen = \strlen($this->raw_data); + $roots = \array_flip($roots); + $result = ''; + $start = 0; + + $this->meta = []; + $this->meta['count'] = 0; + + while (($pos = @\strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) + || ($start < $datalen && ($pos = $datalen)) + ) { + $len = $pos - $start; + $elem = \substr($this->raw_data, $start, $len); + $start = $pos + 1; + + // extract root message ID + if ($npos = \strpos($elem, self::SEPARATOR_ITEM)) { + $root = (int)\substr($elem, 0, $npos); + } + else { + $root = $elem; + } + + if (isset($roots[$root])) { + $this->meta['count']++; + $result .= self::SEPARATOR_ELEMENT . $elem; + } + } + + $this->raw_data = \ltrim($result, self::SEPARATOR_ELEMENT); + } + + /** + * Return IDs of all messages in the result. Threaded data will be flattened. + * + * @return array List of message identifiers + */ + public function get() : array + { + if (empty($this->raw_data)) { + return []; + } + + $regexp = '/(' . \preg_quote(self::SEPARATOR_ELEMENT, '/') + . '|' . \preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . \preg_quote(self::SEPARATOR_LEVEL, '/') + . ')/'; + + return \preg_split($regexp, $this->raw_data); + } + + /** + * Returns response parameters e.g. MAILBOX, ORDER + * + * @param string $param Parameter name + * + * @return array|string Response parameters or parameter value + */ + public function get_parameters(?string $param = null) + { + $params = []; + $params['MAILBOX'] = $this->mailbox; + $params['ORDER'] = $this->order; + + if ($param) { + return $params[$param]; + } + + return $params; + } + + /** + * Returns data as tree + * + * @return array Data tree + */ + public function get_tree() : array + { + $datalen = \strlen($this->raw_data); + $result = []; + $start = 0; + + while (($pos = @\strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) + || ($start < $datalen && ($pos = $datalen)) + ) { + $len = $pos - $start; + $elem = \substr($this->raw_data, $start, $len); + $items = \explode(self::SEPARATOR_ITEM, $elem); + $result[\array_shift($items)] = $this->build_thread($items); + $start = $pos + 1; + } + + return $result; + } + + /** + * Initializes object with IMAP command response + * + * @param string $data IMAP response string + */ + public function init(?string $data = null) : void + { + $this->meta = []; + + $data = \explode('*', (string)$data); + + // ...skip unilateral untagged server responses + for ($i = 0, $len = \count($data); $i < $len; $i++) { + if (\preg_match('/^ THREAD/i', $data[$i])) { + // valid response, initialize raw_data for is_error() + $this->raw_data = ''; + $data[$i] = \substr($data[$i], 7); + + break; + } + + unset($data[$i]); + } + + if (empty($data)) { + return; + } + + $data = \array_shift($data); + $data = \trim($data); + $data = \preg_replace('/[\r\n]/', '', $data); + $data = \preg_replace('/\s+/', ' ', $data); + + $this->raw_data = $this->parse_thread($data); + } + + /** + * Checks the result from IMAP command + * + * @return bool True if the result is an error, False otherwise + */ + public function is_error() : bool + { + return '' === $this->raw_data; + } + + /** + * Returns maximum message identifier in the result + * + * @return int Maximum message identifier + */ + public function max() + { + if (! isset($this->meta['max'])) { + $this->meta['max'] = (int)@\max($this->get()); + } + + return $this->meta['max']; + } + + /** + * Returns minimum message identifier in the result + * + * @return int Minimum message identifier + */ + public function min() + { + if (! isset($this->meta['min'])) { + $this->meta['min'] = (int)@\min($this->get()); + } + + return $this->meta['min']; + } + + /** + * THREAD=REFS sorting implementation (based on provided index) + * + * @param ResultIndex $index Sorted message identifiers + */ + public function sort($index) : void + { + $this->order = $index->get_parameters('ORDER'); + + if (empty($this->raw_data)) { + return; + } + + // when sorting search result it's good to make the index smaller + if ($index->count() != $this->count_messages()) { + $index->filter($this->get()); + } + + $result = \array_fill_keys($index->get(), null); + $datalen = \strlen($this->raw_data); + $start = 0; + + // Here we're parsing raw_data twice, we want only one big array + // in memory at a time + + // Assign roots + while (($pos = @\strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) + || ($start < $datalen && ($pos = $datalen)) + ) { + $len = $pos - $start; + $elem = \substr($this->raw_data, $start, $len); + $start = $pos + 1; + + $items = \explode(self::SEPARATOR_ITEM, $elem); + $root = (int)\array_shift($items); + + if ($root) { + $result[$root] = $root; + + foreach ($items as $item) { + [$lv, $id] = \explode(self::SEPARATOR_LEVEL, $item); + $result[$id] = $root; + } + } + } + + // get only unique roots + $result = \array_filter($result); // make sure there are no nulls + $result = \array_unique($result); + + // Re-sort raw data + $result = \array_fill_keys($result, null); + $start = 0; + + while (($pos = @\strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) + || ($start < $datalen && ($pos = $datalen)) + ) { + $len = $pos - $start; + $elem = \substr($this->raw_data, $start, $len); + $start = $pos + 1; + + $npos = \strpos($elem, self::SEPARATOR_ITEM); + $root = (int)($npos ? \substr($elem, 0, $npos) : $elem); + + $result[$root] = $elem; + } + + $this->raw_data = \implode(self::SEPARATOR_ELEMENT, $result); + } + + /** + * Converts part of the raw thread into an array + */ + protected function build_thread($items, $level = 1, &$pos = 0) + { + $result = []; + + for ($len = \count($items); $pos < $len; $pos++) { + [$lv, $id] = \explode(self::SEPARATOR_LEVEL, $items[$pos]); + + if ($level == $lv) { + $pos++; + $result[$id] = $this->build_thread($items, $level + 1, $pos); + } + else { + $pos--; + + break; + } + } + + return $result; + } + + /** + * Creates 'depth' and 'children' arrays from stored thread 'tree' data. + */ + protected function build_thread_data($data, &$depth, &$children, $level = 0) : void + { + foreach ((array)$data as $key => $val) { + $empty = empty($val) || ! \is_array($val); + $children[$key] = ! $empty; + $depth[$key] = $level; + + if (! $empty) { + $this->build_thread_data($val, $depth, $children, $level + 1); + } + } + } + + /** + * IMAP THREAD response parser + */ + protected function parse_thread(string $str, int $begin = 0, int $end = 0, int $depth = 0) : string + { + // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about + // 7 times instead :-) See comments on http://uk2.php.net/references and this article: + // http://derickrethans.nl/files/phparch-php-variables-article.pdf + $node = ''; + + if (! $end) { + $end = \strlen($str); + } + + // Let's try to store data in max. compacted stracture as a string, + // arrays handling is much more expensive + // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96)) + // -- 2 + // -- 3 + // \-- 6 + // |-- 4 + // | \-- 23 + // | + // \-- 44 + // \-- 7 + // \-- 96 + // + // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96 + + if ('(' != $str[$begin]) { + // find next bracket + $stop = $begin + \strcspn($str, '()', $begin, $end - $begin); + $messages = \explode(' ', \trim(\substr($str, $begin, $stop - $begin))); + + foreach ($messages as $msg) { + if ($msg) { + $node .= ($depth ? self::SEPARATOR_ITEM . $depth . self::SEPARATOR_LEVEL : '') . $msg; + $this->meta['messages']++; + $depth++; + } + } + + if ($stop < $end) { + $node .= $this->parse_thread($str, $stop, $end, $depth); + } + } + else { + $off = $begin; + + while ($off < $end) { + $start = $off; + $off++; + $n = 1; + + while ($n > 0) { + $p = \strpos($str, ')', $off); + + if (false === $p) { + // error, wrong structure, mismatched brackets in IMAP THREAD response + // @TODO: write error to the log or maybe set $this->raw_data = null; + return $node; + } + $p1 = \strpos($str, '(', $off); + + if (false !== $p1 && $p1 < $p) { + $off = $p1 + 1; + $n++; + } + else { + $off = $p + 1; + $n--; + } + } + + $thread = $this->parse_thread($str, $start + 1, $off - 1, $depth); + + if ($thread) { + if (! $depth) { + if ($node) { + $node .= self::SEPARATOR_ELEMENT; + } + } + $node .= $thread; + } + } + } + + return $node; + } +} diff --git a/src/Imap2/Roundcube/Utils.php b/src/Imap2/Roundcube/Utils.php new file mode 100644 index 0000000..995703d --- /dev/null +++ b/src/Imap2/Roundcube/Utils.php @@ -0,0 +1,147 @@ + | + * | Author: Aleksander Machniak | + * +-----------------------------------------------------------------------+ + */ + +namespace PHPFUI\Imap2\Roundcube; + +use PHPFUI\Imap2\DateTime; + +/** + * Utility class providing common functions + * + * @package Framework + * @subpackage Utils + */ +class Utils +{ + /** + * Clean up date string for strtotime() input + * + * @param string $date Date string + * + * @return string Date string + */ + public static function clean_datestr(string $date) : string + { + $date = \trim($date); + + // check for MS Outlook vCard date format YYYYMMDD + if (\preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) { + return \sprintf('%04d-%02d-%02d 00:00:00', (int)($m[1]), (int)($m[2]), (int)($m[3])); + } + + // Clean malformed data + $date = \preg_replace( + [ + '/\(.*\)/', // remove RFC comments + '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal + '/[^a-z0-9\x20\x09:\/\.+-]/i', // remove any invalid characters + '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names + ], + [ + '', + '\\1', + '', + '', + ], + $date + ); + + $date = \trim($date); + + // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here + if (\preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(\s.*)?$/', $date, $m)) { + $mdy = $m[2] > 12 && $m[1] <= 12; + $day = $mdy ? $m[2] : $m[1]; + $month = $mdy ? $m[1] : $m[2]; + $date = \sprintf('%04d-%02d-%02d%s', $m[3], $month, $day, $m[4] ?? ' 00:00:00'); + } + // I've found that YYYY.MM.DD is recognized wrong, so here's a fix + elseif (\preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})(\s.*)?$/', $date, $m)) { + $date = \sprintf('%04d-%02d-%02d%s', $m[1], $m[2], $m[3], $m[4] ?? ' 00:00:00'); + } + + return $date; + } + + /** + * Explode quoted string + * + * @param string $delimiter Delimiter expression string for preg_match() + * @param string $string Input string + * + * @return array String items + */ + public static function explode_quoted_string(string $delimiter, string $string) : array + { + $result = []; + $strlen = \strlen($string); + + for ($q = $p = $i = 0; $i < $strlen; $i++) { + if ('"' == $string[$i] && '\\' != $string[$i - 1]) { + $q = $q ? false : true; + } + elseif (! $q && \preg_match("/{$delimiter}/", $string[$i])) { + $result[] = \substr($string, $p, $i - $p); + $p = $i + 1; + } + } + + $result[] = (string)\substr($string, $p); + + return $result; + } + + /** + * Improved equivalent to strtotime() + * + * @param string $date Date string + * @param \DateTimeZone $timezone Timezone to use for DateTime object + * + * @return int Unix timestamp + */ + public static function strtotime(string $date, ?\DateTimeZone $timezone = null) : int + { + $date = self::clean_datestr($date); + $tzname = $timezone ? ' ' . $timezone->getName() : ''; + + // unix timestamp + if (\is_numeric($date)) { + return (int)$date; + } + + // It can be very slow when provided string is not a date and very long + if (\strlen($date) > 128) { + $date = \substr($date, 0, 128); + } + + // if date parsing fails, we have a date in non-rfc format. + // remove token from the end and try again + while (($ts = @\strtotime($date . $tzname)) === false || $ts < 0) { + if (($pos = \strrpos($date, ' ')) === false) { + break; + } + + $date = \rtrim(\substr($date, 0, $pos)); + } + + return (int)$ts; + } +} diff --git a/src/Imap2/Thread.php b/src/Imap2/Thread.php new file mode 100644 index 0000000..2b7e937 --- /dev/null +++ b/src/Imap2/Thread.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Thread + { + public static function thread(\IMAP\Connection $imap, int $flags = SE_FREE) : false | array + { + $client = $imap->getClient(); + $thread = $client->thread($imap->getMailboxName()); + + if (empty($thread->count())) + { + return false; + } + + return $thread->get(); + } + } diff --git a/src/Imap2/Timeout.php b/src/Imap2/Timeout.php new file mode 100644 index 0000000..f4cdffd --- /dev/null +++ b/src/Imap2/Timeout.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPFUI\Imap2; + +class Timeout + { + protected static array $timeout = []; + + public static function get(int $timeoutType) : int + { + return self::$timeout[$timeoutType]; + } + + public static function set(int $timeoutType, int $timeout = -1) : int | true + { + if (-1 == $timeout) + { + return self::get($timeoutType); + } + + self::$timeout[$timeoutType] = $timeout; + + return true; + } + } diff --git a/src/ImapHelpers.php b/src/ImapHelpers.php deleted file mode 100644 index b7d7234..0000000 --- a/src/ImapHelpers.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class ImapHelpers -{ - /** - * Convert a string contain a sequence of message id to and equivalent with uid. - * - * @param $imap - * @param $messageNums - * - * @return string - */ - public static function idToUid($imap, $messageNums) - { - $client = $imap->getClient(); - - $messages = $client->fetch($imap->getMailboxName(), $messageNums, false, ['UID']); - - $uid = []; - foreach ($messages as $message) { - $uid[] = $message->uid; - } - - return implode(',', $uid); - } - - /** - * Convert a string contain a sequence of uid(s) to an equivalent with id(s). - * - * @param $imap - * @param $messageUid - * - * @return string - */ - public static function uidToId($imap, $messageUid) - { - $client = $imap->getClient(); - - $messages = $client->fetch($imap->getMailboxName(), $messageUid, true, ['UID']); - - $id = []; - foreach ($messages as $message) { - $id[] = $message->id; - } - - return implode(',', $id); - } -} diff --git a/src/Mail.php b/src/Mail.php deleted file mode 100644 index 47e7a8c..0000000 --- a/src/Mail.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Mail -{ - /** - * Copy specified messages to a mailbox. - * - * @param $imap - * @param $messageNums - * @param $mailbox - * @param $flags - * - * @return false|mixed - */ - public static function copy($imap, $messageNums, $mailbox, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - if ($flags & CP_MOVE) { - return Mail::move($imap, $messageNums, $mailbox, $flags); - } - - $client = $imap->getClient(); - - if (!($flags & CP_UID)) { - $messageNums = ImapHelpers::idToUid($imap, $messageNums); - } - - $from = $imap->getMailboxName(); - $to = $mailbox; - - return $client->copy($messageNums, $from, $to); - } - - /** - * Move specified messages to a mailbox. - * - * @param $imap - * @param $messageNums - * @param $mailbox - * @param $flags - * - * @return false|mixed - */ - public static function move($imap, $messageNums, $mailbox, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - if (!($flags & CP_UID)) { - $messageNums = ImapHelpers::idToUid($imap, $messageNums); - } - - return $client->move($messageNums, $imap->getMailboxName(), $mailbox); - } - - /** - * Send an email message. - * - * @param $to - * @param $subject - * @param $message - * @param $additionalHeaders - * @param $cc - * @param $bcc - * @param $returnPath - * - * @return false|mixed - */ - public static function send($to, $subject, $message, $additionalHeaders = null, $cc = null, $bcc = null, $returnPath = null) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - if (!($options & ST_UID)) { - $messages = $client->fetch($imap->getMailboxName(), $sequence, false, ['UID']); - - $uid = []; - foreach ($messages as $message) { - $uid[] = $message->uid; - } - - $sequence = implode(',', $uid); - } - - $client->flag($imap->getMailboxName(), $sequence, strtoupper(substr($flag, 1))); - - return false; - } -} diff --git a/src/Mailbox.php b/src/Mailbox.php deleted file mode 100644 index 490fd2f..0000000 --- a/src/Mailbox.php +++ /dev/null @@ -1,354 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -use Javanile\Imap2\Roundcube\ImapClient; - -class Mailbox -{ - public static function check($imap) - { - if (is_a($imap, Connection::class)) { - $imap->selectMailbox(); - - $client = $imap->getClient(); - $status = $client->status($imap->getMailboxName(), ['MESSAGES', 'RECENT']); - - return (object) [ - 'Date' => date('D, j M Y G:i:s').' +0000 (UTC)', - 'Driver' => 'imap', - 'Mailbox' => $imap->getMailbox(), - 'Nmsgs' => intval($status['MESSAGES']), - 'Recent' => intval($status['RECENT']), - ]; - - } elseif (IMAP2_RETROFIT_MODE && Functions::isRetrofitResource($imap)) { - return imap_check($imap); - } - - trigger_error(Errors::invalidImapConnection(debug_backtrace(), 1, false), E_USER_WARNING); - - return false; - } - - public static function numMsg($imap) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $imap->selectMailbox(); - $client = $imap->getClient(); - - $status = $client->status($imap->getMailboxName(), ['MESSAGES']); - - return intval($status['MESSAGES']); - } - - public static function numRecent($imap) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - $imap->selectMailbox(); - - return (object) [ - 'Driver' => 'imap', - 'Mailbox' => $imap->getMailbox(), - 'Nmsgs' => $client->data['EXISTS'], - 'Recent' => $client->data['RECENT'], - ]; - } - - return imap_check($imap); - } - - public static function status($imap, $mailbox, $flags) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $mailboxName = Functions::getMailboxName($mailbox); - - $client = $imap->getClient(); - - $items = []; - - $statusKeys = [ - 'MESSAGES' => 'messages', - 'UNSEEN' => 'unseen', - 'RECENT' => 'recent', - 'UIDNEXT' => 'uidnext', - 'UIDVALIDITY' => 'uidvalidity', - ]; - - if ($flags & SA_MESSAGES || $flags & SA_ALL) { - $items[] = 'MESSAGES'; - } - if ($flags & SA_RECENT || $flags & SA_ALL) { - $items[] = 'RECENT'; - } - if ($flags & SA_UNSEEN || $flags & SA_ALL) { - $items[] = 'UNSEEN'; - } - if ($flags & SA_UIDNEXT || $flags & SA_ALL) { - $items[] = 'UIDNEXT'; - } - if ($flags & SA_UIDVALIDITY || $flags & SA_ALL) { - $items[] = 'UIDVALIDITY'; - } - - $status = $client->status($mailboxName, $items); - - if (empty($status)) { - return false; - } - - $returnStatus = []; - foreach ($status as $key => $value) { - $returnStatus[$statusKeys[$key]] = is_numeric($value) ? intval($value) : $value; - } - - return (object) $returnStatus; - } - - public static function mailboxMsgInfo($imap) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $imap->selectMailbox(); - $mailboxName = $imap->getMailboxName(); - - $status = $client->status($mailboxName, [ - 'MESSAGES', - 'UNSEEN', - 'RECENT', - 'UIDNEXT', - 'UIDVALIDITY' - ]); - - $mailboxInfo = [ - 'Unread' => intval($status['UNSEEN']), - 'Deleted' => 0, - 'Nmsgs' => intval($status['MESSAGES']), - 'Size' => 0, - 'Date' => date('D, j M Y G:i:s').' +0000 (UTC)', - 'Driver' => 'imap', - 'Mailbox' => $imap->getMailbox(), - 'Recent' => intval($status['RECENT']) - ]; - - return (object) $mailboxInfo; - } - - public static function list($imap, $reference, $pattern) - { - if (is_a($imap, Connection::class)) { - $referenceParts = explode('}', $reference); - $client = $imap->getClient(); - $return = []; - $mailboxes = $client->listMailboxes($referenceParts[1], $pattern); - foreach ($mailboxes as $mailbox) { - if (in_array('\\Noselect', $client->data['LIST'][$mailbox])) { - continue; - } - $return[] = $referenceParts[0].'}'.$mailbox; - } - - return $return; - } - - return imap_list($imap, $reference, $pattern); - } - - public static function listScan($imap, $reference, $pattern) - { - if (is_a($imap, Connection::class)) { - $referenceParts = explode('}', $reference); - $client = $imap->getClient(); - $return = []; - $mailboxes = $client->listMailboxes($referenceParts[1], $pattern); - foreach ($mailboxes as $mailbox) { - if (in_array('\\Noselect', $client->data['LIST'][$mailbox])) { - continue; - } - $return[] = $referenceParts[0].'}'.$mailbox; - } - - return $return; - } - - return imap_list($imap, $reference, $pattern); - } - - public static function getMailboxes($imap, $reference, $pattern) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $referenceParts = explode('}', $reference); - $client = $imap->getClient(); - #$client->setDebug(true); - $return = []; - $delimiter = $client->getHierarchyDelimiter(); - $mailboxes = $client->listMailboxes($referenceParts[1], $pattern); - foreach ($mailboxes as $mailbox) { - $attributesValue = Functions::getListAttributesValue($client->data['LIST'][$mailbox]); - if ($mailbox == '[Gmail]' && $imap->getHost() == 'imap.gmail.com') { - $attributesValue = 34; - } - $return[] = (object) [ - 'name' => $referenceParts[0].'}'.$mailbox, - 'attributes' => $attributesValue, - 'delimiter' => $delimiter, - ]; - } - - return $return; - } - - public static function createMailbox($imap, $mailbox) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - if ($mailbox[0] == '{') { - $mailbox = (string) \preg_replace('/^{.+}/', '', $mailbox); - } - - $success = $client->createFolder($mailbox); - - if (!$success) { - Errors::appendError($client->getRawLastLine()); - } - - return $success; - } - - public static function renameMailbox($imap, $from, $to) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - - return $client->createFolder($mailbox); - } - - return imap_createmailbox($imap, $mailbox); - } - - public static function deleteMailbox($imap, $mailbox) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - if ($mailbox[0] == '{') { - $mailbox = (string) \preg_replace('/^{.+}/', '', $mailbox); - } - - $result = $client->execute('DELETE', array($client->escape($mailbox)), ImapClient::COMMAND_RAW_LASTLINE); - - $success = $result[0] == ImapClient::ERROR_OK; - - if (!$success && $imap->getRegistryValue('mailbox', $mailbox, 'deleted')) { - Errors::appendError($result[1]); - } elseif (!$success) { - Errors::appendError("Can't delete mailbox {$mailbox}: no such mailbox"); - } else { - $imap->setRegistryValue('mailbox', $mailbox, 'deleted', true); - } - - return $success; - } - - /** - * Append a string message to a specified mailbox. - * - * @param $imap - * @param $folder - * @param $message - * @param $options - * @param $internalDate - * - * @return bool - */ - public static function append($imap, $folder, $message, $options = null, $internalDate = null) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $folderParts = explode('}', $folder); - $client = $imap->getClient(); - - $mailbox = empty($folderParts[1]) ? 'INBOX' : $folderParts[1]; - - $success = $client->append($mailbox, $message); - - return boolval($success); - } - - public static function getSubscribed($imap, $mailbox) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - - return $client->deleteFolder($mailbox); - } - - return imap_deletemailbox($imap, $mailbox); - } - - public static function listSubscribed($imap, $mailbox) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - - return $client->deleteFolder($mailbox); - } - - return imap_deletemailbox($imap, $mailbox); - } - - public static function subscribe($imap, $mailbox) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - - return $client->deleteFolder($mailbox); - } - - return imap_deletemailbox($imap, $mailbox); - } - - public static function unsubscribe($imap, $mailbox) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - - return $client->deleteFolder($mailbox); - } - - return imap_deletemailbox($imap, $mailbox); - } -} diff --git a/src/Message.php b/src/Message.php deleted file mode 100644 index 4393ebf..0000000 --- a/src/Message.php +++ /dev/null @@ -1,557 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Message -{ - /** - * Returns an array of messages matching the given search criteria. - * - * @param $imap - * @param $criteria - * @param $flags - * @param $charset - * - * @return array|false|mixed - */ - public static function search($imap, $criteria, $flags = SE_FREE, $charset = "") - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $result = $client->search($imap->getMailboxName(), $criteria, $flags & SE_UID); - - if (empty($result->count())) { - return false; - } - - $messages = $result->get(); - foreach ($messages as &$message) { - $message = is_numeric($message) ? intval($message) : $message; - } - - return $messages; - } - - public static function sort($imap, $criteria, $reverse, $flags = 0, $searchCriteria = null, $charset = null) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - #$client->setDebug(true); - - $result = $client->search($imap->getMailboxName(), $criteria, $flags & SE_UID); - - if (empty($result->count())) { - return false; - } - - $messages = $result->get(); - foreach ($messages as &$message) { - $message = is_numeric($message) ? intval($message) : $message; - } - - return $messages; - } - - return imap_sort($imap, $criteria, $reverse, $flags, $searchCriteria, $charset); - } - - public static function headerInfo($imap, $messageNum, $fromLength = 0, $subjectLength = 0, $defaultHost = null) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $messages = $client->fetch($imap->getMailboxName(), $messageNum, false, [ - 'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO CC REPLY-TO DATE SIZE REFERENCES)]', - 'ENVELOPE', - 'INTERNALDATE', - 'UID', - 'FLAGS', - 'RFC822.SIZE', - 'RFC822.HEADER' - ]); - - if (empty($messages)) { - return false; - } - - foreach ($messages as $message) { - return HeaderInfo::fromMessage($message, $defaultHost); - } - } - - public static function headers($imap) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $status = $client->status($imap->getMailboxName(), ['MESSAGES']); - if (empty($status['MESSAGES'])) { - return []; - } - - $sequence = '1:'.intval($status['MESSAGES']); - $messages = $client->fetch($imap->getMailboxName(), $sequence, false, [ - 'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO CC REPLYTO MESSAGEID DATE SIZE REFERENCES)]', - #'UID', - 'FLAGS', - 'INTERNALDATE', - 'RFC822.SIZE', - #'ENVELOPE', - 'RFC822.HEADER' - ]); - - if (empty($messages)) { - return []; - } - - $headers = []; - foreach ($messages as $message) { - $from = ' '; - if ($message->from != 'no_host') { - $from = imap_rfc822_parse_adrlist($message->from, 'no_host'); - $from = isset($from[0]->personal) ? $from[0]->personal : $message->from; - } - - $date = explode(' ', $message->internaldate); - $subject = empty($message->subject) ? ' ' : $message->subject; - $unseen = empty($message->flags['SEEN']) ? 'U' : ' '; - $flagged = empty($message->flags['FLAGGED']) ? ' ' : 'F'; - $answered = empty($message->flags['ANSWERED']) ? ' ' : 'A'; - $draft = empty($message->flags['DRAFT']) ? ' ' : 'D'; - $deleted = empty($message->flags['DELETED']) ? ' ' : 'X'; - - $header = ' ' . $unseen . $flagged . $answered . $draft . $deleted . ' ' - . str_pad($message->id, 3, ' ', STR_PAD_LEFT) . ')' . $date[0] .' ' . str_pad($from, 20, ' ') . ' ' - . substr($subject, 0, 25) . ' (' . $message->size . ' chars)'; - - $headers[] = $header; - } - - return $headers; - } - - public static function body($imap, $messageNum, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $isUid = boolval($flags & FT_UID); - - /** - TODO - The information on whether the fetch operation will be performed in PEEK mode should be obtained - from the $flags parameter. The $flags parameter needs to be moved to where the fetch() function is called, - and the PEEK mode needs to be decided whether the $flags variable contains the FT_PEEK value. - */ - $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK[TEXT]']); - - if ($isUid && is_array($messages)) { - $messages = Functions::keyBy('uid', $messages); - } - - return $messages[$messageNum]->bodypart['TEXT']; - } - - public static function fetchBody($imap, $messageNum, $section, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $isUid = boolval($flags & FT_UID); - $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK['.$section.']']); - - if (empty($messages)) { - trigger_error(Errors::badMessageNumber(debug_backtrace(), 1), E_USER_WARNING); - - return false; - } - - if ($isUid && is_array($messages)) { - $messages = Functions::keyBy('uid', $messages); - } - - if ($section) { - return $messages[$messageNum]->bodypart[$section]; - } - - return $messages[$messageNum]->body; - } - - public static function fetchMime($imap, $messageNum, $section, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - if ($messageNum <= 0) { - trigger_error(Errors::badMessageNumber(debug_backtrace(), 1), E_USER_WARNING); - - return false; - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $isUid = boolval($flags & FT_UID); - - $sectionKey = $section.'.MIME'; - $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK['.$sectionKey.']']); - - if (empty($messages)) { - return ""; - } - - if ($isUid && is_array($messages)) { - $messages = Functions::keyBy('uid', $messages); - } - - if ($section && isset($messages[$messageNum]->bodypart[$sectionKey])) { - return $messages[$messageNum]->bodypart[$sectionKey]; - } - - return $messages[$messageNum]->body; - } - - public static function saveBody($imap, $file, $messageNum, $section = "", $flags = 0) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - #$client->setDebug(true); - - $messages = $client->fetch($imap->getMailboxName(), $messageNum, false, ['BODY.PEEK['.$section.']']); - - $body = $section ? $messages[$messageNum]->bodypart[$section] : $messages[$messageNum]->body; - - return file_put_contents($file, $body); - } - - return imap_savebody($imap, $file, $messageNum, $section, $flags); - } - - public static function fetchStructure($imap, $messageNum, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $isUid = boolval($flags & FT_UID); - - $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODYSTRUCTURE']); - - if (empty($messages)) { - return false; - } - - foreach ($messages as $message) { - return BodyStructure::fromMessage($message); - } - } - - public static function bodyStruct($imap, $messageNum, $section) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $messages = $client->fetch($imap->getMailboxName(), $messageNum, false, ['BODY.PEEK['.$section.']']); - - if ($section) { - return $messages[$messageNum]->bodypart[$section]; - } - - return $messages[$messageNum]->body; - } - - public static function fetchHeader($imap, $messageNum, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - /* - * FT_UID - The message_num argument is a UID - FT_INTERNAL - The return string is in "internal" format, without any attempt to canonicalize to CRLF newlines - FT_PREFETCHTEXT - * */ - - $client = $imap->getClient(); - #$client->setDebug(true); - - $isUid = boolval($flags & FT_UID); - - $messages = $client->fetch($imap->getMailboxName(), $messageNum, $isUid, ['BODY.PEEK[HEADER]']); - - if (empty($messages)) { - return false; - } - - foreach ($messages as $message) { - return $message->bodypart['HEADER'] ?? false; - } - } - - public static function fetchOverview($imap, $sequence, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - #$client->setDebug(true); - - $isUid = (bool)($flags & FT_UID); - $messages = $client->fetch($imap->getMailboxName(), $sequence, $isUid, [ - 'BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO CC REPLYTO MESSAGEID DATE SIZE REFERENCES)]', - 'UID', - 'FLAGS', - 'INTERNALDATE', - 'RFC822.SIZE', - 'ENVELOPE', - 'RFC822.HEADER' - ]); - - - if (false === $messages) { - return []; - } - - if ($sequence != '*' && count($messages) < Functions::expectedNumberOfMessages($sequence)) { - return []; - } - - $overview = []; - foreach ($messages as $message) { - $messageEntry = (object) [ - 'subject' => $message->envelope[1], - 'from' => Functions::writeAddressFromEnvelope($message->envelope[2]), - 'to' => $message->get('to'), - 'date' => $message->envelope[0], - 'message_id' => $message->envelope[9], - 'references' => $message->references, - 'in_reply_to' => $message->envelope[8], - 'size' => $message->size, - 'uid' => $message->uid, - 'msgno' => $message->id, - 'recent' => intval($message->flags['RECENT'] ?? 0), - 'flagged' => intval($message->flags['FLAGGED'] ?? 0), - 'answered' => intval($message->flags['ANSWERED'] ?? 0), - 'deleted' => intval($message->flags['DELETED'] ?? 0), - 'seen' => intval($message->flags['SEEN'] ?? 0), - 'draft' => intval($message->flags['DRAFT'] ?? 0), - 'udate' => strtotime($message->internaldate), - ]; - - if (empty($messageEntry->subject)) { - unset($messageEntry->subject); - } - - if (empty($messageEntry->references)) { - unset($messageEntry->references); - } - - if (empty($messageEntry->in_reply_to)) { - unset($messageEntry->in_reply_to); - } - - if (empty($messageEntry->to)) { - unset($messageEntry->to); - } - - $overview[] = $messageEntry; - } - - return $overview; - } - - public static function fetchUids($imap, string $sequence, int $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - $isUid = boolval($flags & FT_UID); - $messages = $client->fetch($imap->getMailboxName(), $sequence, $isUid, ['UID']); - - if ($sequence != '*' && count($messages) < Functions::expectedNumberOfMessages($sequence)) { - return false; - } - - return $messages; - } - - public static function delete($imap, $messageNums, $flags = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - $messages = $client->fetch($imap->getMailboxName(), $messageNums, false, ['UID']); - - $uid = []; - foreach ($messages as $message) { - $uid[] = $message->uid; - } - - $client->flag($imap->getMailboxName(), implode(',', $uid), $client->flags['DELETED']); - - return true; - } - - public static function undelete($imap, $messageNums, $flags = 0) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - #$client->setDebug(true); - - $messages = $client->fetch($imap->getMailboxName(), $messageNums, false, ['UID']); - foreach ($messages as $message) { - $client->unflag($imap->getMailboxName(), $message->uid, $client->flags['DELETED']); - } - - return true; - } - - return imap_undelete($imap, $messageNums, $flags); - } - - public static function expunge($imap) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - - return $client->expunge($imap->getMailboxName()); - } - - return imap_expunge($imap); - } - - /** - * Sets flags on messages. - * - * @param $imap - * @param $sequence - * @param $flag - * @param $options - * - * @return bool - */ - public static function setFlagFull($imap, $sequence, $flag, $options = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - if (!($options & ST_UID)) { - $messages = $client->fetch($imap->getMailboxName(), $sequence, false, ['UID']); - - $uid = []; - foreach ($messages as $message) { - $uid[] = $message->uid; - } - - $sequence = implode(',', $uid); - } - - return $client->flag($imap->getMailboxName(), $sequence, strtoupper(substr($flag, 1))); - } - - /** - * Clears flags on messages. - * - * @param $imap - * @param $sequence - * @param $flag - * @param $options - * - * @return false|string - */ - public static function clearFlagFull($imap, $sequence, $flag, $options = 0) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - if (!($options & ST_UID)) { - $messages = $client->fetch($imap->getMailboxName(), $sequence, false, ['UID']); - - $uid = []; - foreach ($messages as $message) { - $uid[] = $message->uid; - } - - $sequence = implode(',', $uid); - } - - $client->unflag($imap->getMailboxName(), $sequence, strtoupper(substr($flag, 1))); - - return false; - } - - public static function msgno($imap, $messageUid) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $client = $imap->getClient(); - - $msgNo = ImapHelpers::uidToId($imap, $messageUid); - - return is_numeric($msgNo) ? intval($msgNo) : $msgNo; - } - - public static function uid($imap, $messageNum) - { - if (!is_a($imap, Connection::class)) { - return Errors::invalidImapConnection(debug_backtrace(), 1, false); - } - - $uid = ImapHelpers::idToUid($imap, $messageNum); - - return is_numeric($uid) ? intval($uid) : $uid; - } -} diff --git a/src/Polyfill.php b/src/Polyfill.php deleted file mode 100644 index 1df6443..0000000 --- a/src/Polyfill.php +++ /dev/null @@ -1,121 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -use ZBateson\MailMimeParser\Message; -use ZBateson\MailMimeParser\Header\HeaderConsts; - -class Polyfill -{ - public static function convert8bit($string) - { - return $string; - } - - public static function mimeHeaderDecode($string) - { - return $string; - } - - public static function mutf7ToUtf8($string) - { - return $string; - } - - public static function qPrint($string) - { - return $string; - } - - public static function rfc822ParseAdrList($string, $defaultHost) - { - $message = Message::from('To: '.$string, false); - - return Functions::getAddressObjectList( - $message->getHeader(HeaderConsts::TO)->getAddresses(), - $defaultHost - ); - } - - /** - * - * @param $headers - * @param $defaultHostname - * - * @return mixed - */ - public static function rfc822ParseHeaders($headers, $defaultHost = 'UNKNOWN') - { - $message = Message::from($headers, false); - - $date = $message->getHeaderValue(HeaderConsts::DATE); - $subject = $message->getHeaderValue(HeaderConsts::SUBJECT); - - $hasReplyTo = $message->getHeader(HeaderConsts::REPLY_TO) !== null; - $hasSender = $message->getHeader(HeaderConsts::SENDER) !== null; - - return (object) [ - 'date' => $date, - 'Date' => $date, - 'subject' => $subject, - 'Subject' => $subject, - 'message_id' => '<'.$message->getHeaderValue(HeaderConsts::MESSAGE_ID).'>', - 'toaddress' => $message->getHeaderValue(HeaderConsts::TO), - 'to' => Functions::getAddressObjectList($message->getHeader(HeaderConsts::TO)->getAddresses()), - 'fromaddress' => $message->getHeaderValue(HeaderConsts::FROM), - 'from' => Functions::getAddressObjectList($message->getHeader(HeaderConsts::FROM)->getAddresses()), - 'reply_toaddress' => $message->getHeaderValue($hasReplyTo ? HeaderConsts::REPLY_TO : HeaderConsts::FROM), - 'reply_to' => Functions::getAddressObjectList($message->getHeader($hasReplyTo ? HeaderConsts::REPLY_TO : HeaderConsts::FROM)->getAddresses()), - 'senderaddress' => $message->getHeaderValue($hasSender ? HeaderConsts::SENDER : HeaderConsts::FROM), - 'sender' => Functions::getAddressObjectList($message->getHeader($hasSender ? HeaderConsts::SENDER : HeaderConsts::FROM)->getAddresses()), - ]; - } - - public static function rfc822WriteHeaders($mailbox, $hostname, $personal) - { - $ret = $mailbox; - if (!empty($hostname)) - { - $ret .= '@' . $hostname; - } -// if (!empty($personal)) -// { -// $ret .= ' <' . $personal . '>'; -// } - return $ret; - } - - public static function utf7Decode($string) - { - return mb_convert_decoding($string, "UTF7-IMAP", "UTF-8"); - } - - public static function utf7Encode($string) - { - return mb_convert_encoding($string, "UTF-8", "UTF7-IMAP"); - } - - public static function utf8ToMutf7($string) - { - return $string; - } - - public static function utf8($string) - { - return $string; - } - - public static function mailCompose($envelope, $bodies) - { - return false; - } -} diff --git a/src/Roundcube/Charset.php b/src/Roundcube/Charset.php deleted file mode 100644 index 99222a8..0000000 --- a/src/Roundcube/Charset.php +++ /dev/null @@ -1,915 +0,0 @@ - | -| | -| Licensed under the GNU General Public License version 3 or | -| any later version with exceptions for skins & plugins. | -| See the README file for a full license statement. | -| | -| PURPOSE: | -| Provide charset conversion functionality | -+-----------------------------------------------------------------------+ -| Author: Thomas Bruederli | -| Author: Aleksander Machniak | -| Author: Edmund Grimley Evans | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -use Javanile\Imap2\ErrorException; -use Javanile\Imap2\rcube; - -/** - * Character sets conversion functionality - * - * @package Framework - * @subpackage Core - */ -class Charset -{ - // Aliases: some of them from HTML5 spec. - static public $aliases = array( - 'USASCII' => 'WINDOWS-1252', - 'ANSIX31101983' => 'WINDOWS-1252', - 'ANSIX341968' => 'WINDOWS-1252', - 'UNKNOWN8BIT' => 'ISO-8859-15', - 'UNKNOWN' => 'ISO-8859-15', - 'USERDEFINED' => 'ISO-8859-15', - 'KSC56011987' => 'EUC-KR', - 'GB2312' => 'GBK', - 'GB231280' => 'GBK', - 'UNICODE' => 'UTF-8', - 'UTF7IMAP' => 'UTF7-IMAP', - 'TIS620' => 'WINDOWS-874', - 'ISO88599' => 'WINDOWS-1254', - 'ISO885911' => 'WINDOWS-874', - 'MACROMAN' => 'MACINTOSH', - '77' => 'MAC', - '128' => 'SHIFT-JIS', - '129' => 'CP949', - '130' => 'CP1361', - '134' => 'GBK', - '136' => 'BIG5', - '161' => 'WINDOWS-1253', - '162' => 'WINDOWS-1254', - '163' => 'WINDOWS-1258', - '177' => 'WINDOWS-1255', - '178' => 'WINDOWS-1256', - '186' => 'WINDOWS-1257', - '204' => 'WINDOWS-1251', - '222' => 'WINDOWS-874', - '238' => 'WINDOWS-1250', - 'MS950' => 'CP950', - 'WINDOWS949' => 'UHC', - ); - - /** - * Windows codepages - * - * @var array - */ - static public $windows_codepages = array( - 37 => 'IBM037', // IBM EBCDIC US-Canada - 437 => 'IBM437', // OEM United States - 500 => 'IBM500', // IBM EBCDIC International - 708 => 'ASMO-708', // Arabic (ASMO 708) - 720 => 'DOS-720', // Arabic (Transparent ASMO); Arabic (DOS) - 737 => 'IBM737', // OEM Greek (formerly 437G); Greek (DOS) - 775 => 'IBM775', // OEM Baltic; Baltic (DOS) - 850 => 'IBM850', // OEM Multilingual Latin 1; Western European (DOS) - 852 => 'IBM852', // OEM Latin 2; Central European (DOS) - 855 => 'IBM855', // OEM Cyrillic (primarily Russian) - 857 => 'IBM857', // OEM Turkish; Turkish (DOS) - 858 => 'IBM00858', // OEM Multilingual Latin 1 + Euro symbol - 860 => 'IBM860', // OEM Portuguese; Portuguese (DOS) - 861 => 'IBM861', // OEM Icelandic; Icelandic (DOS) - 862 => 'DOS-862', // OEM Hebrew; Hebrew (DOS) - 863 => 'IBM863', // OEM French Canadian; French Canadian (DOS) - 864 => 'IBM864', // OEM Arabic; Arabic (864) - 865 => 'IBM865', // OEM Nordic; Nordic (DOS) - 866 => 'cp866', // OEM Russian; Cyrillic (DOS) - 869 => 'IBM869', // OEM Modern Greek; Greek, Modern (DOS) - 870 => 'IBM870', // IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2 - 874 => 'windows-874', // ANSI/OEM Thai (ISO 8859-11); Thai (Windows) - 875 => 'cp875', // IBM EBCDIC Greek Modern - 932 => 'shift_jis', // ANSI/OEM Japanese; Japanese (Shift-JIS) - 936 => 'gb2312', // ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312) - 950 => 'big5', // ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5) - 1026 => 'IBM1026', // IBM EBCDIC Turkish (Latin 5) - 1047 => 'IBM01047', // IBM EBCDIC Latin 1/Open System - 1140 => 'IBM01140', // IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro) - 1141 => 'IBM01141', // IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro) - 1142 => 'IBM01142', // IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro) - 1143 => 'IBM01143', // IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro) - 1144 => 'IBM01144', // IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro) - 1145 => 'IBM01145', // IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro) - 1146 => 'IBM01146', // IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro) - 1147 => 'IBM01147', // IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro) - 1148 => 'IBM01148', // IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro) - 1149 => 'IBM01149', // IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro) - 1200 => 'UTF-16', // Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications - 1201 => 'UTF-16BE', // Unicode UTF-16, big endian byte order; available only to managed applications - 1250 => 'windows-1250', // ANSI Central European; Central European (Windows) - 1251 => 'windows-1251', // ANSI Cyrillic; Cyrillic (Windows) - 1252 => 'windows-1252', // ANSI Latin 1; Western European (Windows) - 1253 => 'windows-1253', // ANSI Greek; Greek (Windows) - 1254 => 'windows-1254', // ANSI Turkish; Turkish (Windows) - 1255 => 'windows-1255', // ANSI Hebrew; Hebrew (Windows) - 1256 => 'windows-1256', // ANSI Arabic; Arabic (Windows) - 1257 => 'windows-1257', // ANSI Baltic; Baltic (Windows) - 1258 => 'windows-1258', // ANSI/OEM Vietnamese; Vietnamese (Windows) - 10000 => 'macintosh', // MAC Roman; Western European (Mac) - 12000 => 'UTF-32', // Unicode UTF-32, little endian byte order; available only to managed applications - 12001 => 'UTF-32BE', // Unicode UTF-32, big endian byte order; available only to managed applications - 20127 => 'US-ASCII', // US-ASCII (7-bit) - 20273 => 'IBM273', // IBM EBCDIC Germany - 20277 => 'IBM277', // IBM EBCDIC Denmark-Norway - 20278 => 'IBM278', // IBM EBCDIC Finland-Sweden - 20280 => 'IBM280', // IBM EBCDIC Italy - 20284 => 'IBM284', // IBM EBCDIC Latin America-Spain - 20285 => 'IBM285', // IBM EBCDIC United Kingdom - 20290 => 'IBM290', // IBM EBCDIC Japanese Katakana Extended - 20297 => 'IBM297', // IBM EBCDIC France - 20420 => 'IBM420', // IBM EBCDIC Arabic - 20423 => 'IBM423', // IBM EBCDIC Greek - 20424 => 'IBM424', // IBM EBCDIC Hebrew - 20838 => 'IBM-Thai', // IBM EBCDIC Thai - 20866 => 'koi8-r', // Russian (KOI8-R); Cyrillic (KOI8-R) - 20871 => 'IBM871', // IBM EBCDIC Icelandic - 20880 => 'IBM880', // IBM EBCDIC Cyrillic Russian - 20905 => 'IBM905', // IBM EBCDIC Turkish - 20924 => 'IBM00924', // IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) - 20932 => 'EUC-JP', // Japanese (JIS 0208-1990 and 0212-1990) - 20936 => 'cp20936', // Simplified Chinese (GB2312); Chinese Simplified (GB2312-80) - 20949 => 'cp20949', // Korean Wansung - 21025 => 'cp1025', // IBM EBCDIC Cyrillic Serbian-Bulgarian - 21866 => 'koi8-u', // Ukrainian (KOI8-U); Cyrillic (KOI8-U) - 28591 => 'iso-8859-1', // ISO 8859-1 Latin 1; Western European (ISO) - 28592 => 'iso-8859-2', // ISO 8859-2 Central European; Central European (ISO) - 28593 => 'iso-8859-3', // ISO 8859-3 Latin 3 - 28594 => 'iso-8859-4', // ISO 8859-4 Baltic - 28595 => 'iso-8859-5', // ISO 8859-5 Cyrillic - 28596 => 'iso-8859-6', // ISO 8859-6 Arabic - 28597 => 'iso-8859-7', // ISO 8859-7 Greek - 28598 => 'iso-8859-8', // ISO 8859-8 Hebrew; Hebrew (ISO-Visual) - 28599 => 'iso-8859-9', // ISO 8859-9 Turkish - 28603 => 'iso-8859-13', // ISO 8859-13 Estonian - 28605 => 'iso-8859-15', // ISO 8859-15 Latin 9 - 38598 => 'iso-8859-8-i', // ISO 8859-8 Hebrew; Hebrew (ISO-Logical) - 50220 => 'iso-2022-jp', // ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS) - 50221 => 'csISO2022JP', // ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana) - 50222 => 'iso-2022-jp', // ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI) - 50225 => 'iso-2022-kr', // ISO 2022 Korean - 51932 => 'EUC-JP', // EUC Japanese - 51936 => 'EUC-CN', // EUC Simplified Chinese; Chinese Simplified (EUC) - 51949 => 'EUC-KR', // EUC Korean - 52936 => 'hz-gb-2312', // HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ) - 54936 => 'GB18030', // Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030) - 65000 => 'UTF-7', - 65001 => 'UTF-8', - ); - - /** - * Catch an error and throw an exception. - * - * @param int $errno Level of the error - * @param string $errstr Error message - */ - public static function error_handler($errno, $errstr) - { - throw new ErrorException($errstr, 0, $errno); - } - - /** - * Parse and validate charset name string. - * Sometimes charset string is malformed, there are also charset aliases, - * but we need strict names for charset conversion (specially utf8 class) - * - * @param string $input Input charset name - * - * @return string The validated charset name - */ - public static function parse_charset($input) - { - static $charsets = array(); - $charset = strtoupper($input); - - if (isset($charsets[$input])) { - return $charsets[$input]; - } - - $charset = preg_replace(array( - '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO - '/\$.*$/', // e.g. _ISO-8859-JP$SIO - '/UNICODE-1-1-*/', // RFC1641/1642 - '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8) - '/\*.*$/' // lang code according to RFC 2231.5 - ), '', $charset); - - if ($charset == 'BINARY') { - return $charsets[$input] = null; - } - - // allow A-Z and 0-9 only - $str = preg_replace('/[^A-Z0-9]/', '', $charset); - - if (isset(self::$aliases[$str])) { - $result = self::$aliases[$str]; - } - // UTF - else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) { - $result = 'UTF-' . $m[1] . @$m[2]; - } - // ISO-8859 - else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) { - $iso = 'ISO-8859-' . ($m[1] ?: 1); - // some clients sends windows-1252 text as latin1, - // it is safe to use windows-1252 for all latin1 - $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso; - } - // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE - else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) { - $result = 'WINDOWS-' . $m[2]; - } - // LATIN - else if (preg_match('/LATIN(.*)/', $str, $m)) { - $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10, - '7' => 13, '8' => 14, '9' => 15, '10' => 16, - 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8 - ); - - // some clients sends windows-1252 text as latin1, - // it is safe to use windows-1252 for all latin1 - if ($m[1] == 1) { - $result = 'WINDOWS-1252'; - } - // if iconv is not supported we need ISO labels, it's also safe for iconv - else if (!empty($aliases[$m[1]])) { - $result = 'ISO-8859-'.$aliases[$m[1]]; - } - // iconv requires conversion of e.g. LATIN-1 to LATIN1 - else { - $result = $str; - } - } - else { - $result = $charset; - } - - $charsets[$input] = $result; - - return $result; - } - - /** - * Convert a string from one charset to another. - * Uses mbstring and iconv functions if possible - * - * @param string $str Input string - * @param string $from Suspected charset of the input string - * @param string $to Target charset to convert to; defaults to RCUBE_CHARSET - * - * @return string Converted string - */ - public static function convert($str, $from, $to = null) - { - static $iconv_options = null; - static $mbstring_sc = null; - - $to = empty($to) ? IMAP2_CHARSET : strtoupper($to); - $from = self::parse_charset($from); - - // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) - // In that case we can just skip the conversion (use UTF-8) - if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) { - $from = 'UTF-8'; - } - - if ($from == $to || empty($str) || empty($from)) { - return $str; - } - - if ($iconv_options === null) { - if (function_exists('iconv')) { - // ignore characters not available in output charset - $iconv_options = '//IGNORE'; - if (iconv('', $iconv_options, '') === false) { - // iconv implementation does not support options - $iconv_options = ''; - } - } - else { - $iconv_options = false; - } - } - - // convert charset using iconv module - if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP' - && $from !== 'ISO-2022-JP' - ) { - // throw an exception if iconv reports an illegal character in input - // it means that input string has been truncated - #set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE); - try { - $out = iconv($from, $to . $iconv_options, $str); - } - catch (ErrorException $e) { - $out = false; - } - #restore_error_handler(); - - if ($out !== false) { - return $out; - } - } - - if ($mbstring_sc === null) { - $mbstring_sc = extension_loaded('mbstring') ? mb_substitute_character() : false; - } - - // convert charset using mbstring module - if ($mbstring_sc !== false) { - $aliases = array( - 'WINDOWS-1257' => 'ISO-8859-13', - 'US-ASCII' => 'ASCII', - 'ISO-2022-JP' => 'ISO-2022-JP-MS', - ); - - $mb_from = $aliases[$from] ?: $from; - $mb_to = $aliases[$to] ?: $to; - - // Do the same as //IGNORE with iconv - mb_substitute_character('none'); - - // throw an exception if mbstring reports an illegal character in input - // using mb_check_encoding() is much slower - #set_error_handler(array('rcube_charset', 'error_handler'), E_WARNING); - try { - $out = mb_convert_encoding($str, $mb_to, $mb_from); - } - catch (ErrorException $e) { - $out = false; - } - #restore_error_handler(); - - mb_substitute_character($mbstring_sc); - - if ($out !== false) { - return $out; - } - } - - // convert charset using bundled classes/functions - if ($to == 'UTF-8') { - if ($from == 'UTF7-IMAP') { - if ($out = self::utf7imap_to_utf8($str)) { - return $out; - } - } - else if ($from == 'UTF-7') { - if ($out = self::utf7_to_utf8($str)) { - return $out; - } - } - } - - // encode string for output - if ($from == 'UTF-8') { - // @TODO: we need a function for UTF-7 (RFC2152) conversion - if ($to == 'UTF7-IMAP' || $to == 'UTF-7') { - if ($out = self::utf8_to_utf7imap($str)) { - return $out; - } - } - } - - if (!isset($out)) { - trigger_error("No suitable function found for '$from' to '$to' conversion"); - } - - // return original string - return $str; - } - - /** - * Converts string from standard UTF-7 (RFC 2152) to UTF-8. - * - * @param string $str Input string (UTF-7) - * - * @return string Converted string (UTF-8) - */ - public static function utf7_to_utf8($str) - { - $Index_64 = array( - 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0, - 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0, - 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, - 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, - 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, - 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, - ); - - $u7len = strlen($str); - $str = strval($str); - $res = ''; - - for ($i=0; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - if ($u7 == '+') { - $i++; - $u7len--; - $ch = ''; - - for (; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - - if (!$Index_64[ord($u7)]) { - break; - } - - $ch .= $u7; - } - - if ($ch == '') { - if ($u7 == '-') { - $res .= '+'; - } - - continue; - } - - $res .= self::utf16_to_utf8(base64_decode($ch)); - } - else { - $res .= $u7; - } - } - - return $res; - } - - /** - * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion) - * - * @param string $str Input string - * - * @return string The converted string - */ - public static function utf16_to_utf8($str) - { - $len = strlen($str); - $dec = ''; - - for ($i = 0; $i < $len; $i += 2) { - $c = ord($str[$i]) << 8 | ord($str[$i + 1]); - if ($c >= 0x0001 && $c <= 0x007F) { - $dec .= chr($c); - } - else if ($c > 0x07FF) { - $dec .= chr(0xE0 | (($c >> 12) & 0x0F)); - $dec .= chr(0x80 | (($c >> 6) & 0x3F)); - $dec .= chr(0x80 | (($c >> 0) & 0x3F)); - } - else { - $dec .= chr(0xC0 | (($c >> 6) & 0x1F)); - $dec .= chr(0x80 | (($c >> 0) & 0x3F)); - } - } - - return $dec; - } - - /** - * Convert the data ($str) from RFC 2060's UTF-7 to UTF-8. - * If input data is invalid, return the original input string. - * RFC 2060 obviously intends the encoding to be unique (see - * point 5 in section 5.1.3), so we reject any non-canonical - * form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead - * of &AMAAwA-). - * - * Translated from C to PHP by Thomas Bruederli - * - * @param string $str Input string (UTF7-IMAP) - * - * @return string Output string (UTF-8) - */ - public static function utf7imap_to_utf8($str) - { - $Index_64 = array( - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, 63,-1,-1,-1, - 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, - 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, - -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, - 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 - ); - - $u7len = strlen($str); - $str = strval($str); - $p = ''; - $err = ''; - - for ($i=0; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - if ($u7 == '&') { - $i++; - $u7len--; - $u7 = $str[$i]; - - if ($u7len && $u7 == '-') { - $p .= '&'; - continue; - } - - $ch = 0; - $k = 10; - for (; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - - if ((ord($u7) & 0x80) || ($b = $Index_64[ord($u7)]) == -1) { - break; - } - - if ($k > 0) { - $ch |= $b << $k; - $k -= 6; - } - else { - $ch |= $b >> (-$k); - if ($ch < 0x80) { - // Printable US-ASCII - if (0x20 <= $ch && $ch < 0x7f) { - return $err; - } - $p .= chr($ch); - } - else if ($ch < 0x800) { - $p .= chr(0xc0 | ($ch >> 6)); - $p .= chr(0x80 | ($ch & 0x3f)); - } - else { - $p .= chr(0xe0 | ($ch >> 12)); - $p .= chr(0x80 | (($ch >> 6) & 0x3f)); - $p .= chr(0x80 | ($ch & 0x3f)); - } - - $ch = ($b << (16 + $k)) & 0xffff; - $k += 10; - } - } - - // Non-zero or too many extra bits - if ($ch || $k < 6) { - return $err; - } - - // BASE64 not properly terminated - if (!$u7len || $u7 != '-') { - return $err; - } - - // Adjacent BASE64 sections - if ($u7len > 2 && $str[$i+1] == '&' && $str[$i+2] != '-') { - return $err; - } - } - // Not printable US-ASCII - else if (ord($u7) < 0x20 || ord($u7) >= 0x7f) { - return $err; - } - else { - $p .= $u7; - } - } - - return $p; - } - - /** - * Convert the data ($str) from UTF-8 to RFC 2060's UTF-7. - * Unicode characters above U+FFFF are replaced by U+FFFE. - * If input data is invalid, return an empty string. - * - * Translated from C to PHP by Thomas Bruederli - * - * @param string $str Input string (UTF-8) - * - * @return string Output string (UTF7-IMAP) - */ - public static function utf8_to_utf7imap($str) - { - $B64Chars = array( - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', - 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', - 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', '+', ',' - ); - - $u8len = strlen($str); - $base64 = 0; - $i = 0; - $p = ''; - $err = ''; - - while ($u8len) { - $u8 = $str[$i]; - $c = ord($u8); - - if ($c < 0x80) { - $ch = $c; - $n = 0; - } - else if ($c < 0xc2) { - return $err; - } - else if ($c < 0xe0) { - $ch = $c & 0x1f; - $n = 1; - } - else if ($c < 0xf0) { - $ch = $c & 0x0f; - $n = 2; - } - else if ($c < 0xf8) { - $ch = $c & 0x07; - $n = 3; - } - else if ($c < 0xfc) { - $ch = $c & 0x03; - $n = 4; - } - else if ($c < 0xfe) { - $ch = $c & 0x01; - $n = 5; - } - else { - return $err; - } - - $i++; - $u8len--; - - if ($n > $u8len) { - return $err; - } - - for ($j=0; $j < $n; $j++) { - $o = ord($str[$i+$j]); - if (($o & 0xc0) != 0x80) { - return $err; - } - $ch = ($ch << 6) | ($o & 0x3f); - } - - if ($n > 1 && !($ch >> ($n * 5 + 1))) { - return $err; - } - - $i += $n; - $u8len -= $n; - - if ($ch < 0x20 || $ch >= 0x7f) { - if (!$base64) { - $p .= '&'; - $base64 = 1; - $b = 0; - $k = 10; - } - if ($ch & ~0xffff) { - $ch = 0xfffe; - } - - $p .= $B64Chars[($b | $ch >> $k)]; - $k -= 6; - for (; $k >= 0; $k -= 6) { - $p .= $B64Chars[(($ch >> $k) & 0x3f)]; - } - - $b = ($ch << (-$k)) & 0x3f; - $k += 16; - } - else { - if ($base64) { - if ($k > 10) { - $p .= $B64Chars[$b]; - } - $p .= '-'; - $base64 = 0; - } - - $p .= chr($ch); - if (chr($ch) == '&') { - $p .= '-'; - } - } - } - - if ($base64) { - if ($k > 10) { - $p .= $B64Chars[$b]; - } - $p .= '-'; - } - - return $p; - } - - /** - * A method to guess character set of a string. - * - * @param string $string String - * @param string $failover Default result for failover - * @param string $language User language - * - * @return string Charset name - */ - public static function detect($string, $failover = null, $language = null) - { - if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian - if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian - if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE'; // Big Endian - if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE'; // Little Endian - if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8'; - - // heuristics - if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE'; - if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE'; - if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE'; - if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE'; - - if (empty($language)) { - $rcube = rcube::get_instance(); - $language = $rcube->get_user_language(); - } - - // Prioritize charsets according to current language (#1485669) - switch ($language) { - case 'ja_JP': - $prio = array('ISO-2022-JP', 'JIS', 'UTF-8', 'EUC-JP', 'eucJP-win', 'SJIS', 'SJIS-win'); - break; - - case 'zh_CN': - case 'zh_TW': - $prio = array('UTF-8', 'BIG-5', 'GB2312', 'EUC-TW'); - break; - - case 'ko_KR': - $prio = array('UTF-8', 'EUC-KR', 'ISO-2022-KR'); - break; - - case 'ru_RU': - $prio = array('UTF-8', 'WINDOWS-1251', 'KOI8-R'); - break; - - case 'tr_TR': - $prio = array('UTF-8', 'ISO-8859-9', 'WINDOWS-1254'); - break; - } - - // mb_detect_encoding() is not reliable for some charsets (#1490135) - // use mb_check_encoding() to make charset priority lists really working - if ($prio && function_exists('mb_check_encoding')) { - foreach ($prio as $encoding) { - if (mb_check_encoding($string, $encoding)) { - return $encoding; - } - } - } - - if (function_exists('mb_detect_encoding')) { - if (!$prio) { - $prio = array('UTF-8', 'SJIS', 'GB2312', - 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', - 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', - 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', - 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG-5', - 'ISO-2022-KR', 'ISO-2022-JP', - ); - } - - $encodings = array_unique(array_merge($prio, mb_list_encodings())); - - if ($encoding = mb_detect_encoding($string, $encodings)) { - return $encoding; - } - } - - // No match, check for UTF-8 - // from http://w3.org/International/questions/qa-forms-utf-8.html - if (preg_match('/\A( - [\x09\x0A\x0D\x20-\x7E] - | [\xC2-\xDF][\x80-\xBF] - | \xE0[\xA0-\xBF][\x80-\xBF] - | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} - | \xED[\x80-\x9F][\x80-\xBF] - | \xF0[\x90-\xBF][\x80-\xBF]{2} - | [\xF1-\xF3][\x80-\xBF]{3} - | \xF4[\x80-\x8F][\x80-\xBF]{2} - )*\z/xs', substr($string, 0, 2048)) - ) { - return 'UTF-8'; - } - - return $failover; - } - - /** - * Removes non-unicode characters from input. - * - * @param mixed $input String or array. - * - * @return mixed String or array - */ - public static function clean($input) - { - // handle input of type array - if (is_array($input)) { - foreach ($input as $idx => $val) { - $input[$idx] = self::clean($val); - } - return $input; - } - - if (!is_string($input) || $input == '') { - return $input; - } - - // iconv/mbstring are much faster (especially with long strings) - if (function_exists('mb_convert_encoding')) { - $msch = mb_substitute_character(); - mb_substitute_character('none'); - $res = mb_convert_encoding($input, 'UTF-8', 'UTF-8'); - mb_substitute_character($msch); - - if ($res !== false) { - return $res; - } - } - - if (function_exists('iconv')) { - if (($res = @iconv('UTF-8', 'UTF-8//IGNORE', $input)) !== false) { - return $res; - } - } - - $seq = ''; - $out = ''; - $regexp = '/^('. -// '[\x00-\x7F]'. // UTF8-1 - '|[\xC2-\xDF][\x80-\xBF]'. // UTF8-2 - '|\xE0[\xA0-\xBF][\x80-\xBF]'. // UTF8-3 - '|[\xE1-\xEC][\x80-\xBF][\x80-\xBF]'. // UTF8-3 - '|\xED[\x80-\x9F][\x80-\xBF]'. // UTF8-3 - '|[\xEE-\xEF][\x80-\xBF][\x80-\xBF]'. // UTF8-3 - '|\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]'. // UTF8-4 - '|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]'.// UTF8-4 - '|\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]'. // UTF8-4 - ')$/'; - - for ($i = 0, $len = strlen($input); $i < $len; $i++) { - $chr = $input[$i]; - $ord = ord($chr); - - // 1-byte character - if ($ord <= 0x7F) { - if ($seq !== '') { - $out .= preg_match($regexp, $seq) ? $seq : ''; - $seq = ''; - } - - $out .= $chr; - } - // first byte of multibyte sequence - else if ($ord >= 0xC0) { - if ($seq !== '') { - $out .= preg_match($regexp, $seq) ? $seq : ''; - $seq = ''; - } - - $seq = $chr; - } - // next byte of multibyte sequence - else if ($seq !== '') { - $seq .= $chr; - } - } - - if ($seq !== '') { - $out .= preg_match($regexp, $seq) ? $seq : ''; - } - - return $out; - } -} diff --git a/src/Roundcube/ImapClient.php b/src/Roundcube/ImapClient.php deleted file mode 100644 index 9a55922..0000000 --- a/src/Roundcube/ImapClient.php +++ /dev/null @@ -1,4169 +0,0 @@ - | -| Author: Ryo Chijiiwa | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -use Javanile\Imap2\Auth_SASL; -use Javanile\Imap2\Exception; -use Javanile\Imap2\GSSAPIContext; -use Javanile\Imap2\KRB5CCache; -use Javanile\Imap2\rcube_message_header; -use Javanile\Imap2\rcube_result_index; -use Javanile\Imap2\rcube_result_thread; - -/** - * PHP based wrapper class to connect to an IMAP server - * - * @package Framework - * @subpackage Storage - */ -class ImapClient -{ - public $error; - public $errornum; - public $result; - public $resultcode; - public $selected; - public $data = array(); - public $flags = array( - 'SEEN' => '\\Seen', - 'DELETED' => '\\Deleted', - 'ANSWERED' => '\\Answered', - 'DRAFT' => '\\Draft', - 'FLAGGED' => '\\Flagged', - 'FORWARDED' => '$Forwarded', - 'MDNSENT' => '$MDNSent', - '*' => '\\*', - ); - - protected $fp; - protected $host; - protected $cmd_tag; - protected $cmd_num = 0; - protected $resourceid; - protected $prefs = array(); - protected $logged = false; - protected $capability = array(); - protected $capability_readed = false; - protected $debug = false; - protected $debug_handler = false; - protected $rawLastLine; - - const ERROR_OK = 0; - const ERROR_NO = -1; - const ERROR_BAD = -2; - const ERROR_BYE = -3; - const ERROR_UNKNOWN = -4; - const ERROR_COMMAND = -5; - const ERROR_READONLY = -6; - - const COMMAND_NORESPONSE = 1; - const COMMAND_CAPABILITY = 2; - const COMMAND_LASTLINE = 4; - const COMMAND_ANONYMIZED = 8; - const COMMAND_RAW_LASTLINE = 16; - - const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n - - - /** - * Send simple (one line) command to the connection stream - * - * @param string $string Command string - * @param bool $endln True if CRLF need to be added at the end of command - * @param bool $anonymized Don't write the given data to log but a placeholder - * - * @param int Number of bytes sent, False on error - */ - protected function putLine($string, $endln = true, $anonymized = false) - { - if (!$this->fp) { - return false; - } - - if ($this->debug) { - // anonymize the sent command for logging - $cut = $endln ? 2 : 0; - if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) { - $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut); - } - else if ($anonymized) { - $log = sprintf('****** [%d]', strlen($string) - $cut); - } - else { - $log = rtrim($string); - } - - $this->debug('C: ' . $log); - } - - if ($endln) { - $string .= "\r\n"; - } - - $res = fwrite($this->fp, $string); - - if ($res === false) { - $this->closeSocket(); - } - - return $res; - } - - /** - * Send command to the connection stream with Command Continuation - * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support - * - * @param string $string Command string - * @param bool $endln True if CRLF need to be added at the end of command - * @param bool $anonymized Don't write the given data to log but a placeholder - * - * @return int|bool Number of bytes sent, False on error - */ - protected function putLineC($string, $endln=true, $anonymized=false) - { - if (!$this->fp) { - return false; - } - - if ($endln) { - $string .= "\r\n"; - } - - $res = 0; - if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) { - for ($i=0, $cnt=count($parts); $i<$cnt; $i++) { - if (isset($parts[$i+1]) && preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) { - // LITERAL+ support - if ($this->prefs['literal+']) { - $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]); - } - - $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized); - if ($bytes === false) { - return false; - } - - $res += $bytes; - - // don't wait if server supports LITERAL+ capability - if (!$this->prefs['literal+']) { - $line = $this->readLine(1000); - // handle error in command - if ($line[0] != '+') { - return false; - } - } - - $i++; - } - else { - $bytes = $this->putLine($parts[$i], false, $anonymized); - if ($bytes === false) { - return false; - } - - $res += $bytes; - } - } - } - - return $res; - } - - /** - * Reads line from the connection stream - * - * @param int $size Buffer size - * - * @return string Line of text response - */ - protected function readLine($size = 1024) - { - $line = ''; - - if (!$size) { - $size = 1024; - } - - do { - if ($this->eof()) { - return $line ?: null; - } - - $buffer = fgets($this->fp, $size); - - if ($buffer === false) { - $this->closeSocket(); - break; - } - - if ($this->debug) { - $this->debug('S: '. rtrim($buffer)); - } - - $line .= $buffer; - } - while (substr($buffer, -1) != "\n"); - - return $line; - } - - /** - * Reads a line of data from the connection stream inluding all - * string continuation literals. - * - * @param int $size Buffer size - * - * @return string Line of text response - */ - protected function readFullLine($size = 1024) - { - $line = $this->readLine($size); - - // include all string literels untile the real end of "line" - while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) { - $bytes = $m[1]; - $out = ''; - - while (strlen($out) < $bytes) { - $out = $this->readBytes($bytes); - if ($out === null) { - break; - } - - $line .= $out; - } - - $line .= $this->readLine($size); - } - - return $line; - } - - /** - * Reads more data from the connection stream when provided - * data contain string literal - * - * @param string $line Response text - * @param bool $escape Enables escaping - * - * @return string Line of text response - */ - protected function multLine($line, $escape = false) - { - $line = rtrim($line); - if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { - $out = ''; - $str = substr($line, 0, -strlen($m[0])); - $bytes = $m[1]; - - while (strlen($out) < $bytes) { - $line = $this->readBytes($bytes); - if ($line === null) { - break; - } - - $out .= $line; - } - - $line = $str . ($escape ? $this->escape($out) : $out); - } - - return $line; - } - - /** - * Reads specified number of bytes from the connection stream - * - * @param int $bytes Number of bytes to get - * - * @return string Response text - */ - protected function readBytes($bytes) - { - $data = ''; - $len = 0; - - while ($len < $bytes && !$this->eof()) { - $d = fread($this->fp, $bytes-$len); - if ($this->debug) { - $this->debug('S: '. $d); - } - $data .= $d; - $data_len = strlen($data); - if ($len == $data_len) { - break; // nothing was read -> exit to avoid apache lockups - } - $len = $data_len; - } - - return $data; - } - - /** - * Reads complete response to the IMAP command - * - * @param array $untagged Will be filled with untagged response lines - * - * @return string Response text - */ - protected function readReply(&$untagged = null) - { - do { - $line = trim($this->readLine(1024)); - // store untagged response lines - if ($line[0] == '*') { - $untagged[] = $line; - } - } - while ($line[0] == '*'); - - if ($untagged) { - $untagged = implode("\n", $untagged); - } - - return $line; - } - - /** - * Response parser. - * - * @param string $string Response text - * @param string $err_prefix Error message prefix - * - * @return int Response status - */ - protected function parseResult($string, $err_prefix = '') - { - if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) { - $res = strtoupper($matches[1]); - $str = trim($matches[2]); - - if ($res == 'OK') { - $this->errornum = self::ERROR_OK; - } - else if ($res == 'NO') { - $this->errornum = self::ERROR_NO; - } - else if ($res == 'BAD') { - $this->errornum = self::ERROR_BAD; - } - else if ($res == 'BYE') { - $this->closeSocket(); - $this->errornum = self::ERROR_BYE; - } - - if ($str) { - $str = trim($str); - // get response string and code (RFC5530) - if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) { - $this->resultcode = strtoupper($m[1]); - $str = trim(substr($str, strlen($m[1]) + 2)); - } - else { - $this->resultcode = null; - // parse response for [APPENDUID 1204196876 3456] - if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) { - $this->data['APPENDUID'] = $m[1]; - } - // parse response for [COPYUID 1204196876 3456:3457 123:124] - else if (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) { - $this->data['COPYUID'] = array($m[1], $m[2]); - } - } - - $this->result = $str; - - if ($this->errornum != self::ERROR_OK) { - $this->error = $err_prefix ? $err_prefix.$str : $str; - } - } - - return $this->errornum; - } - - return self::ERROR_UNKNOWN; - } - - /** - * Checks connection stream state. - * - * @return bool True if connection is closed - */ - protected function eof() - { - if (!is_resource($this->fp)) { - return true; - } - - // If a connection opened by fsockopen() wasn't closed - // by the server, feof() will hang. - $start = microtime(true); - - if (feof($this->fp) || - ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout'])) - ) { - $this->closeSocket(); - return true; - } - - return false; - } - - /** - * Closes connection stream. - */ - protected function closeSocket() - { - @fclose($this->fp); - $this->fp = null; - } - - /** - * Error code/message setter. - */ - public function setError($code, $msg = '') - { - $this->errornum = $code; - $this->error = $msg; - - return $code; - } - - /** - * Checks response status. - * Checks if command response line starts with specified prefix (or * BYE/BAD) - * - * @param string $string Response text - * @param string $match Prefix to match with (case-sensitive) - * @param bool $error Enables BYE/BAD checking - * @param bool $nonempty Enables empty response checking - * - * @return bool True any check is true or connection is closed. - */ - protected function startsWith($string, $match, $error = false, $nonempty = false) - { - if (!$this->fp) { - return true; - } - - if (strncmp($string, $match, strlen($match)) == 0) { - return true; - } - - if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) { - if (strtoupper($m[1]) == 'BYE') { - $this->closeSocket(); - } - return true; - } - - if ($nonempty && !strlen($string)) { - return true; - } - - return false; - } - - /** - * Capabilities checker - */ - protected function hasCapability($name) - { - if (empty($this->capability) || $name == '') { - return false; - } - - if (in_array($name, $this->capability)) { - return true; - } - else if (strpos($name, '=')) { - return false; - } - - $result = array(); - foreach ($this->capability as $cap) { - $entry = explode('=', $cap); - if ($entry[0] == $name) { - $result[] = $entry[1]; - } - } - - return $result ?: false; - } - - /** - * Capabilities checker - * - * @param string $name Capability name - * - * @return mixed Capability values array for key=value pairs, true/false for others - */ - public function getCapability($name) - { - $result = $this->hasCapability($name); - - if (!empty($result)) { - return $result; - } - else if ($this->capability_readed) { - return false; - } - - // get capabilities (only once) because initial - // optional CAPABILITY response may differ - $result = $this->execute('CAPABILITY'); - if ($result[0] == self::ERROR_OK) { - $this->parseCapability($result[1]); - } - - $this->capability_readed = true; - - return $this->hasCapability($name); - } - - /** - * Clears detected server capabilities - */ - public function clearCapability() - { - $this->capability = array(); - $this->capability_readed = false; - } - - /** - * DIGEST-MD5/CRAM-MD5/PLAIN Authentication - * - * @param string $user Username - * @param string $pass Password - * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5) - * - * @return resource Connection resourse on success, error code on error - */ - protected function authenticate($user, $pass, $type = 'PLAIN') - { - if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') { - if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) { - return $this->setError(self::ERROR_BYE, - "The Auth_SASL package is required for DIGEST-MD5 authentication"); - } - - $this->putLine($this->nextTag() . " AUTHENTICATE $type"); - $line = trim($this->readReply()); - - if ($line[0] == '+') { - $challenge = substr($line, 2); - } - else { - return $this->parseResult($line); - } - - if ($type == 'CRAM-MD5') { - // RFC2195: CRAM-MD5 - $ipad = ''; - $opad = ''; - $xor = function($str1, $str2) { - $result = ''; - $size = strlen($str1); - for ($i=0; $i<$size; $i++) { - $result .= chr(ord($str1[$i]) ^ ord($str2[$i])); - } - return $result; - }; - - // initialize ipad, opad - for ($i=0; $i<64; $i++) { - $ipad .= chr(0x36); - $opad .= chr(0x5C); - } - - // pad $pass so it's 64 bytes - $pass = str_pad($pass, 64, chr(0)); - - // generate hash - $hash = md5($xor($pass, $opad) . pack("H*", - md5($xor($pass, $ipad) . base64_decode($challenge)))); - $reply = base64_encode($user . ' ' . $hash); - - // send result - $this->putLine($reply, true, true); - } - else { - // RFC2831: DIGEST-MD5 - // proxy authorization - if (!empty($this->prefs['auth_cid'])) { - $authc = $this->prefs['auth_cid']; - $pass = $this->prefs['auth_pw']; - } - else { - $authc = $user; - $user = ''; - } - - $auth_sasl = new Auth_SASL; - $auth_sasl = $auth_sasl->factory('digestmd5'); - $reply = base64_encode($auth_sasl->getResponse($authc, $pass, - base64_decode($challenge), $this->host, 'imap', $user)); - - // send result - $this->putLine($reply, true, true); - $line = trim($this->readReply()); - - if ($line[0] != '+') { - return $this->parseResult($line); - } - - // check response - $challenge = substr($line, 2); - $challenge = base64_decode($challenge); - if (strpos($challenge, 'rspauth=') === false) { - return $this->setError(self::ERROR_BAD, - "Unexpected response from server to DIGEST-MD5 response"); - } - - $this->putLine(''); - } - - $line = $this->readReply(); - $result = $this->parseResult($line); - } - else if ($type == 'GSSAPI') { - if (!extension_loaded('krb5')) { - return $this->setError(self::ERROR_BYE, - "The krb5 extension is required for GSSAPI authentication"); - } - - if (empty($this->prefs['gssapi_cn'])) { - return $this->setError(self::ERROR_BYE, - "The gssapi_cn parameter is required for GSSAPI authentication"); - } - - if (empty($this->prefs['gssapi_context'])) { - return $this->setError(self::ERROR_BYE, - "The gssapi_context parameter is required for GSSAPI authentication"); - } - - putenv('KRB5CCNAME=' . $this->prefs['gssapi_cn']); - - try { - $ccache = new KRB5CCache(); - $ccache->open($this->prefs['gssapi_cn']); - $gssapicontext = new GSSAPIContext(); - $gssapicontext->acquireCredentials($ccache); - - $token = ''; - $success = $gssapicontext->initSecContext($this->prefs['gssapi_context'], null, null, null, $token); - $token = base64_encode($token); - } - catch (Exception $e) { - trigger_error($e->getMessage(), E_USER_WARNING); - return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed"); - } - - $this->putLine($this->nextTag() . " AUTHENTICATE GSSAPI " . $token); - $line = trim($this->readReply()); - - if ($line[0] != '+') { - return $this->parseResult($line); - } - - try { - $itoken = base64_decode(substr($line, 2)); - - if (!$gssapicontext->unwrap($itoken, $itoken)) { - throw new Exception("GSSAPI SASL input token unwrap failed"); - } - - if (strlen($itoken) < 4) { - throw new Exception("GSSAPI SASL input token invalid"); - } - - // Integrity/encryption layers are not supported. The first bit - // indicates that the server supports "no security layers". - // 0x00 should not occur, but support broken implementations. - $server_layers = ord($itoken[0]); - if ($server_layers && ($server_layers & 0x1) != 0x1) { - throw new Exception("Server requires GSSAPI SASL integrity/encryption"); - } - - // Construct output token. 0x01 in the first octet = SASL layer "none", - // zero in the following three octets = no data follows. - // See https://github.com/cyrusimap/cyrus-sasl/blob/e41cfb986c1b1935770de554872247453fdbb079/plugins/gssapi.c#L1284 - if (!$gssapicontext->wrap(pack("CCCC", 0x1, 0, 0, 0), $otoken, true)) { - throw new Exception("GSSAPI SASL output token wrap failed"); - } - } - catch (Exception $e) { - trigger_error($e->getMessage(), E_USER_WARNING); - return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed"); - } - - $this->putLine(base64_encode($otoken)); - - $line = $this->readReply(); - $result = $this->parseResult($line); - } - else if ($type == 'PLAIN') { - // proxy authorization - if (!empty($this->prefs['auth_cid'])) { - $authc = $this->prefs['auth_cid']; - $pass = $this->prefs['auth_pw']; - } - else { - $authc = $user; - $user = ''; - } - - $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass); - - // RFC 4959 (SASL-IR): save one round trip - if ($this->getCapability('SASL-IR')) { - list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply), - self::COMMAND_RAW_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); - } - else { - $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN"); - $line = trim($this->readReply()); - - if ($line[0] != '+') { - return $this->parseResult($line); - } - - // send result, get reply and process it - $this->putLine($reply, true, true); - $line = $this->readReply(); - $result = $this->parseResult($line); - } - } - else if ($type == 'LOGIN') { - $this->putLine($this->nextTag() . " AUTHENTICATE LOGIN"); - - $line = trim($this->readReply()); - if ($line[0] != '+') { - return $this->parseResult($line); - } - - $this->putLine(base64_encode($user), true, true); - - $line = trim($this->readReply()); - if ($line[0] != '+') { - return $this->parseResult($line); - } - - // send result, get reply and process it - $this->putLine(base64_encode($pass), true, true); - - $line = $this->readReply(); - $result = $this->parseResult($line); - } - else if ($type == 'XOAUTH2') { - $auth = base64_encode("user=$user\1auth=Bearer $pass\1\1"); - $this->putLine($this->nextTag() . " AUTHENTICATE XOAUTH2 $auth", true, true); - - $line = trim($this->readReply()); - if ($line[0] == '+') { - // send empty line - $this->putLine('', true, true); - $line = $this->readReply(); - } - - $result = $this->parseResult($line); - } - - if ($result === self::ERROR_OK) { - // optional CAPABILITY response - if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { - $this->parseCapability($matches[1], true); - } - - return $this->fp; - } - - return $this->setError($result, "AUTHENTICATE $type: $line"); - } - - /** - * LOGIN Authentication - * - * @param string $user Username - * @param string $pass Password - * - * @return resource Connection resourse on success, error code on error - */ - protected function login($user, $password) - { - // Prevent from sending credentials in plain text when connection is not secure - if ($this->getCapability('LOGINDISABLED')) { - return $this->setError(self::ERROR_BAD, "Login disabled by IMAP server"); - } - - list($code, $response) = $this->execute('LOGIN', array( - $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); - - // re-set capabilities list if untagged CAPABILITY response provided - if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) { - $this->parseCapability($matches[1], true); - } - - if ($code == self::ERROR_OK) { - return $this->fp; - } - - return $code; - } - - /** - * Detects hierarchy delimiter - * - * @return string The delimiter - */ - public function getHierarchyDelimiter() - { - if (isset($this->prefs['delimiter']) && $this->prefs['delimiter']) { - return $this->prefs['delimiter']; - } - - // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8) - list($code, $response) = $this->execute('LIST', - array($this->escape(''), $this->escape(''))); - - if ($code == self::ERROR_OK) { - $args = $this->tokenizeResponse($response, 4); - $delimiter = $args[3]; - - if (strlen($delimiter) > 0) { - return ($this->prefs['delimiter'] = $delimiter); - } - } - } - - /** - * NAMESPACE handler (RFC 2342) - * - * @return array Namespace data hash (personal, other, shared) - */ - public function getNamespace() - { - if (array_key_exists('namespace', $this->prefs)) { - return $this->prefs['namespace']; - } - - if (!$this->getCapability('NAMESPACE')) { - return self::ERROR_BAD; - } - - list($code, $response) = $this->execute('NAMESPACE'); - - if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) { - $response = substr($response, 11); - $data = $this->tokenizeResponse($response); - } - - if (!is_array($data)) { - return $code; - } - - $this->prefs['namespace'] = array( - 'personal' => $data[0], - 'other' => $data[1], - 'shared' => $data[2], - ); - - return $this->prefs['namespace']; - } - - /** - * Connects to IMAP server and authenticates. - * - * @param string $host Server hostname or IP - * @param string $user User name - * @param string $password Password - * @param array $options Connection and class options - * - * @return bool True on success, False on failure - */ - public function connect($host, $user, $password, $options = array()) - { - // configure - $this->set_prefs($options); - - $this->host = $host; - $this->user = $user; - $this->logged = false; - $this->selected = null; - - // check input - if (empty($host)) { - $this->setError(self::ERROR_BAD, "Empty host"); - return false; - } - - if (empty($user)) { - $this->setError(self::ERROR_NO, "Empty user"); - return false; - } - - if (empty($password) && empty($options['gssapi_cn'])) { - $this->setError(self::ERROR_NO, "Empty password"); - return false; - } - - // Connect - if (!$this->_connect($host)) { - return false; - } - - // Send ID info - if (!empty($this->prefs['ident']) && $this->getCapability('ID')) { - $this->data['ID'] = $this->id($this->prefs['ident']); - } - - $auth_method = $this->prefs['auth_type']; - - // Switch to XOAUTH2 if password is a JWT token - $token = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', @explode('.', $password)[0]))), true); - if (is_array($token) && isset($token['typ']) && $token['typ'] == 'JWT') { - $auth_method = 'XOAUTH2'; - } - - $auth_methods = array(); - $result = null; - - // check for supported auth methods - if (!$auth_method || $auth_method == 'CHECK') { - if ($auth_caps = $this->getCapability('AUTH')) { - $auth_methods = $auth_caps; - } - - // Use best (for security) supported authentication method - $all_methods = array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN', 'XOAUTH2'); - - if (!empty($this->prefs['gssapi_cn'])) { - array_unshift($all_methods, 'GSSAPI'); - } - - foreach ($all_methods as $auth_method) { - if (in_array($auth_method, $auth_methods)) { - break; - } - } - - // Prefer LOGIN over AUTHENTICATE LOGIN for performance reasons - if ($auth_method == 'LOGIN' && !$this->getCapability('LOGINDISABLED')) { - $auth_method = 'IMAP'; - } - } - - // pre-login capabilities can be not complete - $this->capability_readed = false; - - - - // Authenticate - switch ($auth_method) { - case 'CRAM_MD5': - $auth_method = 'CRAM-MD5'; - case 'CRAM-MD5': - case 'DIGEST-MD5': - case 'GSSAPI': - case 'PLAIN': - case 'LOGIN': - case 'XOAUTH2': - $result = $this->authenticate($user, $password, $auth_method); - break; - - case 'IMAP': - $result = $this->login($user, $password); - break; - - default: - $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method"); - } - - // Connected and authenticated - if (is_resource($result)) { - if ($this->prefs['force_caps']) { - $this->clearCapability(); - } - $this->logged = true; - - return true; - } - - $this->closeConnection(); - - return false; - } - - /** - * Connects to IMAP server. - * - * @param string $host Server hostname or IP - * - * @return bool True on success, False on failure - */ - protected function _connect($host) - { - // initialize connection - $this->error = ''; - $this->errornum = self::ERROR_OK; - - if (!$this->prefs['port']) { - $this->prefs['port'] = 143; - } - - // check for SSL - if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') { - $host = $this->prefs['ssl_mode'] . '://' . $host; - } - - if ($this->prefs['timeout'] <= 0) { - $this->prefs['timeout'] = max(60, intval(ini_get('default_socket_timeout'))); - } - - if ($this->debug) { - // set connection identifier for debug output - $this->resourceid = strtoupper(substr(md5(microtime() . $host . $this->user), 0, 4)); - - $_host = ($this->prefs['ssl_mode'] == 'tls' ? 'tls://' : '') . $host . ':' . $this->prefs['port']; - $this->debug("Connecting to $_host..."); - } - - if (!empty($this->prefs['socket_options'])) { - $context = stream_context_create($this->prefs['socket_options']); - $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr, - $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context); - } - else { - $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); - } - - if (!$this->fp) { - $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", - $host, $this->prefs['port'], $errstr ?: "Unknown reason")); - - return false; - } - - if ($this->prefs['timeout'] > 0) { - stream_set_timeout($this->fp, $this->prefs['timeout']); - } - - $line = trim(fgets($this->fp, 8192)); - - if ($this->debug && $line) { - $this->debug('S: '. $line); - } - - // Connected to wrong port or connection error? - if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { - if ($line) - $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); - else - $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); - - $this->setError(self::ERROR_BAD, $error); - $this->closeConnection(); - return false; - } - - $this->data['GREETING'] = trim(preg_replace('/\[[^\]]+\]\s*/', '', $line)); - - // RFC3501 [7.1] optional CAPABILITY response - if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { - $this->parseCapability($matches[1], true); - } - - // TLS connection - if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { - $res = $this->execute('STARTTLS'); - - if ($res[0] != self::ERROR_OK) { - $this->closeConnection(); - return false; - } - - if (isset($this->prefs['socket_options']['ssl']['crypto_method'])) { - $crypto_method = $this->prefs['socket_options']['ssl']['crypto_method']; - } - else { - // There is no flag to enable all TLS methods. Net_SMTP - // handles enabling TLS similarly. - $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT - | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT - | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; - } - - if (!stream_socket_enable_crypto($this->fp, true, $crypto_method)) { - $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); - $this->closeConnection(); - return false; - } - - // Now we're secure, capabilities need to be reread - $this->clearCapability(); - } - - return true; - } - - /** - * Initializes environment - */ - protected function set_prefs($prefs) - { - // set preferences - if (is_array($prefs)) { - $this->prefs = $prefs; - } - - // set auth method - if (!empty($this->prefs['auth_type'])) { - $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']); - } - else { - $this->prefs['auth_type'] = 'CHECK'; - } - - // disabled capabilities - if (!empty($this->prefs['disabled_caps'])) { - $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']); - } - - // additional message flags - if (!empty($this->prefs['message_flags'])) { - $this->flags = array_merge($this->flags, $this->prefs['message_flags']); - unset($this->prefs['message_flags']); - } - } - - /** - * Checks connection status - * - * @return bool True if connection is active and user is logged in, False otherwise. - */ - public function connected() - { - return $this->fp && $this->logged; - } - - /** - * Closes connection with logout. - */ - public function closeConnection() - { - if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) { - $this->readReply(); - } - - $this->closeSocket(); - } - - /** - * Executes SELECT command (if mailbox is already not in selected state) - * - * @param string $mailbox Mailbox name - * @param array $qresync_data QRESYNC data (RFC5162) - * - * @return boolean True on success, false on error - */ - public function select($mailbox, $qresync_data = null) - { - if (!strlen($mailbox)) { - return false; - } - - if ($this->selected === $mailbox) { - return true; - } - - $params = array($this->escape($mailbox)); - - // QRESYNC data items - // 0. the last known UIDVALIDITY, - // 1. the last known modification sequence, - // 2. the optional set of known UIDs, and - // 3. an optional parenthesized list of known sequence ranges and their - // corresponding UIDs. - if (!empty($qresync_data)) { - if (!empty($qresync_data[2])) { - $qresync_data[2] = self::compressMessageSet($qresync_data[2]); - } - - $params[] = array('QRESYNC', $qresync_data); - } - - list($code, $response) = $this->execute('SELECT', $params); - - if ($code == self::ERROR_OK) { - $this->clear_mailbox_cache(); - - $response = explode("\r\n", $response); - foreach ($response as $line) { - if (preg_match('/^\* OK \[/i', $line)) { - $pos = strcspn($line, ' ]', 6); - $token = strtoupper(substr($line, 6, $pos)); - $pos += 7; - - switch ($token) { - case 'UIDNEXT': - case 'UIDVALIDITY': - case 'UNSEEN': - if ($len = strspn($line, '0123456789', $pos)) { - $this->data[$token] = (int) substr($line, $pos, $len); - } - break; - - case 'HIGHESTMODSEQ': - if ($len = strspn($line, '0123456789', $pos)) { - $this->data[$token] = (string) substr($line, $pos, $len); - } - break; - - case 'NOMODSEQ': - $this->data[$token] = true; - break; - - case 'PERMANENTFLAGS': - $start = strpos($line, '(', $pos); - $end = strrpos($line, ')'); - if ($start && $end) { - $flags = substr($line, $start + 1, $end - $start - 1); - $this->data[$token] = explode(' ', $flags); - } - break; - } - } - else if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT|FETCH)/i', $line, $match)) { - $token = strtoupper($match[2]); - switch ($token) { - case 'EXISTS': - case 'RECENT': - $this->data[$token] = (int) $match[1]; - break; - - case 'FETCH': - // QRESYNC FETCH response (RFC5162) - $line = substr($line, strlen($match[0])); - $fetch_data = $this->tokenizeResponse($line, 1); - $data = array('id' => $match[1]); - - for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) { - $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1]; - } - - $this->data['QRESYNC'][$data['uid']] = $data; - break; - } - } - // QRESYNC VANISHED response (RFC5162) - else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { - $line = substr($line, strlen($match[0])); - $v_data = $this->tokenizeResponse($line, 1); - - $this->data['VANISHED'] = $v_data; - } - } - - $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY'; - $this->selected = $mailbox; - - return true; - } - - return false; - } - - /** - * Executes STATUS command - * - * @param string $mailbox Mailbox name - * @param array $items Additional requested item names. By default - * MESSAGES and UNSEEN are requested. Other defined - * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT - * - * @return array Status item-value hash - * @since 0.5-beta - */ - public function status($mailbox, $items = array()) - { - if (!strlen($mailbox)) { - return false; - } - - if (!in_array('MESSAGES', $items)) { - $items[] = 'MESSAGES'; - } - if (!in_array('UNSEEN', $items)) { - $items[] = 'UNSEEN'; - } - - list($code, $response) = $this->execute('STATUS', - array($this->escape($mailbox), '(' . implode(' ', $items) . ')'), 0, '/^\* STATUS /i'); - - if ($code == self::ERROR_OK && $response) { - $result = array(); - $response = substr($response, 9); // remove prefix "* STATUS " - - list($mbox, $items) = $this->tokenizeResponse($response, 2); - - // Fix for #1487859. Some buggy server returns not quoted - // folder name with spaces. Let's try to handle this situation - if (!is_array($items) && ($pos = strpos($response, '(')) !== false) { - $response = substr($response, $pos); - $items = $this->tokenizeResponse($response, 1); - } - - if (!is_array($items)) { - return $result; - } - - for ($i=0, $len=count($items); $i<$len; $i += 2) { - $result[$items[$i]] = $items[$i+1]; - } - - $this->data['STATUS:'.$mailbox] = $result; - - return $result; - } - - return false; - } - - /** - * Executes EXPUNGE command - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UIDs to expunge - * - * @return boolean True on success, False on error - */ - public function expunge($mailbox, $messages = null) - { - if (!$this->select($mailbox)) { - return false; - } - - if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); - return false; - } - - // Clear internal status cache - $this->clear_status_cache($mailbox); - - if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) { - $messages = self::compressMessageSet($messages); - $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE); - } - else { - $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE); - } - - if ($result == self::ERROR_OK) { - $this->selected = null; // state has changed, need to reselect - return true; - } - - return false; - } - - /** - * Executes CLOSE command - * - * @return boolean True on success, False on error - * @since 0.5 - */ - public function close() - { - $result = $this->execute('CLOSE', null, self::COMMAND_NORESPONSE); - - if ($result == self::ERROR_OK) { - $this->selected = null; - return true; - } - - return false; - } - - /** - * Folder subscription (SUBSCRIBE) - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - public function subscribe($mailbox) - { - $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - /** - * Folder unsubscription (UNSUBSCRIBE) - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - public function unsubscribe($mailbox) - { - $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - /** - * Folder creation (CREATE) - * - * @param string $mailbox Mailbox name - * @param array $types Optional folder types (RFC 6154) - * - * @return bool True on success, False on error - */ - public function createFolder($mailbox, $types = null) - { - $args = array($this->escape($mailbox)); - - // RFC 6154: CREATE-SPECIAL-USE - if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) { - $args[] = '(USE (' . implode(' ', $types) . '))'; - } - - $result = $this->execute('CREATE', $args, self::COMMAND_RAW_LASTLINE); - - $this->rawLastLine = $result[1]; - - return $result[0] == self::ERROR_OK; - } - - /** - * Folder renaming (RENAME) - * - * @param string $mailbox Mailbox name - * - * @return bool True on success, False on error - */ - public function renameFolder($from, $to) - { - $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - /** - * Executes DELETE command - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - public function deleteFolder($mailbox) - { - $result = $this->execute('DELETE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - /** - * Removes all messages in a folder - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - public function clearFolder($mailbox) - { - if ($this->countMessages($mailbox) > 0) { - $res = $this->flag($mailbox, '1:*', 'DELETED'); - } - - if ($res) { - if ($this->selected === $mailbox) { - $res = $this->close(); - } - else { - $res = $this->expunge($mailbox); - } - } - - return $res; - } - - /** - * Returns list of mailboxes - * - * @param string $ref Reference name - * @param string $mailbox Mailbox name - * @param array $return_opts (see self::_listMailboxes) - * @param array $select_opts (see self::_listMailboxes) - * - * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response - * is requested, False on error. - */ - public function listMailboxes($ref, $mailbox, $return_opts = array(), $select_opts = array()) - { - return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts); - } - - /** - * Returns list of subscribed mailboxes - * - * @param string $ref Reference name - * @param string $mailbox Mailbox name - * @param array $return_opts (see self::_listMailboxes) - * - * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response - * is requested, False on error. - */ - public function listSubscribed($ref, $mailbox, $return_opts = array()) - { - return $this->_listMailboxes($ref, $mailbox, true, $return_opts, null); - } - - /** - * IMAP LIST/LSUB command - * - * @param string $ref Reference name - * @param string $mailbox Mailbox name - * @param bool $subscribed Enables returning subscribed mailboxes only - * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED) - * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN, - * MYRIGHTS, SUBSCRIBED, CHILDREN - * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED) - * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE, - * SPECIAL-USE (RFC6154) - * - * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response - * is requested, False on error. - */ - protected function _listMailboxes($ref, $mailbox, $subscribed=false, - $return_opts=array(), $select_opts=array()) - { - if (!strlen($mailbox)) { - $mailbox = '*'; - } - - $args = array(); - $rets = array(); - - if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) { - $select_opts = (array) $select_opts; - - $args[] = '(' . implode(' ', $select_opts) . ')'; - } - - $args[] = $this->escape($ref); - $args[] = $this->escape($mailbox); - - if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) { - $ext_opts = array('SUBSCRIBED', 'CHILDREN'); - $rets = array_intersect($return_opts, $ext_opts); - $return_opts = array_diff($return_opts, $rets); - } - - if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) { - $lstatus = true; - $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'); - $opts = array_diff($return_opts, $status_opts); - $status_opts = array_diff($return_opts, $opts); - - if (!empty($status_opts)) { - $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')'; - } - - if (!empty($opts)) { - $rets = array_merge($rets, $opts); - } - } else { - $lstatus = false; - } - - if (!empty($rets)) { - $args[] = 'RETURN (' . implode(' ', $rets) . ')'; - } - - list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args); - - if ($code == self::ERROR_OK) { - $folders = array(); - $last = 0; - $pos = 0; - $response .= "\r\n"; - - while ($pos = strpos($response, "\r\n", $pos+1)) { - // literal string, not real end-of-command-line - if ($response[$pos-1] == '}') { - continue; - } - - $line = substr($response, $last, $pos - $last); - $last = $pos + 2; - - if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) { - continue; - } - - $cmd = strtoupper($m[1]); - $line = substr($line, strlen($m[0])); - - // * LIST () - if ($cmd == 'LIST' || $cmd == 'LSUB') { - list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3); - - // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879) - if ($delim) { - $mailbox = rtrim($mailbox, $delim); - } - - // Add to result array - if (!$lstatus) { - $folders[] = $mailbox; - } - else { - $folders[$mailbox] = array(); - } - - // store folder options - if ($cmd == 'LIST') { - // Add to options array - if (empty($this->data['LIST'][$mailbox])) { - $this->data['LIST'][$mailbox] = $opts; - } - else if (!empty($opts)) { - $this->data['LIST'][$mailbox] = array_unique(array_merge( - $this->data['LIST'][$mailbox], $opts)); - } - } - } - else if ($lstatus) { - // * STATUS () - if ($cmd == 'STATUS') { - list($mailbox, $status) = $this->tokenizeResponse($line, 2); - - for ($i=0, $len=count($status); $i<$len; $i += 2) { - list($name, $value) = $this->tokenizeResponse($status, 2); - $folders[$mailbox][$name] = $value; - } - } - // * MYRIGHTS - else if ($cmd == 'MYRIGHTS') { - list($mailbox, $acl) = $this->tokenizeResponse($line, 2); - $folders[$mailbox]['MYRIGHTS'] = $acl; - } - } - } - - return $folders; - } - - return false; - } - - /** - * Returns count of all messages in a folder - * - * @param string $mailbox Mailbox name - * - * @return int Number of messages, False on error - */ - public function countMessages($mailbox) - { - if ($this->selected === $mailbox && isset($this->data['EXISTS'])) { - return $this->data['EXISTS']; - } - - // Check internal cache - $cache = $this->data['STATUS:'.$mailbox]; - if (!empty($cache) && isset($cache['MESSAGES'])) { - return (int) $cache['MESSAGES']; - } - - // Try STATUS (should be faster than SELECT) - $counts = $this->status($mailbox); - if (is_array($counts)) { - return (int) $counts['MESSAGES']; - } - - return false; - } - - /** - * Returns count of messages with \Recent flag in a folder - * - * @param string $mailbox Mailbox name - * - * @return int Number of messages, False on error - */ - public function countRecent($mailbox) - { - if ($this->selected === $mailbox && isset($this->data['RECENT'])) { - return $this->data['RECENT']; - } - - // Check internal cache - $cache = $this->data['STATUS:'.$mailbox]; - if (!empty($cache) && isset($cache['RECENT'])) { - return (int) $cache['RECENT']; - } - - // Try STATUS (should be faster than SELECT) - $counts = $this->status($mailbox, array('RECENT')); - if (is_array($counts)) { - return (int) $counts['RECENT']; - } - - return false; - } - - /** - * Returns count of messages without \Seen flag in a specified folder - * - * @param string $mailbox Mailbox name - * - * @return int Number of messages, False on error - */ - public function countUnseen($mailbox) - { - // Check internal cache - $cache = $this->data['STATUS:'.$mailbox]; - if (!empty($cache) && isset($cache['UNSEEN'])) { - return (int) $cache['UNSEEN']; - } - - // Try STATUS (should be faster than SELECT+SEARCH) - $counts = $this->status($mailbox); - if (is_array($counts)) { - return (int) $counts['UNSEEN']; - } - - // Invoke SEARCH as a fallback - $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT')); - if (!$index->is_error()) { - return $index->count(); - } - - return false; - } - - /** - * Executes ID command (RFC2971) - * - * @param array $items Client identification information key/value hash - * - * @return array|false Server identification information key/value hash, False on error - * @since 0.6 - */ - public function id($items = array()) - { - if (is_array($items) && !empty($items)) { - foreach ($items as $key => $value) { - $args[] = $this->escape($key, true); - $args[] = $this->escape($value, true); - } - } - - list($code, $response) = $this->execute('ID', - array(!empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)), - 0, '/^\* ID /i'); - - if ($code == self::ERROR_OK && $response) { - $response = substr($response, 5); // remove prefix "* ID " - $items = $this->tokenizeResponse($response, 1); - $result = array(); - - if (is_array($items)) { - for ($i=0, $len=count($items); $i<$len; $i += 2) { - $result[$items[$i]] = $items[$i+1]; - } - } - - return $result; - } - - return false; - } - - /** - * Executes ENABLE command (RFC5161) - * - * @param mixed $extension Extension name to enable (or array of names) - * - * @return array|bool List of enabled extensions, False on error - * @since 0.6 - */ - public function enable($extension) - { - if (empty($extension)) { - return false; - } - - if (!$this->hasCapability('ENABLE')) { - return false; - } - - if (!is_array($extension)) { - $extension = array($extension); - } - - if (!empty($this->extensions_enabled)) { - // check if all extensions are already enabled - $diff = array_diff($extension, $this->extensions_enabled); - - if (empty($diff)) { - return $extension; - } - - // Make sure the mailbox isn't selected, before enabling extension(s) - if ($this->selected !== null) { - $this->close(); - } - } - - list($code, $response) = $this->execute('ENABLE', $extension, 0, '/^\* ENABLED /i'); - - if ($code == self::ERROR_OK && $response) { - $response = substr($response, 10); // remove prefix "* ENABLED " - $result = (array) $this->tokenizeResponse($response); - - $this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result)); - - return $this->extensions_enabled; - } - - return false; - } - - /** - * Executes SORT command - * - * @param string $mailbox Mailbox name - * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) - * @param string $criteria Searching criteria - * @param bool $return_uid Enables UID SORT usage - * @param string $encoding Character set - * - * @return rcube_result_index Response data - */ - public function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII') - { - $old_sel = $this->selected; - $supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO'); - $field = strtoupper($field); - - if ($field == 'INTERNALDATE') { - $field = 'ARRIVAL'; - } - - if (!in_array($field, $supported)) { - return new ResultIndex($mailbox); - } - - if (!$this->select($mailbox)) { - return new ResultIndex($mailbox); - } - - // return empty result when folder is empty and we're just after SELECT - if ($old_sel != $mailbox && !$this->data['EXISTS']) { - return new ResultIndex($mailbox, '* SORT'); - } - - // RFC 5957: SORT=DISPLAY - if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) { - $field = 'DISPLAY' . $field; - } - - $encoding = $encoding ? trim($encoding) : 'US-ASCII'; - $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL'; - - list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT', - array("($field)", $encoding, $criteria)); - - if ($code != self::ERROR_OK) { - $response = null; - } - - return new ResultIndex($mailbox, $response); - } - - /** - * Executes THREAD command - * - * @param string $mailbox Mailbox name - * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS) - * @param string $criteria Searching criteria - * @param bool $return_uid Enables UIDs in result instead of sequence numbers - * @param string $encoding Character set - * - * @return rcube_result_thread Thread data - */ - public function thread($mailbox, $algorithm = 'REFERENCES', $criteria = '', $return_uid = false, $encoding = 'US-ASCII') - { - $old_sel = $this->selected; - - if (!$this->select($mailbox)) { - return new ResultThread($mailbox); - } - - // return empty result when folder is empty and we're just after SELECT - if ($old_sel != $mailbox && !$this->data['EXISTS']) { - return new ResultThread($mailbox, '* THREAD'); - } - - $encoding = $encoding ? trim($encoding) : 'US-ASCII'; - $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES'; - $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL'; - - list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD', - array($algorithm, $encoding, $criteria)); - - if ($code != self::ERROR_OK) { - $response = null; - } - - return new ResultThread($mailbox, $response); - } - - /** - * Executes SEARCH command - * - * @param string $mailbox Mailbox name - * @param string $criteria Searching criteria - * @param bool $return_uid Enable UID in result instead of sequence ID - * @param array $items Return items (MIN, MAX, COUNT, ALL) - * - * @return rcube_result_index Result data - */ - public function search($mailbox, $criteria, $return_uid = false, $items = array()) - { - $old_sel = $this->selected; - - if (!$this->select($mailbox)) { - return new ResultIndex($mailbox); - } - - // return empty result when folder is empty and we're just after SELECT - if ($old_sel != $mailbox && !$this->data['EXISTS']) { - return new ResultIndex($mailbox, '* SEARCH'); - } - - // If ESEARCH is supported always use ALL - // but not when items are specified or using simple id2uid search - if (empty($items) && preg_match('/[^0-9]/', $criteria)) { - $items = array('ALL'); - } - - $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); - $criteria = trim($criteria); - $params = ''; - - // RFC4731: ESEARCH - if (!empty($items) && $esearch) { - $params .= 'RETURN (' . implode(' ', $items) . ')'; - } - - if (!empty($criteria)) { - $params .= ($params ? ' ' : '') . $criteria; - } - else { - $params .= 'ALL'; - } - - list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', - array($params)); - - if ($code != self::ERROR_OK) { - $response = null; - } - - return new ResultIndex($mailbox, $response); - } - - /** - * Simulates SORT command by using FETCH and sorting. - * - * @param string $mailbox Mailbox name - * @param string|array $message_set Searching criteria (list of messages to return) - * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) - * @param bool $skip_deleted Makes that DELETED messages will be skipped - * @param bool $uidfetch Enables UID FETCH usage - * @param bool $return_uid Enables returning UIDs instead of IDs - * - * @return rcube_result_index Response data - */ - public function index($mailbox, $message_set, $index_field='', $skip_deleted=true, - $uidfetch=false, $return_uid=false) - { - $msg_index = $this->fetchHeaderIndex($mailbox, $message_set, - $index_field, $skip_deleted, $uidfetch, $return_uid); - - if (!empty($msg_index)) { - asort($msg_index); // ASC - $msg_index = array_keys($msg_index); - $msg_index = '* SEARCH ' . implode(' ', $msg_index); - } - else { - $msg_index = is_array($msg_index) ? '* SEARCH' : null; - } - - return new ResultIndex($mailbox, $msg_index); - } - - /** - * Fetches specified header/data value for a set of messages. - * - * @param string $mailbox Mailbox name - * @param string|array $message_set Searching criteria (list of messages to return) - * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) - * @param bool $skip_deleted Makes that DELETED messages will be skipped - * @param bool $uidfetch Enables UID FETCH usage - * @param bool $return_uid Enables returning UIDs instead of IDs - * - * @return array|bool List of header values or False on failure - */ - public function fetchHeaderIndex($mailbox, $message_set, $index_field = '', $skip_deleted = true, - $uidfetch = false, $return_uid = false) - { - if (is_array($message_set)) { - if (!($message_set = $this->compressMessageSet($message_set))) { - return false; - } - } - else { - list($from_idx, $to_idx) = explode(':', $message_set); - if (empty($message_set) || - (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx) - ) { - return false; - } - } - - $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field); - - $fields_a['DATE'] = 1; - $fields_a['INTERNALDATE'] = 4; - $fields_a['ARRIVAL'] = 4; - $fields_a['FROM'] = 1; - $fields_a['REPLY-TO'] = 1; - $fields_a['SENDER'] = 1; - $fields_a['TO'] = 1; - $fields_a['CC'] = 1; - $fields_a['SUBJECT'] = 1; - $fields_a['UID'] = 2; - $fields_a['SIZE'] = 2; - $fields_a['SEEN'] = 3; - $fields_a['RECENT'] = 3; - $fields_a['DELETED'] = 3; - - if (!($mode = $fields_a[$index_field])) { - return false; - } - - // Select the mailbox - if (!$this->select($mailbox)) { - return false; - } - - // build FETCH command string - $key = $this->nextTag(); - $cmd = $uidfetch ? 'UID FETCH' : 'FETCH'; - $fields = array(); - - if ($return_uid) { - $fields[] = 'UID'; - } - if ($skip_deleted) { - $fields[] = 'FLAGS'; - } - - if ($mode == 1) { - if ($index_field == 'DATE') { - $fields[] = 'INTERNALDATE'; - } - $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]"; - } - else if ($mode == 2) { - if ($index_field == 'SIZE') { - $fields[] = 'RFC822.SIZE'; - } - else if (!$return_uid || $index_field != 'UID') { - $fields[] = $index_field; - } - } - else if ($mode == 3 && !$skip_deleted) { - $fields[] = 'FLAGS'; - } - else if ($mode == 4) { - $fields[] = 'INTERNALDATE'; - } - - $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")"; - - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); - return false; - } - - $result = array(); - - do { - $line = rtrim($this->readLine(200)); - $line = $this->multLine($line); - - if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { - $id = $m[1]; - $flags = null; - - if ($return_uid) { - if (preg_match('/UID ([0-9]+)/', $line, $matches)) { - $id = (int) $matches[1]; - } - else { - continue; - } - } - if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { - $flags = explode(' ', strtoupper($matches[1])); - if (in_array('\\DELETED', $flags)) { - continue; - } - } - - if ($mode == 1 && $index_field == 'DATE') { - if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) { - $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]); - $value = trim($value); - $result[$id] = Utils::strtotime($value); - } - // non-existent/empty Date: header, use INTERNALDATE - if (empty($result[$id])) { - if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { - $result[$id] = Utils::strtotime($matches[1]); - } - else { - $result[$id] = 0; - } - } - } - else if ($mode == 1) { - if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) { - $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]); - $result[$id] = trim($value); - } - else { - $result[$id] = ''; - } - } - else if ($mode == 2) { - if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) { - $result[$id] = trim($matches[1]); - } - else { - $result[$id] = 0; - } - } - else if ($mode == 3) { - if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { - $flags = explode(' ', $matches[1]); - } - $result[$id] = in_array("\\".$index_field, (array) $flags) ? 1 : 0; - } - else if ($mode == 4) { - if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { - $result[$id] = Utils::strtotime($matches[1]); - } - else { - $result[$id] = 0; - } - } - } - } - while (!$this->startsWith($line, $key, true, true)); - - return $result; - } - - /** - * Returns message sequence identifier - * - * @param string $mailbox Mailbox name - * @param int $uid Message unique identifier (UID) - * - * @return int Message sequence identifier - */ - public function UID2ID($mailbox, $uid) - { - if ($uid > 0) { - $index = $this->search($mailbox, "UID $uid"); - - if ($index->count() == 1) { - $arr = $index->get(); - return (int) $arr[0]; - } - } - } - - /** - * Returns message unique identifier (UID) - * - * @param string $mailbox Mailbox name - * @param int $uid Message sequence identifier - * - * @return int Message unique identifier - */ - public function ID2UID($mailbox, $id) - { - if (empty($id) || $id < 0) { - return null; - } - - if (!$this->select($mailbox)) { - return null; - } - - if ($uid = $this->data['UID-MAP'][$id]) { - return $uid; - } - - if (isset($this->data['EXISTS']) && $id > $this->data['EXISTS']) { - return null; - } - - $index = $this->search($mailbox, $id, true); - - if ($index->count() == 1) { - $arr = $index->get(); - return $this->data['UID-MAP'][$id] = (int) $arr[0]; - } - } - - /** - * Sets flag of the message(s) - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UID(s) - * @param string $flag Flag name - * - * @return bool True on success, False on failure - */ - public function flag($mailbox, $messages, $flag) - { - return $this->modFlag($mailbox, $messages, $flag, '+'); - } - - /** - * Unsets flag of the message(s) - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UID(s) - * @param string $flag Flag name - * - * @return bool True on success, False on failure - */ - public function unflag($mailbox, $messages, $flag) - { - return $this->modFlag($mailbox, $messages, $flag, '-'); - } - - /** - * Changes flag of the message(s) - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UID(s) - * @param string $flag Flag name - * @param string $mod Modifier [+|-]. Default: "+". - * - * @return bool True on success, False on failure - */ - protected function modFlag($mailbox, $messages, $flag, $mod = '+') - { - if (!$flag) { - return false; - } - - if (!$this->select($mailbox)) { - return false; - } - - if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); - return false; - } - - if (isset($this->flags[strtoupper($flag)]) && $this->flags[strtoupper($flag)]) { - $flag = $this->flags[strtoupper($flag)]; - } - - // if PERMANENTFLAGS is not specified all flags are allowed - if (!empty($this->data['PERMANENTFLAGS']) - && !in_array($flag, (array) $this->data['PERMANENTFLAGS']) - && !in_array('\\*', (array) $this->data['PERMANENTFLAGS']) - ) { - return false; - } - - // Clear internal status cache - if ($flag == 'SEEN') { - unset($this->data['STATUS:'.$mailbox]['UNSEEN']); - } - - if ($mod != '+' && $mod != '-') { - $mod = '+'; - } - - $result = $this->execute('UID STORE', array( - $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - /** - * Copies message(s) from one folder to another - * - * @param string|array $messages Message UID(s) - * @param string $from Mailbox name - * @param string $to Destination mailbox name - * - * @return bool True on success, False on failure - */ - public function copy($messages, $from, $to) - { - // Clear last COPYUID data - unset($this->data['COPYUID']); - - if (!$this->select($from)) { - return false; - } - - // Clear internal status cache - unset($this->data['STATUS:'.$to]); - - $result = $this->execute('UID COPY', array( - $this->compressMessageSet($messages), $this->escape($to)), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - /** - * Moves message(s) from one folder to another. - * - * @param string|array $messages Message UID(s) - * @param string $from Mailbox name - * @param string $to Destination mailbox name - * - * @return bool True on success, False on failure - */ - public function move($messages, $from, $to) - { - if (!$this->select($from)) { - return false; - } - - if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); - return false; - } - - // use MOVE command (RFC 6851) - if ($this->hasCapability('MOVE')) { - // Clear last COPYUID data - unset($this->data['COPYUID']); - - // Clear internal status cache - unset($this->data['STATUS:'.$to]); - $this->clear_status_cache($from); - - $result = $this->execute('UID MOVE', array( - $this->compressMessageSet($messages), $this->escape($to)), - self::COMMAND_NORESPONSE); - - return $result == self::ERROR_OK; - } - - // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE - $result = $this->copy($messages, $from, $to); - - if ($result) { - // Clear internal status cache - unset($this->data['STATUS:'.$from]); - - $result = $this->flag($from, $messages, 'DELETED'); - - if ($messages == '*') { - // CLOSE+SELECT should be faster than EXPUNGE - $this->close(); - } - else { - $this->expunge($from, $messages); - } - } - - return $result; - } - - /** - * FETCH command (RFC3501) - * - * @param string $mailbox Mailbox name - * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) - * @param bool $is_uid True if $message_set contains UIDs - * @param array $query_items FETCH command data items - * @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query - * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query - * - * @return array List of rcube_message_header elements, False on error - * @since 0.6 - */ - public function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(), - $mod_seq = null, $vanished = false) - { - if (!$this->select($mailbox)) { - return false; - } - - $message_set = $this->compressMessageSet($message_set); - $result = array(); - - $key = $this->nextTag(); - $cmd = ($is_uid ? 'UID ' : '') . 'FETCH'; - $request = "$key $cmd $message_set (" . implode(' ', $query_items) . ")"; - - if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) { - $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")"; - } - - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); - return false; - } - - do { - $line = $this->readFullLine(4096); - - if (!$line) { - break; - } - - // Sample reply line: - // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen) - // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...) - // BODY[HEADER.FIELDS ... - - if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { - - if ($is_uid && preg_match('/UID ([0-9]+)/', $line, $m2)) { - $id = intval($m2[1]); - } else { - $id = intval($m[1]); - } - - $message_id = intval($m[1]); - - $result[$id] = new MessageHeader; - $result[$id]->id = $message_id; - $result[$id]->subject = ''; - $result[$id]->messageID = 'mid:' . $message_id; - - $headers = null; - $lines = array(); - $line = substr($line, strlen($m[0]) + 2); - $ln = 0; - - // Tokenize response and assign to object properties - while (@list($name, $value) = $this->tokenizeResponse($line, 2)) { - if ($name == 'UID') { - $result[$id]->uid = intval($value); - } - else if ($name == 'RFC822.SIZE') { - $result[$id]->size = intval($value); - } - else if ($name == 'RFC822.TEXT') { - $result[$id]->body = $value; - } - else if ($name == 'INTERNALDATE') { - $result[$id]->internaldate = $value; - $result[$id]->date = $value; - $result[$id]->timestamp = Utils::strtotime($value); - } - else if ($name == 'FLAGS') { - if (!empty($value)) { - foreach ((array)$value as $flag) { - $flag = str_replace(array('$', "\\"), '', $flag); - $flag = strtoupper($flag); - - $result[$id]->flags[$flag] = true; - } - } - } - else if ($name == 'MODSEQ') { - $result[$id]->modseq = $value[0]; - } - else if ($name == 'ENVELOPE') { - $result[$id]->envelope = $value; - } - else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) { - if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) { - $value = array($value); - } - $result[$id]->bodystructure = $value; - } - else if ($name == 'RFC822') { - $result[$id]->body = $value; - } - else if (stripos($name, 'BODY[') === 0) { - $name = str_replace(']', '', substr($name, 5)); - - if ($name == 'HEADER.FIELDS') { - // skip ']' after headers list - $this->tokenizeResponse($line, 1); - $headers = $this->tokenizeResponse($line, 1); - } - else if (strlen($name)) { - $result[$id]->bodypart[$name] = $value; - } - else { - $result[$id]->body = $value; - } - } - } - - // create array with header field:data - if (!empty($headers)) { - $headers = explode("\n", trim($headers)); - foreach ($headers as $resln) { - if (ord($resln[0]) <= 32) { - $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln); - } - else { - $lines[++$ln] = trim($resln); - } - } - - foreach ($lines as $str) { - list($field, $string) = explode(':', $str, 2); - - $field = strtolower($field); - $string = preg_replace('/\n[\t\s]*/', ' ', trim($string)); - - switch ($field) { - case 'date'; - $string = substr($string, 0, 128); - $result[$id]->date = $string; - $result[$id]->timestamp = Utils::strtotime($string); - break; - case 'to': - $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string); - break; - case 'from': - case 'subject': - $string = substr($string, 0, 2048); - case 'cc': - case 'bcc': - case 'references': - $result[$id]->{$field} = $string; - break; - case 'reply-to': - $result[$id]->replyto = $string; - break; - case 'content-transfer-encoding': - $result[$id]->encoding = substr($string, 0, 32); - break; - case 'content-type': - $ctype_parts = preg_split('/[; ]+/', $string); - $result[$id]->ctype = strtolower(array_shift($ctype_parts)); - if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) { - $result[$id]->charset = $regs[1]; - } - break; - case 'in-reply-to': - $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string); - break; - case 'return-receipt-to': - case 'disposition-notification-to': - case 'x-confirm-reading-to': - $result[$id]->mdn_to = substr($string, 0, 2048); - break; - case 'message-id': - $result[$id]->messageID = substr($string, 0, 2048); - break; - case 'x-priority': - if (preg_match('/^(\d+)/', $string, $matches)) { - $result[$id]->priority = intval($matches[1]); - } - break; - default: - if (strlen($field) < 3) { - break; - } - if ($result[$id]->others[$field]) { - $string = array_merge((array)$result[$id]->others[$field], (array)$string); - } - $result[$id]->others[$field] = $string; - } - } - } - } - // VANISHED response (QRESYNC RFC5162) - // Sample: * VANISHED (EARLIER) 300:310,405,411 - else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { - $line = substr($line, strlen($match[0])); - $v_data = $this->tokenizeResponse($line, 1); - - $this->data['VANISHED'] = $v_data; - } - } - while (!$this->startsWith($line, $key, true)); - - return $result; - } - - /** - * Returns message(s) data (flags, headers, etc.) - * - * @param string $mailbox Mailbox name - * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) - * @param bool $is_uid True if $message_set contains UIDs - * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result - * @param array $add_headers List of additional headers - * - * @return bool|array List of rcube_message_header elements, False on error - */ - public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array()) - { - $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'); - $headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', - 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY'); - - if (!empty($add_headers)) { - $add_headers = array_map('strtoupper', $add_headers); - $headers = array_unique(array_merge($headers, $add_headers)); - } - - if ($bodystr) { - $query_items[] = 'BODYSTRUCTURE'; - } - - $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]'; - - return $this->fetch($mailbox, $message_set, $is_uid, $query_items); - } - - /** - * Returns message data (flags, headers, etc.) - * - * @param string $mailbox Mailbox name - * @param int $id Message sequence identifier or UID - * @param bool $is_uid True if $id is an UID - * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result - * @param array $add_headers List of additional headers - * - * @return bool|rcube_message_header Message data, False on error - */ - public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array()) - { - $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); - if (is_array($a)) { - return array_shift($a); - } - - return false; - } - - /** - * Sort messages by specified header field - * - * @param array $messages Array of rcube_message_header objects - * @param string $field Name of the property to sort by - * @param string $flag Sorting order (ASC|DESC) - * - * @return array Sorted input array - */ - public static function sortHeaders($messages, $field, $flag) - { - $field = empty($field) ? 'uid' : strtolower($field); - $order = empty($flag) ? 'ASC' : strtoupper($flag); - $index = array(); - - reset($messages); - - // Create an index - foreach ($messages as $key => $headers) { - switch ($field) { - case 'arrival': - $field = 'internaldate'; - // no-break - case 'date': - case 'internaldate': - case 'timestamp': - $value = Utils::strtotime($headers->$field); - if (!$value && $field != 'timestamp') { - $value = $headers->timestamp; - } - - break; - - default: - // @TODO: decode header value, convert to UTF-8 - $value = $headers->$field; - if (is_string($value)) { - $value = str_replace('"', '', $value); - - if ($field == 'subject') { - $value = preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value); - } - } - } - - $index[$key] = $value; - } - - $sort_order = $flag == 'ASC' ? SORT_ASC : SORT_DESC; - $sort_flags = SORT_STRING | SORT_FLAG_CASE; - - if (in_array($field, array('arrival', 'date', 'internaldate', 'timestamp'))) { - $sort_flags = SORT_NUMERIC; - } - - array_multisort($index, $sort_order, $sort_flags, $messages); - - return $messages; - } - - /** - * Fetch MIME headers of specified message parts - * - * @param string $mailbox Mailbox name - * @param int $uid Message UID - * @param array $parts Message part identifiers - * @param bool $mime Use MIME instad of HEADER - * - * @return array|bool Array containing headers string for each specified body - * False on failure. - */ - public function fetchMIMEHeaders($mailbox, $uid, $parts, $mime = true) - { - if (!$this->select($mailbox)) { - return false; - } - - $result = false; - $parts = (array) $parts; - $key = $this->nextTag(); - $peeks = array(); - $type = $mime ? 'MIME' : 'HEADER'; - - // format request - foreach ($parts as $part) { - $peeks[] = "BODY.PEEK[$part.$type]"; - } - - $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')'; - - // send request - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Failed to send UID FETCH command"); - return false; - } - - do { - $line = $this->readLine(1024); - - if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+/', $line, $m)) { - $line = ltrim(substr($line, strlen($m[0]))); - while (preg_match('/^BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) { - $line = substr($line, strlen($matches[0])); - $result[$matches[1]] = trim($this->multLine($line)); - $line = $this->readLine(1024); - } - } - } - while (!$this->startsWith($line, $key, true)); - - return $result; - } - - /** - * Fetches message part header - */ - public function fetchPartHeader($mailbox, $id, $is_uid = false, $part = null) - { - $part = empty($part) ? 'HEADER' : $part.'.MIME'; - - return $this->handlePartBody($mailbox, $id, $is_uid, $part); - } - - /** - * Fetches body of the specified message part - */ - public function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=null, $print=null, $file=null, $formatted=false, $max_bytes=0) - { - if (!$this->select($mailbox)) { - return false; - } - - $binary = true; - - do { - if (!$initiated) { - switch ($encoding) { - case 'base64': - $mode = 1; - break; - case 'quoted-printable': - $mode = 2; - break; - case 'x-uuencode': - case 'x-uue': - case 'uue': - case 'uuencode': - $mode = 3; - break; - default: - $mode = 0; - } - - // Use BINARY extension when possible (and safe) - $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); - $fetch_mode = $binary ? 'BINARY' : 'BODY'; - $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; - - // format request - $key = $this->nextTag(); - $cmd = ($is_uid ? 'UID ' : '') . 'FETCH'; - $request = "$key $cmd $id ($fetch_mode.PEEK[$part]$partial)"; - $result = false; - $found = false; - $initiated = true; - - // send request - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); - return false; - } - - if ($binary) { - // WARNING: Use $formatted argument with care, this may break binary data stream - $mode = -1; - } - } - - $line = trim($this->readLine(1024)); - - if (!$line) { - break; - } - - // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request - if ($binary && !$found && preg_match('/^' . $key . ' NO \[(UNKNOWN-CTE|PARSE)\]/i', $line)) { - $binary = $initiated = false; - continue; - } - - // skip irrelevant untagged responses (we have a result already) - if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { - continue; - } - - $line = $m[2]; - - // handle one line response - if ($line[0] == '(' && substr($line, -1) == ')') { - // tokenize content inside brackets - // the content can be e.g.: (UID 9844 BODY[2.4] NIL) - $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line)); - - for ($i=0; $i 0) { - $line = $this->readLine(8192); - - if ($line === null) { - break; - } - - $len = strlen($line); - - if ($len > $bytes) { - $line = substr($line, 0, $bytes); - $len = strlen($line); - } - $bytes -= $len; - - // BASE64 - if ($mode == 1) { - $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line); - // create chunks with proper length for base64 decoding - $line = $prev.$line; - $length = strlen($line); - if ($length % 4) { - $length = floor($length / 4) * 4; - $prev = substr($line, $length); - $line = substr($line, 0, $length); - } - else { - $prev = ''; - } - $line = base64_decode($line); - } - // QUOTED-PRINTABLE - else if ($mode == 2) { - $line = rtrim($line, "\t\r\0\x0B"); - $line = quoted_printable_decode($line); - } - // UUENCODE - else if ($mode == 3) { - $line = rtrim($line, "\t\r\n\0\x0B"); - if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) { - continue; - } - $line = convert_uudecode($line); - } - // default - else if ($formatted) { - $line = rtrim($line, "\t\r\n\0\x0B") . "\n"; - } - - if ($file) { - if (fwrite($file, $line) === false) { - break; - } - } - else if ($print) { - echo $line; - } - else { - $result .= $line; - } - } - } - } - while (!$this->startsWith($line, $key, true) || !$initiated); - - if ($result !== false) { - if ($file) { - return fwrite($file, $result); - } - else if ($print) { - echo $result; - return true; - } - - return $result; - } - - return false; - } - - /** - * Handler for IMAP APPEND command - * - * @param string $mailbox Mailbox name - * @param string|array $message The message source string or array (of strings and file pointers) - * @param array $flags Message flags - * @param string $date Message internal date - * @param bool $binary Enable BINARY append (RFC3516) - * - * @return string|bool On success APPENDUID response (if available) or True, False on failure - */ - public function append($mailbox, &$message, $flags = array(), $date = null, $binary = false) - { - unset($this->data['APPENDUID']); - - if ($mailbox === null || $mailbox === '') { - return false; - } - - $binary = $binary && $this->getCapability('BINARY'); - $literal_plus = !$binary && isset($this->prefs['literal+']) && $this->prefs['literal+']; - $len = 0; - $msg = is_array($message) ? $message : array(&$message); - $chunk_size = 512000; - - for ($i=0, $cnt=count($msg); $i<$cnt; $i++) { - if (is_resource($msg[$i])) { - $stat = fstat($msg[$i]); - if ($stat === false) { - return false; - } - $len += $stat['size']; - } - else { - if (!$binary) { - $msg[$i] = str_replace("\r", '', $msg[$i]); - $msg[$i] = str_replace("\n", "\r\n", $msg[$i]); - } - - $len += strlen($msg[$i]); - } - } - - if (!$len) { - return false; - } - - // build APPEND command - $key = $this->nextTag(); - $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; - if (!empty($date)) { - $request .= ' ' . $this->escape($date); - } - $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}'; - - // send APPEND command - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Failed to send APPEND command"); - return false; - } - - // Do not wait when LITERAL+ is supported - if (!$literal_plus) { - $line = $this->readReply(); - - if ($line[0] != '+') { - $this->parseResult($line, 'APPEND: '); - return false; - } - } - - foreach ($msg as $msg_part) { - // file pointer - if (is_resource($msg_part)) { - rewind($msg_part); - while (!feof($msg_part) && $this->fp) { - $buffer = fread($msg_part, $chunk_size); - $this->putLine($buffer, false); - } - fclose($msg_part); - } - // string - else { - $size = strlen($msg_part); - - // Break up the data by sending one chunk (up to 512k) at a time. - // This approach reduces our peak memory usage - for ($offset = 0; $offset < $size; $offset += $chunk_size) { - $chunk = substr($msg_part, $offset, $chunk_size); - if (!$this->putLine($chunk, false)) { - return false; - } - } - } - } - - if (!$this->putLine('')) { // \r\n - return false; - } - - do { - $line = $this->readLine(); - } while (!$this->startsWith($line, $key, true, true)); - - // Clear internal status cache - unset($this->data['STATUS:'.$mailbox]); - - if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) { - return false; - } - - if (!empty($this->data['APPENDUID'])) { - return $this->data['APPENDUID']; - } - - return true; - } - - /** - * Handler for IMAP APPEND command. - * - * @param string $mailbox Mailbox name - * @param string $path Path to the file with message body - * @param string $headers Message headers - * @param array $flags Message flags - * @param string $date Message internal date - * @param bool $binary Enable BINARY append (RFC3516) - * - * @return string|bool On success APPENDUID response (if available) or True, False on failure - */ - public function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false) - { - // open message file - if (file_exists(realpath($path))) { - $fp = fopen($path, 'r'); - } - - if (!$fp) { - $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading"); - return false; - } - - $message = array(); - if ($headers) { - $message[] = trim($headers, "\r\n") . "\r\n\r\n"; - } - $message[] = $fp; - - return $this->append($mailbox, $message, $flags, $date, $binary); - } - - /** - * Returns QUOTA information - * - * @param string $mailbox Mailbox name - * - * @return array Quota information - */ - public function getQuota($mailbox = null) - { - if ($mailbox === null || $mailbox === '') { - $mailbox = 'INBOX'; - } - - // a0001 GETQUOTAROOT INBOX - // * QUOTAROOT INBOX user/sample - // * QUOTA user/sample (STORAGE 654 9765) - // a0001 OK Completed - - list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox)), 0, '/^\* QUOTA /i'); - - $result = false; - $min_free = PHP_INT_MAX; - $all = array(); - - if ($code == self::ERROR_OK) { - foreach (explode("\n", $response) as $line) { - list(, , $quota_root) = $this->tokenizeResponse($line, 3); - - $quotas = $this->tokenizeResponse($line, 1); - - if (empty($quotas)) { - continue; - } - - foreach (array_chunk($quotas, 3) as $quota) { - list($type, $used, $total) = $quota; - $type = strtolower($type); - - if ($type && $total) { - $all[$quota_root][$type]['used'] = intval($used); - $all[$quota_root][$type]['total'] = intval($total); - } - } - - if (empty($all[$quota_root]['storage'])) { - continue; - } - - $used = $all[$quota_root]['storage']['used']; - $total = $all[$quota_root]['storage']['total']; - $free = $total - $used; - - // calculate lowest available space from all storage quotas - if ($free < $min_free) { - $min_free = $free; - $result['used'] = $used; - $result['total'] = $total; - $result['percent'] = min(100, round(($used/max(1,$total))*100)); - $result['free'] = 100 - $result['percent']; - } - } - } - - if (!empty($result)) { - $result['all'] = $all; - } - - return $result; - } - - /** - * Send the SETACL command (RFC4314) - * - * @param string $mailbox Mailbox name - * @param string $user User name - * @param mixed $acl ACL string or array - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - public function setACL($mailbox, $user, $acl) - { - if (is_array($acl)) { - $acl = implode('', $acl); - } - - $result = $this->execute('SETACL', array( - $this->escape($mailbox), $this->escape($user), strtolower($acl)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the DELETEACL command (RFC4314) - * - * @param string $mailbox Mailbox name - * @param string $user User name - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - public function deleteACL($mailbox, $user) - { - $result = $this->execute('DELETEACL', array( - $this->escape($mailbox), $this->escape($user)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the GETACL command (RFC4314) - * - * @param string $mailbox Mailbox name - * - * @return array User-rights array on success, NULL on error - * @since 0.5-beta - */ - public function getACL($mailbox) - { - list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)), 0, '/^\* ACL /i'); - - if ($code == self::ERROR_OK && $response) { - // Parse server response (remove "* ACL ") - $response = substr($response, 6); - $ret = $this->tokenizeResponse($response); - $mbox = array_shift($ret); - $size = count($ret); - - // Create user-rights hash array - // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1 - // so we could return only standard rights defined in RFC4314, - // excluding 'c' and 'd' defined in RFC2086. - if ($size % 2 == 0) { - for ($i=0; $i<$size; $i++) { - $ret[$ret[$i]] = str_split($ret[++$i]); - unset($ret[$i-1]); - unset($ret[$i]); - } - return $ret; - } - - $this->setError(self::ERROR_COMMAND, "Incomplete ACL response"); - } - } - - /** - * Send the LISTRIGHTS command (RFC4314) - * - * @param string $mailbox Mailbox name - * @param string $user User name - * - * @return array List of user rights - * @since 0.5-beta - */ - public function listRights($mailbox, $user) - { - list($code, $response) = $this->execute('LISTRIGHTS', - array($this->escape($mailbox), $this->escape($user)), 0, '/^\* LISTRIGHTS /i'); - - if ($code == self::ERROR_OK && $response) { - // Parse server response (remove "* LISTRIGHTS ") - $response = substr($response, 13); - - $ret_mbox = $this->tokenizeResponse($response, 1); - $ret_user = $this->tokenizeResponse($response, 1); - $granted = $this->tokenizeResponse($response, 1); - $optional = trim($response); - - return array( - 'granted' => str_split($granted), - 'optional' => explode(' ', $optional), - ); - } - } - - /** - * Send the MYRIGHTS command (RFC4314) - * - * @param string $mailbox Mailbox name - * - * @return array MYRIGHTS response on success, NULL on error - * @since 0.5-beta - */ - public function myRights($mailbox) - { - list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)), 0, '/^\* MYRIGHTS /i'); - - if ($code == self::ERROR_OK && $response) { - // Parse server response (remove "* MYRIGHTS ") - $response = substr($response, 11); - - $ret_mbox = $this->tokenizeResponse($response, 1); - $rights = $this->tokenizeResponse($response, 1); - - return str_split($rights); - } - } - - /** - * Send the SETMETADATA command (RFC5464) - * - * @param string $mailbox Mailbox name - * @param array $entries Entry-value array (use NULL value as NIL) - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - public function setMetadata($mailbox, $entries) - { - if (!is_array($entries) || empty($entries)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); - return false; - } - - foreach ($entries as $name => $value) { - $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true); - } - - $entries = implode(' ', $entries); - $result = $this->execute('SETMETADATA', array( - $this->escape($mailbox), '(' . $entries . ')'), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the SETMETADATA command with NIL values (RFC5464) - * - * @param string $mailbox Mailbox name - * @param array $entries Entry names array - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - public function deleteMetadata($mailbox, $entries) - { - if (!is_array($entries) && !empty($entries)) { - $entries = explode(' ', $entries); - } - - if (empty($entries)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); - return false; - } - - foreach ($entries as $entry) { - $data[$entry] = null; - } - - return $this->setMetadata($mailbox, $data); - } - - /** - * Send the GETMETADATA command (RFC5464) - * - * @param string $mailbox Mailbox name - * @param array $entries Entries - * @param array $options Command options (with MAXSIZE and DEPTH keys) - * - * @return array GETMETADATA result on success, NULL on error - * - * @since 0.5-beta - */ - public function getMetadata($mailbox, $entries, $options=array()) - { - if (!is_array($entries)) { - $entries = array($entries); - } - - // create entries string - foreach ($entries as $idx => $name) { - $entries[$idx] = $this->escape($name); - } - - $optlist = ''; - $entlist = '(' . implode(' ', $entries) . ')'; - - // create options string - if (is_array($options)) { - $options = array_change_key_case($options, CASE_UPPER); - $opts = array(); - - if (!empty($options['MAXSIZE'])) { - $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']); - } - if (!empty($options['DEPTH'])) { - $opts[] = 'DEPTH '.intval($options['DEPTH']); - } - - if ($opts) { - $optlist = '(' . implode(' ', $opts) . ')'; - } - } - - $optlist .= ($optlist ? ' ' : '') . $entlist; - - list($code, $response) = $this->execute('GETMETADATA', array( - $this->escape($mailbox), $optlist)); - - if ($code == self::ERROR_OK) { - $result = array(); - $data = $this->tokenizeResponse($response); - - // The METADATA response can contain multiple entries in a single - // response or multiple responses for each entry or group of entries - for ($i = 0, $size = count($data); $i < $size; $i++) { - if ($data[$i] === '*' - && $data[++$i] === 'METADATA' - && is_string($mbox = $data[++$i]) - && is_array($data[++$i]) - ) { - for ($x = 0, $size2 = count($data[$i]); $x < $size2; $x += 2) { - if ($data[$i][$x+1] !== null) { - $result[$mbox][$data[$i][$x]] = $data[$i][$x+1]; - } - } - } - } - - return $result; - } - } - - /** - * Send the SETANNOTATION command (draft-daboo-imap-annotatemore) - * - * @param string $mailbox Mailbox name - * @param array $data Data array where each item is an array with - * three elements: entry name, attribute name, value - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - public function setAnnotation($mailbox, $data) - { - if (!is_array($data) || empty($data)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); - return false; - } - - foreach ($data as $entry) { - // ANNOTATEMORE drafts before version 08 require quoted parameters - $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true), - $this->escape($entry[1], true), $this->escape($entry[2], true)); - } - - $entries = implode(' ', $entries); - $result = $this->execute('SETANNOTATION', array( - $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore) - * - * @param string $mailbox Mailbox name - * @param array $data Data array where each item is an array with - * two elements: entry name and attribute name - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - public function deleteAnnotation($mailbox, $data) - { - if (!is_array($data) || empty($data)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); - return false; - } - - return $this->setAnnotation($mailbox, $data); - } - - /** - * Send the GETANNOTATION command (draft-daboo-imap-annotatemore) - * - * @param string $mailbox Mailbox name - * @param array $entries Entries names - * @param array $attribs Attribs names - * - * @return array Annotations result on success, NULL on error - * - * @since 0.5-beta - */ - public function getAnnotation($mailbox, $entries, $attribs) - { - if (!is_array($entries)) { - $entries = array($entries); - } - - // create entries string - // ANNOTATEMORE drafts before version 08 require quoted parameters - foreach ($entries as $idx => $name) { - $entries[$idx] = $this->escape($name, true); - } - $entries = '(' . implode(' ', $entries) . ')'; - - if (!is_array($attribs)) { - $attribs = array($attribs); - } - - // create attributes string - foreach ($attribs as $idx => $name) { - $attribs[$idx] = $this->escape($name, true); - } - $attribs = '(' . implode(' ', $attribs) . ')'; - - list($code, $response) = $this->execute('GETANNOTATION', array( - $this->escape($mailbox), $entries, $attribs)); - - if ($code == self::ERROR_OK) { - $result = array(); - $data = $this->tokenizeResponse($response); - - // Here we returns only data compatible with METADATA result format - if (!empty($data) && ($size = count($data))) { - for ($i=0; $i<$size; $i++) { - $entry = $data[$i]; - if (isset($mbox) && is_array($entry)) { - $attribs = $entry; - $entry = $last_entry; - } - else if ($entry == '*') { - if ($data[$i+1] == 'ANNOTATION') { - $mbox = $data[$i+2]; - unset($data[$i]); // "*" - unset($data[++$i]); // "ANNOTATION" - unset($data[++$i]); // Mailbox - } - // get rid of other untagged responses - else { - unset($mbox); - unset($data[$i]); - } - continue; - } - else if (isset($mbox)) { - $attribs = $data[++$i]; - } - else { - unset($data[$i]); - continue; - } - - if (!empty($attribs)) { - for ($x=0, $len=count($attribs); $x<$len;) { - $attr = $attribs[$x++]; - $value = $attribs[$x++]; - if ($attr == 'value.priv' && $value !== null) { - $result[$mbox]['/private' . $entry] = $value; - } - else if ($attr == 'value.shared' && $value !== null) { - $result[$mbox]['/shared' . $entry] = $value; - } - } - } - $last_entry = $entry; - unset($data[$i]); - } - } - - return $result; - } - } - - /** - * Returns BODYSTRUCTURE for the specified message. - * - * @param string $mailbox Folder name - * @param int $id Message sequence number or UID - * @param bool $is_uid True if $id is an UID - * - * @return array/bool Body structure array or False on error. - * @since 0.6 - */ - public function getStructure($mailbox, $id, $is_uid = false) - { - $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE')); - - if (is_array($result)) { - $result = array_shift($result); - return $result->bodystructure; - } - - return false; - } - - /** - * Returns data of a message part according to specified structure. - * - * @param array $structure Message structure (getStructure() result) - * @param string $part Message part identifier - * - * @return array Part data as hash array (type, encoding, charset, size) - */ - public static function getStructurePartData($structure, $part) - { - $part_a = self::getStructurePartArray($structure, $part); - $data = array(); - - if (empty($part_a)) { - return $data; - } - - // content-type - if (is_array($part_a[0])) { - $data['type'] = 'multipart'; - } - else { - $data['type'] = strtolower($part_a[0]); - $data['subtype'] = strtolower($part_a[1]); - $data['encoding'] = strtolower($part_a[5]); - - // charset - if (is_array($part_a[2])) { - foreach ($part_a[2] as $key => $val) { - if (strcasecmp($val, 'charset') == 0) { - $data['charset'] = $part_a[2][$key+1]; - break; - } - } - } - } - - // size - $data['size'] = intval($part_a[6]); - - return $data; - } - - public static function getStructurePartArray($a, $part) - { - if (!is_array($a)) { - return false; - } - - if (empty($part)) { - return $a; - } - - $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : ''; - - if (strcasecmp($ctype, 'message/rfc822') == 0) { - $a = $a[8]; - } - - if (strpos($part, '.') > 0) { - $orig_part = $part; - $pos = strpos($part, '.'); - $rest = substr($orig_part, $pos+1); - $part = substr($orig_part, 0, $pos); - - return self::getStructurePartArray($a[$part-1], $rest); - } - else if ($part > 0) { - return (is_array($a[$part-1])) ? $a[$part-1] : $a; - } - } - - /** - * Creates next command identifier (tag) - * - * @return string Command identifier - * @since 0.5-beta - */ - public function nextTag() - { - $this->cmd_num++; - $this->cmd_tag = sprintf('A%04d', $this->cmd_num); - - return $this->cmd_tag; - } - - /** - * Sends IMAP command and parses result - * - * @param string $command IMAP command - * @param array $arguments Command arguments - * @param int $options Execution options - * @param string $filter Line filter (regexp) - * - * @return mixed Response code or list of response code and data - * @since 0.5-beta - */ - public function execute($command, $arguments = array(), $options = 0, $filter = null) - { - $tag = $this->nextTag(); - $query = $tag . ' ' . $command; - $noresp = ($options & self::COMMAND_NORESPONSE); - $response = $noresp ? null : ''; - - if (!empty($arguments)) { - foreach ($arguments as $arg) { - $query .= ' ' . self::r_implode($arg); - } - } - - // Send command - if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) { - preg_match('/^[A-Z0-9]+ ((UID )?[A-Z]+)/', $query, $matches); - $cmd = $matches[1] ?: 'UNKNOWN'; - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); - - return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, ''); - } - - // Parse response - do { - $line = $this->readFullLine(4096); - - if ($response !== null) { - if (!$filter || preg_match($filter, $line)) { - $response .= $line; - } - } - - // parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851) - if ($line && $command == 'UID MOVE') { - if (preg_match("/^\* OK \[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $line, $m)) { - $this->data['COPYUID'] = array($m[1], $m[2]); - } - } - } - while (!$this->startsWith($line, $tag . ' ', true, true)); - - $code = $this->parseResult($line, $command . ': '); - - // Remove last line from response - if ($response) { - if (!$filter) { - $line_len = min(strlen($response), strlen($line)); - $response = substr($response, 0, -$line_len); - } - - $response = rtrim($response, "\r\n"); - } - - // optional CAPABILITY response - if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK - && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches) - ) { - $this->parseCapability($matches[1], true); - } - - // return last line only (without command tag, result and response code) - if ($line && ($options & self::COMMAND_LASTLINE)) { - $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line)); - } - - // return raw last line only (without command tag, result and response code) - if ($line && ($options & self::COMMAND_RAW_LASTLINE)) { - $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*/i", '', trim($line)); - } - - return $noresp ? $code : array($code, $response); - } - - /** - * Splits IMAP response into string tokens - * - * @param string &$str The IMAP's server response - * @param int $num Number of tokens to return - * - * @return mixed Tokens array or string if $num=1 - * @since 0.5-beta - */ - public static function tokenizeResponse(&$str, $num=0) - { - $result = array(); - - while (!$num || count($result) < $num) { - // remove spaces from the beginning of the string - $str = ltrim($str); - - switch ($str[0]) { - - // String literal - case '{': - if (($epos = strpos($str, "}\r\n", 1)) == false) { - // error - } - if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) { - // error - } - - $result[] = $bytes ? substr($str, $epos + 3, $bytes) : ''; - $str = substr($str, $epos + 3 + $bytes); - break; - - // Quoted string - case '"': - $len = strlen($str); - - for ($pos=1; $pos<$len; $pos++) { - if ($str[$pos] == '"') { - break; - } - if ($str[$pos] == "\\") { - if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") { - $pos++; - } - } - } - - // we need to strip slashes for a quoted string - $result[] = stripslashes(substr($str, 1, $pos - 1)); - $str = substr($str, $pos + 1); - break; - - // Parenthesized list - case '(': - $str = substr($str, 1); - $result[] = self::tokenizeResponse($str); - break; - - case ')': - $str = substr($str, 1); - return $result; - - // String atom, number, astring, NIL, *, % - default: - // empty string - if ($str === '' || $str === null) { - break 2; - } - - // excluded chars: SP, CTL, ), DEL - // we do not exclude [ and ] (#1489223) - if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) { - $result[] = $m[1] == 'NIL' ? null : $m[1]; - $str = substr($str, strlen($m[1])); - } - break; - } - } - - return $num == 1 ? $result[0] : $result; - } - - /** - * Joins IMAP command line elements (recursively) - */ - protected static function r_implode($element) - { - $string = ''; - - if (is_array($element)) { - reset($element); - foreach ($element as $value) { - $string .= ' ' . self::r_implode($value); - } - } - else { - return $element; - } - - return '(' . trim($string) . ')'; - } - - /** - * Converts message identifiers array into sequence-set syntax - * - * @param array $messages Message identifiers - * @param bool $force Forces compression of any size - * - * @return string Compressed sequence-set - */ - public static function compressMessageSet($messages, $force=false) - { - // given a comma delimited list of independent mid's, - // compresses by grouping sequences together - if (!is_array($messages)) { - // if less than 255 bytes long, let's not bother - if (!$force && strlen($messages) < 255) { - return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages; - } - - // see if it's already been compressed - if (strpos($messages, ':') !== false) { - return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages; - } - - // separate, then sort - $messages = explode(',', $messages); - } - - sort($messages); - - $result = array(); - $start = $prev = $messages[0]; - - foreach ($messages as $id) { - $incr = $id - $prev; - if ($incr > 1) { // found a gap - if ($start == $prev) { - $result[] = $prev; // push single id - } - else { - $result[] = $start . ':' . $prev; // push sequence as start_id:end_id - } - $start = $id; // start of new sequence - } - $prev = $id; - } - - // handle the last sequence/id - if ($start == $prev) { - $result[] = $prev; - } - else { - $result[] = $start.':'.$prev; - } - - // return as comma separated string - $result = implode(',', $result); - - return preg_match('/[^0-9:,*]/', $result) ? 'INVALID' : $result; - } - - /** - * Converts message sequence-set into array - * - * @param string $messages Message identifiers - * - * @return array List of message identifiers - */ - public static function uncompressMessageSet($messages) - { - if (empty($messages)) { - return array(); - } - - $result = array(); - $messages = explode(',', $messages); - - foreach ($messages as $idx => $part) { - $items = explode(':', $part); - $max = max($items[0], $items[1] ?? $items[0]); - - for ($x=$items[0]; $x<=$max; $x++) { - $result[] = (int)$x; - } - unset($messages[$idx]); - } - - return $result; - } - - /** - * Clear internal status cache - */ - protected function clear_status_cache($mailbox) - { - unset($this->data['STATUS:' . $mailbox]); - - $keys = array('EXISTS', 'RECENT', 'UNSEEN', 'UID-MAP'); - - foreach ($keys as $key) { - unset($this->data[$key]); - } - } - - /** - * Clear internal cache of the current mailbox - */ - protected function clear_mailbox_cache() - { - $this->clear_status_cache($this->selected); - - $keys = array('UIDNEXT', 'UIDVALIDITY', 'HIGHESTMODSEQ', 'NOMODSEQ', - 'PERMANENTFLAGS', 'QRESYNC', 'VANISHED', 'READ-WRITE'); - - foreach ($keys as $key) { - unset($this->data[$key]); - } - } - - /** - * Converts flags array into string for inclusion in IMAP command - * - * @param array $flags Flags (see self::flags) - * - * @return string Space-separated list of flags - */ - protected function flagsToStr($flags) - { - foreach ((array)$flags as $idx => $flag) { - if ($flag = $this->flags[strtoupper($flag)]) { - $flags[$idx] = $flag; - } - } - - return implode(' ', (array)$flags); - } - - /** - * CAPABILITY response parser - */ - protected function parseCapability($str, $trusted=false) - { - $str = preg_replace('/^\* CAPABILITY /i', '', $str); - - $this->capability = explode(' ', strtoupper($str)); - - if (!empty($this->prefs['disabled_caps'])) { - $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']); - } - - if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) { - $this->prefs['literal+'] = true; - } - - if ($trusted) { - $this->capability_readed = true; - } - } - - /** - * Escapes a string when it contains special characters (RFC3501) - * - * @param string $string IMAP string - * @param boolean $force_quotes Forces string quoting (for atoms) - * - * @return string String atom, quoted-string or string literal - * @todo lists - */ - public static function escape($string, $force_quotes=false) - { - if ($string === null) { - return 'NIL'; - } - - if ($string === '') { - return '""'; - } - - // atom-string (only safe characters) - if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) { - return $string; - } - - // quoted-string - if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) { - return '"' . addcslashes($string, '\\"') . '"'; - } - - // literal-string - return sprintf("{%d}\r\n%s", strlen($string), $string); - } - - /** - * Set the value of the debugging flag. - * - * @param boolean $debug New value for the debugging flag. - * @param callback $handler Logging handler function - * - * @since 0.5-stable - */ - public function setDebug($debug, $handler = null) - { - $this->debug = $debug; - $this->debug_handler = $handler; - } - - /** - * Write the given debug text to the current debug output handler. - * - * @param string $message Debug message text. - * - * @since 0.5-stable - */ - protected function debug($message) - { - if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { - $diff = $len - self::DEBUG_LINE_LENGTH; - $message = substr($message, 0, self::DEBUG_LINE_LENGTH) - . "... [truncated $diff bytes]"; - } - - if ($this->resourceid) { - $message = sprintf('[%s] %s', $this->resourceid, $message); - } - - if ($this->debug_handler) { - call_user_func_array($this->debug_handler, array(&$this, $message)); - } - else { - echo "DEBUG: $message\n"; - } - } - - public function getRawLastLine() - { - return $this->rawLastLine; - } -} diff --git a/src/Roundcube/MessageHeader.php b/src/Roundcube/MessageHeader.php deleted file mode 100644 index 0c1201d..0000000 --- a/src/Roundcube/MessageHeader.php +++ /dev/null @@ -1,276 +0,0 @@ - | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -/** - * Struct representing an e-mail message header - * - * @package Framework - * @subpackage Storage - */ -class MessageHeader -{ - /** - * Message sequence number - * - * @var int - */ - public $id; - - /** - * Message unique identifier - * - * @var int - */ - public $uid; - - /** - * Message subject - * - * @var string - */ - public $subject; - - /** - * Message sender (From) - * - * @var string - */ - public $from; - - /** - * Message recipient (To) - * - * @var string - */ - public $to; - - /** - * Message additional recipients (Cc) - * - * @var string - */ - public $cc; - - /** - * Message Reply-To header - * - * @var string - */ - public $replyto; - - /** - * Message In-Reply-To header - * - * @var string - */ - public $in_reply_to; - - /** - * Message date (Date) - * - * @var string - */ - public $date; - - /** - * Message identifier (Message-ID) - * - * @var string - */ - public $messageID; - - /** - * Message size - * - * @var int - */ - public $size; - - /** - * Message encoding - * - * @var string - */ - public $encoding; - - /** - * Message charset - * - * @var string - */ - public $charset; - - /** - * Message Content-type - * - * @var string - */ - public $ctype; - - /** - * Message timestamp (based on message date) - * - * @var int - */ - public $timestamp; - - /** - * IMAP bodystructure string - * - * @var string - */ - public $bodystructure; - - /** - * IMAP internal date - * - * @var string - */ - public $internaldate; - - /** - * Message References header - * - * @var string - */ - public $references; - - /** - * Message priority (X-Priority) - * - * @var int - */ - public $priority; - - /** - * Message receipt recipient - * - * @var string - */ - public $mdn_to; - - /** - * IMAP folder this message is stored in - * - * @var string - */ - public $folder; - - /** - * Other message headers - * - * @var array - */ - public $others = array(); - - /** - * Message flags - * - * @var array - */ - public $flags = array(); - - // map header to rcube_message_header object property - private $obj_headers = array( - 'date' => 'date', - 'from' => 'from', - 'to' => 'to', - 'subject' => 'subject', - 'reply-to' => 'replyto', - 'cc' => 'cc', - 'bcc' => 'bcc', - 'mbox' => 'folder', - 'folder' => 'folder', - 'content-transfer-encoding' => 'encoding', - 'in-reply-to' => 'in_reply_to', - 'content-type' => 'ctype', - 'charset' => 'charset', - 'references' => 'references', - 'return-receipt-to' => 'mdn_to', - 'disposition-notification-to' => 'mdn_to', - 'x-confirm-reading-to' => 'mdn_to', - 'message-id' => 'messageID', - 'x-priority' => 'priority', - ); - - /** - * Returns header value - */ - public function get($name, $decode = true) - { - $name = strtolower($name); - - if (isset($this->obj_headers[$name])) { - $value = $this->{$this->obj_headers[$name]}; - } - else { - $value = $this->others[$name]; - } - - if ($decode) { - if (is_array($value)) { - foreach ($value as $key => $val) { - $val = Mime::decode_header($val, $this->charset); - $value[$key] = Charset::clean($val); - } - } - else { - $value = Mime::decode_header($value, $this->charset); - $value = Charset::clean($value); - } - } - - return $value; - } - - /** - * Sets header value - */ - public function set($name, $value) - { - $name = strtolower($name); - - if (isset($this->obj_headers[$name])) { - $this->{$this->obj_headers[$name]} = $value; - } - else { - $this->others[$name] = $value; - } - } - - - /** - * Factory method to instantiate headers from a data array - * - * @param array Hash array with header values - * @return object rcube_message_header instance filled with headers values - */ - public static function from_array($arr) - { - $obj = new MessageHeader; - foreach ($arr as $k => $v) - $obj->set($k, $v); - - return $obj; - } -} - diff --git a/src/Roundcube/MessageHeaderSorter.php b/src/Roundcube/MessageHeaderSorter.php deleted file mode 100644 index ebb3b3d..0000000 --- a/src/Roundcube/MessageHeaderSorter.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -class MessageHeaderSorter -{ - private $uids = array(); - - - /** - * Set the predetermined sort order. - * - * @param array $index Numerically indexed array of IMAP UIDs - */ - function set_index($index) - { - $index = array_flip($index); - - $this->uids = $index; - } - - /** - * Sort the array of header objects - * - * @param array $headers Array of rcube_message_header objects indexed by UID - */ - function sort_headers(&$headers) - { - uksort($headers, array($this, "compare_uids")); - } - - /** - * Sort method called by uksort() - * - * @param int $a Array key (UID) - * @param int $b Array key (UID) - */ - function compare_uids($a, $b) - { -// then find each sequence number in my ordered list - $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1; - $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1; - -// return the relative position as the comparison value - return $posa - $posb; - } -} diff --git a/src/Roundcube/Mime.php b/src/Roundcube/Mime.php deleted file mode 100644 index 334f4de..0000000 --- a/src/Roundcube/Mime.php +++ /dev/null @@ -1,901 +0,0 @@ - | -| Author: Aleksander Machniak | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -use Javanile\Imap2\rcube; -use Javanile\Imap2\rcube_mime_decode; - -/** - * Class for parsing MIME messages - * - * @package Framework - * @subpackage Storage - */ -class Mime -{ - private static $default_charset; - - - /** - * Object constructor. - */ - function __construct($default_charset = null) - { - self::$default_charset = $default_charset; - } - - /** - * Returns message/object character set name - * - * @return string Character set name - */ - public static function get_charset() - { - if (self::$default_charset) { - return self::$default_charset; - } - - if ($charset = IMAP2_CHARSET) { - return $charset; - } - - return IMAP2_CHARSET; - } - - /** - * Parse the given raw message source and return a structure - * of rcube_message_part objects. - * - * It makes use of the rcube_mime_decode library - * - * @param string $raw_body The message source - * - * @return object rcube_message_part The message structure - */ - public static function parse_message($raw_body) - { - $conf = array( - 'include_bodies' => true, - 'decode_bodies' => true, - 'decode_headers' => false, - 'default_charset' => self::get_charset(), - ); - - $mime = new rcube_mime_decode($conf); - - return $mime->decode($raw_body); - } - - /** - * Split an address list into a structured array list - * - * @param string|array $input Input string (or list of strings) - * @param int $max List only this number of addresses - * @param boolean $decode Decode address strings - * @param string $fallback Fallback charset if none specified - * @param boolean $addronly Return flat array with e-mail addresses only - * - * @return array Indexed list of addresses - */ - static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false) - { - // A common case when the same header is used many times in a mail message - if (is_array($input)) { - $input = implode(', ', $input); - } - - $a = self::parse_address_list($input, $decode, $fallback); - $out = array(); - $j = 0; - - // Special chars as defined by RFC 822 need to in quoted string (or escaped). - $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]'; - - if (!is_array($a)) { - return $out; - } - - foreach ($a as $val) { - $j++; - $address = trim($val['address']); - - if ($addronly) { - $out[$j] = $address; - } - else { - $name = trim($val['name']); - if ($name && $address && $name != $address) - $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address); - else if ($address) - $string = $address; - else if ($name) - $string = $name; - - $out[$j] = array('name' => $name, 'mailto' => $address, 'string' => $string); - } - - if ($max && $j==$max) - break; - } - - return $out; - } - - /** - * Decode a message header value - * - * @param string $input Header value - * @param string $fallback Fallback charset if none specified - * - * @return string Decoded string - */ - public static function decode_header($input, $fallback = null) - { - $str = self::decode_mime_string((string)$input, $fallback); - - return $str; - } - - /** - * Decode a mime-encoded string to internal charset - * - * @param string $input Header value - * @param string $fallback Fallback charset if none specified - * - * @return string Decoded string - */ - public static function decode_mime_string($input, $fallback = null) - { - $default_charset = $fallback ?: self::get_charset(); - - // rfc: all line breaks or other characters not found - // in the Base64 Alphabet must be ignored by decoding software - // delete all blanks between MIME-lines, differently we can - // receive unnecessary blanks and broken utf-8 symbols - $input = preg_replace("/\?=\s+=\?/", '?==?', $input); - - // encoded-word regexp - $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/'; - - // Find all RFC2047's encoded words - if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { - // Initialize variables - $tmp = array(); - $out = ''; - $start = 0; - - foreach ($matches as $idx => $m) { - $pos = $m[0][1]; - $charset = $m[1][0]; - $encoding = $m[2][0]; - $text = $m[3][0]; - $length = strlen($m[0][0]); - - // Append everything that is before the text to be decoded - if ($start != $pos) { - $substr = substr($input, $start, $pos-$start); - $out .= Charset::convert($substr, $default_charset); - $start = $pos; - } - $start += $length; - - // Per RFC2047, each string part "MUST represent an integral number - // of characters . A multi-octet character may not be split across - // adjacent encoded-words." However, some mailers break this, so we - // try to handle characters spanned across parts anyway by iterating - // through and aggregating sequential encoded parts with the same - // character set and encoding, then perform the decoding on the - // aggregation as a whole. - - $tmp[] = $text; - if (isset($matches[$idx+1]) && ($next_match = $matches[$idx+1])) { - if ($next_match[0][1] == $start - && $next_match[1][0] == $charset - && $next_match[2][0] == $encoding - ) { - continue; - } - } - - $count = count($tmp); - $text = ''; - - // Decode and join encoded-word's chunks - if ($encoding == 'B' || $encoding == 'b') { - $rest = ''; - // base64 must be decoded a segment at a time. - // However, there are broken implementations that continue - // in the following word, we'll handle that (#6048) - for ($i=0; $i<$count; $i++) { - $chunk = $rest . $tmp[$i]; - $length = strlen($chunk); - if ($length % 4) { - $length = floor($length / 4) * 4; - $rest = substr($chunk, $length); - $chunk = substr($chunk, 0, $length); - } - - $text .= base64_decode($chunk); - } - } - else { //if ($encoding == 'Q' || $encoding == 'q') { - // quoted printable can be combined and processed at once - for ($i=0; $i<$count; $i++) - $text .= $tmp[$i]; - - $text = str_replace('_', ' ', $text); - $text = quoted_printable_decode($text); - } - - $out .= Charset::convert($text, $charset); - $tmp = array(); - } - - // add the last part of the input string - if ($start != strlen($input)) { - $out .= Charset::convert(substr($input, $start), $default_charset); - } - - // return the results - return $out; - } - - // no encoding information, use fallback - return Charset::convert($input, $default_charset); - } - - /** - * Decode a mime part - * - * @param string $input Input string - * @param string $encoding Part encoding - * - * @return string Decoded string - */ - public static function decode($input, $encoding = '7bit') - { - switch (strtolower($encoding)) { - case 'quoted-printable': - return quoted_printable_decode($input); - case 'base64': - return base64_decode($input); - case 'x-uuencode': - case 'x-uue': - case 'uue': - case 'uuencode': - return convert_uudecode($input); - case '7bit': - default: - return $input; - } - } - - /** - * Split RFC822 header string into an associative array - */ - public static function parse_headers($headers) - { - $a_headers = array(); - $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers); - $lines = explode("\n", $headers); - $count = count($lines); - - for ($i=0; $i<$count; $i++) { - if ($p = strpos($lines[$i], ': ')) { - $field = strtolower(substr($lines[$i], 0, $p)); - $value = trim(substr($lines[$i], $p+1)); - if (!empty($value)) { - $a_headers[$field] = $value; - } - } - } - - return $a_headers; - } - - /** - * E-mail address list parser - */ - private static function parse_address_list($str, $decode = true, $fallback = null) - { - // remove any newlines and carriage returns before - $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str); - - // extract list items, remove comments - $str = self::explode_header_string(',;', $str, true); - $result = array(); - - // simplified regexp, supporting quoted local part - $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+'; - - foreach ($str as $key => $val) { - $name = ''; - $address = ''; - $val = trim($val); - - if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) { - $address = $m[2]; - $name = trim($m[1]); - } - else if (preg_match('/^('.$email_rx.')$/', $val, $m)) { - $address = $m[1]; - $name = ''; - } - // special case (#1489092) - else if (preg_match('/(\s*)$/', $val, $m)) { - $address = 'MAILER-DAEMON'; - $name = substr($val, 0, -strlen($m[1])); - } - else if (preg_match('/('.$email_rx.')/', $val, $m)) { - $name = $m[1]; - } - else { - $name = $val; - } - - // dequote and/or decode name - if ($name) { - if ($name[0] == '"' && $name[strlen($name)-1] == '"') { - $name = substr($name, 1, -1); - $name = stripslashes($name); - } - if ($decode) { - $name = self::decode_header($name, $fallback); - // some clients encode addressee name with quotes around it - if ($name[0] == '"' && $name[strlen($name)-1] == '"') { - $name = substr($name, 1, -1); - } - } - } - - if (!$address && $name) { - $address = $name; - $name = ''; - } - - if ($address) { - $address = self::fix_email($address); - $result[$key] = array('name' => $name, 'address' => $address); - } - } - - return $result; - } - - /** - * Explodes header (e.g. address-list) string into array of strings - * using specified separator characters with proper handling - * of quoted-strings and comments (RFC2822) - * - * @param string $separator String containing separator characters - * @param string $str Header string - * @param bool $remove_comments Enable to remove comments - * - * @return array Header items - */ - public static function explode_header_string($separator, $str, $remove_comments = false) - { - $length = strlen($str); - $result = array(); - $quoted = false; - $comment = 0; - $out = ''; - - for ($i=0; $i<$length; $i++) { - // we're inside a quoted string - if ($quoted) { - if ($str[$i] == '"') { - $quoted = false; - } - else if ($str[$i] == "\\") { - if ($comment <= 0) { - $out .= "\\"; - } - $i++; - } - } - // we are inside a comment string - else if ($comment > 0) { - if ($str[$i] == ')') { - $comment--; - } - else if ($str[$i] == '(') { - $comment++; - } - else if ($str[$i] == "\\") { - $i++; - } - continue; - } - // separator, add to result array - else if (strpos($separator, $str[$i]) !== false) { - if ($out) { - $result[] = $out; - } - $out = ''; - continue; - } - // start of quoted string - else if ($str[$i] == '"') { - $quoted = true; - } - // start of comment - else if ($remove_comments && $str[$i] == '(') { - $comment++; - } - - if ($comment <= 0) { - $out .= $str[$i]; - } - } - - if ($out && $comment <= 0) { - $result[] = $out; - } - - return $result; - } - - /** - * Interpret a format=flowed message body according to RFC 2646 - * - * @param string $text Raw body formatted as flowed text - * @param string $mark Mark each flowed line with specified character - * @param boolean $delsp Remove the trailing space of each flowed line - * - * @return string Interpreted text with unwrapped lines and stuffed space removed - */ - public static function unfold_flowed($text, $mark = null, $delsp = false) - { - $text = preg_split('/\r?\n/', $text); - $last = -1; - $q_level = 0; - $marks = array(); - - foreach ($text as $idx => $line) { - if ($q = strspn($line, '>')) { - // remove quote chars - $line = substr($line, $q); - // remove (optional) space-staffing - if ($line[0] === ' ') $line = substr($line, 1); - - // The same paragraph (We join current line with the previous one) when: - // - the same level of quoting - // - previous line was flowed - // - previous line contains more than only one single space (and quote char(s)) - if ($q == $q_level - && isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' ' - && !preg_match('/^>+ {0,1}$/', $text[$last]) - ) { - if ($delsp) { - $text[$last] = substr($text[$last], 0, -1); - } - $text[$last] .= $line; - unset($text[$idx]); - - if ($mark) { - $marks[$last] = true; - } - } - else { - $last = $idx; - } - } - else { - if ($line == '-- ') { - $last = $idx; - } - else { - // remove space-stuffing - if ($line[0] === ' ') $line = substr($line, 1); - - if (isset($text[$last]) && $line && !$q_level - && $text[$last] != '-- ' - && $text[$last][strlen($text[$last])-1] == ' ' - ) { - if ($delsp) { - $text[$last] = substr($text[$last], 0, -1); - } - $text[$last] .= $line; - unset($text[$idx]); - - if ($mark) { - $marks[$last] = true; - } - } - else { - $text[$idx] = $line; - $last = $idx; - } - } - } - $q_level = $q; - } - - if (!empty($marks)) { - foreach (array_keys($marks) as $mk) { - $text[$mk] = $mark . $text[$mk]; - } - } - - return implode("\r\n", $text); - } - - /** - * Wrap the given text to comply with RFC 2646 - * - * @param string $text Text to wrap - * @param int $length Length - * @param string $charset Character encoding of $text - * - * @return string Wrapped text - */ - public static function format_flowed($text, $length = 72, $charset=null) - { - $text = preg_split('/\r?\n/', $text); - - foreach ($text as $idx => $line) { - if ($line != '-- ') { - if ($level = strspn($line, '>')) { - // remove quote chars - $line = substr($line, $level); - // remove (optional) space-staffing and spaces before the line end - $line = rtrim($line, ' '); - if ($line[0] === ' ') $line = substr($line, 1); - - $prefix = str_repeat('>', $level) . ' '; - $line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset); - } - else if ($line) { - $line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset); - // space-stuffing - $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line); - } - - $text[$idx] = $line; - } - } - - return implode("\r\n", $text); - } - - /** - * Improved wordwrap function with multibyte support. - * The code is based on Zend_Text_MultiByte::wordWrap(). - * - * @param string $string Text to wrap - * @param int $width Line width - * @param string $break Line separator - * @param bool $cut Enable to cut word - * @param string $charset Charset of $string - * @param bool $wrap_quoted When enabled quoted lines will not be wrapped - * - * @return string Text - */ - public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true) - { - // Note: Never try to use iconv instead of mbstring functions here - // Iconv's substr/strlen are 100x slower (#1489113) - - if ($charset && $charset != IMAP2_CHARSET) { - mb_internal_encoding($charset); - } - - // Convert \r\n to \n, this is our line-separator - $string = str_replace("\r\n", "\n", $string); - $separator = "\n"; // must be 1 character length - $result = array(); - - while (($stringLength = mb_strlen($string)) > 0) { - $breakPos = mb_strpos($string, $separator, 0); - - // quoted line (do not wrap) - if ($wrap_quoted && $string[0] == '>') { - if ($breakPos === $stringLength - 1 || $breakPos === false) { - $subString = $string; - $cutLength = null; - } - else { - $subString = mb_substr($string, 0, $breakPos); - $cutLength = $breakPos + 1; - } - } - // next line found and current line is shorter than the limit - else if ($breakPos !== false && $breakPos < $width) { - if ($breakPos === $stringLength - 1) { - $subString = $string; - $cutLength = null; - } - else { - $subString = mb_substr($string, 0, $breakPos); - $cutLength = $breakPos + 1; - } - } - else { - $subString = mb_substr($string, 0, $width); - - // last line - if ($breakPos === false && $subString === $string) { - $cutLength = null; - } - else { - $nextChar = mb_substr($string, $width, 1); - - if ($nextChar === ' ' || $nextChar === $separator) { - $afterNextChar = mb_substr($string, $width + 1, 1); - - // Note: mb_substr() does never return False - if ($afterNextChar === false || $afterNextChar === '') { - $subString .= $nextChar; - } - - $cutLength = mb_strlen($subString) + 1; - } - else { - $spacePos = mb_strrpos($subString, ' ', 0); - - if ($spacePos !== false) { - $subString = mb_substr($subString, 0, $spacePos); - $cutLength = $spacePos + 1; - } - else if ($cut === false) { - $spacePos = mb_strpos($string, ' ', 0); - - if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) { - $subString = mb_substr($string, 0, $spacePos); - $cutLength = $spacePos + 1; - } - else if ($breakPos === false) { - $subString = $string; - $cutLength = null; - } - else { - $subString = mb_substr($string, 0, $breakPos); - $cutLength = $breakPos + 1; - } - } - else { - $cutLength = $width; - } - } - } - } - - $result[] = $subString; - - if ($cutLength !== null) { - $string = mb_substr($string, $cutLength, ($stringLength - $cutLength)); - } - else { - break; - } - } - - if ($charset && $charset != IMAP2_CHARSET) { - mb_internal_encoding(IMAP2_CHARSET); - } - - return implode($break, $result); - } - - /** - * A method to guess the mime_type of an attachment. - * - * @param string $path Path to the file or file contents - * @param string $name File name (with suffix) - * @param string $failover Mime type supplied for failover - * @param boolean $is_stream Set to True if $path contains file contents - * @param boolean $skip_suffix Set to True if the config/mimetypes.php mappig should be ignored - * - * @return string - * @author Till Klampaeckel - * @see http://de2.php.net/manual/en/ref.fileinfo.php - * @see http://de2.php.net/mime_content_type - */ - public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false) - { - static $mime_ext = array(); - - $mime_type = null; - $config = rcube::get_instance()->config; - - if (!$skip_suffix && empty($mime_ext)) { - foreach ($config->resolve_paths('mimetypes.php') as $fpath) { - $mime_ext = array_merge($mime_ext, (array) @include($fpath)); - } - } - - // use file name suffix with hard-coded mime-type map - if (!$skip_suffix && is_array($mime_ext) && $name) { - if ($suffix = substr($name, strrpos($name, '.')+1)) { - $mime_type = $mime_ext[strtolower($suffix)]; - } - } - - // try fileinfo extension if available - if (!$mime_type && function_exists('finfo_open')) { - $mime_magic = $config->get('mime_magic'); - // null as a 2nd argument should be the same as no argument - // this however is not true on all systems/versions - if ($mime_magic) { - $finfo = finfo_open(FILEINFO_MIME, $mime_magic); - } - else { - $finfo = finfo_open(FILEINFO_MIME); - } - - if ($finfo) { - $func = $is_stream ? 'finfo_buffer' : 'finfo_file'; - $mime_type = $func($finfo, $path, FILEINFO_MIME_TYPE); - finfo_close($finfo); - } - } - - // try PHP's mime_content_type - if (!$mime_type && !$is_stream && function_exists('mime_content_type')) { - $mime_type = @mime_content_type($path); - } - - // fall back to user-submitted string - if (!$mime_type) { - $mime_type = $failover; - } - - return $mime_type; - } - - /** - * Get mimetype => file extension mapping - * - * @param string Mime-Type to get extensions for - * - * @return array List of extensions matching the given mimetype or a hash array - * with ext -> mimetype mappings if $mimetype is not given - */ - public static function get_mime_extensions($mimetype = null) - { - static $mime_types, $mime_extensions; - - // return cached data - if (is_array($mime_types)) { - return $mimetype ? $mime_types[$mimetype] : $mime_extensions; - } - - // load mapping file - $file_paths = array(); - - if ($mime_types = rcube::get_instance()->config->get('mime_types')) { - $file_paths[] = $mime_types; - } - - // try common locations - if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { - $file_paths[] = 'C:/xampp/apache/conf/mime.types.'; - } - else { - $file_paths[] = '/etc/mime.types'; - $file_paths[] = '/etc/httpd/mime.types'; - $file_paths[] = '/etc/httpd2/mime.types'; - $file_paths[] = '/etc/apache/mime.types'; - $file_paths[] = '/etc/apache2/mime.types'; - $file_paths[] = '/etc/nginx/mime.types'; - $file_paths[] = '/usr/local/etc/httpd/conf/mime.types'; - $file_paths[] = '/usr/local/etc/apache/conf/mime.types'; - $file_paths[] = '/usr/local/etc/apache24/mime.types'; - } - - foreach ($file_paths as $fp) { - if (@is_readable($fp)) { - $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - break; - } - } - - $mime_types = $mime_extensions = array(); - $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i"; - foreach ((array)$lines as $line) { - // skip comments or mime types w/o any extensions - if ($line[0] == '#' || !preg_match($regex, $line, $matches)) - continue; - - $mime = $matches[1]; - foreach (explode(' ', $matches[2]) as $ext) { - $ext = trim($ext); - $mime_types[$mime][] = $ext; - $mime_extensions[$ext] = $mime; - } - } - - // fallback to some well-known types most important for daily emails - if (empty($mime_types)) { - foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) { - $mime_extensions = array_merge($mime_extensions, (array) @include($fpath)); - } - - foreach ($mime_extensions as $ext => $mime) { - $mime_types[$mime][] = $ext; - } - } - - // Add some known aliases that aren't included by some mime.types (#1488891) - // the order is important here so standard extensions have higher prio - $aliases = array( - 'image/gif' => array('gif'), - 'image/png' => array('png'), - 'image/x-png' => array('png'), - 'image/jpeg' => array('jpg', 'jpeg', 'jpe'), - 'image/jpg' => array('jpg', 'jpeg', 'jpe'), - 'image/pjpeg' => array('jpg', 'jpeg', 'jpe'), - 'image/tiff' => array('tif'), - 'image/bmp' => array('bmp'), - 'image/x-ms-bmp' => array('bmp'), - 'message/rfc822' => array('eml'), - 'text/x-mail' => array('eml'), - ); - - foreach ($aliases as $mime => $exts) { - $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts)); - - foreach ($exts as $ext) { - if (!isset($mime_extensions[$ext])) { - $mime_extensions[$ext] = $mime; - } - } - } - - return $mimetype ? $mime_types[$mimetype] : $mime_extensions; - } - - /** - * Detect image type of the given binary data by checking magic numbers. - * - * @param string $data Binary file content - * - * @return string Detected mime-type or jpeg as fallback - */ - public static function image_content_type($data) - { - $type = 'jpeg'; - if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png'; - else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif'; - else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico'; - // else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg'; - - return 'image/' . $type; - } - - /** - * Try to fix invalid email addresses - */ - public static function fix_email($email) - { - $parts = Utils::explode_quoted_string('@', $email); - foreach ($parts as $idx => $part) { - // remove redundant quoting (#1490040) - if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) { - $parts[$idx] = $m[1]; - } - } - - return implode('@', $parts); - } -} diff --git a/src/Roundcube/ResultIndex.php b/src/Roundcube/ResultIndex.php deleted file mode 100644 index ce0a371..0000000 --- a/src/Roundcube/ResultIndex.php +++ /dev/null @@ -1,426 +0,0 @@ - | -| Author: Aleksander Machniak | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -use Javanile\Imap2\Offset; -use Javanile\Imap2\rcube_imap_generic; - -/** - * Class for accessing IMAP's SORT/SEARCH/ESEARCH result - * - * @package Framework - * @subpackage Storage - */ -class ResultIndex -{ - public $incomplete = false; - - protected $raw_data; - protected $mailbox; - protected $meta = array(); - protected $params = array(); - protected $order = 'ASC'; - - const SEPARATOR_ELEMENT = ' '; - - - /** - * Object constructor. - */ - public function __construct($mailbox = null, $data = null, $order = null) - { - $this->mailbox = $mailbox; - $this->order = $order == 'DESC' ? 'DESC' : 'ASC'; - $this->init($data); - } - - /** - * Initializes object with SORT command response - * - * @param string $data IMAP response string - */ - public function init($data = null) - { - $this->meta = array(); - - $data = explode('*', (string)$data); - - // ...skip unilateral untagged server responses - for ($i=0, $len=count($data); $i<$len; $i++) { - $data_item = &$data[$i]; - if (preg_match('/^ SORT/i', $data_item)) { - // valid response, initialize raw_data for is_error() - $this->raw_data = ''; - $data_item = substr($data_item, 5); - break; - } - else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) { - // valid response, initialize raw_data for is_error() - $this->raw_data = ''; - $data_item = substr($data_item, strlen($m[0])); - - if (strtoupper($m[1]) == 'ESEARCH') { - $data_item = trim($data_item); - // remove MODSEQ response - if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) { - $data_item = substr($data_item, 0, -strlen($m[0])); - $this->params['MODSEQ'] = $m[1]; - } - // remove TAG response part - if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) { - $data_item = substr($data_item, strlen($m[0])); - } - // remove UID - $data_item = preg_replace('/^UID\s*/i', '', $data_item); - - // ESEARCH parameters - while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) { - $param = strtoupper($m[1]); - $value = $m[2]; - - $this->params[$param] = $value; - $data_item = substr($data_item, strlen($m[0])); - - if (in_array($param, array('COUNT', 'MIN', 'MAX'))) { - $this->meta[strtolower($param)] = (int) $value; - } - } - -// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ? -// @TODO: work with compressed result?! - if (isset($this->params['ALL'])) { - $data_item = implode(self::SEPARATOR_ELEMENT, - ImapClient::uncompressMessageSet($this->params['ALL'])); - } - } - - break; - } - - unset($data[$i]); - } - - $data = array_filter($data); - - if (empty($data)) { - return; - } - - $data = array_shift($data); - $data = trim($data); - $data = preg_replace('/[\r\n]/', '', $data); - $data = preg_replace('/\s+/', ' ', $data); - - $this->raw_data = $data; - } - - /** - * Checks the result from IMAP command - * - * @return bool True if the result is an error, False otherwise - */ - public function is_error() - { - return $this->raw_data === null; - } - - /** - * Checks if the result is empty - * - * @return bool True if the result is empty, False otherwise - */ - public function is_empty() - { - return empty($this->raw_data); - } - - /** - * Returns number of elements in the result - * - * @return int Number of elements - */ - public function count() - { - if (isset($this->meta['count']) && $this->meta['count'] !== null) - return $this->meta['count']; - - if (empty($this->raw_data)) { - $this->meta['count'] = 0; - $this->meta['length'] = 0; - } - else { - $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); - } - - return $this->meta['count']; - } - - /** - * Returns number of elements in the result. - * Alias for count() for compatibility with rcube_result_thread - * - * @return int Number of elements - */ - public function count_messages() - { - return $this->count(); - } - - /** - * Returns maximal message identifier in the result - * - * @return int Maximal message identifier - */ - public function max() - { - if (!isset($this->meta['max'])) { - $this->meta['max'] = (int) @max($this->get()); - } - - return $this->meta['max']; - } - - /** - * Returns minimal message identifier in the result - * - * @return int Minimal message identifier - */ - public function min() - { - if (!isset($this->meta['min'])) { - $this->meta['min'] = (int) @min($this->get()); - } - - return $this->meta['min']; - } - - /** - * Slices data set. - * - * @param $offset Offset (as for PHP's array_slice()) - * @param $length Number of elements (as for PHP's array_slice()) - */ - public function slice($offset, $length) - { - $data = $this->get(); - $data = array_slice($data, $offset, $length); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - /** - * Filters data set. Removes elements not listed in $ids list. - * - * @param array $ids List of IDs to remove. - */ - public function filter($ids = array()) - { - $data = $this->get(); - $data = array_intersect($data, $ids); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - /** - * Reverts order of elements in the result - */ - public function revert() - { - $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; - - if (empty($this->raw_data)) { - return; - } - - $data = $this->get(); - $data = array_reverse($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - - $this->meta['pos'] = array(); - } - - /** - * Check if the given message ID exists in the object - * - * @param int $msgid Message ID - * @param bool $get_index When enabled element's index will be returned. - * Elements are indexed starting with 0 - * - * @return mixed False if message ID doesn't exist, True if exists or - * index of the element if $get_index=true - */ - public function exists($msgid, $get_index = false) - { - if (empty($this->raw_data)) { - return false; - } - - $msgid = (int) $msgid; - $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/'))); - $end = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/'))); - - if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, - $get_index ? PREG_OFFSET_CAPTURE : null) - ) { - if ($get_index) { - $idx = 0; - if ($m[0][1]) { - $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]); - } - // cache position of this element, so we can use it in get_element() - $this->meta['pos'][$idx] = (int)$m[0][1]; - - return $idx; - } - - return true; - } - - return false; - } - - /** - * Return all messages in the result. - * - * @return array List of message IDs - */ - public function get() - { - if (empty($this->raw_data)) { - return array(); - } - - return explode(self::SEPARATOR_ELEMENT, $this->raw_data); - } - - /** - * Return all messages in the result. - * - * @return array List of message IDs - */ - public function get_compressed() - { - if (empty($this->raw_data)) { - return ''; - } - - return rcube_imap_generic::compressMessageSet($this->get()); - } - - /** - * Return result element at specified index - * - * @param int|string $index Element's index or "FIRST" or "LAST" - * - * @return int Element value - */ - public function get_element($index) - { - $count = $this->count(); - - if (!$count) { - return null; - } - - // first element - if ($index === 0 || $index === '0' || $index === 'FIRST') { - $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT); - if ($pos === false) - $result = (int) $this->raw_data; - else - $result = (int) substr($this->raw_data, 0, $pos); - - return $result; - } - - // last element - if ($index === 'LAST' || $index == $count-1) { - $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT); - if ($pos === false) - $result = (int) $this->raw_data; - else - $result = (int) substr($this->raw_data, $pos); - - return $result; - } - - // do we know the position of the element or the neighbour of it? - if (!empty($this->meta['pos'])) { - if (isset($this->meta['pos'][$index])) - $pos = $this->meta['pos'][$index]; - else if (isset($this->meta['pos'][$index-1])) - $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, - $this->meta['pos'][$index-1] + 1); - else if (isset($this->meta['pos'][$index+1])) - $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT, - $this->meta['pos'][$index+1] - $this->length() - 1); - - if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) { - return (int) $m[1]; - } - } - - // Finally use less effective method - $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); - - return $data[$index]; - } - - /** - * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ - * or internal data e.g. MAILBOX, ORDER - * - * @param string $param Parameter name - * - * @return array|string Response parameters or parameter value - */ - public function get_parameters($param=null) - { - $params = $this->params; - $params['MAILBOX'] = $this->mailbox; - $params['ORDER'] = $this->order; - - if ($param !== null) { - return $params[$param]; - } - - return $params; - } - - /** - * Returns length of internal data representation - * - * @return int Data length - */ - protected function length() - { - if (!isset($this->meta['length'])) { - $this->meta['length'] = strlen($this->raw_data); - } - - return $this->meta['length']; - } -} diff --git a/src/Roundcube/ResultThread.php b/src/Roundcube/ResultThread.php deleted file mode 100644 index e8a16f6..0000000 --- a/src/Roundcube/ResultThread.php +++ /dev/null @@ -1,656 +0,0 @@ - | -| Author: Aleksander Machniak | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -use Javanile\Imap2\Offset; -use Javanile\Imap2\rcube_imap_generic; -use Javanile\Imap2\rcube_result_index; - -/** - * Class for accessing IMAP's THREAD result - * - * @package Framework - * @subpackage Storage - */ -class ResultThread -{ - public $incomplete = false; - - protected $raw_data; - protected $mailbox; - protected $meta = array(); - protected $order = 'ASC'; - - const SEPARATOR_ELEMENT = ' '; - const SEPARATOR_ITEM = '~'; - const SEPARATOR_LEVEL = ':'; - - - /** - * Object constructor. - */ - public function __construct($mailbox = null, $data = null) - { - $this->mailbox = $mailbox; - $this->init($data); - } - - /** - * Initializes object with IMAP command response - * - * @param string $data IMAP response string - */ - public function init($data = null) - { - $this->meta = array(); - - $data = explode('*', (string)$data); - - // ...skip unilateral untagged server responses - for ($i=0, $len=count($data); $i<$len; $i++) { - if (preg_match('/^ THREAD/i', $data[$i])) { - // valid response, initialize raw_data for is_error() - $this->raw_data = ''; - $data[$i] = substr($data[$i], 7); - break; - } - - unset($data[$i]); - } - - if (empty($data)) { - return; - } - - $data = array_shift($data); - $data = trim($data); - $data = preg_replace('/[\r\n]/', '', $data); - $data = preg_replace('/\s+/', ' ', $data); - - $this->raw_data = $this->parse_thread($data); - } - - /** - * Checks the result from IMAP command - * - * @return bool True if the result is an error, False otherwise - */ - public function is_error() - { - return $this->raw_data === null; - } - - /** - * Checks if the result is empty - * - * @return bool True if the result is empty, False otherwise - */ - public function is_empty() - { - return empty($this->raw_data); - } - - /** - * Returns number of elements (threads) in the result - * - * @return int Number of elements - */ - public function count() - { - if (isset($this->meta['count']) && $this->meta['count'] !== null) - return $this->meta['count']; - - if (empty($this->raw_data)) { - $this->meta['count'] = 0; - } - else { - $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); - } - - if (!$this->meta['count']) - $this->meta['messages'] = 0; - - return $this->meta['count']; - } - - /** - * Returns number of all messages in the result - * - * @return int Number of elements - */ - public function count_messages() - { - if ($this->meta['messages'] !== null) - return $this->meta['messages']; - - if (empty($this->raw_data)) { - $this->meta['messages'] = 0; - } - else { - $this->meta['messages'] = 1 - + substr_count($this->raw_data, self::SEPARATOR_ELEMENT) - + substr_count($this->raw_data, self::SEPARATOR_ITEM); - } - - if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1) - $this->meta['count'] = $this->meta['messages']; - - return $this->meta['messages']; - } - - /** - * Returns maximum message identifier in the result - * - * @return int Maximum message identifier - */ - public function max() - { - if (!isset($this->meta['max'])) { - $this->meta['max'] = (int) @max($this->get()); - } - return $this->meta['max']; - } - - /** - * Returns minimum message identifier in the result - * - * @return int Minimum message identifier - */ - public function min() - { - if (!isset($this->meta['min'])) { - $this->meta['min'] = (int) @min($this->get()); - } - return $this->meta['min']; - } - - /** - * Slices data set. - * - * @param $offset Offset (as for PHP's array_slice()) - * @param $length Number of elements (as for PHP's array_slice()) - */ - public function slice($offset, $length) - { - $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); - $data = array_slice($data, $offset, $length); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - /** - * Filters data set. Removes threads not listed in $roots list. - * - * @param array $roots List of IDs of thread roots. - */ - public function filter($roots) - { - $datalen = strlen($this->raw_data); - $roots = array_flip($roots); - $result = ''; - $start = 0; - - $this->meta = array(); - $this->meta['count'] = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - // extract root message ID - if ($npos = strpos($elem, self::SEPARATOR_ITEM)) { - $root = (int) substr($elem, 0, $npos); - } - else { - $root = $elem; - } - - if (isset($roots[$root])) { - $this->meta['count']++; - $result .= self::SEPARATOR_ELEMENT . $elem; - } - } - - $this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT); - } - - /** - * Reverts order of elements in the result - */ - public function revert() - { - $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; - - if (empty($this->raw_data)) { - return; - } - - $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); - $data = array_reverse($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - - $this->meta['pos'] = array(); - } - - /** - * Check if the given message ID exists in the object - * - * @param int $msgid Message ID - * @param bool $get_index When enabled element's index will be returned. - * Elements are indexed starting with 0 - * - * @return boolean True on success, False if message ID doesn't exist - */ - public function exists($msgid, $get_index = false) - { - $msgid = (int) $msgid; - $begin = implode('|', array( - '^', - preg_quote(self::SEPARATOR_ELEMENT, '/'), - preg_quote(self::SEPARATOR_LEVEL, '/'), - )); - $end = implode('|', array( - '$', - preg_quote(self::SEPARATOR_ELEMENT, '/'), - preg_quote(self::SEPARATOR_ITEM, '/'), - )); - - if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, - $get_index ? PREG_OFFSET_CAPTURE : null) - ) { - if ($get_index) { - $idx = 0; - if ($m[0][1]) { - $idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1) - + substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1); - } - // cache position of this element, so we can use it in get_element() - $this->meta['pos'][$idx] = (int)$m[0][1]; - - return $idx; - } - return true; - } - - return false; - } - - /** - * Return IDs of all messages in the result. Threaded data will be flattened. - * - * @return array List of message identifiers - */ - public function get() - { - if (empty($this->raw_data)) { - return array(); - } - - $regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/') - . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') - .')/'; - - return preg_split($regexp, $this->raw_data); - } - - /** - * Return all messages in the result. - * - * @return array List of message identifiers - */ - public function get_compressed() - { - if (empty($this->raw_data)) { - return ''; - } - - return rcube_imap_generic::compressMessageSet($this->get()); - } - - /** - * Return result element at specified index (all messages, not roots) - * - * @param int|string $index Element's index or "FIRST" or "LAST" - * - * @return int Element value - */ - public function get_element($index) - { - $count = $this->count(); - - if (!$count) { - return null; - } - - // first element - if ($index === 0 || $index === '0' || $index === 'FIRST') { - preg_match('/^([0-9]+)/', $this->raw_data, $m); - $result = (int) $m[1]; - return $result; - } - - // last element - if ($index === 'LAST' || $index == $count-1) { - preg_match('/([0-9]+)$/', $this->raw_data, $m); - $result = (int) $m[1]; - return $result; - } - - // do we know the position of the element or the neighbour of it? - if (!empty($this->meta['pos'])) { - $element = preg_quote(self::SEPARATOR_ELEMENT, '/'); - $item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?'; - $regexp = '(' . $element . '|' . $item . ')'; - - if (isset($this->meta['pos'][$index])) { - if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index])) - $result = $m[1]; - } - else if (isset($this->meta['pos'][$index-1])) { - // get chunk of data after previous element - $data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50); - $data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position - $data = preg_replace("/^$regexp/", '', $data); // remove separator - if (preg_match('/^([0-9]+)/', $data, $m)) - $result = $m[1]; - } - else if (isset($this->meta['pos'][$index+1])) { - // get chunk of data before next element - $pos = max(0, $this->meta['pos'][$index+1] - 50); - $len = min(50, $this->meta['pos'][$index+1]); - $data = substr($this->raw_data, $pos, $len); - $data = preg_replace("/$regexp\$/", '', $data); // remove separator - - if (preg_match('/([0-9]+)$/', $data, $m)) - $result = $m[1]; - } - - if (isset($result)) { - return (int) $result; - } - } - - // Finally use less effective method - $data = $this->get(); - - return $data[$index]; - } - - /** - * Returns response parameters e.g. MAILBOX, ORDER - * - * @param string $param Parameter name - * - * @return array|string Response parameters or parameter value - */ - public function get_parameters($param=null) - { - $params = array(); - $params['MAILBOX'] = $this->mailbox; - $params['ORDER'] = $this->order; - - if ($param !== null) { - return $params[$param]; - } - - return $params; - } - - /** - * THREAD=REFS sorting implementation (based on provided index) - * - * @param rcube_result_index $index Sorted message identifiers - */ - public function sort($index) - { - $this->sort_order = $index->get_parameters('ORDER'); - - if (empty($this->raw_data)) { - return; - } - - // when sorting search result it's good to make the index smaller - if ($index->count() != $this->count_messages()) { - $index->filter($this->get()); - } - - $result = array_fill_keys($index->get(), null); - $datalen = strlen($this->raw_data); - $start = 0; - - // Here we're parsing raw_data twice, we want only one big array - // in memory at a time - - // Assign roots - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - $items = explode(self::SEPARATOR_ITEM, $elem); - $root = (int) array_shift($items); - - if ($root) { - $result[$root] = $root; - foreach ($items as $item) { - list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item); - $result[$id] = $root; - } - } - } - - // get only unique roots - $result = array_filter($result); // make sure there are no nulls - $result = array_unique($result); - - // Re-sort raw data - $result = array_fill_keys($result, null); - $start = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - $npos = strpos($elem, self::SEPARATOR_ITEM); - $root = (int) ($npos ? substr($elem, 0, $npos) : $elem); - - $result[$root] = $elem; - } - - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $result); - } - - /** - * Returns data as tree - * - * @return array Data tree - */ - public function get_tree() - { - $datalen = strlen($this->raw_data); - $result = array(); - $start = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $items = explode(self::SEPARATOR_ITEM, $elem); - $result[array_shift($items)] = $this->build_thread($items); - $start = $pos + 1; - } - - return $result; - } - - /** - * Returns thread depth and children data - * - * @return array Thread data - */ - public function get_thread_data() - { - $data = $this->get_tree(); - $depth = array(); - $children = array(); - - $this->build_thread_data($data, $depth, $children); - - return array($depth, $children); - } - - /** - * Creates 'depth' and 'children' arrays from stored thread 'tree' data. - */ - protected function build_thread_data($data, &$depth, &$children, $level = 0) - { - foreach ((array)$data as $key => $val) { - $empty = empty($val) || !is_array($val); - $children[$key] = !$empty; - $depth[$key] = $level; - if (!$empty) { - $this->build_thread_data($val, $depth, $children, $level + 1); - } - } - } - - /** - * Converts part of the raw thread into an array - */ - protected function build_thread($items, $level = 1, &$pos = 0) - { - $result = array(); - - for ($len=count($items); $pos < $len; $pos++) { - list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]); - if ($level == $lv) { - $pos++; - $result[$id] = $this->build_thread($items, $level+1, $pos); - } - else { - $pos--; - break; - } - } - - return $result; - } - - /** - * IMAP THREAD response parser - */ - protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0) - { - // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about - // 7 times instead :-) See comments on http://uk2.php.net/references and this article: - // http://derickrethans.nl/files/phparch-php-variables-article.pdf - $node = ''; - if (!$end) { - $end = strlen($str); - } - - // Let's try to store data in max. compacted stracture as a string, - // arrays handling is much more expensive - // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96)) - // -- 2 - // -- 3 - // \-- 6 - // |-- 4 - // | \-- 23 - // | - // \-- 44 - // \-- 7 - // \-- 96 - // - // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96 - - if ($str[$begin] != '(') { - // find next bracket - $stop = $begin + strcspn($str, '()', $begin, $end - $begin); - $messages = explode(' ', trim(substr($str, $begin, $stop - $begin))); - - if (empty($messages)) { - return $node; - } - - foreach ($messages as $msg) { - if ($msg) { - $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg; - $this->meta['messages']++; - $depth++; - } - } - - if ($stop < $end) { - $node .= $this->parse_thread($str, $stop, $end, $depth); - } - } - else { - $off = $begin; - while ($off < $end) { - $start = $off; - $off++; - $n = 1; - while ($n > 0) { - $p = strpos($str, ')', $off); - if ($p === false) { - // error, wrong structure, mismatched brackets in IMAP THREAD response - // @TODO: write error to the log or maybe set $this->raw_data = null; - return $node; - } - $p1 = strpos($str, '(', $off); - if ($p1 !== false && $p1 < $p) { - $off = $p1 + 1; - $n++; - } - else { - $off = $p + 1; - $n--; - } - } - - $thread = $this->parse_thread($str, $start + 1, $off - 1, $depth); - if ($thread) { - if (!$depth) { - if ($node) { - $node .= self::SEPARATOR_ELEMENT; - } - } - $node .= $thread; - } - } - } - - return $node; - } -} diff --git a/src/Roundcube/Utils.php b/src/Roundcube/Utils.php deleted file mode 100644 index b5a83cc..0000000 --- a/src/Roundcube/Utils.php +++ /dev/null @@ -1,1467 +0,0 @@ - | -| Author: Aleksander Machniak | -+-----------------------------------------------------------------------+ - */ - -namespace Javanile\Imap2\Roundcube; - -use Javanile\Imap2\DateTime; -use Javanile\Imap2\DateTimeZone; -use Javanile\Imap2\Exception; -use Javanile\Imap2\rcube; -use Javanile\Imap2\rcube_charset; -use Javanile\Imap2\rcube_string_replacer; - -/** - * Utility class providing common functions - * - * @package Framework - * @subpackage Utils - */ -class Utils -{ - // define constants for input reading - const INPUT_GET = 1; - const INPUT_POST = 2; - const INPUT_COOKIE = 4; - const INPUT_GP = 3; // GET + POST - const INPUT_GPC = 7; // GET + POST + COOKIE - - - /** - * Helper method to set a cookie with the current path and host settings - * - * @param string $name Cookie name - * @param string $value Cookie value - * @param int $exp Expiration time - * @param bool $http_only HTTP Only - */ - public static function setcookie($name, $value, $exp = 0, $http_only = true) - { - if (headers_sent()) { - return; - } - - $cookie = session_get_cookie_params(); - $secure = $cookie['secure'] || self::https_check(); - - setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'], $secure, $http_only); - } - - /** - * E-mail address validation. - * - * @param string $email Email address - * @param bool $dns_check True to check dns - * - * @return bool True on success, False if address is invalid - */ - public static function check_email($email, $dns_check=true) - { - // Check for invalid (control) characters - if (preg_match('/\p{Cc}/u', $email)) { - return false; - } - - // Check for length limit specified by RFC 5321 (#1486453) - if (strlen($email) > 254) { - return false; - } - - $pos = strrpos($email, '@'); - if (!$pos) { - return false; - } - - $domain_part = substr($email, $pos + 1); - $local_part = substr($email, 0, $pos); - - // quoted-string, make sure all backslashes and quotes are - // escaped - if (substr($local_part,0,1) == '"') { - $local_quoted = preg_replace('/\\\\(\\\\|\")/','', substr($local_part, 1, -1)); - if (preg_match('/\\\\|"/', $local_quoted)) { - return false; - } - } - // dot-atom portion, make sure there's no prohibited characters - else if (preg_match('/(^\.|\.\.|\.$)/', $local_part) - || preg_match('/[\\ ",:;<>@]/', $local_part) - ) { - return false; - } - - // Validate domain part - if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) { - return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address - } - else { - // If not an IP address - $domain_array = explode('.', $domain_part); - // Not enough parts to be a valid domain - if (count($domain_array) < 2) { - return false; - } - - foreach ($domain_array as $part) { - if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) { - return false; - } - } - - // last domain part - $last_part = array_pop($domain_array); - if (strpos($last_part, 'xn--') !== 0 && preg_match('/[^a-zA-Z]/', $last_part)) { - return false; - } - - $rcube = rcube::get_instance(); - - if (!$dns_check || !function_exists('checkdnsrr') || !$rcube->config->get('email_dns_check')) { - return true; - } - - // Check DNS record(s) - // Note: We can't use ANY (#6581) - foreach (array('A', 'MX', 'CNAME', 'AAAA') as $type) { - if (checkdnsrr($domain_part, $type)) { - return true; - } - } - } - - return false; - } - - /** - * Validates IPv4 or IPv6 address - * - * @param string $ip IP address in v4 or v6 format - * - * @return bool True if the address is valid - */ - public static function check_ip($ip) - { - return filter_var($ip, FILTER_VALIDATE_IP) !== false; - } - - /** - * Replacing specials characters to a specific encoding type - * - * @param string $str Input string - * @param string $enctype Encoding type: text|html|xml|js|url - * @param string $mode Replace mode for tags: show|remove|strict - * @param bool $newlines Convert newlines - * - * @return string The quoted string - */ - public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true) - { - static $html_encode_arr = false; - static $js_rep_table = false; - static $xml_rep_table = false; - - if (!is_string($str)) { - $str = strval($str); - } - - // encode for HTML output - if ($enctype == 'html') { - if (!$html_encode_arr) { - $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS); - unset($html_encode_arr['?']); - } - - $encode_arr = $html_encode_arr; - - if ($mode == 'remove') { - $str = strip_tags($str); - } - else if ($mode != 'strict') { - // don't replace quotes and html tags - $ltpos = strpos($str, '<'); - if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) { - unset($encode_arr['"']); - unset($encode_arr['<']); - unset($encode_arr['>']); - unset($encode_arr['&']); - } - } - - $out = strtr($str, $encode_arr); - - return $newlines ? nl2br($out) : $out; - } - - // if the replace tables for XML and JS are not yet defined - if ($js_rep_table === false) { - $js_rep_table = $xml_rep_table = array(); - $xml_rep_table['&'] = '&'; - - // can be increased to support more charsets - for ($c=160; $c<256; $c++) { - $xml_rep_table[chr($c)] = "&#$c;"; - } - - $xml_rep_table['"'] = '"'; - $js_rep_table['"'] = '\\"'; - $js_rep_table["'"] = "\\'"; - $js_rep_table["\\"] = "\\\\"; - // Unicode line and paragraph separators (#1486310) - $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A8'))] = '
'; - $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A9'))] = '
'; - } - - // encode for javascript use - if ($enctype == 'js') { - return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table)); - } - - // encode for plaintext - if ($enctype == 'text') { - return str_replace("\r\n", "\n", $mode == 'remove' ? strip_tags($str) : $str); - } - - if ($enctype == 'url') { - return rawurlencode($str); - } - - // encode for XML - if ($enctype == 'xml') { - return strtr($str, $xml_rep_table); - } - - // no encoding given -> return original string - return $str; - } - - /** - * Read input value and convert it for internal use - * Performs stripslashes() and charset conversion if necessary - * - * @param string $fname Field name to read - * @param int $source Source to get value from (see self::INPUT_*) - * @param bool $allow_html Allow HTML tags in field value - * @param string $charset Charset to convert into - * - * @return string Field value or NULL if not available - */ - public static function get_input_value($fname, $source, $allow_html = false, $charset = null) - { - $value = null; - - if (($source & self::INPUT_GET) && isset($_GET[$fname])) { - $value = $_GET[$fname]; - } - - if (($source & self::INPUT_POST) && isset($_POST[$fname])) { - $value = $_POST[$fname]; - } - - if (($source & self::INPUT_COOKIE) && isset($_COOKIE[$fname])) { - $value = $_COOKIE[$fname]; - } - - return self::parse_input_value($value, $allow_html, $charset); - } - - /** - * Parse/validate input value. See self::get_input_value() - * Performs stripslashes() and charset conversion if necessary - * - * @param string $value Input value - * @param bool $allow_html Allow HTML tags in field value - * @param string $charset Charset to convert into - * - * @return string Parsed value - */ - public static function parse_input_value($value, $allow_html = false, $charset = null) - { - global $OUTPUT; - - if (empty($value)) { - return $value; - } - - if (is_array($value)) { - foreach ($value as $idx => $val) { - $value[$idx] = self::parse_input_value($val, $allow_html, $charset); - } - return $value; - } - - // remove HTML tags if not allowed - if (!$allow_html) { - $value = strip_tags($value); - } - - $output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null; - - // remove invalid characters (#1488124) - if ($output_charset == 'UTF-8') { - $value = rcube_charset::clean($value); - } - - // convert to internal charset - if ($charset && $output_charset) { - $value = rcube_charset::convert($value, $output_charset, $charset); - } - - return $value; - } - - /** - * Convert array of request parameters (prefixed with _) - * to a regular array with non-prefixed keys. - * - * @param int $mode Source to get value from (GPC) - * @param string $ignore PCRE expression to skip parameters by name - * @param bool $allow_html Allow HTML tags in field value - * - * @return array Hash array with all request parameters - */ - public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false) - { - $out = array(); - $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST); - - foreach (array_keys($src) as $key) { - $fname = $key[0] == '_' ? substr($key, 1) : $key; - if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) { - $out[$fname] = self::get_input_value($key, $mode, $allow_html); - } - } - - return $out; - } - - /** - * Convert the given string into a valid HTML identifier - * Same functionality as done in app.js with rcube_webmail.html_identifier() - * - * @param string $str String input - * @param bool $encode Use base64 encoding - * - * @param string Valid HTML identifier - */ - public static function html_identifier($str, $encode = false) - { - if ($encode) { - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); - } - - return asciiwords($str, true, '_'); - } - - /** - * Replace all css definitions with #container [def] - * and remove css-inlined scripting, make position style safe - * - * @param string $source CSS source code - * @param string $container_id Container ID to use as prefix - * @param bool $allow_remote Allow remote content - * @param string $prefix Prefix to be added to id/class identifier - * - * @return string Modified CSS source - */ - public static function mod_css_styles($source, $container_id, $allow_remote = false, $prefix = '') - { - $last_pos = 0; - $replacements = new rcube_string_replacer; - - // ignore the whole block if evil styles are detected - $source = self::xss_entity_decode($source); - $stripped = preg_replace('/[^a-z\(:;]/i', '', $source); - $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\((?!data:image)' : ''); - - if (preg_match("/$evilexpr/i", $stripped)) { - return '/* evil! */'; - } - - $strict_url_regexp = '!url\s*\(\s*["\']?(https?:)//[a-z0-9/._+-]+["\']?\s*\)!Uims'; - - // cut out all contents between { and } - while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) { - $nested = strpos($source, '{', $pos+1); - if ($nested && $nested < $pos2) // when dealing with nested blocks (e.g. @media), take the inner one - $pos = $nested; - $length = $pos2 - $pos - 1; - $styles = substr($source, $pos+1, $length); - - // Convert position:fixed to position:absolute (#5264) - $styles = preg_replace('/position[^a-z]*:[\s\r\n]*fixed/i', 'position: absolute', $styles); - - // check every line of a style block... - if ($allow_remote) { - $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY); - - for ($i=0, $len=count($a_styles); $i < $len; $i++) { - $line = $a_styles[$i]; - $stripped = preg_replace('/[^a-z\(:;]/i', '', $line); - - // allow data:image uri, join with continuation - if (stripos($stripped, 'url(data:image')) { - $a_styles[$i] .= ';' . $a_styles[$i+1]; - unset($a_styles[$i+1]); - } - // allow strict url() values only - else if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) { - $a_styles = array('/* evil! */'); - break; - } - } - - $styles = implode(";\n", $a_styles); - } - - $key = $replacements->add($styles); - $repl = $replacements->get_replacement($key); - $source = substr_replace($source, $repl, $pos+1, $length); - $last_pos = $pos2 - ($length - strlen($repl)); - } - - // remove html comments - $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source); - - // add #container to each tag selector and prefix to id/class identifiers - if ($container_id || $prefix) { - // (?!##str) below is to not match with ##str_replacement_0## - // from rcube_string_replacer used above, this is needed for - // cases like @media { body { position: fixed; } } (#5811) - $regexp = '/(^\s*|,\s*|\}\s*|\{\s*)((?!##str):?[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im'; - $callback = function($matches) use ($container_id, $prefix) { - $replace = $matches[2]; - - if (stripos($replace, ':root') === 0) { - $replace = substr($replace, 5); - } - - if ($prefix) { - $replace = str_replace(array('.', '#'), array(".$prefix", "#$prefix"), $replace); - } - - if ($container_id) { - $replace = "#$container_id " . $replace; - } - - // Remove redundant spaces (for simpler testing) - $replace = preg_replace('/\s+/', ' ', $replace); - - return str_replace($matches[2], $replace, $matches[0]); - }; - - $source = preg_replace_callback($regexp, $callback, $source); - } - - // replace body definition because we also stripped off the tag - if ($container_id) { - $regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i'; - $source = preg_replace($regexp, "#$container_id", $source); - } - - // put block contents back in - $source = $replacements->resolve($source); - - return $source; - } - - /** - * Generate CSS classes from mimetype and filename extension - * - * @param string $mimetype Mimetype - * @param string $filename Filename - * - * @return string CSS classes separated by space - */ - public static function file2class($mimetype, $filename) - { - $mimetype = strtolower($mimetype); - $filename = strtolower($filename); - - list($primary, $secondary) = explode('/', $mimetype); - - $classes = array($primary ?: 'unknown'); - - if ($secondary) { - $classes[] = $secondary; - } - - if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) { - if (!in_array($m[1], $classes)) { - $classes[] = $m[1]; - } - } - - return implode(' ', $classes); - } - - /** - * Decode escaped entities used by known XSS exploits. - * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples - * - * @param string $content CSS content to decode - * - * @return string Decoded string - */ - public static function xss_entity_decode($content) - { - $callback = function($matches) { return chr(hexdec($matches[1])); }; - - $out = html_entity_decode(html_entity_decode($content)); - $out = trim(preg_replace('/(^$)/', '', trim($out))); - $out = preg_replace_callback('/\\\([0-9a-f]{2,6})\s*/i', $callback, $out); - $out = preg_replace('/\\\([^0-9a-f])/i', '\\1', $out); - $out = preg_replace('#/\*.*\*/#Ums', '', $out); - $out = strip_tags($out); - - return $out; - } - - /** - * Check if we can process not exceeding memory_limit - * - * @param integer $need Required amount of memory - * - * @return bool True if memory won't be exceeded, False otherwise - */ - public static function mem_check($need) - { - $mem_limit = parse_bytes(ini_get('memory_limit')); - $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB - - return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true; - } - - /** - * Check if working in SSL mode - * - * @param int $port HTTPS port number - * @param bool $use_https Enables 'use_https' option checking - * - * @return bool True in SSL mode, False otherwise - */ - public static function https_check($port = null, $use_https = true) - { - if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') { - return true; - } - - if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) - && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https' - && in_array($_SERVER['REMOTE_ADDR'], rcube::get_instance()->config->get('proxy_whitelist', array())) - ) { - return true; - } - - if ($port && $_SERVER['SERVER_PORT'] == $port) { - return true; - } - - if ($use_https && rcube::get_instance()->config->get('use_https')) { - return true; - } - - return false; - } - - /** - * Replaces hostname variables. - * - * @param string $name Hostname - * @param string $host Optional IMAP hostname - * - * @return string Hostname - */ - public static function parse_host($name, $host = '') - { - if (!is_string($name)) { - return $name; - } - - // %n - host - $n = self::server_name(); - // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld - // If %n=domain.tld then %t=domain.tld as well (remains valid) - $t = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $n); - // %d - domain name without first part (up to domain.tld) - $d = preg_replace('/^[^.]+\.(?![^.]+$)/', '', self::server_name('HTTP_HOST')); - // %h - IMAP host - $h = $_SESSION['storage_host'] ?: $host; - // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld - // If %h=domain.tld then %z=domain.tld as well (remains valid) - $z = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $h); - // %s - domain name after the '@' from e-mail address provided at login screen. - // Returns FALSE if an invalid email is provided - if (strpos($name, '%s') !== false) { - $user_email = self::idn_to_ascii(self::get_input_value('_user', self::INPUT_POST)); - $matches = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s); - if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) { - return false; - } - } - - return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name); - } - - /** - * Returns the server name after checking it against trusted hostname patterns. - * - * Returns 'localhost' and logs a warning when the hostname is not trusted. - * - * @param string $type The $_SERVER key, e.g. 'HTTP_HOST', Default: 'SERVER_NAME'. - * @param bool $strip_port Strip port from the host name - * - * @return string Server name - */ - public static function server_name($type = null, $strip_port = true) - { - $name = $_SERVER[$type ?: 'SERVER_NAME']; - $rcube = rcube::get_instance(); - $patterns = (array) $rcube->config->get('trusted_host_patterns'); - - if ($strip_port) { - $name = preg_replace('/:\d+$/', '', $name); - } - - if (empty($patterns) || in_array_nocase($name, $patterns)) { - return $name; - } - - if (!empty($name)) { - foreach ($patterns as $pattern) { - if (preg_match("/$pattern/", $name)) { - return $name; - } - } - - $rcube->raise_error(array('file' => __FILE__, 'line' => __LINE__, - 'message' => "Specified host is not trusted. Using 'localhost'."), true, false); - } - - return 'localhost'; - } - - /** - * Returns remote IP address and forwarded addresses if found - * - * @return string Remote IP address(es) - */ - public static function remote_ip() - { - $address = $_SERVER['REMOTE_ADDR']; - - // append the NGINX X-Real-IP header, if set - if (!empty($_SERVER['HTTP_X_REAL_IP'])) { - $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP']; - } - - // append the X-Forwarded-For header, if set - if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR']; - } - - if (!empty($remote_ip)) { - $address .= '(' . implode(',', $remote_ip) . ')'; - } - - return $address; - } - - /** - * Returns the real remote IP address - * - * @return string Remote IP address - */ - public static function remote_addr() - { - // Check if any of the headers are set first to improve performance - if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) { - $proxy_whitelist = rcube::get_instance()->config->get('proxy_whitelist', array()); - if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) { - if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) { - $forwarded_ip = trim($forwarded_ip); - if (!in_array($forwarded_ip, $proxy_whitelist)) { - return $forwarded_ip; - } - } - } - - if (!empty($_SERVER['HTTP_X_REAL_IP'])) { - return $_SERVER['HTTP_X_REAL_IP']; - } - } - } - - if (!empty($_SERVER['REMOTE_ADDR'])) { - return $_SERVER['REMOTE_ADDR']; - } - - return ''; - } - - /** - * Read a specific HTTP request header. - * - * @param string $name Header name - * - * @return string|null Header value or null if not available - */ - public static function request_header($name) - { - if (function_exists('apache_request_headers')) { - $headers = apache_request_headers(); - $key = strtoupper($name); - } - else { - $headers = $_SERVER; - $key = 'HTTP_' . strtoupper(strtr($name, '-', '_')); - } - - if (!empty($headers)) { - $headers = array_change_key_case($headers, CASE_UPPER); - - return $headers[$key]; - } - } - - /** - * Explode quoted string - * - * @param string $delimiter Delimiter expression string for preg_match() - * @param string $string Input string - * - * @return array String items - */ - public static function explode_quoted_string($delimiter, $string) - { - $result = array(); - $strlen = strlen($string); - - for ($q=$p=$i=0; $i < $strlen; $i++) { - if ($string[$i] == "\"" && $string[$i-1] != "\\") { - $q = $q ? false : true; - } - else if (!$q && preg_match("/$delimiter/", $string[$i])) { - $result[] = substr($string, $p, $i - $p); - $p = $i + 1; - } - } - - $result[] = (string) substr($string, $p); - - return $result; - } - - /** - * Improved equivalent to strtotime() - * - * @param string $date Date string - * @param DateTimeZone $timezone Timezone to use for DateTime object - * - * @return int Unix timestamp - */ - public static function strtotime($date, $timezone = null) - { - $date = self::clean_datestr($date); - $tzname = $timezone ? ' ' . $timezone->getName() : ''; - - // unix timestamp - if (is_numeric($date)) { - return (int) $date; - } - - // It can be very slow when provided string is not a date and very long - if (strlen($date) > 128) { - $date = substr($date, 0, 128); - } - - // if date parsing fails, we have a date in non-rfc format. - // remove token from the end and try again - while (($ts = @strtotime($date . $tzname)) === false || $ts < 0) { - if (($pos = strrpos($date, ' ')) === false) { - break; - } - - $date = rtrim(substr($date, 0, $pos)); - } - - return (int) $ts; - } - - /** - * Date parsing function that turns the given value into a DateTime object - * - * @param string $date Date string - * @param DateTimeZone $timezone Timezone to use for DateTime object - * - * @return DateTime|false DateTime object or False on failure - */ - public static function anytodatetime($date, $timezone = null) - { - if ($date instanceof DateTime) { - return $date; - } - - $dt = false; - $date = self::clean_datestr($date); - - // try to parse string with DateTime first - if (!empty($date)) { - try { - $_date = preg_match('/^[0-9]+$/', $date) ? "@$date" : $date; - $dt = $timezone ? new DateTime($_date, $timezone) : new DateTime($_date); - } - catch (Exception $e) { - // ignore - } - } - - // try our advanced strtotime() method - if (!$dt && ($timestamp = self::strtotime($date, $timezone))) { - try { - $dt = new DateTime("@".$timestamp); - if ($timezone) { - $dt->setTimezone($timezone); - } - } - catch (Exception $e) { - // ignore - } - } - - return $dt; - } - - /** - * Clean up date string for strtotime() input - * - * @param string $date Date string - * - * @return string Date string - */ - public static function clean_datestr($date) - { - $date = trim($date); - - // check for MS Outlook vCard date format YYYYMMDD - if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) { - return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3])); - } - - // Clean malformed data - $date = preg_replace( - array( - '/\(.*\)/', // remove RFC comments - '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal - '/[^a-z0-9\x20\x09:\/\.+-]/i', // remove any invalid characters - '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names - ), - array( - '', - '\\1', - '', - '', - ), $date); - - $date = trim($date); - - // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here - if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(\s.*)?$/', $date, $m)) { - $mdy = $m[2] > 12 && $m[1] <= 12; - $day = $mdy ? $m[2] : $m[1]; - $month = $mdy ? $m[1] : $m[2]; - $date = sprintf('%04d-%02d-%02d%s', $m[3], $month, $day, $m[4] ?: ' 00:00:00'); - } - // I've found that YYYY.MM.DD is recognized wrong, so here's a fix - else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})(\s.*)?$/', $date, $m)) { - $date = sprintf('%04d-%02d-%02d%s', $m[1], $m[2], $m[3], $m[4] ?: ' 00:00:00'); - } - - return $date; - } - - /** - * Turns the given date-only string in defined format into YYYY-MM-DD format. - * - * Supported formats: 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y' - * - * @param string $date Date string - * @param string $format Input date format - * - * @return string Date string in YYYY-MM-DD format, or the original string - * if format is not supported - */ - public static function format_datestr($date, $format) - { - $format_items = preg_split('/[.-\/\\\\]/', $format); - $date_items = preg_split('/[.-\/\\\\]/', $date); - $iso_format = '%04d-%02d-%02d'; - - if (count($format_items) == 3 && count($date_items) == 3) { - if ($format_items[0] == 'Y') { - $date = sprintf($iso_format, $date_items[0], $date_items[1], $date_items[2]); - } - else if (strpos('dj', $format_items[0]) !== false) { - $date = sprintf($iso_format, $date_items[2], $date_items[1], $date_items[0]); - } - else if (strpos('mn', $format_items[0]) !== false) { - $date = sprintf($iso_format, $date_items[2], $date_items[0], $date_items[1]); - } - } - - return $date; - } - - /** - * Wrapper for idn_to_ascii with support for e-mail address. - * - * Warning: Domain names may be lowercase'd. - * Warning: An empty string may be returned on invalid domain. - * - * @param string $str Decoded e-mail address - * - * @return string Encoded e-mail address - */ - public static function idn_to_ascii($str) - { - return self::idn_convert($str, true); - } - - /** - * Wrapper for idn_to_utf8 with support for e-mail address - * - * @param string $str Decoded e-mail address - * - * @return string Encoded e-mail address - */ - public static function idn_to_utf8($str) - { - return self::idn_convert($str, false); - } - - /** - * Convert a string to ascii or utf8 (using IDNA standard) - * - * @param string $input Decoded e-mail address - * @param boolean $is_utf Convert by idn_to_ascii if true and idn_to_utf8 if false - * - * @return string Encoded e-mail address - */ - public static function idn_convert($input, $is_utf = false) - { - if ($at = strpos($input, '@')) { - $user = substr($input, 0, $at); - $domain = substr($input, $at + 1); - } - else { - $user = ''; - $domain = $input; - } - - // Note that in PHP 7.2/7.3 calling idn_to_* functions with default arguments - // throws a warning, so we have to set the variant explicitely (#6075) - $variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : null; - $options = 0; - - // Because php-intl extension lowercases domains and return false - // on invalid input (#6224), we skip conversion when not needed - // for compatibility with our Net_IDNA2 wrappers in bootstrap.php - - if ($is_utf) { - if (preg_match('/[^\x20-\x7E]/', $domain)) { - $options = defined('IDNA_NONTRANSITIONAL_TO_ASCII') ? IDNA_NONTRANSITIONAL_TO_ASCII : 0; - $domain = idn_to_ascii($domain, $options, $variant); - } - } - else if (preg_match('/(^|\.)xn--/i', $domain)) { - $options = defined('IDNA_NONTRANSITIONAL_TO_UNICODE') ? IDNA_NONTRANSITIONAL_TO_UNICODE : 0; - $domain = idn_to_utf8($domain, $options, $variant); - } - - if ($domain === false) { - return ''; - } - - return $at ? $user . '@' . $domain : $domain; - } - - /** - * Split the given string into word tokens - * - * @param string $str Input to tokenize - * @param int $minlen Minimum length of a single token - * - * @return array List of tokens - */ - public static function tokenize_string($str, $minlen = 2) - { - $expr = array('/[\s;,"\'\/+-]+/ui', '/(\d)[-.\s]+(\d)/u'); - $repl = array(' ', '\\1\\2'); - - if ($minlen > 1) { - $minlen--; - $expr[] = "/(^|\s+)\w{1,$minlen}(\s+|$)/u"; - $repl[] = ' '; - } - - return array_filter(explode(" ", preg_replace($expr, $repl, $str))); - } - - /** - * Normalize the given string for fulltext search. - * Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended - * - * @param string $str Input string (UTF-8) - * @param bool $as_array True to return list of words as array - * @param int $minlen Minimum length of tokens - * - * @return string|array Normalized string or a list of normalized tokens - */ - public static function normalize_string($str, $as_array = false, $minlen = 2) - { - // replace 4-byte unicode characters with '?' character, - // these are not supported in default utf-8 charset on mysql, - // the chance we'd need them in searching is very low - $str = preg_replace('/(' - . '\xF0[\x90-\xBF][\x80-\xBF]{2}' - . '|[\xF1-\xF3][\x80-\xBF]{3}' - . '|\xF4[\x80-\x8F][\x80-\xBF]{2}' - . ')/', '?', $str); - - // split by words - $arr = self::tokenize_string($str, $minlen); - - // detect character set - if (utf8_encode(utf8_decode($str)) == $str) { - // ISO-8859-1 (or ASCII) - preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys); - preg_match_all('/./', 'aaaaaaaceeeeiiiinoooooouuuuyy', $values); - - $mapping = array_combine($keys[0], $values[0]); - $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')); - } - else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) { - // ISO-8859-2 - preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys); - preg_match_all('/./', 'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values); - - $mapping = array_combine($keys[0], $values[0]); - $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')); - } - - foreach ($arr as $i => $part) { - $part = mb_strtolower($part); - - if (!empty($mapping)) { - $part = strtr($part, $mapping); - } - - $arr[$i] = $part; - } - - return $as_array ? $arr : implode(' ', $arr); - } - - /** - * Compare two strings for matching words (order not relevant) - * - * @param string $haystack Haystack - * @param string $needle Needle - * - * @return bool True if match, False otherwise - */ - public static function words_match($haystack, $needle) - { - $a_needle = self::tokenize_string($needle, 1); - $_haystack = implode(' ', self::tokenize_string($haystack, 1)); - $valid = strlen($_haystack) > 0; - $hits = 0; - - foreach ($a_needle as $w) { - if ($valid) { - if (stripos($_haystack, $w) !== false) { - $hits++; - } - } - else if (stripos($haystack, $w) !== false) { - $hits++; - } - } - - return $hits >= count($a_needle); - } - - /** - * Parse commandline arguments into a hash array - * - * @param array $aliases Argument alias names - * - * @return array Argument values hash - */ - public static function get_opt($aliases = array()) - { - $args = array(); - $bool = array(); - - // find boolean (no value) options - foreach ($aliases as $key => $alias) { - if ($pos = strpos($alias, ':')) { - $aliases[$key] = substr($alias, 0, $pos); - $bool[] = $key; - $bool[] = $aliases[$key]; - } - } - - for ($i=1; $i < count($_SERVER['argv']); $i++) { - $arg = $_SERVER['argv'][$i]; - $value = true; - $key = null; - - if ($arg[0] == '-') { - $key = preg_replace('/^-+/', '', $arg); - $sp = strpos($arg, '='); - - if ($sp > 0) { - $key = substr($key, 0, $sp - 2); - $value = substr($arg, $sp+1); - } - else if (in_array($key, $bool)) { - $value = true; - } - else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') { - $value = $_SERVER['argv'][++$i]; - } - - $args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value; - } - else { - $args[] = $arg; - } - - if ($alias = $aliases[$key]) { - $args[$alias] = $args[$key]; - } - } - - return $args; - } - - /** - * Safe password prompt for command line - * from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/ - * - * @param string $prompt Prompt text - * - * @return string Password - */ - public static function prompt_silent($prompt = "Password:") - { - if (preg_match('/^win/i', PHP_OS)) { - $vbscript = sys_get_temp_dir() . 'prompt_password.vbs'; - $vbcontent = 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))'; - #file_put_contents($vbscript, $vbcontent); - - $command = "cscript //nologo " . escapeshellarg($vbscript); - $password = rtrim(shell_exec($command)); - unlink($vbscript); - - return $password; - } - - $command = "/usr/bin/env bash -c 'echo OK'"; - - if (rtrim(shell_exec($command)) !== 'OK') { - echo $prompt; - $pass = trim(fgets(STDIN)); - echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n"; - - return $pass; - } - - $command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'"; - $password = rtrim(shell_exec($command)); - echo "\n"; - - return $password; - } - - /** - * Find out if the string content means true or false - * - * @param string $str Input value - * - * @return bool Boolean value - */ - public static function get_boolean($str) - { - $str = strtolower($str); - - return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true); - } - - /** - * OS-dependent absolute path detection - * - * @param string $path File path - * - * @return bool True if the path is absolute, False otherwise - */ - public static function is_absolute_path($path) - { - if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { - return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path); - } - - return $path[0] == '/'; - } - - /** - * Resolve relative URL - * - * @param string $url Relative URL - * - * @return string Absolute URL - */ - public static function resolve_url($url) - { - // prepend protocol://hostname:port - if (!preg_match('|^https?://|', $url)) { - $schema = 'http'; - $default_port = 80; - - if (self::https_check()) { - $schema = 'https'; - $default_port = 443; - } - - $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']); - if ($_SERVER['SERVER_PORT'] != $default_port && $_SERVER['SERVER_PORT'] != 80) { - $prefix .= ':' . $_SERVER['SERVER_PORT']; - } - - $url = $prefix . ($url[0] == '/' ? '' : '/') . $url; - } - - return $url; - } - - /** - * Generate a random string - * - * @param int $length String length - * @param bool $raw Return RAW data instead of ascii - * - * @return string The generated random string - */ - public static function random_bytes($length, $raw = false) - { - $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - $tabsize = strlen($hextab); - - // Use PHP7 true random generator - if ($raw && function_exists('random_bytes')) { - return random_bytes($length); - } - - if (!$raw && function_exists('random_int')) { - $result = ''; - while ($length-- > 0) { - $result .= $hextab[random_int(0, $tabsize - 1)]; - } - - return $result; - } - - $random = openssl_random_pseudo_bytes($length); - - if ($random === false && $length > 0) { - throw new Exception("Failed to get random bytes"); - } - - if (!$raw) { - for ($x = 0; $x < $length; $x++) { - $random[$x] = $hextab[ord($random[$x]) % $tabsize]; - } - } - - return $random; - } - - /** - * Convert binary data into readable form (containing a-zA-Z0-9 characters) - * - * @param string $input Binary input - * - * @return string Readable output (Base62) - * @deprecated since 1.3.1 - */ - public static function bin2ascii($input) - { - $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - $result = ''; - - for ($x = 0; $x < strlen($input); $x++) { - $result .= $hextab[ord($input[$x]) % 62]; - } - - return $result; - } - - /** - * Format current date according to specified format. - * This method supports microseconds (u). - * - * @param string $format Date format (default: 'd-M-Y H:i:s O') - * - * @return string Formatted date - */ - public static function date_format($format = null) - { - if (empty($format)) { - $format = 'd-M-Y H:i:s O'; - } - - if (strpos($format, 'u') !== false) { - $dt = number_format(microtime(true), 6, '.', ''); - $dt .= '.' . date_default_timezone_get(); - - if ($date = date_create_from_format('U.u.e', $dt)) { - return $date->format($format); - } - } - - return date($format); - } - - /** - * Parses socket options and returns options for specified hostname. - * - * @param array &$options Configured socket options - * @param string $host Hostname - */ - public static function parse_socket_options(&$options, $host = null) - { - if (empty($host) || empty($options)) { - return; - } - - // get rid of schema and port from the hostname - $host_url = parse_url($host); - if (isset($host_url['host'])) { - $host = $host_url['host']; - } - - // find per-host options - if ($host && array_key_exists($host, $options)) { - $options = $options[$host]; - } - } - - /** - * Get maximum upload size - * - * @return int Maximum size in bytes - */ - public static function max_upload_size() - { - // find max filesize value - $max_filesize = parse_bytes(ini_get('upload_max_filesize')); - $max_postsize = parse_bytes(ini_get('post_max_size')); - - if ($max_postsize && $max_postsize < $max_filesize) { - $max_filesize = $max_postsize; - } - - return $max_filesize; - } - - /** - * Detect and log last PREG operation error - * - * @param array $error Error data (line, file, code, message) - * @param bool $terminate Stop script execution - * - * @return bool True on error, False otherwise - */ - public static function preg_error($error = array(), $terminate = false) - { - if (($preg_error = preg_last_error()) != PREG_NO_ERROR) { - $errstr = "PCRE Error: $preg_error."; - - if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) { - $errstr .= " Consider raising pcre.backtrack_limit!"; - } - if ($preg_error == PREG_RECURSION_LIMIT_ERROR) { - $errstr .= " Consider raising pcre.recursion_limit!"; - } - - $error = array_merge(array('code' => 620, 'line' => __LINE__, 'file' => __FILE__), $error); - - if (!empty($error['message'])) { - $error['message'] .= ' ' . $errstr; - } - else { - $error['message'] = $errstr; - } - - rcube::raise_error($error, true, $terminate); - - return true; - } - - return false; - } - - /** - * Generate a temporary file path in the Roundcube temp directory - * - * @param string $file_name String identifier for the type of temp file - * @param bool $unique Generate unique file names based on $file_name - * @param bool $create Create the temp file or not - * - * @return string temporary file path - */ - public static function temp_filename($file_name, $unique = true, $create = true) - { - $temp_dir = rcube::get_instance()->config->get('temp_dir'); - - // Fall back to system temp dir if configured dir is not writable - if (!is_writable($temp_dir)) { - $temp_dir = sys_get_temp_dir(); - } - - // On Windows tempnam() uses only the first three characters of prefix so use uniqid() and manually add the prefix - // Full prefix is required for garbage collection to recognise the file - $temp_file = $unique ? str_replace('.', '', uniqid($file_name, true)) : $file_name; - $temp_path = unslashify($temp_dir) . '/' . RCUBE_TEMP_FILE_PREFIX . $temp_file; - - // Sanity check for unique file name - if ($unique && file_exists($temp_path)) { - return self::temp_filename($file_name, $unique, $create); - } - - // Create the file to prevent possible race condition like tempnam() does - if ($create) { - touch($temp_path); - } - - return $temp_path; - } -} diff --git a/src/Thread.php b/src/Thread.php deleted file mode 100644 index 97afb85..0000000 --- a/src/Thread.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Thread -{ - public static function thread($imap, $flags = SE_FREE) - { - if (is_a($imap, Connection::class)) { - $client = $imap->getClient(); - #$client->setDebug(true); - - $thread = $client->thread($imap->getMailboxName()); - - if (empty($thread->count())) { - return false; - } - - return $thread->get(); - } - - return imap_thread($imap, $flags); - } -} diff --git a/src/Timeout.php b/src/Timeout.php deleted file mode 100644 index 858c771..0000000 --- a/src/Timeout.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Javanile\Imap2; - -class Timeout -{ - protected static $timeout; - - public static function set($timeoutType, $timeout = -1) - { - if ($timeout == -1) { - return self::get($timeoutType); - } - - self::$timeout[$timeoutType] = $timeout; - - return true; - } - - public static function get($timeoutType) - { - return self::$timeout[$timeoutType]; - } -} diff --git a/tests/ImapHelpersTest.php b/tests/ImapHelpersTest.php deleted file mode 100644 index 67d83fb..0000000 --- a/tests/ImapHelpersTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertEquals(1, 1); - /* - $imap = @imap2_open($this->mailbox, $this->username, $this->accessToken, OP_XOAUTH2); - $this->assertEquals($close1, $close2); - */ - } -} diff --git a/tests/legacy/test.php b/tests/legacy/test.php deleted file mode 100755 index 5566a26..0000000 --- a/tests/legacy/test.php +++ /dev/null @@ -1,19 +0,0 @@ -