From f83ccdaaee150e060ef7fc46c3e98ea071a8ce43 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 05:12:49 +0200 Subject: [PATCH 001/600] php-imap replaced by direct socket communication --- README.md | 137 +-- composer.json | 3 +- src/Attachment.php | 95 +- src/Client.php | 422 ++------ src/Connection/Protocols/ImapProtocol.php | 941 ++++++++++++++++++ src/Connection/Protocols/Protocol.php | 167 ++++ .../Protocols/ProtocolInterface.php | 275 +++++ src/EncodingAliases.php | 8 +- ...eException.php => AuthFailedException.php} | 8 +- ...eption.php => FolderFetchingException.php} | 6 +- .../MessageContentFetchingException.php | 24 + .../MessageHeaderFetchingException.php | 24 + .../ProtocolNotSupportedException.php | 24 + src/Exceptions/RuntimeException.php | 24 + src/Folder.php | 340 +++---- src/Header.php | 530 ++++++++++ src/Message.php | 664 +++--------- src/Part.php | 193 ++++ src/Query/Query.php | 90 +- src/Query/WhereQuery.php | 40 +- src/Structure.php | 119 +++ src/config/imap.php | 28 +- 22 files changed, 2877 insertions(+), 1285 deletions(-) create mode 100644 src/Connection/Protocols/ImapProtocol.php create mode 100644 src/Connection/Protocols/Protocol.php create mode 100644 src/Connection/Protocols/ProtocolInterface.php rename src/Exceptions/{InvalidImapTimeoutTypeException.php => AuthFailedException.php} (54%) rename src/Exceptions/{MailboxFetchingException.php => FolderFetchingException.php} (64%) create mode 100644 src/Exceptions/MessageContentFetchingException.php create mode 100644 src/Exceptions/MessageHeaderFetchingException.php create mode 100644 src/Exceptions/ProtocolNotSupportedException.php create mode 100644 src/Exceptions/RuntimeException.php create mode 100644 src/Header.php create mode 100644 src/Part.php create mode 100644 src/Structure.php diff --git a/README.md b/README.md index 68cebd51..cf6b030c 100755 --- a/README.md +++ b/README.md @@ -3,18 +3,12 @@ [![Latest Version on Packagist][ico-version]][link-packagist] [![Software License][ico-license]](LICENSE.md) [![Total Downloads][ico-downloads]][link-downloads] +[![Hits][ico-hits]][link-hits] ## Description -PHPIMAP is an easy way to integrate the native php imap library into your php app. - ->This package is a fork of one of my other projects [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) -which was specially designed to work with the laravel framework. - ->Since this library received way more interest than I thought, I decided to take the core features and -and move them into a new repository. - ->This repository will be part of [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) within the next few releases. +PHP-IMAP is a wrapper for common IMAP communication without the need to have the php-imap module installed / enabled. +The functionality is almost completely integrated and even supports IDLE operation. ## Table of Contents @@ -23,6 +17,7 @@ and move them into a new repository. - [Usage](#usage) - [Basic usage example](#basic-usage-example) - [Folder / Mailbox](#folder--mailbox) + - [Idle](#idle) - [Search](#search-for-messages) - [Counting messages](#counting-messages) - [Result limiting](#result-limiting) @@ -56,15 +51,20 @@ and move them into a new repository. ## Installation -1) Install the php-imap library if it isn't already installed: +1) Install decoding modules: + +``` shell +sudo apt-get install php*-mbstring php*-mcrypt && sudo apache2ctl graceful +``` +1.1) (optional) Install php-imap module if you are having encoding problems: ``` shell -sudo apt-get install php*-imap php*-mbstring php*-mcrypt && sudo apache2ctl graceful +sudo apt-get install php*-imap && sudo apache2ctl graceful ``` -You might also want to check `phpinfo()` if the extension is enabled. +You might also want to check `phpinfo()` if the extensions are enabled. -2) Now install the Laravel IMAP package by running the following command: +2) Now install the PHP-IMAP package by running the following command: ``` shell composer require webklex/php-imap @@ -76,15 +76,13 @@ composer require webklex/php-imap Supported protocols: - `imap` — Use IMAP [default] -- `pop3` — Use POP3 -- `nntp` — Use NNTP +- `pop3` — ~~Use POP3~~ [not supported jet] +- `nntp` — ~~Use NNTP~~ [not supported jet] The following encryption methods are supported: - `false` — Disable encryption - `ssl` — Use SSL - `tls` — Use TLS -- `starttls` — Use STARTTLS (alias TLS) -- `notls` — Use NoTLS Detailed [config/imap.php](src/config/imap.php) configuration: - `default` — used default account. It will be used as default for any missing account parameters. If however the default account is missing a parameter the package default will be used. Set to `false` to disable this functionality. @@ -96,18 +94,15 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `validate_cert` — decide weather you want to verify the certificate or not - `username` — imap account username - `password` — imap account password + - `authentication` — imap authentication method. Use `oauth` to use oAuth for Google, etc. - `date_format` — The default date format is used to convert any given Carbon::class object into a valid date string. (`d-M-Y`, `d-M-y`, `d M y`) - `options` — additional fetch options - `delimiter` — you can use any supported char such as ".", "/", etc - - `fetch` — `IMAP::FT_UID` (message marked as read by fetching the message) or `IMAP::FT_PEEK` (fetch the message without setting the "read" flag) - `fetch_body` — If set to `false` all messages will be fetched without the body and any potential attachments - - `fetch_attachment` — If set to `false` all messages will be fetched without any attachments - `fetch_flags` — If set to `false` all messages will be fetched without any flags - `message_key` — Message key identifier option - `fetch_order` — Message fetch order - - `open` — special configuration for imap_open() - - `DISABLE_AUTHENTICATOR` — Disable authentication properties. - - `decoder` — Currently only the message subject and attachment name decoder can be set + - `decoder` — Currently only the message and attachment decoder can be set - `masks` — Default [masking](#masking) config - `message` — Default message mask - `attachment` — Default attachment mask @@ -173,13 +168,15 @@ foreach($aFolder as $oFolder){ ``` #### Folder / Mailbox -There is an experimental function available to get a Folder instance by name. -For an easier access please take a look at the new config option `imap.options.delimiter` however the `getFolder` -method takes three options: the required (string) $folder_name and two optional variables. An integer $attributes which -seems to be sometimes 32 or 64 (I honestly have no clue what this number does, so feel free to enlighten me and anyone -else) and a delimiter which if it isn't set will use the default option configured inside the [config/imap.php](src/config/imap.php) file. -> If you are using Exchange you might want to set all parameter and the last `$prefix_address` to `false` e.g. `$oClient->getFolder('name', 32, null, false)` #234 +List all available folders: +``` php +/** @var \Webklex\PHPIMAP\Client $oClient */ +/** @var \Webklex\PHPIMAP\Support\FolderCollection $aFolder */ +$aFolder = $oClient->getFolders(); +``` + +Get a specific folder: ``` php /** @var \Webklex\PHPIMAP\Client $oClient */ @@ -187,15 +184,17 @@ else) and a delimiter which if it isn't set will use the default option configur $oFolder = $oClient->getFolder('INBOX.name'); ``` -List all available folders: +#### Idle +Every time a new message is received the server will notify the client and return the new message. ``` php /** @var \Webklex\PHPIMAP\Client $oClient */ -/** @var \Webklex\PHPIMAP\Support\FolderCollection $aFolder */ -$aFolder = $oClient->getFolders(); +/** @var \Webklex\PHPIMAP\Folder $oFolder */ +$oFolder->idle(function($message){ + dump($message->subject); +}); ``` - #### Search for messages Search for specific emails: ``` php @@ -263,7 +262,7 @@ $aMessage = $oFolder->search()->text('hello world')->since('15.03.2018')->get(); $aMessage = $oFolder->messages()->text('hello world')->since('15.03.2018')->get(); ``` -All available query / search methods can be found here: [Query::class](src/IMAP/Query/WhereQuery.php) +All available query / search methods can be found here: [Query::class](src/Query/WhereQuery.php) Available search criteria: - `ALL` — return all messages matching the rest of the criteria @@ -381,7 +380,7 @@ $paginator = $oFolder->search() #### View examples -You can find a few blade examples under [/examples](https://github.com/Webklex/php-imap/tree/master/examples). +You can find a few blade examples under [/examples](examples). #### Fetch a specific message Get a specific message by uid (Please note that the uid is not unique and can change): @@ -389,7 +388,7 @@ Get a specific message by uid (Please note that the uid is not unique and can ch /** @var \Webklex\PHPIMAP\Folder $oFolder */ /** @var \Webklex\PHPIMAP\Message $oMessage */ -$oMessage = $oFolder->getMessage($uid = 1); +$oMessage = $oFolder->query()->getMessage($msgn = 1); ``` #### Message flags @@ -400,20 +399,6 @@ $oMessage->setFlag(['Seen', 'Spam']); $oMessage->unsetFlag('Spam'); ``` -Mark all messages as "read" while fetching: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->text('Hello world')->markAsRead()->get(); -``` - -Don't mark all messages as "read" while fetching: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->text('Hello world')->leaveUnread()->get(); -``` - #### Attachments Save message attachments: ``` php @@ -424,7 +409,7 @@ $aAttachment = $oMessage->getAttachments(); $aAttachment->each(function ($oAttachment) { /** @var \Webklex\PHPIMAP\Attachment $oAttachment */ - $oAttachment->save(); + $oAttachment->save("/some/path/"); }); ``` @@ -437,7 +422,7 @@ Fetch messages without body fetching (decrease load): $aMessage = $oFolder->query()->whereText('Hello world')->setFetchBody(false)->get(); /** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->whereAll()->setFetchBody(false)->setFetchAttachment(); +$aMessage = $oFolder->query()->whereAll()->setFetchBody(false)->get(); ``` Fetch messages without body, flag and attachment fetching (decrease load): @@ -448,14 +433,12 @@ Fetch messages without body, flag and attachment fetching (decrease load): $aMessage = $oFolder->query()->whereText('Hello world') ->setFetchFlags(false) ->setFetchBody(false) -->setFetchAttachment(false) ->get(); /** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ $aMessage = $oFolder->query()->whereAll() ->setFetchFlags(false) ->setFetchBody(false) -->setFetchAttachment(false) ->get(); ``` @@ -525,7 +508,7 @@ Additional examples can be found here: #### Specials Find the folder containing a message: ``` php -$oFolder = $aMessage->getContainingFolder(); +$oFolder = $aMessage->getFolder(); ``` ## Support @@ -564,30 +547,18 @@ if you're just wishing a feature ;) | ------------------------- | ------------------------------------------------------------------------------- | :---------------: | ---------------------------------------------------------------------------------------------------------------------------- | | setConfig | array $config | self | Set the Client configuration. Take a look at `config/imap.php` for more inspiration. | | getConnection | resource $connection | resource | Get the current imap resource | -| setReadOnly | bool $readOnly | self | Set read only property and reconnect if it's necessary. | -| isReadOnly | | bool | Determine if connection is in read only mode. | | isConnected | | bool | Determine if connection was established. | | checkConnection | | | Determine if connection was established and connect if not. | -| connect | int $attempts | | Connect to server. | +| connect | | | Connect to server. | +| reconnect | | | Terminate and reconnect to server. | | disconnect | | | Disconnect from server. | | getFolder | string $folder_name, int $attributes = 32, int or null $delimiter, bool $prefix_address | Folder | Get a Folder instance by name | | getFolders | bool $hierarchical, string or null $parent_folder | FolderCollection | Get folders list. If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array. | | openFolder | string or Folder $folder, integer $attempts | | Open a given folder. | | createFolder | string $name | boolean | Create a new folder. | -| renameFolder | string $old_name, string $new_name | boolean | Rename a folder. | -| deleteFolder | string $name | boolean | Delete a folder. | -| getMessages | Folder $folder, string $criteria, bool $fetch_body, bool $fetch_attachment, bool $fetch_flags | MessageCollection | Get messages from folder. | -| getUnseenMessages | Folder $folder, string $criteria, bool $fetch_body, bool $fetch_attachment, bool $fetch_flags | MessageCollection | Get Unseen messages from folder. | -| searchMessages | array $where, Folder $folder, $fetch_options, bool $fetch_body, string $charset, bool $fetch_attachment, bool $fetch_flags | MessageCollection | Get specific messages from a given folder. | | getQuota | | array | Retrieve the quota level settings, and usage statics per mailbox | | getQuotaRoot | string $quota_root | array | Retrieve the quota settings per user | -| countMessages | | int | Gets the number of messages in the current mailbox | -| countRecentMessages | | int | Gets the number of recent messages in current mailbox | -| getAlerts | | array | Returns all IMAP alert messages that have occurred | -| getErrors | | array | Returns all of the IMAP errors that have occurred | -| getLastError | | string | Gets the last IMAP error that occurred during this page request | | expunge | | bool | Delete all messages marked for deletion | -| checkCurrentMailbox | | object | Check current mailbox | | setTimeout | string or int $type, int $timeout | boolean | Set the timeout for certain imap operations: 1: Open, 2: Read, 3: Write, 4: Close | | getTimeout | string or int $type | int | Check current mailbox | | setDefaultMessageMask | string $mask | self | Set the default message mask class | @@ -603,10 +574,8 @@ if you're just wishing a feature ;) | parseBody | | Message | Parse the Message body | | delete | boolean $expunge | boolean | Delete the current Message | | restore | boolean $expunge | boolean | Restore a deleted Message | -| copy | string $mailbox, int $options | boolean | Copy the current Messages to a mailbox | -| move | string $mailbox, int $options | boolean | Move the current Messages to a mailbox | -| getContainingFolder | Folder or null $folder | Folder or null | Get the folder containing the message | -| moveToFolder | string $mailbox, boolean $expunge, boolean $create_folder | Message | Move the Message into an other Folder | +| copy | string $mailbox | boolean | Copy the current Messages to a mailbox | +| move | string $mailbox, boolean $expunge | boolean | Move the current Messages to a mailbox | | setFlag | string or array $flag | boolean | Set one or many flags | | unsetFlag | string or array $flag | boolean | Unset one or many flags | | hasTextBody | | | Check if the Message has a text body | @@ -650,13 +619,14 @@ if you're just wishing a feature ;) | ----------------- | ----------------------------------------------------------------------------------- | :---------------: | ---------------------------------------------- | | hasChildren | | bool | Determine if folder has children. | | setChildren | array $children | self | Set children. | -| getMessage | integer $uid, integer or null $msglist, int or null fetch_options, bool $fetch_body, bool $fetch_attachment, bool $fetch_flags | Message | Get a specific message from folder. | -| getMessages | string $criteria, int or null $fetch_options, bool $fetch_body, bool $fetch_attachment, bool $fetch_flags | MessageCollection | Get messages from folder. | -| getUnseenMessages | string $criteria, int or null $fetch_options, bool $fetch_body, bool $fetch_attachment, bool $fetch_flags | MessageCollection | Get Unseen messages from folder. | -| searchMessages | array $where, int or null $fetch_options, bool $fetch_body, string $charset, bool $fetch_attachment, bool $fetch_flags | MessageCollection | Get specific messages from a given folder. | | delete | | | Delete the current Mailbox | +| subscribe | | | Subscribe to the current Mailbox | +| unsubscribe | | | Unsubscribe from the current Mailbox | +| idle | callable $callback(Message $new_message) | | Idle the current folder | | move | string $mailbox | | Move or Rename the current Mailbox | -| getStatus | integer $options | object | Returns status information on a mailbox | +| rename | string $mailbox | | Move or Rename the current Mailbox | +| getStatus | | array | Returns status information on the current mailbox | +| examine | | array | Returns status information on the current mailbox | | appendMessage | string $message, string $options, string $internal_date | bool | Append a string message to the current mailbox | | getClient | | Client | Get the current Client instance | | query | string $charset = 'UTF-8' | WhereQuery | Get the current Client instance | @@ -704,7 +674,6 @@ if you're just wishing a feature ;) | limit | integer $limit, integer $page = 1 | WhereQuery | Limit the amount of messages being fetched | | setFetchOptions | boolean $fetch_options | WhereQuery | Set the fetch options | | setFetchBody | boolean $fetch_body | WhereQuery | Set the fetch body option | -| getFetchAttachment | boolean $fetch_attachment | WhereQuery | Set the fetch attachment option | | setFetchFlags | boolean $fetch_flags | WhereQuery | Set the fetch flags option | | leaveUnread | | WhereQuery | Don't mark all messages as "read" while fetching: | | markAsRead | | WhereQuery | Mark all messages as "read" while fetching | @@ -720,13 +689,11 @@ if you're just wishing a feature ;) | getExtension | | string or null | Get a guessed attachment extension | | getId | | string or null | Get attachment id | | getName | | string or null | Get attachment name | -| getPartNumber | | int or null | Get attachment part number | | getContent | | string or null | Get attachment content | | setSize | | int or null | Get attachment size | | getType | | string or null | Get attachment type | | getDisposition | | string or null | Get attachment disposition | | getContentType | | string or null | Get attachment content type | -| getImgSrc | | string or null | Get attachment image source as base64 encoded data url | | save | string $path, string $filename | boolean | Save the attachment content to your filesystem | | mask | string $mask = null | Mask | Get a masked instance | | setMask | string $mask | Attachment | Set the mask class | @@ -789,13 +756,7 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum ### Known issues | Error | Solution | | ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside `config/imap.php` | -| imap_fetchbody() expects parameter 4 to be long, string given (...) | Make sure that `imap.options.fetch` is a valid integer | -| Use of undefined constant FT_UID - assumed 'FT_UID' (...) | Please take a look at [#14](https://github.com/Webklex/php-imap/issues/14) [#30](https://github.com/Webklex/php-imap/issues/30) | -| DateTime::__construct(): Failed to parse time string (...) | Please report any new invalid timestamps to [#45](https://github.com/Webklex/php-imap/issues/45) | -| imap_open(): Couldn't open (...) Please log in your web browser: (...) | In order to use IMAP on some services (such as Gmail) you need to enable it first. [Google help page]( https://support.google.com/mail/answer/7126229?hl=en) | -| imap_headerinfo(): Bad message number | This happens if no Message number is available. Please make sure Message::parseHeader() has run before | -| imap_fetchheader(): Bad message number | Try to change the `message_key` [#243](https://github.com/Webklex/laravel-imap/issues/243) +| NaN | NaN | ## Milestones & upcoming features @@ -825,6 +786,7 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/Webklex/php-imap.svg?style=flat-square [ico-code-quality]: https://img.shields.io/scrutinizer/g/Webklex/php-imap.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/Webklex/php-imap.svg?style=flat-square +[ico-hits]: https://hits.webklex.com/svg/webklex/php-imap [link-packagist]: https://packagist.org/packages/Webklex/php-imap [link-travis]: https://travis-ci.org/Webklex/php-imap @@ -834,3 +796,4 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [link-author]: https://github.com/webklex [link-contributors]: https://github.com/Webklex/php-imap/graphs/contributors [link-jetbrains]: https://www.jetbrains.com +[link-hits]: https://hits.webklex.com diff --git a/composer.json b/composer.json index 0ff14414..5fb56bde 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ ], "require": { "php": ">=5.5.9", - "ext-imap": "*", + "ext-openssl": "*", + "ext-json": "*", "ext-mbstring": "*", "ext-iconv": "*", "ext-fileinfo": "*", diff --git a/src/Attachment.php b/src/Attachment.php index 8f617f56..802e1e67 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -59,12 +59,11 @@ class Attachment { /** @var array $config */ protected $config = []; - /** @var object $structure */ - protected $structure; + /** @var Part $part */ + protected $part; /** @var array $attributes */ protected $attributes = [ - 'part_number' => 1, 'content' => null, 'type' => null, 'content_type' => null, @@ -85,17 +84,13 @@ class Attachment { * Attachment constructor. * * @param Message $oMessage - * @param object $structure - * @param integer $part_number - * - * @throws Exceptions\ConnectionFailedException + * @param Part $part */ - public function __construct(Message $oMessage, $structure, $part_number = 1) { + public function __construct(Message $oMessage, Part $part) { $this->config = ClientManager::get('options'); $this->oMessage = $oMessage; - $this->structure = $structure; - $this->part_number = ($part_number) ? $part_number : $this->part_number; + $this->part = $part; $default_mask = $this->oMessage->getClient()->getDefaultAttachmentMask(); if($default_mask != null) { @@ -163,7 +158,7 @@ public function __get($name) { * Determine the structure type */ protected function findType() { - switch ($this->structure->type) { + switch ($this->part->type) { case IMAP::ATTACHMENT_TYPE_MESSAGE: $this->type = 'message'; break; @@ -196,49 +191,33 @@ protected function findType() { /** * Fetch the given attachment - * - * @throws Exceptions\ConnectionFailedException */ protected function fetch() { - $content = \imap_fetchbody($this->oMessage->getClient()->getConnection(), $this->oMessage->getUid(), $this->part_number, $this->oMessage->getFetchOptions() | FT_UID); + $content = $this->part->content; - $this->content_type = $this->type.'/'.strtolower($this->structure->subtype); - $this->content = $this->oMessage->decodeString($content, $this->structure->encoding); + $this->content_type = $this->type.'/'.strtolower($this->part->subtype); + $this->content = $this->oMessage->decodeString($content, $this->part->encoding); - if (property_exists($this->structure, 'id')) { - $this->id = str_replace(['<', '>'], '', $this->structure->id); + if (($id = $this->part->id) !== null) { + $this->id = str_replace(['<', '>'], '', $id); } - if (property_exists($this->structure, 'bytes')) { - $this->size = $this->structure->bytes; - } + $this->size = $this->part->bytes; - if (property_exists($this->structure, 'dparameters')) { - foreach ($this->structure->dparameters as $parameter) { - if (strtolower($parameter->attribute) == "filename") { - $this->setName($parameter->value); - $this->disposition = property_exists($this->structure, 'disposition') ? $this->structure->disposition : null; - break; - } - } + if (($name = $this->part->name) !== null) { + $this->setName($name); + $this->disposition = $this->part->disposition; + }elseif (($filename = $this->part->filename) !== null) { + $this->setName($filename); + $this->disposition = $this->part->disposition; } - if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->structure->type) { - if ($this->structure->ifdescription) { - $this->setName($this->structure->description); + if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) { + if ($this->part->ifdescription) { + $this->setName($this->part->description); } else { - $this->setName($this->structure->subtype); - } - } - - if (!$this->name && property_exists($this->structure, 'parameters')) { - foreach ($this->structure->parameters as $parameter) { - if (strtolower($parameter->attribute) == "name") { - $this->setName($parameter->value); - $this->disposition = property_exists($this->structure, 'disposition') ? $this->structure->disposition : null; - break; - } + $this->setName($this->part->subtype); } } } @@ -246,17 +225,14 @@ protected function fetch() { /** * Save the attachment content to your filesystem * - * @param string|null $path + * @param string $path * @param string|null $filename * * @return boolean */ - public function save($path = null, $filename = null) { - $path = $path ?: storage_path(); + public function save($path, $filename = null) { $filename = $filename ?: $this->getName(); - $path = substr($path, -1) == DIRECTORY_SEPARATOR ? $path : $path.DIRECTORY_SEPARATOR; - return File::put($path.$filename, $this->getContent()) !== false; } @@ -264,23 +240,14 @@ public function save($path = null, $filename = null) { * @param $name */ public function setName($name) { - if($this->config['decoder']['message']['subject'] === 'utf-8') { - $this->name = \imap_utf8($name); - }else{ - $this->name = mb_decode_mimeheader($name); - } - } - - /** - * @return null|string - * - * @deprecated 1.4.0:2.0.0 No longer needed. Use AttachmentMask::getImageSrc() instead - */ - public function getImgSrc() { - if ($this->type == 'image' && $this->img_src == null) { - $this->img_src = 'data:'.$this->content_type.';base64,'.base64_encode($this->content); + $decoder = $this->config['decoder']['attachment']; + if ($name !== null) { + if($decoder === 'utf-8' && extension_loaded('imap')) { + $this->name = \imap_utf8($name); + }else{ + $this->name = mb_decode_mimeheader($name); + } } - return $this->img_src; } /** diff --git a/src/Client.php b/src/Client.php index 5862694c..6a39eba6 100755 --- a/src/Client.php +++ b/src/Client.php @@ -12,16 +12,16 @@ namespace Webklex\PHPIMAP; +use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; +use Webklex\PHPIMAP\Connection\Protocols\Protocol; +use Webklex\PHPIMAP\Connection\Protocols\ProtocolInterface; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; -use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; -use Webklex\PHPIMAP\Exceptions\InvalidImapTimeoutTypeException; -use Webklex\PHPIMAP\Exceptions\MailboxFetchingException; +use Webklex\PHPIMAP\Exceptions\FolderFetchingException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; -use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; +use Webklex\PHPIMAP\Exceptions\ProtocolNotSupportedException; use Webklex\PHPIMAP\Support\FolderCollection; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; use Webklex\PHPIMAP\Support\Masks\MessageMask; -use Webklex\PHPIMAP\Support\MessageCollection; /** * Class Client @@ -32,7 +32,7 @@ class Client { /** - * @var boolean|resource + * @var boolean|Protocol */ public $connection = false; @@ -87,11 +87,11 @@ class Client { public $password; /** - * Read only parameter. + * Account authentication method. * - * @var bool + * @var string */ - protected $read_only = false; + public $authentication; /** * Active folder. @@ -100,26 +100,13 @@ class Client { */ protected $active_folder = false; - /** - * Connected parameter - * - * @var bool - */ - protected $connected = false; - - /** - * IMAP errors that might have ben occurred - * - * @var array $errors - */ - protected $errors = []; - /** * All valid and available account config parameters * * @var array $validConfigKeys */ - protected $valid_config_keys = ['host', 'port', 'encryption', 'validate_cert', 'username', 'password', 'protocol']; + protected $valid_config_keys = ['host', 'port', 'encryption', 'validate_cert', 'username', 'password', 'protocol', + 'authentication']; /** * @var string $default_message_mask @@ -222,7 +209,7 @@ protected function setMaskFromConfig($config) { /** * Get the current imap resource * - * @return bool|resource + * @return bool|Protocol|ProtocolInterface * @throws ConnectionFailedException */ public function getConnection() { @@ -230,88 +217,86 @@ public function getConnection() { return $this->connection; } - /** - * Set read only property and reconnect if it's necessary. - * - * @param bool $read_only - * - * @return self - */ - public function setReadOnly($read_only = true) { - $this->read_only = $read_only; - - return $this; - } - /** * Determine if connection was established. * * @return bool */ public function isConnected() { - return $this->connected; + return $this->connection ? $this->connection->connected() : false; } /** - * Determine if connection is in read only mode. + * Determine if connection was established and connect if not. * - * @return bool + * @throws ConnectionFailedException */ - public function isReadOnly() { - return $this->read_only; + public function checkConnection() { + if (!$this->isConnected()) { + $this->connect(); + } } /** - * Determine if connection was established and connect if not. + * Force a reconnect * * @throws ConnectionFailedException */ - public function checkConnection() { - if (!$this->isConnected() || $this->connection === false) { - $this->connect(); + public function reconnect() { + if ($this->isConnected()) { + $this->disconnect(); } + $this->connect(); } /** * Connect to server. * - * @param int $attempts - * * @return $this * @throws ConnectionFailedException */ - public function connect($attempts = 3) { + public function connect() { $this->disconnect(); - try { - $this->connection = \imap_open( - $this->getAddress(), - $this->username, - $this->password, - $this->getOptions(), - $attempts, - ClientManager::get('options.open') - ); - $this->connected = !!$this->connection; - } catch (\ErrorException $e) { - $errors = \imap_errors(); - $message = $e->getMessage().'. '.implode("; ", (is_array($errors) ? $errors : array())); - - throw new ConnectionFailedException($message); + if (strtolower($this->protocol) == "imap") { + $timeout = $this->connection !== false ? $this->connection->getConnectionTimeout() : null; + $this->connection = new ImapProtocol($this->validate_cert); + $this->connection->setConnectionTimeout($timeout); + }else{ + throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($this->protocol." is an unsupported protocol")); } + $this->connection->connect($this->host, $this->port, $this->encryption); + $this->authenticate(); + return $this; } + /** + * Authenticate the current session + * + * @throws ConnectionFailedException + */ + protected function authenticate() { + try { + if ($this->authentication == "oauth") { + $this->connection->authenticate($this->username, $this->password); + }else{ + $this->connection->login($this->username, $this->password); + } + } catch (\Exception $e) { + throw new ConnectionFailedException("connection setup failed", 0, $e); + } + } + /** * Disconnect from server. * * @return $this */ public function disconnect() { - if ($this->isConnected() && $this->connection !== false && is_integer($this->connection) === false) { - $this->errors = array_merge($this->errors, \imap_errors() ?: []); - $this->connected = !\imap_close($this->connection, IMAP::CL_EXPUNGE); + if ($this->isConnected() && $this->connection !== false) { + $this->connection->logout(); } return $this; @@ -319,29 +304,13 @@ public function disconnect() { /** * Get a folder instance by a folder name - * --------------------------------------------- - * PLEASE NOTE: This is an experimental function - * --------------------------------------------- - * @param string $folder_name - * @param int $attributes - * @param null|string $delimiter - * @param boolean $prefix_address - * - * @return Folder + * @param $folder_name + * @return mixed + * @throws ConnectionFailedException + * @throws FolderFetchingException */ - public function getFolder($folder_name, $attributes = 32, $delimiter = null, $prefix_address = true) { - - $delimiter = $delimiter === null ? ClientManager::get('imap.options.delimiter', '/') : $delimiter; - - $folder_name = $prefix_address ? $this->getAddress().$folder_name : $folder_name; - - $oFolder = new Folder($this, (object) [ - 'name' => $folder_name, - 'attributes' => $attributes, - 'delimiter' => $delimiter - ]); - - return $oFolder; + public function getFolder($folder_name) { + return $this->getFolders(false)->where("name", $folder_name)->first(); } /** @@ -353,18 +322,18 @@ public function getFolder($folder_name, $attributes = 32, $delimiter = null, $pr * * @return FolderCollection * @throws ConnectionFailedException - * @throws MailboxFetchingException + * @throws FolderFetchingException */ public function getFolders($hierarchical = true, $parent_folder = null) { $this->checkConnection(); $folders = FolderCollection::make([]); $pattern = $parent_folder.($hierarchical ? '%' : '*'); + $items = $this->connection->folders('', $pattern); - $items = \imap_getmailboxes($this->connection, $this->getAddress(), $pattern); if(is_array($items)){ - foreach ($items as $item) { - $folder = new Folder($this, $item); + foreach ($items as $folder_name => $item) { + $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; @@ -378,179 +347,60 @@ public function getFolders($hierarchical = true, $parent_folder = null) { return $folders; }else{ - throw new MailboxFetchingException($this->getLastError()); + throw new FolderFetchingException("failed to fetch any folders"); } } /** * Open folder. * - * @param string|Folder $folder_path - * @param int $attempts - * + * @param string $folder + * @return mixed * @throws ConnectionFailedException */ - public function openFolder($folder_path, $attempts = 3) { - $this->checkConnection(); - - if(property_exists($folder_path, 'path')) { - $folder_path = $folder_path->path; - } - - if ($this->active_folder !== $folder_path) { - $this->active_folder = $folder_path; - - \imap_reopen($this->getConnection(), $folder_path, $this->getOptions(), $attempts); + public function openFolder($folder) { + if ($this->active_folder == $folder && $this->isConnected()) { + return true; } - } - - /** - * Create a new Folder - * @param string $name - * @param boolean $expunge - * - * @return bool - * @throws ConnectionFailedException - */ - public function createFolder($name, $expunge = true) { $this->checkConnection(); - $status = \imap_createmailbox($this->getConnection(), $this->getAddress() . \imap_utf7_encode($name)); - if($expunge) $this->expunge(); - - return $status; + $this->active_folder = $folder; + return $this->connection->selectFolder($folder); } /** - * Rename Folder - * @param string $old_name - * @param string $new_name + * Create a new Folder + * @param string $folder * @param boolean $expunge * * @return bool * @throws ConnectionFailedException */ - public function renameFolder($old_name, $new_name, $expunge = true) { + public function createFolder($folder, $expunge = true) { $this->checkConnection(); - $status = \imap_renamemailbox($this->getConnection(), $this->getAddress() . \imap_utf7_encode($old_name), $this->getAddress() . \imap_utf7_encode($new_name)); + $status = $this->connection->createFolder($folder); if($expunge) $this->expunge(); return $status; } /** - * Delete Folder - * @param string $name - * @param boolean $expunge - * - * @return bool + * Check a given folder + * @param $folder + * @return false|object * @throws ConnectionFailedException */ - public function deleteFolder($name, $expunge = true) { + public function checkFolder($folder) { $this->checkConnection(); - $status = \imap_deletemailbox($this->getConnection(), $this->getAddress() . \imap_utf7_encode($name)); - if($expunge) $this->expunge(); - - return $status; - } - - /** - * Get messages from folder. - * - * @param Folder $folder - * @param string $criteria - * @param int|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * - * @return MessageCollection - * @throws ConnectionFailedException - * @throws Exceptions\InvalidWhereQueryCriteriaException - * @throws GetMessagesFailedException - * - * @deprecated 1.0.5.2:2.0.0 No longer needed. Use Folder::getMessages() instead - * @see Folder::getMessages() - */ - public function getMessages(Folder $folder, $criteria = 'ALL', $fetch_options = null, $fetch_body = true, $fetch_attachment = true, $fetch_flags = false) { - return $folder->getMessages($criteria, $fetch_options, $fetch_body, $fetch_attachment, $fetch_flags); - } - - /** - * Get all unseen messages from folder - * - * @param Folder $folder - * @param string $criteria - * @param int|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * - * @return MessageCollection - * @throws ConnectionFailedException - * @throws Exceptions\InvalidWhereQueryCriteriaException - * @throws GetMessagesFailedException - * @throws MessageSearchValidationException - * - * @deprecated 1.0.5:2.0.0 No longer needed. Use Folder::getMessages('UNSEEN') instead - * @see Folder::getMessages() - */ - public function getUnseenMessages(Folder $folder, $criteria = 'UNSEEN', $fetch_options = null, $fetch_body = true, $fetch_attachment = true, $fetch_flags = false) { - return $folder->getUnseenMessages($criteria, $fetch_options, $fetch_body, $fetch_attachment, $fetch_flags); - } - - /** - * Search messages by a given search criteria - * - * @param array $where - * @param Folder $folder - * @param int|null $fetch_options - * @param boolean $fetch_body - * @param string $charset - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * - * @return MessageCollection - * @throws ConnectionFailedException - * @throws Exceptions\InvalidWhereQueryCriteriaException - * @throws GetMessagesFailedException - * - * @deprecated 1.0.5:2.0.0 No longer needed. Use Folder::searchMessages() instead - * @see Folder::searchMessages() - * - */ - public function searchMessages(array $where, Folder $folder, $fetch_options = null, $fetch_body = true, $charset = "UTF-8", $fetch_attachment = true, $fetch_flags = false) { - return $folder->searchMessages($where, $fetch_options, $fetch_body, $charset, $fetch_attachment, $fetch_flags); + return $this->connection->examineFolder($folder); } /** - * Get option for \imap_open and \imap_reopen. - * It supports only isReadOnly feature. - * - * @return int - */ - protected function getOptions() { - return ($this->isReadOnly()) ? IMAP::OP_READONLY : 0; - } - - /** - * Get full address of mailbox. + * Get the current active folder * - * @return string + * @return Folder */ - protected function getAddress() { - $address = "{".$this->host.":".$this->port."/".($this->protocol ? $this->protocol : 'imap'); - if (!$this->validate_cert) { - $address .= '/novalidate-cert'; - } - if (in_array($this->encryption,['tls', 'notls', 'ssl'])) { - $address .= '/'.$this->encryption; - } elseif ($this->encryption === "starttls") { - $address .= '/tls'; - } - - $address .= '}'; - - return $address; + public function getFolderPath(){ + return $this->active_folder; } /** @@ -561,7 +411,7 @@ protected function getAddress() { */ public function getQuota() { $this->checkConnection(); - return \imap_get_quota($this->getConnection(), 'user.'.$this->username); + return $this->connection->requestAndResponse("GETQUOTA", ['"#user/'.$this->username.'"']); } /** @@ -574,58 +424,7 @@ public function getQuota() { */ public function getQuotaRoot($quota_root = 'INBOX') { $this->checkConnection(); - return \imap_get_quotaroot($this->getConnection(), $quota_root); - } - - /** - * Gets the number of messages in the current mailbox - * - * @return int - * @throws ConnectionFailedException - */ - public function countMessages() { - $this->checkConnection(); - return \imap_num_msg($this->connection); - } - - /** - * Gets the number of recent messages in current mailbox - * - * @return int - * @throws ConnectionFailedException - */ - public function countRecentMessages() { - $this->checkConnection(); - return \imap_num_recent($this->connection); - } - - /** - * Returns all IMAP alert messages that have occurred - * - * @return array - */ - public function getAlerts() { - return \imap_alerts(); - } - - /** - * Returns all of the IMAP errors that have occurred - * - * @return array - */ - public function getErrors() { - $this->errors = array_merge($this->errors, \imap_errors() ?: []); - - return $this->errors; - } - - /** - * Gets the last IMAP error that occurred during this page request - * - * @return string - */ - public function getLastError() { - return \imap_last_error(); + return $this->connection->requestAndResponse("QUOTA", [$quota_root]); } /** @@ -636,55 +435,27 @@ public function getLastError() { */ public function expunge() { $this->checkConnection(); - return \imap_expunge($this->connection); - } - - /** - * Check current mailbox - * - * @return object { - * Date [string(37) "Wed, 8 Mar 2017 22:17:54 +0100 (CET)"] current system time formatted according to » RFC2822 - * Driver [string(4) "imap"] protocol used to access this mailbox: POP3, IMAP, NNTP - * Mailbox ["{root@example.com:993/imap/user="root@example.com"}INBOX"] the mailbox name - * Nmsgs [int(1)] number of messages in the mailbox - * Recent [int(0)] number of recent messages in the mailbox - * } - * @throws ConnectionFailedException - */ - public function checkCurrentMailbox() { - $this->checkConnection(); - return \imap_check($this->connection); + return $this->connection->expunge(); } /** * Set the imap timeout for a given operation type - * @param $type * @param $timeout * - * @return mixed - * @throws InvalidImapTimeoutTypeException + * @return Protocol */ - public function setTimeout($type, $timeout) { - if(0 <= $type && $type <= 4) { - return \imap_timeout($type, $timeout); - } - - throw new InvalidImapTimeoutTypeException("Invalid imap timeout type provided."); + public function setTimeout($timeout) { + return $this->connection->setConnectionTimeout($timeout); } /** * Get the timeout for a certain operation * @param $type * - * @return mixed - * @throws InvalidImapTimeoutTypeException + * @return int */ public function getTimeout($type){ - if(0 <= $type && $type <= 4) { - return \imap_timeout($type); - } - - throw new InvalidImapTimeoutTypeException("Invalid imap timeout type provided."); + return $this->connection->getConnectionTimeout(); } /** @@ -732,13 +503,4 @@ public function setDefaultAttachmentMask($mask) { throw new MaskNotFoundException("Unknown mask provided: ".$mask); } - - /** - * Get the current active folder - * - * @return Folder - */ - public function getFolderPath(){ - return $this->active_folder; - } } diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php new file mode 100644 index 00000000..361c48e8 --- /dev/null +++ b/src/Connection/Protocols/ImapProtocol.php @@ -0,0 +1,941 @@ +setCertValidation($cert_validation); + } + + /** + * Public destructor + */ + public function __destruct() { + $this->logout(); + } + + /** + * Open connection to IMAP server + * @param string $host hostname or IP address of IMAP server + * @param int|null $port of IMAP server, default is 143 and 993 for ssl + * @param string|bool $encryption use 'SSL', 'TLS' or false + * + * @throws ConnectionFailedException + */ + public function connect($host, $port = null, $encryption = false) { + $transport = 'tcp'; + + if ($encryption) { + $encryption = strtolower($encryption); + if ($encryption == "ssl") { + $transport = 'ssl'; + $port = $port === null ? 993 : $port; + } + } + $port = $port === null ? 143 : $port; + try { + $this->stream = $this->createStream($transport, $host, $port, $this->connection_timeout); + if (!$this->assumedNextLine('* OK')) { + throw new ConnectionFailedException('connection refused'); + } + if ($encryption == "tls") { + $this->enableTls(); + } + } catch (\Exception $e) { + throw new ConnectionFailedException('connection failed', 0, $e); + } + } + + /** + * Enable tls on the current connection + * + * @throws ConnectionFailedException + * @throws RuntimeException + */ + protected function enableTls(){ + $response = $this->requestAndResponse('STARTTLS'); + $result = $response && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); + if (!$result) { + throw new ConnectionFailedException('failed to enable TLS'); + } + } + + /** + * Get the next line from stream + * + * @return string next line + * @throws RuntimeException + */ + public function nextLine() { + $line = fgets($this->stream); + + if ($line === false) { + throw new RuntimeException('failed to read - connection closed?'); + } + + return $line; + } + + /** + * Get the next line and check if it starts with a given string + * @param string $start + * + * @return bool + * @throws RuntimeException + */ + protected function assumedNextLine($start) { + $line = $this->nextLine(); + return strpos($line, $start) === 0; + } + + /** + * Get the next line and split the tag + * @param string $tag reference tag + * + * @return string next line + * @throws RuntimeException + */ + protected function nextTaggedLine(&$tag) { + $line = $this->nextLine(); + list($tag, $line) = explode(' ', $line, 2); + + return $line; + } + + /** + * Split a given line in values. A value is literal of any form or a list + * @param string $line + * + * @return array + * @throws RuntimeException + */ + protected function decodeLine($line) { + $tokens = []; + $stack = []; + + // replace any trailing including spaces with a single space + $line = rtrim($line) . ' '; + while (($pos = strpos($line, ' ')) !== false) { + $token = substr($line, 0, $pos); + if (!strlen($token)) { + continue; + } + while ($token[0] == '(') { + array_push($stack, $tokens); + $tokens = []; + $token = substr($token, 1); + } + if ($token[0] == '"') { + if (preg_match('%^\(*"((.|\\\\|\\")*?)" *%', $line, $matches)) { + $tokens[] = $matches[1]; + $line = substr($line, strlen($matches[0])); + continue; + } + } + if ($token[0] == '{') { + $endPos = strpos($token, '}'); + $chars = substr($token, 1, $endPos - 1); + if (is_numeric($chars)) { + $token = ''; + while (strlen($token) < $chars) { + $token .= $this->nextLine(); + } + $line = ''; + if (strlen($token) > $chars) { + $line = substr($token, $chars); + $token = substr($token, 0, $chars); + } else { + $line .= $this->nextLine(); + } + $tokens[] = $token; + $line = trim($line) . ' '; + continue; + } + } + if ($stack && $token[strlen($token) - 1] == ')') { + // closing braces are not separated by spaces, so we need to count them + $braces = strlen($token); + $token = rtrim($token, ')'); + // only count braces if more than one + $braces -= strlen($token) + 1; + // only add if token had more than just closing braces + if (rtrim($token) != '') { + $tokens[] = rtrim($token); + } + $token = $tokens; + $tokens = array_pop($stack); + // special handline if more than one closing brace + while ($braces-- > 0) { + $tokens[] = $token; + $token = $tokens; + $tokens = array_pop($stack); + } + } + $tokens[] = $token; + $line = substr($line, $pos + 1); + } + + // maybe the server forgot to send some closing braces + while ($stack) { + $child = $tokens; + $tokens = array_pop($stack); + $tokens[] = $child; + } + + return $tokens; + } + + /** + * Read abd decode a response "line" + * @param array|string $tokens to decode + * @param string $wantedTag targeted tag + * @param bool $dontParse if true only the unparsed line is returned in $tokens + * + * @return bool + * @throws RuntimeException + */ + public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false) { + $line = $this->nextTaggedLine($tag); // get next tag + if (!$dontParse) { + $tokens = $this->decodeLine($line); + } else { + $tokens = $line; + } + + // if tag is wanted tag we might be at the end of a multiline response + return $tag == $wantedTag; + } + + /** + * Read all lines of response until given tag is found + * @param string $tag request tag + * @param bool $dontParse if true every line is returned unparsed instead of the decoded tokens + * + * @return void|null|bool|array tokens if success, false if error, null if bad request + * @throws RuntimeException + */ + public function readResponse($tag, $dontParse = false) { + $lines = []; + $tokens = null; // define $tokens variable before first use + while (!$this->readLine($tokens, $tag, $dontParse)) { + $lines[] = $tokens; + } + + if ($dontParse) { + // last to chars are still needed for response code + $tokens = [substr($tokens, 0, 2)]; + } + if (is_array($lines)){ + if ($this->debug) echo "<< ".json_encode($lines)."\n"; + }else{ + if ($this->debug) echo "<< ".$lines."\n"; + } + // last line has response code + if ($tokens[0] == 'OK') { + return $lines ? $lines : true; + } elseif ($tokens[0] == 'NO') { + return false; + } + return; + } + + /** + * Send a new request + * @param string $command + * @param array $tokens additional parameters to command, use escapeString() to prepare + * @param string $tag provide a tag otherwise an autogenerated is returned + * + * @throws RuntimeException + */ + public function sendRequest($command, $tokens = [], &$tag = null) { + if (!$tag) { + $this->noun++; + $tag = 'TAG' . $this->noun; + } + + $line = $tag . ' ' . $command; + + foreach ($tokens as $token) { + if (is_array($token)) { + if (fwrite($this->stream, $line . ' ' . $token[0] . "\r\n") === false) { + throw new RuntimeException('failed to write - connection closed?'); + } + if (!$this->assumedNextLine('+ ')) { + throw new RuntimeException('failed to send literal string'); + } + $line = $token[1]; + } else { + $line .= ' ' . $token; + } + } + if ($this->debug) echo ">> ".$line."\n"; + + if (fwrite($this->stream, $line . "\r\n") === false) { + throw new RuntimeException('failed to write - connection closed?'); + } + } + + /** + * Send a request and get response at once + * @param string $command + * @param array $tokens parameters as in sendRequest() + * @param bool $dontParse if true unparsed lines are returned instead of tokens + * + * @return void|null|bool|array response as in readResponse() + * @throws RuntimeException + */ + public function requestAndResponse($command, $tokens = [], $dontParse = false) { + $this->sendRequest($command, $tokens, $tag); + + return $this->readResponse($tag, $dontParse); + } + + /** + * Escape one or more literals i.e. for sendRequest + * @param string|array $string the literal/-s + * + * @return string|array escape literals, literals with newline ar returned + * as array('{size}', 'string'); + */ + public function escapeString($string) { + if (func_num_args() < 2) { + if (strpos($string, "\n") !== false) { + return ['{' . strlen($string) . '}', $string]; + } else { + return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $string) . '"'; + } + } + $result = []; + foreach (func_get_args() as $string) { + $result[] = $this->escapeString($string); + } + return $result; + } + + /** + * Escape a list with literals or lists + * @param array $list list with literals or lists as PHP array + * + * @return string escaped list for imap + */ + public function escapeList($list) { + $result = []; + foreach ($list as $v) { + if (!is_array($v)) { + $result[] = $v; + continue; + } + $result[] = $this->escapeList($v); + } + return '(' . implode(' ', $result) . ')'; + } + + /** + * Login to a new session. + * @param string $user username + * @param string $password password + * + * @return bool|mixed + * @throws AuthFailedException + */ + public function login($user, $password) { + try { + return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); + } catch (RuntimeException $e) { + throw new AuthFailedException("failed to authenticate", 0, $e); + } + } + + /** + * Authenticate your current IMAP session. + * @param string $user username + * @param string $token access token + * + * @return bool|mixed + * @throws AuthFailedException + */ + public function authenticate($user, $token) { + try { + $authenticateParams = ['XOAUTH2', base64_encode("user=$user\1auth=Bearer $token\1\1")]; + $this->sendRequest('AUTHENTICATE', $authenticateParams); + + while (true) { + $response = ""; + $is_plus = $this->readLine($response, '+', true); + if ($is_plus) { + // try to log the challenge somewhere where it can be found + error_log("got an extra server challenge: $response"); + // respond with an empty response. + $this->sendRequest(''); + } else { + if (preg_match('/^NO /i', $response) || + preg_match('/^BAD /i', $response)) { + error_log("got failure response: $response"); + return false; + } else if (preg_match("/^OK /i", $response)) { + return true; + } + } + } + } catch (RuntimeException $e) { + throw new AuthFailedException("failed to authenticate", 0, $e); + } + return false; + } + + /** + * Logout of imap server + * + * @return bool success + */ + public function logout() { + $result = false; + if ($this->stream) { + try { + $result = $this->requestAndResponse('LOGOUT', [], true); + } catch (\Exception $e) {} + fclose($this->stream); + $this->stream = null; + } + return $result; + } + + /** + * Check if the current session is connected + * + * @return bool + */ + public function connected(){ + return (boolean) $this->stream; + } + + /** + * Get an array of available capabilities + * + * @return array list of capabilities + * @throws RuntimeException + */ + public function getCapabilities() { + $response = $this->requestAndResponse('CAPABILITY'); + + if (!$response) return []; + + $capabilities = []; + foreach ($response as $line) { + $capabilities = array_merge($capabilities, $line); + } + return $capabilities; + } + + /** + * Examine and select have the same response. + * @param string $command can be 'EXAMINE' or 'SELECT' + * @param string $folder target folder + * + * @return bool|array + * @throws RuntimeException + */ + public function examineOrSelect($command = 'EXAMINE', $folder = 'INBOX') { + $this->sendRequest($command, [$this->escapeString($folder)], $tag); + + $result = []; + $tokens = null; // define $tokens variable before first use + while (!$this->readLine($tokens, $tag)) { + if ($tokens[0] == 'FLAGS') { + array_shift($tokens); + $result['flags'] = $tokens; + continue; + } + switch ($tokens[1]) { + case 'EXISTS': + case 'RECENT': + $result[strtolower($tokens[1])] = $tokens[0]; + break; + case '[UIDVALIDITY': + $result['uidvalidity'] = (int)$tokens[2]; + break; + case '[UIDNEXT': + $result['uidnext'] = (int)$tokens[2]; + break; + default: + // ignore + break; + } + } + + if ($tokens[0] != 'OK') { + return false; + } + return $result; + } + + /** + * Change the current folder + * @param string $folder change to this folder + * + * @return bool|array see examineOrselect() + * @throws RuntimeException + */ + public function selectFolder($folder = 'INBOX') { + return $this->examineOrSelect('SELECT', $folder); + } + + /** + * Examine a given folder + * @param string $folder examine this folder + * + * @return bool|array see examineOrselect() + * @throws RuntimeException + */ + public function examineFolder($folder = 'INBOX') { + return $this->examineOrSelect('EXAMINE', $folder); + } + + /** + * Fetch one or more items of one or more messages + * @param string|array $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] + * @param int|array $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * @param bool $uid set to true if passing a unique id + * + * @return string|array if only one item of one message is fetched it's returned as string + * if items of one message are fetched it's returned as (name => value) + * if one items of messages are fetched it's returned as (msgno => value) + * if items of messages are fetched it's returned as (msgno => (name => value)) + * @throws RuntimeException + */ + public function fetch($items, $from, $to = null, $uid = false) { + if (is_array($from)) { + $set = implode(',', $from); + } elseif ($to === null) { + $set = (int)$from; + } elseif ($to === INF) { + $set = (int)$from . ':*'; + } else { + $set = (int)$from . ':' . (int)$to; + } + + $items = (array)$items; + $itemList = $this->escapeList($items); + + $this->sendRequest(($uid ? 'UID ' : '') . 'FETCH', [$set, $itemList], $tag); + + $result = []; + $tokens = null; // define $tokens variable before first use + while (!$this->readLine($tokens, $tag)) { + // ignore other responses + if ($tokens[1] != 'FETCH') { + continue; + } + + // find array key of UID value; try the last elements, or search for it + if ($uid) { + $count = count($tokens[2]); + if ($tokens[2][$count - 2] == 'UID') { + $uidKey = $count - 1; + } else { + $uidKey = array_search('UID', $tokens[2]) + 1; + } + } + + // ignore other messages + if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { + continue; + } + + // if we only want one item we return that one directly + if (count($items) == 1) { + if ($tokens[2][0] == $items[0]) { + $data = $tokens[2][1]; + } elseif ($uid && $tokens[2][2] == $items[0]) { + $data = $tokens[2][3]; + } else { + // maybe the server send an other field we didn't wanted + $count = count($tokens[2]); + // we start with 2, because 0 was already checked + for ($i = 2; $i < $count; $i += 2) { + if ($tokens[2][$i] != $items[0]) { + continue; + } + $data = $tokens[2][$i + 1]; + break; + } + } + } else { + $data = []; + while (key($tokens[2]) !== null) { + $data[current($tokens[2])] = next($tokens[2]); + next($tokens[2]); + } + } + + // if we want only one message we can ignore everything else and just return + if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { + // we still need to read all lines + while (!$this->readLine($tokens, $tag)) + + return $data; + } + $result[$tokens[0]] = $data; + } + + if ($to === null && !is_array($from)) { + throw new RuntimeException('the single id was not found in response'); + } + + return $result; + } + + /** + * Fetch message headers + * @param array|int $uids + * @param string $rfc + * + * @return array + * @throws RuntimeException + */ + public function content($uids, $rfc = "RFC822") { + return $this->fetch(["$rfc.TEXT"], $uids); + } + + /** + * Fetch message headers + * @param array|int $uids + * @param string $rfc + * + * @return array + * @throws RuntimeException + */ + public function headers($uids, $rfc = "RFC822"){ + return $this->fetch(["$rfc.HEADER"], $uids); + } + + /** + * Fetch message flags + * @param array|int $uids + * + * @return array + * @throws RuntimeException + */ + public function flags($uids){ + return $this->fetch(["FLAGS"], $uids); + } + + /** + * Get uid for a given id + * @param int|null $id message number + * + * @return array|string message number for given message or all messages as array + * @throws RuntimeException + */ + public function getUid($id = null) { + $uids = $this->fetch('http://github.com/UID', 1, INF); + if ($id == null) { + return $uids; + } + + foreach ($uids as $k => $v) { + if ($k == $id) { + return $v; + } + } + + throw new RuntimeException('unique id not found'); + } + + /** + * Get a message number for a uid + * @param string $id uid + * + * @return int message number + * @throws RuntimeException + */ + public function getMessageNumber($id) { + $ids = $this->getUid(); + foreach ($ids as $k => $v) { + if ($v == $id) { + return $k; + } + } + + throw new RuntimeException('unique id not found'); + } + + /** + * Get a list of available folders + * @param string $reference mailbox reference for list + * @param string $folder mailbox name match with wildcards + * + * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) + * @throws RuntimeException + */ + public function folders($reference = '', $folder = '*') { + $result = []; + $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); + if (!$list || $list === true) { + return $result; + } + + foreach ($list as $item) { + if (count($item) != 4 || $item[0] != 'LIST') { + continue; + } + $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]]; + } + + return $result; + } + + /** + * Manage flags + * @param array $flags flags to set, add or remove - see $mode + * @param int $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given + * @param bool $silent if false the return values are the new flags for the wanted messages + * + * @return bool|array new flags if $silent is false, else true or false depending on success + * @throws RuntimeException + */ + public function store(array $flags, $from, $to = null, $mode = null, $silent = true) { + $item = 'FLAGS'; + if ($mode == '+' || $mode == '-') { + $item = $mode . $item; + } + if ($silent) { + $item .= '.SILENT'; + } + + $flags = $this->escapeList($flags); + $set = (int)$from; + if ($to !== null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + $result = $this->requestAndResponse('STORE', [$set, $item, $flags], $silent); + + if ($silent) { + return (bool)$result; + } + + $tokens = $result; + $result = []; + foreach ($tokens as $token) { + if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') { + continue; + } + $result[$token[0]] = $token[2][1]; + } + + return $result; + } + + /** + * Append a new message to given folder + * @param string $folder name of target folder + * @param string $message full message content + * @param array $flags flags for new message + * @param string $date date for new message + * + * @return bool success + * @throws RuntimeException + */ + public function appendMessage($folder, $message, $flags = null, $date = null) { + $tokens = []; + $tokens[] = $this->escapeString($folder); + if ($flags !== null) { + $tokens[] = $this->escapeList($flags); + } + if ($date !== null) { + $tokens[] = $this->escapeString($date); + } + $tokens[] = $this->escapeString($message); + + return $this->requestAndResponse('APPEND', $tokens, true); + } + + /** + * Copy message set from current folder to other folder + * @param string $folder destination folder + * @param $from + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * + * @return bool success + * @throws RuntimeException + */ + public function copyMessage($folder, $from, $to = null) { + $set = (int)$from; + if ($to !== null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + return $this->requestAndResponse('COPY', [$set, $this->escapeString($folder)], true); + } + + /** + * Create a new folder (and parent folders if needed) + * @param string $folder folder name + * + * @return bool success + * @throws RuntimeException + */ + public function createFolder($folder) { + return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); + } + + /** + * Rename an existing folder + * @param string $old old name + * @param string $new new name + * + * @return bool success + * @throws RuntimeException + */ + public function renameFolder($old, $new) { + return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); + } + + /** + * Delete a folder + * @param string $folder folder name + * + * @return bool success + * @throws RuntimeException + */ + public function deleteFolder($folder) { + return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); + } + + /** + * Subscribe to a folder + * @param string $folder folder name + * + * @return bool success + * @throws RuntimeException + */ + public function subscribeFolder($folder) { + return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); + } + + /** + * Unsubscribe from a folder + * @param string $folder folder name + * + * @return bool success + * @throws RuntimeException + */ + public function unsubscribeFolder($folder) { + return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); + } + + /** + * Apply session saved changes to the server + * + * @return bool success + * @throws RuntimeException + */ + public function expunge() { + return $this->requestAndResponse('EXPUNGE'); + } + + /** + * Send noop command + * + * @return bool success + * @throws RuntimeException + */ + public function noop() { + return $this->requestAndResponse('NOOP'); + } + + /** + * Send idle command + * @throws RuntimeException + */ + public function idle() { + $this->sendRequest('IDLE'); + if (!$this->assumedNextLine('+ ')) { + throw new RuntimeException('idle failed'); + } + } + + /** + * Send done command + * @throws RuntimeException + */ + public function done() { + if (fwrite($this->stream, "DONE\r\n") === false) { + throw new RuntimeException('failed to write - connection closed?'); + } + return $this->readResponse("*", false); + } + + /** + * Search for matching messages + * + * @param array $params + * @return array message ids + * @throws RuntimeException + */ + public function search(array $params) { + $response = $this->requestAndResponse('SEARCH', $params); + if (!$response) { + return $response; + } + + foreach ($response as $ids) { + if ($ids[0] == 'SEARCH') { + array_shift($ids); + return $ids; + } + } + return []; + } + + /** + * Enable the debug mode + */ + public function enableDebug(){ + $this->debug = true; + } + + /** + * Disable the debug mode + */ + public function disableDebug(){ + $this->debug = false; + } +} \ No newline at end of file diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php new file mode 100644 index 00000000..5b88ab7c --- /dev/null +++ b/src/Connection/Protocols/Protocol.php @@ -0,0 +1,167 @@ +cert_validation = true; + return $this; + } + + /** + * Disable SSL certificate validation + * @return $this + */ + public function disableCertValidation() { + $this->cert_validation = false; + return $this; + } + + /** + * Set SSL certificate validation + * @var int $cert_validation + * + * @return $this + */ + public function setCertValidation($cert_validation) { + $this->cert_validation = $cert_validation; + return $this; + } + + /** + * Should we validate SSL certificate? + * + * @return bool + */ + public function getCertValidation() { + return $this->cert_validation; + } + + /** + * Prepare socket options + * + * @return array + */ + private function defaultSocketOptions() { + return $this->getCertValidation() + ? [ + 'ssl' => [ + 'verify_peer_name' => false, + 'verify_peer' => false, + ] + ] + : []; + } + + /** + * Create a new resource stream + * @param $transport + * @param string $host hostname or IP address of IMAP server + * @param int $port of IMAP server, default is 143 (993 for ssl) + * @param int $timeout timeout in seconds for initiating session + * + * @return resource|boolean The socket created. + * @throws ConnectionFailedException + * @throws \ErrorException + */ + protected function createStream($transport, $host, $port, $timeout) { + $socket = "$transport://$host:$port"; + $stream = stream_socket_client($socket, $errno, $errstr, $timeout, + STREAM_CLIENT_CONNECT, + stream_context_create($this->defaultSocketOptions()) + ); + + if (!$stream) { + throw new ConnectionFailedException("Failed to connect to host", 0, $error); + } + + if (false === stream_set_timeout($stream, $timeout)) { + throw new ConnectionFailedException('Failed to set stream timeout'); + } + + return $stream; + } + + /** + * @return int + */ + public function getConnectionTimeout() { + return $this->connection_timeout; + } + + /** + * @param int $connection_timeout + * @return Protocol + */ + public function setConnectionTimeout($connection_timeout) { + if ($connection_timeout !== null) { + $this->connection_timeout = $connection_timeout; + } + return $this; + } + +} \ No newline at end of file diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php new file mode 100644 index 00000000..d73e344f --- /dev/null +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -0,0 +1,275 @@ + value) + * if one items of messages are fetched it's returned as (msgno => value) + * if items of messages are fetched it's returned as (msgno => (name => value)) + * @throws RuntimeException + */ + public function fetch($items, $from, $to = null, $uid = false); + + /** + * Fetch message headers + * @param array|int $uids + * @param string $rfc + * + * @return array + * @throws RuntimeException + */ + public function content($uids, $rfc = "RFC822"); + + /** + * Fetch message headers + * @param array|int $uids + * @param string $rfc + * + * @return array + * @throws RuntimeException + */ + public function headers($uids, $rfc = "RFC822"); + + /** + * Fetch message flags + * @param array|int $uids + * + * @return array + * @throws RuntimeException + */ + public function flags($uids); + + /** + * Get uid for a given id + * @param int|null $id message number + * + * @return array|string message number for given message or all messages as array + * @throws RuntimeException + */ + public function getUid($id = null); + + /** + * Get a message number for a uid + * @param string $id uid + * + * @return int message number + * @throws RuntimeException + */ + public function getMessageNumber($id); + + /** + * Get a list of available folders + * + * @param string $reference mailbox reference for list + * @param string $mailbox mailbox name match with wildcards + * @return array mailboxes that matched $mailbox as array(globalName => array('delim' => .., 'flags' => ..)) + * @throws RuntimeException + */ + public function folders($reference = '', $mailbox = '*'); + + /** + * Set message flags + * + * @param array $flags flags to set, add or remove + * @param int $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given + * @param bool $silent if false the return values are the new flags for the wanted messages + * @return bool|array new flags if $silent is false, else true or false depending on success + * @throws RuntimeException + */ + public function store(array $flags, $from, $to = null, $mode = null, $silent = true); + + /** + * Append a new message to given folder + * + * @param string $folder name of target folder + * @param string $message full message content + * @param array $flags flags for new message + * @param string $date date for new message + * @return bool success + * @throws RuntimeException + */ + public function appendMessage($folder, $message, $flags = null, $date = null); + + /** + * Copy message set from current folder to other folder + * + * @param string $folder destination folder + * @param $from + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * @return bool success + * @throws RuntimeException + */ + public function copyMessage($folder, $from, $to = null); + + /** + * Create a new folder + * + * @param string $folder folder name + * @return bool success + * @throws RuntimeException + */ + public function createFolder($folder); + + /** + * Rename an existing folder + * + * @param string $old old name + * @param string $new new name + * @return bool success + * @throws RuntimeException + */ + public function renameFolder($old, $new); + + /** + * Delete a folder + * + * @param string $folder folder name + * @return bool success + * @throws RuntimeException + */ + public function deleteFolder($folder); + + /** + * Subscribe to a folder + * + * @param string $folder folder name + * @return bool success + * @throws RuntimeException + */ + public function subscribeFolder($folder); + + /** + * Apply session saved changes to the server + * + * @return bool success + * @throws RuntimeException + */ + public function expunge(); + + /** + * Send noop command + * + * @return bool success + * @throws RuntimeException + */ + public function noop(); + + /** + * Do a search request + * + * @param array $params + * @return array message ids + * @throws RuntimeException + */ + public function search(array $params); + + /** + * Enable the debug mode + */ + public function enableDebug(); + + /** + * Disable the debug mode + */ + public function disableDebug(); +} \ No newline at end of file diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 79f610e2..bc5cdea9 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -466,16 +466,16 @@ class EncodingAliases { /** * Returns proper encoding mapping, if exsists. If it doesn't, return unchanged $encoding - * * @param string $encoding + * @param string|null $fallback + * * @return string */ - public static function get($encoding) { + public static function get($encoding, $fallback = null) { if (isset(self::$aliases[strtolower($encoding)])) { return self::$aliases[strtolower($encoding)]; - } else { - return $encoding; } + return $fallback !== null ? $fallback : $encoding; } } diff --git a/src/Exceptions/InvalidImapTimeoutTypeException.php b/src/Exceptions/AuthFailedException.php similarity index 54% rename from src/Exceptions/InvalidImapTimeoutTypeException.php rename to src/Exceptions/AuthFailedException.php index 267c322c..6f0d295a 100644 --- a/src/Exceptions/InvalidImapTimeoutTypeException.php +++ b/src/Exceptions/AuthFailedException.php @@ -1,9 +1,9 @@ client = $client; - $this->setDelimiter($structure->delimiter); - $this->path = $structure->name; - $this->full_name = $this->decodeName($structure->name); + $this->setDelimiter($delimiter); + $this->path = $folder_name; + $this->full_name = $this->decodeName($folder_name); $this->name = $this->getSimpleName($this->delimiter, $this->full_name); - $this->parseAttributes($structure->attributes); + $this->parseAttributes($attributes); } /** @@ -178,154 +178,6 @@ public function setChildren($children = []) { return $this; } - /** - * Get a specific message by UID - * - * @param integer $uid Please note that the uid is not unique and can change - * @param integer|null $msglist - * @param integer|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * - * @return Message|null - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\InvalidMessageDateException - */ - public function getMessage($uid, $msglist = null, $fetch_options = null, $fetch_body = false, $fetch_attachment = false, $fetch_flags = true) { - $this->client->openFolder($this->path); - if (\imap_msgno($this->getClient()->getConnection(), $uid) > 0) { - return new Message($uid, $msglist, $this->getClient(), $fetch_options, $fetch_body, $fetch_attachment, $fetch_flags); - } - - return null; - } - - /** - * Get all messages - * - * @param string $criteria - * @param int|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * @param int|null $limit - * @param int $page - * @param string $charset - * - * @return MessageCollection - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\InvalidWhereQueryCriteriaException - * @throws GetMessagesFailedException - */ - public function getMessages($criteria = 'ALL', $fetch_options = null, $fetch_body = true, $fetch_attachment = true, $fetch_flags = true, $limit = null, $page = 1, $charset = "UTF-8") { - - return $this->query($charset)->where($criteria)->setFetchOptions($fetch_options)->setFetchBody($fetch_body) - ->setFetchAttachment($fetch_attachment)->setFetchFlags($fetch_flags) - ->limit($limit, $page)->get(); - } - - /** - * Get all unseen messages - * - * @param string $criteria - * @param int|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * @param int|null $limit - * @param int $page - * @param string $charset - * - * @return MessageCollection - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\InvalidWhereQueryCriteriaException - * @throws GetMessagesFailedException - * @throws MessageSearchValidationException - * - * @deprecated 1.0.5:2.0.0 No longer needed. Use Folder::getMessages('UNSEEN') instead - * @see Folder::getMessages() - */ - public function getUnseenMessages($criteria = 'UNSEEN', $fetch_options = null, $fetch_body = true, $fetch_attachment = true, $fetch_flags = true, $limit = null, $page = 1, $charset = "UTF-8") { - return $this->getMessages($criteria, $fetch_options, $fetch_body, $fetch_attachment, $fetch_flags, $limit, $page, $charset); - } - - /** - * Search messages by a given search criteria - * - * @param array $where Is a two dimensional array where each array represents a criteria set: - * --------------------------------------------------------------------------------------- - * The following sample would search for all messages received from someone@example.com or - * contain the text "Hello world!": - * [['FROM' => 'someone@example.com'],[' TEXT' => 'Hello world!']] - * --------------------------------------------------------------------------------------- - * The following sample would search for all messages received since march 15 2018: - * [['SINCE' => Carbon::parse('15.03.2018')]] - * --------------------------------------------------------------------------------------- - * The following sample would search for all flagged messages: - * [['FLAGGED']] - * --------------------------------------------------------------------------------------- - * @param int|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags - * @param int|null $limit - * @param int $page - * @param string $charset - * - * @return MessageCollection - * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\InvalidWhereQueryCriteriaException - * @throws GetMessagesFailedException - * - * @doc http://php.net/manual/en/function.imap-search.php - * \imap_search() only supports IMAP2 search criterias, because the function mail_criteria() (from c-client lib) - * is used in ext/imap/php_imap.c for parsing the search string. - * IMAP2 search criteria is defined in RFC 1176, section "tag SEARCH search_criteria". - * - * https://tools.ietf.org/html/rfc1176 - INTERACTIVE MAIL ACCESS PROTOCOL - VERSION 2 - * https://tools.ietf.org/html/rfc1064 - INTERACTIVE MAIL ACCESS PROTOCOL - VERSION 2 - * https://tools.ietf.org/html/rfc822 - STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES - * Date and time example from RFC822: - * date-time = [ day "," ] date time ; dd mm yy - * ; hh:mm:ss zzz - * - * day = "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun" - * - * date = 1*2DIGIT month 2DIGIT ; day month year - * ; e.g. 20 Jun 82 - * - * month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" - * - * time = hour zone ; ANSI and Military - * - * hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT] ; 00:00:00 - 23:59:59 - * - * zone = "UT" / "GMT" ; Universal Time - * ; North American : UT - * = "EST" / "EDT" ; Eastern: - 5/ - 4 - * = "CST" / "CDT" ; Central: - 6/ - 5 - * = "MST" / "MDT" ; Mountain: - 7/ - 6 - * = "PST" / "PDT" ; Pacific: - 8/ - 7 - * = 1ALPHA ; Military: Z = UT; - * ; A:-1; (J not used) - * ; M:-12; N:+1; Y:+12 - * / ( ("+" / "-") 4DIGIT ) ; Local differential - * ; hours+min. (HHMM) - * - * @deprecated 1.2.1:2.0.0 No longer needed. Use Folder::query() instead - * @see Folder::query() - */ - public function searchMessages(array $where, $fetch_options = null, $fetch_body = true, $fetch_attachment = true, $fetch_flags = true, $limit = null, $page = 1, $charset = "UTF-8") { - $this->getClient()->checkConnection(); - - return $this->query($charset)->where($where)->setFetchOptions($fetch_options)->setFetchBody($fetch_body) - ->setFetchAttachment($fetch_attachment)->setFetchFlags($fetch_flags) - ->limit($limit, $page)->get(); - - } - /** * Decode name. * It converts UTF7-IMAP encoding to UTF-8. @@ -335,8 +187,7 @@ public function searchMessages(array $where, $fetch_options = null, $fetch_body * @return mixed|string */ protected function decodeName($name) { - preg_match('#\{(.*)\}(.*)#', $name, $preg); - return mb_convert_encoding($preg[2], "UTF-8", "UTF7-IMAP"); + return mb_convert_encoding($name, "UTF-8", "UTF7-IMAP"); } /** @@ -359,88 +210,177 @@ protected function getSimpleName($delimiter, $full_name) { * @param $attributes */ protected function parseAttributes($attributes) { - $this->no_inferiors = ($attributes & LATT_NOINFERIORS) ? true : false; - $this->no_select = ($attributes & LATT_NOSELECT) ? true : false; - $this->marked = ($attributes & LATT_MARKED) ? true : false; - $this->referal = ($attributes & LATT_REFERRAL) ? true : false; - $this->has_children = ($attributes & LATT_HASCHILDREN) ? true : false; + $this->no_inferiors = in_array('\NoInferiors', $attributes) ? true : false; + $this->no_select = in_array('\NoSelect', $attributes) ? true : false; + $this->marked = in_array('\Marked', $attributes) ? true : false; + $this->referal = in_array('\Referal', $attributes) ? true : false; + $this->has_children = in_array('\HasChildren', $attributes) ? true : false; } /** - * Delete the current Mailbox + * Move or rename the current folder + * + * @param string $new_name * @param boolean $expunge * * @return bool - * * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ - public function delete($expunge = true) { - $status = \imap_deletemailbox($this->client->getConnection(), $this->path); + public function move($new_name, $expunge = true) { + $this->client->checkConnection(); + $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name); if($expunge) $this->client->expunge(); return $status; } /** - * Move or Rename the current Mailbox + * Append a string message to the current mailbox * - * @param string $target_mailbox + * @param string $message + * @param string $options + * @param string $internal_date + * + * @return bool + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException + */ + public function appendMessage($message, $options = null, $internal_date = null) { + /** + * Check if $internal_date is parsed. If it is null it should not be set. Otherwise the message can't be stored. + * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a + * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. + */ + + if ($internal_date != null) { + if ($internal_date instanceof Carbon){ + $internal_date = $internal_date->format('d-M-Y H:i:s O'); + } + } + + return $this->client->getConnection()->appendMessage($this->full_name, $message, $options, $internal_date); + } + + /** + * Rename the current folder + * @param string $new_name + * @param boolean $expunge + * + * @return bool + * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException + */ + public function rename($new_name, $expunge = true) { + return $this->move($new_name, $expunge); + } + + /** + * Delete the current folder * @param boolean $expunge * * @return bool * * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ - public function move($target_mailbox, $expunge = true) { - $status = \imap_renamemailbox($this->client->getConnection(), $this->path, $target_mailbox); + public function delete($expunge = true) { + $status = $this->client->getConnection()->deleteFolder($this->path); if($expunge) $this->client->expunge(); return $status; } /** - * Returns status information on a mailbox + * Subscribe the current folder * - * @param integer $options - * SA_MESSAGES - set $status->messages to the number of messages in the mailbox - * SA_RECENT - set $status->recent to the number of recent messages in the mailbox - * SA_UNSEEN - set $status->unseen to the number of unseen (new) messages in the mailbox - * SA_UIDNEXT - set $status->uidnext to the next uid to be used in the mailbox - * SA_UIDVALIDITY - set $status->uidvalidity to a constant that changes when uids for the mailbox may no longer be valid - * SA_ALL - set all of the above + * @return bool * - * @return object * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ - public function getStatus($options) { - return \imap_status($this->client->getConnection(), $this->path, $options); + public function subscribe() { + $this->client->openFolder($this->path); + return $this->client->getConnection()->subscribeFolder($this->path); } /** - * Append a string message to the current mailbox - * - * @param string $message - * @param string $options - * @param string $internal_date + * Unsubscribe the current folder * * @return bool + * * @throws Exceptions\ConnectionFailedException */ - public function appendMessage($message, $options = null, $internal_date = null) { - /** - * Check if $internal_date is parsed. If it is null it should not be set. Otherwise the message can't be stored. - * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a - * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. - */ + public function unsubscribe() { + $this->client->openFolder($this->path); + return $this->client->getConnection()->unsubscribeFolder($this->path); + } - if ($internal_date != null) { - if ($internal_date instanceof Carbon){ - $internal_date = $internal_date->format('d-M-Y H:i:s O'); + /** + * Idle the current connection + * @param callable $callback + * @param integer $timeout max 1740 seconds - recommended by rfc2177 §3 + * + * @throws ConnectionFailedException + * @throws Exceptions\InvalidMessageDateException + * @throws Exceptions\MessageContentFetchingException + * @throws Exceptions\MessageHeaderFetchingException + * @throws Exceptions\RuntimeException + */ + public function idle(callable $callback, $timeout = 1200) { + $this->client->getConnection()->setConnectionTimeout($timeout); + + $this->client->reconnect(); + $this->client->openFolder($this->path); + $connection = $this->client->getConnection(); + $connection->idle(); + + while (true) { + try { + $line = $connection->nextLine(); + if (($pos = strpos($line, "EXISTS")) !== false) { + $msgn = (int) substr($line, 2, $pos -2); + $connection->done(); + + $message = $this->query()->getMessage($msgn); + $callback($message); + + $connection->idle(); + } + }catch (Exceptions\RuntimeException $e) { + if(strpos($e->getMessage(), "connection closed") === false) { + throw $e; + }else{ + $this->client->connect(); + $this->client->openFolder($this->path); + $connection = $this->client->getConnection(); + $connection->idle(); + } } - return \imap_append($this->client->getConnection(), $this->path, $message, $options, $internal_date); } + } - return \imap_append($this->client->getConnection(), $this->path, $message, $options); + /** + * Get folder status information + * + * @return array|bool + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException + */ + public function getStatus() { + return $this->examine(); + } + + /** + * Examine the current folder + * + * @return array + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException + */ + public function examine() { + return $this->client->getConnection()->examineFolder($this->path); } /** diff --git a/src/Header.php b/src/Header.php new file mode 100644 index 00000000..5cab2b67 --- /dev/null +++ b/src/Header.php @@ -0,0 +1,530 @@ +raw = $raw_header; + $this->config = ClientManager::get('options'); + $this->parse(); + } + + /** + * Call dynamic attribute setter and getter methods + * @param string $method + * @param array $arguments + * + * @return mixed + * @throws MethodNotFoundException + */ + public function __call($method, $arguments) { + if(strtolower(substr($method, 0, 3)) === 'get') { + $name = Str::snake(substr($method, 3)); + + if(in_array($name, array_keys($this->attributes))) { + return $this->attributes[$name]; + } + + } + + throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported'); + } + + /** + * @param $name + * + * @return mixed|null + */ + public function __get($name) { + return $this->get($name); + } + + /** + * @param $name + * + * @return mixed|null + */ + public function get($name) { + if(isset($this->attributes[$name])) { + return $this->attributes[$name]; + } + + return null; + } + + /** + * @param $pattern + * + * @return mixed|null + */ + public function find($pattern) { + if (preg_match_all($pattern, $this->raw, $matches)) { + if (isset($matches[1])) { + if(count($matches[1]) > 0) { + return $matches[1][0]; + } + } + } + return null; + } + + /** + * Parse the raw headers + * @throws InvalidMessageDateException + */ + protected function parse(){ + $header = $this->rfc822_parse_headers($this->raw); + + $this->extractAddresses($header); + + if (property_exists($header, 'subject')) { + $this->attributes["subject"] = $this->decode($header->subject); + } + if (property_exists($header, 'references')) { + $this->attributes["references"] = $this->decode($header->references); + } + if (property_exists($header, 'message_id')) { + $this->attributes["message_id"] = str_replace(['<', '>'], '', $header->message_id); + } + + $this->parseDate($header); + foreach ($header as $key => $value) { + $key = trim(rtrim(strtolower($key))); + if(!isset($this->attributes[$key])){ + $this->attributes[$key] = $value; + } + } + + $this->extractHeaderExtensions(); + $this->findPriority(); + } + + /** + * Parse mail headers from a string + * @link https://php.net/manual/en/function.imap-rfc822-parse-headers.php + * @param $raw_headers + * + * @return object + */ + public function rfc822_parse_headers($raw_headers){ + $headers = []; + $imap_headers = []; + if (extension_loaded('imap')) { + $imap_headers = (array) \imap_rfc822_parse_headers($this->raw); + } + + $lines = explode("\r\n", $raw_headers); + $prev_header = null; + foreach($lines as $line) { + if (substr($line, 0, 1) === "\t") { + $line = substr($line, 2); + if ($prev_header !== null) { + $headers[$prev_header][] = $line; + } + }else{ + if (($pos = strpos($line, ":")) > 0) { + $key = strtolower(substr($line, 0, $pos)); + $value = trim(rtrim(strtolower(substr($line, $pos + 1)))); + $headers[$key] = [$value]; + $prev_header = $key; + } + } + } + + foreach($headers as $key => $values) { + if (isset($imap_headers[$key])) continue; + $value = null; + switch($key){ + case 'from': + case 'to': + case 'cc': + case 'bcc': + case 'reply_to': + case 'sender': + $value = $this->decodeAddresses($values); + $headers[$key."address"] = implode(", ", $values); + break; + default: + $value = isset($values[0]) ? $values[0] : $value; + break; + } + $headers[$key] = $value; + } + + return (object) array_merge($headers, $imap_headers); + } + + /** + * Decode MIME header elements + * @link https://php.net/manual/en/function.imap-mime-header-decode.php + * @param string $text The MIME text + * @return array The decoded elements are returned in an array of objects, where each + * object has two properties, charset and text. + */ + public function mime_header_decode($text){ + if (extension_loaded('imap')) { + return \imap_mime_header_decode($text); + } + $charset = $this->getEncoding($text); + return [(object)[ + "charset" => $charset, + "text" => $this->convertEncoding($text, $charset) + ]]; + } + + /** + * Check if a given pair of strings has ben decoded + * @param $encoded + * @param $decoded + * @return bool + */ + private function notDecoded($encoded, $decoded) { + return 0 === strpos($decoded, '=?') + && strlen($decoded) - 2 === strpos($decoded, '?=') + && false !== strpos($encoded, $decoded); + } + + /** + * Convert the encoding + * + * @param $str + * @param string $from + * @param string $to + * + * @return mixed|string + */ + public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { + + $from = EncodingAliases::get($from, $this->fallback_encoding); + $to = EncodingAliases::get($to, $this->fallback_encoding); + + if ($from === $to) { + return $str; + } + + // We don't need to do convertEncoding() if charset is ASCII (us-ascii): + // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded + // https://stackoverflow.com/a/11303410 + // + // us-ascii is the same as ASCII: + // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) + // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and + // based on the typographical symbols predominantly in use there. + // https://en.wikipedia.org/wiki/ASCII + // + // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. + if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') { + return $str; + } + + try { + if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') { + return iconv($from, $to, $str); + } else { + if (!$from) { + return mb_convert_encoding($str, $to); + } + return mb_convert_encoding($str, $to, $from); + } + } catch (\Exception $e) { + if (strstr($from, '-')) { + $from = str_replace('-', '', $from); + return $this->convertEncoding($str, $from, $to); + } else { + return $str; + } + } + } + + /** + * Get the encoding of a given abject + * + * @param object|string $structure + * + * @return string + */ + public function getEncoding($structure) { + if (property_exists($structure, 'parameters')) { + foreach ($structure->parameters as $parameter) { + if (strtolower($parameter->attribute) == "charset") { + return EncodingAliases::get($parameter->value, $this->fallback_encoding); + } + } + }elseif (property_exists($structure, 'charset')) { + return EncodingAliases::get($structure->charset, $this->fallback_encoding); + }elseif (is_string($structure) === true){ + return mb_detect_encoding($structure); + } + + return $this->fallback_encoding; + } + + /** + * Try to decode a specific header + * @param $value + * + * @return string|null + */ + private function decode($value) { + $original_value = $value; + $decoder = $this->config['decoder']['message']; + + if ($value !== null) { + if($decoder === 'utf-8' && extension_loaded('imap')) { + $value = \imap_utf8($value); + if (Str::startsWith(mb_strtolower($value), '=?utf-8?')) { + $value = mb_decode_mimeheader($value); + } + if ($this->notDecoded($original_value, $value)) { + $value = $this->mime_header_decode(imap_utf8($value)); + } + }elseif($decoder === 'iconv') { + $value = iconv_mime_decode($value); + }else{ + $value = mb_decode_mimeheader($value); + } + + if ($this->notDecoded($original_value, $value)) { + $value = $this->convertEncoding($original_value, $this->getEncoding($original_value)); + } + } + + return $value; + } + + /** + * Try to extract the priority from a given raw header string + */ + private function findPriority() { + if(($priority = $this->get("x-priority")) === null) return; + switch($priority){ + case IMAP::MESSAGE_PRIORITY_HIGHEST; + $priority = IMAP::MESSAGE_PRIORITY_HIGHEST; + break; + case IMAP::MESSAGE_PRIORITY_HIGH; + $priority = IMAP::MESSAGE_PRIORITY_HIGH; + break; + case IMAP::MESSAGE_PRIORITY_NORMAL; + $priority = IMAP::MESSAGE_PRIORITY_NORMAL; + break; + case IMAP::MESSAGE_PRIORITY_LOW; + $priority = IMAP::MESSAGE_PRIORITY_LOW; + break; + case IMAP::MESSAGE_PRIORITY_LOWEST; + $priority = IMAP::MESSAGE_PRIORITY_LOWEST; + break; + default: + $priority = IMAP::MESSAGE_PRIORITY_UNKNOWN; + break; + } + + $this->attributes["priority"] = $priority; + } + + /** + * Extract a given part as address array from a given header + * @param $values + * + * @return array + */ + private function decodeAddresses($values) { + $addresses = []; + foreach($values as $address) { + if (preg_match( + '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', + $address, + $matches + )){ + $name = trim(rtrim($matches["name"])); + $email = trim(rtrim($matches["email"])); + list($mailbox, $host) = explode("@", $email); + $addresses[] = (object) [ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ]; + } + } + return $addresses; + } + + /** + * Extract a given part as address array from a given header + * @param object $header + */ + private function extractAddresses($header) { + foreach(['from', 'to', 'cc', 'bcc', 'reply_to', 'sender', 'in_reply_to'] as $key){ + if (property_exists($header, $key)) { + $this->attributes[$key] = $this->parseAddresses($header->$key); + } + } + } + + /** + * Parse Addresses + * @param $list + * + * @return array + */ + private function parseAddresses($list) { + $addresses = []; + + foreach ($list as $item) { + $address = (object) $item; + + if (!property_exists($address, 'mailbox')) { + $address->mailbox = false; + } + if (!property_exists($address, 'host')) { + $address->host = false; + } + if (!property_exists($address, 'personal')) { + $address->personal = false; + } else { + $personalParts = $this->mime_header_decode($address->personal); + + if(is_array($personalParts)) { + $address->personal = ''; + foreach ($personalParts as $p) { + $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); + } + } + } + + $address->mail = ($address->mailbox && $address->host) ? $address->mailbox.'@'.$address->host : false; + $address->full = ($address->personal) ? $address->personal.' <'.$address->mail.'>' : $address->mail; + + $addresses[] = $address; + } + + return $addresses; + } + + private function extractHeaderExtensions(){ + foreach ($this->attributes as $key => $value) { + if (is_string($value) === true) { + if (($pos = strpos($value, ";")) !== false){ + $original = substr($value, 0, $pos); + $this->attributes[$key] = trim(rtrim($original)); + $extensions = explode(";", substr($value, $pos + 1)); + foreach($extensions as $extension) { + if (($pos = strpos($extension, "=")) !== false){ + $key = substr($extension, 0, $pos); + $value = substr($extension, $pos + 1); + $this->attributes[trim(rtrim(strtolower($key)))] = trim(rtrim($value)); + } + } + } + } + } + } + + /** + * Exception handling for invalid dates + * + * Currently known invalid formats: + * ^ Datetime ^ Problem ^ Cause + * | Mon, 20 Nov 2017 20:31:31 +0800 (GMT+8:00) | Double timezone specification | A Windows feature + * | Thu, 8 Nov 2018 08:54:58 -0200 (-02) | + * | | and invalid timezone (max 6 char) | + * | 04 Jan 2018 10:12:47 UT | Missing letter "C" | Unknown + * | Thu, 31 May 2018 18:15:00 +0800 (added by) | Non-standard details added by the | Unknown + * | | mail server | + * | Sat, 31 Aug 2013 20:08:23 +0580 | Invalid timezone | PHPMailer bug https://sourceforge.net/p/phpmailer/mailman/message/6132703/ + * + * Please report any new invalid timestamps to [#45](https://github.com/Webklex/php-imap/issues) + * + * @param object $header + * + * @return Carbon|null + * @throws InvalidMessageDateException + */ + private function parseDate($header) { + + if (property_exists($header, 'date')) { + $parsed_date = null; + $date = $header->date; + + if(preg_match('/\+0580/', $date)) { + $date = str_replace('+0580', '+0530', $date); + } + + $date = trim(rtrim($date)); + try { + $parsed_date = Carbon::parse($date); + } catch (\Exception $e) { + switch (true) { + case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: + $date .= 'C'; + break; + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}\, \ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: + case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{2,4}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}\ [A-Z]{2}\ \-[0-9]{2}\:[0-9]{2}\ \([A-Z]{2,3}\ \-[0-9]{2}:[0-9]{2}\))+$/i', $date) > 0: + $array = explode('(', $date); + $array = array_reverse($array); + $date = trim(array_pop($array)); + break; + } + try{ + $parsed_date = Carbon::parse($date); + } catch (\Exception $_e) { + throw new InvalidMessageDateException("Invalid message date. ID:".$this->get("message_id"), 1100, $e); + } + } + + $this->attributes["date"] = $parsed_date; + } + } + +} \ No newline at end of file diff --git a/src/Message.php b/src/Message.php index 4c1f048e..09a356fe 100755 --- a/src/Message.php +++ b/src/Message.php @@ -15,6 +15,8 @@ use Carbon\Carbon; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; use Webklex\PHPIMAP\Support\AttachmentCollection; use Webklex\PHPIMAP\Support\FlagCollection; @@ -29,7 +31,6 @@ * @property integer msglist * @property integer uid * @property integer msgn - * @property integer priority * @property string subject * @property string message_id * @property string message_no @@ -96,19 +97,7 @@ class Message { /** @var array $attributes */ protected $attributes = [ - 'message_id' => '', 'message_no' => null, - 'subject' => '', - 'references' => null, - 'date' => null, - 'from' => [], - 'to' => [], - 'cc' => [], - 'bcc' => [], - 'reply_to' => [], - 'in_reply_to' => '', - 'sender' => [], - 'priority' => 0, ]; /** @@ -132,13 +121,6 @@ class Message { */ public $fetch_body = null; - /** - * Fetch attachments options - * - * @var bool - */ - public $fetch_attachment = null; - /** * Fetch flags options * @@ -147,19 +129,14 @@ class Message { public $fetch_flags = null; /** - * @var string $header + * @var Header $header */ public $header = null; - /** - * @var null|object $header_info - */ - public $header_info = null; - /** @var null|string $raw_body */ public $raw_body = null; - /** @var null $structure */ + /** @var Structure $structure */ protected $structure = null; /** @@ -183,18 +160,20 @@ class Message { /** * Message constructor. * - * @param integer $uid - * @param integer|null $msglist - * @param Client $client - * @param integer|null $fetch_options - * @param boolean $fetch_body - * @param boolean $fetch_attachment - * @param boolean $fetch_flags + * @param integer $msgn + * @param integer|null $msglist + * @param Client $client + * @param integer|null $fetch_options + * @param boolean $fetch_body + * @param boolean $fetch_flags * * @throws Exceptions\ConnectionFailedException * @throws InvalidMessageDateException + * @throws Exceptions\RuntimeException + * @throws MessageHeaderFetchingException + * @throws MessageContentFetchingException */ - public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_attachment = false, $fetch_flags = false) { + public function __construct($msgn, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false) { $default_mask = $client->getDefaultMessageMask(); if($default_mask != null) { @@ -207,17 +186,18 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul $this->setFetchOption($fetch_options); $this->setFetchBodyOption($fetch_body); - $this->setFetchAttachmentOption($fetch_attachment); $this->setFetchFlagsOption($fetch_flags); $this->attachments = AttachmentCollection::make([]); $this->flags = FlagCollection::make([]); - $this->msglist = $msglist; $this->client = $client; + $this->client->openFolder($this->folder_path); + + $this->msgn = $msgn; + $this->msglist = $msglist; - $this->uid = ($this->fetch_options == IMAP::FT_UID) ? $uid : $uid; - $this->msgn = ($this->fetch_options == IMAP::FT_UID) ? \imap_msgno($this->client->getConnection(), $uid) : $uid; + $this->uid = $this->client->getConnection()->getUid($this->msgn); $this->parseHeader(); @@ -241,11 +221,7 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul public function __call($method, $arguments) { if(strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); - - if(in_array($name, array_keys($this->attributes))) { - return $this->attributes[$name]; - } - + return $this->get($name); }elseif (strtolower(substr($method, 0, 3)) === 'set') { $name = Str::snake(substr($method, 3)); @@ -278,39 +254,20 @@ public function __set($name, $value) { * @return mixed|null */ public function __get($name) { - if(isset($this->attributes[$name])) { - return $this->attributes[$name]; - } - - return null; + return $this->get($name); } /** - * Copy the current Messages to a mailbox - * - * @param $mailbox - * @param int $options + * @param $name * - * @return bool - * @throws Exceptions\ConnectionFailedException + * @return mixed|null */ - public function copy($mailbox, $options = 0) { - $this->client->openFolder($this->folder_path); - return \imap_mail_copy($this->client->getConnection(), $this->uid, $mailbox, IMAP::CP_UID); - } + public function get($name) { + if(isset($this->attributes[$name])) { + return $this->attributes[$name]; + } - /** - * Move the current Messages to a mailbox - * - * @param $mailbox - * @param int $options - * - * @return bool - * @throws Exceptions\ConnectionFailedException - */ - public function move($mailbox, $options = 0) { - $this->client->openFolder($this->folder_path); - return \imap_mail_move($this->client->getConnection(), $this->uid, $mailbox, IMAP::CP_UID); + return $this->header->get($name); } /** @@ -346,179 +303,32 @@ public function hasHTMLBody() { /** * Get the Message html body - * If $replaceImages is callable it should expect string $body as first parameter, $oAttachment as second and return - * the resulting $body. - * - * @var bool|callable $replaceImages * * @return string|null - * - * @deprecated 1.4.0:2.0.0 No longer needed. Use AttachmentMask::getImageSrc() instead */ - public function getHTMLBody($replaceImages = false) { + public function getHTMLBody() { if (!isset($this->bodies['html'])) { return null; } - $body = $this->bodies['html']->content; - if ($replaceImages !== false) { - $this->attachments->each(function($oAttachment) use(&$body, $replaceImages) { - /** @var Attachment $oAttachment */ - if(is_callable($replaceImages)) { - $body = $replaceImages($body, $oAttachment); - }elseif(is_string($replaceImages)) { - call_user_func($replaceImages, [$body, $oAttachment]); - }else{ - if ($oAttachment->id && $oAttachment->getImgSrc() != null) { - $body = str_replace('cid:'.$oAttachment->id, $oAttachment->getImgSrc(), $body); - } - } - }); - } - - return $body; + return $this->bodies['html']->content; } /** * Parse all defined headers * - * @return void * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException * @throws InvalidMessageDateException + * @throws MessageHeaderFetchingException */ private function parseHeader() { - $this->client->openFolder($this->folder_path); - $this->header = $header = \imap_fetchheader($this->client->getConnection(), $this->uid, IMAP::FT_UID); - - $this->priority = $this->extractPriority($this->header); - - if ($this->header) { - $header = \imap_rfc822_parse_headers($this->header); - } - - if (property_exists($header, 'subject')) { - if($this->config['decoder']['message']['subject'] === 'utf-8') { - $this->subject = \imap_utf8($header->subject); - }else{ - $this->subject = mb_decode_mimeheader($header->subject); - } - } - - foreach(['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $part){ - $this->extractHeaderAddressPart($header, $part); + $headers = $this->client->getConnection()->headers([$this->msgn]); + if (!isset($headers[$this->msgn])) { + throw new MessageHeaderFetchingException("no headers found", 0); } - if (property_exists($header, 'references')) { - $this->references = $header->references; - } - if (property_exists($header, 'in_reply_to')) { - $this->in_reply_to = str_replace(['<', '>'], '', $header->in_reply_to); - } - if (property_exists($header, 'message_id')) { - $this->message_id = str_replace(['<', '>'], '', $header->message_id); - } - if (property_exists($header, 'Msgno')) { - $messageNo = (int) trim($header->Msgno); - $this->message_no = ($this->fetch_options == IMAP::FT_UID) ? $messageNo : \imap_msgno($this->client->getConnection(), $messageNo); - } else { - $this->message_no = \imap_msgno($this->client->getConnection(), $this->getUid()); - } - - $this->date = $this->parseDate($header); - } - - /** - * Try to extract the priority from a given raw header string - * @param string $header - * - * @return int|null - */ - private function extractPriority($header) { - if(preg_match('/x\-priority\:.*([0-9]{1,2})/i', $header, $priority)){ - $priority = isset($priority[1]) ? (int) $priority[1] : 0; - switch($priority){ - case IMAP::MESSAGE_PRIORITY_HIGHEST; - $priority = IMAP::MESSAGE_PRIORITY_HIGHEST; - break; - case IMAP::MESSAGE_PRIORITY_HIGH; - $priority = IMAP::MESSAGE_PRIORITY_HIGH; - break; - case IMAP::MESSAGE_PRIORITY_NORMAL; - $priority = IMAP::MESSAGE_PRIORITY_NORMAL; - break; - case IMAP::MESSAGE_PRIORITY_LOW; - $priority = IMAP::MESSAGE_PRIORITY_LOW; - break; - case IMAP::MESSAGE_PRIORITY_LOWEST; - $priority = IMAP::MESSAGE_PRIORITY_LOWEST; - break; - default: - $priority = IMAP::MESSAGE_PRIORITY_UNKNOWN; - break; - } - } - - return $priority; - } - - /** - * Exception handling for invalid dates - * - * Currently known invalid formats: - * ^ Datetime ^ Problem ^ Cause - * | Mon, 20 Nov 2017 20:31:31 +0800 (GMT+8:00) | Double timezone specification | A Windows feature - * | Thu, 8 Nov 2018 08:54:58 -0200 (-02) | - * | | and invalid timezone (max 6 char) | - * | 04 Jan 2018 10:12:47 UT | Missing letter "C" | Unknown - * | Thu, 31 May 2018 18:15:00 +0800 (added by) | Non-standard details added by the | Unknown - * | | mail server | - * | Sat, 31 Aug 2013 20:08:23 +0580 | Invalid timezone | PHPMailer bug https://sourceforge.net/p/phpmailer/mailman/message/6132703/ - * - * Please report any new invalid timestamps to [#45](https://github.com/Webklex/laravel-imap/issues/45) - * - * @param object $header - * - * @return Carbon|null - * @throws InvalidMessageDateException - */ - private function parseDate($header) { - $parsed_date = null; - - if (property_exists($header, 'date')) { - $date = $header->date; - - if(preg_match('/\+0580/', $date)) { - $date = str_replace('+0580', '+0530', $date); - } - - $date = trim(rtrim($date)); - try { - $parsed_date = Carbon::parse($date); - } catch (\Exception $e) { - switch (true) { - case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: - $date .= 'C'; - break; - case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}\, \ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: - case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{2,4}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}\ [A-Z]{2}\ \-[0-9]{2}\:[0-9]{2}\ \([A-Z]{2,3}\ \-[0-9]{2}:[0-9]{2}\))+$/i', $date) > 0: - $array = explode('(', $date); - $array = array_reverse($array); - $date = trim(array_pop($array)); - break; - } - try{ - $parsed_date = Carbon::parse($date); - } catch (\Exception $_e) { - throw new InvalidMessageDateException("Invalid message date. ID:".$this->getMessageId(), 1000, $e); - } - } - } - - return $parsed_date; + $this->header = new Header($headers[$this->msgn]); } /** @@ -526,104 +336,25 @@ private function parseDate($header) { * * @return void * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ private function parseFlags() { - $this->flags = FlagCollection::make([]); - $this->client->openFolder($this->folder_path); - $flags = \imap_fetch_overview($this->client->getConnection(), $this->uid, IMAP::FT_UID); - if (is_array($flags) && isset($flags[0])) { - foreach($this->available_flags as $flag) { - $this->parseFlag($flags, $flag); - } - } - } - - /** - * Extract a possible flag information from a given array - * @param array $flags - * @param string $flag - */ - private function parseFlag($flags, $flag) { - $flag = strtolower($flag); - - if (property_exists($flags[0], strtoupper($flag))) { - $this->flags->put($flag, $flags[0]->{strtoupper($flag)}); - } elseif (property_exists($flags[0], ucfirst($flag))) { - $this->flags->put($flag, $flags[0]->{ucfirst($flag)}); - } elseif (property_exists($flags[0], $flag)) { - $this->flags->put($flag, $flags[0]->$flag); - } - } - - /** - * Get the current Message header info - * - * @return object - * @throws Exceptions\ConnectionFailedException - */ - public function getHeaderInfo() { - if ($this->header_info == null) { - $this->client->openFolder($this->folder_path); - $this->header_info = \imap_headerinfo($this->client->getConnection(), $this->getMessageNo()); - } - - return $this->header_info; - } - - /** - * Extract a given part as address array from a given header - * @param object $header - * @param string $part - */ - private function extractHeaderAddressPart($header, $part) { - if (property_exists($header, $part)) { - $this->$part = $this->parseAddresses($header->$part); - } - } - - /** - * Parse Addresses - * @param $list - * - * @return array - */ - private function parseAddresses($list) { - $addresses = []; + $this->flags = FlagCollection::make([]); - foreach ($list as $item) { - $address = (object) $item; + $flags = $this->client->getConnection()->flags([$this->msgn]); - if (!property_exists($address, 'mailbox')) { - $address->mailbox = false; - } - if (!property_exists($address, 'host')) { - $address->host = false; - } - if (!property_exists($address, 'personal')) { - $address->personal = false; - } - if (!property_exists($address, 'personal')) { - $address->personal = false; - } else { - $personalParts = \imap_mime_header_decode($address->personal); - - if(is_array($personalParts)) { - $address->personal = ''; - foreach ($personalParts as $p) { - $encoding = $this->getEncoding($p->text); - $address->personal .= $this->convertEncoding($p->text, $encoding); - } + if (isset($flags[$this->msgn])) { + foreach($flags[$this->msgn] as $flag) { + if (strpos($flag, "\\") === 0){ + $flag = substr($flag, 1); + } + $flag_key = strtolower($flag); + if (in_array($flag_key, $this->available_flags)) { + $this->flags->put($flag_key, $flag); } } - - $address->mail = ($address->mailbox && $address->host) ? $address->mailbox.'@'.$address->host : false; - $address->full = ($address->personal) ? $address->personal.' <'.$address->mail.'>' : $address->mail; - - $addresses[] = $address; } - - return $addresses; } /** @@ -631,27 +362,20 @@ private function parseAddresses($list) { * * @return $this * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\MessageContentFetchingException + * @throws InvalidMessageDateException + * @throws Exceptions\RuntimeException */ public function parseBody() { $this->client->openFolder($this->folder_path); - $this->structure = \imap_fetchstructure($this->client->getConnection(), $this->uid, IMAP::FT_UID); - - if(property_exists($this->structure, 'parts')){ - $parts = $this->structure->parts; - foreach ($parts as $part) { - foreach ($part->parameters as $parameter) { - if($parameter->attribute == "charset") { - $encoding = $parameter->value; - - $encoding = preg_replace('/Content-Transfer-Encoding/', '', $encoding); - $encoding = preg_replace('/iso-8859-8-i/', 'iso-8859-8', $encoding); - - $parameter->value = $encoding; - } - } - } + $contents = $this->client->getConnection()->content([$this->msgn]); + if (!isset($contents[$this->msgn])) { + throw new MessageContentFetchingException("no content found", 0); } + $content = $contents[$this->msgn]; + + $this->structure = new Structure($content, $this->header); $this->fetchStructure($this->structure); @@ -666,23 +390,29 @@ public function parseBody() { * * @throws Exceptions\ConnectionFailedException */ - private function fetchStructure($structure, $partNumber = null) { + private function fetchStructure($structure) { $this->client->openFolder($this->folder_path); - if ($structure->type == IMAP::MESSAGE_TYPE_TEXT && - ($structure->ifdisposition == 0 || - (empty($structure->disposition) || strtolower($structure->disposition) != 'attachment') + foreach ($structure->parts as $part) { + $this->fetchPart($part); + } + } + + /** + * @param Part $part + * @throws Exceptions\ConnectionFailedException + */ + private function fetchPart(Part $part) { + + if ($part->type == IMAP::MESSAGE_TYPE_TEXT && + ($part->ifdisposition == 0 || + (empty($part->disposition) || strtolower($part->disposition) != 'attachment') ) ) { - if (strtolower($structure->subtype) == "plain" || strtolower($structure->subtype) == "csv") { - if (!$partNumber) { - $partNumber = 1; - } - - $encoding = $this->getEncoding($structure); + if (strtolower($part->subtype) == "plain" || strtolower($part->subtype) == "csv") { + $encoding = $this->getEncoding($part); - $content = \imap_fetchbody($this->client->getConnection(), $this->uid, $partNumber, $this->fetch_options | IMAP::FT_UID); - $content = $this->decodeString($content, $structure->encoding); + $content = $this->decodeString($part->content, $part->encoding); // We don't need to do convertEncoding() if charset is ASCII (us-ascii): // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded @@ -699,63 +429,37 @@ private function fetchStructure($structure, $partNumber = null) { $content = $this->convertEncoding($content, $encoding); } - $body = new \stdClass; - $body->type = "text"; - $body->content = $content; - - $this->bodies['text'] = $body; - - $this->fetchAttachment($structure, $partNumber); + $this->bodies['text'] = $content; - } elseif (strtolower($structure->subtype) == "html") { - if (!$partNumber) { - $partNumber = 1; - } + $this->fetchAttachment($part); - $encoding = $this->getEncoding($structure); + } elseif (strtolower($part->subtype) == "html") { + $encoding = $this->getEncoding($part); - $content = \imap_fetchbody($this->client->getConnection(), $this->uid, $partNumber, $this->fetch_options | IMAP::FT_UID); - $content = $this->decodeString($content, $structure->encoding); + $content = $this->decodeString($part->content, $part->encoding); if ($encoding != 'us-ascii') { $content = $this->convertEncoding($content, $encoding); } - $body = new \stdClass; - $body->type = "html"; - $body->content = $content; - - $this->bodies['html'] = $body; - } elseif ($structure->ifdisposition == 1 && strtolower($structure->disposition) == 'attachment') { - if ($this->getFetchAttachmentOption() === true) { - $this->fetchAttachment($structure, $partNumber); - } - } - } elseif ($structure->type == IMAP::MESSAGE_TYPE_MULTIPART) { - foreach ($structure->parts as $index => $subStruct) { - $prefix = ""; - if ($partNumber) { - $prefix = $partNumber."."; - } - $this->fetchStructure($subStruct, $prefix.($index + 1)); + $this->bodies['html'] = $content; + } elseif ($part->ifdisposition == 1 && strtolower($part->disposition) == 'attachment') { + $this->fetchAttachment($part); } } else { - if ($this->getFetchAttachmentOption() === true) { - $this->fetchAttachment($structure, $partNumber); - } + $this->fetchAttachment($part); } } /** * Fetch the Message attachment * - * @param object $structure - * @param mixed $partNumber + * @param Part $part * * @throws Exceptions\ConnectionFailedException */ - protected function fetchAttachment($structure, $partNumber) { + protected function fetchAttachment($part) { - $oAttachment = new Attachment($this, $structure, $partNumber); + $oAttachment = new Attachment($this, $part); if ($oAttachment->getName() !== null) { if ($oAttachment->getId() !== null) { @@ -802,24 +506,6 @@ public function setFetchBodyOption($option) { return $this; } - /** - * Fail proof setter for $fetch_attachment - * - * @param $option - * - * @return $this - */ - public function setFetchAttachmentOption($option) { - if (is_bool($option)) { - $this->fetch_attachment = $option; - } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_attachment', true); - $this->fetch_attachment = is_bool($config) ? $config : true; - } - - return $this; - } - /** * Fail proof setter for $fetch_flags * @@ -848,18 +534,18 @@ public function setFetchFlagsOption($option) { */ public function decodeString($string, $encoding) { switch ($encoding) { - case IMAP::MESSAGE_ENC_7BIT: - return $string; - case IMAP::MESSAGE_ENC_8BIT: - return quoted_printable_decode(\imap_8bit($string)); case IMAP::MESSAGE_ENC_BINARY: - return \imap_binary($string); + if (extension_loaded('imap')) { + return base64_decode(\imap_binary($string)); + } + return base64_decode($string); case IMAP::MESSAGE_ENC_BASE64: - return \imap_base64($string); + return base64_decode($string); + case IMAP::MESSAGE_ENC_8BIT: case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE: return quoted_printable_decode($string); + case IMAP::MESSAGE_ENC_7BIT: case IMAP::MESSAGE_ENC_OTHER: - return $string; default: return $string; } @@ -922,6 +608,8 @@ public function getEncoding($structure) { return EncodingAliases::get($parameter->value); } } + }elseif (property_exists($structure, 'charset')){ + return EncodingAliases::get($structure->charset); }elseif (is_string($structure) === true){ return mb_detect_encoding($structure); } @@ -930,93 +618,76 @@ public function getEncoding($structure) { } /** - * Find the folder containing this message. - * @param null|Folder $folder where to start searching from (top-level inbox by default) + * Get the messages folder * - * @return mixed|null|Folder + * @return mixed * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\MailboxFetchingException - * @throws InvalidMessageDateException - * @throws MaskNotFoundException + * @throws Exceptions\FolderFetchingException */ - public function getContainingFolder(Folder $folder = null) { - $folder = $folder ?: $this->client->getFolders()->first(); - $this->client->checkConnection(); - - // Try finding the message by uid in the current folder - $client = new Client; - $client->openFolder($folder->path); - $uidMatches = \imap_fetch_overview($client->getConnection(), $this->uid, IMAP::FT_UID); - $uidMatch = count($uidMatches) - ? new Message($uidMatches[0]->uid, $uidMatches[0]->msgno, $client) - : null; - $client->disconnect(); - - // \imap_fetch_overview() on a parent folder will return the matching message - // even when the message is in a child folder so we need to recursively - // search the children - foreach ($folder->children as $child) { - $childFolder = $this->getContainingFolder($child); - - if ($childFolder) { - return $childFolder; - } - } - - // before returning the parent - if ($this->is($uidMatch)) { - return $folder; - } - - // or signalling that the message was not found in any folder - return null; - } - public function getFolder(){ return $this->client->getFolder($this->folder_path); } /** - * Move the Message into an other Folder - * @param string $mailbox - * @param bool $expunge - * @param bool $create_folder + * Copy the current Messages to a mailbox + * @param string $folder * * @return null|Message * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\FolderFetchingException + * @throws Exceptions\RuntimeException * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageHeaderFetchingException */ - public function moveToFolder($mailbox = 'INBOX', $expunge = false, $create_folder = true) { - - if($create_folder) $this->client->createFolder($mailbox, true); - - $target_folder = $this->client->getFolder($mailbox); - $target_status = $target_folder->getStatus(IMAP::SA_ALL); - + public function copy($folder) { $this->client->openFolder($this->folder_path); - $status = \imap_mail_move($this->client->getConnection(), $this->uid, $mailbox, IMAP::CP_UID); - - if($status === true){ - if($expunge) $this->client->expunge(); - $this->client->openFolder($target_folder->path); - - return $target_folder->getMessage($target_status->uidnext, null, $this->fetch_options, $this->fetch_body, $this->fetch_attachment, $this->fetch_flags); + $status = $this->client->getConnection()->examineFolder($folder); + /** @var Folder $folder */ + $folder = $this->client->getFolder($folder); + if (isset($status["uidnext"]) && $folder !== null) { + $next_uid = $status["uidnext"]; + if ($this->client->getConnection()->copyMessage($folder->path, $this->msgn) == true) { + $this->client->openFolder($folder->path); + $message_num = $this->client->getConnection()->getMessageNumber($next_uid); + return $folder->query()->getMessage($message_num); + } } return null; } + /** + * Move the current Messages to a mailbox + * @param $folder + * @param boolean $expunge + * + * @return Message|null + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\FolderFetchingException + * @throws Exceptions\RuntimeException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageHeaderFetchingException + */ + public function move($folder, $expunge = false) { + $status = $this->copy($folder); + if ($status !== null) { + $status = $this->delete($expunge); + } + return $status; + } + /** * Delete the current Message * @param bool $expunge * * @return bool * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function delete($expunge = true) { - $this->client->openFolder($this->folder_path); - - $status = \imap_delete($this->client->getConnection(), $this->uid, IMAP::FT_UID); + $status = $this->setFlag("Deleted"); if($expunge) $this->client->expunge(); return $status; @@ -1028,46 +699,27 @@ public function delete($expunge = true) { * * @return bool * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function restore($expunge = true) { - $this->client->openFolder($this->folder_path); - - $status = \imap_undelete($this->client->getConnection(), $this->uid, IMAP::FT_UID); + $status = $this->unsetFlag("Deleted"); if($expunge) $this->client->expunge(); return $status; } - /** - * Get all message attachments. - * - * @return AttachmentCollection - */ - public function getAttachments() { - return $this->attachments; - } - - /** - * Checks if there are any attachments present - * - * @return boolean - */ - public function hasAttachments() { - return $this->attachments->isEmpty() === false; - } - /** * Set a given flag * @param string|array $flag * * @return bool * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function setFlag($flag) { $this->client->openFolder($this->folder_path); - $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); - $status = \imap_setflag_full($this->client->getConnection(), $this->getUid(), $flag, SE_UID); + $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "+"); $this->parseFlags(); return $status; @@ -1079,17 +731,36 @@ public function setFlag($flag) { * * @return bool * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function unsetFlag($flag) { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); - $status = \imap_clearflag_full($this->client->getConnection(), $this->getUid(), $flag, SE_UID); + $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "-"); $this->parseFlags(); return $status; } + /** + * Get all message attachments. + * + * @return AttachmentCollection + */ + public function getAttachments() { + return $this->attachments; + } + + /** + * Checks if there are any attachments present + * + * @return boolean + */ + public function hasAttachments() { + return $this->attachments->isEmpty() === false; + } + /** * @return null|object|string * @throws Exceptions\ConnectionFailedException @@ -1098,14 +769,14 @@ public function getRawBody() { if ($this->raw_body === null) { $this->client->openFolder($this->folder_path); - $this->raw_body = \imap_fetchbody($this->client->getConnection(), $this->getUid(), '', $this->fetch_options | IMAP::FT_UID); + $this->raw_body = $this->structure->raw; } return $this->raw_body; } /** - * @return string + * @return Header */ public function getHeader() { return $this->header; @@ -1132,13 +803,6 @@ public function getFetchBodyOption() { return $this->fetch_body; } - /** - * @return boolean - */ - public function getFetchAttachmentOption() { - return $this->fetch_attachment; - } - /** * @return boolean */ diff --git a/src/Part.php b/src/Part.php new file mode 100644 index 00000000..7c68bf30 --- /dev/null +++ b/src/Part.php @@ -0,0 +1,193 @@ +raw = $raw_part; + $this->parse(); + } + + /** + * Parse the raw parts + * @throws InvalidMessageDateException + */ + protected function parse(){ + $headers = ""; + $body = $this->raw; + + if (preg_match_all("/(.*)(?s)(?-m)\\r\\n.+?(?=\n([^A-Z]|.{1,2}[^A-Z])|$)/im", $body, $matches)) { + if (isset($matches[0][0])) { + $headers = $matches[0][0]; + $body = substr($body, strlen($headers) + 2, -2); + } + } + + $this->header = new Header($headers); + $this->parseSubtype(); + $this->parseDisposition(); + $this->parseDescription(); + $this->parseEncoding(); + + $this->charset = $this->header->get("charset"); + $this->name = $this->header->get("name"); + $this->filename = $this->header->get("filename"); + $this->id = $this->header->get("id"); + + $this->content = trim(rtrim($body)); + $this->bytes = strlen($this->content); + } + + private function parseSubtype(){ + $content_type = $this->header->get("content-type"); + if (($pos = strpos($content_type, "/")) !== false){ + $this->subtype = substr($content_type, $pos + 1); + } + } + + private function parseDisposition(){ + $content_disposition = $this->header->get("content-disposition"); + if($content_disposition !== null) { + $this->ifdisposition = true; + $this->disposition = $content_disposition; + } + } + + private function parseDescription(){ + $content_description = $this->header->get("content-description"); + if($content_description !== null) { + $this->ifdescription = true; + $this->description = $content_description; + } + } + + private function parseEncoding(){ + $encoding = $this->header->get("content-transfer-encoding"); + if($encoding !== null) { + switch (strtolower($encoding)) { + case "quoted-printable": + $this->encoding = IMAP::MESSAGE_ENC_QUOTED_PRINTABLE; + break; + case "base64": + $this->encoding = IMAP::MESSAGE_ENC_BASE64; + break; + case "7bit": + $this->encoding = IMAP::MESSAGE_ENC_7BIT; + break; + case "8bit": + $this->encoding = IMAP::MESSAGE_ENC_8BIT; + break; + case "binary": + $this->encoding = IMAP::MESSAGE_ENC_BINARY; + break; + default: + $this->encoding = IMAP::MESSAGE_ENC_OTHER; + break; + + } + } + } + +} \ No newline at end of file diff --git a/src/Query/Query.php b/src/Query/Query.php index 28ffa0ad..bbdc17a9 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -15,8 +15,10 @@ use Carbon\Carbon; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\IMAP; use Webklex\PHPIMAP\Message; use Webklex\PHPIMAP\Support\MessageCollection; @@ -52,9 +54,6 @@ class Query { /** @var int $fetch_body */ protected $fetch_body = true; - /** @var int $fetch_attachment */ - protected $fetch_attachment = true; - /** @var int $fetch_flags */ protected $fetch_flags = true; @@ -144,19 +143,17 @@ public function markAsRead() { * Perform an imap search request * * @return \Illuminate\Support\Collection - * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws GetMessagesFailedException */ protected function search(){ $this->generate_query(); - /** - * Don't set the charset if it isn't used - prevent strange outlook mail server errors - * @see https://github.com/Webklex/laravel-imap/issues/100 - */ - if($this->getCharset() === null){ - $available_messages = \imap_search($this->getClient()->getConnection(), $this->getRawQuery(), IMAP::SE_UID); - }else{ - $available_messages = \imap_search($this->getClient()->getConnection(), $this->getRawQuery(), IMAP::SE_UID, $this->getCharset()); + try { + $available_messages = $this->client->getConnection()->search([$this->getRawQuery()]); + } catch (RuntimeException $e) { + $available_messages = false; + } catch (ConnectionFailedException $e) { + throw new GetMessagesFailedException("failed to fetch messages", 0, $e); } if ($available_messages !== false) { @@ -170,7 +167,7 @@ protected function search(){ * Count all available messages matching the current search criteria * * @return int - * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws GetMessagesFailedException */ public function count() { return $this->search()->count(); @@ -185,11 +182,9 @@ public function count() { public function get() { $messages = MessageCollection::make([]); + $available_messages = $this->search(); try { - $available_messages = $this->search(); - $available_messages_count = $available_messages->count(); - - if ($available_messages_count > 0) { + if (($available_messages_count = $available_messages->count()) > 0) { $messages->total($available_messages_count); @@ -202,20 +197,20 @@ public function get() { $query =& $this; $available_messages->forPage($this->page, $this->limit)->each(function($msgno, $msglist) use(&$messages, $options, $query) { - $oMessage = new Message($msgno, $msglist, $query->getClient(), $query->getFetchOptions(), $query->getFetchBody(), $query->getFetchAttachment(), $query->getFetchFlags()); + $message = $query->getMessage($msgno, $msglist); switch ($options['message_key']){ case 'number': - $message_key = $oMessage->getMessageNo(); + $message_key = $message->getMessageNo(); break; case 'list': $message_key = $msglist; break; default: - $message_key = $oMessage->getMessageId(); + $message_key = $message->getMessageId(); break; } - $messages->put($message_key, $oMessage); + $messages->put($message_key, $message); }); } @@ -225,6 +220,22 @@ public function get() { } } + /** + * Get a new Message instance + * @param $msgno + * @param null $msglist + * + * @return Message + * @throws ConnectionFailedException + * @throws RuntimeException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException + */ + public function getMessage($msgno, $msglist = null){ + return new Message($msgno, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags()); + } + /** * Paginate the current query * @param int $per_page @@ -238,7 +249,18 @@ public function paginate($per_page = 5, $page = null, $page_name = 'imap_page'){ $this->page = $page > $this->page ? $page : $this->page; $this->limit = $per_page; - return $this->get()->paginate($per_page, $this->page, $page_name); + $messages = $this->get(); + if (($count = $messages->count()) > 0) { + $limit = $this->limit > $count ? $count : $this->limit; + $collection = array_fill(0, $messages->total() - $limit, true); + $messages->each(function($message) use(&$collection){ + $collection[] = $message; + }); + }else{ + $collection = array_fill(0, $messages->total(), true); + } + + return MessageCollection::make($collection)->paginate($per_page, $this->page, $page_name); } /** @@ -427,30 +449,6 @@ public function fetchBody($fetch_body) { return $this->setFetchBody($fetch_body); } - /** - * @return boolean - */ - public function getFetchAttachment() { - return $this->fetch_attachment; - } - - /** - * @param boolean $fetch_attachment - * @return Query - */ - public function setFetchAttachment($fetch_attachment) { - $this->fetch_attachment = $fetch_attachment; - return $this; - } - - /** - * @param boolean $fetch_attachment - * @return Query - */ - public function fetchAttachment($fetch_attachment) { - return $this->setFetchAttachment($fetch_attachment); - } - /** * @return int */ diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 54a5add3..80829325 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -20,8 +20,8 @@ /** * Class WhereQuery * - * @package Webklex\PHPIMAP\Query - * + * @package Webklex\IMAP\Query + * * @method WhereQuery all() * @method WhereQuery answered() * @method WhereQuery deleted() @@ -33,6 +33,7 @@ * @method WhereQuery undeleted() * @method WhereQuery unflagged() * @method WhereQuery unseen() + * @method WhereQuery not() * @method WhereQuery unkeyword($value) * @method WhereQuery to($value) * @method WhereQuery text($value) @@ -80,7 +81,12 @@ public function __call($name, $arguments) { $name = substr($name, 3); } - $method = 'where'.ucfirst($name); + if (strpos(strtolower($name), "where") === false){ + $method = 'where'.ucfirst($name); + }else{ + $method = lcfirst($name); + } + if(method_exists($this, $method) === true){ return call_user_func_array([$that, $method], $arguments); } @@ -97,9 +103,8 @@ public function __call($name, $arguments) { */ protected function validate_criteria($criteria) { $criteria = strtoupper($criteria); - - if (substr($criteria, 0, 6) === "CUSTOM") { - return substr($criteria, 6); + if (substr($criteria, 0, 7) === "CUSTOM ") { + return substr($criteria, 7); } if(in_array($criteria, $this->available_criteria) === false) { throw new InvalidWhereQueryCriteriaException(); @@ -117,12 +122,11 @@ protected function validate_criteria($criteria) { */ public function where($criteria, $value = null) { if(is_array($criteria)){ - foreach($criteria as $arguments){ - if(count($arguments) == 1){ - $this->where($arguments[0]); - }elseif(count($arguments) == 2){ - $this->where($arguments[0], $arguments[1]); + foreach($criteria as $key => $value){ + if(is_numeric($key)){ + return $this->where($value); } + return $this->where($key, $value); } }else{ $criteria = $this->validate_criteria($criteria); @@ -393,22 +397,12 @@ public function whereUnseen() { return $this->where('UNSEEN'); } - /** - * @param $msg_id - * - * @return WhereQuery - * @throws InvalidWhereQueryCriteriaException - */ - public function whereMessageId($msg_id) { - return $this->where("Message-ID <$msg_id>"); - } - /** * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ public function whereNoXSpam(){ - return $this->where("X-Spam-Flag NO"); + return $this->where("CUSTOM X-Spam-Flag NO"); } /** @@ -416,7 +410,7 @@ public function whereNoXSpam(){ * @throws InvalidWhereQueryCriteriaException */ public function whereIsXSpam(){ - return $this->where("X-Spam-Flag YES"); + return $this->where("CUSTOM X-Spam-Flag YES"); } /** diff --git a/src/Structure.php b/src/Structure.php new file mode 100644 index 00000000..603901d7 --- /dev/null +++ b/src/Structure.php @@ -0,0 +1,119 @@ +raw = $raw_structure; + $this->header = $header; + $this->config = ClientManager::get('options'); + $this->parse(); + } + + /** + * @throws MessageContentFetchingException + * @throws InvalidMessageDateException + */ + protected function parse(){ + $this->findContentType(); + $this->parts = $this->find_parts(); + } + + /** + * Determine the message content type + */ + public function findContentType(){ + if(stripos($this->header->get("content-type"), 'multipart') === 0) { + $this->type = IMAP::MESSAGE_TYPE_MULTIPART; + }else{ + $this->type = IMAP::MESSAGE_TYPE_TEXT; + } + } + + /** + * Determine the message content type + */ + public function getBoundary(){ + return $this->header->find("/boundary\=\"(.*)\"/"); + } + + /** + * @return array + * @throws MessageContentFetchingException + * @throws InvalidMessageDateException + */ + public function find_parts(){ + if($this->type === IMAP::MESSAGE_TYPE_MULTIPART) { + if (($boundary = $this->getBoundary()) === null) { + throw new MessageContentFetchingException("no content found", 0); + } + + $raw_parts = explode($boundary, $this->raw); + $parts = []; + foreach($raw_parts as $part) { + $part = trim(rtrim($part)); + if ($part !== "--") { + $parts[] = new Part($part); + } + } + return $parts; + } + + return [new Part($this->raw)]; + } +} \ No newline at end of file diff --git a/src/config/imap.php b/src/config/imap.php index 6d97f561..1db0f074 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -54,6 +54,7 @@ 'validate_cert' => true, 'username' => 'root@example.com', 'password' => '', + 'authentication' => null, ], /* @@ -64,6 +65,7 @@ 'validate_cert' => true, 'username' => 'example@gmail.com', 'password' => 'PASSWORD', + 'authentication' => 'oauth', ], 'another' => [ // account identifier @@ -86,13 +88,8 @@ | -Delimiter (optional): | This option is only used when calling $oClient-> | You can use any supported char such as ".", "/", (...) - | -Fetch option: - | IMAP::FT_UID - Message marked as read by fetching the message - | IMAP::FT_PEEK - Fetch the message without setting the "read" flag | -Body download option | Default TRUE - | -Attachment download option - | Default TRUE | -Flag download option | Default TRUE | -Message key identifier option @@ -103,12 +100,6 @@ | -Fetch order | 'asc' - Order all messages ascending (probably results in oldest first) | 'desc' - Order all messages descending (probably results in newest first) - | -Open IMAP options: - | DISABLE_AUTHENTICATOR - Disable authentication properties. - | Use 'GSSAPI' if you encounter the following - | error: "Kerberos error: No credentials cache - | file found (try running kinit) (...)" - | or ['GSSAPI','PLAIN'] if you are using outlook mail | -Decoder options (currently only the message subject and attachment name decoder can be set) | 'utf-8' - Uses imap_utf8($string) to decode a string | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string @@ -116,22 +107,13 @@ */ 'options' => [ 'delimiter' => '/', - 'fetch' => \Webklex\PHPIMAP\IMAP::FT_UID, 'fetch_body' => true, - 'fetch_attachment' => true, 'fetch_flags' => true, - 'message_key' => 'id', + 'message_key' => 'list', 'fetch_order' => 'asc', - 'open' => [ - // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' - ], 'decoder' => [ - 'message' => [ - 'subject' => 'utf-8' // mimeheader - ], - 'attachment' => [ - 'name' => 'utf-8' // mimeheader - ] + 'message' => 'utf-8', // mimeheader + 'attachment' => 'utf-8' // mimeheader ] ], From 7ece3def94f43ac8f165ee5bbe1a3a70f5458aa2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 05:14:39 +0200 Subject: [PATCH 002/600] Changelog updated --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8723410b..a820e937 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,15 @@ All notable changes to `webklex/php-imap` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. -## [UNRELEASED] +## [UNRELEASED] [2.x] ### Fixed -- Point to root namespace if handling native functions #279 +- NaN ### Added -- NaN +- php-imap replaced by direct socket communication ### Affected Classes -- [Query::class](src/Query/WhereQuery.php) -- [Attachment::class](src/Attachment.php) -- [Client::class](src/Client.php) -- [Folder::class](src/Folder.php) -- [Message::class](src/Message.php) +- All ## [1.4.5] - 2019-01-23 ### Fixed From 61faf96ad5349c8225c1a396d5bb507530efae71 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 21:12:22 +0200 Subject: [PATCH 003/600] Legacy support added --- README.md | 7 +- src/Client.php | 13 +- src/Connection/Protocols/ImapProtocol.php | 2 +- src/Connection/Protocols/LegacyProtocol.php | 499 ++++++++++++++++++ .../Protocols/ProtocolInterface.php | 17 - .../MethodNotSupportedException.php | 24 + src/Query/Query.php | 2 +- src/config/imap.php | 9 + 8 files changed, 550 insertions(+), 23 deletions(-) create mode 100644 src/Connection/Protocols/LegacyProtocol.php create mode 100644 src/Exceptions/MethodNotSupportedException.php diff --git a/README.md b/README.md index cf6b030c..6b66efa1 100755 --- a/README.md +++ b/README.md @@ -76,13 +76,16 @@ composer require webklex/php-imap Supported protocols: - `imap` — Use IMAP [default] -- `pop3` — ~~Use POP3~~ [not supported jet] -- `nntp` — ~~Use NNTP~~ [not supported jet] +- `legacy-imap` — Use the php imap module instead +- `pop3` — Use POP3 +- `nntp` — Use NNTP The following encryption methods are supported: - `false` — Disable encryption - `ssl` — Use SSL - `tls` — Use TLS +- `starttls` — Use STARTTLS (alias TLS) (legacy only) +- `notls` — Use NoTLS (legacy only) Detailed [config/imap.php](src/config/imap.php) configuration: - `default` — used default account. It will be used as default for any missing account parameters. If however the default account is missing a parameter the package default will be used. Set to `false` to disable this functionality. diff --git a/src/Client.php b/src/Client.php index 6a39eba6..c1c0f26a 100755 --- a/src/Client.php +++ b/src/Client.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; +use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol; use Webklex\PHPIMAP\Connection\Protocols\Protocol; use Webklex\PHPIMAP\Connection\Protocols\ProtocolInterface; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; @@ -257,13 +258,21 @@ public function reconnect() { */ public function connect() { $this->disconnect(); + $protocol = strtolower($this->protocol); - if (strtolower($this->protocol) == "imap") { + if ($protocol == "imap") { $timeout = $this->connection !== false ? $this->connection->getConnectionTimeout() : null; $this->connection = new ImapProtocol($this->validate_cert); $this->connection->setConnectionTimeout($timeout); }else{ - throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($this->protocol." is an unsupported protocol")); + if (extension_loaded('imap') === false) { + throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); + } + $this->connection = new LegacyProtocol($this->validate_cert); + if (strpos($protocol, "legacy-") === 0) { + $protocol = substr($protocol, 7); + } + $this->connection->setProtocol($protocol); } $this->connection->connect($this->host, $this->port, $this->encryption); diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 361c48e8..ba790271 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -535,7 +535,7 @@ public function examineFolder($folder = 'INBOX') { * if items of messages are fetched it's returned as (msgno => (name => value)) * @throws RuntimeException */ - public function fetch($items, $from, $to = null, $uid = false) { + protected function fetch($items, $from, $to = null, $uid = false) { if (is_array($from)) { $set = implode(',', $from); } elseif ($to === null) { diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php new file mode 100644 index 00000000..11a3fe2c --- /dev/null +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -0,0 +1,499 @@ +setCertValidation($cert_validation); + } + + /** + * Public destructor + */ + public function __destruct() { + $this->logout(); + } + + /** + * Save the information for a nw connection + * @param string $host + * @param null $port + * @param bool $encryption + */ + public function connect($host, $port = null, $encryption = false) { + if ($port === null) { + $port = $encryption == "ssl" ? 995 : 110; + } + $this->host = $host; + $this->port = $port; + $this->encryption = $encryption; + } + + /** + * Login to a new session. + * @param string $user username + * @param string $password password + * + * @return bool + * @throws AuthFailedException + */ + public function login($user, $password) { + try { + $this->stream = \imap_open( + $this->getAddress(), + $user, + $password, + IMAP::OP_READONLY, + $attempts = 3, + ClientManager::get('options.open') + ); + } catch (\ErrorException $e) { + $errors = \imap_errors(); + $message = $e->getMessage().'. '.implode("; ", (is_array($errors) ? $errors : array())); + + throw new AuthFailedException($message); + } + + if(!$this->stream) { + $errors = \imap_errors(); + $message = implode("; ", (is_array($errors) ? $errors : array())); + throw new AuthFailedException($message); + } + + return !$this->stream; + } + + /** + * Authenticate your current session. + * @param string $user username + * @param string $token access token + * + * @return bool + * @throws AuthFailedException + */ + public function authenticate($user, $token) { + return $this->login($user, $token); + } + + /** + * Get full address of mailbox. + * + * @return string + */ + protected function getAddress() { + $address = "{".$this->host.":".$this->port."/".$this->protocol; + if (!$this->cert_validation) { + $address .= '/novalidate-cert'; + } + if (in_array($this->encryption,['tls', 'notls', 'ssl'])) { + $address .= '/'.$this->encryption; + } elseif ($this->encryption === "starttls") { + $address .= '/tls'; + } + + $address .= '}'; + + return $address; + } + + /** + * Logout of the current session + * + * @return bool success + */ + public function logout() { + if ($this->stream) { + return \imap_close($this->stream, IMAP::CL_EXPUNGE); + } + return false; + } + + /** + * Check if the current session is connected + * + * @return bool + */ + public function connected(){ + return !$this->stream; + } + + /** + * Get an array of available capabilities + * + * @throws MethodNotSupportedException + */ + public function getCapabilities() { + throw new MethodNotSupportedException(); + } + + /** + * Change the current folder + * @param string $folder change to this folder + * + * @return bool|array see examineOrselect() + * @throws RuntimeException + */ + public function selectFolder($folder = 'INBOX') { + \imap_reopen($this->stream, $folder, IMAP::OP_READONLY, 3); + return $this->examineFolder($folder); + } + + /** + * Examine a given folder + * @param string $folder examine this folder + * + * @return bool|array see examineOrselect() + * @throws RuntimeException + */ + public function examineFolder($folder = 'INBOX') { + if (strpos($folder, ".") === 0) { + throw new RuntimeException("Segmentation fault prevented. Folders starts with an illegal char '.'."); + } + $folder = $this->getAddress().$folder; + $status = \imap_status($this->stream, $folder, IMAP::SA_ALL); + return [ + "flags" => [], + "exists" => $status->messages, + "recent" => $status->recent, + "unseen" => $status->unseen, + "uidnext" => $status->uidnext, + ]; + } + + /** + * Fetch message content + * @param array|int $uids + * @param string $rfc + * + * @return array + */ + public function content($uids, $rfc = "RFC822") { + $content = \imap_fetchbody($this->stream, $uids[0], "", IMAP::FT_UID); + return [$uids[0] => $content]; + } + + /** + * Fetch message headers + * @param array|int $uids + * @param string $rfc + * + * @return array + */ + public function headers($uids, $rfc = "RFC822"){ + $headers = \imap_fetchheader($this->stream, $uids[0], IMAP::FT_UID); + return [$uids[0] => $headers]; + } + + /** + * Fetch message flags + * @param array|int $uids + * + * @return array + */ + public function flags($uids){ + $flags = \imap_fetch_overview($this->stream, $uids[0], IMAP::FT_UID); + $result = []; + if (is_array($flags) && isset($flags[0])) { + $flags = (array) $flags[0]; + foreach($flags as $flag => $value) { + if ($value === 1 && in_array($flag, ["size", "uid", "msgno", "update"]) === false){ + $result[] = "\\".ucfirst($flag); + } + } + } + return [$uids[0] => $result]; + } + + /** + * Get uid for a given id + * @param int|null $id message number + * + * @return array|string message number for given message or all messages as array + */ + public function getUid($id = null) { + if ($id === null) { + $overview = $this->overview("1:*"); + $uids = []; + foreach($overview as $set){ + $uids[$set->msgno] = $set->uid; + } + return $uids; + } + return \imap_uid($this->stream, $id); + } + + /** + * Get a message number for a uid + * @param string $id uid + * + * @return int message number + */ + public function getMessageNumber($id) { + return \imap_msgno($this->stream, $id); + } + + /** + * Get a message number for a uid + * @param string $sequence uid sequence + * + * @return array + */ + public function overview($sequence) { + return \imap_fetch_overview($this->stream, $sequence); + } + + /** + * Get a list of available folders + * @param string $reference mailbox reference for list + * @param string $folder mailbox name match with wildcards + * + * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) + * @throws RuntimeException + */ + public function folders($reference = '', $folder = '*') { + $result = []; + + $items = \imap_getmailboxes($this->stream, $this->getAddress(), $reference.$folder); + if(is_array($items)){ + foreach ($items as $item) { + $name = $this->decodeFolderName($item->name); + $result[$name] = ['delimiter' => $item->delimiter, 'flags' => []]; + } + }else{ + throw new RuntimeException(\imap_last_error()); + } + + return $result; + } + + /** + * Manage flags + * @param array $flags flags to set, add or remove - see $mode + * @param int $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given + * @param bool $silent if false the return values are the new flags for the wanted messages + * + * @return bool|array new flags if $silent is false, else true or false depending on success + */ + public function store(array $flags, $from, $to = null, $mode = null, $silent = true) { + $flag = "\\".trim(is_array($flags) ? implode(" \\", $flags) : $flags); + + if ($mode == "+"){ + $status = \imap_setflag_full($this->stream, $from, $flag, IMAP::SE_UID); + }else{ + $status = \imap_clearflag_full($this->stream, $from, $flag, IMAP::SE_UID); + } + + if ($silent === true) { + return $status; + } + + return $this->flags($from); + } + + /** + * Append a new message to given folder + * @param string $folder name of target folder + * @param string $message full message content + * @param array $flags flags for new message + * @param string $date date for new message + * + * @return bool success + */ + public function appendMessage($folder, $message, $flags = null, $date = null) { + if ($date != null) { + if ($date instanceof \Carbon\Carbon){ + $date = $date->format('d-M-Y H:i:s O'); + } + return \imap_append($this->stream, $folder, $message, $flags, $date); + } + + return \imap_append($this->stream, $folder, $message, $flags); + } + + /** + * Copy message set from current folder to other folder + * @param string $folder destination folder + * @param $from + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * + * @return bool success + */ + public function copyMessage($folder, $from, $to = null) { + return \imap_mail_copy($this->stream, $from, $folder, IMAP::CP_UID); + } + + /** + * Create a new folder (and parent folders if needed) + * @param string $folder folder name + * + * @return bool success + */ + public function createFolder($folder) { + return \imap_createmailbox($this->stream, $folder); + } + + /** + * Rename an existing folder + * @param string $old old name + * @param string $new new name + * + * @return bool success + */ + public function renameFolder($old, $new) { + return \imap_renamemailbox($this->stream, $old, $new); + } + + /** + * Delete a folder + * @param string $folder folder name + * + * @return bool success + */ + public function deleteFolder($folder) { + return \imap_deletemailbox($this->stream, $folder); + } + + /** + * Subscribe to a folder + * @param string $folder folder name + * + * @throws MethodNotSupportedException + */ + public function subscribeFolder($folder) { + throw new MethodNotSupportedException(); + } + + /** + * Unsubscribe from a folder + * @param string $folder folder name + * + * @throws MethodNotSupportedException + */ + public function unsubscribeFolder($folder) { + throw new MethodNotSupportedException(); + } + + /** + * Apply session saved changes to the server + * + * @return bool success + */ + public function expunge() { + return \imap_expunge($this->stream); + } + + /** + * Send noop command + * + * @throws MethodNotSupportedException + */ + public function noop() { + throw new MethodNotSupportedException(); + } + + /** + * Send idle command + * + * @throws MethodNotSupportedException + */ + public function idle() { + throw new MethodNotSupportedException(); + } + + /** + * Send done command + * + * @throws MethodNotSupportedException + */ + public function done() { + throw new MethodNotSupportedException(); + } + + /** + * Search for matching messages + * + * @param array $params + * @return array message ids + */ + public function search(array $params) { + return \imap_search($this->stream, $params[0], IMAP::SE_UID); + } + + /** + * Enable the debug mode + */ + public function enableDebug(){ + $this->debug = true; + } + + /** + * Disable the debug mode + */ + public function disableDebug(){ + $this->debug = false; + } + + /** + * Decode name. + * It converts UTF7-IMAP encoding to UTF-8. + * + * @param $name + * + * @return mixed|string + */ + protected function decodeFolderName($name) { + preg_match('#\{(.*)\}(.*)#', $name, $preg); + return mb_convert_encoding($preg[2], "UTF-8", "UTF7-IMAP"); + } + + /** + * @return string + */ + public function getProtocol() { + return $this->protocol; + } + + /** + * @param string $protocol + * @return Pop3Protocol + */ + public function setProtocol($protocol) { + $this->protocol = $protocol; + return $this; + } +} \ No newline at end of file diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index d73e344f..6bc111d0 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -89,23 +89,6 @@ public function selectFolder($folder = 'INBOX'); */ public function examineFolder($folder = 'INBOX'); - /** - * Fetch one or more items of one or more messages - * - * @param string|array $items items to fetch from message(s) as string (if only one item) - * or array of strings - * @param int|array $from message for items or start message if $to !== null - * @param int|null $to if null only one message ($from) is fetched, else it's the - * last message, INF means last message available - * @param bool $uid set to true if passing a unique id - * @return string|array if only one item of one message is fetched it's returned as string - * if items of one message are fetched it's returned as (name => value) - * if one items of messages are fetched it's returned as (msgno => value) - * if items of messages are fetched it's returned as (msgno => (name => value)) - * @throws RuntimeException - */ - public function fetch($items, $from, $to = null, $uid = false); - /** * Fetch message headers * @param array|int $uids diff --git a/src/Exceptions/MethodNotSupportedException.php b/src/Exceptions/MethodNotSupportedException.php new file mode 100644 index 00000000..9230a6de --- /dev/null +++ b/src/Exceptions/MethodNotSupportedException.php @@ -0,0 +1,24 @@ +getMessage()); + throw new GetMessagesFailedException($e->getMessage(),0, $e); } } diff --git a/src/config/imap.php b/src/config/imap.php index 1db0f074..61e4bc8c 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -100,6 +100,12 @@ | -Fetch order | 'asc' - Order all messages ascending (probably results in oldest first) | 'desc' - Order all messages descending (probably results in newest first) + | -Open IMAP options: + | DISABLE_AUTHENTICATOR - Disable authentication properties. + | Use 'GSSAPI' if you encounter the following + | error: "Kerberos error: No credentials cache + | file found (try running kinit) (...)" + | or ['GSSAPI','PLAIN'] if you are using outlook mail | -Decoder options (currently only the message subject and attachment name decoder can be set) | 'utf-8' - Uses imap_utf8($string) to decode a string | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string @@ -111,6 +117,9 @@ 'fetch_flags' => true, 'message_key' => 'list', 'fetch_order' => 'asc', + 'open' => [ + // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' + ], 'decoder' => [ 'message' => 'utf-8', // mimeheader 'attachment' => 'utf-8' // mimeheader From 255a54273c7f9b6162225d19081696a03bc0c840 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 21:13:36 +0200 Subject: [PATCH 004/600] Changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a820e937..fa0c212f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - php-imap replaced by direct socket communication +- Legacy support added ### Affected Classes - All From 0ffb970047964f614d99ee0c424c8f2cefe2aba5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 21:19:38 +0200 Subject: [PATCH 005/600] oAuth example added --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 6b66efa1..238349fd 100755 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The functionality is almost completely integrated and even supports IDLE operati - [Usage](#usage) - [Basic usage example](#basic-usage-example) - [Folder / Mailbox](#folder--mailbox) + - [oAuth](#oauth) - [Idle](#idle) - [Search](#search-for-messages) - [Counting messages](#counting-messages) @@ -105,6 +106,8 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `fetch_flags` — If set to `false` all messages will be fetched without any flags - `message_key` — Message key identifier option - `fetch_order` — Message fetch order + - `open` — special configuration for imap_open() + - `DISABLE_AUTHENTICATOR` — disable authentication properties. - `decoder` — Currently only the message and attachment decoder can be set - `masks` — Default [masking](#masking) config - `message` — Default message mask @@ -187,6 +190,25 @@ Get a specific folder: $oFolder = $oClient->getFolder('INBOX.name'); ``` +#### oAuth +If you are using google mail or something similar, you might want to use oauth instead: +``` php +/** @var \Webklex\PHPIMAP\Client $oClient */ +$oClient = new Client([ + 'host' => 'imap.gmail.com', + 'port' => 993, + 'encryption' => 'ssl', + 'validate_cert' => true, + 'username' => 'example@gmail.com', + 'password' => 'PASSWORD', + 'authentication' => "oath", + 'protocol' => 'imap' +]); + +//Connect to the IMAP Server +$oClient->connect(); +``` + #### Idle Every time a new message is received the server will notify the client and return the new message. ``` php From 6c0e77b3dc56e13782e8732f23dbd5a2ef37bc65 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 21:25:01 +0200 Subject: [PATCH 006/600] Changelog updated --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0c212f..8b67a3ee 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,15 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] [2.x] ### Fixed -- NaN +- Missing pagination item records fixed ### Added -- php-imap replaced by direct socket communication +- php-imap module replaced by direct socket communication - Legacy support added +- IDLE support added +- oAuth support added +- Charset detection method updated +- Decoding fallback charsets added ### Affected Classes - All From 617cdcaa845abdfe68dddadccf2d2c0e081eb17f Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 23:14:41 +0200 Subject: [PATCH 007/600] Fix regular message handling --- src/Part.php | 20 ++++++++++++-------- src/Structure.php | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Part.php b/src/Part.php index 7c68bf30..c602cb88 100644 --- a/src/Part.php +++ b/src/Part.php @@ -103,10 +103,12 @@ class Part { /** * Part constructor. * @param $raw_part + * @param Header $header * @throws InvalidMessageDateException */ - public function __construct($raw_part) { + public function __construct($raw_part, $header = null) { $this->raw = $raw_part; + $this->header = $header; $this->parse(); } @@ -115,17 +117,19 @@ public function __construct($raw_part) { * @throws InvalidMessageDateException */ protected function parse(){ - $headers = ""; $body = $this->raw; - if (preg_match_all("/(.*)(?s)(?-m)\\r\\n.+?(?=\n([^A-Z]|.{1,2}[^A-Z])|$)/im", $body, $matches)) { - if (isset($matches[0][0])) { - $headers = $matches[0][0]; - $body = substr($body, strlen($headers) + 2, -2); + if ($this->header === null) { + $headers = ""; + if (preg_match_all("/(.*)(?s)(?-m)\\r\\n.+?(?=\n([^A-Z]|.{1,2}[^A-Z])|$)/im", $body, $matches)) { + if (isset($matches[0][0])) { + $headers = $matches[0][0]; + $body = substr($body, strlen($headers) + 2, -2); + } } - } - $this->header = new Header($headers); + $this->header = new Header($headers); + } $this->parseSubtype(); $this->parseDisposition(); $this->parseDescription(); diff --git a/src/Structure.php b/src/Structure.php index 603901d7..3d2e0b81 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -114,6 +114,6 @@ public function find_parts(){ return $parts; } - return [new Part($this->raw)]; + return [new Part($this->raw, $this->header)]; } } \ No newline at end of file From 3138fa78924435560c57fad43e14d30e5e0d7d0b Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 23:19:12 +0200 Subject: [PATCH 008/600] Release updated --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b67a3ee..45dddf74 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,17 @@ All notable changes to `webklex/php-imap` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. -## [UNRELEASED] [2.x] +## [UNRELEASED] +### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +## [2.0.0] - 2020-09-20 ### Fixed - Missing pagination item records fixed From a8bdacaac343cd9844cbec23a838ed83681d780c Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 23:39:03 +0200 Subject: [PATCH 009/600] Comments updated --- src/Query/Query.php | 17 +++++++++++------ src/Query/WhereQuery.php | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index dee13fb2..4456fcea 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -13,10 +13,15 @@ namespace Webklex\PHPIMAP\Query; use Carbon\Carbon; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\IMAP; @@ -142,7 +147,7 @@ public function markAsRead() { /** * Perform an imap search request * - * @return \Illuminate\Support\Collection + * @return Collection * @throws GetMessagesFailedException */ protected function search(){ @@ -228,9 +233,9 @@ public function get() { * @return Message * @throws ConnectionFailedException * @throws RuntimeException - * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException - * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException - * @throws \Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageHeaderFetchingException */ public function getMessage($msgno, $msglist = null){ return new Message($msgno, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags()); @@ -242,7 +247,7 @@ public function getMessage($msgno, $msglist = null){ * @param null $page * @param string $page_name * - * @return \Illuminate\Pagination\LengthAwarePaginator + * @return LengthAwarePaginator * @throws GetMessagesFailedException */ public function paginate($per_page = 5, $page = null, $page_name = 'imap_page'){ @@ -291,7 +296,7 @@ public function generate_query() { /** * @return Client - * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws ConnectionFailedException */ public function getClient() { $this->client->checkConnection(); diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 80829325..80f86e76 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -20,7 +20,7 @@ /** * Class WhereQuery * - * @package Webklex\IMAP\Query + * @package Webklex\PHPIMAP\Query * * @method WhereQuery all() * @method WhereQuery answered() From 8d64e260f807ce88609bf592508a04f913502ccc Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 23:39:25 +0200 Subject: [PATCH 010/600] Carbon dependency fixed --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5fb56bde..311a347e 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "ext-mbstring": "*", "ext-iconv": "*", "ext-fileinfo": "*", - "nesbot/carbon": ">=1.33", + "nesbot/carbon": ">=1.0", "symfony/http-foundation": ">=2.8.0", "illuminate/pagination": ">=5.0.0" }, From 192a5dec05a944de2cbff3b690a0aa4211a5a7f2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Sep 2020 23:40:24 +0200 Subject: [PATCH 011/600] Release information added --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45dddf74..10784e0f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.0.1] - 2020-09-20 +### Fixed +- Carbon dependency fixed + ## [2.0.0] - 2020-09-20 ### Fixed - Missing pagination item records fixed From 2fe724985bffb502e01a5895c99f141ad5d15198 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 22 Sep 2020 00:01:44 +0200 Subject: [PATCH 012/600] Quota handling fixed --- src/Client.php | 4 ++-- src/Connection/Protocols/ImapProtocol.php | 22 +++++++++++++++++++ src/Connection/Protocols/LegacyProtocol.php | 22 ++++++++++++++++++- .../Protocols/ProtocolInterface.php | 19 ++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Client.php b/src/Client.php index c1c0f26a..4c0a8701 100755 --- a/src/Client.php +++ b/src/Client.php @@ -420,7 +420,7 @@ public function getFolderPath(){ */ public function getQuota() { $this->checkConnection(); - return $this->connection->requestAndResponse("GETQUOTA", ['"#user/'.$this->username.'"']); + return $this->connection->getQuota($this->username); } /** @@ -433,7 +433,7 @@ public function getQuota() { */ public function getQuotaRoot($quota_root = 'INBOX') { $this->checkConnection(); - return $this->connection->requestAndResponse("QUOTA", [$quota_root]); + return $this->connection->getQuotaRoot($quota_root); } /** diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ba790271..16315917 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -881,6 +881,28 @@ public function noop() { return $this->requestAndResponse('NOOP'); } + /** + * Retrieve the quota level settings, and usage statics per mailbox + * @param $username + * + * @return array + * @throws RuntimeException + */ + public function getQuota($username) { + return $this->requestAndResponse("GETQUOTA", ['"#user/'.$username.'"']); + } + + /** + * Retrieve the quota settings per user + * @param string $quota_root + * + * @return array + * @throws RuntimeException + */ + public function getQuotaRoot($quota_root = 'INBOX') { + return $this->requestAndResponse("QUOTA", [$quota_root]); + } + /** * Send idle command * @throws RuntimeException diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 11a3fe2c..e5dead28 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -488,9 +488,29 @@ public function getProtocol() { return $this->protocol; } + /** + * Retrieve the quota level settings, and usage statics per mailbox + * @param $username + * + * @return array + */ + public function getQuota($username) { + return \imap_get_quota($this->stream, 'user.'.$username); + } + + /** + * Retrieve the quota settings per user + * @param string $quota_root + * + * @return array + */ + public function getQuotaRoot($quota_root = 'INBOX') { + return \imap_get_quotaroot($this->stream, $quota_root); + } + /** * @param string $protocol - * @return Pop3Protocol + * @return LegacyProtocol */ public function setProtocol($protocol) { $this->protocol = $protocol; diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 6bc111d0..18b42f43 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -229,6 +229,25 @@ public function subscribeFolder($folder); */ public function expunge(); + /** + * Retrieve the quota level settings, and usage statics per mailbox + * @param $username + * + * @return array + * @throws RuntimeException + */ + public function getQuota($username); + + /** + * Retrieve the quota settings per user + * + * @param string $quota_root + * + * @return array + * @throws ConnectionFailedException + */ + public function getQuotaRoot($quota_root = 'INBOX'); + /** * Send noop command * From 0b8e6ae2445c6ba7728334ed27fb7cefe39a3825 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 22 Sep 2020 00:03:04 +0200 Subject: [PATCH 013/600] Event system and callbacks added --- CHANGELOG.md | 4 +- README.md | 38 +++++++++++- src/Client.php | 36 +++++++++++- src/Events/Event.php | 14 +++++ src/Events/FlagDeletedEvent.php | 8 +++ src/Events/FlagNewEvent.php | 24 ++++++++ src/Events/FolderDeletedEvent.php | 7 +++ src/Events/FolderMovedEvent.php | 23 ++++++++ src/Events/FolderNewEvent.php | 20 +++++++ src/Events/MessageCopiedEvent.php | 8 +++ src/Events/MessageDeletedEvent.php | 8 +++ src/Events/MessageMovedEvent.php | 23 ++++++++ src/Events/MessageNewEvent.php | 20 +++++++ src/Events/MessageRestoredEvent.php | 8 +++ src/Exceptions/EventNotFoundException.php | 24 ++++++++ src/Folder.php | 23 +++++++- src/Header.php | 1 - src/Message.php | 40 +++++++++++-- src/Traits/HasEvents.php | 72 +++++++++++++++++++++++ src/config/imap.php | 25 ++++++++ 20 files changed, 415 insertions(+), 11 deletions(-) create mode 100644 src/Events/Event.php create mode 100644 src/Events/FlagDeletedEvent.php create mode 100644 src/Events/FlagNewEvent.php create mode 100644 src/Events/FolderDeletedEvent.php create mode 100644 src/Events/FolderMovedEvent.php create mode 100644 src/Events/FolderNewEvent.php create mode 100644 src/Events/MessageCopiedEvent.php create mode 100644 src/Events/MessageDeletedEvent.php create mode 100644 src/Events/MessageMovedEvent.php create mode 100644 src/Events/MessageNewEvent.php create mode 100644 src/Events/MessageRestoredEvent.php create mode 100644 src/Exceptions/EventNotFoundException.php create mode 100644 src/Traits/HasEvents.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 10784e0f..595c0812 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Quota handling fixed ### Added -- NaN +- Event system and callbacks added ### Affected Classes - NaN diff --git a/README.md b/README.md index 238349fd..16942a2d 100755 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The functionality is almost completely integrated and even supports IDLE operati - [Message flags](#message-flags) - [Attachments](#attachments) - [Advanced fetching](#advanced-fetching) + - [Events](#events) - [Masking](#masking) - [Specials](#specials) - [Support](#support) @@ -210,13 +211,13 @@ $oClient->connect(); ``` #### Idle -Every time a new message is received the server will notify the client and return the new message. +Every time a new message is received, the server will notify the client and return the new message. ``` php /** @var \Webklex\PHPIMAP\Client $oClient */ /** @var \Webklex\PHPIMAP\Folder $oFolder */ $oFolder->idle(function($message){ - dump($message->subject); + echo $message->subject."\n"; }); ``` @@ -467,6 +468,39 @@ $aMessage = $oFolder->query()->whereAll() ->get(); ``` +#### Events +The following events are available: +- `Webklex\PHPIMAP\Events\MessageNewEvent($message)` — can get triggered by `Folder::idle` +- `Webklex\PHPIMAP\Events\MessageDeletedEvent($message)` — triggered by `Message::delete` +- `Webklex\PHPIMAP\Events\MessageRestoredEvent($message)` — triggered by `Message::restore` +- `Webklex\PHPIMAP\Events\MessageMovedEvent($old_message, $new_message)` — triggered by `Message::move` +- `Webklex\PHPIMAP\Events\MessageCopiedEvent($old_message, $new_message)` — triggered by `Message::copy` +- `Webklex\PHPIMAP\Events\FlagNewEvent($flag)` — triggered by `Message::setFlag` +- `Webklex\PHPIMAP\Events\FlagDeletedEvent($flag)` — triggered by `Message::unsetFlag` +- `Webklex\PHPIMAP\Events\FolderNewEvent($folder)` — can get triggered by `Client::createFolder` +- `Webklex\PHPIMAP\Events\FolderDeletedEvent($folder)` — triggered by `Folder::delete` +- `Webklex\PHPIMAP\Events\FolderMovedEvent($old_folder, $new_folder)` — triggered by `Folder::move` + +Create and register your own custom event: +``` php +class CustomMessageNewEvent extends Webklex\PHPIMAP\Events\MessageNewEvent { + + /** + * Create a new event instance. + * @var Message[] $messages + * @return void + */ + public function __construct($messages) { + $this->message = $messages[0]; + echo "New message: ".$this->message->subject."\n"; + } +} + +/** @var \Webklex\PHPIMAP\Client $client */ +$client->setEvent("message", "new", CustomMessageNewEvent::class); +``` +..or set it in your config file under `events.message.new`, + #### Masking Laravel-IMAP already comes with two default masks [MessageMask::class](#messagemaskclass) and [AttachmentMask::class](#attachmentmaskclass). diff --git a/src/Client.php b/src/Client.php index 4c0a8701..6ee06883 100755 --- a/src/Client.php +++ b/src/Client.php @@ -23,6 +23,7 @@ use Webklex\PHPIMAP\Support\FolderCollection; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; use Webklex\PHPIMAP\Support\Masks\MessageMask; +use Webklex\PHPIMAP\Traits\HasEvents; /** * Class Client @@ -30,6 +31,7 @@ * @package Webklex\PHPIMAP */ class Client { + use HasEvents; /** @@ -128,6 +130,7 @@ class Client { public function __construct($config = []) { $this->setConfig($config); $this->setMaskFromConfig($config); + $this->setEventsFromConfig($config); } /** @@ -155,6 +158,21 @@ public function setConfig(array $config) { return $this; } + /** + * Look for a possible events in any available config + * @param $config + */ + protected function setEventsFromConfig($config) { + $this->events = ClientManager::get("events"); + if(isset($config['events'])){ + if(isset($config['events'])) { + foreach($config['events'] as $section => $events) { + $this->events[$section] = array_merge($this->events[$section], $events); + } + } + } + } + /** * Look for a possible mask in any available config * @param $config @@ -383,13 +401,21 @@ public function openFolder($folder) { * * @return bool * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws Exceptions\EventNotFoundException */ public function createFolder($folder, $expunge = true) { $this->checkConnection(); $status = $this->connection->createFolder($folder); if($expunge) $this->expunge(); - return $status; + $folder = $this->getFolder($folder); + if($status && $folder) { + $event = $this->getEvent("folder", "new"); + $event::dispatch($folder); + } + + return $folder; } /** @@ -474,6 +500,14 @@ public function getDefaultMessageMask(){ return $this->default_message_mask; } + /** + * @param $section + * @return array + */ + public function getDefaultEvents($section){ + return $this->events[$section]; + } + /** * @param $mask * diff --git a/src/Events/Event.php b/src/Events/Event.php new file mode 100644 index 00000000..cac69326 --- /dev/null +++ b/src/Events/Event.php @@ -0,0 +1,14 @@ +message = $arguments[0]; + $this->flag = $arguments[1]; + } +} diff --git a/src/Events/FolderDeletedEvent.php b/src/Events/FolderDeletedEvent.php new file mode 100644 index 00000000..600fa247 --- /dev/null +++ b/src/Events/FolderDeletedEvent.php @@ -0,0 +1,7 @@ +old_folder = $folders[0]; + $this->new_folder = $folders[1]; + } +} diff --git a/src/Events/FolderNewEvent.php b/src/Events/FolderNewEvent.php new file mode 100644 index 00000000..962d7a8c --- /dev/null +++ b/src/Events/FolderNewEvent.php @@ -0,0 +1,20 @@ +folder = $folders[0]; + } +} diff --git a/src/Events/MessageCopiedEvent.php b/src/Events/MessageCopiedEvent.php new file mode 100644 index 00000000..e22842c6 --- /dev/null +++ b/src/Events/MessageCopiedEvent.php @@ -0,0 +1,8 @@ +old_message = $messages[0]; + $this->new_message = $messages[1]; + } +} diff --git a/src/Events/MessageNewEvent.php b/src/Events/MessageNewEvent.php new file mode 100644 index 00000000..bc74ef4d --- /dev/null +++ b/src/Events/MessageNewEvent.php @@ -0,0 +1,20 @@ +message = $messages[0]; + } +} diff --git a/src/Events/MessageRestoredEvent.php b/src/Events/MessageRestoredEvent.php new file mode 100644 index 00000000..08ce7e62 --- /dev/null +++ b/src/Events/MessageRestoredEvent.php @@ -0,0 +1,8 @@ +client = $client; + $this->events["message"] = $client->getDefaultEvents("message"); + $this->events["folder"] = $client->getDefaultEvents("folder"); + $this->setDelimiter($delimiter); $this->path = $folder_name; $this->full_name = $this->decodeName($folder_name); @@ -224,7 +229,9 @@ protected function parseAttributes($attributes) { * @param boolean $expunge * * @return bool - * @throws Exceptions\ConnectionFailedException + * @throws ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ public function move($new_name, $expunge = true) { @@ -232,6 +239,10 @@ public function move($new_name, $expunge = true) { $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name); if($expunge) $this->client->expunge(); + $folder = $this->client->getFolder($new_name); + $event = $this->getEvent("folder", "moved"); + $event::dispatch($this, $folder); + return $status; } @@ -269,6 +280,8 @@ public function appendMessage($message, $options = null, $internal_date = null) * * @return bool * @throws ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ public function rename($new_name, $expunge = true) { @@ -283,11 +296,15 @@ public function rename($new_name, $expunge = true) { * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function delete($expunge = true) { $status = $this->client->getConnection()->deleteFolder($this->path); if($expunge) $this->client->expunge(); + $event = $this->getEvent("folder", "deleted"); + $event::dispatch($this); + return $status; } @@ -326,6 +343,7 @@ public function unsubscribe() { * @throws Exceptions\MessageContentFetchingException * @throws Exceptions\MessageHeaderFetchingException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function idle(callable $callback, $timeout = 1200) { $this->client->getConnection()->setConnectionTimeout($timeout); @@ -345,6 +363,9 @@ public function idle(callable $callback, $timeout = 1200) { $message = $this->query()->getMessage($msgn); $callback($message); + $event = $this->getEvent("message", "new"); + $event::dispatch($message); + $connection->idle(); } }catch (Exceptions\RuntimeException $e) { diff --git a/src/Header.php b/src/Header.php index 5cab2b67..c3cd028c 100644 --- a/src/Header.php +++ b/src/Header.php @@ -484,7 +484,6 @@ private function extractHeaderExtensions(){ * * @param object $header * - * @return Carbon|null * @throws InvalidMessageDateException */ private function parseDate($header) { diff --git a/src/Message.php b/src/Message.php index 09a356fe..5a57cea1 100755 --- a/src/Message.php +++ b/src/Message.php @@ -22,6 +22,7 @@ use Webklex\PHPIMAP\Support\FlagCollection; use Webklex\PHPIMAP\Support\Masks\MessageMask; use Illuminate\Support\Str; +use Webklex\PHPIMAP\Traits\HasEvents; /** * Class Message @@ -78,6 +79,7 @@ * @method array setSender(array $sender) */ class Message { + use HasEvents; /** * Client instance @@ -179,6 +181,8 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu if($default_mask != null) { $this->mask = $default_mask; } + $this->events["message"] = $client->getDefaultEvents("message"); + $this->events["flag"] = $client->getDefaultEvents("flag"); $this->folder_path = $client->getFolderPath(); @@ -639,6 +643,7 @@ public function getFolder(){ * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException + * @throws Exceptions\EventNotFoundException */ public function copy($folder) { $this->client->openFolder($this->folder_path); @@ -650,7 +655,12 @@ public function copy($folder) { if ($this->client->getConnection()->copyMessage($folder->path, $this->msgn) == true) { $this->client->openFolder($folder->path); $message_num = $this->client->getConnection()->getMessageNumber($next_uid); - return $folder->query()->getMessage($message_num); + + $message = $folder->query()->getMessage($message_num); + $event = $this->getEvent("message", "copied"); + $event::dispatch($this, $message); + + return $message; } } @@ -669,13 +679,19 @@ public function copy($folder) { * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException + * @throws Exceptions\EventNotFoundException */ public function move($folder, $expunge = false) { - $status = $this->copy($folder); - if ($status !== null) { + $message = $this->copy($folder); + if ($message !== null) { $status = $this->delete($expunge); + + $event = $this->getEvent("message", "moved"); + $event::dispatch($this, $message); } - return $status; + + + return $message; } /** @@ -685,11 +701,15 @@ public function move($folder, $expunge = false) { * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function delete($expunge = true) { $status = $this->setFlag("Deleted"); if($expunge) $this->client->expunge(); + $event = $this->getEvent("message", "deleted"); + $event::dispatch($this); + return $status; } @@ -700,11 +720,15 @@ public function delete($expunge = true) { * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function restore($expunge = true) { $status = $this->unsetFlag("Deleted"); if($expunge) $this->client->expunge(); + $event = $this->getEvent("message", "restored"); + $event::dispatch($this); + return $status; } @@ -715,6 +739,7 @@ public function restore($expunge = true) { * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function setFlag($flag) { $this->client->openFolder($this->folder_path); @@ -722,6 +747,9 @@ public function setFlag($flag) { $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "+"); $this->parseFlags(); + $event = $this->getEvent("flag", "new"); + $event::dispatch($this); + return $status; } @@ -732,6 +760,7 @@ public function setFlag($flag) { * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function unsetFlag($flag) { $this->client->openFolder($this->folder_path); @@ -740,6 +769,9 @@ public function unsetFlag($flag) { $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "-"); $this->parseFlags(); + $event = $this->getEvent("flag", "deleted"); + $event::dispatch($this); + return $status; } diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php new file mode 100644 index 00000000..74157b41 --- /dev/null +++ b/src/Traits/HasEvents.php @@ -0,0 +1,72 @@ +events[$section])) { + $this->events[$section][$event] = $class; + } + } + + /** + * Set all events + * @param $events + */ + public function setEvents($events) { + $this->events = $events; + } + + /** + * Get a specific event callback + * @param $section + * @param $event + * + * @return callable + * @throws EventNotFoundException + */ + public function getEvent($section, $event) { + if (isset($this->events[$section])) { + return $this->events[$section][$event]; + } + throw new EventNotFoundException(); + } + + /** + * Get all events + * + * @return array + */ + public function getEvents(){ + return $this->events; + } + +} \ No newline at end of file diff --git a/src/config/imap.php b/src/config/imap.php index 61e4bc8c..ac05d362 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -126,6 +126,31 @@ ] ], + /* + |-------------------------------------------------------------------------- + | Available events + |-------------------------------------------------------------------------- + | + */ + 'events' => [ + "message" => [ + 'new' => \Webklex\PHPIMAP\Events\MessageNewEvent::class, + 'moved' => \Webklex\PHPIMAP\Events\MessageMovedEvent::class, + 'copied' => \Webklex\PHPIMAP\Events\MessageCopiedEvent::class, + 'deleted' => \Webklex\PHPIMAP\Events\MessageDeletedEvent::class, + 'restored' => \Webklex\PHPIMAP\Events\MessageRestoredEvent::class, + ], + "folder" => [ + 'new' => \Webklex\PHPIMAP\Events\FolderNewEvent::class, + 'moved' => \Webklex\PHPIMAP\Events\FolderMovedEvent::class, + 'deleted' => \Webklex\PHPIMAP\Events\FolderDeletedEvent::class, + ], + "flag" => [ + 'new' => \Webklex\PHPIMAP\Events\FlagNewEvent::class, + 'deleted' => \Webklex\PHPIMAP\Events\FlagDeletedEvent::class, + ], + ], + /* |-------------------------------------------------------------------------- | Available masking options From 50dd9cb5f3a7bbbbe3a6211cce6ca991bc07eafc Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 22 Sep 2020 00:04:35 +0200 Subject: [PATCH 014/600] Release information updated --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 595c0812..5cde7b3a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,25 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +## [2.1.0] - 2020-09-22 +### Fixed - Quota handling fixed ### Added - Event system and callbacks added ### Affected Classes -- NaN +- [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) +- [Message::class](src/Message.php) ## [2.0.1] - 2020-09-20 ### Fixed From 6c88b6125e684a2149c37aefb29b2d61f849976d Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 22 Sep 2020 19:03:55 +0200 Subject: [PATCH 015/600] Typos fixed --- README.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 16942a2d..428eb39c 100755 --- a/README.md +++ b/README.md @@ -6,12 +6,13 @@ [![Hits][ico-hits]][link-hits] ## Description - PHP-IMAP is a wrapper for common IMAP communication without the need to have the php-imap module installed / enabled. -The functionality is almost completely integrated and even supports IDLE operation. +The protocol is completely integrated and therefore supports IMAP IDLE operation and the "new" oAuth authentication +process as well. +You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if +you want to use old protocols such as pop3. ## Table of Contents - - [Installation](#installation) - [Configuration](#configuration) - [Usage](#usage) @@ -52,14 +53,12 @@ The functionality is almost completely integrated and even supports IDLE operati - [License](#license) ## Installation - 1) Install decoding modules: - ``` shell sudo apt-get install php*-mbstring php*-mcrypt && sudo apache2ctl graceful ``` -1.1) (optional) Install php-imap module if you are having encoding problems: +1.1) (optional) Install php-imap module if you are having encoding problems: ``` shell sudo apt-get install php*-imap && sudo apache2ctl graceful ``` @@ -67,7 +66,6 @@ sudo apt-get install php*-imap && sudo apache2ctl graceful You might also want to check `phpinfo()` if the extensions are enabled. 2) Now install the PHP-IMAP package by running the following command: - ``` shell composer require webklex/php-imap ``` @@ -110,6 +108,7 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `open` — special configuration for imap_open() - `DISABLE_AUTHENTICATOR` — disable authentication properties. - `decoder` — Currently only the message and attachment decoder can be set + - `events` — Default [event handling](#events) config - `masks` — Default [masking](#masking) config - `message` — Default message mask - `attachment` — Default attachment mask @@ -202,7 +201,7 @@ $oClient = new Client([ 'validate_cert' => true, 'username' => 'example@gmail.com', 'password' => 'PASSWORD', - 'authentication' => "oath", + 'authentication' => "oauth", 'protocol' => 'imap' ]); @@ -213,8 +212,6 @@ $oClient->connect(); #### Idle Every time a new message is received, the server will notify the client and return the new message. ``` php -/** @var \Webklex\PHPIMAP\Client $oClient */ - /** @var \Webklex\PHPIMAP\Folder $oFolder */ $oFolder->idle(function($message){ echo $message->subject."\n"; @@ -815,11 +812,7 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum ### Known issues | Error | Solution | | ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| NaN | NaN | - - -## Milestones & upcoming features -* Wiki!! +| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside and use the `legacy-imap` protocol | ## Change log From c108cca409ebe194fcc92a27a48e050a5c569a5b Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 22 Sep 2020 21:18:30 +0200 Subject: [PATCH 016/600] New links added --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 428eb39c..432a7af4 100755 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ process as well. You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if you want to use old protocols such as pop3. +Wiki: [webklex/php-imap/wiki](https://github.com/Webklex/php-imap/wiki) + +Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) + ## Table of Contents - [Installation](#installation) - [Configuration](#configuration) @@ -496,10 +500,10 @@ class CustomMessageNewEvent extends Webklex\PHPIMAP\Events\MessageNewEvent { /** @var \Webklex\PHPIMAP\Client $client */ $client->setEvent("message", "new", CustomMessageNewEvent::class); ``` -..or set it in your config file under `events.message.new`, +..or set it in your config file under `events.message.new`. #### Masking -Laravel-IMAP already comes with two default masks [MessageMask::class](#messagemaskclass) and [AttachmentMask::class](#attachmentmaskclass). +PHP-IMAP already comes with two default masks [MessageMask::class](#messagemaskclass) and [AttachmentMask::class](#attachmentmaskclass). The masked instance has to be called manually and is designed to add custom functionality. @@ -557,8 +561,8 @@ echo $mask->token().'@'.$mask->uid; ``` Additional examples can be found here: -- [Custom message mask](https://github.com/Webklex/laravel-imap/blob/master/examples/custom_message_mask.php) -- [Custom attachment mask](https://github.com/Webklex/laravel-imap/blob/master/examples/custom_attachment_mask.php) +- [Custom message mask](https://github.com/Webklex/php-imap/blob/master/examples/custom_message_mask.php) +- [Custom attachment mask](https://github.com/Webklex/php-imap/blob/master/examples/custom_attachment_mask.php) #### Specials From 5424c17ab28ba42a570e57d475c40b587e2cfc93 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 23 Sep 2020 01:33:45 +0200 Subject: [PATCH 017/600] Missing default config parameter added --- src/config/imap.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/imap.php b/src/config/imap.php index ac05d362..c81732af 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -75,6 +75,7 @@ 'validate_cert' => true, 'username' => '', 'password' => '', + 'authentication' => null, ] */ ], From c6cdd434ad5786f6b1f3c2f1753f233d47fde336 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 23 Sep 2020 01:34:04 +0200 Subject: [PATCH 018/600] Default account config fallback added --- CHANGELOG.md | 10 ++++++++++ src/Client.php | 42 +++++++++++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cde7b3a..14d977ca 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.1] - 2020-09-23 +### Fixed +- Missing default config parameter added + +### Added +- Default account config fallback added + +### Affected Classes +- [Client::class](src/Client.php) + ## [2.1.0] - 2020-09-22 ### Fixed - Quota handling fixed diff --git a/src/Client.php b/src/Client.php index 6ee06883..54a3a41b 100755 --- a/src/Client.php +++ b/src/Client.php @@ -33,7 +33,6 @@ class Client { use HasEvents; - /** * @var boolean|Protocol */ @@ -103,14 +102,6 @@ class Client { */ protected $active_folder = false; - /** - * All valid and available account config parameters - * - * @var array $validConfigKeys - */ - protected $valid_config_keys = ['host', 'port', 'encryption', 'validate_cert', 'username', 'password', 'protocol', - 'authentication']; - /** * @var string $default_message_mask */ @@ -121,6 +112,20 @@ class Client { */ protected $default_attachment_mask = AttachmentMask::class; + /** + * @var array $default_account_config + */ + protected $default_account_config = [ + 'host' => 'localhost', + 'port' => 993, + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'validate_cert' => true, + 'username' => '', + 'password' => '', + 'authentication' => null, + ]; + /** * Client constructor. * @param array $config @@ -151,13 +156,28 @@ public function setConfig(array $config) { $default_account = ClientManager::get('default'); $default_config = ClientManager::get("accounts.$default_account"); - foreach ($this->valid_config_keys as $key) { - $this->$key = isset($config[$key]) ? $config[$key] : $default_config[$key]; + foreach ($this->default_account_config as $key => $value) { + $this->setAccountConfig($key, $config, $default_config); } return $this; } + /** + * @param string $key + * @param array $config + * @param array $default_config + */ + private function setAccountConfig($key, $config, $default_config){ + $value = $this->default_account_config[$key]; + if(isset($config[$key])) { + $value = $config[$key]; + }elseif(isset($default_config[$key])) { + $value = $default_config[$key]; + } + $this->$key = $value; + } + /** * Look for a possible events in any available config * @param $config From eafbd3679eb953bbbb44696a524beff09262dc78 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 24 Sep 2020 21:26:59 +0200 Subject: [PATCH 019/600] Travis config updated --- .travis.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 07213f9e..f09b9fe8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,30 @@ language: php php: - - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 + - 7.3 + - hhvm matrix: fast_finish: true + allow_failures: + - php: 5.6 + - php: 7.0 + - php: 7.1 + - php: hhvm sudo: false -install: composer install --no-interaction +before-install: + - COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-source --no-interaction --dev + +install: + - COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction notifications: email: on_success: always on_failure: always - From 993a628aed3fc0d421e3b55a0d8544bbb0811558 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 24 Sep 2020 21:48:24 +0200 Subject: [PATCH 020/600] Documentation updated --- README.md | 365 +++++++++++++++++++++++++++++------------------------- 1 file changed, 196 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index 432a7af4..7d4a1787 100755 --- a/README.md +++ b/README.md @@ -1,21 +1,24 @@ # IMAP Library for PHP [![Latest Version on Packagist][ico-version]][link-packagist] -[![Software License][ico-license]](LICENSE.md) +[![Software License][ico-license]][link-license] +[![Build Status][ico-build]][link-scrutinizer] [![Total Downloads][ico-downloads]][link-downloads] [![Hits][ico-hits]][link-hits] + ## Description PHP-IMAP is a wrapper for common IMAP communication without the need to have the php-imap module installed / enabled. The protocol is completely integrated and therefore supports IMAP IDLE operation and the "new" oAuth authentication process as well. You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if -you want to use old protocols such as pop3. +you want to use legacy protocols such as pop3. Wiki: [webklex/php-imap/wiki](https://github.com/Webklex/php-imap/wiki) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) + ## Table of Contents - [Installation](#installation) - [Configuration](#configuration) @@ -56,28 +59,29 @@ Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) - [Credits](#credits) - [License](#license) + ## Installation -1) Install decoding modules: -``` shell +1.) Install decoding modules: +```shell script sudo apt-get install php*-mbstring php*-mcrypt && sudo apache2ctl graceful ``` -1.1) (optional) Install php-imap module if you are having encoding problems: -``` shell +1.1.) (optional) Install php-imap module if you are having encoding problems: +```shell script sudo apt-get install php*-imap && sudo apache2ctl graceful ``` You might also want to check `phpinfo()` if the extensions are enabled. -2) Now install the PHP-IMAP package by running the following command: -``` shell +2.) Now install the PHP-IMAP package by running the following command: +```shell script composer require webklex/php-imap ``` -3) Create your own custom config file like [config/imap.php](src/config/imap.php): +3.) Create your own custom config file like [config/imap.php](src/config/imap.php): -## Configuration +## Configuration Supported protocols: - `imap` — Use IMAP [default] - `legacy-imap` — Use the php imap module instead @@ -117,26 +121,27 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `message` — Default message mask - `attachment` — Default attachment mask + ## Usage #### Basic usage example This is a basic example, which will echo out all Mails within all imap folders and will move every message into INBOX.read. Please be aware that this should not be -tested in real live but it gives an impression on how things work. +tested in real life and is only meant to gives an impression on how things work. -``` php +```php use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Client; $cm = new ClientManager('path/to/config/imap.php'); // or use an array of options instead -$cm = new ClientManager(array $options = []); +$cm = new ClientManager($options = []); -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$oClient = $cm->account('account_identifier'); +/** @var \Webklex\PHPIMAP\Client $client */ +$client = $cm->account('account_identifier'); // or create a new instance manually -$oClient = new Client([ +$client = new Client([ 'host' => 'somehost.com', 'port' => 993, 'encryption' => 'ssl', @@ -147,28 +152,28 @@ $oClient = new Client([ ]); //Connect to the IMAP Server -$oClient->connect(); +$client->connect(); //Get all Mailboxes -/** @var \Webklex\PHPIMAP\Support\FolderCollection $aFolder */ -$aFolder = $oClient->getFolders(); +/** @var \Webklex\PHPIMAP\Support\FolderCollection $folders */ +$folders = $client->getFolders(); //Loop through every Mailbox -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -foreach($aFolder as $oFolder){ +/** @var \Webklex\PHPIMAP\Folder $folder */ +foreach($folders as $folder){ -    //Get all Messages of the current Mailbox $oFolder -    /** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -    $aMessage = $oFolder->messages()->all()->get(); + //Get all Messages of the current Mailbox $folder + /** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ + $messages = $folder->messages()->all()->get(); - /** @var \Webklex\PHPIMAP\Message $oMessage */ - foreach($aMessage as $oMessage){ - echo $oMessage->getSubject().'
'; - echo 'Attachments: '.$oMessage->getAttachments()->count().'
'; - echo $oMessage->getHTMLBody(true); + /** @var \Webklex\PHPIMAP\Message $message */ + foreach($messages as $message){ + echo $message->getSubject().'
'; + echo 'Attachments: '.$message->getAttachments()->count().'
'; + echo $message->getHTMLBody(); //Move the current Message to 'INBOX.read' - if($oMessage->moveToFolder('INBOX.read') == true){ + if($message->moveToFolder('INBOX.read') == true){ echo 'Message has ben moved'; }else{ echo 'Message could not be moved'; @@ -177,28 +182,32 @@ foreach($aFolder as $oFolder){ } ``` + #### Folder / Mailbox List all available folders: -``` php -/** @var \Webklex\PHPIMAP\Client $oClient */ +```php +/** @var \Webklex\PHPIMAP\Client $client */ -/** @var \Webklex\PHPIMAP\Support\FolderCollection $aFolder */ -$aFolder = $oClient->getFolders(); +/** @var \Webklex\PHPIMAP\Support\FolderCollection $folders */ +$folders = $client->getFolders(); ``` Get a specific folder: -``` php -/** @var \Webklex\PHPIMAP\Client $oClient */ +```php +/** @var \Webklex\PHPIMAP\Client $client */ -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -$oFolder = $oClient->getFolder('INBOX.name'); +/** @var \Webklex\PHPIMAP\Folder $folder */ +$folder = $client->getFolder('INBOX.name'); ``` + #### oAuth If you are using google mail or something similar, you might want to use oauth instead: -``` php -/** @var \Webklex\PHPIMAP\Client $oClient */ -$oClient = new Client([ +```php +use Webklex\PHPIMAP\Client; + +/** @var \Webklex\PHPIMAP\Client $client */ +$client = new Client([ 'host' => 'imap.gmail.com', 'port' => 993, 'encryption' => 'ssl', @@ -210,84 +219,82 @@ $oClient = new Client([ ]); //Connect to the IMAP Server -$oClient->connect(); +$client->connect(); ``` + #### Idle Every time a new message is received, the server will notify the client and return the new message. -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -$oFolder->idle(function($message){ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ +$folder->idle(function($message){ echo $message->subject."\n"; }); ``` + #### Search for messages Search for specific emails: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ //Get all messages -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->all()->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->all()->get(); //Get all messages from example@domain.com -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->from('example@domain.com')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->from('example@domain.com')->get(); //Get all messages since march 15 2018 -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->since('15.03.2018')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->since('15.03.2018')->get(); //Get all messages within the last 5 days -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->since(now()->subDays(5))->get(); -//Or for older laravel versions.. -$aMessage = $oFolder->query()->since(\Carbon::now()->subDays(5))->get(); +$messages = $folder->query()->since(\Carbon\Carbon::now()->subDays(5))->get(); //Get all messages containing "hello world" -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->text('hello world')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->text('hello world')->get(); //Get all unseen messages containing "hello world" -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->unseen()->text('hello world')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->unseen()->text('hello world')->get(); //Extended custom search query for all messages containing "hello world" //and have been received since march 15 2018 -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->text('hello world')->since('15.03.2018')->get(); -$aMessage = $oFolder->query()->Text('hello world')->Since('15.03.2018')->get(); -$aMessage = $oFolder->query()->whereText('hello world')->whereSince('15.03.2018')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->text('hello world')->since('15.03.2018')->get(); +$messages = $folder->query()->Text('hello world')->Since('15.03.2018')->get(); +$messages = $folder->query()->whereText('hello world')->whereSince('15.03.2018')->get(); // Build a custom search query -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query() -->where([['TEXT', 'Hello world'], ['SINCE', \Carbon::parse('15.03.2018')]]) +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query() +->where([['TEXT', 'Hello world'], ['SINCE', \Carbon\Carbon::parse('15.03.2018')]]) ->get(); //!EXPERIMENTAL! //Get all messages NOT containing "hello world" -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->notText('hello world')->get(); -$aMessage = $oFolder->query()->not_text('hello world')->get(); -$aMessage = $oFolder->query()->not()->text('hello world')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->notText('hello world')->get(); +$messages = $folder->query()->not_text('hello world')->get(); +$messages = $folder->query()->not()->text('hello world')->get(); //Get all messages by custom search criteria -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->where(["CUSTOM_FOOBAR" => "fooBar"]])->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->where(["CUSTOM_FOOBAR" => "fooBar"]])->get(); ``` Available search aliases for a better code reading: -``` php +```php // Folder::search() is just an alias for Folder::query() -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->search()->text('hello world')->since('15.03.2018')->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->search()->text('hello world')->since('15.03.2018')->get(); // Folder::messages() is just an alias for Folder::query() -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->messages()->text('hello world')->since('15.03.2018')->get(); - +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->messages()->text('hello world')->since('15.03.2018')->get(); ``` All available query / search methods can be found here: [Query::class](src/Query/WhereQuery.php) @@ -325,55 +332,58 @@ Further information: - https://tools.ietf.org/html/rfc822 - https://gist.github.com/martinrusev/6121028 + #### Result limiting Limiting the request emails: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ //Get all messages for page 2 since march 15 2018 where each page contains 10 messages -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->since('15.03.2018')->limit(10, 2)->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->since('15.03.2018')->limit(10, 2)->get(); ``` + #### Counting messages Count all available messages matching the current search criteria: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ //Count all messages -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$count = $oFolder->query()->all()->count(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$count = $folder->query()->all()->count(); //Count all messages since march 15 2018 -$count = $oFolder->query()->since('15.03.2018')->count(); +$count = $folder->query()->since('15.03.2018')->count(); ``` + #### Pagination Paginate a query: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ /** @var \Illuminate\Pagination\LengthAwarePaginator $paginator */ -$paginator = $oFolder->query()->since('15.03.2018')->paginate(); +$paginator = $folder->query()->since('15.03.2018')->paginate(); ``` Paginate a message collection: -``` php -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ +```php +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ /** @var \Illuminate\Pagination\LengthAwarePaginator $paginator */ -$paginator = $aMessage->paginate(); +$paginator = $messages->paginate(); ``` View example for a paginated list: -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ /** @var \Illuminate\Pagination\LengthAwarePaginator $paginator */ -$paginator = $oFolder->search() -->since(\Carbon::now()->subDays(14))->get() +$paginator = $folder->search() +->since(\Carbon\Carbon::now()->subDays(14))->get() ->paginate($perPage = 5, $page = null, $pageName = 'imap_blade_example'); ``` -``` html +```html @@ -385,12 +395,12 @@ $paginator = $oFolder->search() count() > 0): ?> - + - - - - + + + + @@ -409,66 +419,71 @@ $paginator = $oFolder->search() #### View examples You can find a few blade examples under [/examples](examples). + #### Fetch a specific message Get a specific message by uid (Please note that the uid is not unique and can change): -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ -/** @var \Webklex\PHPIMAP\Message $oMessage */ -$oMessage = $oFolder->query()->getMessage($msgn = 1); +/** @var \Webklex\PHPIMAP\Message $message */ +$message = $folder->query()->getMessage($msgn = 1); ``` + #### Message flags Flag or "unflag" a message: -``` php -/** @var \Webklex\PHPIMAP\Message $oMessage */ -$oMessage->setFlag(['Seen', 'Spam']); -$oMessage->unsetFlag('Spam'); +```php +/** @var \Webklex\PHPIMAP\Message $message */ +$message->setFlag(['Seen', 'Spam']); +$message->unsetFlag('Spam'); ``` + #### Attachments Save message attachments: -``` php -/** @var \Webklex\PHPIMAP\Message $oMessage */ +```php +/** @var \Webklex\PHPIMAP\Message $message */ -/** @var \Webklex\PHPIMAP\Support\AttachmentCollection $aAttachment */ -$aAttachment = $oMessage->getAttachments(); +/** @var \Webklex\PHPIMAP\Support\AttachmentCollection $attachments */ +$attachments = $message->getAttachments(); -$aAttachment->each(function ($oAttachment) { - /** @var \Webklex\PHPIMAP\Attachment $oAttachment */ - $oAttachment->save("/some/path/"); +$attachments->each(function ($attachment) { + /** @var \Webklex\PHPIMAP\Attachment $attachment */ + $attachment->save("/some/path/"); }); ``` + #### Advanced fetching Fetch messages without body fetching (decrease load): -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->whereText('Hello world')->setFetchBody(false)->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereText('Hello world')->setFetchBody(false)->get(); -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->whereAll()->setFetchBody(false)->get(); +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereAll()->setFetchBody(false)->get(); ``` Fetch messages without body, flag and attachment fetching (decrease load): -``` php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->whereText('Hello world') +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereText('Hello world') ->setFetchFlags(false) ->setFetchBody(false) ->get(); -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->whereAll() +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereAll() ->setFetchFlags(false) ->setFetchBody(false) ->get(); ``` + #### Events The following events are available: - `Webklex\PHPIMAP\Events\MessageNewEvent($message)` — can get triggered by `Folder::idle` @@ -483,12 +498,12 @@ The following events are available: - `Webklex\PHPIMAP\Events\FolderMovedEvent($old_folder, $new_folder)` — triggered by `Folder::move` Create and register your own custom event: -``` php +```php class CustomMessageNewEvent extends Webklex\PHPIMAP\Events\MessageNewEvent { /** * Create a new event instance. - * @var Message[] $messages + * @var \Webklex\PHPIMAP\Message[] $messages * @return void */ public function __construct($messages) { @@ -502,48 +517,49 @@ $client->setEvent("message", "new", CustomMessageNewEvent::class); ``` ..or set it in your config file under `events.message.new`. + #### Masking PHP-IMAP already comes with two default masks [MessageMask::class](#messagemaskclass) and [AttachmentMask::class](#attachmentmaskclass). The masked instance has to be called manually and is designed to add custom functionality. You can call the default mask by calling the mask method without any arguments. -``` php -/** @var \Webklex\PHPIMAP\Message $oMessage */ -$mask = $oMessage->mask(); +```php +/** @var \Webklex\PHPIMAP\Message $message */ +$mask = $message->mask(); ``` There are several methods available to set the default mask: -``` php -/** @var \Webklex\PHPIMAP\Client $oClient */ -/** @var \Webklex\PHPIMAP\Message $oMessage */ +```php +/** @var \Webklex\PHPIMAP\Client $client */ +/** @var \Webklex\PHPIMAP\Message $message */ $message_mask = \Webklex\PHPIMAP\Support\Masks\MessageMask::class; -$oClient->setDefaultMessageMask($message_mask); -$oMessage->setMask($message_mask); -$mask = $oMessage->mask($message_mask); +$client->setDefaultMessageMask($message_mask); +$message->setMask($message_mask); +$mask = $message->mask($message_mask); ``` The last one wont set the mask but generate a masked instance using the provided mask. You could also set the default masks inside your `config/imap.php` file under `masks`. You can also apply a mask on [attachments](#attachmentclass): -``` php -/** @var \Webklex\PHPIMAP\Client $oClient */ -/** @var \Webklex\PHPIMAP\Attachment $oAttachment */ +```php +/** @var \Webklex\PHPIMAP\Client $client */ +/** @var \Webklex\PHPIMAP\Attachment $attachment */ $attachment_mask = \Webklex\PHPIMAP\Support\Masks\AttachmentMask::class; -$oClient->setDefaultAttachmentMask($attachment_mask); -$oAttachment->setMask($attachment_mask); -$mask = $oAttachment->mask($attachment_mask); +$client->setDefaultAttachmentMask($attachment_mask); +$attachment->setMask($attachment_mask); +$mask = $attachment->mask($attachment_mask); ``` If you want to implement your own mask just extend [MessageMask::class](#messagemaskclass), [AttachmentMask::class](#attachmentmaskclass) or [Mask::class](#maskclass) and implement your desired logic: -``` php -/** @var \Webklex\PHPIMAP\Message $oMessage */ +```php +/** @var \Webklex\PHPIMAP\Message $message */ class CustomMessageMask extends \Webklex\PHPIMAP\Support\Masks\MessageMask { /** @@ -555,7 +571,7 @@ class CustomMessageMask extends \Webklex\PHPIMAP\Support\Masks\MessageMask { } } -$mask = $oMessage->mask(CustomMessageMask::class); +$mask = $message->mask(CustomMessageMask::class); echo $mask->token().'@'.$mask->uid; ``` @@ -567,16 +583,20 @@ Additional examples can be found here: #### Specials Find the folder containing a message: -``` php -$oFolder = $aMessage->getFolder(); +```php +/** @var \Webklex\PHPIMAP\Message $message */ +$folder = $message->getFolder(); ``` + ## Support If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). However please be aware that it might take some time to get an answer. +Off topic, rude or abusive issues will be deleted without any notice. If you need **immediate** or **commercial** support, feel free to send me a mail at github@webklex.com. + ##### A little notice If you write source code in your issue, please consider to format it correctly. This makes it so much nicer to read and people are more likely to comment and help :) @@ -588,21 +608,20 @@ echo 'your php code...'; ``` will turn into: -``` php +```php echo 'your php code...'; ``` + ### Features & pull requests Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) if you're just wishing a feature ;) ->Off topic, rude or abusive issues will be deleted without any notice. ## Documentation ### [Client::class](src/Client.php) - | Method | Arguments | Return | Description | | ------------------------- | ------------------------------------------------------------------------------- | :---------------: | ---------------------------------------------------------------------------------------------------------------------------- | | setConfig | array $config | self | Set the Client configuration. Take a look at `config/imap.php` for more inspiration. | @@ -627,8 +646,8 @@ if you're just wishing a feature ;) | getDefaultAttachmentMask | | string | Get the current default attachment mask class name | | getFolderPath | | string | Get the current folder path | -### [Message::class](src/Message.php) +### [Message::class](src/Message.php) | Method | Arguments | Return | Description | | --------------- | ----------------------------- | :------------------: | -------------------------------------- | | parseBody | | Message | Parse the Message body | @@ -673,8 +692,8 @@ if you're just wishing a feature ;) | setMask | string $mask | Message | Set the mask class | | getMask | | string | Get the current mask class name | -### [Folder::class](src/Folder.php) +### [Folder::class](src/Folder.php) | Method | Arguments | Return | Description | | ----------------- | ----------------------------------------------------------------------------------- | :---------------: | ---------------------------------------------- | | hasChildren | | bool | Determine if folder has children. | @@ -692,9 +711,9 @@ if you're just wishing a feature ;) | query | string $charset = 'UTF-8' | WhereQuery | Get the current Client instance | | messages | string $charset = 'UTF-8' | WhereQuery | Alias for Folder::query() | | search | string $charset = 'UTF-8' | WhereQuery | Alias for Folder::query() | - -### [Query::class](src/Query/WhereQuery.php) + +### [Query::class](src/Query/WhereQuery.php) | Method | Arguments | Return | Description | | ------------------ | --------------------------------- | :---------------: | ---------------------------------------------- | | where | mixed $criteria, $value = null | WhereQuery | Add new criteria to the current query | @@ -741,7 +760,6 @@ if you're just wishing a feature ;) ### [Attachment::class](src/Attachment.php) - | Method | Arguments | Return | Description | | -------------- | ------------------------------ | :------------: | ------------------------------------------------------ | | getContent | | string or null | Get attachment content | @@ -759,8 +777,8 @@ if you're just wishing a feature ;) | setMask | string $mask | Attachment | Set the mask class | | getMask | | string | Get the current mask class name | -### [Mask::class](src/Support/Masks/Mask.php) +### [Mask::class](src/Support/Masks/Mask.php) | Method | Arguments | Return | Description | | -------------- | ------------------------------ | :------------: | ------------------------------------------------------ | | getParent | | Masked parent | Get the masked parent object | @@ -769,8 +787,8 @@ if you're just wishing a feature ;) | __set | | mixed | Set any cloned parent attribute | | __inherit | | mixed | All public methods of the given parent are callable | -### [MessageMask::class](src/Support/Masks/MessageMask.php) +### [MessageMask::class](src/Support/Masks/MessageMask.php) | Method | Arguments | Return | Description | | ----------------------------------- | -------------------------------------- | :------------: | ------------------------------------------ | | getHtmlBody | | string or null | Get HTML body | @@ -778,13 +796,14 @@ if you're just wishing a feature ;) | getHTMLBodyWithEmbeddedBase64Images | | string or null | Get HTML body with embedded base64 images | | getHTMLBodyWithEmbeddedUrlImages | string $route_name, array $params = [] | string or null | Get HTML body with embedded routed images | -### [AttachmentMask::class](src/Support/Masks/AttachmentMask.php) +### [AttachmentMask::class](src/Support/Masks/AttachmentMask.php) | Method | Arguments | Return | Description | | -------------- | ------------------------------ | :------------: | ------------------------------------------------------ | | getContentBase64Encoded | | string or null | Get attachment content | | getImageSrc | | string or null | Get attachment mime type | + ### [MessageCollection::class](src/Support/MessageCollection.php) Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) @@ -792,6 +811,7 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum | -------- | --------------------------------------------------- | :------------------: | -------------------------------- | | paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | + ### [FlagCollection::class](src/Support/FlagCollection.php) Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) @@ -799,6 +819,7 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum | -------- | --------------------------------------------------- | :------------------: | -------------------------------- | | paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | + ### [AttachmentCollection::class](src/Support/AttachmentCollection.php) Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) @@ -806,6 +827,7 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum | -------- | --------------------------------------------------- | :------------------: | -------------------------------- | | paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | + ### [FolderCollection::class](src/Support/FolderCollection.php) Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) @@ -813,28 +835,29 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum | -------- | --------------------------------------------------- | :------------------: | -------------------------------- | | paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | + ### Known issues | Error | Solution | | ------------------------------------------------------------------------- | ---------------------------------------------------------- | | Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside and use the `legacy-imap` protocol | + ## Change log +Please see [CHANGELOG][link-changelog] for more information what has changed recently. -Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. ## Security - If you discover any security related issues, please email github@webklex.com instead of using the issue tracker. -## Credits +## Credits - [Webklex][link-author] - [All Contributors][link-contributors] ## License +The MIT License (MIT). Please see [License File][link-license] for more information. -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. [ico-version]: https://img.shields.io/packagist/v/Webklex/php-imap.svg?style=flat-square [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square @@ -842,6 +865,8 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/Webklex/php-imap.svg?style=flat-square [ico-code-quality]: https://img.shields.io/scrutinizer/g/Webklex/php-imap.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/Webklex/php-imap.svg?style=flat-square +[ico-build]: https://img.shields.io/scrutinizer/build/g/Webklex/php-imap/master?style=flat-square +[ico-quality]: https://img.shields.io/scrutinizer/quality/g/Webklex/php-imap/master?style=flat-square [ico-hits]: https://hits.webklex.com/svg/webklex/php-imap [link-packagist]: https://packagist.org/packages/Webklex/php-imap @@ -851,5 +876,7 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [link-downloads]: https://packagist.org/packages/Webklex/php-imap [link-author]: https://github.com/webklex [link-contributors]: https://github.com/Webklex/php-imap/graphs/contributors +[link-license]: https://github.com/Webklex/php-imap/blob/master/LICENSE +[link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md [link-jetbrains]: https://www.jetbrains.com [link-hits]: https://hits.webklex.com From 84e778ea996c62d32cba0470a1001f4d57f103be Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 24 Sep 2020 22:04:38 +0200 Subject: [PATCH 021/600] Examples updated --- examples/custom_attachment_mask.php | 10 +++++----- examples/custom_message_mask.php | 8 ++++---- examples/folder_structure.blade.php | 10 +++++----- examples/message_table.blade.php | 25 ++++++++++++------------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/examples/custom_attachment_mask.php b/examples/custom_attachment_mask.php index 177ed6f9..32b7b91e 100644 --- a/examples/custom_attachment_mask.php +++ b/examples/custom_attachment_mask.php @@ -33,14 +33,14 @@ public function custom_save() { } -/** @var \Webklex\PHPIMAP\Client $oClient */ +/** @var \Webklex\PHPIMAP\Client $client */ $cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); -$oClient = $cm->account('default'); -$oClient->connect(); -$oClient->setDefaultAttachmentMask(CustomAttachmentMask::class); +$client = $cm->account('default'); +$client->connect(); +$client->setDefaultAttachmentMask(CustomAttachmentMask::class); /** @var \Webklex\PHPIMAP\Folder $folder */ -$folder = $oClient->getFolder('INBOX'); +$folder = $client->getFolder('INBOX'); /** @var \Webklex\PHPIMAP\Message $message */ $message = $folder->query()->limit(1)->get()->first(); diff --git a/examples/custom_message_mask.php b/examples/custom_message_mask.php index 855c4385..25d05667 100644 --- a/examples/custom_message_mask.php +++ b/examples/custom_message_mask.php @@ -30,13 +30,13 @@ public function getAttachmentCount() { } -/** @var \Webklex\PHPIMAP\Client $oClient */ +/** @var \Webklex\PHPIMAP\Client $client */ $cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); -$oClient = $cm->account('default'); -$oClient->connect(); +$client = $cm->account('default'); +$client->connect(); /** @var \Webklex\PHPIMAP\Folder $folder */ -$folder = $oClient->getFolder('INBOX'); +$folder = $client->getFolder('INBOX'); /** @var \Webklex\PHPIMAP\Message $message */ $message = $folder->query()->limit(1)->get()->first(); diff --git a/examples/folder_structure.blade.php b/examples/folder_structure.blade.php index 701e991c..a80dfb6c 100644 --- a/examples/folder_structure.blade.php +++ b/examples/folder_structure.blade.php @@ -12,7 +12,7 @@ /** * @var \Webklex\PHPIMAP\Support\FolderCollection $paginator - * @var \Webklex\PHPIMAP\Folder $oFolder + * @var \Webklex\PHPIMAP\Folder $folder */ ?> @@ -25,17 +25,17 @@ count() > 0): ?> - + - - + + - +
getUid(); ?>getSubject(); ?>getFrom()[0]->mail; ?>getAttachments()->count() > 0 ? 'yes' : 'no'; ?>getUid(); ?>getSubject(); ?>getFrom()[0]->mail; ?>getAttachments()->count() > 0 ? 'yes' : 'no'; ?>
name; ?>search()->unseen()->leaveUnread()->setFetchBody(false)->setFetchAttachment(false)->get()->count(); ?>name; ?>search()->unseen()->setFetchBody(false)->count(); ?>
No folders found
diff --git a/examples/message_table.blade.php b/examples/message_table.blade.php index 517a399b..c3bd7af8 100644 --- a/examples/message_table.blade.php +++ b/examples/message_table.blade.php @@ -12,8 +12,7 @@ /** * @var \Webklex\PHPIMAP\Support\FolderCollection $paginator - * @var \Webklex\PHPIMAP\Folder $oFolder - * @var \Webklex\PHPIMAP\Message $oMessage + * @var \Webklex\PHPIMAP\Message $message */ ?> @@ -28,18 +27,18 @@ count() > 0): ?> - - - getUid(); ?> - getSubject(); ?> - getFrom()[0]->mail; ?> - getAttachments()->count() > 0 ? 'yes' : 'no'; ?> - - + + + getUid(); ?> + getSubject(); ?> + getFrom()[0]->mail; ?> + getAttachments()->count() > 0 ? 'yes' : 'no'; ?> + + - - No messages found - + + No messages found + From 195fbd4a3118c646137487c6ae644c48522e5ed0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 24 Sep 2020 22:10:40 +0200 Subject: [PATCH 022/600] phpunit added --- .travis.yml | 3 +++ phpunit.xml.dist | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 phpunit.xml.dist diff --git a/.travis.yml b/.travis.yml index f09b9fe8..c9fb4d2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,9 @@ before-install: install: - COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction +script: + - phpunit + notifications: email: on_success: always diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..df4426e4 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + From cb8daa8c2f419f7bdc247ebbb6594f5d4a2dc3f7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 28 Sep 2020 12:31:01 +0200 Subject: [PATCH 023/600] Dependency problem in Attachement::getExtension() fixed --- CHANGELOG.md | 7 +++++++ src/Attachment.php | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d977ca..be4589ce 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.2] - 2020-09-28 +### Fixed +- Dependency problem in `Attachement::getExtension()` fixed (#9) + +### Affected Classes +- [Attachment::class](src/Attachment.php) + ## [2.1.1] - 2020-09-23 ### Fixed - Missing default config parameter added diff --git a/src/Attachment.php b/src/Attachment.php index 802e1e67..d64fc75f 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -14,7 +14,6 @@ use Illuminate\Support\Str; use Illuminate\Support\Facades\File; -use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; @@ -261,7 +260,13 @@ public function getMimeType(){ * @return string|null */ public function getExtension(){ - return ExtensionGuesser::getInstance()->guess($this->getMimeType()); + $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; + if (class_exists($deprecated_guesser) !== false){ + return $deprecated_guesser::getInstance()->guess($this->getMimeType()); + } + $guesser = "\Symfony\Component\Mime\MimeTypes"; + $extensions = $guesser::getDefault()->getExtensions($this->getMimeType()); + return isset($extensions[0]) ? $extensions[0] : null; } /** From c15e8eeb269312cc1d6ae530080a50c80582a8c8 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 29 Sep 2020 20:36:10 +0200 Subject: [PATCH 024/600] Possible decoding problem fixed --- CHANGELOG.md | 8 ++++++++ src/Header.php | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be4589ce..20ed5407 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.3] - 2020-09-29 +### Fixed +- Possible decoding problem fixed +- `Str::class` dependency removed from `Header::class` + +### Affected Classes +- [Header::class](src/Header.php) + ## [2.1.2] - 2020-09-28 ### Fixed - Dependency problem in `Attachement::getExtension()` fixed (#9) diff --git a/src/Header.php b/src/Header.php index c3cd028c..1cbfd71c 100644 --- a/src/Header.php +++ b/src/Header.php @@ -14,7 +14,6 @@ use Carbon\Carbon; -use Illuminate\Support\Str; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; @@ -67,7 +66,7 @@ public function __construct($raw_header) { */ public function __call($method, $arguments) { if(strtolower(substr($method, 0, 3)) === 'get') { - $name = Str::snake(substr($method, 3)); + $name = preg_replace('/(.)(?=[A-Z])/u', '$1_', substr(strtolower($method), 3)); if(in_array($name, array_keys($this->attributes))) { return $this->attributes[$name]; @@ -320,11 +319,16 @@ private function decode($value) { if ($value !== null) { if($decoder === 'utf-8' && extension_loaded('imap')) { $value = \imap_utf8($value); - if (Str::startsWith(mb_strtolower($value), '=?utf-8?')) { + if (strpos(strtolower($value), '=?utf-8?') === 0) { $value = mb_decode_mimeheader($value); } if ($this->notDecoded($original_value, $value)) { - $value = $this->mime_header_decode(imap_utf8($value)); + $decoded_value = $this->mime_header_decode($value); + if (count($decoded_value) > 0) { + if(property_exists($decoded_value[0], "text")) { + $value = $decoded_value[0]->text; + } + } } }elseif($decoder === 'iconv') { $value = iconv_mime_decode($value); @@ -332,6 +336,10 @@ private function decode($value) { $value = mb_decode_mimeheader($value); } + if (strpos(strtolower($value), '=?utf-8?') === 0) { + $value = mb_decode_mimeheader($value); + } + if ($this->notDecoded($original_value, $value)) { $value = $this->convertEncoding($original_value, $this->getEncoding($original_value)); } From 916c780f1ec01ecd546a0a338eba1b95b2989df0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 29 Sep 2020 20:53:49 +0200 Subject: [PATCH 025/600] Spacings and docs updated --- src/Message.php | 13 +------------ src/Traits/HasEvents.php | 3 ++- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Message.php b/src/Message.php index 5a57cea1..dd9272b1 100755 --- a/src/Message.php +++ b/src/Message.php @@ -390,7 +390,6 @@ public function parseBody() { * Fetch the Message structure * * @param $structure - * @param mixed $partNumber * * @throws Exceptions\ConnectionFailedException */ @@ -404,7 +403,6 @@ private function fetchStructure($structure) { /** * @param Part $part - * @throws Exceptions\ConnectionFailedException */ private function fetchPart(Part $part) { @@ -456,10 +454,7 @@ private function fetchPart(Part $part) { /** * Fetch the Message attachment - * * @param Part $part - * - * @throws Exceptions\ConnectionFailedException */ protected function fetchAttachment($part) { @@ -476,7 +471,6 @@ protected function fetchAttachment($part) { /** * Fail proof setter for $fetch_option - * * @param $option * * @return $this @@ -494,7 +488,6 @@ public function setFetchOption($option) { /** * Fail proof setter for $fetch_body - * * @param $option * * @return $this @@ -512,7 +505,6 @@ public function setFetchBodyOption($option) { /** * Fail proof setter for $fetch_flags - * * @param $option * * @return $this @@ -530,7 +522,6 @@ public function setFetchFlagsOption($option) { /** * Decode a given string - * * @param $string * @param $encoding * @@ -557,7 +548,6 @@ public function decodeString($string, $encoding) { /** * Convert the encoding - * * @param $str * @param string $from * @param string $to @@ -600,7 +590,6 @@ public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { /** * Get the encoding of a given abject - * * @param object|string $structure * * @return string @@ -684,7 +673,7 @@ public function copy($folder) { public function move($folder, $expunge = false) { $message = $this->copy($folder); if ($message !== null) { - $status = $this->delete($expunge); + $this->delete($expunge); $event = $this->getEvent("message", "moved"); $event::dispatch($this, $message); diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index 74157b41..8387b107 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP\Traits; +use Webklex\PHPIMAP\Events\Event; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; /** @@ -50,7 +51,7 @@ public function setEvents($events) { * @param $section * @param $event * - * @return callable + * @return Event * @throws EventNotFoundException */ public function getEvent($section, $event) { From feea41c09e4494f65bb0ef1e660f0386639025bb Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 30 Sep 2020 17:33:59 +0200 Subject: [PATCH 026/600] Fix header extension values --- CHANGELOG.md | 4 ++-- src/Header.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ed5407..b2ac77ff 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix header extension values ### Added - NaN ### Affected Classes -- NaN +- [Header::class](src/Header.php) ## [2.1.3] - 2020-09-29 ### Fixed diff --git a/src/Header.php b/src/Header.php index 1cbfd71c..349d217e 100644 --- a/src/Header.php +++ b/src/Header.php @@ -467,6 +467,7 @@ private function extractHeaderExtensions(){ if (($pos = strpos($extension, "=")) !== false){ $key = substr($extension, 0, $pos); $value = substr($extension, $pos + 1); + $value = str_replace('"', "", $value); $this->attributes[trim(rtrim(strtolower($key)))] = trim(rtrim($value)); } } From 61d00e2f787751c27509b1ff0dc40acd189993be Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 30 Sep 2020 17:39:12 +0200 Subject: [PATCH 027/600] Part header detection method changed (#10) --- CHANGELOG.md | 2 ++ src/Part.php | 36 +++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ac77ff..d7ebc837 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- Part header detection method changed (#10) - Fix header extension values ### Added @@ -13,6 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Header::class](src/Header.php) +- [Part::class](src/Part.php) ## [2.1.3] - 2020-09-29 ### Fixed diff --git a/src/Part.php b/src/Part.php index c602cb88..b31b645e 100644 --- a/src/Part.php +++ b/src/Part.php @@ -104,6 +104,7 @@ class Part { * Part constructor. * @param $raw_part * @param Header $header + * * @throws InvalidMessageDateException */ public function __construct($raw_part, $header = null) { @@ -114,22 +115,16 @@ public function __construct($raw_part, $header = null) { /** * Parse the raw parts + * * @throws InvalidMessageDateException */ protected function parse(){ - $body = $this->raw; - if ($this->header === null) { - $headers = ""; - if (preg_match_all("/(.*)(?s)(?-m)\\r\\n.+?(?=\n([^A-Z]|.{1,2}[^A-Z])|$)/im", $body, $matches)) { - if (isset($matches[0][0])) { - $headers = $matches[0][0]; - $body = substr($body, strlen($headers) + 2, -2); - } - } - - $this->header = new Header($headers); + $body = $this->findHeaders(); + }else{ + $body = $this->raw; } + $this->parseSubtype(); $this->parseDisposition(); $this->parseDescription(); @@ -144,6 +139,25 @@ protected function parse(){ $this->bytes = strlen($this->content); } + /** + * Find all available headers and return the left over body segment + * + * @return string + * @throws InvalidMessageDateException + */ + private function findHeaders(){ + $body = $this->raw; + while (($pos = strpos($body, "\r\n")) > 0) { + $body = substr($body, $pos + 2); + } + $headers = substr($this->raw, 0, strlen($body) * -1); + $body = substr($body, 0, -2); + + $this->header = new Header($headers); + + return (string) $body; + } + private function parseSubtype(){ $content_type = $this->header->get("content-type"); if (($pos = strpos($content_type, "/")) !== false){ From 351345aba9c4e9c6fad56dd9e26e4e6f105e09f0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 30 Sep 2020 17:39:51 +0200 Subject: [PATCH 028/600] Version information added --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ebc837..5be1bf80 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Part header detection method changed (#10) -- Fix header extension values +- NaN ### Added - NaN +### Affected Classes +- NaN + +## [2.1.4] - 2020-09-30 +### Fixed +- Fix header extension values +- Part header detection method changed (#10) + ### Affected Classes - [Header::class](src/Header.php) - [Part::class](src/Part.php) From 03852f31983bbcfeddd79341eb8384a3f42d2397 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 30 Sep 2020 18:12:00 +0200 Subject: [PATCH 029/600] Wrong message content property reference fixed (#10) --- CHANGELOG.md | 4 ++++ src/Message.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5be1bf80..f70bb5e6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.5] - 2020-09-30 +### Fixed +- Wrong message content property reference fixed (#10) + ## [2.1.4] - 2020-09-30 ### Fixed - Fix header extension values diff --git a/src/Message.php b/src/Message.php index dd9272b1..150e8513 100755 --- a/src/Message.php +++ b/src/Message.php @@ -293,7 +293,7 @@ public function getTextBody() { return false; } - return $this->bodies['text']->content; + return $this->bodies['text']; } /** @@ -315,7 +315,7 @@ public function getHTMLBody() { return null; } - return $this->bodies['html']->content; + return $this->bodies['html']; } /** From 872ca7d85314a4c4cfae7472e68d83bf65b995bd Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 2 Oct 2020 00:09:21 +0200 Subject: [PATCH 030/600] Part number added to attachment --- CHANGELOG.md | 2 +- src/Attachment.php | 2 ++ src/Part.php | 9 ++++++++- src/Structure.php | 4 +++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f70bb5e6..500a008f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- Part number added to attachment ### Affected Classes - NaN diff --git a/src/Attachment.php b/src/Attachment.php index d64fc75f..398aeb9b 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -65,6 +65,7 @@ class Attachment { protected $attributes = [ 'content' => null, 'type' => null, + 'part_number' => 0, 'content_type' => null, 'id' => null, 'name' => null, @@ -90,6 +91,7 @@ public function __construct(Message $oMessage, Part $part) { $this->oMessage = $oMessage; $this->part = $part; + $this->part_number = $part->part_number; $default_mask = $this->oMessage->getClient()->getDefaultAttachmentMask(); if($default_mask != null) { diff --git a/src/Part.php b/src/Part.php index b31b645e..2e9cdad7 100644 --- a/src/Part.php +++ b/src/Part.php @@ -90,6 +90,11 @@ class Part { */ public $id = null; + /** + * @var integer $part_number + */ + public $part_number = 0; + /** * @var integer $bytes */ @@ -104,12 +109,14 @@ class Part { * Part constructor. * @param $raw_part * @param Header $header + * @param integer $part_number * * @throws InvalidMessageDateException */ - public function __construct($raw_part, $header = null) { + public function __construct($raw_part, $header = null, $part_number = 0) { $this->raw = $raw_part; $this->header = $header; + $this->part_number = $part_number; $this->parse(); } diff --git a/src/Structure.php b/src/Structure.php index 3d2e0b81..5422386c 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -105,10 +105,12 @@ public function find_parts(){ $raw_parts = explode($boundary, $this->raw); $parts = []; + $part_number = 0; foreach($raw_parts as $part) { $part = trim(rtrim($part)); if ($part !== "--") { - $parts[] = new Part($part); + $parts[] = new Part($part, null, $part_number); + $part_number++; } } return $parts; From a4bd5ea030871c28d7c1cca9b81edb61e63680e5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 2 Oct 2020 00:55:42 +0200 Subject: [PATCH 031/600] Class docs updated --- src/Attachment.php | 32 ++++++++++++--- src/Client.php | 23 +++++++++-- src/ClientManager.php | 8 +--- src/EncodingAliases.php | 1 + src/Folder.php | 11 +---- src/Header.php | 19 ++++++++- src/Message.php | 63 ++++++++++++++++++++++++----- src/Part.php | 45 +++++++++++++++++++-- src/Structure.php | 16 ++++++-- src/Support/Masks/Mask.php | 10 +++++ src/Support/Masks/MessageMask.php | 1 + src/Support/PaginatedCollection.php | 9 +++-- src/Traits/HasEvents.php | 6 ++- 13 files changed, 196 insertions(+), 48 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 398aeb9b..db5b71a3 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -52,16 +52,26 @@ */ class Attachment { - /** @var Message $oMessage */ + /** + * @var Message $oMessage + */ protected $oMessage; - /** @var array $config */ + /** + * Used config + * + * @var array $config + */ protected $config = []; /** @var Part $part */ protected $part; - /** @var array $attributes */ + /** + * Attribute holder + * + * @var array $attributes + */ protected $attributes = [ 'content' => null, 'type' => null, @@ -76,13 +86,13 @@ class Attachment { /** * Default mask + * * @var string $mask */ protected $mask = AttachmentMask::class; /** * Attachment constructor. - * * @param Message $oMessage * @param Part $part */ @@ -131,6 +141,7 @@ public function __call($method, $arguments) { } /** + * Magic setter * @param $name * @param $value * @@ -143,6 +154,7 @@ public function __set($name, $value) { } /** + * magic getter * @param $name * * @return mixed|null @@ -225,7 +237,6 @@ protected function fetch() { /** * Save the attachment content to your filesystem - * * @param string $path * @param string|null $filename * @@ -238,6 +249,7 @@ public function save($path, $filename = null) { } /** + * Set the attachment name and try to decode it * @param $name */ public function setName($name) { @@ -252,6 +264,8 @@ public function setName($name) { } /** + * Get the attachment mime type + * * @return string|null */ public function getMimeType(){ @@ -259,6 +273,8 @@ public function getMimeType(){ } /** + * Try to guess the attachment file extension + * * @return string|null */ public function getExtension(){ @@ -272,6 +288,8 @@ public function getExtension(){ } /** + * Get all attributes + * * @return array */ public function getAttributes(){ @@ -286,7 +304,9 @@ public function getMessage(){ } /** + * Set the default mask * @param $mask + * * @return $this */ public function setMask($mask){ @@ -298,6 +318,8 @@ public function setMask($mask){ } /** + * Get the used default mask + * * @return string */ public function getMask(){ diff --git a/src/Client.php b/src/Client.php index 54a3a41b..179f3205 100755 --- a/src/Client.php +++ b/src/Client.php @@ -34,6 +34,8 @@ class Client { use HasEvents; /** + * Connection resource + * * @var boolean|Protocol */ public $connection = false; @@ -103,16 +105,22 @@ class Client { protected $active_folder = false; /** + * Default message mask + * * @var string $default_message_mask */ protected $default_message_mask = MessageMask::class; /** + * Default attachment mask + * * @var string $default_attachment_mask */ protected $default_attachment_mask = AttachmentMask::class; /** + * Used default account values + * * @var array $default_account_config */ protected $default_account_config = [ @@ -147,7 +155,6 @@ public function __destruct() { /** * Set the Client configuration - * * @param array $config * * @return self @@ -164,6 +171,7 @@ public function setConfig(array $config) { } /** + * Set a specific account config * @param string $key * @param array $config * @param array $default_config @@ -352,6 +360,7 @@ public function disconnect() { /** * Get a folder instance by a folder name * @param $folder_name + * * @return mixed * @throws ConnectionFailedException * @throws FolderFetchingException @@ -400,8 +409,8 @@ public function getFolders($hierarchical = true, $parent_folder = null) { /** * Open folder. - * * @param string $folder + * * @return mixed * @throws ConnectionFailedException */ @@ -441,6 +450,7 @@ public function createFolder($folder, $expunge = true) { /** * Check a given folder * @param $folder + * * @return false|object * @throws ConnectionFailedException */ @@ -471,7 +481,6 @@ public function getQuota() { /** * Retrieve the quota settings per user - * * @param string $quota_root * * @return array @@ -514,6 +523,8 @@ public function getTimeout($type){ } /** + * Get the default message mask + * * @return string */ public function getDefaultMessageMask(){ @@ -521,7 +532,9 @@ public function getDefaultMessageMask(){ } /** + * Get the default events for a given section * @param $section + * * @return array */ public function getDefaultEvents($section){ @@ -529,6 +542,7 @@ public function getDefaultEvents($section){ } /** + * Set the default message mask * @param $mask * * @return $this @@ -545,6 +559,8 @@ public function setDefaultMessageMask($mask) { } /** + * Get the default attachment mask + * * @return string */ public function getDefaultAttachmentMask(){ @@ -552,6 +568,7 @@ public function getDefaultAttachmentMask(){ } /** + * Set the default attachment mask * @param $mask * * @return $this diff --git a/src/ClientManager.php b/src/ClientManager.php index afb7f1b5..db163fc8 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -34,8 +34,7 @@ class ClientManager { protected $accounts = []; /** - * Client constructor. - * + * ClientManager constructor. * @param array|string $config */ public function __construct($config = []) { @@ -44,7 +43,6 @@ public function __construct($config = []) { /** * Dynamically pass calls to the default account. - * * @param string $method * @param array $parameters * @@ -59,7 +57,6 @@ public function __call($method, $parameters) { /** * Get a dotted config parameter - * * @param string $key * @param null $default * @@ -89,7 +86,6 @@ public static function get($key, $default = null) { /** * Resolve a account instance. - * * @param string $name * * @return Client @@ -124,7 +120,6 @@ protected function resolve($name) { /** * Get the account configuration. - * * @param string $name * * @return array @@ -148,7 +143,6 @@ public function getDefaultAccount() { /** * Set the name of the default account. - * * @param string $name * * @return void diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index bc5cdea9..1eb16c13 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -22,6 +22,7 @@ class EncodingAliases { /** * Contains email encoding mappings + * * @var array */ private static $aliases = [ diff --git a/src/Folder.php b/src/Folder.php index 62d36cb3..78bbf09e 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -172,7 +172,6 @@ public function hasChildren() { /** * Set children. - * * @param FolderCollection|array $children * * @return self @@ -186,7 +185,6 @@ public function setChildren($children = []) { /** * Decode name. * It converts UTF7-IMAP encoding to UTF-8. - * * @param $name * * @return mixed|string @@ -197,7 +195,6 @@ protected function decodeName($name) { /** * Get simple name (without parent folders). - * * @param $delimiter * @param $full_name * @@ -211,7 +208,6 @@ protected function getSimpleName($delimiter, $full_name) { /** * Parse attributes and set it to object properties. - * * @param $attributes */ protected function parseAttributes($attributes) { @@ -224,7 +220,6 @@ protected function parseAttributes($attributes) { /** * Move or rename the current folder - * * @param string $new_name * @param boolean $expunge * @@ -248,7 +243,6 @@ public function move($new_name, $expunge = true) { /** * Append a string message to the current mailbox - * * @param string $message * @param string $options * @param string $internal_date @@ -293,7 +287,6 @@ public function rename($new_name, $expunge = true) { * @param boolean $expunge * * @return bool - * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException @@ -312,7 +305,6 @@ public function delete($expunge = true) { * Subscribe the current folder * * @return bool - * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ @@ -325,7 +317,6 @@ public function subscribe() { * Unsubscribe the current folder * * @return bool - * * @throws Exceptions\ConnectionFailedException */ public function unsubscribe() { @@ -396,7 +387,6 @@ public function getStatus() { * Examine the current folder * * @return array - * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ @@ -414,6 +404,7 @@ public function getClient() { } /** + * Set the delimiter * @param $delimiter */ public function setDelimiter($delimiter){ diff --git a/src/Header.php b/src/Header.php index 349d217e..7906e229 100644 --- a/src/Header.php +++ b/src/Header.php @@ -25,22 +25,29 @@ class Header { /** + * Raw header + * * @var string $raw */ public $raw = ""; /** + * Attribute holder + * * @var array $attributes */ protected $attributes = []; /** + * Config holder + * * @var array $config */ protected $config = []; /** * Fallback Encoding + * * @var string */ public $fallback_encoding = 'UTF-8'; @@ -48,6 +55,7 @@ class Header { /** * Header constructor. * @param $raw_header + * * @throws InvalidMessageDateException */ public function __construct($raw_header) { @@ -78,6 +86,7 @@ public function __call($method, $arguments) { } /** + * Magic getter * @param $name * * @return mixed|null @@ -87,6 +96,7 @@ public function __get($name) { } /** + * Get a specific header attribute * @param $name * * @return mixed|null @@ -100,6 +110,7 @@ public function get($name) { } /** + * Perform a regex match all on the raw header and return the first result * @param $pattern * * @return mixed|null @@ -117,6 +128,7 @@ public function find($pattern) { /** * Parse the raw headers + * * @throws InvalidMessageDateException */ protected function parse(){ @@ -205,6 +217,7 @@ public function rfc822_parse_headers($raw_headers){ * Decode MIME header elements * @link https://php.net/manual/en/function.imap-mime-header-decode.php * @param string $text The MIME text + * * @return array The decoded elements are returned in an array of objects, where each * object has two properties, charset and text. */ @@ -223,6 +236,7 @@ public function mime_header_decode($text){ * Check if a given pair of strings has ben decoded * @param $encoded * @param $decoded + * * @return bool */ private function notDecoded($encoded, $decoded) { @@ -233,7 +247,6 @@ private function notDecoded($encoded, $decoded) { /** * Convert the encoding - * * @param $str * @param string $from * @param string $to @@ -285,7 +298,6 @@ public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { /** * Get the encoding of a given abject - * * @param object|string $structure * * @return string @@ -456,6 +468,9 @@ private function parseAddresses($list) { return $addresses; } + /** + * Search and extract potential header extensions + */ private function extractHeaderExtensions(){ foreach ($this->attributes as $key => $value) { if (is_string($value) === true) { diff --git a/src/Message.php b/src/Message.php index 150e8513..a68cba57 100755 --- a/src/Message.php +++ b/src/Message.php @@ -90,14 +90,23 @@ class Message { /** * Default mask + * * @var string $mask */ protected $mask = MessageMask::class; - /** @var array $config */ + /** + * Used config + * + * @var array $config + */ protected $config = []; - /** @var array $attributes */ + /** + * Attribute holder + * + * @var array $attributes + */ protected $attributes = [ 'message_no' => null, ]; @@ -135,10 +144,18 @@ class Message { */ public $header = null; - /** @var null|string $raw_body */ + /** + * Raw message body + * + * @var null|string $raw_body + */ public $raw_body = null; - /** @var Structure $structure */ + /** + * Message structure + * + * @var Structure $structure + */ protected $structure = null; /** @@ -161,7 +178,6 @@ class Message { /** * Message constructor. - * * @param integer $msgn * @param integer|null $msglist * @param Client $client @@ -241,6 +257,7 @@ public function __call($method, $arguments) { } /** + * Magic setter * @param $name * @param $value * @@ -253,6 +270,7 @@ public function __set($name, $value) { } /** + * Magic getter * @param $name * * @return mixed|null @@ -262,6 +280,7 @@ public function __get($name) { } /** + * Get an available message or message header attribute * @param $name * * @return mixed|null @@ -388,7 +407,6 @@ public function parseBody() { /** * Fetch the Message structure - * * @param $structure * * @throws Exceptions\ConnectionFailedException @@ -402,6 +420,7 @@ private function fetchStructure($structure) { } /** + * Fetch a given part * @param Part $part */ private function fetchPart(Part $part) { @@ -783,7 +802,9 @@ public function hasAttachments() { } /** - * @return null|object|string + * Get the raw body + * + * @return string * @throws Exceptions\ConnectionFailedException */ public function getRawBody() { @@ -797,6 +818,8 @@ public function getRawBody() { } /** + * Get the message header + * * @return Header */ public function getHeader() { @@ -804,6 +827,8 @@ public function getHeader() { } /** + * Get the current client + * * @return Client */ public function getClient() { @@ -811,6 +836,8 @@ public function getClient() { } /** + * Get the used fetch option + * * @return integer */ public function getFetchOptions() { @@ -818,6 +845,8 @@ public function getFetchOptions() { } /** + * Get the used fetch body option + * * @return boolean */ public function getFetchBodyOption() { @@ -825,6 +854,8 @@ public function getFetchBodyOption() { } /** + * Get the used fetch flags option + * * @return boolean */ public function getFetchFlagsOption() { @@ -832,13 +863,17 @@ public function getFetchFlagsOption() { } /** - * @return mixed + * Get all available bodies + * + * @return array */ public function getBodies() { return $this->bodies; } /** + * Get all set flags + * * @return FlagCollection */ public function getFlags() { @@ -846,6 +881,8 @@ public function getFlags() { } /** + * Get the fetched structure + * * @return object|null */ public function getStructure(){ @@ -853,9 +890,7 @@ public function getStructure(){ } /** - * Does this message match another one? - * - * A match means same uid, message id, subject and date/time. + * Check if a message matches an other by comparing basic attributes * * @param null|Message $message * @return boolean @@ -872,6 +907,8 @@ public function is(Message $message = null) { } /** + * Get all message attributes + * * @return array */ public function getAttributes(){ @@ -879,7 +916,9 @@ public function getAttributes(){ } /** + * Set the message mask * @param $mask + * * @return $this */ public function setMask($mask){ @@ -891,6 +930,8 @@ public function setMask($mask){ } /** + * Get the used message mask + * * @return string */ public function getMask(){ diff --git a/src/Part.php b/src/Part.php index 2e9cdad7..f0f3b019 100644 --- a/src/Part.php +++ b/src/Part.php @@ -13,10 +13,7 @@ namespace Webklex\PHPIMAP; -use Carbon\Carbon; -use Illuminate\Support\Str; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; -use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; /** * Class Part @@ -26,76 +23,106 @@ class Part { /** + * Raw part + * * @var string $raw */ public $raw = ""; /** + * Part type + * * @var int $type */ public $type = IMAP::MESSAGE_TYPE_TEXT; /** + * Part content + * * @var string $content */ public $content = ""; /** + * Part subtype + * * @var string $subtype */ public $subtype = null; /** + * Part charset - if available + * * @var string $charset */ public $charset = "utf-8"; /** + * Part encoding method + * * @var int $encoding */ public $encoding = IMAP::MESSAGE_ENC_OTHER; /** + * Alias to check if the part is an attachment + * * @var boolean $ifdisposition */ public $ifdisposition = false; /** + * Indicates if the part is an attachment + * * @var string $disposition */ public $disposition = null; /** + * Alias to check if the part has a description + * * @var boolean $ifdescription */ public $ifdescription = false; /** + * Part description if available + * * @var string $description */ public $description = null; /** + * Part filename if available + * * @var string $filename */ public $filename = null; /** + * Part name if available + * * @var string $name */ public $name = null; /** + * Part id if available + * * @var string $id */ public $id = null; /** + * The part number of the current part + * * @var integer $part_number */ public $part_number = 0; /** + * Part length in bytes + * * @var integer $bytes */ public $bytes = null; @@ -165,6 +192,9 @@ private function findHeaders(){ return (string) $body; } + /** + * Try to parse the subtype if any is present + */ private function parseSubtype(){ $content_type = $this->header->get("content-type"); if (($pos = strpos($content_type, "/")) !== false){ @@ -172,6 +202,9 @@ private function parseSubtype(){ } } + /** + * Try to parse the disposition if any is present + */ private function parseDisposition(){ $content_disposition = $this->header->get("content-disposition"); if($content_disposition !== null) { @@ -180,6 +213,9 @@ private function parseDisposition(){ } } + /** + * Try to parse the description if any is present + */ private function parseDescription(){ $content_description = $this->header->get("content-description"); if($content_description !== null) { @@ -188,6 +224,9 @@ private function parseDescription(){ } } + /** + * Try to parse the encoding if any is present + */ private function parseEncoding(){ $encoding = $this->header->get("content-transfer-encoding"); if($encoding !== null) { diff --git a/src/Structure.php b/src/Structure.php index 5422386c..446d80b2 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -13,11 +13,8 @@ namespace Webklex\PHPIMAP; -use Carbon\Carbon; -use Illuminate\Support\Str; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; -use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; /** * Class Structure @@ -27,6 +24,8 @@ class Structure { /** + * Raw structure + * * @var string $raw */ public $raw = ""; @@ -37,16 +36,22 @@ class Structure { private $header = null; /** + * Message type (if multipart or not) + * * @var int $type */ public $type = IMAP::MESSAGE_TYPE_TEXT; /** + * All available parts + * * @var Part[] $parts */ public $parts = []; /** + * Config holder + * * @var array $config */ protected $config = []; @@ -55,6 +60,7 @@ class Structure { * Structure constructor. * @param $raw_structure * @param Header $header + * * @throws MessageContentFetchingException * @throws InvalidMessageDateException */ @@ -66,6 +72,8 @@ public function __construct($raw_structure, Header $header) { } /** + * Parse the given raw structure + * * @throws MessageContentFetchingException * @throws InvalidMessageDateException */ @@ -93,6 +101,8 @@ public function getBoundary(){ } /** + * Find all available parts + * * @return array * @throws MessageContentFetchingException * @throws InvalidMessageDateException diff --git a/src/Support/Masks/Mask.php b/src/Support/Masks/Mask.php index f679d090..7483bd5e 100755 --- a/src/Support/Masks/Mask.php +++ b/src/Support/Masks/Mask.php @@ -23,11 +23,15 @@ class Mask { /** + * Available attributes + * * @var array $attributes */ protected $attributes = []; /** + * Parent instance + * * @var object $parent */ protected $parent; @@ -86,6 +90,7 @@ public function __call($method, $arguments) { } /** + * Magic setter * @param $name * @param $value * @@ -98,6 +103,7 @@ public function __set($name, $value) { } /** + * Magic getter * @param $name * * @return mixed|null @@ -111,6 +117,8 @@ public function __get($name) { } /** + * Get the parent instance + * * @return mixed */ public function getParent(){ @@ -118,6 +126,8 @@ public function getParent(){ } /** + * Get all available attributes + * * @return array */ public function getAttributes(){ diff --git a/src/Support/Masks/MessageMask.php b/src/Support/Masks/MessageMask.php index 30186c20..bc8fe0a7 100644 --- a/src/Support/Masks/MessageMask.php +++ b/src/Support/Masks/MessageMask.php @@ -27,6 +27,7 @@ class MessageMask extends Mask { /** * Get the message html body + * * @return null */ public function getHtmlBody(){ diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 49ebca3c..72f3bdba 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -23,12 +23,15 @@ */ class PaginatedCollection extends Collection { - /** @var int $total */ + /** + * Number of total entries + * + * @var int $total + */ protected $total; /** * Paginate the current collection. - * * @param int $per_page * @param int|null $page * @param string $page_name @@ -50,7 +53,6 @@ public function paginate($per_page = 15, $page = null, $page_name = 'page') { /** * Create a new length-aware paginator instance. - * * @param array $items * @param int $total * @param int $per_page @@ -64,6 +66,7 @@ protected function paginator($items, $total, $per_page, $current_page, array $op } /** + * Get and set the total amount * @param null $total * * @return int|null diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index 8387b107..bc7ae68e 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -23,7 +23,11 @@ */ trait HasEvents { - /** @var array $events */ + /** + * Event holder + * + * @var array $events + */ protected $events = []; /** From 4d0badc1b95ec08abb30da1d3744f6282b51bab8 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 2 Oct 2020 01:00:27 +0200 Subject: [PATCH 032/600] Message::getAttributes() hasn't returned all parameters --- CHANGELOG.md | 2 +- src/Header.php | 9 +++++++++ src/Message.php | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 500a008f..1e7a4733 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- `Message::getAttributes()` hasn't returned all parameters ### Added - Part number added to attachment diff --git a/src/Header.php b/src/Header.php index 7906e229..249f0aa3 100644 --- a/src/Header.php +++ b/src/Header.php @@ -550,4 +550,13 @@ private function parseDate($header) { } } + /** + * Get all available attributes + * + * @return array + */ + public function getAttributes() { + return $this->attributes; + } + } \ No newline at end of file diff --git a/src/Message.php b/src/Message.php index a68cba57..e17dd066 100755 --- a/src/Message.php +++ b/src/Message.php @@ -912,7 +912,7 @@ public function is(Message $message = null) { * @return array */ public function getAttributes(){ - return $this->attributes; + return array_merge($this->attributes, $this->header->getAttributes()); } /** From 6dffce099c122d0f75fd45b8b8e377003697aac1 Mon Sep 17 00:00:00 2001 From: Adnan RIHAN Date: Fri, 2 Oct 2020 15:50:03 +0200 Subject: [PATCH 033/600] Adds Client::getFolderByPath() (#12) * Adds Client::getFolderByPath() * Adds Client::getFolderByName() to prevent ambiguity --- src/Client.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Client.php b/src/Client.php index 179f3205..53f088bd 100755 --- a/src/Client.php +++ b/src/Client.php @@ -366,9 +366,33 @@ public function disconnect() { * @throws FolderFetchingException */ public function getFolder($folder_name) { + return $this->getFolderByName($folder_name); + } + + /** + * Get a folder instance by a folder name + * @param $folder_name + * + * @return mixed + * @throws ConnectionFailedException + * @throws FolderFetchingException + */ + public function getFolderByName($folder_name) { return $this->getFolders(false)->where("name", $folder_name)->first(); } + /** + * Get a folder instance by a folder path + * @param $folder_path + * + * @return mixed + * @throws ConnectionFailedException + * @throws FolderFetchingException + */ + public function getFolderByPath($folder_path) { + return $this->getFolders(false)->where("path", $folder_path)->first(); + } + /** * Get folders list. * If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array. From a9ec2e07564f3253a1093e84cd3a4c3803d435a8 Mon Sep 17 00:00:00 2001 From: Adnan RIHAN Date: Fri, 2 Oct 2020 15:53:14 +0200 Subject: [PATCH 034/600] Throws exception on Client::authenticate() when fails silently (#11) --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 53f088bd..54866d9a 100755 --- a/src/Client.php +++ b/src/Client.php @@ -336,8 +336,8 @@ protected function authenticate() { try { if ($this->authentication == "oauth") { $this->connection->authenticate($this->username, $this->password); - }else{ - $this->connection->login($this->username, $this->password); + } elseif (!$this->connection->login($this->username, $this->password)) { + throw new \Exception; } } catch (\Exception $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); From b4dc869147e5ecf840dd7c6c6a04776709b7ffca Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 2 Oct 2020 15:55:53 +0200 Subject: [PATCH 035/600] Exceptions changed from generic to AuthFailedException --- src/Client.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 54866d9a..866f48e6 100755 --- a/src/Client.php +++ b/src/Client.php @@ -16,6 +16,7 @@ use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol; use Webklex\PHPIMAP\Connection\Protocols\Protocol; use Webklex\PHPIMAP\Connection\Protocols\ProtocolInterface; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\FolderFetchingException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; @@ -335,9 +336,11 @@ public function connect() { protected function authenticate() { try { if ($this->authentication == "oauth") { - $this->connection->authenticate($this->username, $this->password); + if (!$this->connection->authenticate($this->username, $this->password)) { + throw new AuthFailedException(); + } } elseif (!$this->connection->login($this->username, $this->password)) { - throw new \Exception; + throw new AuthFailedException(); } } catch (\Exception $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); From fdbdae0f88906fb56fdc2195c25c9b270eb173c6 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 2 Oct 2020 15:56:04 +0200 Subject: [PATCH 036/600] Changelog updated --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e7a4733..923392c9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,12 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Part number added to attachment +- `Client::getFolderByPath()` added (#12 by [@Max13](https://github.com/Max13)) +- `Client::getFolderByName()` added (#12 by [@Max13](https://github.com/Max13)) +- Throws exceptions if the authentication fails (#11 by [@Max13](https://github.com/Max13)) ### Affected Classes -- NaN +- [Client::class](src/Client.php) ## [2.1.5] - 2020-09-30 ### Fixed From 2eeef0083083eb19c33a62927d1e8e8718db0594 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 2 Oct 2020 15:58:39 +0200 Subject: [PATCH 037/600] Version information updated --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 923392c9..4bd924bc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +## [2.1.6] - 2020-10-02 +### Fixed - `Message::getAttributes()` hasn't returned all parameters ### Added From 12dd9ca85b7b54d3d50cd3f77bfb0da7d6f32275 Mon Sep 17 00:00:00 2001 From: Adnan RIHAN Date: Sat, 3 Oct 2020 01:45:32 +0200 Subject: [PATCH 038/600] Fixed Query::paginate() (#14) --- src/Query/Query.php | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 4456fcea..317b4f4d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -251,21 +251,19 @@ public function getMessage($msgno, $msglist = null){ * @throws GetMessagesFailedException */ public function paginate($per_page = 5, $page = null, $page_name = 'imap_page'){ - $this->page = $page > $this->page ? $page : $this->page; - $this->limit = $per_page; - - $messages = $this->get(); - if (($count = $messages->count()) > 0) { - $limit = $this->limit > $count ? $count : $this->limit; - $collection = array_fill(0, $messages->total() - $limit, true); - $messages->each(function($message) use(&$collection){ - $collection[] = $message; - }); - }else{ - $collection = array_fill(0, $messages->total(), true); + if ( + $page === null + && isset($_GET[$page_name]) + && $_GET[$page_name] > 0 + ) { + $this->page = intval($_GET[$page_name]); + } elseif ($page > 0) { + $this->page = $page; } - return MessageCollection::make($collection)->paginate($per_page, $this->page, $page_name); + $this->limit = $per_page; + + return $this->get()->paginate($per_page, $this->page, $page_name); } /** From 5ad1246e8a966610186208d57ca262686d23361c Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 3 Oct 2020 01:48:43 +0200 Subject: [PATCH 039/600] Version information added --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd924bc..af66521c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.7] - 2020-10-03 +### Fixed +- Fixed `Query::paginate()` (#13 #14 by [@Max13](https://github.com/Max13)) + ## [2.1.6] - 2020-10-02 ### Fixed - `Message::getAttributes()` hasn't returned all parameters From 17810744925c9907622bcd819b12428d2cce47ea Mon Sep 17 00:00:00 2001 From: dwalczyk <61882865+dwalczyk@users.noreply.github.com> Date: Thu, 8 Oct 2020 06:06:51 +0200 Subject: [PATCH 040/600] fix messages with multiple boundary (#19) Empty message html and text body #17 --- src/Structure.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Structure.php b/src/Structure.php index 446d80b2..33a1cc47 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -113,7 +113,23 @@ public function find_parts(){ throw new MessageContentFetchingException("no content found", 0); } - $raw_parts = explode($boundary, $this->raw); + $boundaries = [ + $boundary + ]; + + if (preg_match("/boundary\=\"(.*)\"/", $this->raw, $match) == 1) { + if(is_array($match[1])){ + foreach($match[1] as $matched){ + $boundaries[] = $matched; + } + }else{ + if(!empty($match[1])) { + $boundaries[] = $match[1]; + } + } + } + + $raw_parts = explode( $boundaries[0], str_replace($boundaries, $boundaries[0], $this->raw) ); $parts = []; $part_number = 0; foreach($raw_parts as $part) { From b09076a5f5c0c1f319c05fb05c939fc880e1a89c Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 Oct 2020 07:11:07 +0300 Subject: [PATCH 041/600] Fix error decode address (#16) This is fix message error "Undefined offset: 1" by using query()->get() --- src/Header.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Header.php b/src/Header.php index 249f0aa3..764bc423 100644 --- a/src/Header.php +++ b/src/Header.php @@ -405,7 +405,7 @@ private function decodeAddresses($values) { )){ $name = trim(rtrim($matches["name"])); $email = trim(rtrim($matches["email"])); - list($mailbox, $host) = explode("@", $email); + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); $addresses[] = (object) [ "personal" => $name, "mailbox" => $mailbox, @@ -559,4 +559,4 @@ public function getAttributes() { return $this->attributes; } -} \ No newline at end of file +} From bcb658c6d7099af803a04c63b34fd78f5a540899 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 8 Oct 2020 06:18:56 +0200 Subject: [PATCH 042/600] Flag event dispatching fixed #15 --- src/Message.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Message.php b/src/Message.php index e17dd066..e2391fb8 100755 --- a/src/Message.php +++ b/src/Message.php @@ -756,7 +756,7 @@ public function setFlag($flag) { $this->parseFlags(); $event = $this->getEvent("flag", "new"); - $event::dispatch($this); + $event::dispatch($this, $flag); return $status; } @@ -778,7 +778,7 @@ public function unsetFlag($flag) { $this->parseFlags(); $event = $this->getEvent("flag", "deleted"); - $event::dispatch($this); + $event::dispatch($this, $flag); return $status; } From 0179b08f3cc1edcb2bb9ca008e1cb7ec61a750a4 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 8 Oct 2020 06:19:33 +0200 Subject: [PATCH 043/600] Version information added --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af66521c..9f9f52d0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,14 +14,31 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.8] - 2020-10-08 +### Fixed +- Possible error during address decoding fixed (#16 [@Slauta](https://github.com/Slauta)) +- Flag event dispatching fixed #15 + +### Added +- Support multiple boundaries (#17, #19 [@dwalczyk](https://github.com/dwalczyk)) + +### Affected Classes +- [Structure::class](src/Structure.php) + ## [2.1.7] - 2020-10-03 ### Fixed - Fixed `Query::paginate()` (#13 #14 by [@Max13](https://github.com/Max13)) +### Affected Classes +- [Query::class](src/Query/Query.php) + ## [2.1.6] - 2020-10-02 ### Fixed - `Message::getAttributes()` hasn't returned all parameters +### Affected Classes +- [Message::class](src/Message.php) + ### Added - Part number added to attachment - `Client::getFolderByPath()` added (#12 by [@Max13](https://github.com/Max13)) From cb1a9cba2f2ff7afb47140b3e9669ec7a5836889 Mon Sep 17 00:00:00 2001 From: dwalczyk <61882865+dwalczyk@users.noreply.github.com> Date: Thu, 8 Oct 2020 16:24:34 +0200 Subject: [PATCH 044/600] fix inline attachments and embedded images (#22) Co-authored-by: d.walczyk@esprzedaz.com --- src/Attachment.php | 2 +- src/Message.php | 7 ++----- src/Part.php | 26 +++++++++++++++++++++++++- src/Support/Masks/MessageMask.php | 9 ++++++--- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index db5b71a3..ee07fcd7 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -209,7 +209,7 @@ protected function fetch() { $content = $this->part->content; - $this->content_type = $this->type.'/'.strtolower($this->part->subtype); + $this->content_type = $this->part->content_type; $this->content = $this->oMessage->decodeString($content, $this->part->encoding); if (($id = $this->part->id) !== null) { diff --git a/src/Message.php b/src/Message.php index e2391fb8..bf79a83a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -425,11 +425,8 @@ private function fetchStructure($structure) { */ private function fetchPart(Part $part) { - if ($part->type == IMAP::MESSAGE_TYPE_TEXT && - ($part->ifdisposition == 0 || - (empty($part->disposition) || strtolower($part->disposition) != 'attachment') - ) - ) { + if ($part->type == IMAP::MESSAGE_TYPE_TEXT && ($part->ifdisposition == 0 || (empty($part->disposition) || !in_array(strtolower($part->disposition), ['attachment', 'inline'])) ) ) { + if (strtolower($part->subtype) == "plain" || strtolower($part->subtype) == "csv") { $encoding = $this->getEncoding($part); diff --git a/src/Part.php b/src/Part.php index f0f3b019..e9488282 100644 --- a/src/Part.php +++ b/src/Part.php @@ -127,6 +127,13 @@ class Part { */ public $bytes = null; + /** + * Part content type + * + * @var string|null $content_type + */ + public $content_type = null; + /** * @var Header $header */ @@ -167,7 +174,24 @@ protected function parse(){ $this->charset = $this->header->get("charset"); $this->name = $this->header->get("name"); $this->filename = $this->header->get("filename"); - $this->id = $this->header->get("id"); + + if(!empty($this->header->get("id"))) { + $this->id = $this->header->get("id"); + } else if(!empty($this->header->get("x-attachment-id"))){ + $this->id = $this->header->get("x-attachment-id"); + } else if(!empty($this->header->get("content-id"))){ + $this->id = strtr($this->header->get("content-id"), [ + '<' => '', + '>' => '' + ]); + } + + if(!empty($this->header->get("content-type"))){ + $rawContentType = $this->header->get("content-type"); + $contentTypeArray = explode(';', $rawContentType); + $this->content_type = trim($contentTypeArray[0]); + } + $this->content = trim(rtrim($body)); $this->bytes = strlen($this->content); diff --git a/src/Support/Masks/MessageMask.php b/src/Support/Masks/MessageMask.php index bc8fe0a7..11f3b9ad 100644 --- a/src/Support/Masks/MessageMask.php +++ b/src/Support/Masks/MessageMask.php @@ -36,7 +36,10 @@ public function getHtmlBody(){ return null; } - return $bodies['html']->content; + if(is_object($bodies['html']) && property_exists($bodies['html'], 'content')) { + return $bodies['html']->content; + } + return $bodies['html']; } /** @@ -73,8 +76,8 @@ public function getCustomHTMLBody($callback = false) { public function getHTMLBodyWithEmbeddedBase64Images() { return $this->getCustomHTMLBody(function($body, $oAttachment){ /** @var \Webklex\PHPIMAP\Attachment $oAttachment */ - if ($oAttachment->id && $oAttachment->getImgSrc() != null) { - $body = str_replace('cid:'.$oAttachment->id, $oAttachment->getImgSrc(), $body); + if ($oAttachment->id) { + $body = str_replace('cid:'.$oAttachment->id, 'data:'.$oAttachment->getContentType().';base64, '.base64_encode($oAttachment->getContent()), $body); } return $body; From 5628df6b9a03f8e317bb66df0ed2df80d1d682f7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 8 Oct 2020 16:30:51 +0200 Subject: [PATCH 045/600] Alternative attachment names support added #20 --- src/Attachment.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index ee07fcd7..135d9e97 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -217,13 +217,14 @@ protected function fetch() { } $this->size = $this->part->bytes; + $this->disposition = $this->part->disposition; if (($name = $this->part->name) !== null) { $this->setName($name); - $this->disposition = $this->part->disposition; }elseif (($filename = $this->part->filename) !== null) { $this->setName($filename); - $this->disposition = $this->part->disposition; + } else { + $this->setName("undefined"); } if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) { From 556262148faeaaf4069e99e53972a8594a54e262 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 8 Oct 2020 16:32:10 +0200 Subject: [PATCH 046/600] Changelog updated --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9f52d0..c0b7e955 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,15 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix inline attachments and embedded images (#22 [@dwalczyk](https://github.com/dwalczyk)) ### Added -- NaN +- Alternative attachment names support added (#20 [@oneFoldSoftware](https://github.com/oneFoldSoftware)) ### Affected Classes -- NaN +- [Attachment::class](src/Attachment.php) +- [Message::class](src/Message.php) +- [Part::class](src/Part.php) ## [2.1.8] - 2020-10-08 ### Fixed From 0c61e0973ce14d5c4a86c306d1a02547b4c15a18 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 8 Oct 2020 21:04:57 +0200 Subject: [PATCH 047/600] Fetch message content without leaving a "Seen" flag behind --- CHANGELOG.md | 12 ++++++++++++ README.md | 15 +++++++++++++++ src/Message.php | 17 +++++++++++++---- src/Query/Query.php | 1 + src/config/imap.php | 4 ++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b7e955..cc73dae8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,27 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +## [2.1.9] - 2020-10-08 +### Fixed - Fix inline attachments and embedded images (#22 [@dwalczyk](https://github.com/dwalczyk)) ### Added - Alternative attachment names support added (#20 [@oneFoldSoftware](https://github.com/oneFoldSoftware)) +- Fetch message content without leaving a "Seen" flag behind ### Affected Classes - [Attachment::class](src/Attachment.php) - [Message::class](src/Message.php) - [Part::class](src/Part.php) +- [Query::class](src/Query/Query.php) ## [2.1.8] - 2020-10-08 ### Fixed diff --git a/README.md b/README.md index 7d4a1787..193c7886 100755 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `date_format` — The default date format is used to convert any given Carbon::class object into a valid date string. (`d-M-Y`, `d-M-y`, `d M y`) - `options` — additional fetch options - `delimiter` — you can use any supported char such as ".", "/", etc + - `fetch` — `IMAP::FT_UID` (message marked as read by fetching the message body) or `IMAP::FT_PEEK` (fetch the message without setting the "seen" flag) - `fetch_body` — If set to `false` all messages will be fetched without the body and any potential attachments - `fetch_flags` — If set to `false` all messages will be fetched without any flags - `message_key` — Message key identifier option @@ -438,6 +439,20 @@ $message->setFlag(['Seen', 'Spam']); $message->unsetFlag('Spam'); ``` +Mark all messages as "read" while fetching: +```php +/** @var \Webklex\PHPIMAP\Folder $oFolder */ +/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ +$aMessage = $oFolder->query()->text('Hello world')->markAsRead()->get(); +``` + +Don't mark all messages as "read" while fetching: +```php +/** @var \Webklex\PHPIMAP\Folder $oFolder */ +/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ +$aMessage = $oFolder->query()->text('Hello world')->leaveUnread()->get(); +``` + #### Attachments Save message attachments: diff --git a/src/Message.php b/src/Message.php index bf79a83a..f4fe287b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -190,6 +190,7 @@ class Message { * @throws Exceptions\RuntimeException * @throws MessageHeaderFetchingException * @throws MessageContentFetchingException + * @throws Exceptions\EventNotFoundException */ public function __construct($msgn, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false) { @@ -221,13 +222,13 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu $this->parseHeader(); - if ($this->getFetchFlagsOption() === true) { - $this->parseFlags(); - } - if ($this->getFetchBodyOption() === true) { $this->parseBody(); } + + if ($this->getFetchFlagsOption() === true && $this->flags->count() == 0) { + $this->parseFlags(); + } } /** @@ -388,6 +389,7 @@ private function parseFlags() { * @throws Exceptions\MessageContentFetchingException * @throws InvalidMessageDateException * @throws Exceptions\RuntimeException + * @throws Exceptions\EventNotFoundException */ public function parseBody() { $this->client->openFolder($this->folder_path); @@ -402,6 +404,13 @@ public function parseBody() { $this->fetchStructure($this->structure); + if ($this->fetch_options == IMAP::FT_PEEK) { + $this->parseFlags(); + if ($this->getFlags()->get("seen") !== null) { + $this->unsetFlag("Seen"); + } + } + return $this; } diff --git a/src/Query/Query.php b/src/Query/Query.php index 317b4f4d..226746d2 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -236,6 +236,7 @@ public function get() { * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException */ public function getMessage($msgno, $msglist = null){ return new Message($msgno, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags()); diff --git a/src/config/imap.php b/src/config/imap.php index c81732af..f6f6becd 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -89,6 +89,9 @@ | -Delimiter (optional): | This option is only used when calling $oClient-> | You can use any supported char such as ".", "/", (...) + | -Fetch option: + | IMAP::FT_UID - Message marked as read by fetching the body message + | IMAP::FT_PEEK - Fetch the message without setting the "seen" flag | -Body download option | Default TRUE | -Flag download option @@ -114,6 +117,7 @@ */ 'options' => [ 'delimiter' => '/', + 'fetch' => \Webklex\PHPIMAP\IMAP::FT_UID, 'fetch_body' => true, 'fetch_flags' => true, 'message_key' => 'list', From 13e76da3cdb547819012ecabe7b96f2dfee1d7be Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 9 Oct 2020 13:12:26 +0200 Subject: [PATCH 048/600] ClientManager::make() method added to support undefined accounts --- CHANGELOG.md | 2 +- README.md | 8 +++++--- src/ClientManager.php | 11 +++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc73dae8..dadfa727 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- `ClientManager::make()` method added to support undefined accounts ### Added - NaN diff --git a/README.md b/README.md index 193c7886..9d511d0c 100755 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ $cm = new ClientManager($options = []); $client = $cm->account('account_identifier'); // or create a new instance manually -$client = new Client([ +$client = $cm->make([ 'host' => 'somehost.com', 'port' => 993, 'encryption' => 'ssl', @@ -205,10 +205,12 @@ $folder = $client->getFolder('INBOX.name'); #### oAuth If you are using google mail or something similar, you might want to use oauth instead: ```php -use Webklex\PHPIMAP\Client; +use Webklex\PHPIMAP\Clientmanager; + +$cm = new Clientmanager(); /** @var \Webklex\PHPIMAP\Client $client */ -$client = new Client([ +$client = $cm->make([ 'host' => 'imap.gmail.com', 'port' => 993, 'encryption' => 'ssl', diff --git a/src/ClientManager.php b/src/ClientManager.php index db163fc8..3daf9f83 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -55,6 +55,17 @@ public function __call($method, $parameters) { return call_user_func_array($callable, $parameters); } + /** + * Safely create a new client instance which is not listed in accounts + * @param array $config + * + * @return Client + * @throws Exceptions\MaskNotFoundException + */ + public function make($config) { + return new Client($config); + } + /** * Get a dotted config parameter * @param string $key From 2d86d1aab00a381794820c82302416e6a3412a8b Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 9 Oct 2020 13:20:58 +0200 Subject: [PATCH 049/600] Changelog updated --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dadfa727..82ab3703 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- `ClientManager::make()` method added to support undefined accounts +- NaN ### Added - NaN @@ -14,6 +14,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.10] - 2020-10-09 +### Added +- `ClientManager::make()` method added to support undefined accounts + +### Affected Classes +- [ClientManager::class](src/ClientManager.php) + ## [2.1.9] - 2020-10-08 ### Fixed - Fix inline attachments and embedded images (#22 [@dwalczyk](https://github.com/dwalczyk)) From 14d53a4305d5c21b8765e4741f03b608261405aa Mon Sep 17 00:00:00 2001 From: Tobbel Date: Mon, 12 Oct 2020 14:50:52 +0200 Subject: [PATCH 050/600] Add Wrapper to test if a path is used for Client::getFolder() (#23) * Changed getFolder to also accept Paths and try to detect if a Path or a Name ist used * added a switch to enforce selection by name * changed the way of accessing the default delimiter Co-authored-by: Tobias Weber --- src/Client.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 866f48e6..9ed74a13 100755 --- a/src/Client.php +++ b/src/Client.php @@ -362,13 +362,22 @@ public function disconnect() { /** * Get a folder instance by a folder name - * @param $folder_name + * @param string $folder_name + * @param string|bool $delimiter * * @return mixed * @throws ConnectionFailedException * @throws FolderFetchingException */ - public function getFolder($folder_name) { + public function getFolder($folder_name, $delimiter = null) { + // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) + if ($delimiter !== false) { + $delimiter = (is_null($delimiter)) ? ClientManager::get('options.delimiter', "/") : $delimiter; + if (strpos($folder_name, $delimiter) !== false) { + return $this->getFolderByPath($folder_name); + } + } + return $this->getFolderByName($folder_name); } From b6f13b2f8459e29cdf4d3aefc8c41a339de9bf15 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 12 Oct 2020 14:55:45 +0200 Subject: [PATCH 051/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ab3703..cd322276 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- Expended `Client::getFolder($name, $deleimiter = null)` to accept either a folder name or path ### Affected Classes - NaN From 5127afb07bcdd7413c5b71c439c20a372293014f Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 12 Oct 2020 14:56:26 +0200 Subject: [PATCH 052/600] Client::getFolder doc updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d511d0c..00e21b78 100755 --- a/README.md +++ b/README.md @@ -648,7 +648,7 @@ if you're just wishing a feature ;) | connect | | | Connect to server. | | reconnect | | | Terminate and reconnect to server. | | disconnect | | | Disconnect from server. | -| getFolder | string $folder_name, int $attributes = 32, int or null $delimiter, bool $prefix_address | Folder | Get a Folder instance by name | +| getFolder | string $folder_name, $delimiter = null | Folder | Get a Folder instance by name or path | | getFolders | bool $hierarchical, string or null $parent_folder | FolderCollection | Get folders list. If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array. | | openFolder | string or Folder $folder, integer $attempts | | Open a given folder. | | createFolder | string $name | boolean | Create a new folder. | From 8ee2166092242bd4662a241617873a39231c81f7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 13 Oct 2020 07:11:12 +0200 Subject: [PATCH 053/600] Potential problematic prefixed white-spaces removed from header keys --- src/Header.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 764bc423..b30c39b2 100644 --- a/src/Header.php +++ b/src/Header.php @@ -175,14 +175,19 @@ public function rfc822_parse_headers($raw_headers){ $lines = explode("\r\n", $raw_headers); $prev_header = null; foreach($lines as $line) { + if (substr($line, 0, 1) === "\n") { + $line = substr($line, 1); + } if (substr($line, 0, 1) === "\t") { $line = substr($line, 2); + $line = substr($line, 1); + $line = trim(rtrim($line)); if ($prev_header !== null) { $headers[$prev_header][] = $line; } }else{ if (($pos = strpos($line, ":")) > 0) { - $key = strtolower(substr($line, 0, $pos)); + $key = trim(rtrim(strtolower(substr($line, 0, $pos)))); $value = trim(rtrim(strtolower(substr($line, $pos + 1)))); $headers[$key] = [$value]; $prev_header = $key; From d7c35cb4e099847295426a74d0feeeada9b5d005 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 13 Oct 2020 07:11:52 +0200 Subject: [PATCH 054/600] Special MS-Exchange header decoding support added --- src/Header.php | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Header.php b/src/Header.php index b30c39b2..a74a70a7 100644 --- a/src/Header.php +++ b/src/Header.php @@ -178,13 +178,26 @@ public function rfc822_parse_headers($raw_headers){ if (substr($line, 0, 1) === "\n") { $line = substr($line, 1); } + if (substr($line, 0, 1) === "\t") { - $line = substr($line, 2); $line = substr($line, 1); $line = trim(rtrim($line)); if ($prev_header !== null) { $headers[$prev_header][] = $line; } + }elseif (substr($line, 0, 1) === " ") { + $line = substr($line, 1); + $line = trim(rtrim($line)); + if ($prev_header !== null) { + if (!isset($headers[$prev_header])) { + $headers[$prev_header] = ""; + } + if (is_array($headers[$prev_header])) { + $headers[$prev_header][] = $line; + }else{ + $headers[$prev_header] .= $line; + } + } }else{ if (($pos = strpos($line, ":")) > 0) { $key = trim(rtrim(strtolower(substr($line, 0, $pos)))); @@ -208,8 +221,27 @@ public function rfc822_parse_headers($raw_headers){ $value = $this->decodeAddresses($values); $headers[$key."address"] = implode(", ", $values); break; + case 'subject': + $value = implode(" ", $values); + break; default: - $value = isset($values[0]) ? $values[0] : $value; + if (is_array($values)) { + foreach($values as $k => $v) { + if ($v == "") { + unset($values[$k]); + } + } + $available_values = count($values); + if ($available_values === 1) { + $value = array_pop($values); + } elseif ($available_values === 2) { + $value = implode(" ", $values); + } elseif ($available_values > 2) { + $value = array_values($values); + } else { + $value = ""; + } + } break; } $headers[$key] = $value; @@ -478,10 +510,13 @@ private function parseAddresses($list) { */ private function extractHeaderExtensions(){ foreach ($this->attributes as $key => $value) { - if (is_string($value) === true) { + // Only parse strings and don't parse any attributes like the user-agent + if (is_string($value) === true && in_array($key, ["user-agent"]) === false) { if (($pos = strpos($value, ";")) !== false){ $original = substr($value, 0, $pos); $this->attributes[$key] = trim(rtrim($original)); + + // Get all potential extensions $extensions = explode(";", substr($value, $pos + 1)); foreach($extensions as $extension) { if (($pos = strpos($extension, "=")) !== false){ From 662963f8829b66219c745a320b6897a558f5005e Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 13 Oct 2020 07:12:24 +0200 Subject: [PATCH 055/600] Version information added --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd322276..2e1228c6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,23 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- Expended `Client::getFolder($name, $deleimiter = null)` to accept either a folder name or path +- NaN ### Affected Classes - NaN +## [2.1.11] - 2020-10-13 +### Fixed +- Potential problematic prefixed white-spaces removed from header attributes + +### Added +- Expended `Client::getFolder($name, $deleimiter = null)` to accept either a folder name or path ([@Max13](https://github.com/Max13)) +- Special MS-Exchange header decoding support added + +### Affected Classes +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) + ## [2.1.10] - 2020-10-09 ### Added - `ClientManager::make()` method added to support undefined accounts From ba9aaa29d4256b3c8f25bb99a7acaab847340c45 Mon Sep 17 00:00:00 2001 From: Tobbel Date: Tue, 13 Oct 2020 15:17:43 +0200 Subject: [PATCH 056/600] If content disposition is multiline, implode the array to a simple string (#25) Co-authored-by: Tobias Weber --- src/Part.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Part.php b/src/Part.php index e9488282..5fd0b9fe 100644 --- a/src/Part.php +++ b/src/Part.php @@ -233,7 +233,7 @@ private function parseDisposition(){ $content_disposition = $this->header->get("content-disposition"); if($content_disposition !== null) { $this->ifdisposition = true; - $this->disposition = $content_disposition; + $this->disposition = (is_array($content_disposition)) ? implode(' ', $content_disposition) : $content_disposition; } } @@ -278,4 +278,4 @@ private function parseEncoding(){ } } -} \ No newline at end of file +} From 2ae199c6fc984b923d1e79db87e0938b8c2b4721 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 13 Oct 2020 15:20:12 +0200 Subject: [PATCH 057/600] Chnagelog updated --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1228c6..d9e8327c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.12] - 2020-10-13 +### Fixed +- If content disposition is multiline, implode the array to a simple string (#25 [@DasTobbel](https://github.com/DasTobbel)) + +### Affected Classes +- [Part::class](src/Part.php) + ## [2.1.11] - 2020-10-13 ### Fixed - Potential problematic prefixed white-spaces removed from header attributes ### Added -- Expended `Client::getFolder($name, $deleimiter = null)` to accept either a folder name or path ([@Max13](https://github.com/Max13)) +- Expended `Client::getFolder($name, $deleimiter = null)` to accept either a folder name or path ([@DasTobbel](https://github.com/DasTobbel)) - Special MS-Exchange header decoding support added ### Affected Classes From 4d98ed7d346f62c3a39e1d08af8aecff8e61a0d5 Mon Sep 17 00:00:00 2001 From: Tobbel Date: Tue, 13 Oct 2020 20:03:32 +0200 Subject: [PATCH 058/600] boundary was looking for a quote mark, but not all boundaries have one / need one , altered regex. content-type may alsoe be multiline/array and is now forced to be a string (#28) Co-authored-by: Tobias Weber --- src/Structure.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Structure.php b/src/Structure.php index 33a1cc47..1b12b80c 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -86,7 +86,10 @@ protected function parse(){ * Determine the message content type */ public function findContentType(){ - if(stripos($this->header->get("content-type"), 'multipart') === 0) { + + $content_type = $this->header->get("content-type"); + $content_type = (is_array($content_type)) ? implode(' ', $content_type) : $content_type; + if(stripos($content_type, 'multipart') === 0) { $this->type = IMAP::MESSAGE_TYPE_MULTIPART; }else{ $this->type = IMAP::MESSAGE_TYPE_TEXT; @@ -97,7 +100,8 @@ public function findContentType(){ * Determine the message content type */ public function getBoundary(){ - return $this->header->find("/boundary\=\"(.*)\"/"); + $boundary = $this->header->find("/boundary\=\"?(.*)\"?/"); + return str_replace('"', '', $boundary); } /** @@ -117,14 +121,14 @@ public function find_parts(){ $boundary ]; - if (preg_match("/boundary\=\"(.*)\"/", $this->raw, $match) == 1) { + if (preg_match("/boundary\=\"?(.*)\"?/", $this->raw, $match) == 1) { if(is_array($match[1])){ foreach($match[1] as $matched){ - $boundaries[] = $matched; + $boundaries[] = str_replace('"', '', $matched); } }else{ if(!empty($match[1])) { - $boundaries[] = $match[1]; + $boundaries[] = str_replace('"', '', $match[1]); } } } @@ -144,4 +148,4 @@ public function find_parts(){ return [new Part($this->raw, $this->header)]; } -} \ No newline at end of file +} From a08fd06c81e312cdb5df6dc5f03a844b974f7883 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 13 Oct 2020 20:06:48 +0200 Subject: [PATCH 059/600] Version information updated --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e8327c..11f4a687 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - NaN +## [2.1.13] - 2020-10-13 +### Fixed +- Boundary detection problem fixed (#28 [@DasTobbel](https://github.com/DasTobbel)) +- Content-Type detection problem fixed (#28 [@DasTobbel](https://github.com/DasTobbel)) + +### Affected Classes +- [Structure::class](src/Structure.php) + ## [2.1.12] - 2020-10-13 ### Fixed - If content disposition is multiline, implode the array to a simple string (#25 [@DasTobbel](https://github.com/DasTobbel)) From 6b82256bc1d465c09e0e964ec12c15a8d50a9b85 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 15 Oct 2020 03:44:46 +0200 Subject: [PATCH 060/600] oAth documentation updated --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 00e21b78..1374f804 100755 --- a/README.md +++ b/README.md @@ -203,7 +203,9 @@ $folder = $client->getFolder('INBOX.name'); #### oAuth -If you are using google mail or something similar, you might want to use oauth instead: +Please take a look at [the wiki article](https://github.com/Webklex/php-imap/wiki/Google-Mail---Gmail) for gmail / google mail setup. + +Basic oAuth example: ```php use Webklex\PHPIMAP\Clientmanager; @@ -216,7 +218,7 @@ $client = $cm->make([ 'encryption' => 'ssl', 'validate_cert' => true, 'username' => 'example@gmail.com', - 'password' => 'PASSWORD', + 'password' => 'ACCESS-TOKEN', 'authentication' => "oauth", 'protocol' => 'imap' ]); From 8354f23af3ed86fb955846f306e45512e01d6d85 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 15 Oct 2020 23:56:04 +0200 Subject: [PATCH 061/600] Prevent text bodies from being fetched as attachment #27 --- CHANGELOG.md | 7 +++++-- src/Message.php | 18 +++--------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f4a687..dc5c1720 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Prevent text bodies from being fetched as attachment #27 ### Added - NaN ### Affected Classes -- NaN +- [Message::class](src/Message.php) + +### Breaking changes +- Text bodies might no longer get fetched as attachment ## [2.1.13] - 2020-10-13 ### Fixed diff --git a/src/Message.php b/src/Message.php index f4fe287b..a3ddb957 100755 --- a/src/Message.php +++ b/src/Message.php @@ -436,7 +436,7 @@ private function fetchPart(Part $part) { if ($part->type == IMAP::MESSAGE_TYPE_TEXT && ($part->ifdisposition == 0 || (empty($part->disposition) || !in_array(strtolower($part->disposition), ['attachment', 'inline'])) ) ) { - if (strtolower($part->subtype) == "plain" || strtolower($part->subtype) == "csv") { + if ( in_array(($subtype = strtolower($part->subtype)), ["plain", "csv", "html"]) && $part->filename == null && $part->name == null) { $encoding = $this->getEncoding($part); $content = $this->decodeString($part->content, $part->encoding); @@ -456,20 +456,8 @@ private function fetchPart(Part $part) { $content = $this->convertEncoding($content, $encoding); } - $this->bodies['text'] = $content; - - $this->fetchAttachment($part); - - } elseif (strtolower($part->subtype) == "html") { - $encoding = $this->getEncoding($part); - - $content = $this->decodeString($part->content, $part->encoding); - if ($encoding != 'us-ascii') { - $content = $this->convertEncoding($content, $encoding); - } - - $this->bodies['html'] = $content; - } elseif ($part->ifdisposition == 1 && strtolower($part->disposition) == 'attachment') { + $this->bodies[$subtype == "plain" ? "text" : $subtype] = $content; + } else { $this->fetchAttachment($part); } } else { From 4995a3e9a1c403a97cf0a710848b96c7cb91e7e6 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 00:01:49 +0200 Subject: [PATCH 062/600] Missing variable check added to prevent error while parsing an address https://github.com/Webklex/laravel-imap/issues/356 --- CHANGELOG.md | 2 ++ src/Header.php | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5c1720..8e5ced7a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Prevent text bodies from being fetched as attachment #27 +- Missing variable check added to prevent exception while parsing an address [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) ### Added - NaN ### Affected Classes - [Message::class](src/Message.php) +- [Header::class](src/Header.php) ### Breaking changes - Text bodies might no longer get fetched as attachment diff --git a/src/Header.php b/src/Header.php index a74a70a7..d93ec205 100644 --- a/src/Header.php +++ b/src/Header.php @@ -474,6 +474,10 @@ private function extractAddresses($header) { private function parseAddresses($list) { $addresses = []; + if (is_array($list) === false) { + return $addresses; + } + foreach ($list as $item) { $address = (object) $item; From c888a38ff01c4e8ccb07baa506b8420f58a795ad Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 04:51:43 +0200 Subject: [PATCH 063/600] Protocol::moveMessage() method added #29 --- CHANGELOG.md | 2 +- src/Connection/Protocols/ImapProtocol.php | 19 +++++++++++++++++++ src/Connection/Protocols/LegacyProtocol.php | 13 +++++++++++++ .../Protocols/ProtocolInterface.php | 11 +++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5ced7a..c06e72c1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing variable check added to prevent exception while parsing an address [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) ### Added -- NaN +- `Protocol::moveMessage()` method added #29 ### Affected Classes - [Message::class](src/Message.php) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 16315917..5911e92d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -805,6 +805,25 @@ public function copyMessage($folder, $from, $to = null) { return $this->requestAndResponse('COPY', [$set, $this->escapeString($folder)], true); } + /** + * Move a message set from current folder to an other folder + * @param string $folder destination folder + * @param $from + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * + * @return bool success + * @throws RuntimeException + */ + public function moveMessage($folder, $from, $to = null) { + $set = (int)$from; + if ($to !== null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + return $this->requestAndResponse('MOVE', [$set, $this->escapeString($folder)], true); + } + /** * Create a new folder (and parent folders if needed) * @param string $folder folder name diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index e5dead28..5b6baa3e 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -357,6 +357,19 @@ public function copyMessage($folder, $from, $to = null) { return \imap_mail_copy($this->stream, $from, $folder, IMAP::CP_UID); } + /** + * Move a message set from current folder to an other folder + * @param string $folder destination folder + * @param $from + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * + * @return bool success + */ + public function moveMessage($folder, $from, $to = null) { + return \imap_mail_move($this->stream, $from, $folder, IMAP::CP_UID); + } + /** * Create a new folder (and parent folders if needed) * @param string $folder folder name diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 18b42f43..a9519051 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -184,6 +184,17 @@ public function appendMessage($folder, $message, $flags = null, $date = null); */ public function copyMessage($folder, $from, $to = null); + /** + * Move a message set from current folder to an other folder + * @param string $folder destination folder + * @param $from + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * + * @return bool success + */ + public function moveMessage($folder, $from, $to = null); + /** * Create a new folder * From 09eb28dea8f99200f2e0df72076fc826ebf9812f Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 04:55:36 +0200 Subject: [PATCH 064/600] Typos fixed --- src/Connection/Protocols/ImapProtocol.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 5911e92d..a9460886 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -252,7 +252,7 @@ public function readResponse($tag, $dontParse = false) { } if ($dontParse) { - // last to chars are still needed for response code + // First two chars are still needed for the response code $tokens = [substr($tokens, 0, 2)]; } if (is_array($lines)){ @@ -260,12 +260,14 @@ public function readResponse($tag, $dontParse = false) { }else{ if ($this->debug) echo "<< ".$lines."\n"; } + // last line has response code if ($tokens[0] == 'OK') { return $lines ? $lines : true; } elseif ($tokens[0] == 'NO') { return false; } + return; } @@ -787,7 +789,7 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { } /** - * Copy message set from current folder to other folder + * Copy a message set from current folder to an other folder * @param string $folder destination folder * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the From e781f748ebc09a188c4945a06c309c6719869ede Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 15:48:39 +0200 Subject: [PATCH 065/600] Move messages through `Protocol::moveMessage()` instead of copy / delete --- src/Message.php | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Message.php b/src/Message.php index a3ddb957..aeca1132 100755 --- a/src/Message.php +++ b/src/Message.php @@ -671,7 +671,7 @@ public function copy($folder) { /** * Move the current Messages to a mailbox - * @param $folder + * @param string $folder_path * @param boolean $expunge * * @return Message|null @@ -683,17 +683,32 @@ public function copy($folder) { * @throws MessageHeaderFetchingException * @throws Exceptions\EventNotFoundException */ - public function move($folder, $expunge = false) { - $message = $this->copy($folder); - if ($message !== null) { - $this->delete($expunge); + public function move($folder_path, $expunge = false) { + $this->client->openFolder($folder_path); + $status = $this->client->getConnection()->examineFolder($folder_path); - $event = $this->getEvent("message", "moved"); - $event::dispatch($this, $message); - } + if (isset($status["uidnext"])) { + $next_uid = $status["uidnext"]; + + /** @var Folder $folder */ + $folder = $this->client->getFolder($folder_path); + + $this->client->openFolder($this->folder_path); + if ($this->client->getConnection()->moveMessage($folder->path, $this->msgn) == true) { + if($expunge) $this->client->expunge(); + $this->client->openFolder($folder->path); + $message_num = $this->client->getConnection()->getMessageNumber($next_uid); - return $message; + $message = $folder->query()->getMessage($message_num); + $event = $this->getEvent("message", "moved"); + $event::dispatch($this, $message); + + return $message; + } + } + + return null; } /** From 19affcc160e1ebfeff5803446669bbef1d8be721 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 15:49:07 +0200 Subject: [PATCH 066/600] Potential message moving / copying problem fixed #29 --- src/Message.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Message.php b/src/Message.php index aeca1132..f5e41d8a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -636,7 +636,8 @@ public function getFolder(){ /** * Copy the current Messages to a mailbox - * @param string $folder + * @param string $folder_path + * @param boolean $expunge * * @return null|Message * @throws Exceptions\ConnectionFailedException @@ -647,14 +648,20 @@ public function getFolder(){ * @throws MessageHeaderFetchingException * @throws Exceptions\EventNotFoundException */ - public function copy($folder) { - $this->client->openFolder($this->folder_path); - $status = $this->client->getConnection()->examineFolder($folder); - /** @var Folder $folder */ - $folder = $this->client->getFolder($folder); - if (isset($status["uidnext"]) && $folder !== null) { + public function copy($folder_path, $expunge = false) { + $this->client->openFolder($folder_path); + $status = $this->client->getConnection()->examineFolder($folder_path); + + if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; + + /** @var Folder $folder */ + $folder = $this->client->getFolder($folder_path); + + $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->copyMessage($folder->path, $this->msgn) == true) { + if($expunge) $this->client->expunge(); + $this->client->openFolder($folder->path); $message_num = $this->client->getConnection()->getMessageNumber($next_uid); From 624dc77acf882c94b1d4f83f592ad26a2a1f91d7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 15:50:16 +0200 Subject: [PATCH 067/600] Mixed message header attribute `in_reply_to` "unified" #26 --- src/Header.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index d93ec205..3006669a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -139,6 +139,9 @@ protected function parse(){ if (property_exists($header, 'subject')) { $this->attributes["subject"] = $this->decode($header->subject); } + if (property_exists($header, 'in_reply_to')) { + $this->attributes["in_reply_to"] = is_array($header->in_reply_to) ? $header->in_reply_to : [$header->in_reply_to]; + } if (property_exists($header, 'references')) { $this->attributes["references"] = $this->decode($header->references); } @@ -458,7 +461,7 @@ private function decodeAddresses($values) { * @param object $header */ private function extractAddresses($header) { - foreach(['from', 'to', 'cc', 'bcc', 'reply_to', 'sender', 'in_reply_to'] as $key){ + foreach(['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key){ if (property_exists($header, $key)) { $this->attributes[$key] = $this->parseAddresses($header->$key); } From 0e5e6f2285d9755ec7b6b6ebcefd92555766d248 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 15:59:35 +0200 Subject: [PATCH 068/600] Missing variable check added to prevent part subtype parsing error #27 --- src/Part.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Part.php b/src/Part.php index 5fd0b9fe..9bfa5774 100644 --- a/src/Part.php +++ b/src/Part.php @@ -166,7 +166,7 @@ protected function parse(){ $body = $this->raw; } - $this->parseSubtype(); + $this->subtype = $this->parseSubtype($this->header->get("content-type")); $this->parseDisposition(); $this->parseDescription(); $this->parseEncoding(); @@ -218,12 +218,23 @@ private function findHeaders(){ /** * Try to parse the subtype if any is present + * @param $content_type + * + * @return string */ - private function parseSubtype(){ - $content_type = $this->header->get("content-type"); + private function parseSubtype($content_type){ + if (is_array($content_type)) { + foreach ($content_type as $part){ + if ((strpos($part, "/")) !== false){ + return $this->parseSubtype($part); + } + } + return null; + } if (($pos = strpos($content_type, "/")) !== false){ - $this->subtype = substr($content_type, $pos + 1); + return substr($content_type, $pos + 1); } + return null; } /** From c8051a5a9ea6b1a83a5ca18a044db26fd3d00abd Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 16:06:13 +0200 Subject: [PATCH 069/600] Missing variable check added to prevent content-type parsing error #356 https://github.com/Webklex/laravel-imap/issues/356 --- src/Part.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Part.php b/src/Part.php index 9bfa5774..524a878d 100644 --- a/src/Part.php +++ b/src/Part.php @@ -166,7 +166,6 @@ protected function parse(){ $body = $this->raw; } - $this->subtype = $this->parseSubtype($this->header->get("content-type")); $this->parseDisposition(); $this->parseDescription(); $this->parseEncoding(); @@ -186,10 +185,15 @@ protected function parse(){ ]); } - if(!empty($this->header->get("content-type"))){ - $rawContentType = $this->header->get("content-type"); - $contentTypeArray = explode(';', $rawContentType); - $this->content_type = trim($contentTypeArray[0]); + $content_types = $this->header->get("content-type"); + if(!empty($content_types)){ + $this->subtype = $this->parseSubtype($content_types); + $content_type = $content_types; + if (is_array($content_types)) { + $content_type = $content_types[0]; + } + $parts = explode(';', $content_type); + $this->content_type = trim($parts[0]); } From f7ce01a46c01041ed698e5fd44c6cffd04dea8ee Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 16:06:45 +0200 Subject: [PATCH 070/600] Changelog updated --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06e72c1..884f2a13 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Prevent text bodies from being fetched as attachment #27 - Missing variable check added to prevent exception while parsing an address [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) +- Missing variable check added to prevent exception while parsing a part subtype #27 +- Missing variable check added to prevent exception while parsing a part content-type [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) +- Mixed message header attribute `in_reply_to` "unified" to be always an array #26 +- Potential message moving / copying problem fixed #29 +- Move messages by using `Protocol::moveMessage()` instead of `Protocol::copyMessage()` and `Message::delete()` #29 ### Added - `Protocol::moveMessage()` method added #29 @@ -18,6 +23,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - Text bodies might no longer get fetched as attachment +- `Message::$in_reply_to` type changed from mixed to array ## [2.1.13] - 2020-10-13 ### Fixed From 91a9f3106b9594e9258fdb1cbe7c30cbfaa734e9 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 16:09:18 +0200 Subject: [PATCH 071/600] Version information added --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 884f2a13..413ffddb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +-NaN + +### Added +-NaN + +### Affected Classes +-NaN + +### Breaking changes +-NaN + + +## [2.2.0] - 2020-10-16 +### Fixed - Prevent text bodies from being fetched as attachment #27 - Missing variable check added to prevent exception while parsing an address [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) - Missing variable check added to prevent exception while parsing a part subtype #27 @@ -20,6 +34,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Message::class](src/Message.php) - [Header::class](src/Header.php) +- [Part::class](src/Part.php) ### Breaking changes - Text bodies might no longer get fetched as attachment From f96412a2b8807b51c02f0408ade90ff0ddc3da37 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 17:35:10 +0200 Subject: [PATCH 072/600] Search for messages by message id --- CHANGELOG.md | 4 ++-- src/Query/WhereQuery.php | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413ffddb..00152942 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip -NaN ### Added --NaN +- Search for messages by message id ### Affected Classes --NaN +- [Query::class](src/Query/Query.php) ### Breaking changes -NaN diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 80f86e76..59c138b7 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -413,6 +413,29 @@ public function whereIsXSpam(){ return $this->where("CUSTOM X-Spam-Flag YES"); } + /** + * Search for a specific header value + * @param $header + * @param $value + * + * @return WhereQuery + * @throws InvalidWhereQueryCriteriaException + */ + public function whereHeader($header, $value){ + return $this->where("CUSTOM HEADER $header $value"); + } + + /** + * Search for a specific message id + * @param $messageId + * + * @return WhereQuery + * @throws InvalidWhereQueryCriteriaException + */ + public function whereMessageId($messageId){ + return $this->whereHeader("Message-ID", $messageId); + } + /** * @param $country_code * From deab0ec32a32d6785249facfa23482b7e25cdb94 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 18:31:34 +0200 Subject: [PATCH 073/600] Search for messages by In-Reply-To --- CHANGELOG.md | 3 ++- src/Query/WhereQuery.php | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00152942..e7f4dff2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip -NaN ### Added -- Search for messages by message id +- Search for messages by message-Id +- Search for messages by In-Reply-To ### Affected Classes - [Query::class](src/Query/Query.php) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 59c138b7..13e2cde0 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -436,6 +436,17 @@ public function whereMessageId($messageId){ return $this->whereHeader("Message-ID", $messageId); } + /** + * Search for a specific message id + * @param $messageId + * + * @return WhereQuery + * @throws InvalidWhereQueryCriteriaException + */ + public function whereInReplyTo($messageId){ + return $this->whereHeader("In-Reply-To", $messageId); + } + /** * @param $country_code * From cee30e46be543f2e1ea3c6b092faa4a351e79048 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 18:40:49 +0200 Subject: [PATCH 074/600] Default folder locations added --- CHANGELOG.md | 1 + README.md | 1 + src/config/imap.php | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f4dff2..44ab3ae2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Search for messages by message-Id - Search for messages by In-Reply-To +- Default folder locations added ### Affected Classes - [Query::class](src/Query/Query.php) diff --git a/README.md b/README.md index 1374f804..f90da4a5 100755 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `fetch_flags` — If set to `false` all messages will be fetched without any flags - `message_key` — Message key identifier option - `fetch_order` — Message fetch order + - `common_folders` — Default folder locations and paths assumed if none is provided - `open` — special configuration for imap_open() - `DISABLE_AUTHENTICATOR` — disable authentication properties. - `decoder` — Currently only the message and attachment decoder can be set diff --git a/src/config/imap.php b/src/config/imap.php index f6f6becd..d270421f 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -104,6 +104,8 @@ | -Fetch order | 'asc' - Order all messages ascending (probably results in oldest first) | 'desc' - Order all messages descending (probably results in newest first) + | -Common folders + | Default folder locations and paths assumed if none is provided | -Open IMAP options: | DISABLE_AUTHENTICATOR - Disable authentication properties. | Use 'GSSAPI' if you encounter the following @@ -122,6 +124,13 @@ 'fetch_flags' => true, 'message_key' => 'list', 'fetch_order' => 'asc', + 'common_folders' => [ + "root" => "INBOX", + "junk" => "INBOX/Junk", + "draft" => "INBOX/Drafts", + "sent" => "INBOX/Sent", + "trash" => "INBOX/Trash", + ], 'open' => [ // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' ], From d94da570b7268bd65d56b00c377225cf3d354507 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 18:42:41 +0200 Subject: [PATCH 075/600] Missing docs added --- src/Query/WhereQuery.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 13e2cde0..3041a746 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -47,6 +47,8 @@ * @method WhereQuery body($value) * @method WhereQuery before($date) * @method WhereQuery bcc($value) + * @method WhereQuery inReplyTo($value) + * @method WhereQuery messageId($value) * * @mixin Query */ From aa4b53f54c21e621bd9d102d1911224df56a583b Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 16 Oct 2020 18:43:38 +0200 Subject: [PATCH 076/600] Message threading added --- CHANGELOG.md | 2 ++ src/Message.php | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ab3ae2..cabcfd49 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Search for messages by message-Id - Search for messages by In-Reply-To +- Message threading added `Message::thread()` - Default folder locations added ### Affected Classes - [Query::class](src/Query/Query.php) +- [Message::class](src/Message.php) ### Breaking changes -NaN diff --git a/src/Message.php b/src/Message.php index f5e41d8a..95f5ab22 100755 --- a/src/Message.php +++ b/src/Message.php @@ -22,6 +22,7 @@ use Webklex\PHPIMAP\Support\FlagCollection; use Webklex\PHPIMAP\Support\Masks\MessageMask; use Illuminate\Support\Str; +use Webklex\PHPIMAP\Support\MessageCollection; use Webklex\PHPIMAP\Traits\HasEvents; /** @@ -634,6 +635,63 @@ public function getFolder(){ return $this->client->getFolder($this->folder_path); } + /** + * Create a message thread based on the current message + * @param Folder|null $sent_folder + * @param MessageCollection|null $thread + * @param Folder|null $folder + * + * @return MessageCollection|null + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\FolderFetchingException + * @throws Exceptions\GetMessagesFailedException + */ + public function thread($sent_folder = null, &$thread = null, $folder = null){ + $thread = $thread ? $thread : MessageCollection::make([]); + $folder = $folder ? $folder : $this->getFolder(); + $sent_folder = $sent_folder ? $sent_folder : $this->client->getFolder(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); + + /** @var Message $message */ + foreach($thread as $message) { + if ($message->message_id == $this->message_id) { + return $thread; + } + } + $thread->push($this); + + $folder->query()->inReplyTo($this->message_id) + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ + /** @var Message $message */ + $message->thread($sent_folder, $thread, $folder); + }); + $sent_folder->query()->inReplyTo($this->message_id) + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ + /** @var Message $message */ + $message->thread($sent_folder, $thread, $folder); + }); + + if (is_array($this->in_reply_to)) { + foreach($this->in_reply_to as $in_reply_to) { + $folder->query()->messageId($in_reply_to) + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ + /** @var Message $message */ + $message->thread($sent_folder, $thread, $folder); + }); + $sent_folder->query()->messageId($in_reply_to) + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ + /** @var Message $message */ + $message->thread($sent_folder, $thread, $folder); + }); + } + } + + return $thread; + } + /** * Copy the current Messages to a mailbox * @param string $folder_path From be0c60a99e0ee63cea0b581967f0f0f9e2d7e2dc Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 19 Oct 2020 11:11:39 +0200 Subject: [PATCH 077/600] Header decoding problem fixed #31 --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 3006669a..497c04d0 100644 --- a/src/Header.php +++ b/src/Header.php @@ -204,7 +204,7 @@ public function rfc822_parse_headers($raw_headers){ }else{ if (($pos = strpos($line, ":")) > 0) { $key = trim(rtrim(strtolower(substr($line, 0, $pos)))); - $value = trim(rtrim(strtolower(substr($line, $pos + 1)))); + $value = trim(rtrim(substr($line, $pos + 1))); $headers[$key] = [$value]; $prev_header = $key; } From ce24f7039a163b60693d435ea8cbfecd80702ea2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 19 Oct 2020 11:11:56 +0200 Subject: [PATCH 078/600] Version information added --- CHANGELOG.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cabcfd49..1600cdd3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,20 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed --NaN +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.2.1] - 2020-10-19 +### Fixed +- Header decoding problem fixed #31 ### Added - Search for messages by message-Id @@ -17,9 +30,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Query::class](src/Query/Query.php) - [Message::class](src/Message.php) - -### Breaking changes --NaN +- [Header::class](src/Header.php) ## [2.2.0] - 2020-10-16 From decd810adc9b0ba7708d07f20fd48069243eafef Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 20 Oct 2020 20:17:41 +0200 Subject: [PATCH 079/600] IMAP::FT_PEEK removing "Seen" flag issue fixed #33 --- CHANGELOG.md | 7 +++++++ src/Message.php | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1600cdd3..80289ea0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [2.2.2] - 2020-10-20 +### Fixed +- IMAP::FT_PEEK removing "Seen" flag issue fixed #33 + +### Affected Classes +- [Message::class](src/Message.php) + ## [2.2.1] - 2020-10-19 ### Fixed - Header decoding problem fixed #31 diff --git a/src/Message.php b/src/Message.php index 95f5ab22..2e0d3ee1 100755 --- a/src/Message.php +++ b/src/Message.php @@ -227,7 +227,7 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu $this->parseBody(); } - if ($this->getFetchFlagsOption() === true && $this->flags->count() == 0) { + if ($this->getFetchFlagsOption() === true) { $this->parseFlags(); } } @@ -395,6 +395,10 @@ private function parseFlags() { public function parseBody() { $this->client->openFolder($this->folder_path); + if ($this->fetch_options == IMAP::FT_PEEK && $this->flags->count() == 0) { + $this->parseFlags(); + } + $contents = $this->client->getConnection()->content([$this->msgn]); if (!isset($contents[$this->msgn])) { throw new MessageContentFetchingException("no content found", 0); @@ -406,8 +410,7 @@ public function parseBody() { $this->fetchStructure($this->structure); if ($this->fetch_options == IMAP::FT_PEEK) { - $this->parseFlags(); - if ($this->getFlags()->get("seen") !== null) { + if ($this->getFlags()->get("seen") == null) { $this->unsetFlag("Seen"); } } From 881ae2182ee6a26a64967d1f063030418cc538bc Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 24 Oct 2020 01:38:18 +0200 Subject: [PATCH 080/600] Text/Html body fetched as attachment if subtype is null #34 --- CHANGELOG.md | 5 +++-- src/Message.php | 52 ++++++++++++++++++++++++------------------------- src/Part.php | 14 +++++++++++++ 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80289ea0..a15c0b1b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Text/Html body fetched as attachment if subtype is null #34 ### Added - NaN ### Affected Classes -- NaN +- [Message::class](src/Message.php) +- [Part::class](src/Part.php) ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index 2e0d3ee1..9d802b49 100755 --- a/src/Message.php +++ b/src/Message.php @@ -438,34 +438,32 @@ private function fetchStructure($structure) { */ private function fetchPart(Part $part) { - if ($part->type == IMAP::MESSAGE_TYPE_TEXT && ($part->ifdisposition == 0 || (empty($part->disposition) || !in_array(strtolower($part->disposition), ['attachment', 'inline'])) ) ) { - - if ( in_array(($subtype = strtolower($part->subtype)), ["plain", "csv", "html"]) && $part->filename == null && $part->name == null) { - $encoding = $this->getEncoding($part); - - $content = $this->decodeString($part->content, $part->encoding); - - // We don't need to do convertEncoding() if charset is ASCII (us-ascii): - // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded - // https://stackoverflow.com/a/11303410 - // - // us-ascii is the same as ASCII: - // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) - // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and - // based on the typographical symbols predominantly in use there. - // https://en.wikipedia.org/wiki/ASCII - // - // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. - if ($encoding != 'us-ascii') { - $content = $this->convertEncoding($content, $encoding); - } - - $this->bodies[$subtype == "plain" ? "text" : $subtype] = $content; - } else { - $this->fetchAttachment($part); - } - } else { + if ($part->isAttachment()) { $this->fetchAttachment($part); + }else{ + $encoding = $this->getEncoding($part); + + $content = $this->decodeString($part->content, $part->encoding); + + // We don't need to do convertEncoding() if charset is ASCII (us-ascii): + // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded + // https://stackoverflow.com/a/11303410 + // + // us-ascii is the same as ASCII: + // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) + // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and + // based on the typographical symbols predominantly in use there. + // https://en.wikipedia.org/wiki/ASCII + // + // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. + if ($encoding != 'us-ascii') { + $content = $this->convertEncoding($content, $encoding); + } + + $subtype = strtolower($part->subtype); + $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; + + $this->bodies[$subtype] = $content; } } diff --git a/src/Part.php b/src/Part.php index 524a878d..d67c6850 100644 --- a/src/Part.php +++ b/src/Part.php @@ -293,4 +293,18 @@ private function parseEncoding(){ } } + /** + * Check if the current part represents an attachment + * + * @return bool + */ + public function isAttachment(){ + if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || (empty($this->disposition) || !in_array(strtolower($this->disposition), ['attachment', 'inline'])) ) ) { + if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "csv", "html"])) && $this->filename == null && $this->name == null) { + return false; + } + } + return true; + } + } From a069c88146e788c6943048c89692deb898146dc5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 24 Oct 2020 01:57:57 +0200 Subject: [PATCH 081/600] Potential header overwriting through header extensions #35 --- CHANGELOG.md | 2 ++ src/Header.php | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a15c0b1b..39e76660 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Text/Html body fetched as attachment if subtype is null #34 +- Potential header overwriting through header extensions #35 ### Added - NaN @@ -14,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Message::class](src/Message.php) - [Part::class](src/Part.php) +- [Header::class](src/Header.php) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index 497c04d0..8134ca3a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -528,9 +528,15 @@ private function extractHeaderExtensions(){ foreach($extensions as $extension) { if (($pos = strpos($extension, "=")) !== false){ $key = substr($extension, 0, $pos); - $value = substr($extension, $pos + 1); - $value = str_replace('"', "", $value); - $this->attributes[trim(rtrim(strtolower($key)))] = trim(rtrim($value)); + $key = trim(rtrim(strtolower($key))); + + if (isset($this->attributes[$key]) === false) { + $value = substr($extension, $pos + 1); + $value = str_replace('"', "", $value); + $value = trim(rtrim($value)); + + $this->attributes[$key] = $value; + } } } } From 230e5042244dfe0d963a9d71307881c784423813 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 31 Oct 2020 14:45:02 +0100 Subject: [PATCH 082/600] Prevent empty attachments #37 --- CHANGELOG.md | 1 + src/Message.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e76660..003cafdc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Text/Html body fetched as attachment if subtype is null #34 - Potential header overwriting through header extensions #35 +- Prevent empty attachments #37 ### Added - NaN diff --git a/src/Message.php b/src/Message.php index 9d802b49..d8b121ea 100755 --- a/src/Message.php +++ b/src/Message.php @@ -475,7 +475,7 @@ protected function fetchAttachment($part) { $oAttachment = new Attachment($this, $part); - if ($oAttachment->getName() !== null) { + if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { if ($oAttachment->getId() !== null) { $this->attachments->put($oAttachment->getId(), $oAttachment); } else { From aad7db5ccee37de84c85d28ff5f7876b1c23110b Mon Sep 17 00:00:00 2001 From: Adnan RIHAN Date: Mon, 2 Nov 2020 13:41:22 +0100 Subject: [PATCH 083/600] Set fetch order during query (#41) * Set fetch order during query * Documents query fetching order * Add back `$message_key` to `Query::get()` Fix bug introduced by cb43ff2 --- README.md | 39 +++++++++++++++++++++++ src/Query/Query.php | 77 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f90da4a5..4e0f5f98 100755 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # IMAP Library for PHP [![Latest Version on Packagist][ico-version]][link-packagist] @@ -503,6 +504,41 @@ $messages = $folder->query()->whereAll() ->get(); ``` +Change messages fetch order: +```php +/** @var \Webklex\PHPIMAP\Folder $folder */ + +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereText('Hello world') +->setFetchOrder('asc') +->get(); + +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereAll() +->setFetchOrderAsc() +->get(); + +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereAll() +->fetchOrderAsc() +->get(); + +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereText('Hello world') +->setFetchOrder('desc') +->get(); + +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereAll() +->setFetchOrderDesc() +->get(); + +/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ +$messages = $folder->query()->whereAll() +->fetchOrderDesc() +->get(); +``` + #### Events The following events are available: @@ -774,6 +810,9 @@ if you're just wishing a feature ;) | setFetchOptions | boolean $fetch_options | WhereQuery | Set the fetch options | | setFetchBody | boolean $fetch_body | WhereQuery | Set the fetch body option | | setFetchFlags | boolean $fetch_flags | WhereQuery | Set the fetch flags option | +| setFetchOrder | string $fetch_order | WhereQuery | Change the fetch ordering (`asc` is "oldest first", `desc` is "newest first") | +| setFetchOrderAsc | | WhereQuery | Change the fetch ordering to ascending | +| setFetchOrderDesc | | WhereQuery | Change the fetch ordering to descending | | leaveUnread | | WhereQuery | Don't mark all messages as "read" while fetching: | | markAsRead | | WhereQuery | Mark all messages as "read" while fetching | | paginate | int $perPage = 5, $page = null, $pageName = 'imap_page' | LengthAwarePaginator | Paginate the current query. | diff --git a/src/Query/Query.php b/src/Query/Query.php index 226746d2..a14d16a2 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -62,6 +62,9 @@ class Query { /** @var int $fetch_flags */ protected $fetch_flags = true; + /** @var int $fetch_order */ + protected $fetch_order; + /** @var string $date_format */ protected $date_format; @@ -75,6 +78,12 @@ public function __construct(Client $client, $charset = 'UTF-8') { if(ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); + if (ClientManager::get('options.fetch_order') === 'desc') { + $this->fetch_order = 'desc'; + } else { + $this->fetch_order = 'asc'; + } + $this->date_format = ClientManager::get('date_format', 'd M y'); $this->charset = $charset; @@ -193,17 +202,16 @@ public function get() { $messages->total($available_messages_count); - $options = ClientManager::get('options'); - - if(strtolower($options['fetch_order']) === 'desc'){ + if ($this->fetch_order === 'desc') { $available_messages = $available_messages->reverse(); } + $message_key = ClientManager::get('options.message_key'); $query =& $this; - $available_messages->forPage($this->page, $this->limit)->each(function($msgno, $msglist) use(&$messages, $options, $query) { + $available_messages->forPage($this->page, $this->limit)->each(function($msgno, $msglist) use(&$messages, $message_key, $query) { $message = $query->getMessage($msgno, $msglist); - switch ($options['message_key']){ + switch ($message_key){ case 'number': $message_key = $message->getMessageNo(); break; @@ -468,4 +476,61 @@ public function setFetchFlags($fetch_flags) { $this->fetch_flags = $fetch_flags; return $this; } -} \ No newline at end of file + + /** + * @param string $fetch_order + * @return Query + */ + public function setFetchOrder($fetch_order) { + $fetch_order = strtolower($fetch_order); + + if (in_array($fetch_order, ['asc', 'desc'])) { + $this->fetch_order = $fetch_order; + } + + return $this; + } + + /** + * @param string $fetch_order + * @return Query + */ + public function fetchOrder($fetch_order) { + return $this->setFetchOrder($fetch_order); + } + + /** + * @return string + */ + public function getFetchOrder() { + return $this->fetch_order; + } + + /** + * @return Query + */ + public function setFetchOrderAsc() { + return $this->setFetchOrder('asc'); + } + + /** + * @return Query + */ + public function fetchOrderAsc($fetch_order) { + return $this->setFetchOrderAsc(); + } + + /** + * @return Query + */ + public function setFetchOrderDesc() { + return $this->setFetchOrder('desc'); + } + + /** + * @return Query + */ + public function fetchOrderDesc($fetch_order) { + return $this->setFetchOrderDesc(); + } +} From 2ead74f6f57e47976306f6a6afbcfcc8771b7a1d Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 2 Nov 2020 13:42:46 +0100 Subject: [PATCH 084/600] Type fixed --- src/Query/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index a14d16a2..9d3fb49b 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -62,7 +62,7 @@ class Query { /** @var int $fetch_flags */ protected $fetch_flags = true; - /** @var int $fetch_order */ + /** @var string $fetch_order */ protected $fetch_order; /** @var string $date_format */ From 4b5f38a2ea7ba4228f3eefd2028c59804955bed5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 2 Nov 2020 13:43:21 +0100 Subject: [PATCH 085/600] Version information updated --- CHANGELOG.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 003cafdc..e47218d0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,33 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + + +## [2.2.3] - 2020-11-02 +### Fixed - Text/Html body fetched as attachment if subtype is null #34 - Potential header overwriting through header extensions #35 - Prevent empty attachments #37 ### Added -- NaN +- Set fetch order during query #41 [@Max13](https://github.com/Max13) ### Affected Classes - [Message::class](src/Message.php) - [Part::class](src/Part.php) - [Header::class](src/Header.php) +- [Query::class](src/Query/Query.php) -### Breaking changes -- NaN ## [2.2.2] - 2020-10-20 ### Fixed From 6aa44d0829975bb9975a95293d72cf061df6d581 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 3 Nov 2020 07:28:32 +0100 Subject: [PATCH 086/600] Missing message setter methods added --- src/Message.php | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/Message.php b/src/Message.php index d8b121ea..0dd3e39f 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1028,4 +1028,84 @@ public function mask($mask = null){ throw new MaskNotFoundException("Unknown mask provided: ".$mask); } + + /** + * Set the message path aka folder path + * @param $folder_path + * + * @return $this + */ + public function setFolderPath($folder_path){ + $this->folder_path = $folder_path; + + return $this; + } + + /** + * Set the config + * @param $config + * + * @return $this + */ + public function setConfig($config){ + $this->config = $config; + + return $this; + } + + /** + * Set the attachment collection + * @param $attachments + * + * @return $this + */ + public function setAttachments($attachments){ + $this->attachments = $attachments; + + return $this; + } + + /** + * Set the flag collection + * @param $flags + * + * @return $this + */ + public function setFlags($flags){ + $this->flags = $flags; + + return $this; + } + + /** + * Set the client + * @param $client + * + * @throws Exceptions\ConnectionFailedException + * @return $this + */ + public function setClient($client){ + $this->client = $client; + $this->client->openFolder($this->folder_path); + + return $this; + } + + /** + * Set the message number + * @param $msgn + * @param null $msglist + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException + * @return $this + */ + public function setMsgn($msgn, $msglist = null){ + $this->msgn = $msgn; + $this->msglist = $msglist; + + $this->uid = $this->client->getConnection()->getUid($this->msgn); + + return $this; + } } From 4b6ca8c37be91f930d61abc3b641a374be7859dc Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 3 Nov 2020 13:28:09 +0100 Subject: [PATCH 087/600] Typo fixed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e0f5f98..5003e732 100755 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ foreach($folders as $folder){ echo $message->getHTMLBody(); //Move the current Message to 'INBOX.read' - if($message->moveToFolder('INBOX.read') == true){ + if($message->move('INBOX.read') == true){ echo 'Message has ben moved'; }else{ echo 'Message could not be moved'; From 7b77e0ea09eb83e4564dd324f178ea56281fc583 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 3 Nov 2020 18:35:58 +0100 Subject: [PATCH 088/600] Search performance increased by fetching everything at once #42 --- src/Message.php | 104 +++++++++++++++++++++++++++++++++++++++----- src/Query/Query.php | 25 +++++++++-- 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/src/Message.php b/src/Message.php index 0dd3e39f..4a05a94f 100755 --- a/src/Message.php +++ b/src/Message.php @@ -51,7 +51,6 @@ * @method integer getUid() * @method integer setUid(integer $uid) * @method integer getMsgn() - * @method integer setMsgn(integer $msgn) * @method integer getPriority() * @method integer setPriority(integer $priority) * @method string getSubject() @@ -232,6 +231,53 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu } } + /** + * Create a new instance without fetching the message header and providing them raw instead + * @param int $msgn + * @param int|null $msglist + * @param Client $client + * @param string $raw_header + * @param string $raw_body + * @param string $raw_flags + * @param null $fetch_options + * + * @return Message + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws Exceptions\RuntimeException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws \ReflectionException + */ + public static function make($msgn, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null){ + $reflection = new \ReflectionClass(self::class); + /** @var self $instance */ + $instance = $reflection->newInstanceWithoutConstructor(); + + $default_mask = $client->getDefaultMessageMask(); + if($default_mask != null) { + $instance->setMask($default_mask); + } + $instance->setEvents([ + "message" => $client->getDefaultEvents("message"), + "flag" => $client->getDefaultEvents("flag"), + ]); + $instance->setFolderPath($client->getFolderPath()); + $instance->setConfig(ClientManager::get('options')); + $instance->setFetchOption($fetch_options); + + $instance->setAttachments(AttachmentCollection::make([])); + + $instance->setClient($client); + $instance->setMsgn($msgn, $msglist); + + $instance->parseRawHeader($raw_header); + $instance->parseRawFlags($raw_flags); + $instance->parseRawBody($raw_body); + + return $instance; + } + /** * Call dynamic attribute setter and getter methods * @param string $method @@ -353,7 +399,34 @@ private function parseHeader() { throw new MessageHeaderFetchingException("no headers found", 0); } - $this->header = new Header($headers[$this->msgn]); + $this->parseRawHeader($headers[$this->msgn]); + } + + /** + * @param string $raw_header + * + * @throws InvalidMessageDateException + */ + public function parseRawHeader($raw_header){ + $this->header = new Header($raw_header); + } + + /** + * Parse additional raw flags + * @param $raw_flags + */ + public function parseRawFlags($raw_flags) { + $this->flags = FlagCollection::make([]); + + foreach($raw_flags as $flag) { + if (strpos($flag, "\\") === 0){ + $flag = substr($flag, 1); + } + $flag_key = strtolower($flag); + if (in_array($flag_key, $this->available_flags)) { + $this->flags->put($flag_key, $flag); + } + } } /** @@ -370,15 +443,7 @@ private function parseFlags() { $flags = $this->client->getConnection()->flags([$this->msgn]); if (isset($flags[$this->msgn])) { - foreach($flags[$this->msgn] as $flag) { - if (strpos($flag, "\\") === 0){ - $flag = substr($flag, 1); - } - $flag_key = strtolower($flag); - if (in_array($flag_key, $this->available_flags)) { - $this->flags->put($flag_key, $flag); - } - } + $this->parseRawFlags($flags[$this->msgn]); } } @@ -405,7 +470,22 @@ public function parseBody() { } $content = $contents[$this->msgn]; - $this->structure = new Structure($content, $this->header); + return $this->parseRawBody($content); + } + + /** + * Parse a given message body + * @param string $raw_body + * + * @return $this + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws Exceptions\RuntimeException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + */ + public function parseRawBody($raw_body) { + $this->structure = new Structure($raw_body, $this->header); $this->fetchStructure($this->structure); diff --git a/src/Query/Query.php b/src/Query/Query.php index 9d3fb49b..b2b08afa 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -207,10 +207,26 @@ public function get() { } $message_key = ClientManager::get('options.message_key'); - $query =& $this; - $available_messages->forPage($this->page, $this->limit)->each(function($msgno, $msglist) use(&$messages, $message_key, $query) { - $message = $query->getMessage($msgno, $msglist); + $msgnos = $available_messages->forPage($this->page, $this->limit)->toArray(); + + $raw_headers = $this->client->getConnection()->headers($msgnos); + $raw_contents = []; + $raw_flags = []; + + if ($this->getFetchBody()) { + $raw_contents = $this->client->getConnection()->content($msgnos); + } + if ($this->getFetchFlags()) { + $raw_flags = $this->client->getConnection()->flags($msgnos); + } + + $msglist = 0; + foreach ($raw_headers as $msgno => $raw_header) { + $raw_content = isset($raw_contents[$msgno]) ? $raw_contents[$msgno] : ""; + $raw_flag = isset($raw_flags[$msgno]) ? $raw_flags[$msgno] : []; + + $message = Message::make($msgno, $msglist, $this->getClient(), $raw_header, $raw_content, $raw_flag, $this->getFetchOptions()); switch ($message_key){ case 'number': $message_key = $message->getMessageNo(); @@ -224,7 +240,8 @@ public function get() { } $messages->put($message_key, $message); - }); + $msglist++; + } } return $messages; From 812ed75bcf4a6b47ed0db5df63545a6d4faf3e69 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 3 Nov 2020 19:06:36 +0100 Subject: [PATCH 089/600] Legacy protocol support updated --- src/Connection/Protocols/LegacyProtocol.php | 46 ++++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 5b6baa3e..1f6f6196 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -91,7 +91,7 @@ public function login($user, $password) { throw new AuthFailedException($message); } - return !$this->stream; + return $this->stream; } /** @@ -199,8 +199,12 @@ public function examineFolder($folder = 'INBOX') { * @return array */ public function content($uids, $rfc = "RFC822") { - $content = \imap_fetchbody($this->stream, $uids[0], "", IMAP::FT_UID); - return [$uids[0] => $content]; + $result = []; + $uids = is_array($uids) ? $uids : [$uids]; + foreach ($uids as $uid) { + $result[$uid] = \imap_fetchbody($this->stream, $uid, "", IMAP::NIL); + } + return $result; } /** @@ -211,8 +215,12 @@ public function content($uids, $rfc = "RFC822") { * @return array */ public function headers($uids, $rfc = "RFC822"){ - $headers = \imap_fetchheader($this->stream, $uids[0], IMAP::FT_UID); - return [$uids[0] => $headers]; + $result = []; + $uids = is_array($uids) ? $uids : [$uids]; + foreach ($uids as $uid) { + $result[$uid] = \imap_fetchheader($this->stream, $uid, IMAP::NIL); + } + return $result; } /** @@ -222,17 +230,23 @@ public function headers($uids, $rfc = "RFC822"){ * @return array */ public function flags($uids){ - $flags = \imap_fetch_overview($this->stream, $uids[0], IMAP::FT_UID); $result = []; - if (is_array($flags) && isset($flags[0])) { - $flags = (array) $flags[0]; - foreach($flags as $flag => $value) { - if ($value === 1 && in_array($flag, ["size", "uid", "msgno", "update"]) === false){ - $result[] = "\\".ucfirst($flag); + $uids = is_array($uids) ? $uids : [$uids]; + foreach ($uids as $uid) { + $raw_flags = \imap_fetch_overview($this->stream, $uid, IMAP::NIL); + $flags = []; + if (is_array($raw_flags) && isset($raw_flags[0])) { + $raw_flags = (array) $raw_flags[0]; + foreach($raw_flags as $flag => $value) { + if ($value === 1 && in_array($flag, ["size", "uid", "msgno", "update"]) === false){ + $flags[] = "\\".ucfirst($flag); + } } } + $result[$uid] = $flags; } - return [$uids[0] => $result]; + + return $result; } /** @@ -309,12 +323,12 @@ public function folders($reference = '', $folder = '*') { * @return bool|array new flags if $silent is false, else true or false depending on success */ public function store(array $flags, $from, $to = null, $mode = null, $silent = true) { - $flag = "\\".trim(is_array($flags) ? implode(" \\", $flags) : $flags); + $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); if ($mode == "+"){ - $status = \imap_setflag_full($this->stream, $from, $flag, IMAP::SE_UID); + $status = \imap_setflag_full($this->stream, $from, $flag, IMAP::NIL); }else{ - $status = \imap_clearflag_full($this->stream, $from, $flag, IMAP::SE_UID); + $status = \imap_clearflag_full($this->stream, $from, $flag, IMAP::NIL); } if ($silent === true) { @@ -464,7 +478,7 @@ public function done() { * @return array message ids */ public function search(array $params) { - return \imap_search($this->stream, $params[0], IMAP::SE_UID); + return \imap_search($this->stream, $params[0], IMAP::NIL); } /** From f273c75c66cf4e27e14fa5db55f3e130c59926fc Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 3 Nov 2020 19:31:56 +0100 Subject: [PATCH 090/600] Fetch all headers of all messages in the current folder --- src/Connection/Protocols/ImapProtocol.php | 27 +++++++++++++++++++ src/Connection/Protocols/LegacyProtocol.php | 2 +- .../Protocols/ProtocolInterface.php | 8 ++++++ src/Folder.php | 13 +++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index a9460886..ead7d33d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -15,6 +15,7 @@ use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\RuntimeException; +use Webklex\PHPIMAP\Header; /** * Class ImapProtocol @@ -968,6 +969,32 @@ public function search(array $params) { return []; } + /** + * Get a message overview + * @param string $sequence + * @return array + * + * @throws RuntimeException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + */ + public function overview($sequence) { + $result = []; + list($from, $to) = explode(":", $sequence); + + $uids = $this->getUid(); + $ids = []; + foreach ($uids as $msgn => $v) { + if ( ($to >= $msgn && $from <= $msgn) || ($to === "*" && $from <= $msgn) ){ + $ids[] = $msgn; + } + } + $headers = $this->headers($ids); + foreach ($headers as $msgn => $raw_header) { + $result[$msgn] = (new Header($raw_header))->getAttributes(); + } + return $result; + } + /** * Enable the debug mode */ diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 1f6f6196..d303b761 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -278,7 +278,7 @@ public function getMessageNumber($id) { } /** - * Get a message number for a uid + * Get a message overview * @param string $sequence uid sequence * * @return array diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index a9519051..8acbb461 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -276,6 +276,14 @@ public function noop(); */ public function search(array $params); + /** + * Get a message overview + * @param string $sequence uid sequence + * + * @return array + */ + public function overview($sequence); + /** * Enable the debug mode */ diff --git a/src/Folder.php b/src/Folder.php index 78bbf09e..c37f2079 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -241,6 +241,19 @@ public function move($new_name, $expunge = true) { return $status; } + /** + * Get a message overview + * @param string|null $sequence uid sequence + * + * @return array + * @throws ConnectionFailedException + */ + public function overview($sequence = null){ + $this->client->openFolder($this->path); + $sequence = $sequence === null ? "1:*" : $sequence; + return $this->client->getConnection()->overview($sequence); + } + /** * Append a string message to the current mailbox * @param string $message From 0448830064c278b8c36b9ee15a3f4bdae21dbc90 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 3 Nov 2020 19:32:16 +0100 Subject: [PATCH 091/600] Changelog updated --- CHANGELOG.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47218d0..67aea199 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Search performance increased by fetching all headers, bodies and flags at once #42 +- Legacy protocol support updated ### Added -- NaN +- Missing message setter methods added +- `Folder::overview()` method added to fetch all headers of all messages in the current folder ### Affected Classes -- NaN +- [Message::class](src/Message.php) +- [Folder::class](src/Folder.php) +- [Query::class](src/Query/Query.php) ### Breaking changes - NaN From 66bbb3c857595d6108a6cd0df7a035a57bae8750 Mon Sep 17 00:00:00 2001 From: Mike Miller <66344776+mikemiller891@users.noreply.github.com> Date: Tue, 8 Dec 2020 09:08:37 -0600 Subject: [PATCH 092/600] Fix Query pagination. (#52) --- src/Query/Query.php | 2 +- src/Support/PaginatedCollection.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index b2b08afa..4e2d21c6 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -289,7 +289,7 @@ public function paginate($per_page = 5, $page = null, $page_name = 'imap_page'){ $this->limit = $per_page; - return $this->get()->paginate($per_page, $this->page, $page_name); + return $this->get()->paginate($per_page, $this->page, $page_name, true); } /** diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 72f3bdba..4c8bab32 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -38,12 +38,12 @@ class PaginatedCollection extends Collection { * * @return LengthAwarePaginator */ - public function paginate($per_page = 15, $page = null, $page_name = 'page') { + public function paginate($per_page = 15, $page = null, $page_name = 'page', $prepaginated = false) { $page = $page ?: Paginator::resolveCurrentPage($page_name); $total = $this->total ? $this->total : $this->count(); - $results = $total ? $this->forPage($page, $per_page) : $this->all(); + $results = !$prepaginated && $total ? $this->forPage($page, $per_page) : $this->all(); return $this->paginator($results, $total, $per_page, $page, [ 'path' => Paginator::resolveCurrentPath(), @@ -78,4 +78,4 @@ public function total($total = null) { return $this->total = $total; } -} \ No newline at end of file +} From b1b81e425625cb6215e28bb3662a5a3e3cf82cdc Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 8 Dec 2020 16:21:32 +0100 Subject: [PATCH 093/600] Update CHANGELOG.md --- CHANGELOG.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67aea199..797b8168 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,22 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.2.4] - 2020-12-08 +### Fixed - Search performance increased by fetching all headers, bodies and flags at once #42 - Legacy protocol support updated +- Fix Query pagination. (#52 [@mikemiller891](https://github.com/mikemiller891)) ### Added - Missing message setter methods added @@ -17,10 +31,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Message::class](src/Message.php) - [Folder::class](src/Folder.php) - [Query::class](src/Query/Query.php) - -### Breaking changes -- NaN - +- [PaginatedCollection::class](src/Support/PaginatedCollection.php) ## [2.2.3] - 2020-11-02 ### Fixed From 9b186e2e528f92f8ed72a1189d29b7e260dbf265 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 10 Dec 2020 14:31:56 +0100 Subject: [PATCH 094/600] Missing array decoder method added #51 --- CHANGELOG.md | 4 ++-- src/Header.php | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 797b8168..272ae51a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) ### Added - NaN ### Affected Classes -- NaN +- [Header::class](src/Header.php) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index 8134ca3a..dcda848e 100644 --- a/src/Header.php +++ b/src/Header.php @@ -360,11 +360,14 @@ public function getEncoding($structure) { /** * Try to decode a specific header - * @param $value + * @param mixed $value * - * @return string|null + * @return mixed */ private function decode($value) { + if (is_array($value)) { + return $this->decodeArray($value); + } $original_value = $value; $decoder = $this->config['decoder']['message']; @@ -400,6 +403,19 @@ private function decode($value) { return $value; } + /** + * Decode a given array + * @param array $values + * + * @return array + */ + private function decodeArray($values) { + foreach($values as $key => $value) { + $values[$key] = $this->decode($value); + } + return $values; + } + /** * Try to extract the priority from a given raw header string */ From fee6aa310eef5546304c24f30540f8fcea0addef Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 01:24:48 +0100 Subject: [PATCH 095/600] Force a folder to be opened --- CHANGELOG.md | 3 ++- src/Client.php | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 272ae51a..58e1dcf1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) ### Added -- NaN +- Force a folder to be opened ### Affected Classes - [Header::class](src/Header.php) +- [Folder::class](src/Folder.php) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index 9ed74a13..ffa9f400 100755 --- a/src/Client.php +++ b/src/Client.php @@ -356,6 +356,7 @@ public function disconnect() { if ($this->isConnected() && $this->connection !== false) { $this->connection->logout(); } + $this->active_folder = false; return $this; } @@ -376,7 +377,7 @@ public function getFolder($folder_name, $delimiter = null) { if (strpos($folder_name, $delimiter) !== false) { return $this->getFolderByPath($folder_name); } - } + } return $this->getFolderByName($folder_name); } @@ -446,12 +447,13 @@ public function getFolders($hierarchical = true, $parent_folder = null) { /** * Open folder. * @param string $folder + * @param boolean $force_select * * @return mixed * @throws ConnectionFailedException */ - public function openFolder($folder) { - if ($this->active_folder == $folder && $this->isConnected()) { + public function openFolder($folder, $force_select = false) { + if ($this->active_folder == $folder && $this->isConnected() && $force_select === false) { return true; } $this->checkConnection(); @@ -472,6 +474,7 @@ public function openFolder($folder) { public function createFolder($folder, $expunge = true) { $this->checkConnection(); $status = $this->connection->createFolder($folder); + if($expunge) $this->expunge(); $folder = $this->getFolder($folder); From d00b0417b194020ed8065118d27049c55d76ef3e Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 01:44:20 +0100 Subject: [PATCH 096/600] Checks added to prevent message from getting marked as seen #33 --- CHANGELOG.md | 3 +++ src/Message.php | 33 ++++++++++++++++++++++++--------- src/Query/Query.php | 6 +++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e1dcf1..1cb35cd5 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) +- Additional checks added to prevent message from getting marked as seen #33 ### Added - Force a folder to be opened @@ -14,6 +15,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Header::class](src/Header.php) - [Folder::class](src/Folder.php) +- [Query::class](src/Query/Query.php) +- [Message::class](src/Message.php) ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index 4a05a94f..a6bbbe7a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -273,8 +273,22 @@ public static function make($msgn, $msglist, Client $client, $raw_header, $raw_b $instance->parseRawHeader($raw_header); $instance->parseRawFlags($raw_flags); + + + if ($fetch_options == IMAP::FT_PEEK && $instance->getFlags()->count() == 0) { + $instance->parseFlags(); + } + $instance->parseRawBody($raw_body); + if ($fetch_options == IMAP::FT_PEEK) { + if ($instance->getFlags()->get("seen") == null) { + $instance->unsetFlag("Seen"); + } + } elseif ($instance->getFlags()->get("seen") == null) { + $instance->setFlag("Seen"); + } + return $instance; } @@ -470,7 +484,15 @@ public function parseBody() { } $content = $contents[$this->msgn]; - return $this->parseRawBody($content); + $body = $this->parseRawBody($content); + + if ($this->fetch_options == IMAP::FT_PEEK) { + if ($this->getFlags()->get("seen") == null) { + $this->unsetFlag("Seen"); + } + } + + return $body; } /** @@ -489,12 +511,6 @@ public function parseRawBody($raw_body) { $this->fetchStructure($this->structure); - if ($this->fetch_options == IMAP::FT_PEEK) { - if ($this->getFlags()->get("seen") == null) { - $this->unsetFlag("Seen"); - } - } - return $this; } @@ -1038,7 +1054,7 @@ public function getFlags() { /** * Get the fetched structure * - * @return object|null + * @return Structure|null */ public function getStructure(){ return $this->structure; @@ -1183,7 +1199,6 @@ public function setClient($client){ public function setMsgn($msgn, $msglist = null){ $this->msgn = $msgn; $this->msglist = $msglist; - $this->uid = $this->client->getConnection()->getUid($this->msgn); return $this; diff --git a/src/Query/Query.php b/src/Query/Query.php index 4e2d21c6..820c5e94 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -214,12 +214,12 @@ public function get() { $raw_contents = []; $raw_flags = []; - if ($this->getFetchBody()) { - $raw_contents = $this->client->getConnection()->content($msgnos); - } if ($this->getFetchFlags()) { $raw_flags = $this->client->getConnection()->flags($msgnos); } + if ($this->getFetchBody()) { + $raw_contents = $this->client->getConnection()->content($msgnos); + } $msglist = 0; foreach ($raw_headers as $msgno => $raw_header) { From 5fd862bcdd5ca6e4f94d5d89159940b9a98c0dc2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 01:52:17 +0100 Subject: [PATCH 097/600] Boundary parsing improved #39 #36 --- CHANGELOG.md | 2 ++ src/Structure.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb35cd5..2844bda8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) - Additional checks added to prevent message from getting marked as seen #33 +- Boundary parsing improved #39 #36 (thanks [@AntonioDiPassio-AppSys](https://github.com/AntonioDiPassio-AppSys)) ### Added - Force a folder to be opened @@ -17,6 +18,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Folder::class](src/Folder.php) - [Query::class](src/Query/Query.php) - [Message::class](src/Message.php) +- [Structure::class](src/Structure.php) ### Breaking changes - NaN diff --git a/src/Structure.php b/src/Structure.php index 1b12b80c..5fc7e316 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -100,7 +100,7 @@ public function findContentType(){ * Determine the message content type */ public function getBoundary(){ - $boundary = $this->header->find("/boundary\=\"?(.*)\"?/"); + $boundary = $this->header->find("/boundary=\"?([^\"]*)\"?/"); return str_replace('"', '', $boundary); } From 1852eaea9c843a901ecd0a3deefdad509462c764 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 02:04:33 +0100 Subject: [PATCH 098/600] Idle operation updated #44 --- CHANGELOG.md | 1 + src/Folder.php | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2844bda8..a7708652 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) - Additional checks added to prevent message from getting marked as seen #33 - Boundary parsing improved #39 #36 (thanks [@AntonioDiPassio-AppSys](https://github.com/AntonioDiPassio-AppSys)) +- Idle operation updated #44 ### Added - Force a folder to be opened diff --git a/src/Folder.php b/src/Folder.php index c37f2079..6adc8c51 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -353,17 +353,19 @@ public function idle(callable $callback, $timeout = 1200) { $this->client->getConnection()->setConnectionTimeout($timeout); $this->client->reconnect(); - $this->client->openFolder($this->path); + $this->client->openFolder($this->path, true); $connection = $this->client->getConnection(); $connection->idle(); while (true) { try { $line = $connection->nextLine(); + var_dump($line); if (($pos = strpos($line, "EXISTS")) !== false) { $msgn = (int) substr($line, 2, $pos -2); $connection->done(); + $this->client->openFolder($this->path, true); $message = $this->query()->getMessage($msgn); $callback($message); @@ -375,11 +377,6 @@ public function idle(callable $callback, $timeout = 1200) { }catch (Exceptions\RuntimeException $e) { if(strpos($e->getMessage(), "connection closed") === false) { throw $e; - }else{ - $this->client->connect(); - $this->client->openFolder($this->path); - $connection = $this->client->getConnection(); - $connection->idle(); } } } From 7e786db08ab135139c181a6bfa41169070bc4a09 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 02:08:46 +0100 Subject: [PATCH 099/600] Dump removed --- src/Folder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Folder.php b/src/Folder.php index 6adc8c51..8609e790 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -360,7 +360,6 @@ public function idle(callable $callback, $timeout = 1200) { while (true) { try { $line = $connection->nextLine(); - var_dump($line); if (($pos = strpos($line, "EXISTS")) !== false) { $msgn = (int) substr($line, 2, $pos -2); $connection->done(); From f6b0b6b75b413533b60dba2b5a037c7d0dcc3d92 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 02:17:38 +0100 Subject: [PATCH 100/600] Idle note added --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5003e732..17adc6d8 100755 --- a/README.md +++ b/README.md @@ -238,6 +238,8 @@ $folder->idle(function($message){ echo $message->subject."\n"; }); ``` +**Attention!** Every received message will be flagged as "Seen". Update your config `options.fetch` from +`\Webklex\PHPIMAP\IMAP::FT_UID` to `\Webklex\PHPIMAP\IMAP::FT_PEEK` to prevent this behavior. #### Search for messages From 0d10741eb1e90a567d839f6de2e5a9c9feb2218a Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 02:18:20 +0100 Subject: [PATCH 101/600] Version information added --- CHANGELOG.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7708652..1d855cae 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.2.5] - 2020-12-11 +### Fixed - Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) - Additional checks added to prevent message from getting marked as seen #33 - Boundary parsing improved #39 #36 (thanks [@AntonioDiPassio-AppSys](https://github.com/AntonioDiPassio-AppSys)) @@ -21,9 +34,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Message::class](src/Message.php) - [Structure::class](src/Structure.php) -### Breaking changes -- NaN - ## [2.2.4] - 2020-12-08 ### Fixed - Search performance increased by fetching all headers, bodies and flags at once #42 From eef78d3e808df0113de7b9fa3fa7c407a8d8789a Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 09:05:09 +0100 Subject: [PATCH 102/600] Proxy support added #53 & cert validation issue fixed --- CHANGELOG.md | 8 +-- README.md | 5 ++ src/Client.php | 28 ++++++-- src/Connection/Protocols/ImapProtocol.php | 11 +-- src/Connection/Protocols/LegacyProtocol.php | 4 +- src/Connection/Protocols/Protocol.php | 72 ++++++++++++++++--- .../Protocols/ProtocolInterface.php | 3 +- src/config/imap.php | 6 ++ 8 files changed, 112 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d855cae..75b6c016 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,16 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Cert validation issue fixed ### Added -- NaN +- Proxy support added ### Affected Classes -- NaN +- [Client::class](src/Client.php) ### Breaking changes -- NaN +- Depending on your configuration, your certificates actually get checked. Which can cause an aborted connection if the certificate can not be validated. ## [2.2.5] - 2020-12-11 ### Fixed diff --git a/README.md b/README.md index 17adc6d8..c16be95a 100755 --- a/README.md +++ b/README.md @@ -107,6 +107,11 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `username` — imap account username - `password` — imap account password - `authentication` — imap authentication method. Use `oauth` to use oAuth for Google, etc. + - `proxy` — Optional proxy settings + - `socket` — Proxy host including the port e.g. example.com:123 + - `request_fulluri` — When set to TRUE, the entire URI will be used when constructing the request. While this is a non-standard request format, some proxy servers require it. + - `username` — Optional username if required + - `password` — Optional password if required - `date_format` — The default date format is used to convert any given Carbon::class object into a valid date string. (`d-M-Y`, `d-M-y`, `d M y`) - `options` — additional fetch options - `delimiter` — you can use any supported char such as ".", "/", etc diff --git a/src/Client.php b/src/Client.php index ffa9f400..fc568804 100755 --- a/src/Client.php +++ b/src/Client.php @@ -73,9 +73,20 @@ class Client { /** * If server has to validate cert. * - * @var mixed + * @var bool */ - public $validate_cert; + public $validate_cert = true; + + /** + * Proxy settings + * @var array + */ + protected $proxy = [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ]; /** * Account username/ @@ -133,6 +144,12 @@ class Client { 'username' => '', 'password' => '', 'authentication' => null, + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ] ]; /** @@ -309,20 +326,21 @@ public function connect() { if ($protocol == "imap") { $timeout = $this->connection !== false ? $this->connection->getConnectionTimeout() : null; - $this->connection = new ImapProtocol($this->validate_cert); + $this->connection = new ImapProtocol($this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($timeout); + $this->connection->setProxy($this->proxy); }else{ if (extension_loaded('imap') === false) { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); } - $this->connection = new LegacyProtocol($this->validate_cert); + $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption); if (strpos($protocol, "legacy-") === 0) { $protocol = substr($protocol, 7); } $this->connection->setProtocol($protocol); } - $this->connection->connect($this->host, $this->port, $this->encryption); + $this->connection->connect($this->host, $this->port); $this->authenticate(); return $this; diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ead7d33d..757e7966 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -33,9 +33,11 @@ class ImapProtocol extends Protocol implements ProtocolInterface { /** * Imap constructor. * @param bool $cert_validation set to false to skip SSL certificate validation + * @param mixed $encryption Connection encryption method */ - public function __construct($cert_validation = true) { + public function __construct($cert_validation = true, $encryption = false) { $this->setCertValidation($cert_validation); + $this->encryption = $encryption; } /** @@ -49,15 +51,14 @@ public function __destruct() { * Open connection to IMAP server * @param string $host hostname or IP address of IMAP server * @param int|null $port of IMAP server, default is 143 and 993 for ssl - * @param string|bool $encryption use 'SSL', 'TLS' or false * * @throws ConnectionFailedException */ - public function connect($host, $port = null, $encryption = false) { + public function connect($host, $port = null) { $transport = 'tcp'; - if ($encryption) { - $encryption = strtolower($encryption); + if ($this->encryption) { + $encryption = strtolower($this->encryption); if ($encryption == "ssl") { $transport = 'ssl'; $port = $port === null ? 993 : $port; diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index d303b761..cd5ca832 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -33,9 +33,11 @@ class LegacyProtocol extends Protocol implements ProtocolInterface { /** * Imap constructor. * @param bool $cert_validation set to false to skip SSL certificate validation + * @param mixed $encryption Connection encryption method */ - public function __construct($cert_validation = true) { + public function __construct($cert_validation = true, $encryption = false) { $this->setCertValidation($cert_validation); + $this->encryption = $encryption; } /** diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 5b88ab7c..18abc90d 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -36,12 +36,29 @@ abstract class Protocol { */ public $stream = false; + /** + * Connection encryption method + * @var mixed $encryption + */ + protected $encryption = false; + /** * Set to false to ignore SSL certificate validation * @var bool */ protected $cert_validation = true; + /** + * Proxy settings + * @var array + */ + protected $proxy = [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ]; + /** * Get an available cryptographic method * @@ -101,20 +118,59 @@ public function getCertValidation() { return $this->cert_validation; } + /** + * Set connection proxy settings + * @var array $options + * + * @return $this + */ + public function setProxy($options) { + foreach ($this->proxy as $key => $val) { + if (isset($options[$key])) { + $this->proxy[$key] = $options[$key]; + } + } + + return $this; + } + + /** + * Get the current proxy settings + * + * @return array + */ + public function getProxy() { + return $this->proxy; + } + /** * Prepare socket options * * @return array */ private function defaultSocketOptions() { - return $this->getCertValidation() - ? [ - 'ssl' => [ - 'verify_peer_name' => false, - 'verify_peer' => false, - ] - ] - : []; + $options = []; + if ($this->encryption != false) { + $options["ssl"] = [ + 'verify_peer_name' => $this->getCertValidation(), + 'verify_peer' => $this->getCertValidation(), + ]; + } + + if ($this->proxy["socket"] != null) { + $options["proxy"] = $this->proxy["socket"]; + $options["request_fulluri"] = $this->proxy["request_fulluri"]; + + if ($this->proxy["username"] != null) { + $auth = base64_encode($this->proxy["username"].':'.$this->proxy["password"]); + + $options["header"] = [ + "Proxy-Authorization: Basic $auth" + ]; + } + } + + return $options; } /** diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 8acbb461..38269a5f 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -38,13 +38,12 @@ public function __destruct(); * Open a new connection / session * @param string $host hostname or IP address of IMAP server * @param int|null $port of service server - * @param string|bool $encryption use 'SSL', 'TLS' or false * * @throws \ErrorException * @throws ConnectionFailedException * @throws RuntimeException */ - public function connect($host, $port = null, $encryption = false); + public function connect($host, $port = null); /** * Login to a new session. diff --git a/src/config/imap.php b/src/config/imap.php index d270421f..57556c4a 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -55,6 +55,12 @@ 'username' => 'root@example.com', 'password' => '', 'authentication' => null, + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ] ], /* From 0b325a010656e739cf2e9fbe164c18d01c6475ec Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 11 Dec 2020 09:12:54 +0100 Subject: [PATCH 103/600] Allow ending with space or semicolon --- CHANGELOG.md | 2 ++ src/Structure.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b6c016..86303164 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Cert validation issue fixed +- Allow boundaries ending with a space or semicolon (thanks [@smartilabs](https://github.com/smartilabs)) ### Added - Proxy support added ### Affected Classes - [Client::class](src/Client.php) +- [Structure::class](src/Structure.php) ### Breaking changes - Depending on your configuration, your certificates actually get checked. Which can cause an aborted connection if the certificate can not be validated. diff --git a/src/Structure.php b/src/Structure.php index 5fc7e316..8f5b06ed 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -100,7 +100,7 @@ public function findContentType(){ * Determine the message content type */ public function getBoundary(){ - $boundary = $this->header->find("/boundary=\"?([^\"]*)\"?/"); + $boundary = $this->header->find("/boundary=\"?([^\"]*)[\";\s]/"); return str_replace('"', '', $boundary); } From effb8172b2bcf119e9b618c5cfb7e4eb97750d5d Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 14 Dec 2020 07:02:12 +0100 Subject: [PATCH 104/600] Flag example fixed --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c16be95a..13129bc7 100755 --- a/README.md +++ b/README.md @@ -448,8 +448,8 @@ $message = $folder->query()->getMessage($msgn = 1); Flag or "unflag" a message: ```php /** @var \Webklex\PHPIMAP\Message $message */ -$message->setFlag(['Seen', 'Spam']); -$message->unsetFlag('Spam'); +$message->setFlag(['Seen', 'Flagged']); +$message->unsetFlag('Flagged'); ``` Mark all messages as "read" while fetching: From bb2dd818ba00bb675127d686dd5939eab3d68144 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 14 Dec 2020 07:04:16 +0100 Subject: [PATCH 105/600] Typo fixed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13129bc7..f7480b0d 100755 --- a/README.md +++ b/README.md @@ -905,7 +905,7 @@ Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illum ### Known issues | Error | Solution | | ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside and use the `legacy-imap` protocol | +| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | ## Change log From db50e9e86022bd2bee4fa7f73fdbf73d475b55d6 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 17 Dec 2020 14:34:59 +0100 Subject: [PATCH 106/600] Ignore IMAP DONE command response #57 --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 757e7966..ec9771a4 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -945,7 +945,7 @@ public function done() { if (fwrite($this->stream, "DONE\r\n") === false) { throw new RuntimeException('failed to write - connection closed?'); } - return $this->readResponse("*", false); + return true; } /** From a9d60f5caf7fa33aa8e3f5479caeeb0e771d5e6b Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 17 Dec 2020 14:36:21 +0100 Subject: [PATCH 107/600] Changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86303164..f0dd3809 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Cert validation issue fixed - Allow boundaries ending with a space or semicolon (thanks [@smartilabs](https://github.com/smartilabs)) +- Ignore IMAP DONE command response #57 ### Added - Proxy support added From 264feae026e57eb557a5556f1c57560b8c7f865d Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 17 Dec 2020 15:30:47 +0100 Subject: [PATCH 108/600] Default fetch option set to IMAP::FT_PEEK --- src/config/imap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/imap.php b/src/config/imap.php index 57556c4a..ee342b39 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -125,7 +125,7 @@ */ 'options' => [ 'delimiter' => '/', - 'fetch' => \Webklex\PHPIMAP\IMAP::FT_UID, + 'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK, 'fetch_body' => true, 'fetch_flags' => true, 'message_key' => 'list', From 59988aa3959d65520ee37eaa3b0a749f683267bb Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 17 Dec 2020 15:31:05 +0100 Subject: [PATCH 109/600] Proxy example added --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index f7480b0d..539ecb78 100755 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) - [Basic usage example](#basic-usage-example) - [Folder / Mailbox](#folder--mailbox) - [oAuth](#oauth) + - [Proxy](#proxy) - [Idle](#idle) - [Search](#search-for-messages) - [Counting messages](#counting-messages) @@ -235,6 +236,32 @@ $client->connect(); ``` +#### Proxy +Basic proxy example with authorization: +```php +/** @var \Webklex\PHPIMAP\Clientmanager $cm */ +/** @var \Webklex\PHPIMAP\Client $client */ +$client = $cm->make([ + 'host' => 'imap.somehost.com', + 'port' => 993, + 'encryption' => 'ssl', + 'validate_cert' => true, + 'username' => 'example@somehost.com', + 'password' => 'Password', + 'protocol' => 'imap', + 'proxy' => [ + 'socket' => "tcp://proxy.com:3444", + 'request_fulluri' => false, + 'username' => "my_username", + 'password' => "secret_password", + ] +]); + +//Connect to the IMAP Server +$client->connect(); +``` + + #### Idle Every time a new message is received, the server will notify the client and return the new message. ```php From 46d81ae07225bb96d50468f45cc17c567fe6f895 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 17 Dec 2020 20:02:54 +0100 Subject: [PATCH 110/600] Flexible disposition support added #58 --- README.md | 1 + src/Part.php | 6 +++++- src/config/imap.php | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 539ecb78..2efdd876 100755 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `fetch_flags` — If set to `false` all messages will be fetched without any flags - `message_key` — Message key identifier option - `fetch_order` — Message fetch order + - `dispositions` — All disposition types potentially considered an attachment - `common_folders` — Default folder locations and paths assumed if none is provided - `open` — special configuration for imap_open() - `DISABLE_AUTHENTICATOR` — disable authentication properties. diff --git a/src/Part.php b/src/Part.php index d67c6850..488cdbfc 100644 --- a/src/Part.php +++ b/src/Part.php @@ -299,7 +299,11 @@ private function parseEncoding(){ * @return bool */ public function isAttachment(){ - if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || (empty($this->disposition) || !in_array(strtolower($this->disposition), ['attachment', 'inline'])) ) ) { + if ($this->type == IMAP::MESSAGE_TYPE_TEXT && + ($this->ifdisposition == 0 || + (empty($this->disposition) || !in_array(strtolower($this->disposition), ClientManager::get('options.dispositions'))) + ) + ) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "csv", "html"])) && $this->filename == null && $this->name == null) { return false; } diff --git a/src/config/imap.php b/src/config/imap.php index ee342b39..a3e7c55e 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -110,6 +110,8 @@ | -Fetch order | 'asc' - Order all messages ascending (probably results in oldest first) | 'desc' - Order all messages descending (probably results in newest first) + | -Disposition types potentially considered an attachment + | Default ['attachment', 'inline'] | -Common folders | Default folder locations and paths assumed if none is provided | -Open IMAP options: @@ -130,6 +132,7 @@ 'fetch_flags' => true, 'message_key' => 'list', 'fetch_order' => 'asc', + 'dispositions' => ['attachment', 'inline'], 'common_folders' => [ "root" => "INBOX", "junk" => "INBOX/Junk", From f70b4b70661bd60ffdf19783d5351f81a9c9055c Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 18 Dec 2020 19:23:12 +0100 Subject: [PATCH 111/600] Address parsing fixed #60 --- src/Header.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Header.php b/src/Header.php index dcda848e..f1763a48 100644 --- a/src/Header.php +++ b/src/Header.php @@ -454,6 +454,10 @@ private function findPriority() { private function decodeAddresses($values) { $addresses = []; foreach($values as $address) { + $address = trim(rtrim($address)); + if (strpos($address, ",") == strlen($address) - 1) { + $address = substr($address, 0, -1); + } if (preg_match( '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', $address, From 3737204f97b595b9eb83106c76ef418cfbcd2297 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 18 Dec 2020 19:27:07 +0100 Subject: [PATCH 112/600] Alternative rfc822 header parsing fixed #60 --- src/Header.php | 12 +++++++++--- src/Part.php | 16 ++++++++-------- src/Structure.php | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Header.php b/src/Header.php index f1763a48..78ebe868 100644 --- a/src/Header.php +++ b/src/Header.php @@ -172,7 +172,11 @@ public function rfc822_parse_headers($raw_headers){ $headers = []; $imap_headers = []; if (extension_loaded('imap')) { - $imap_headers = (array) \imap_rfc822_parse_headers($this->raw); + $raw_imap_headers = (array) \imap_rfc822_parse_headers($this->raw); + foreach($raw_imap_headers as $key => $values) { + $key = str_replace("-", "_", $key); + $imap_headers[$key] = $values; + } } $lines = explode("\r\n", $raw_headers); @@ -204,6 +208,8 @@ public function rfc822_parse_headers($raw_headers){ }else{ if (($pos = strpos($line, ":")) > 0) { $key = trim(rtrim(strtolower(substr($line, 0, $pos)))); + $key = str_replace("-", "_", $key); + $value = trim(rtrim(substr($line, $pos + 1))); $headers[$key] = [$value]; $prev_header = $key; @@ -420,7 +426,7 @@ private function decodeArray($values) { * Try to extract the priority from a given raw header string */ private function findPriority() { - if(($priority = $this->get("x-priority")) === null) return; + if(($priority = $this->get("x_priority")) === null) return; switch($priority){ case IMAP::MESSAGE_PRIORITY_HIGHEST; $priority = IMAP::MESSAGE_PRIORITY_HIGHEST; @@ -538,7 +544,7 @@ private function parseAddresses($list) { private function extractHeaderExtensions(){ foreach ($this->attributes as $key => $value) { // Only parse strings and don't parse any attributes like the user-agent - if (is_string($value) === true && in_array($key, ["user-agent"]) === false) { + if (is_string($value) === true && in_array($key, ["user_agent"]) === false) { if (($pos = strpos($value, ";")) !== false){ $original = substr($value, 0, $pos); $this->attributes[$key] = trim(rtrim($original)); diff --git a/src/Part.php b/src/Part.php index 488cdbfc..24ec2019 100644 --- a/src/Part.php +++ b/src/Part.php @@ -176,16 +176,16 @@ protected function parse(){ if(!empty($this->header->get("id"))) { $this->id = $this->header->get("id"); - } else if(!empty($this->header->get("x-attachment-id"))){ - $this->id = $this->header->get("x-attachment-id"); - } else if(!empty($this->header->get("content-id"))){ - $this->id = strtr($this->header->get("content-id"), [ + } else if(!empty($this->header->get("x_attachment_id"))){ + $this->id = $this->header->get("x_attachment_id"); + } else if(!empty($this->header->get("content_id"))){ + $this->id = strtr($this->header->get("content_id"), [ '<' => '', '>' => '' ]); } - $content_types = $this->header->get("content-type"); + $content_types = $this->header->get("content_type"); if(!empty($content_types)){ $this->subtype = $this->parseSubtype($content_types); $content_type = $content_types; @@ -245,7 +245,7 @@ private function parseSubtype($content_type){ * Try to parse the disposition if any is present */ private function parseDisposition(){ - $content_disposition = $this->header->get("content-disposition"); + $content_disposition = $this->header->get("content_disposition"); if($content_disposition !== null) { $this->ifdisposition = true; $this->disposition = (is_array($content_disposition)) ? implode(' ', $content_disposition) : $content_disposition; @@ -256,7 +256,7 @@ private function parseDisposition(){ * Try to parse the description if any is present */ private function parseDescription(){ - $content_description = $this->header->get("content-description"); + $content_description = $this->header->get("content_description"); if($content_description !== null) { $this->ifdescription = true; $this->description = $content_description; @@ -267,7 +267,7 @@ private function parseDescription(){ * Try to parse the encoding if any is present */ private function parseEncoding(){ - $encoding = $this->header->get("content-transfer-encoding"); + $encoding = $this->header->get("content_transfer_encoding"); if($encoding !== null) { switch (strtolower($encoding)) { case "quoted-printable": diff --git a/src/Structure.php b/src/Structure.php index 8f5b06ed..a2d7d7c9 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -87,7 +87,7 @@ protected function parse(){ */ public function findContentType(){ - $content_type = $this->header->get("content-type"); + $content_type = $this->header->get("content_type"); $content_type = (is_array($content_type)) ? implode(' ', $content_type) : $content_type; if(stripos($content_type, 'multipart') === 0) { $this->type = IMAP::MESSAGE_TYPE_MULTIPART; From 81c85c5db53f7d63cde816f3f74ed59ecb185332 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 18 Dec 2020 19:32:19 +0100 Subject: [PATCH 113/600] Changelog updated --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0dd3809..2efe0e65 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,24 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Cert validation issue fixed - Allow boundaries ending with a space or semicolon (thanks [@smartilabs](https://github.com/smartilabs)) - Ignore IMAP DONE command response #57 +- Default `options.fetch` set to `IMAP::FT_PEEK` +- Address parsing fixed #60 +- Alternative rfc822 header parsing fixed #60 ### Added - Proxy support added +- Flexible disposition support added #58 ### Affected Classes -- [Client::class](src/Client.php) - [Structure::class](src/Structure.php) +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) +- [Part::class](src/Part.php) ### Breaking changes - Depending on your configuration, your certificates actually get checked. Which can cause an aborted connection if the certificate can not be validated. +- Messages don't get flagged as read unless you are using your own custom config. +- All `Header::class` attribute keys are now in a snake_format and no longer minus-separated. ## [2.2.5] - 2020-12-11 ### Fixed From aee8488488caa22de4009dc8fc302e21c65152ba Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 18 Dec 2020 23:41:20 +0100 Subject: [PATCH 114/600] Protocol UID support added --- src/Connection/Protocols/ImapProtocol.php | 53 ++++++++++++------- src/Connection/Protocols/LegacyProtocol.php | 41 ++++++++------ .../Protocols/ProtocolInterface.php | 27 +++++++--- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ec9771a4..08c82fb2 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -568,6 +568,8 @@ protected function fetch($items, $from, $to = null, $uid = false) { $count = count($tokens[2]); if ($tokens[2][$count - 2] == 'UID') { $uidKey = $count - 1; + } else if ($tokens[2][0] == 'UID') { + $uidKey = 1; } else { $uidKey = array_search('UID', $tokens[2]) + 1; } @@ -611,7 +613,11 @@ protected function fetch($items, $from, $to = null, $uid = false) { return $data; } - $result[$tokens[0]] = $data; + if ($uid) { + $result[$tokens[2][$uidKey]] = $data; + }else{ + $result[$tokens[0]] = $data; + } } if ($to === null && !is_array($from)) { @@ -625,35 +631,38 @@ protected function fetch($items, $from, $to = null, $uid = false) { * Fetch message headers * @param array|int $uids * @param string $rfc + * @param bool $uid set to true if passing a unique id * * @return array * @throws RuntimeException */ - public function content($uids, $rfc = "RFC822") { - return $this->fetch(["$rfc.TEXT"], $uids); + public function content($uids, $rfc = "RFC822", $uid = false) { + return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); } /** * Fetch message headers * @param array|int $uids * @param string $rfc + * @param bool $uid set to true if passing a unique id * * @return array * @throws RuntimeException */ - public function headers($uids, $rfc = "RFC822"){ - return $this->fetch(["$rfc.HEADER"], $uids); + public function headers($uids, $rfc = "RFC822", $uid = false){ + return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); } /** * Fetch message flags * @param array|int $uids + * @param bool $uid set to true if passing a unique id * * @return array * @throws RuntimeException */ - public function flags($uids){ - return $this->fetch(["FLAGS"], $uids); + public function flags($uids, $uid = false){ + return $this->fetch(["FLAGS"], $uids, null, $uid); } /** @@ -693,7 +702,7 @@ public function getMessageNumber($id) { } } - throw new RuntimeException('unique id not found'); + throw new RuntimeException('message number not found'); } /** @@ -729,11 +738,12 @@ public function folders($reference = '', $folder = '*') { * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages + * @param bool $uid set to true if passing a unique id * * @return bool|array new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true) { + public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false) { $item = 'FLAGS'; if ($mode == '+' || $mode == '-') { $item = $mode . $item; @@ -796,11 +806,12 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available + * @param bool $uid set to true if passing a unique id * * @return bool success * @throws RuntimeException */ - public function copyMessage($folder, $from, $to = null) { + public function copyMessage($folder, $from, $to = null, $uid = false) { $set = (int)$from; if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); @@ -815,11 +826,12 @@ public function copyMessage($folder, $from, $to = null) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available + * @param bool $uid set to true if passing a unique id * * @return bool success * @throws RuntimeException */ - public function moveMessage($folder, $from, $to = null) { + public function moveMessage($folder, $from, $to = null, $uid = false) { $set = (int)$from; if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); @@ -928,9 +940,11 @@ public function getQuotaRoot($quota_root = 'INBOX') { /** * Send idle command + * @param bool $uid set to true if passing a unique id + * * @throws RuntimeException */ - public function idle() { + public function idle($uid = false) { $this->sendRequest('IDLE'); if (!$this->assumedNextLine('+ ')) { throw new RuntimeException('idle failed'); @@ -950,13 +964,15 @@ public function done() { /** * Search for matching messages - * * @param array $params + * @param bool $uid set to true if passing a unique id + * * @return array message ids * @throws RuntimeException */ - public function search(array $params) { - $response = $this->requestAndResponse('SEARCH', $params); + public function search(array $params, $uid = false) { + $token = $uid == true ? "UID SEARCH" : "SEARCH"; + $response = $this->requestAndResponse($token, $params); if (!$response) { return $response; } @@ -973,12 +989,13 @@ public function search(array $params) { /** * Get a message overview * @param string $sequence - * @return array + * @param bool $uid set to true if passing a unique id * + * @return array * @throws RuntimeException * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException */ - public function overview($sequence) { + public function overview($sequence, $uid = false) { $result = []; list($from, $to) = explode(":", $sequence); @@ -989,7 +1006,7 @@ public function overview($sequence) { $ids[] = $msgn; } } - $headers = $this->headers($ids); + $headers = $this->headers($ids, $uid); foreach ($headers as $msgn => $raw_header) { $result[$msgn] = (new Header($raw_header))->getAttributes(); } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index cd5ca832..20adcd51 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -197,14 +197,15 @@ public function examineFolder($folder = 'INBOX') { * Fetch message content * @param array|int $uids * @param string $rfc + * @param bool $uid set to true if passing a unique id * * @return array */ - public function content($uids, $rfc = "RFC822") { + public function content($uids, $rfc = "RFC822", $uid = false) { $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $uid) { - $result[$uid] = \imap_fetchbody($this->stream, $uid, "", IMAP::NIL); + $result[$uid] = \imap_fetchbody($this->stream, $uid, "", $uid ? IMAP::FT_UID : IMAP::NIL); } return $result; } @@ -213,14 +214,15 @@ public function content($uids, $rfc = "RFC822") { * Fetch message headers * @param array|int $uids * @param string $rfc + * @param bool $uid set to true if passing a unique id * * @return array */ - public function headers($uids, $rfc = "RFC822"){ + public function headers($uids, $rfc = "RFC822", $uid = false){ $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $uid) { - $result[$uid] = \imap_fetchheader($this->stream, $uid, IMAP::NIL); + $result[$uid] = \imap_fetchheader($this->stream, $uid, $uid ? IMAP::FT_UID : IMAP::NIL); } return $result; } @@ -228,14 +230,15 @@ public function headers($uids, $rfc = "RFC822"){ /** * Fetch message flags * @param array|int $uids + * @param bool $uid set to true if passing a unique id * * @return array */ - public function flags($uids){ + public function flags($uids, $uid = false){ $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $uid) { - $raw_flags = \imap_fetch_overview($this->stream, $uid, IMAP::NIL); + $raw_flags = \imap_fetch_overview($this->stream, $uid, $uid ? IMAP::FT_UID : IMAP::NIL); $flags = []; if (is_array($raw_flags) && isset($raw_flags[0])) { $raw_flags = (array) $raw_flags[0]; @@ -282,11 +285,12 @@ public function getMessageNumber($id) { /** * Get a message overview * @param string $sequence uid sequence + * @param bool $uid set to true if passing a unique id * * @return array */ - public function overview($sequence) { - return \imap_fetch_overview($this->stream, $sequence); + public function overview($sequence, $uid = false) { + return \imap_fetch_overview($this->stream, $sequence,$uid ? IMAP::FT_UID : IMAP::NIL); } /** @@ -321,16 +325,17 @@ public function folders($reference = '', $folder = '*') { * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages + * @param bool $uid set to true if passing a unique id * * @return bool|array new flags if $silent is false, else true or false depending on success */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true) { + public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false) { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); if ($mode == "+"){ - $status = \imap_setflag_full($this->stream, $from, $flag, IMAP::NIL); + $status = \imap_setflag_full($this->stream, $from, $flag, $uid ? IMAP::FT_UID : IMAP::NIL); }else{ - $status = \imap_clearflag_full($this->stream, $from, $flag, IMAP::NIL); + $status = \imap_clearflag_full($this->stream, $from, $flag, $uid ? IMAP::FT_UID : IMAP::NIL); } if ($silent === true) { @@ -366,11 +371,12 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available + * @param bool $uid set to true if passing a unique id * * @return bool success */ - public function copyMessage($folder, $from, $to = null) { - return \imap_mail_copy($this->stream, $from, $folder, IMAP::CP_UID); + public function copyMessage($folder, $from, $to = null, $uid = false) { + return \imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::FT_UID : IMAP::NIL); } /** @@ -379,11 +385,12 @@ public function copyMessage($folder, $from, $to = null) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available + * @param bool $uid set to true if passing a unique id * * @return bool success */ - public function moveMessage($folder, $from, $to = null) { - return \imap_mail_move($this->stream, $from, $folder, IMAP::CP_UID); + public function moveMessage($folder, $from, $to = null, $uid = false) { + return \imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::FT_UID : IMAP::NIL); } /** @@ -479,8 +486,8 @@ public function done() { * @param array $params * @return array message ids */ - public function search(array $params) { - return \imap_search($this->stream, $params[0], IMAP::NIL); + public function search(array $params, $uid = false) { + return \imap_search($this->stream, $params[0], $uid ? IMAP::FT_UID : IMAP::NIL); } /** diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 38269a5f..eae0ab27 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -92,30 +92,33 @@ public function examineFolder($folder = 'INBOX'); * Fetch message headers * @param array|int $uids * @param string $rfc + * @param bool $uid set to true if passing a unique id * * @return array * @throws RuntimeException */ - public function content($uids, $rfc = "RFC822"); + public function content($uids, $rfc = "RFC822", $uid = false); /** * Fetch message headers * @param array|int $uids * @param string $rfc + * @param bool $uid set to true if passing a unique id * * @return array * @throws RuntimeException */ - public function headers($uids, $rfc = "RFC822"); + public function headers($uids, $rfc = "RFC822", $uid = false); /** * Fetch message flags * @param array|int $uids + * @param bool $uid set to true if passing a unique id * * @return array * @throws RuntimeException */ - public function flags($uids); + public function flags($uids, $uid = false); /** * Get uid for a given id @@ -154,10 +157,12 @@ public function folders($reference = '', $mailbox = '*'); * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages + * @param bool $uid set to true if passing a unique id + * * @return bool|array new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true); + public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false); /** * Append a new message to given folder @@ -178,10 +183,12 @@ public function appendMessage($folder, $message, $flags = null, $date = null); * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available + * @param bool $uid set to true if passing a unique id + * * @return bool success * @throws RuntimeException */ - public function copyMessage($folder, $from, $to = null); + public function copyMessage($folder, $from, $to = null, $uid = false); /** * Move a message set from current folder to an other folder @@ -189,10 +196,11 @@ public function copyMessage($folder, $from, $to = null); * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available + * @param bool $uid set to true if passing a unique id * * @return bool success */ - public function moveMessage($folder, $from, $to = null); + public function moveMessage($folder, $from, $to = null, $uid = false); /** * Create a new folder @@ -270,18 +278,21 @@ public function noop(); * Do a search request * * @param array $params + * @param bool $uid set to true if passing a unique id + * * @return array message ids * @throws RuntimeException */ - public function search(array $params); + public function search(array $params, $uid = false); /** * Get a message overview * @param string $sequence uid sequence + * @param bool $uid set to true if passing a unique id * * @return array */ - public function overview($sequence); + public function overview($sequence, $uid = false); /** * Enable the debug mode From 492fec6babe31dd4559bbb45d93b148440a617d5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 18 Dec 2020 23:42:22 +0100 Subject: [PATCH 115/600] New `options.message_key` option `uid` added --- src/Query/Query.php | 3 +++ src/config/imap.php | 1 + 2 files changed, 4 insertions(+) diff --git a/src/Query/Query.php b/src/Query/Query.php index 820c5e94..99919305 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -234,6 +234,9 @@ public function get() { case 'list': $message_key = $msglist; break; + case 'uid': + $message_key = $message->getUid(); + break; default: $message_key = $message->getMessageId(); break; diff --git a/src/config/imap.php b/src/config/imap.php index a3e7c55e..0a612cf4 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -107,6 +107,7 @@ | 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail) | 'number' - Use the message number as array key (isn't always unique and can cause some interesting behavior) | 'list' - Use the message list number as array key (incrementing integer (does not always start at 0 or 1) + | 'uid' - Use the message uid as array key (isn't always unique and can cause some interesting behavior) | -Fetch order | 'asc' - Order all messages ascending (probably results in oldest first) | 'desc' - Order all messages descending (probably results in newest first) From b3bf4641353b4d979c3c6181fdb45c7502d478f1 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 18 Dec 2020 23:43:49 +0100 Subject: [PATCH 116/600] Flexible sequence type support added --- CHANGELOG.md | 4 ++ README.md | 1 + src/IMAP.php | 1 + src/Message.php | 152 +++++++++++++++++++++++++++++++++----------- src/Query/Query.php | 50 +++++++++------ src/config/imap.php | 4 ++ 6 files changed, 158 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2efe0e65..09ed01d7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Proxy support added - Flexible disposition support added #58 +- New `options.message_key` option `uid` added +- Protocol UID support added +- Flexible sequence type support added ### Affected Classes - [Structure::class](src/Structure.php) +- [Query::class](src/Query/Query.php) - [Client::class](src/Client.php) - [Header::class](src/Header.php) - [Part::class](src/Part.php) diff --git a/README.md b/README.md index 2efdd876..e34e46a6 100755 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ Detailed [config/imap.php](src/config/imap.php) configuration: - `options` — additional fetch options - `delimiter` — you can use any supported char such as ".", "/", etc - `fetch` — `IMAP::FT_UID` (message marked as read by fetching the message body) or `IMAP::FT_PEEK` (fetch the message without setting the "seen" flag) + - `sequence` — `IMAP::ST_UID` (fetch message components using the message uid) or `IMAP::ST_MSGN` (Fetch message components using the message number) - `fetch_body` — If set to `false` all messages will be fetched without the body and any potential attachments - `fetch_flags` — If set to `false` all messages will be fetched without any flags - `message_key` — Message key identifier option diff --git a/src/IMAP.php b/src/IMAP.php index 314b1922..41ae8248 100644 --- a/src/IMAP.php +++ b/src/IMAP.php @@ -223,6 +223,7 @@ class IMAP { */ const ST_UID = 1; const ST_SILENT = 2; + const ST_MSGN = 3; const ST_SET = 4; /** diff --git a/src/Message.php b/src/Message.php index a6bbbe7a..c5a3acb3 100755 --- a/src/Message.php +++ b/src/Message.php @@ -49,7 +49,6 @@ * @method integer getMsglist() * @method integer setMsglist(integer $msglist) * @method integer getUid() - * @method integer setUid(integer $uid) * @method integer getMsgn() * @method integer getPriority() * @method integer setPriority(integer $priority) @@ -125,6 +124,11 @@ class Message { */ public $fetch_options = null; + /** + * @var integer + */ + protected $sequence = IMAP::NIL; + /** * Fetch body options * @@ -178,12 +182,13 @@ class Message { /** * Message constructor. - * @param integer $msgn + * @param integer $uid * @param integer|null $msglist * @param Client $client * @param integer|null $fetch_options * @param boolean $fetch_body * @param boolean $fetch_flags + * @param integer $sequence * * @throws Exceptions\ConnectionFailedException * @throws InvalidMessageDateException @@ -192,7 +197,7 @@ class Message { * @throws MessageContentFetchingException * @throws Exceptions\EventNotFoundException */ - public function __construct($msgn, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false) { + public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false, $sequence = null) { $default_mask = $client->getDefaultMessageMask(); if($default_mask != null) { @@ -205,6 +210,7 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu $this->config = ClientManager::get('options'); + $this->setSequence($sequence); $this->setFetchOption($fetch_options); $this->setFetchBodyOption($fetch_body); $this->setFetchFlagsOption($fetch_flags); @@ -215,10 +221,18 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu $this->client = $client; $this->client->openFolder($this->folder_path); - $this->msgn = $msgn; + if ($this->sequence === IMAP::ST_UID) { + $this->uid = $uid; + $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid); + }else{ + $this->msgn = $uid; + $this->uid = $this->client->getConnection()->getUid($this->msgn); + } $this->msglist = $msglist; - $this->uid = $this->client->getConnection()->getUid($this->msgn); + if ($this->fetch_options == IMAP::FT_PEEK) { + $this->parseFlags(); + } $this->parseHeader(); @@ -226,20 +240,21 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu $this->parseBody(); } - if ($this->getFetchFlagsOption() === true) { + if ($this->getFetchFlagsOption() === true && $this->fetch_options !== IMAP::FT_PEEK) { $this->parseFlags(); } } /** * Create a new instance without fetching the message header and providing them raw instead - * @param int $msgn + * @param int $uid * @param int|null $msglist * @param Client $client * @param string $raw_header * @param string $raw_body * @param string $raw_flags * @param null $fetch_options + * @param null $sequence * * @return Message * @throws Exceptions\ConnectionFailedException @@ -249,7 +264,7 @@ public function __construct($msgn, $msglist, Client $client, $fetch_options = nu * @throws MessageContentFetchingException * @throws \ReflectionException */ - public static function make($msgn, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null){ + public static function make($uid, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null, $sequence = null){ $reflection = new \ReflectionClass(self::class); /** @var self $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -264,21 +279,22 @@ public static function make($msgn, $msglist, Client $client, $raw_header, $raw_b ]); $instance->setFolderPath($client->getFolderPath()); $instance->setConfig(ClientManager::get('options')); + $instance->setSequence($sequence); $instance->setFetchOption($fetch_options); $instance->setAttachments(AttachmentCollection::make([])); $instance->setClient($client); - $instance->setMsgn($msgn, $msglist); - - $instance->parseRawHeader($raw_header); - $instance->parseRawFlags($raw_flags); - - if ($fetch_options == IMAP::FT_PEEK && $instance->getFlags()->count() == 0) { - $instance->parseFlags(); + if ($instance->getSequence() === IMAP::ST_UID) { + $instance->setUid($uid); + $instance->setMsglist($msglist); + }else{ + $instance->setMsgn($uid, $msglist); } + $instance->parseRawHeader($raw_header); + $instance->parseRawFlags($raw_flags); $instance->parseRawBody($raw_body); if ($fetch_options == IMAP::FT_PEEK) { @@ -408,12 +424,13 @@ public function getHTMLBody() { * @throws MessageHeaderFetchingException */ private function parseHeader() { - $headers = $this->client->getConnection()->headers([$this->msgn]); - if (!isset($headers[$this->msgn])) { + $sequence_id = $this->getSequenceId(); + $headers = $this->client->getConnection()->headers([$sequence_id], $this->sequence === IMAP::ST_UID); + if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } - $this->parseRawHeader($headers[$this->msgn]); + $this->parseRawHeader($headers[$sequence_id]); } /** @@ -454,10 +471,11 @@ private function parseFlags() { $this->client->openFolder($this->folder_path); $this->flags = FlagCollection::make([]); - $flags = $this->client->getConnection()->flags([$this->msgn]); + $sequence_id = $this->getSequenceId(); + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID); - if (isset($flags[$this->msgn])) { - $this->parseRawFlags($flags[$this->msgn]); + if (isset($flags[$sequence_id])) { + $this->parseRawFlags($flags[$sequence_id]); } } @@ -474,15 +492,12 @@ private function parseFlags() { public function parseBody() { $this->client->openFolder($this->folder_path); - if ($this->fetch_options == IMAP::FT_PEEK && $this->flags->count() == 0) { - $this->parseFlags(); - } - - $contents = $this->client->getConnection()->content([$this->msgn]); - if (!isset($contents[$this->msgn])) { + $sequence_id = $this->getSequenceId(); + $contents = $this->client->getConnection()->content([$sequence_id], $this->sequence === IMAP::ST_UID); + if (!isset($contents[$sequence_id])) { throw new MessageContentFetchingException("no content found", 0); } - $content = $contents[$this->msgn]; + $content = $contents[$sequence_id]; $body = $this->parseRawBody($content); @@ -490,6 +505,8 @@ public function parseBody() { if ($this->getFlags()->get("seen") == null) { $this->unsetFlag("Seen"); } + }elseif ($this->getFlags()->get("seen") != null) { + $this->setFlag("Seen"); } return $body; @@ -597,6 +614,23 @@ public function setFetchOption($option) { return $this; } + /** + * Set the sequence type + * @param int $sequence + * + * @return $this + */ + public function setSequence($sequence) { + if (is_long($sequence)) { + $this->sequence = $sequence; + } elseif (is_null($sequence)) { + $config = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $this->sequence = is_long($config) ? $config : IMAP::ST_MSGN; + } + + return $this; + } + /** * Fail proof setter for $fetch_body * @param $option @@ -814,13 +848,18 @@ public function copy($folder_path, $expunge = false) { $folder = $this->client->getFolder($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->copyMessage($folder->path, $this->msgn) == true) { + if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { if($expunge) $this->client->expunge(); $this->client->openFolder($folder->path); - $message_num = $this->client->getConnection()->getMessageNumber($next_uid); - $message = $folder->query()->getMessage($message_num); + if ($this->sequence === IMAP::ST_UID) { + $sequence_id = $next_uid; + }else{ + $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid); + } + + $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); $event = $this->getEvent("message", "copied"); $event::dispatch($this, $message); @@ -856,13 +895,18 @@ public function move($folder_path, $expunge = false) { $folder = $this->client->getFolder($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->moveMessage($folder->path, $this->msgn) == true) { + if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { if($expunge) $this->client->expunge(); $this->client->openFolder($folder->path); - $message_num = $this->client->getConnection()->getMessageNumber($next_uid); - $message = $folder->query()->getMessage($message_num); + if ($this->sequence === IMAP::ST_UID) { + $sequence_id = $next_uid; + }else{ + $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid); + } + + $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); $event = $this->getEvent("message", "moved"); $event::dispatch($this, $message); @@ -923,7 +967,8 @@ public function restore($expunge = true) { public function setFlag($flag) { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); - $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "+"); + $sequence_id = $this->getSequenceId(); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID); $this->parseFlags(); $event = $this->getEvent("flag", "new"); @@ -945,7 +990,8 @@ public function unsetFlag($flag) { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); - $status = $this->client->getConnection()->store([$flag], $this->msgn, $this->msgn, "-"); + $sequence_id = $this->getSequenceId(); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID); $this->parseFlags(); $event = $this->getEvent("flag", "deleted"); @@ -1187,6 +1233,22 @@ public function setClient($client){ return $this; } + /** + * Set the message number + * @param int $uid + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException + * @return $this + */ + public function setUid($uid){ + $this->uid = $uid; + $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid); + $this->msglist = null; + + return $this; + } + /** * Set the message number * @param $msgn @@ -1203,4 +1265,22 @@ public function setMsgn($msgn, $msglist = null){ return $this; } + + /** + * Get the current sequence type + * + * @return int + */ + public function getSequence(){ + return $this->sequence; + } + + /** + * Set the sequence type + * + * @return int + */ + public function getSequenceId(){ + return $this->sequence === IMAP::ST_UID ? $this->uid : $this->msgn; + } } diff --git a/src/Query/Query.php b/src/Query/Query.php index 99919305..a0f3539d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -62,6 +62,9 @@ class Query { /** @var int $fetch_flags */ protected $fetch_flags = true; + /** @var int $sequence */ + protected $sequence = IMAP::NIL; + /** @var string $fetch_order */ protected $fetch_order; @@ -76,6 +79,7 @@ class Query { public function __construct(Client $client, $charset = 'UTF-8') { $this->setClient($client); + $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); if(ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); if (ClientManager::get('options.fetch_order') === 'desc') { @@ -153,6 +157,18 @@ public function markAsRead() { return $this; } + /** + * Set the sequence type + * @param int $sequence + * + * @return $this + */ + public function setSequence($sequence) { + $this->sequence = $sequence != IMAP::ST_MSGN ? IMAP::ST_UID : $sequence; + + return $this; + } + /** * Perform an imap search request * @@ -163,7 +179,7 @@ protected function search(){ $this->generate_query(); try { - $available_messages = $this->client->getConnection()->search([$this->getRawQuery()]); + $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence == IMAP::ST_UID); } catch (RuntimeException $e) { $available_messages = false; } catch (ConnectionFailedException $e) { @@ -208,25 +224,22 @@ public function get() { $message_key = ClientManager::get('options.message_key'); - $msgnos = $available_messages->forPage($this->page, $this->limit)->toArray(); + $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); - $raw_headers = $this->client->getConnection()->headers($msgnos); - $raw_contents = []; - $raw_flags = []; + $raw_flags = $this->client->getConnection()->flags($uids, $this->sequence == IMAP::ST_UID); + $raw_headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence == IMAP::ST_UID); - if ($this->getFetchFlags()) { - $raw_flags = $this->client->getConnection()->flags($msgnos); - } + $raw_contents = []; if ($this->getFetchBody()) { - $raw_contents = $this->client->getConnection()->content($msgnos); + $raw_contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence == IMAP::ST_UID); } $msglist = 0; - foreach ($raw_headers as $msgno => $raw_header) { - $raw_content = isset($raw_contents[$msgno]) ? $raw_contents[$msgno] : ""; - $raw_flag = isset($raw_flags[$msgno]) ? $raw_flags[$msgno] : []; + foreach ($raw_headers as $uid => $raw_header) { + $raw_content = isset($raw_contents[$uid]) ? $raw_contents[$uid] : ""; + $raw_flag = isset($raw_flags[$uid]) ? $raw_flags[$uid] : []; - $message = Message::make($msgno, $msglist, $this->getClient(), $raw_header, $raw_content, $raw_flag, $this->getFetchOptions()); + $message = Message::make($uid, $msglist, $this->getClient(), $raw_header, $raw_content, $raw_flag, $this->getFetchOptions(), $this->sequence); switch ($message_key){ case 'number': $message_key = $message->getMessageNo(); @@ -255,8 +268,9 @@ public function get() { /** * Get a new Message instance - * @param $msgno + * @param int $uid * @param null $msglist + * @param null $sequence * * @return Message * @throws ConnectionFailedException @@ -266,8 +280,8 @@ public function get() { * @throws MessageHeaderFetchingException * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException */ - public function getMessage($msgno, $msglist = null){ - return new Message($msgno, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags()); + public function getMessage($uid, $msglist = null, $sequence = null){ + return new Message($uid, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags(), $sequence ? $sequence : $this->sequence); } /** @@ -536,7 +550,7 @@ public function setFetchOrderAsc() { /** * @return Query */ - public function fetchOrderAsc($fetch_order) { + public function fetchOrderAsc() { return $this->setFetchOrderAsc(); } @@ -550,7 +564,7 @@ public function setFetchOrderDesc() { /** * @return Query */ - public function fetchOrderDesc($fetch_order) { + public function fetchOrderDesc() { return $this->setFetchOrderDesc(); } } diff --git a/src/config/imap.php b/src/config/imap.php index 0a612cf4..f6d2f167 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -98,6 +98,9 @@ | -Fetch option: | IMAP::FT_UID - Message marked as read by fetching the body message | IMAP::FT_PEEK - Fetch the message without setting the "seen" flag + | -Fetch sequence id: + | IMAP::ST_UID - Fetch message components using the message uid + | IMAP::ST_MSGN - Fetch message components using the message number | -Body download option | Default TRUE | -Flag download option @@ -129,6 +132,7 @@ 'options' => [ 'delimiter' => '/', 'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK, + 'sequence' => \Webklex\PHPIMAP\IMAP::ST_MSGN, 'fetch_body' => true, 'fetch_flags' => true, 'message_key' => 'list', From 17f0155afe741ecd45503fa744b1e35732cc0329 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Dec 2020 20:48:14 +0100 Subject: [PATCH 117/600] Parse more than one Received header #61 --- CHANGELOG.md | 1 + src/Header.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ed01d7..94152b97 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Default `options.fetch` set to `IMAP::FT_PEEK` - Address parsing fixed #60 - Alternative rfc822 header parsing fixed #60 +- Parse more than one Received: header #61 ### Added - Proxy support added diff --git a/src/Header.php b/src/Header.php index 78ebe868..d4e4624f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -211,7 +211,11 @@ public function rfc822_parse_headers($raw_headers){ $key = str_replace("-", "_", $key); $value = trim(rtrim(substr($line, $pos + 1))); - $headers[$key] = [$value]; + if (isset($headers[$key])) { + $headers[$key][] = $value; + }else{ + $headers[$key] = [$value]; + } $prev_header = $key; } } From 4796cb19846fff7cbe51e2b72681abd6ff204931 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Dec 2020 17:53:19 +0100 Subject: [PATCH 118/600] Fetch folder overview fixed --- src/Connection/Protocols/ImapProtocol.php | 4 ++-- src/Folder.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 08c82fb2..518f7740 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1003,10 +1003,10 @@ public function overview($sequence, $uid = false) { $ids = []; foreach ($uids as $msgn => $v) { if ( ($to >= $msgn && $from <= $msgn) || ($to === "*" && $from <= $msgn) ){ - $ids[] = $msgn; + $ids[] = $uid ? $v : $msgn; } } - $headers = $this->headers($ids, $uid); + $headers = $this->headers($ids, $rfc = "RFC822", $uid); foreach ($headers as $msgn => $raw_header) { $result[$msgn] = (new Header($raw_header))->getAttributes(); } diff --git a/src/Folder.php b/src/Folder.php index 8609e790..0eb12610 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -251,7 +251,8 @@ public function move($new_name, $expunge = true) { public function overview($sequence = null){ $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - return $this->client->getConnection()->overview($sequence); + $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN) == IMAP::ST_UID; + return $this->client->getConnection()->overview($sequence, $uid); } /** From 8863c778a87812cc9c454892a05c16acdc51f6fc Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Dec 2020 17:53:42 +0100 Subject: [PATCH 119/600] Config option position changed --- src/config/imap.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/config/imap.php b/src/config/imap.php index f6d2f167..e7fa9090 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -14,30 +14,30 @@ /* |-------------------------------------------------------------------------- - | IMAP default account + | Default date format |-------------------------------------------------------------------------- | - | The default account identifier. It will be used as default for any missing account parameters. - | If however the default account is missing a parameter the package default will be used. - | Set to 'false' [boolean] to disable this functionality. + | The default date format is used to convert any given Carbon::class object into a valid date string. + | These are currently known working formats: "d-M-Y", "d-M-y", "d M y" | */ - 'default' => 'default', + 'date_format' => 'd-M-Y', /* |-------------------------------------------------------------------------- - | Default date format + | Default account |-------------------------------------------------------------------------- | - | The default date format is used to convert any given Carbon::class object into a valid date string. - | These are currently known working formats: "d-M-Y", "d-M-y", "d M y" + | The default account identifier. It will be used as default for any missing account parameters. + | If however the default account is missing a parameter the package default will be used. + | Set to 'false' [boolean] to disable this functionality. | */ - 'date_format' => 'd-M-Y', + 'default' => 'default', /* |-------------------------------------------------------------------------- - | Available IMAP accounts + | Available accounts |-------------------------------------------------------------------------- | | Please list all IMAP accounts which you are planning to use within the @@ -106,7 +106,7 @@ | -Flag download option | Default TRUE | -Message key identifier option - | You can choose between 'id', 'number' or 'list' + | You can choose between the following: | 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail) | 'number' - Use the message number as array key (isn't always unique and can cause some interesting behavior) | 'list' - Use the message list number as array key (incrementing integer (does not always start at 0 or 1) @@ -145,12 +145,12 @@ "sent" => "INBOX/Sent", "trash" => "INBOX/Trash", ], - 'open' => [ - // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' - ], 'decoder' => [ 'message' => 'utf-8', // mimeheader 'attachment' => 'utf-8' // mimeheader + ], + 'open' => [ + // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' ] ], From 1242dbcae2407a7bd36d21b1e02e8cedd54d4be9 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 20 Dec 2020 19:24:15 +0100 Subject: [PATCH 120/600] `Message::getTextBody()` fallback value fixed --- CHANGELOG.md | 4 ++++ src/Message.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94152b97..9f745ba9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Address parsing fixed #60 - Alternative rfc822 header parsing fixed #60 - Parse more than one Received: header #61 +- Fetch folder overview fixed +- `Message::getTextBody()` fallback value fixed ### Added - Proxy support added @@ -26,12 +28,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Query::class](src/Query/Query.php) - [Client::class](src/Client.php) - [Header::class](src/Header.php) +- [Folder::class](src/Folder.php) - [Part::class](src/Part.php) ### Breaking changes - Depending on your configuration, your certificates actually get checked. Which can cause an aborted connection if the certificate can not be validated. - Messages don't get flagged as read unless you are using your own custom config. - All `Header::class` attribute keys are now in a snake_format and no longer minus-separated. +- `Message::getTextBody()` no longer returns false if no text body is present. `null` is returned instead. ## [2.2.5] - 2020-12-11 ### Fixed diff --git a/src/Message.php b/src/Message.php index c5a3acb3..c11a238c 100755 --- a/src/Message.php +++ b/src/Message.php @@ -387,7 +387,7 @@ public function hasTextBody() { */ public function getTextBody() { if (!isset($this->bodies['text'])) { - return false; + return null; } return $this->bodies['text']; From 070dfa1630b6c4016dcc8d80741a17e3ffe5ba81 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 21 Dec 2020 01:49:14 +0100 Subject: [PATCH 121/600] Documentation moved to webklex/php-imap-documentation --- README.md | 880 +----------------------------------------------------- 1 file changed, 7 insertions(+), 873 deletions(-) diff --git a/README.md b/README.md index e34e46a6..9a16d0fa 100755 --- a/README.md +++ b/README.md @@ -15,153 +15,24 @@ process as well. You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if you want to use legacy protocols such as pop3. -Wiki: [webklex/php-imap/wiki](https://github.com/Webklex/php-imap/wiki) +Official documentation: [php-imap.com](https://www.php-imap.com/) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) -## Table of Contents -- [Installation](#installation) -- [Configuration](#configuration) -- [Usage](#usage) - - [Basic usage example](#basic-usage-example) - - [Folder / Mailbox](#folder--mailbox) - - [oAuth](#oauth) - - [Proxy](#proxy) - - [Idle](#idle) - - [Search](#search-for-messages) - - [Counting messages](#counting-messages) - - [Result limiting](#result-limiting) - - [Pagination](#pagination) - - [View examples](#view-examples) - - [Fetch a specific message](#fetch-a-specific-message) - - [Message flags](#message-flags) - - [Attachments](#attachments) - - [Advanced fetching](#advanced-fetching) - - [Events](#events) - - [Masking](#masking) - - [Specials](#specials) -- [Support](#support) -- [Documentation](#documentation) - - [Client::class](#clientclass) - - [Message::class](#messageclass) - - [Folder::class](#folderclass) - - [Query::class](#queryclass) - - [Attachment::class](#attachmentclass) - - [Mask::class](#maskclass) - - [MessageMask::class](#messagemaskclass) - - [AttachmentMask::class](#attachmentmaskclass) - - [MessageCollection::class](#messagecollectionclass) - - [AttachmentCollection::class](#attachmentcollectionclass) - - [FolderCollection::class](#foldercollectionclass) - - [FlagCollection::class](#flagcollectionclass) -- [Known issues](#known-issues) -- [Milestones & upcoming features](#milestones--upcoming-features) -- [Security](#security) -- [Credits](#credits) -- [License](#license) - - -## Installation -1.) Install decoding modules: -```shell script -sudo apt-get install php*-mbstring php*-mcrypt && sudo apache2ctl graceful -``` - -1.1.) (optional) Install php-imap module if you are having encoding problems: -```shell script -sudo apt-get install php*-imap && sudo apache2ctl graceful -``` - -You might also want to check `phpinfo()` if the extensions are enabled. - -2.) Now install the PHP-IMAP package by running the following command: -```shell script -composer require webklex/php-imap -``` - -3.) Create your own custom config file like [config/imap.php](src/config/imap.php): - - -## Configuration -Supported protocols: -- `imap` — Use IMAP [default] -- `legacy-imap` — Use the php imap module instead -- `pop3` — Use POP3 -- `nntp` — Use NNTP - -The following encryption methods are supported: -- `false` — Disable encryption -- `ssl` — Use SSL -- `tls` — Use TLS -- `starttls` — Use STARTTLS (alias TLS) (legacy only) -- `notls` — Use NoTLS (legacy only) - -Detailed [config/imap.php](src/config/imap.php) configuration: - - `default` — used default account. It will be used as default for any missing account parameters. If however the default account is missing a parameter the package default will be used. Set to `false` to disable this functionality. - - `accounts` — all available accounts - - `default` — The account identifier (in this case `default` but could also be `fooBar` etc). - - `host` — imap host - - `port` — imap port - - `encryption` — desired encryption method - - `validate_cert` — decide weather you want to verify the certificate or not - - `username` — imap account username - - `password` — imap account password - - `authentication` — imap authentication method. Use `oauth` to use oAuth for Google, etc. - - `proxy` — Optional proxy settings - - `socket` — Proxy host including the port e.g. example.com:123 - - `request_fulluri` — When set to TRUE, the entire URI will be used when constructing the request. While this is a non-standard request format, some proxy servers require it. - - `username` — Optional username if required - - `password` — Optional password if required - - `date_format` — The default date format is used to convert any given Carbon::class object into a valid date string. (`d-M-Y`, `d-M-y`, `d M y`) - - `options` — additional fetch options - - `delimiter` — you can use any supported char such as ".", "/", etc - - `fetch` — `IMAP::FT_UID` (message marked as read by fetching the message body) or `IMAP::FT_PEEK` (fetch the message without setting the "seen" flag) - - `sequence` — `IMAP::ST_UID` (fetch message components using the message uid) or `IMAP::ST_MSGN` (Fetch message components using the message number) - - `fetch_body` — If set to `false` all messages will be fetched without the body and any potential attachments - - `fetch_flags` — If set to `false` all messages will be fetched without any flags - - `message_key` — Message key identifier option - - `fetch_order` — Message fetch order - - `dispositions` — All disposition types potentially considered an attachment - - `common_folders` — Default folder locations and paths assumed if none is provided - - `open` — special configuration for imap_open() - - `DISABLE_AUTHENTICATOR` — disable authentication properties. - - `decoder` — Currently only the message and attachment decoder can be set - - `events` — Default [event handling](#events) config - - `masks` — Default [masking](#masking) config - - `message` — Default message mask - - `attachment` — Default attachment mask - - -## Usage -#### Basic usage example +## Basic usage example This is a basic example, which will echo out all Mails within all imap folders and will move every message into INBOX.read. Please be aware that this should not be tested in real life and is only meant to gives an impression on how things work. ```php use Webklex\PHPIMAP\ClientManager; -use Webklex\PHPIMAP\Client; $cm = new ClientManager('path/to/config/imap.php'); -// or use an array of options instead -$cm = new ClientManager($options = []); - /** @var \Webklex\PHPIMAP\Client $client */ $client = $cm->account('account_identifier'); -// or create a new instance manually -$client = $cm->make([ - 'host' => 'somehost.com', - 'port' => 993, - 'encryption' => 'ssl', - 'validate_cert' => true, - 'username' => 'username', - 'password' => 'password', - 'protocol' => 'imap' -]); - //Connect to the IMAP Server $client->connect(); @@ -194,748 +65,11 @@ foreach($folders as $folder){ ``` -#### Folder / Mailbox -List all available folders: -```php -/** @var \Webklex\PHPIMAP\Client $client */ - -/** @var \Webklex\PHPIMAP\Support\FolderCollection $folders */ -$folders = $client->getFolders(); -``` - -Get a specific folder: -```php -/** @var \Webklex\PHPIMAP\Client $client */ - -/** @var \Webklex\PHPIMAP\Folder $folder */ -$folder = $client->getFolder('INBOX.name'); -``` - - -#### oAuth -Please take a look at [the wiki article](https://github.com/Webklex/php-imap/wiki/Google-Mail---Gmail) for gmail / google mail setup. - -Basic oAuth example: -```php -use Webklex\PHPIMAP\Clientmanager; - -$cm = new Clientmanager(); - -/** @var \Webklex\PHPIMAP\Client $client */ -$client = $cm->make([ - 'host' => 'imap.gmail.com', - 'port' => 993, - 'encryption' => 'ssl', - 'validate_cert' => true, - 'username' => 'example@gmail.com', - 'password' => 'ACCESS-TOKEN', - 'authentication' => "oauth", - 'protocol' => 'imap' -]); - -//Connect to the IMAP Server -$client->connect(); -``` - - -#### Proxy -Basic proxy example with authorization: -```php -/** @var \Webklex\PHPIMAP\Clientmanager $cm */ -/** @var \Webklex\PHPIMAP\Client $client */ -$client = $cm->make([ - 'host' => 'imap.somehost.com', - 'port' => 993, - 'encryption' => 'ssl', - 'validate_cert' => true, - 'username' => 'example@somehost.com', - 'password' => 'Password', - 'protocol' => 'imap', - 'proxy' => [ - 'socket' => "tcp://proxy.com:3444", - 'request_fulluri' => false, - 'username' => "my_username", - 'password' => "secret_password", - ] -]); - -//Connect to the IMAP Server -$client->connect(); -``` - - -#### Idle -Every time a new message is received, the server will notify the client and return the new message. -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ -$folder->idle(function($message){ - echo $message->subject."\n"; -}); -``` -**Attention!** Every received message will be flagged as "Seen". Update your config `options.fetch` from -`\Webklex\PHPIMAP\IMAP::FT_UID` to `\Webklex\PHPIMAP\IMAP::FT_PEEK` to prevent this behavior. - - -#### Search for messages -Search for specific emails: -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -//Get all messages -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->all()->get(); - -//Get all messages from example@domain.com -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->from('example@domain.com')->get(); - -//Get all messages since march 15 2018 -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->since('15.03.2018')->get(); - -//Get all messages within the last 5 days -$messages = $folder->query()->since(\Carbon\Carbon::now()->subDays(5))->get(); - -//Get all messages containing "hello world" -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->text('hello world')->get(); - -//Get all unseen messages containing "hello world" -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->unseen()->text('hello world')->get(); - -//Extended custom search query for all messages containing "hello world" -//and have been received since march 15 2018 -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->text('hello world')->since('15.03.2018')->get(); -$messages = $folder->query()->Text('hello world')->Since('15.03.2018')->get(); -$messages = $folder->query()->whereText('hello world')->whereSince('15.03.2018')->get(); - -// Build a custom search query -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query() -->where([['TEXT', 'Hello world'], ['SINCE', \Carbon\Carbon::parse('15.03.2018')]]) -->get(); - -//!EXPERIMENTAL! -//Get all messages NOT containing "hello world" -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->notText('hello world')->get(); -$messages = $folder->query()->not_text('hello world')->get(); -$messages = $folder->query()->not()->text('hello world')->get(); - -//Get all messages by custom search criteria -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->where(["CUSTOM_FOOBAR" => "fooBar"]])->get(); -``` - -Available search aliases for a better code reading: -```php -// Folder::search() is just an alias for Folder::query() -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->search()->text('hello world')->since('15.03.2018')->get(); - -// Folder::messages() is just an alias for Folder::query() -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->messages()->text('hello world')->since('15.03.2018')->get(); -``` -All available query / search methods can be found here: [Query::class](src/Query/WhereQuery.php) - -Available search criteria: -- `ALL` — return all messages matching the rest of the criteria -- `ANSWERED` — match messages with the \\ANSWERED flag set -- `BCC` "string" — match messages with "string" in the Bcc: field -- `BEFORE` "date" — match messages with Date: before "date" -- `BODY` "string" — match messages with "string" in the body of the message -- `CC` "string" — match messages with "string" in the Cc: field -- `DELETED` — match deleted messages -- `FLAGGED` — match messages with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set -- `FROM` "string" — match messages with "string" in the From: field -- `KEYWORD` "string" — match messages with "string" as a keyword -- `NEW` — match new messages -- `NOT` — not matching -- `OLD` — match old messages -- `ON` "date" — match messages with Date: matching "date" -- `RECENT` — match messages with the \\RECENT flag set -- `SEEN` — match messages that have been read (the \\SEEN flag is set) -- `SINCE` "date" — match messages with Date: after "date" -- `SUBJECT` "string" — match messages with "string" in the Subject: -- `TEXT` "string" — match messages with text "string" -- `TO` "string" — match messages with "string" in the To: -- `UNANSWERED` — match messages that have not been answered -- `UNDELETED` — match messages that are not deleted -- `UNFLAGGED` — match messages that are not flagged -- `UNKEYWORD` "string" — match messages that do not have the keyword "string" -- `UNSEEN` — match messages which have not been read yet - -Further information: -- http://php.net/manual/en/function.imap-search.php -- https://tools.ietf.org/html/rfc1176 -- https://tools.ietf.org/html/rfc1064 -- https://tools.ietf.org/html/rfc822 -- https://gist.github.com/martinrusev/6121028 - - -#### Result limiting -Limiting the request emails: -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -//Get all messages for page 2 since march 15 2018 where each page contains 10 messages -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->since('15.03.2018')->limit(10, 2)->get(); -``` - - -#### Counting messages -Count all available messages matching the current search criteria: -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -//Count all messages -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$count = $folder->query()->all()->count(); - -//Count all messages since march 15 2018 -$count = $folder->query()->since('15.03.2018')->count(); -``` - - -#### Pagination -Paginate a query: -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -/** @var \Illuminate\Pagination\LengthAwarePaginator $paginator */ -$paginator = $folder->query()->since('15.03.2018')->paginate(); -``` -Paginate a message collection: -```php -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ - -/** @var \Illuminate\Pagination\LengthAwarePaginator $paginator */ -$paginator = $messages->paginate(); -``` -View example for a paginated list: -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -/** @var \Illuminate\Pagination\LengthAwarePaginator $paginator */ -$paginator = $folder->search() -->since(\Carbon\Carbon::now()->subDays(14))->get() -->paginate($perPage = 5, $page = null, $pageName = 'imap_blade_example'); -``` - -```html - - - - - - - - - - - count() > 0): ?> - - - - - - - - - - - - - - -
UIDSubjectFromAttachments
getUid(); ?>getSubject(); ?>getFrom()[0]->mail; ?>getAttachments()->count() > 0 ? 'yes' : 'no'; ?>
No messages found
- -links(); ?> -``` -> You can also paginate a Folder-, Attachment- or FlagCollection instance. - - -#### View examples -You can find a few blade examples under [/examples](examples). - - -#### Fetch a specific message -Get a specific message by uid (Please note that the uid is not unique and can change): -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -/** @var \Webklex\PHPIMAP\Message $message */ -$message = $folder->query()->getMessage($msgn = 1); -``` - - -#### Message flags -Flag or "unflag" a message: -```php -/** @var \Webklex\PHPIMAP\Message $message */ -$message->setFlag(['Seen', 'Flagged']); -$message->unsetFlag('Flagged'); -``` - -Mark all messages as "read" while fetching: -```php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->text('Hello world')->markAsRead()->get(); -``` - -Don't mark all messages as "read" while fetching: -```php -/** @var \Webklex\PHPIMAP\Folder $oFolder */ -/** @var \Webklex\PHPIMAP\Support\MessageCollection $aMessage */ -$aMessage = $oFolder->query()->text('Hello world')->leaveUnread()->get(); -``` - - -#### Attachments -Save message attachments: -```php -/** @var \Webklex\PHPIMAP\Message $message */ - -/** @var \Webklex\PHPIMAP\Support\AttachmentCollection $attachments */ -$attachments = $message->getAttachments(); - -$attachments->each(function ($attachment) { - /** @var \Webklex\PHPIMAP\Attachment $attachment */ - $attachment->save("/some/path/"); -}); -``` - - -#### Advanced fetching -Fetch messages without body fetching (decrease load): -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereText('Hello world')->setFetchBody(false)->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereAll()->setFetchBody(false)->get(); -``` - -Fetch messages without body, flag and attachment fetching (decrease load): -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereText('Hello world') -->setFetchFlags(false) -->setFetchBody(false) -->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereAll() -->setFetchFlags(false) -->setFetchBody(false) -->get(); -``` - -Change messages fetch order: -```php -/** @var \Webklex\PHPIMAP\Folder $folder */ - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereText('Hello world') -->setFetchOrder('asc') -->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereAll() -->setFetchOrderAsc() -->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereAll() -->fetchOrderAsc() -->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereText('Hello world') -->setFetchOrder('desc') -->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereAll() -->setFetchOrderDesc() -->get(); - -/** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ -$messages = $folder->query()->whereAll() -->fetchOrderDesc() -->get(); -``` - - -#### Events -The following events are available: -- `Webklex\PHPIMAP\Events\MessageNewEvent($message)` — can get triggered by `Folder::idle` -- `Webklex\PHPIMAP\Events\MessageDeletedEvent($message)` — triggered by `Message::delete` -- `Webklex\PHPIMAP\Events\MessageRestoredEvent($message)` — triggered by `Message::restore` -- `Webklex\PHPIMAP\Events\MessageMovedEvent($old_message, $new_message)` — triggered by `Message::move` -- `Webklex\PHPIMAP\Events\MessageCopiedEvent($old_message, $new_message)` — triggered by `Message::copy` -- `Webklex\PHPIMAP\Events\FlagNewEvent($flag)` — triggered by `Message::setFlag` -- `Webklex\PHPIMAP\Events\FlagDeletedEvent($flag)` — triggered by `Message::unsetFlag` -- `Webklex\PHPIMAP\Events\FolderNewEvent($folder)` — can get triggered by `Client::createFolder` -- `Webklex\PHPIMAP\Events\FolderDeletedEvent($folder)` — triggered by `Folder::delete` -- `Webklex\PHPIMAP\Events\FolderMovedEvent($old_folder, $new_folder)` — triggered by `Folder::move` - -Create and register your own custom event: -```php -class CustomMessageNewEvent extends Webklex\PHPIMAP\Events\MessageNewEvent { - - /** - * Create a new event instance. - * @var \Webklex\PHPIMAP\Message[] $messages - * @return void - */ - public function __construct($messages) { - $this->message = $messages[0]; - echo "New message: ".$this->message->subject."\n"; - } -} - -/** @var \Webklex\PHPIMAP\Client $client */ -$client->setEvent("message", "new", CustomMessageNewEvent::class); -``` -..or set it in your config file under `events.message.new`. - - -#### Masking -PHP-IMAP already comes with two default masks [MessageMask::class](#messagemaskclass) and [AttachmentMask::class](#attachmentmaskclass). - -The masked instance has to be called manually and is designed to add custom functionality. - -You can call the default mask by calling the mask method without any arguments. -```php -/** @var \Webklex\PHPIMAP\Message $message */ -$mask = $message->mask(); -``` - -There are several methods available to set the default mask: -```php -/** @var \Webklex\PHPIMAP\Client $client */ -/** @var \Webklex\PHPIMAP\Message $message */ - -$message_mask = \Webklex\PHPIMAP\Support\Masks\MessageMask::class; - -$client->setDefaultMessageMask($message_mask); -$message->setMask($message_mask); -$mask = $message->mask($message_mask); -``` -The last one wont set the mask but generate a masked instance using the provided mask. - -You could also set the default masks inside your `config/imap.php` file under `masks`. - -You can also apply a mask on [attachments](#attachmentclass): -```php -/** @var \Webklex\PHPIMAP\Client $client */ -/** @var \Webklex\PHPIMAP\Attachment $attachment */ -$attachment_mask = \Webklex\PHPIMAP\Support\Masks\AttachmentMask::class; - -$client->setDefaultAttachmentMask($attachment_mask); -$attachment->setMask($attachment_mask); -$mask = $attachment->mask($attachment_mask); -``` - -If you want to implement your own mask just extend [MessageMask::class](#messagemaskclass), [AttachmentMask::class](#attachmentmaskclass) -or [Mask::class](#maskclass) and implement your desired logic: - -```php -/** @var \Webklex\PHPIMAP\Message $message */ -class CustomMessageMask extends \Webklex\PHPIMAP\Support\Masks\MessageMask { - - /** - * New custom method which can be called through a mask - * @return string - */ - public function token(){ - return implode('-', [$this->message_id, $this->uid, $this->message_no]); - } -} - -$mask = $message->mask(CustomMessageMask::class); - -echo $mask->token().'@'.$mask->uid; -``` - -Additional examples can be found here: -- [Custom message mask](https://github.com/Webklex/php-imap/blob/master/examples/custom_message_mask.php) -- [Custom attachment mask](https://github.com/Webklex/php-imap/blob/master/examples/custom_attachment_mask.php) - - -#### Specials -Find the folder containing a message: -```php -/** @var \Webklex\PHPIMAP\Message $message */ -$folder = $message->getFolder(); -``` - - -## Support -If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). -However please be aware that it might take some time to get an answer. -Off topic, rude or abusive issues will be deleted without any notice. - -If you need **immediate** or **commercial** support, feel free to send me a mail at github@webklex.com. - - -##### A little notice -If you write source code in your issue, please consider to format it correctly. This makes it so much nicer to read -and people are more likely to comment and help :) - -``` php - -echo 'your php code...'; - -``` - -will turn into: -```php -echo 'your php code...'; -``` - - -### Features & pull requests -Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. -To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) -first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) -if you're just wishing a feature ;) - - -## Documentation -### [Client::class](src/Client.php) -| Method | Arguments | Return | Description | -| ------------------------- | ------------------------------------------------------------------------------- | :---------------: | ---------------------------------------------------------------------------------------------------------------------------- | -| setConfig | array $config | self | Set the Client configuration. Take a look at `config/imap.php` for more inspiration. | -| getConnection | resource $connection | resource | Get the current imap resource | -| isConnected | | bool | Determine if connection was established. | -| checkConnection | | | Determine if connection was established and connect if not. | -| connect | | | Connect to server. | -| reconnect | | | Terminate and reconnect to server. | -| disconnect | | | Disconnect from server. | -| getFolder | string $folder_name, $delimiter = null | Folder | Get a Folder instance by name or path | -| getFolders | bool $hierarchical, string or null $parent_folder | FolderCollection | Get folders list. If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array. | -| openFolder | string or Folder $folder, integer $attempts | | Open a given folder. | -| createFolder | string $name | boolean | Create a new folder. | -| getQuota | | array | Retrieve the quota level settings, and usage statics per mailbox | -| getQuotaRoot | string $quota_root | array | Retrieve the quota settings per user | -| expunge | | bool | Delete all messages marked for deletion | -| setTimeout | string or int $type, int $timeout | boolean | Set the timeout for certain imap operations: 1: Open, 2: Read, 3: Write, 4: Close | -| getTimeout | string or int $type | int | Check current mailbox | -| setDefaultMessageMask | string $mask | self | Set the default message mask class | -| getDefaultMessageMask | | string | Get the current default message mask class name | -| setDefaultAttachmentMask | string $mask | self | Set the default attachment mask class | -| getDefaultAttachmentMask | | string | Get the current default attachment mask class name | -| getFolderPath | | string | Get the current folder path | - - -### [Message::class](src/Message.php) -| Method | Arguments | Return | Description | -| --------------- | ----------------------------- | :------------------: | -------------------------------------- | -| parseBody | | Message | Parse the Message body | -| delete | boolean $expunge | boolean | Delete the current Message | -| restore | boolean $expunge | boolean | Restore a deleted Message | -| copy | string $mailbox | boolean | Copy the current Messages to a mailbox | -| move | string $mailbox, boolean $expunge | boolean | Move the current Messages to a mailbox | -| setFlag | string or array $flag | boolean | Set one or many flags | -| unsetFlag | string or array $flag | boolean | Unset one or many flags | -| hasTextBody | | | Check if the Message has a text body | -| hasHTMLBody | | | Check if the Message has a html body | -| getTextBody | | string | Get the Message text body | -| getHTMLBody | | string | Get the Message html body | -| getAttachments | | AttachmentCollection | Get all message attachments | -| hasAttachments | | boolean | Checks if there are any attachments present | -| getClient | | Client | Get the current Client instance | -| getUid | | string | Get the current UID | -| getFetchOptions | | string | Get the current fetch option | -| getMsglist | | integer | Get the current message list | -| getHeaderInfo | | object | Get the current header_info object | -| getHeader | | string | Get the current raw header | -| getMessageId | | string | Get the current message ID | -| getMessageNo | | integer | Get the current message number | -| getPriority | | integer | Get the current message priority | -| getSubject | | string | Get the current subject | -| getReferences | | mixed | Get any potentially present references | -| getDate | | Carbon | Get the current date object | -| getFrom | | array | Get the current from information | -| getTo | | array | Get the current to information | -| getCc | | array | Get the current cc information | -| getBcc | | array | Get the current bcc information | -| getReplyTo | | array | Get the current reply to information | -| getInReplyTo | | string | Get the current In-Reply-To | -| getSender | | array | Get the current sender information | -| getBodies | | mixed | Get the current bodies | -| getRawBody | | mixed | Get the current raw message body | -| getFlags | | FlagCollection | Get the current message flags | -| is | | boolean | Does this message match another one? | -| getStructure | | object | The raw message structure | -| getFolder | | Folder | The current folder | -| mask | string $mask = null | Mask | Get a masked instance | -| setMask | string $mask | Message | Set the mask class | -| getMask | | string | Get the current mask class name | - - -### [Folder::class](src/Folder.php) -| Method | Arguments | Return | Description | -| ----------------- | ----------------------------------------------------------------------------------- | :---------------: | ---------------------------------------------- | -| hasChildren | | bool | Determine if folder has children. | -| setChildren | array $children | self | Set children. | -| delete | | | Delete the current Mailbox | -| subscribe | | | Subscribe to the current Mailbox | -| unsubscribe | | | Unsubscribe from the current Mailbox | -| idle | callable $callback(Message $new_message) | | Idle the current folder | -| move | string $mailbox | | Move or Rename the current Mailbox | -| rename | string $mailbox | | Move or Rename the current Mailbox | -| getStatus | | array | Returns status information on the current mailbox | -| examine | | array | Returns status information on the current mailbox | -| appendMessage | string $message, string $options, string $internal_date | bool | Append a string message to the current mailbox | -| getClient | | Client | Get the current Client instance | -| query | string $charset = 'UTF-8' | WhereQuery | Get the current Client instance | -| messages | string $charset = 'UTF-8' | WhereQuery | Alias for Folder::query() | -| search | string $charset = 'UTF-8' | WhereQuery | Alias for Folder::query() | - - -### [Query::class](src/Query/WhereQuery.php) -| Method | Arguments | Return | Description | -| ------------------ | --------------------------------- | :---------------: | ---------------------------------------------- | -| where | mixed $criteria, $value = null | WhereQuery | Add new criteria to the current query | -| orWhere | Closure $closure | WhereQuery | If supported you can perform extended search requests | -| andWhere | Closure $closure | WhereQuery | If supported you can perform extended search requests | -| all | | WhereQuery | Select all available messages | -| answered | | WhereQuery | Select answered messages | -| deleted | | WhereQuery | Select deleted messages | -| new | | WhereQuery | Select new messages | -| not | | WhereQuery | Not select messages | -| old | | WhereQuery | Select old messages | -| recent | | WhereQuery | Select recent messages | -| seen | | WhereQuery | Select seen messages | -| unanswered | | WhereQuery | Select unanswered messages | -| undeleted | | WhereQuery | Select undeleted messages | -| unflagged | | WhereQuery | Select unflagged messages | -| unseen | | WhereQuery | Select unseen messages | -| noXSpam | | WhereQuery | Select as no xspam flagged messages | -| isXSpam | | WhereQuery | Select as xspam flagged messages | -| language | string $value | WhereQuery | Select messages matching a given language | -| unkeyword | string $value | WhereQuery | Select messages matching a given unkeyword | -| messageId | string $value | WhereQuery | Select messages matching a given message id | -| to | string $value | WhereQuery | Select messages matching a given receiver (To:) | -| text | string $value | WhereQuery | Select messages matching a given text body | -| subject | string $value | WhereQuery | Select messages matching a given subject | -| since | string $value | WhereQuery | Select messages since a given date | -| on | string $value | WhereQuery | Select messages on a given date | -| keyword | string $value | WhereQuery | Select messages matching a given keyword | -| from | string $value | WhereQuery | Select messages matching a given sender (From:) | -| flagged | string $value | WhereQuery | Select messages matching a given flag | -| cc | string $value | WhereQuery | Select messages matching a given receiver (CC:) | -| body | string $value | WhereQuery | Select messages matching a given HTML body | -| before | string $value | WhereQuery | Select messages before a given date | -| bcc | string $value | WhereQuery | Select messages matching a given receiver (BCC:) | -| count | | integer | Count all available messages matching the current search criteria | -| get | | MessageCollection | Fetch messages with the current query | -| limit | integer $limit, integer $page = 1 | WhereQuery | Limit the amount of messages being fetched | -| setFetchOptions | boolean $fetch_options | WhereQuery | Set the fetch options | -| setFetchBody | boolean $fetch_body | WhereQuery | Set the fetch body option | -| setFetchFlags | boolean $fetch_flags | WhereQuery | Set the fetch flags option | -| setFetchOrder | string $fetch_order | WhereQuery | Change the fetch ordering (`asc` is "oldest first", `desc` is "newest first") | -| setFetchOrderAsc | | WhereQuery | Change the fetch ordering to ascending | -| setFetchOrderDesc | | WhereQuery | Change the fetch ordering to descending | -| leaveUnread | | WhereQuery | Don't mark all messages as "read" while fetching: | -| markAsRead | | WhereQuery | Mark all messages as "read" while fetching | -| paginate | int $perPage = 5, $page = null, $pageName = 'imap_page' | LengthAwarePaginator | Paginate the current query. | - - -### [Attachment::class](src/Attachment.php) -| Method | Arguments | Return | Description | -| -------------- | ------------------------------ | :------------: | ------------------------------------------------------ | -| getContent | | string or null | Get attachment content | -| getMimeType | | string or null | Get attachment mime type | -| getExtension | | string or null | Get a guessed attachment extension | -| getId | | string or null | Get attachment id | -| getName | | string or null | Get attachment name | -| getContent | | string or null | Get attachment content | -| setSize | | int or null | Get attachment size | -| getType | | string or null | Get attachment type | -| getDisposition | | string or null | Get attachment disposition | -| getContentType | | string or null | Get attachment content type | -| save | string $path, string $filename | boolean | Save the attachment content to your filesystem | -| mask | string $mask = null | Mask | Get a masked instance | -| setMask | string $mask | Attachment | Set the mask class | -| getMask | | string | Get the current mask class name | - - -### [Mask::class](src/Support/Masks/Mask.php) -| Method | Arguments | Return | Description | -| -------------- | ------------------------------ | :------------: | ------------------------------------------------------ | -| getParent | | Masked parent | Get the masked parent object | -| getAttributes | | array | Get all cloned attributes | -| __get | | mixed | Access any cloned parent attribute | -| __set | | mixed | Set any cloned parent attribute | -| __inherit | | mixed | All public methods of the given parent are callable | - - -### [MessageMask::class](src/Support/Masks/MessageMask.php) -| Method | Arguments | Return | Description | -| ----------------------------------- | -------------------------------------- | :------------: | ------------------------------------------ | -| getHtmlBody | | string or null | Get HTML body | -| getCustomHTMLBody | callable or bool $callback | string or null | Get a custom HTML body | -| getHTMLBodyWithEmbeddedBase64Images | | string or null | Get HTML body with embedded base64 images | -| getHTMLBodyWithEmbeddedUrlImages | string $route_name, array $params = [] | string or null | Get HTML body with embedded routed images | - - -### [AttachmentMask::class](src/Support/Masks/AttachmentMask.php) -| Method | Arguments | Return | Description | -| -------------- | ------------------------------ | :------------: | ------------------------------------------------------ | -| getContentBase64Encoded | | string or null | Get attachment content | -| getImageSrc | | string or null | Get attachment mime type | - - -### [MessageCollection::class](src/Support/MessageCollection.php) -Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) - -| Method | Arguments | Return | Description | -| -------- | --------------------------------------------------- | :------------------: | -------------------------------- | -| paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | - - -### [FlagCollection::class](src/Support/FlagCollection.php) -Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) - -| Method | Arguments | Return | Description | -| -------- | --------------------------------------------------- | :------------------: | -------------------------------- | -| paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | - - -### [AttachmentCollection::class](src/Support/AttachmentCollection.php) -Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) - -| Method | Arguments | Return | Description | -| -------- | --------------------------------------------------- | :------------------: | -------------------------------- | -| paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | - - -### [FolderCollection::class](src/Support/FolderCollection.php) -Extends [Illuminate\Support\Collection::class](https://laravel.com/api/5.4/Illuminate/Support/Collection.html) - -| Method | Arguments | Return | Description | -| -------- | --------------------------------------------------- | :------------------: | -------------------------------- | -| paginate | int $perPage = 15, $page = null, $pageName = 'page' | LengthAwarePaginator | Paginate the current collection. | - - -### Known issues -| Error | Solution | -| ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | - +### Known issues +| Error | Solution | +| ------------------------------------------------------------------------- | ---------------------------------------------------------- | +| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | + ## Change log Please see [CHANGELOG][link-changelog] for more information what has changed recently. From 0031598598d4c9d769b8d595d8d4e884d5023bc3 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 21 Dec 2020 02:01:00 +0100 Subject: [PATCH 122/600] toc updated --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a16d0fa..c8cbb2bc 100755 --- a/README.md +++ b/README.md @@ -20,6 +20,23 @@ Official documentation: [php-imap.com](https://www.php-imap.com/) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) +## Table of Contents +- [Documentations](#documentations) +- [Basic usage example](#basic-usage-example) +- [Support](#support) +- [Known issues](#known-issues) +- [Support](#support) +- [Features & pull requests](#features--pull-requests) +- [Security](#security) +- [Credits](#credits) +- [License](#license) + + +## Documentations +- Legacy (< v2.0.0): [legacy documentation](https://github.com/Webklex/php-imap/tree/1.4.5) +- Core documentation: [php-imap.com](https://www.php-imap.com/) + + ## Basic usage example This is a basic example, which will echo out all Mails within all imap folders and will move every message into INBOX.read. Please be aware that this should not be @@ -69,7 +86,37 @@ foreach($folders as $folder){ | Error | Solution | | ------------------------------------------------------------------------- | ---------------------------------------------------------- | | Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | - + + +## Support +If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). +However please be aware that it might take some time to get an answer. +Off topic, rude or abusive issues will be deleted without any notice. + +If you need **commercial** support, feel free to send me a mail at github@webklex.com. + + +##### A little notice +If you write source code in your issue, please consider to format it correctly. This makes it so much nicer to read +and people are more likely to comment and help :) + +```php + +echo 'your php code...'; + +``` + +will turn into: +```php +echo 'your php code...'; +``` + + +## Features & pull requests +Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. +To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) +first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) +if you're just wishing a feature ;) ## Change log Please see [CHANGELOG][link-changelog] for more information what has changed recently. From 2b0e812d990c2688e8bea56adc303dacb65b949f Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 21 Dec 2020 02:08:50 +0100 Subject: [PATCH 123/600] Lines fixed --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8cbb2bc..11f11da9 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) ## Table of Contents - [Documentations](#documentations) - [Basic usage example](#basic-usage-example) -- [Support](#support) - [Known issues](#known-issues) - [Support](#support) - [Features & pull requests](#features--pull-requests) @@ -111,13 +110,14 @@ will turn into: echo 'your php code...'; ``` - + ## Features & pull requests Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) if you're just wishing a feature ;) + ## Change log Please see [CHANGELOG][link-changelog] for more information what has changed recently. From 835373a337f81ee11c97b6a690fe2e871fecaf2a Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 21 Dec 2020 19:57:53 +0100 Subject: [PATCH 124/600] Proxy support fixed #53 --- src/Connection/Protocols/Protocol.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 18abc90d..60081453 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -145,10 +145,11 @@ public function getProxy() { /** * Prepare socket options + * @var string $transport * * @return array */ - private function defaultSocketOptions() { + private function defaultSocketOptions($transport) { $options = []; if ($this->encryption != false) { $options["ssl"] = [ @@ -158,13 +159,13 @@ private function defaultSocketOptions() { } if ($this->proxy["socket"] != null) { - $options["proxy"] = $this->proxy["socket"]; - $options["request_fulluri"] = $this->proxy["request_fulluri"]; + $options[$transport]["proxy"] = $this->proxy["socket"]; + $options[$transport]["request_fulluri"] = $this->proxy["request_fulluri"]; if ($this->proxy["username"] != null) { $auth = base64_encode($this->proxy["username"].':'.$this->proxy["password"]); - $options["header"] = [ + $options[$transport]["header"] = [ "Proxy-Authorization: Basic $auth" ]; } @@ -188,7 +189,7 @@ protected function createStream($transport, $host, $port, $timeout) { $socket = "$transport://$host:$port"; $stream = stream_socket_client($socket, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, - stream_context_create($this->defaultSocketOptions()) + stream_context_create($this->defaultSocketOptions($transport)) ); if (!$stream) { From d74fbddfafb8e2d8d296a1b0c7352c7d2b88367a Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 21 Dec 2020 20:00:37 +0100 Subject: [PATCH 125/600] Version information updated --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f745ba9..635f1b9a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + + +## [2.3.0] - 2020-12-21 +### Fixed - Cert validation issue fixed - Allow boundaries ending with a space or semicolon (thanks [@smartilabs](https://github.com/smartilabs)) - Ignore IMAP DONE command response #57 From 417c1c0f8940dd9662f6ad059600311361f2e6e2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 00:43:14 +0100 Subject: [PATCH 126/600] Missing RFC attributes added --- CHANGELOG.md | 4 ++-- src/Message.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 635f1b9a..7612819c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Missing RFC attributes added ### Added - NaN ### Affected Classes -- NaN +- [Message::class](src/Message.php) ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index c11a238c..1c8348cf 100755 --- a/src/Message.php +++ b/src/Message.php @@ -425,7 +425,7 @@ public function getHTMLBody() { */ private function parseHeader() { $sequence_id = $this->getSequenceId(); - $headers = $this->client->getConnection()->headers([$sequence_id], $this->sequence === IMAP::ST_UID); + $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } @@ -493,7 +493,7 @@ public function parseBody() { $this->client->openFolder($this->folder_path); $sequence_id = $this->getSequenceId(); - $contents = $this->client->getConnection()->content([$sequence_id], $this->sequence === IMAP::ST_UID); + $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); if (!isset($contents[$sequence_id])) { throw new MessageContentFetchingException("no content found", 0); } From abd91e1d9ebef501fac8b00eac1ed72d04196b72 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 00:45:22 +0100 Subject: [PATCH 127/600] Get a message by its message number --- CHANGELOG.md | 3 ++- src/Query/Query.php | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7612819c..c01c0cdc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing RFC attributes added ### Added -- NaN +- Get a message by its message number ### Affected Classes - [Message::class](src/Message.php) +- [Query::class](src/Query/Query.php) ### Breaking changes - NaN diff --git a/src/Query/Query.php b/src/Query/Query.php index a0f3539d..1645d1e8 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -285,6 +285,21 @@ public function getMessage($uid, $msglist = null, $sequence = null){ } /** + * Get a message by its message number + * @param $msgn + * @param null $msglist + * + * @return Message + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageHeaderFetchingException + * @throws RuntimeException + * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + */ + public function getMessageByMsgn($msgn, $msglist = null){ + return $this->getMessage($msgn, $msglist, IMAP::ST_MSGN); + } * Paginate the current query * @param int $per_page * @param null $page From c0bdb982f8291597726652dcc18e7781a6c0d703 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 00:49:49 +0100 Subject: [PATCH 128/600] Get a message by its uid #72 #66 #63 --- CHANGELOG.md | 1 + src/Query/Query.php | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c01c0cdc..8b8febe3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Get a message by its message number +- Get a message by its uid #72 #66 #63 ### Affected Classes - [Message::class](src/Message.php) diff --git a/src/Query/Query.php b/src/Query/Query.php index 1645d1e8..5fc606db 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -288,7 +288,7 @@ public function getMessage($uid, $msglist = null, $sequence = null){ * Get a message by its message number * @param $msgn * @param null $msglist - * + * * @return Message * @throws ConnectionFailedException * @throws InvalidMessageDateException @@ -300,6 +300,24 @@ public function getMessage($uid, $msglist = null, $sequence = null){ public function getMessageByMsgn($msgn, $msglist = null){ return $this->getMessage($msgn, $msglist, IMAP::ST_MSGN); } + + /** + * Get a message by its uid + * @param $uid + * + * @return Message + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageHeaderFetchingException + * @throws RuntimeException + * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + */ + public function getMessageByUid($uid){ + return $this->getMessage($uid, null, IMAP::ST_UID); + } + + /** * Paginate the current query * @param int $per_page * @param null $page From 27fbaaf8346a5888df9993932aa3ad52ddac5376 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 00:51:25 +0100 Subject: [PATCH 129/600] Set the message sequence when idling --- CHANGELOG.md | 2 ++ src/Folder.php | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b8febe3..cae061b7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Missing RFC attributes added +- Set the message sequence when idling ### Added - Get a message by its message number @@ -14,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Message::class](src/Message.php) +- [Folder::class](src/Folder.php) - [Query::class](src/Query/Query.php) ### Breaking changes diff --git a/src/Folder.php b/src/Folder.php index 0eb12610..d84a80de 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -356,6 +356,8 @@ public function idle(callable $callback, $timeout = 1200) { $this->client->reconnect(); $this->client->openFolder($this->path, true); $connection = $this->client->getConnection(); + + $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); $connection->idle(); while (true) { @@ -366,7 +368,8 @@ public function idle(callable $callback, $timeout = 1200) { $connection->done(); $this->client->openFolder($this->path, true); - $message = $this->query()->getMessage($msgn); + $message = $this->query()->getMessageByMsgn($msgn); + $message->setSequence($sequence); $callback($message); $event = $this->getEvent("message", "new"); From 4bc48f218280c69edee6b89e80ad32c999cbac7d Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 00:53:10 +0100 Subject: [PATCH 130/600] Missing UID commands added #64 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cae061b7..869b5236 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Missing RFC attributes added - Set the message sequence when idling +- Missing UID commands added #64 ### Added - Get a message by its message number diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 518f7740..83abf5ec 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -758,7 +758,8 @@ public function store(array $flags, $from, $to = null, $mode = null, $silent = t $set .= ':' . ($to == INF ? '*' : (int)$to); } - $result = $this->requestAndResponse('STORE', [$set, $item, $flags], $silent); + $command = ($uid ? "UID " : "")."STORE"; + $result = $this->requestAndResponse($command, [$set, $item, $flags], $silent); if ($silent) { return (bool)$result; @@ -816,8 +817,9 @@ public function copyMessage($folder, $from, $to = null, $uid = false) { if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); } + $command = ($uid ? "UID " : "")."COPY"; - return $this->requestAndResponse('COPY', [$set, $this->escapeString($folder)], true); + return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** @@ -836,8 +838,9 @@ public function moveMessage($folder, $from, $to = null, $uid = false) { if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); } + $command = ($uid ? "UID " : "")."MOVE"; - return $this->requestAndResponse('MOVE', [$set, $this->escapeString($folder)], true); + return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** @@ -940,12 +943,12 @@ public function getQuotaRoot($quota_root = 'INBOX') { /** * Send idle command - * @param bool $uid set to true if passing a unique id + * @param bool $uid set to true if passing a unique id (depreciated argument: will be removed. CMD UID IDLE is not supported) * * @throws RuntimeException */ public function idle($uid = false) { - $this->sendRequest('IDLE'); + $this->sendRequest("IDLE"); if (!$this->assumedNextLine('+ ')) { throw new RuntimeException('idle failed'); } From 1bd367ed0bc1a0dc31bded1664c32272ca66f5bf Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 00:54:50 +0100 Subject: [PATCH 131/600] Version information updated --- CHANGELOG.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 869b5236..7d1eb39c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + + +## [2.3.1] - 2020-12-30 +### Fixed - Missing RFC attributes added - Set the message sequence when idling - Missing UID commands added #64 @@ -19,10 +33,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Folder::class](src/Folder.php) - [Query::class](src/Query/Query.php) -### Breaking changes -- NaN - - ## [2.3.0] - 2020-12-21 ### Fixed - Cert validation issue fixed From 540dcc946130355f677f315694c2237bcc141e79 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 01:21:56 +0100 Subject: [PATCH 132/600] Travis updated --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c9fb4d2a..f09b9fe8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,9 +24,6 @@ before-install: install: - COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction -script: - - phpunit - notifications: email: on_success: always From 84c28cb96a1b4277b3fbd400b2b9e572db0b9f76 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 01:27:12 +0100 Subject: [PATCH 133/600] unused scripts removed --- composer.json | 3 --- phpunit.xml.dist | 29 ----------------------------- 2 files changed, 32 deletions(-) delete mode 100644 phpunit.xml.dist diff --git a/composer.json b/composer.json index 311a347e..d272c8a0 100644 --- a/composer.json +++ b/composer.json @@ -39,9 +39,6 @@ "Tests\\": "tests" } }, - "scripts": { - "test": "phpunit" - }, "extra": { "branch-alias": { "dev-master": "1.0-dev" diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index df4426e4..00000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - tests - - - - - src/ - - - - - - - - - - From 9c2feb7815f3ce609907f2f991b5fcb258224475 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 01:42:52 +0100 Subject: [PATCH 134/600] Basic test added --- .gitignore | 3 ++- .travis.yml | 3 +++ composer.json | 6 ++++++ phpunit.xml.dist | 29 +++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 phpunit.xml.dist diff --git a/.gitignore b/.gitignore index 116f35f2..77d608c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor composer.lock -.idea \ No newline at end of file +.idea +/build/ diff --git a/.travis.yml b/.travis.yml index f09b9fe8..c9fb4d2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,9 @@ before-install: install: - COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction +script: + - phpunit + notifications: email: on_success: always diff --git a/composer.json b/composer.json index d272c8a0..9bc3849a 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,9 @@ "symfony/http-foundation": ">=2.8.0", "illuminate/pagination": ">=5.0.0" }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, "autoload": { "psr-4": { "Webklex\\PHPIMAP\\": "src" @@ -39,6 +42,9 @@ "Tests\\": "tests" } }, + "scripts": { + "test": "phpunit" + }, "extra": { "branch-alias": { "dev-master": "1.0-dev" diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..8f96011e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + \ No newline at end of file From baa1ea4c6db69a40e9c3007fc47a1717c867522a Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 31 Dec 2020 01:46:51 +0100 Subject: [PATCH 135/600] php 7.4 test added --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9fb4d2a..de3d0244 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,14 +6,16 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 - hhvm matrix: fast_finish: true allow_failures: - - php: 5.6 - php: 7.0 - php: 7.1 + - php: 7.3 + - php: 7.4 - php: hhvm sudo: false From 2c2bb89042c5a106636087e1967b4a3f9c25b7e3 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 01:48:38 +0100 Subject: [PATCH 136/600] Get partial overview when `IMAP::ST_UID` is set #74 --- CHANGELOG.md | 2 +- src/Connection/Protocols/ImapProtocol.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1eb39c..75b24de6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Get partial overview when `IMAP::ST_UID` is set #74 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 83abf5ec..199585aa 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1005,13 +1005,14 @@ public function overview($sequence, $uid = false) { $uids = $this->getUid(); $ids = []; foreach ($uids as $msgn => $v) { - if ( ($to >= $msgn && $from <= $msgn) || ($to === "*" && $from <= $msgn) ){ - $ids[] = $uid ? $v : $msgn; + $id = $uid ? $v : $msgn; + if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){ + $ids[] = $id; } } $headers = $this->headers($ids, $rfc = "RFC822", $uid); - foreach ($headers as $msgn => $raw_header) { - $result[$msgn] = (new Header($raw_header))->getAttributes(); + foreach ($headers as $id => $raw_header) { + $result[$id] = (new Header($raw_header))->getAttributes(); } return $result; } From 3cab009779ba83088b5f19d7268df2be427968ca Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 02:05:19 +0100 Subject: [PATCH 137/600] Configurable supported default flags added --- CHANGELOG.md | 4 ++-- src/Message.php | 18 ++++++++++++++++-- src/config/imap.php | 9 +++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b24de6..53bbf939 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Get partial overview when `IMAP::ST_UID` is set #74 ### Added -- NaN +- Configurable supported default flags added ### Affected Classes -- NaN +- [Message::class](src/Message.php) ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index 1c8348cf..e141f1ea 100755 --- a/src/Message.php +++ b/src/Message.php @@ -178,7 +178,7 @@ class Message { * * @var array $available_flags */ - private $available_flags = ['recent', 'flagged', 'answered', 'deleted', 'seen', 'draft']; + private $available_flags = null; /** * Message constructor. @@ -209,6 +209,7 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul $this->folder_path = $client->getFolderPath(); $this->config = ClientManager::get('options'); + $this->available_flags = ClientManager::get('flags'); $this->setSequence($sequence); $this->setFetchOption($fetch_options); @@ -279,6 +280,7 @@ public static function make($uid, $msglist, Client $client, $raw_header, $raw_bo ]); $instance->setFolderPath($client->getFolderPath()); $instance->setConfig(ClientManager::get('options')); + $instance->setAvailableFlags(ClientManager::get('flags')); $instance->setSequence($sequence); $instance->setFetchOption($fetch_options); @@ -454,7 +456,7 @@ public function parseRawFlags($raw_flags) { $flag = substr($flag, 1); } $flag_key = strtolower($flag); - if (in_array($flag_key, $this->available_flags)) { + if (in_array($flag_key, $this->available_flags) || $this->available_flags === null) { $this->flags->put($flag_key, $flag); } } @@ -1195,6 +1197,18 @@ public function setConfig($config){ return $this; } + /** + * Set the available flags + * @param $available_flags + * + * @return $this + */ + public function setAvailableFlags($available_flags){ + $this->available_flags = $available_flags; + + return $this; + } + /** * Set the attachment collection * @param $attachments diff --git a/src/config/imap.php b/src/config/imap.php index e7fa9090..9147a996 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -154,6 +154,15 @@ ] ], + /* + |-------------------------------------------------------------------------- + | Available flags + |-------------------------------------------------------------------------- + | + | List all available / supported flags. Set to null to accept all given flags. + */ + 'flags' => ['recent', 'flagged', 'answered', 'deleted', 'seen', 'draft'], + /* |-------------------------------------------------------------------------- | Available events From d59e8b8713e122c3d58e3f12bb06c81de5ff7c35 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 02:17:52 +0100 Subject: [PATCH 138/600] Issue templates updated and additional context added --- .github/ISSUE_TEMPLATE/bug_report.md | 18 ++++++++++-------- .github/ISSUE_TEMPLATE/general-help-request.md | 7 ++++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8fc2190b..735fbe01 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,12 +7,14 @@ about: Create a report to help us improve **Describe the bug** A clear and concise description of what the bug is. -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +**Used config** +Please provide the used config, if you are not using the package default config. + +**Code to Reproduce** +The troubling code section which produces the reported bug. +```php +echo "Bug"; +``` **Expected behavior** A clear and concise description of what you expected to happen. @@ -21,9 +23,9 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop / Server (please complete the following information):** - - OS: [e.g. iOS] +- OS: [e.g. Debian 10] - PHP: [e.g. 5.5.9] - - Version [e.g. 22] +- Version [e.g. v2.3.1] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/general-help-request.md b/.github/ISSUE_TEMPLATE/general-help-request.md index 6543a1c2..49809d14 100644 --- a/.github/ISSUE_TEMPLATE/general-help-request.md +++ b/.github/ISSUE_TEMPLATE/general-help-request.md @@ -4,4 +4,9 @@ about: Feel free to ask about any project related stuff --- -Please be aware that these issues will be closed if inactive for more then 14 days :) +Please be aware that these issues will be closed if inactive for more then 14 days. + +Also make sure to use https://github.com/Webklex/php-imap/issues/new?template=bug_report.md if you want to report a bug +or https://github.com/Webklex/php-imap/issues/new?template=feature_request.md if you want to suggest a feature. + +Still here? Well clean this out and go ahead :) From cc8b1fd0e596435560ca3809ce313fc2403a60cd Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 02:44:49 +0100 Subject: [PATCH 139/600] Missing `Query::paginate()` docs added #70 --- src/Query/Query.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 5fc606db..b3e67973 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -319,9 +319,9 @@ public function getMessageByUid($uid){ /** * Paginate the current query - * @param int $per_page - * @param null $page - * @param string $page_name + * @param int $per_page Results you which to receive per page + * @param int $page The current page you are on (e.g. 0, 1, 2, ...) use `null` to enable auto mode + * @param string $page_name The page name / uri parameter used for the generated links and the auto mode * * @return LengthAwarePaginator * @throws GetMessagesFailedException From 72d6bed975b3cfdd6b666b36614f892f83c831ab Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 21:38:43 +0100 Subject: [PATCH 140/600] Unnecessary "'" removed from address names --- CHANGELOG.md | 1 + src/Header.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53bbf939..20c4a572 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Get partial overview when `IMAP::ST_UID` is set #74 +- Unnecessary "'" removed from address names ### Added - Configurable supported default flags added diff --git a/src/Header.php b/src/Header.php index d4e4624f..e687ec25 100644 --- a/src/Header.php +++ b/src/Header.php @@ -531,6 +531,10 @@ private function parseAddresses($list) { $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); } } + + if (strpos($address->personal, "'") === 0) { + $address->personal = str_replace("'", "", $address->personal); + } } $address->mail = ($address->mailbox && $address->host) ? $address->mailbox.'@'.$address->host : false; From 43cfd0f098e30937551f85a55b898dde467fb5a4 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 21:43:59 +0100 Subject: [PATCH 141/600] Message attribute class added to unify value handling --- CHANGELOG.md | 6 +- src/Attribute.php | 194 ++++++++++++++++++++++++++++++++++++++++++++++ src/Header.php | 52 ++++++++----- 3 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 src/Attribute.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c4a572..6653fb55 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Configurable supported default flags added +- Message attribute class added to unify value handling ### Affected Classes - [Message::class](src/Message.php) +- [Header::class](src/Header.php) +- [Attribute::class](src/Attribute.php) ### Breaking changes -- NaN +- Stringified message headers are now separated by ", " instead of " ". +- All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance ## [2.3.1] - 2020-12-30 diff --git a/src/Attribute.php b/src/Attribute.php new file mode 100644 index 00000000..5fb02957 --- /dev/null +++ b/src/Attribute.php @@ -0,0 +1,194 @@ +setName($name); + $this->add($value); + } + + + /** + * Return the stringified attribute + * + * @return string + */ + public function __toString() { + return implode(", ", $this->values); + } + + /** + * Return the serialized attribute + * + * @return array + */ + public function __serialize(){ + return $this->values; + } + + /** + * Add one or more values to the attribute + * @param array|mixed $value + * @param boolean $strict + * + * @return Attribute + */ + public function add($value, $strict = false) { + if (is_array($value)) { + return $this->merge($value, $strict); + }elseif ($value !== null) { + $this->attach($value, $strict); + } + + return $this; + } + + /** + * Merge a given array of values with the current values array + * @param array $values + * @param boolean $strict + * + * @return Attribute + */ + public function merge($values, $strict = false) { + if (is_array($values)) { + foreach ($values as $value) { + $this->attach($value, $strict); + } + } + + return $this; + } + + /** + * Check if the attribute contains the given value + * @param mixed $value + * + * @return bool + */ + public function contains($value) { + foreach ($this->values as $v) { + if ($v === $value) { + return true; + } + } + return false; + } + + /** + * Attach a given value to the current value array + * @param $value + * @param bool $strict + */ + public function attach($value, $strict = false) { + if ($strict === true) { + if ($this->contains($value) === false) { + $this->values[] = $value; + } + }else{ + $this->values[] = $value; + } + } + + /** + * Set the attribute name + * @param $name + * + * @return Attribute + */ + public function setName($name){ + $this->name = $name; + + return $this; + } + + /** + * Get the attribute name + * + * @return string + */ + public function getName(){ + return $this->name; + } + + /** + * Get all values + * + * @return array + */ + public function get(){ + return $this->values; + } + + /** + * Alias method for self::get() + * + * @return array + */ + public function all(){ + return $this->get(); + } + + /** + * Get the first value if possible + * + * @return mixed|null + */ + public function first(){ + if (count($this->values) > 0) { + return $this->values[0]; + } + return null; + } + + /** + * Get the last value if possible + * + * @return mixed|null + */ + public function last(){ + $cnt = count($this->values); + if ($cnt > 0) { + return $this->values[$cnt - 1]; + } + return null; + } +} \ No newline at end of file diff --git a/src/Header.php b/src/Header.php index e687ec25..9696f4d1 100644 --- a/src/Header.php +++ b/src/Header.php @@ -34,7 +34,7 @@ class Header { /** * Attribute holder * - * @var array $attributes + * @var Attribute[] $attributes */ protected $attributes = []; @@ -69,7 +69,7 @@ public function __construct($raw_header) { * @param string $method * @param array $arguments * - * @return mixed + * @return Attribute * @throws MethodNotFoundException */ public function __call($method, $arguments) { @@ -89,7 +89,7 @@ public function __call($method, $arguments) { * Magic getter * @param $name * - * @return mixed|null + * @return Attribute|null */ public function __get($name) { return $this->get($name); @@ -99,7 +99,7 @@ public function __get($name) { * Get a specific header attribute * @param $name * - * @return mixed|null + * @return Attribute|null */ public function get($name) { if(isset($this->attributes[$name])) { @@ -109,6 +109,24 @@ public function get($name) { return null; } + /** + * Set a specific attribute + * @param string $name + * @param array|mixed $value + * @param boolean $strict + * + * @return Attribute + */ + public function set($name, $value, $strict = false) { + if(isset($this->attributes[$name]) && $strict === false) { + $this->attributes[$name]->add($value, true); + }else{ + $this->attributes[$name] = new Attribute($name, $value); + } + + return $this->attributes[$name]; + } + /** * Perform a regex match all on the raw header and return the first result * @param $pattern @@ -137,23 +155,20 @@ protected function parse(){ $this->extractAddresses($header); if (property_exists($header, 'subject')) { - $this->attributes["subject"] = $this->decode($header->subject); - } - if (property_exists($header, 'in_reply_to')) { - $this->attributes["in_reply_to"] = is_array($header->in_reply_to) ? $header->in_reply_to : [$header->in_reply_to]; + $this->set("subject", $this->decode($header->subject)); } if (property_exists($header, 'references')) { - $this->attributes["references"] = $this->decode($header->references); + $this->set("references", $this->decode($header->references)); } if (property_exists($header, 'message_id')) { - $this->attributes["message_id"] = str_replace(['<', '>'], '', $header->message_id); + $this->set("message_id", str_replace(['<', '>'], '', $header->message_id)); } $this->parseDate($header); foreach ($header as $key => $value) { $key = trim(rtrim(strtolower($key))); if(!isset($this->attributes[$key])){ - $this->attributes[$key] = $value; + $this->set($key, $value); } } @@ -431,7 +446,7 @@ private function decodeArray($values) { */ private function findPriority() { if(($priority = $this->get("x_priority")) === null) return; - switch($priority){ + switch((int)"$priority"){ case IMAP::MESSAGE_PRIORITY_HIGHEST; $priority = IMAP::MESSAGE_PRIORITY_HIGHEST; break; @@ -452,7 +467,7 @@ private function findPriority() { break; } - $this->attributes["priority"] = $priority; + $this->set("priority", $priority); } /** @@ -493,7 +508,7 @@ private function decodeAddresses($values) { private function extractAddresses($header) { foreach(['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key){ if (property_exists($header, $key)) { - $this->attributes[$key] = $this->parseAddresses($header->$key); + $this->set($key, $this->parseAddresses($header->$key)); } } } @@ -551,11 +566,12 @@ private function parseAddresses($list) { */ private function extractHeaderExtensions(){ foreach ($this->attributes as $key => $value) { + $value = (string)$value; // Only parse strings and don't parse any attributes like the user-agent - if (is_string($value) === true && in_array($key, ["user_agent"]) === false) { + if (in_array($key, ["user_agent"]) === false) { if (($pos = strpos($value, ";")) !== false){ $original = substr($value, 0, $pos); - $this->attributes[$key] = trim(rtrim($original)); + $this->set($key, trim(rtrim($original)), true); // Get all potential extensions $extensions = explode(";", substr($value, $pos + 1)); @@ -569,7 +585,7 @@ private function extractHeaderExtensions(){ $value = str_replace('"', "", $value); $value = trim(rtrim($value)); - $this->attributes[$key] = $value; + $this->set($key, $value); } } } @@ -633,7 +649,7 @@ private function parseDate($header) { } } - $this->attributes["date"] = $parsed_date; + $this->set("date", $parsed_date); } } From 7ecf05f2587b47ea265938206ba8f4ce304cccc0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 21:45:07 +0100 Subject: [PATCH 142/600] Address class added and integrated --- CHANGELOG.md | 3 ++ src/Address.php | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Header.php | 2 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/Address.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6653fb55..698c0686 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,18 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Configurable supported default flags added - Message attribute class added to unify value handling +- Address class added and integrated ### Affected Classes - [Message::class](src/Message.php) - [Header::class](src/Header.php) +- [Address::class](src/Address.php) - [Attribute::class](src/Attribute.php) ### Breaking changes - Stringified message headers are now separated by ", " instead of " ". - All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance +- The formal address object "from", "to", etc now consists of an `Address::class` instance ## [2.3.1] - 2020-12-30 diff --git a/src/Address.php b/src/Address.php new file mode 100644 index 00000000..3eebcf5e --- /dev/null +++ b/src/Address.php @@ -0,0 +1,78 @@ +personal = $object->personal; } + if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox; } + if (property_exists($object, "host")){ $this->host = $object->host; } + if (property_exists($object, "mail")){ $this->mail = $object->mail; } + if (property_exists($object, "full")){ $this->full = $object->full; } + } + + + /** + * Return the stringified address + * + * @return string + */ + public function __toString() { + return $this->full; + } + + /** + * Return the serialized address + * + * @return array + */ + public function __serialize(){ + return [ + "personal" => $this->personal, + "mailbox" => $this->mailbox, + "host" => $this->host, + "mail" => $this->mail, + "full" => $this->full, + ]; + } +} \ No newline at end of file diff --git a/src/Header.php b/src/Header.php index 9696f4d1..653806f3 100644 --- a/src/Header.php +++ b/src/Header.php @@ -555,7 +555,7 @@ private function parseAddresses($list) { $address->mail = ($address->mailbox && $address->host) ? $address->mailbox.'@'.$address->host : false; $address->full = ($address->personal) ? $address->personal.' <'.$address->mail.'>' : $address->mail; - $addresses[] = $address; + $addresses[] = new Address($address); } return $addresses; From bbfee207d8112636e44c6d5ce4bad2bf1e385947 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 1 Jan 2021 21:46:25 +0100 Subject: [PATCH 143/600] Additional comments added --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 698c0686..347221cc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - Stringified message headers are now separated by ", " instead of " ". -- All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance -- The formal address object "from", "to", etc now consists of an `Address::class` instance +- All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) +- The formal address object "from", "to", etc now consists of an `Address::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) ## [2.3.1] - 2020-12-30 From 50c9b38b27163c439b04768cc774b72719e29b5b Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:08:27 +0100 Subject: [PATCH 144/600] toString alias added --- src/Attribute.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Attribute.php b/src/Attribute.php index 5fb02957..110b749b 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -55,6 +55,15 @@ public function __toString() { return implode(", ", $this->values); } + /** + * Return the stringified attribute + * + * @return string + */ + public function toString(){ + return $this->__toString(); + } + /** * Return the serialized attribute * From 8a4dcded7a1830100800491aa4a332468971a10f Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:08:37 +0100 Subject: [PATCH 145/600] toArray alias added --- src/Attribute.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Attribute.php b/src/Attribute.php index 110b749b..15fb65de 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -73,6 +73,15 @@ public function __serialize(){ return $this->values; } + /** + * Convert instance to array + * + * @return array + */ + public function toArray(){ + return $this->__serialize(); + } + /** * Add one or more values to the attribute * @param array|mixed $value From 01dd4696dd150b82f8df12568322fb0704c7b4de Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:18:23 +0100 Subject: [PATCH 146/600] Peek logic moved into its own method --- src/Message.php | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Message.php b/src/Message.php index e141f1ea..4d98ac61 100755 --- a/src/Message.php +++ b/src/Message.php @@ -298,14 +298,7 @@ public static function make($uid, $msglist, Client $client, $raw_header, $raw_bo $instance->parseRawHeader($raw_header); $instance->parseRawFlags($raw_flags); $instance->parseRawBody($raw_body); - - if ($fetch_options == IMAP::FT_PEEK) { - if ($instance->getFlags()->get("seen") == null) { - $instance->unsetFlag("Seen"); - } - } elseif ($instance->getFlags()->get("seen") == null) { - $instance->setFlag("Seen"); - } + $instance->peek(); return $instance; } @@ -502,7 +495,19 @@ public function parseBody() { $content = $contents[$sequence_id]; $body = $this->parseRawBody($content); + $this->peek(); + return $body; + } + + /** + * Handle auto "Seen" flag handling + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException + */ + public function peek(){ if ($this->fetch_options == IMAP::FT_PEEK) { if ($this->getFlags()->get("seen") == null) { $this->unsetFlag("Seen"); @@ -510,8 +515,6 @@ public function parseBody() { }elseif ($this->getFlags()->get("seen") != null) { $this->setFlag("Seen"); } - - return $body; } /** From cbf7c9e8e095c6ec51a2634ed8be1f229022c8ac Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:19:10 +0100 Subject: [PATCH 147/600] Alias `Message::addFlag()` for `Message::setFlag()` added --- src/Message.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Message.php b/src/Message.php index 4d98ac61..c750145c 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1006,6 +1006,18 @@ public function unsetFlag($flag) { } /** + * Set a given flag + * @param string|array $flag + * + * @return bool + * @throws Exceptions\ConnectionFailedException + * @throws MessageFlagException + * @throws Exceptions\EventNotFoundException + */ + public function addFlag($flag) { + return $this->setFlag($flag); + } + * Get all message attachments. * * @return AttachmentCollection From 07654d96c266ff6cbcc07878262829cf4548b65e Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:20:04 +0100 Subject: [PATCH 148/600] Alias `Message::removeFlag()` for `Message::unsetFlag()` added --- CHANGELOG.md | 2 ++ src/Message.php | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347221cc..3b915c9c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Configurable supported default flags added - Message attribute class added to unify value handling - Address class added and integrated +- Alias `Message::addFlag()` for `Message::setFlag()` added +- Alias `Message::removeFlag()` for `Message::unsetFlag()` added ### Affected Classes - [Message::class](src/Message.php) diff --git a/src/Message.php b/src/Message.php index c750145c..dab67c9f 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1018,6 +1018,20 @@ public function addFlag($flag) { return $this->setFlag($flag); } + /** + * Unset a given flag + * @param string|array $flag + * + * @return bool + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException + */ + public function removeFlag($flag) { + return $this->unsetFlag($flag); + } + + /** * Get all message attachments. * * @return AttachmentCollection From 0105492c62b86f3b1b0ad11f4cd4163dc1fd0ff5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:20:36 +0100 Subject: [PATCH 149/600] Alias `Message::attachments()` for `Message::getAttachments()` added --- CHANGELOG.md | 1 + src/Message.php | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b915c9c..76ea6ef1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Configurable supported default flags added - Message attribute class added to unify value handling - Address class added and integrated +- Alias `Message::attachments()` for `Message::getAttachments()` added - Alias `Message::addFlag()` for `Message::setFlag()` added - Alias `Message::removeFlag()` for `Message::unsetFlag()` added diff --git a/src/Message.php b/src/Message.php index dab67c9f..3322bb03 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1040,6 +1040,15 @@ public function getAttachments() { return $this->attachments; } + /** + * Get all message attachments. + * + * @return AttachmentCollection + */ + public function attachments(){ + return $this->getAttachments(); + } + /** * Checks if there are any attachments present * From f0bafdc89ff993bce9ddc42f49a1419cd1a52600 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:20:56 +0100 Subject: [PATCH 150/600] Alias `Message::flags()` for `Message::getFlags()` added --- CHANGELOG.md | 1 + src/Message.php | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ea6ef1..d45ef94f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Alias `Message::attachments()` for `Message::getAttachments()` added - Alias `Message::addFlag()` for `Message::setFlag()` added - Alias `Message::removeFlag()` for `Message::unsetFlag()` added +- Alias `Message::flags()` for `Message::getFlags()` added ### Affected Classes - [Message::class](src/Message.php) diff --git a/src/Message.php b/src/Message.php index 3322bb03..34c2aa0b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1137,6 +1137,15 @@ public function getFlags() { return $this->flags; } + /** + * Get all set flags + * + * @return FlagCollection + */ + public function flags(){ + return $this->getFlags(); + } + /** * Get the fetched structure * From cd916825184f4aa0e6aa39f7f49df2261b93c663 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 03:23:32 +0100 Subject: [PATCH 151/600] New Exception `MessageFlagException::class` added --- CHANGELOG.md | 3 +- src/Exceptions/MessageFlagException.php | 24 ++++++++++++ src/Folder.php | 1 + src/Message.php | 50 +++++++++++++++---------- src/Query/Query.php | 3 ++ 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 src/Exceptions/MessageFlagException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d45ef94f..02e0d00c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Alias `Message::addFlag()` for `Message::setFlag()` added - Alias `Message::removeFlag()` for `Message::unsetFlag()` added - Alias `Message::flags()` for `Message::getFlags()` added +- New Exception `MessageFlagException::class` added ### Affected Classes - [Message::class](src/Message.php) @@ -28,7 +29,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Stringified message headers are now separated by ", " instead of " ". - All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - The formal address object "from", "to", etc now consists of an `Address::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - +- When fetching or manipulating message flags a `MessageFlagException::class` exception can be thrown if a runtime error occurs ## [2.3.1] - 2020-12-30 ### Fixed diff --git a/src/Exceptions/MessageFlagException.php b/src/Exceptions/MessageFlagException.php new file mode 100644 index 00000000..443057a4 --- /dev/null +++ b/src/Exceptions/MessageFlagException.php @@ -0,0 +1,24 @@ +client->getConnection()->setConnectionTimeout($timeout); diff --git a/src/Message.php b/src/Message.php index 34c2aa0b..965b9d24 100755 --- a/src/Message.php +++ b/src/Message.php @@ -16,6 +16,7 @@ use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; use Webklex\PHPIMAP\Support\AttachmentCollection; @@ -196,6 +197,7 @@ class Message { * @throws MessageHeaderFetchingException * @throws MessageContentFetchingException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false, $sequence = null) { @@ -260,10 +262,11 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul * @return Message * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException - * @throws Exceptions\RuntimeException * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws \ReflectionException + * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public static function make($uid, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null, $sequence = null){ $reflection = new \ReflectionClass(self::class); @@ -319,9 +322,7 @@ public function __call($method, $arguments) { $name = Str::snake(substr($method, 3)); if(in_array($name, array_keys($this->attributes))) { - $this->attributes[$name] = array_pop($arguments); - - return $this->attributes[$name]; + return $this->__set($name, array_pop($arguments)); } } @@ -460,14 +461,18 @@ public function parseRawFlags($raw_flags) { * * @return void * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws MessageFlagException */ private function parseFlags() { $this->client->openFolder($this->folder_path); $this->flags = FlagCollection::make([]); $sequence_id = $this->getSequenceId(); - $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID); + try { + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID); + } catch (Exceptions\RuntimeException $e) { + throw new MessageFlagException("flag could not be fetched", 0, $e); + } if (isset($flags[$sequence_id])) { $this->parseRawFlags($flags[$sequence_id]); @@ -481,14 +486,18 @@ private function parseFlags() { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\MessageContentFetchingException * @throws InvalidMessageDateException - * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function parseBody() { $this->client->openFolder($this->folder_path); $sequence_id = $this->getSequenceId(); - $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); + try { + $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); + } catch (Exceptions\RuntimeException $e) { + throw new MessageContentFetchingException("failed to fetch content", 0); + } if (!isset($contents[$sequence_id])) { throw new MessageContentFetchingException("no content found", 0); } @@ -523,14 +532,11 @@ public function peek(){ * * @return $this * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\RuntimeException * @throws InvalidMessageDateException * @throws MessageContentFetchingException */ public function parseRawBody($raw_body) { $this->structure = new Structure($raw_body, $this->header); - $this->fetchStructure($this->structure); return $this; @@ -555,7 +561,6 @@ private function fetchStructure($structure) { * @param Part $part */ private function fetchPart(Part $part) { - if ($part->isAttachment()) { $this->fetchAttachment($part); }else{ @@ -590,7 +595,6 @@ private function fetchPart(Part $part) { * @param Part $part */ protected function fetchAttachment($part) { - $oAttachment = new Attachment($this, $part); if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { @@ -928,8 +932,8 @@ public function move($folder_path, $expunge = false) { * * @return bool * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function delete($expunge = true) { $status = $this->setFlag("Deleted"); @@ -947,8 +951,8 @@ public function delete($expunge = true) { * * @return bool * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function restore($expunge = true) { $status = $this->unsetFlag("Deleted"); @@ -966,14 +970,18 @@ public function restore($expunge = true) { * * @return bool * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws MessageFlagException * @throws Exceptions\EventNotFoundException */ public function setFlag($flag) { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID); + try { + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID); + } catch (Exceptions\RuntimeException $e) { + throw new MessageFlagException("flag could not be set", 0, $e); + } $this->parseFlags(); $event = $this->getEvent("flag", "new"); @@ -988,15 +996,19 @@ public function setFlag($flag) { * * @return bool * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function unsetFlag($flag) { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID); + try { + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID); + } catch (Exceptions\RuntimeException $e) { + throw new MessageFlagException("flag could not be removed", 0, $e); + } $this->parseFlags(); $event = $this->getEvent("flag", "deleted"); diff --git a/src/Query/Query.php b/src/Query/Query.php index b3e67973..f4168858 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -279,6 +279,7 @@ public function get() { * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException */ public function getMessage($uid, $msglist = null, $sequence = null){ return new Message($uid, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags(), $sequence ? $sequence : $this->sequence); @@ -296,6 +297,7 @@ public function getMessage($uid, $msglist = null, $sequence = null){ * @throws MessageHeaderFetchingException * @throws RuntimeException * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException */ public function getMessageByMsgn($msgn, $msglist = null){ return $this->getMessage($msgn, $msglist, IMAP::ST_MSGN); @@ -312,6 +314,7 @@ public function getMessageByMsgn($msgn, $msglist = null){ * @throws MessageHeaderFetchingException * @throws RuntimeException * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException */ public function getMessageByUid($uid){ return $this->getMessage($uid, null, IMAP::ST_UID); From 56f40a9d75001b39de2db947a78ee45c8c7217ae Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:06:37 +0100 Subject: [PATCH 152/600] Folder referral typo fixed --- CHANGELOG.md | 8 +++++++- src/Folder.php | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e0d00c..40952343 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Get partial overview when `IMAP::ST_UID` is set #74 - Unnecessary "'" removed from address names +- Folder referral typo fixed ### Added - Configurable supported default flags added @@ -20,9 +21,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - New Exception `MessageFlagException::class` added ### Affected Classes -- [Message::class](src/Message.php) +- [Folder::class](src/Folder.php) - [Header::class](src/Header.php) +- [Message::class](src/Message.php) - [Address::class](src/Address.php) +- [Query::class](src/Query/Query.php) - [Attribute::class](src/Attribute.php) ### Breaking changes @@ -30,6 +33,9 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - The formal address object "from", "to", etc now consists of an `Address::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - When fetching or manipulating message flags a `MessageFlagException::class` exception can be thrown if a runtime error occurs +- Learn more about the new `Attribute` class here: [www.php-imap.com/api/attribute](https://www.php-imap.com/api/attribute) +- Learn more about the new `Address` class here: [www.php-imap.com/api/address](https://www.php-imap.com/api/address) +- Folder attribute "referal" is now "referral" ## [2.3.1] - 2020-12-30 ### Fixed diff --git a/src/Folder.php b/src/Folder.php index 21bbfefb..18f00a82 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -108,7 +108,7 @@ class Folder { * * @var boolean */ - public $referal; + public $referral; /** * Folder constructor. @@ -214,7 +214,7 @@ protected function parseAttributes($attributes) { $this->no_inferiors = in_array('\NoInferiors', $attributes) ? true : false; $this->no_select = in_array('\NoSelect', $attributes) ? true : false; $this->marked = in_array('\Marked', $attributes) ? true : false; - $this->referal = in_array('\Referal', $attributes) ? true : false; + $this->referral = in_array('\Referral', $attributes) ? true : false; $this->has_children = in_array('\HasChildren', $attributes) ? true : false; } From d9711e09b7f1eadef243d7e714eb9429f4866454 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:07:57 +0100 Subject: [PATCH 153/600] new mail fetching logic moved into a new method --- src/Message.php | 64 +++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/Message.php b/src/Message.php index 965b9d24..f04d2e97 100755 --- a/src/Message.php +++ b/src/Message.php @@ -845,6 +845,7 @@ public function thread($sent_folder = null, &$thread = null, $folder = null){ * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function copy($folder_path, $expunge = false) { $this->client->openFolder($folder_path); @@ -858,21 +859,7 @@ public function copy($folder_path, $expunge = false) { $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { - if($expunge) $this->client->expunge(); - - $this->client->openFolder($folder->path); - - if ($this->sequence === IMAP::ST_UID) { - $sequence_id = $next_uid; - }else{ - $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid); - } - - $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); - $event = $this->getEvent("message", "copied"); - $event::dispatch($this, $message); - - return $message; + return $this->fetchNewMail($folder, $next_uid, "copied", $expunge); } } @@ -892,6 +879,7 @@ public function copy($folder_path, $expunge = false) { * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException * @throws Exceptions\EventNotFoundException + * @throws MessageFlagException */ public function move($folder_path, $expunge = false) { $this->client->openFolder($folder_path); @@ -905,25 +893,45 @@ public function move($folder_path, $expunge = false) { $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { - if($expunge) $this->client->expunge(); + return $this->fetchNewMail($folder, $next_uid, "moved", $expunge); + } + } - $this->client->openFolder($folder->path); + return null; + } - if ($this->sequence === IMAP::ST_UID) { - $sequence_id = $next_uid; - }else{ - $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid); - } + /** + * Fetch a new message and fire a given event + * @param Folder $folder + * @param int $next_uid + * @param string $event + * @param boolean $expunge + * + * @return mixed + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\EventNotFoundException + * @throws Exceptions\RuntimeException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + */ + protected function fetchNewMail($folder, $next_uid, $event, $expunge){ + if($expunge) $this->client->expunge(); - $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); - $event = $this->getEvent("message", "moved"); - $event::dispatch($this, $message); + $this->client->openFolder($folder->path); - return $message; - } + if ($this->sequence === IMAP::ST_UID) { + $sequence_id = $next_uid; + }else{ + $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid); } - return null; + $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); + $event = $this->getEvent("message", $event); + $event::dispatch($this, $message); + + return $message; } /** From 92912aee6f785ab104b055699190aa5d92bcec6b Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:08:35 +0100 Subject: [PATCH 154/600] mail fetching for a thread moved into a new method --- src/Message.php | 68 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/src/Message.php b/src/Message.php index f04d2e97..40529504 100755 --- a/src/Message.php +++ b/src/Message.php @@ -799,39 +799,59 @@ public function thread($sent_folder = null, &$thread = null, $folder = null){ } $thread->push($this); - $folder->query()->inReplyTo($this->message_id) - ->setFetchBody($this->getFetchBodyOption()) - ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ - /** @var Message $message */ - $message->thread($sent_folder, $thread, $folder); - }); - $sent_folder->query()->inReplyTo($this->message_id) - ->setFetchBody($this->getFetchBodyOption()) - ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ - /** @var Message $message */ - $message->thread($sent_folder, $thread, $folder); - }); + $this->fetchThreadByInReplyTo($thread, $this->message_id, $folder, $folder, $sent_folder); + $this->fetchThreadByInReplyTo($thread, $this->message_id, $sent_folder, $folder, $sent_folder); if (is_array($this->in_reply_to)) { foreach($this->in_reply_to as $in_reply_to) { - $folder->query()->messageId($in_reply_to) - ->setFetchBody($this->getFetchBodyOption()) - ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ - /** @var Message $message */ - $message->thread($sent_folder, $thread, $folder); - }); - $sent_folder->query()->messageId($in_reply_to) - ->setFetchBody($this->getFetchBodyOption()) - ->leaveUnread()->get()->each(function($message) use(&$thread, $folder, $sent_folder){ - /** @var Message $message */ - $message->thread($sent_folder, $thread, $folder); - }); + $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder); + $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder); } } return $thread; } + /** + * Fetch a partial thread by message id + * @param MessageCollection $thread + * @param string $in_reply_to + * @param Folder $primary_folder + * @param Folder $secondary_folder + * @param Folder $sent_folder + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\GetMessagesFailedException + */ + protected function fetchThreadByInReplyTo(&$thread, $in_reply_to, $primary_folder, $secondary_folder, $sent_folder){ + $primary_folder->query()->inReplyTo($in_reply_to) + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ + /** @var Message $message */ + $message->thread($sent_folder, $thread, $secondary_folder); + }); + } + + /** + * Fetch a partial thread by message id + * @param MessageCollection $thread + * @param string $message_id + * @param Folder $primary_folder + * @param Folder $secondary_folder + * @param Folder $sent_folder + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\GetMessagesFailedException + */ + protected function fetchThreadByMessageId(&$thread, $message_id, $primary_folder, $secondary_folder, $sent_folder){ + $primary_folder->query()->messageId($message_id) + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ + /** @var Message $message */ + $message->thread($sent_folder, $thread, $secondary_folder); + }); + } + /** * Copy the current Messages to a mailbox * @param string $folder_path From d77b64f884d9f891e46fe3c87fbb4ff430a698a0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:16:23 +0100 Subject: [PATCH 155/600] New method `Message::setSequenceId($id)` added --- CHANGELOG.md | 3 ++- src/Message.php | 34 +++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40952343..8a1e6f8f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Alias `Message::removeFlag()` for `Message::unsetFlag()` added - Alias `Message::flags()` for `Message::getFlags()` added - New Exception `MessageFlagException::class` added +- New method `Message::setSequenceId($id)` added ### Affected Classes - [Folder::class](src/Folder.php) @@ -35,7 +36,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - When fetching or manipulating message flags a `MessageFlagException::class` exception can be thrown if a runtime error occurs - Learn more about the new `Attribute` class here: [www.php-imap.com/api/attribute](https://www.php-imap.com/api/attribute) - Learn more about the new `Address` class here: [www.php-imap.com/api/address](https://www.php-imap.com/api/address) -- Folder attribute "referal" is now "referral" +- Folder attribute "referal" is now called "referral" ## [2.3.1] - 2020-12-30 ### Fixed diff --git a/src/Message.php b/src/Message.php index 40529504..8cf50550 100755 --- a/src/Message.php +++ b/src/Message.php @@ -224,14 +224,7 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul $this->client = $client; $this->client->openFolder($this->folder_path); - if ($this->sequence === IMAP::ST_UID) { - $this->uid = $uid; - $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid); - }else{ - $this->msgn = $uid; - $this->uid = $this->client->getConnection()->getUid($this->msgn); - } - $this->msglist = $msglist; + $this->setSequenceId($uid, $msglist); if ($this->fetch_options == IMAP::FT_PEEK) { $this->parseFlags(); @@ -290,13 +283,7 @@ public static function make($uid, $msglist, Client $client, $raw_header, $raw_bo $instance->setAttachments(AttachmentCollection::make([])); $instance->setClient($client); - - if ($instance->getSequence() === IMAP::ST_UID) { - $instance->setUid($uid); - $instance->setMsglist($msglist); - }else{ - $instance->setMsgn($uid, $msglist); - } + $instance->setSequenceId($uid, $msglist); $instance->parseRawHeader($raw_header); $instance->parseRawFlags($raw_flags); @@ -1384,4 +1371,21 @@ public function getSequence(){ public function getSequenceId(){ return $this->sequence === IMAP::ST_UID ? $this->uid : $this->msgn; } + + /** + * Set the sequence id + * @param $uid + * @param null $msglist + * + * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException + */ + public function setSequenceId($uid, $msglist = null){ + if ($this->getSequence() === IMAP::ST_UID) { + $this->setUid($uid); + $this->setMsglist($msglist); + }else{ + $this->setMsgn($uid, $msglist); + } + } } From e60d4a6f3ef96b17adea6bdd8fff12698768c5bb Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:28:14 +0100 Subject: [PATCH 156/600] Missing throw tags added --- src/Client.php | 21 ++++++++++++++++++--- src/Folder.php | 5 +++++ src/Message.php | 19 ++++++++++++++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Client.php b/src/Client.php index fc568804..31b9d919 100755 --- a/src/Client.php +++ b/src/Client.php @@ -37,7 +37,7 @@ class Client { /** * Connection resource * - * @var boolean|Protocol + * @var boolean|Protocol|ProtocolInterface */ public $connection = false; @@ -340,7 +340,13 @@ public function connect() { $this->connection->setProtocol($protocol); } - $this->connection->connect($this->host, $this->port); + try { + $this->connection->connect($this->host, $this->port); + } catch (\ErrorException $e) { + throw new ConnectionFailedException("connection setup failed", 0, $e); + } catch (Exceptions\RuntimeException $e) { + throw new ConnectionFailedException("connection setup failed", 0, $e); + } $this->authenticate(); return $this; @@ -387,6 +393,7 @@ public function disconnect() { * @return mixed * @throws ConnectionFailedException * @throws FolderFetchingException + * @throws Exceptions\RuntimeException */ public function getFolder($folder_name, $delimiter = null) { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) @@ -407,6 +414,7 @@ public function getFolder($folder_name, $delimiter = null) { * @return mixed * @throws ConnectionFailedException * @throws FolderFetchingException + * @throws Exceptions\RuntimeException */ public function getFolderByName($folder_name) { return $this->getFolders(false)->where("name", $folder_name)->first(); @@ -419,6 +427,7 @@ public function getFolderByName($folder_name) { * @return mixed * @throws ConnectionFailedException * @throws FolderFetchingException + * @throws Exceptions\RuntimeException */ public function getFolderByPath($folder_path) { return $this->getFolders(false)->where("path", $folder_path)->first(); @@ -428,12 +437,13 @@ public function getFolderByPath($folder_path) { * Get folders list. * If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array. * - * @param boolean $hierarchical + * @param boolean $hierarchical * @param string|null $parent_folder * * @return FolderCollection * @throws ConnectionFailedException * @throws FolderFetchingException + * @throws Exceptions\RuntimeException */ public function getFolders($hierarchical = true, $parent_folder = null) { $this->checkConnection(); @@ -469,6 +479,7 @@ public function getFolders($hierarchical = true, $parent_folder = null) { * * @return mixed * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function openFolder($folder, $force_select = false) { if ($this->active_folder == $folder && $this->isConnected() && $force_select === false) { @@ -488,6 +499,7 @@ public function openFolder($folder, $force_select = false) { * @throws ConnectionFailedException * @throws FolderFetchingException * @throws Exceptions\EventNotFoundException + * @throws Exceptions\RuntimeException */ public function createFolder($folder, $expunge = true) { $this->checkConnection(); @@ -510,6 +522,7 @@ public function createFolder($folder, $expunge = true) { * * @return false|object * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function checkFolder($folder) { $this->checkConnection(); @@ -530,6 +543,7 @@ public function getFolderPath(){ * * @return array * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function getQuota() { $this->checkConnection(); @@ -553,6 +567,7 @@ public function getQuotaRoot($quota_root = 'INBOX') { * * @return bool * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function expunge() { $this->checkConnection(); diff --git a/src/Folder.php b/src/Folder.php index 18f00a82..21909ff2 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -137,6 +137,7 @@ public function __construct(Client $client, $folder_name, $delimiter, $attribute * * @return WhereQuery * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function query($charset = 'UTF-8'){ $this->getClient()->checkConnection(); @@ -148,6 +149,7 @@ public function query($charset = 'UTF-8'){ /** * @inheritdoc self::query($charset = 'UTF-8') * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function search($charset = 'UTF-8'){ return $this->query($charset); @@ -156,6 +158,7 @@ public function search($charset = 'UTF-8'){ /** * @inheritdoc self::query($charset = 'UTF-8') * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function messages($charset = 'UTF-8'){ return $this->query($charset); @@ -247,6 +250,7 @@ public function move($new_name, $expunge = true) { * * @return array * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function overview($sequence = null){ $this->client->openFolder($this->path); @@ -332,6 +336,7 @@ public function subscribe() { * * @return bool * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function unsubscribe() { $this->client->openFolder($this->path); diff --git a/src/Message.php b/src/Message.php index 8cf50550..ee228e9d 100755 --- a/src/Message.php +++ b/src/Message.php @@ -449,6 +449,7 @@ public function parseRawFlags($raw_flags) { * @return void * @throws Exceptions\ConnectionFailedException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ private function parseFlags() { $this->client->openFolder($this->folder_path); @@ -475,6 +476,7 @@ private function parseFlags() { * @throws InvalidMessageDateException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public function parseBody() { $this->client->openFolder($this->folder_path); @@ -502,6 +504,7 @@ public function parseBody() { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public function peek(){ if ($this->fetch_options == IMAP::FT_PEEK) { @@ -521,6 +524,7 @@ public function peek(){ * @throws Exceptions\ConnectionFailedException * @throws InvalidMessageDateException * @throws MessageContentFetchingException + * @throws Exceptions\RuntimeException */ public function parseRawBody($raw_body) { $this->structure = new Structure($raw_body, $this->header); @@ -534,6 +538,7 @@ public function parseRawBody($raw_body) { * @param $structure * * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ private function fetchStructure($structure) { $this->client->openFolder($this->folder_path); @@ -757,6 +762,7 @@ public function getEncoding($structure) { * @return mixed * @throws Exceptions\ConnectionFailedException * @throws Exceptions\FolderFetchingException + * @throws Exceptions\RuntimeException */ public function getFolder(){ return $this->client->getFolder($this->folder_path); @@ -772,6 +778,7 @@ public function getFolder(){ * @throws Exceptions\ConnectionFailedException * @throws Exceptions\FolderFetchingException * @throws Exceptions\GetMessagesFailedException + * @throws Exceptions\RuntimeException */ public function thread($sent_folder = null, &$thread = null, $folder = null){ $thread = $thread ? $thread : MessageCollection::make([]); @@ -809,6 +816,7 @@ public function thread($sent_folder = null, &$thread = null, $folder = null){ * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\GetMessagesFailedException + * @throws Exceptions\RuntimeException */ protected function fetchThreadByInReplyTo(&$thread, $in_reply_to, $primary_folder, $secondary_folder, $sent_folder){ $primary_folder->query()->inReplyTo($in_reply_to) @@ -829,6 +837,7 @@ protected function fetchThreadByInReplyTo(&$thread, $in_reply_to, $primary_folde * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\GetMessagesFailedException + * @throws Exceptions\RuntimeException */ protected function fetchThreadByMessageId(&$thread, $message_id, $primary_folder, $secondary_folder, $sent_folder){ $primary_folder->query()->messageId($message_id) @@ -949,6 +958,7 @@ protected function fetchNewMail($folder, $next_uid, $event, $expunge){ * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public function delete($expunge = true) { $status = $this->setFlag("Deleted"); @@ -968,6 +978,7 @@ public function delete($expunge = true) { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public function restore($expunge = true) { $status = $this->unsetFlag("Deleted"); @@ -987,6 +998,7 @@ public function restore($expunge = true) { * @throws Exceptions\ConnectionFailedException * @throws MessageFlagException * @throws Exceptions\EventNotFoundException + * @throws Exceptions\RuntimeException */ public function setFlag($flag) { $this->client->openFolder($this->folder_path); @@ -1013,6 +1025,7 @@ public function setFlag($flag) { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public function unsetFlag($flag) { $this->client->openFolder($this->folder_path); @@ -1040,6 +1053,7 @@ public function unsetFlag($flag) { * @throws Exceptions\ConnectionFailedException * @throws MessageFlagException * @throws Exceptions\EventNotFoundException + * @throws Exceptions\RuntimeException */ public function addFlag($flag) { return $this->setFlag($flag); @@ -1053,6 +1067,7 @@ public function addFlag($flag) { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\RuntimeException */ public function removeFlag($flag) { return $this->unsetFlag($flag); @@ -1090,6 +1105,7 @@ public function hasAttachments() { * * @return string * @throws Exceptions\ConnectionFailedException + * @throws Exceptions\RuntimeException */ public function getRawBody() { if ($this->raw_body === null) { @@ -1311,8 +1327,9 @@ public function setFlags($flags){ * Set the client * @param $client * - * @throws Exceptions\ConnectionFailedException * @return $this + * @throws Exceptions\RuntimeException + * @throws Exceptions\ConnectionFailedException */ public function setClient($client){ $this->client = $client; From dd2bba78a9860196a91dcc4d8c399cca8226357a Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:51:49 +0100 Subject: [PATCH 157/600] Initial test added --- phpunit.xml.dist => phpunit.xml | 5 ++++- tests/InitialTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) rename phpunit.xml.dist => phpunit.xml (89%) create mode 100644 tests/InitialTest.php diff --git a/phpunit.xml.dist b/phpunit.xml similarity index 89% rename from phpunit.xml.dist rename to phpunit.xml index 8f96011e..bfbb0b09 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml @@ -10,7 +10,7 @@ processIsolation="false" stopOnFailure="false"> - + tests @@ -26,4 +26,7 @@ + + + \ No newline at end of file diff --git a/tests/InitialTest.php b/tests/InitialTest.php new file mode 100644 index 00000000..fb854db4 --- /dev/null +++ b/tests/InitialTest.php @@ -0,0 +1,26 @@ +cm = new ClientManager(); + } + + public function testConfigDefaultAccount() { + $this->assertEquals("default", ClientManager::get("default")); + } +} \ No newline at end of file From ff1a39d4a9a7e7a68a105463cdc31dc610883fb6 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:52:00 +0100 Subject: [PATCH 158/600] method doc updated --- src/Message.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Message.php b/src/Message.php index ee228e9d..316b369e 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1357,7 +1357,7 @@ public function setUid($uid){ /** * Set the message number * @param $msgn - * @param null $msglist + * @param int|null $msglist * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException @@ -1392,7 +1392,7 @@ public function getSequenceId(){ /** * Set the sequence id * @param $uid - * @param null $msglist + * @param int|null $msglist * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException From 515b6e3520582d0873f0d3be589e4a454fc4c784 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 04:58:51 +0100 Subject: [PATCH 159/600] travis test --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index de3d0244..c09157fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ install: - COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction script: - - phpunit + - ./vendor/bin/phpunit notifications: email: From 9e60e0734808c161dc541af53075ad3cb6ef4b0c Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 2 Jan 2021 05:05:41 +0100 Subject: [PATCH 160/600] Depricated travis options removed --- .travis.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index c09157fa..26e564ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: php +os: linux +dist: xenial + php: - 5.6 - 7.0 @@ -9,18 +12,14 @@ php: - 7.4 - hhvm -matrix: +jobs: fast_finish: true allow_failures: - - php: 7.0 - - php: 7.1 - php: 7.3 - php: 7.4 - php: hhvm -sudo: false - -before-install: +before_install: - COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-source --no-interaction --dev install: From 9d7376000b491a19e6e14894046d4b852cb9abb7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 3 Jan 2021 22:05:19 +0100 Subject: [PATCH 161/600] Legacy protocol fixed --- CHANGELOG.md | 1 + src/Connection/Protocols/LegacyProtocol.php | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1e6f8f..15812c5a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Get partial overview when `IMAP::ST_UID` is set #74 - Unnecessary "'" removed from address names - Folder referral typo fixed +- Legacy protocol fixed ### Added - Configurable supported default flags added diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 20adcd51..be4ed5e3 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -53,13 +53,16 @@ public function __destruct() { * @param null $port * @param bool $encryption */ - public function connect($host, $port = null, $encryption = false) { - if ($port === null) { - $port = $encryption == "ssl" ? 995 : 110; + public function connect($host, $port = null) { + if ($this->encryption) { + $encryption = strtolower($this->encryption); + if ($encryption == "ssl") { + $port = $port === null ? 993 : $port; + } } + $port = $port === null ? 143 : $port; $this->host = $host; $this->port = $port; - $this->encryption = $encryption; } /** @@ -83,7 +86,6 @@ public function login($user, $password) { } catch (\ErrorException $e) { $errors = \imap_errors(); $message = $e->getMessage().'. '.implode("; ", (is_array($errors) ? $errors : array())); - throw new AuthFailedException($message); } @@ -549,6 +551,9 @@ public function getQuotaRoot($quota_root = 'INBOX') { * @return LegacyProtocol */ public function setProtocol($protocol) { + if (($pos = strpos($protocol, "legacy")) > 0) { + $protocol = substr($protocol, 0, ($pos + 2) * -1); + } $this->protocol = $protocol; return $this; } From d4f191fdc317c425ffe34dd6c3db543a072d04ef Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 3 Jan 2021 22:06:31 +0100 Subject: [PATCH 162/600] Optional header attributizion option added --- src/Connection/Protocols/ImapProtocol.php | 2 +- src/Header.php | 39 +++++++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 199585aa..4aeebe2e 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1012,7 +1012,7 @@ public function overview($sequence, $uid = false) { } $headers = $this->headers($ids, $rfc = "RFC822", $uid); foreach ($headers as $id => $raw_header) { - $result[$id] = (new Header($raw_header))->getAttributes(); + $result[$id] = (new Header($raw_header, false))->getAttributes(); } return $result; } diff --git a/src/Header.php b/src/Header.php index 653806f3..f1764014 100644 --- a/src/Header.php +++ b/src/Header.php @@ -34,7 +34,7 @@ class Header { /** * Attribute holder * - * @var Attribute[] $attributes + * @var Attribute[]|array $attributes */ protected $attributes = []; @@ -52,15 +52,24 @@ class Header { */ public $fallback_encoding = 'UTF-8'; + /** + * Convert parsed values to attributes + * + * @var bool + */ + protected $attributize = false; + /** * Header constructor. * @param $raw_header + * @param boolean $attributize * * @throws InvalidMessageDateException */ - public function __construct($raw_header) { + public function __construct($raw_header, $attributize = true) { $this->raw = $raw_header; $this->config = ClientManager::get('options'); + $this->attributize = $attributize; $this->parse(); } @@ -69,7 +78,7 @@ public function __construct($raw_header) { * @param string $method * @param array $arguments * - * @return Attribute + * @return Attribute|mixed * @throws MethodNotFoundException */ public function __call($method, $arguments) { @@ -99,7 +108,7 @@ public function __get($name) { * Get a specific header attribute * @param $name * - * @return Attribute|null + * @return Attribute|mixed */ public function get($name) { if(isset($this->attributes[$name])) { @@ -119,7 +128,21 @@ public function get($name) { */ public function set($name, $value, $strict = false) { if(isset($this->attributes[$name]) && $strict === false) { - $this->attributes[$name]->add($value, true); + if ($this->attributize) { + $this->attributes[$name]->add($value, true); + }else{ + if(isset($this->attributes[$name])) { + if (is_array($this->attributes[$name]) == false) { + $this->attributes[$name] = [$this->attributes[$name], $value]; + }else{ + $this->attributes[$name][] = $value; + } + }else{ + $this->attributes[$name] = $value; + } + } + }elseif($this->attributize == false){ + $this->attributes[$name] = $value; }else{ $this->attributes[$name] = new Attribute($name, $value); } @@ -566,7 +589,11 @@ private function parseAddresses($list) { */ private function extractHeaderExtensions(){ foreach ($this->attributes as $key => $value) { - $value = (string)$value; + if (is_array($value)) { + $value = implode(", ", $value); + }else{ + $value = (string)$value; + } // Only parse strings and don't parse any attributes like the user-agent if (in_array($key, ["user_agent"]) === false) { if (($pos = strpos($value, ";")) !== false){ From eb920a47788b6ac0821119b4e9beeff3a7389515 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 3 Jan 2021 22:07:43 +0100 Subject: [PATCH 163/600] Treat message collection keys always as strings --- CHANGELOG.md | 2 ++ src/Query/Query.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15812c5a..9bcdf1dd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Unnecessary "'" removed from address names - Folder referral typo fixed - Legacy protocol fixed +- Treat message collection keys always as strings ### Added - Configurable supported default flags added @@ -21,6 +22,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Alias `Message::flags()` for `Message::getFlags()` added - New Exception `MessageFlagException::class` added - New method `Message::setSequenceId($id)` added +- Optional Header attributizion option added ### Affected Classes - [Folder::class](src/Folder.php) diff --git a/src/Query/Query.php b/src/Query/Query.php index f4168858..f9d8994e 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -255,7 +255,7 @@ public function get() { break; } - $messages->put($message_key, $message); + $messages->put("$message_key", $message); $msglist++; } } From 6120e702cca9460c2d498f2eff3df944d2cd8167 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 3 Jan 2021 22:28:51 +0100 Subject: [PATCH 164/600] Additional alias methods added --- src/Address.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Address.php b/src/Address.php index 3eebcf5e..a91dc15e 100644 --- a/src/Address.php +++ b/src/Address.php @@ -75,4 +75,22 @@ public function __serialize(){ "full" => $this->full, ]; } + + /** + * Convert instance to array + * + * @return array + */ + public function toArray(){ + return $this->__serialize(); + } + + /** + * Return the stringified attribute + * + * @return string + */ + public function toString(){ + return $this->__toString(); + } } \ No newline at end of file From 6c2c54f9f99a1f5fff99f98bfacc2dcd706c9c9a Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 3 Jan 2021 22:52:25 +0100 Subject: [PATCH 165/600] Version information updated --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bcdf1dd..ed01c205 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.4.0] - 2021-01-03 +### Fixed - Get partial overview when `IMAP::ST_UID` is set #74 - Unnecessary "'" removed from address names - Folder referral typo fixed From a3e87ac4b93b420ff3497506bb5cb3a65bda4403 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 6 Jan 2021 14:22:32 +0100 Subject: [PATCH 166/600] Debug line position fixed --- CHANGELOG.md | 2 +- src/Connection/Protocols/ImapProtocol.php | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed01c205..572b6702 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Debug line position fixed ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4aeebe2e..6dee4241 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -233,6 +233,7 @@ public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false) { } else { $tokens = $line; } + if ($this->debug) echo "<< ".$line."\n"; // if tag is wanted tag we might be at the end of a multiline response return $tag == $wantedTag; @@ -257,11 +258,6 @@ public function readResponse($tag, $dontParse = false) { // First two chars are still needed for the response code $tokens = [substr($tokens, 0, 2)]; } - if (is_array($lines)){ - if ($this->debug) echo "<< ".json_encode($lines)."\n"; - }else{ - if ($this->debug) echo "<< ".$lines."\n"; - } // last line has response code if ($tokens[0] == 'OK') { From 46c05527ef2c7d4f0f3b5532d62a2a25fdb45d32 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 6 Jan 2021 14:23:28 +0100 Subject: [PATCH 167/600] Handle incomplete address to string conversion #83 --- CHANGELOG.md | 1 + src/Address.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 572b6702..1e4eab64 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Debug line position fixed +- Handle incomplete address to string conversion #83 ### Added - NaN diff --git a/src/Address.php b/src/Address.php index a91dc15e..63cf1c20 100644 --- a/src/Address.php +++ b/src/Address.php @@ -58,7 +58,7 @@ public function __construct($object) { * @return string */ public function __toString() { - return $this->full; + return $this->full ? $this->full : ""; } /** From 356432e631e1adac495d48f832444806a2d300c3 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 6 Jan 2021 14:24:40 +0100 Subject: [PATCH 168/600] Version information updated --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e4eab64..240c1049 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Debug line position fixed -- Handle incomplete address to string conversion #83 +- NaN ### Added - NaN @@ -18,6 +17,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [2.4.1] - 2021-01-06 +### Fixed +- Debug line position fixed +- Handle incomplete address to string conversion #83 + +### Affected Classes +- [Address::class](src/Address.php) + ## [2.4.0] - 2021-01-03 ### Fixed - Get partial overview when `IMAP::ST_UID` is set #74 From a057f2e8afcc17f671e76bf1eaf79d47e7e95374 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 6 Jan 2021 15:12:15 +0100 Subject: [PATCH 169/600] Configured message key gets overwritten by the first fetched message #84 --- CHANGELOG.md | 4 ++-- src/Query/Query.php | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 240c1049..fd95a9d8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Configured message key gets overwritten by the first fetched message #84 ### Added - NaN ### Affected Classes -- NaN +- [Query::class](src/Query/Query.php) ### Breaking changes - NaN diff --git a/src/Query/Query.php b/src/Query/Query.php index f9d8994e..6701a71f 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -242,20 +242,20 @@ public function get() { $message = Message::make($uid, $msglist, $this->getClient(), $raw_header, $raw_content, $raw_flag, $this->getFetchOptions(), $this->sequence); switch ($message_key){ case 'number': - $message_key = $message->getMessageNo(); + $key = $message->getMessageNo(); break; case 'list': - $message_key = $msglist; + $key = $msglist; break; case 'uid': - $message_key = $message->getUid(); + $key = $message->getUid(); break; default: - $message_key = $message->getMessageId(); + $key = $message->getMessageId(); break; } - $messages->put("$message_key", $message); + $messages->put("$key", $message); $msglist++; } } From 597d01863e08eedb1ee11a6b46a93b0ddc0c8726 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 6 Jan 2021 15:17:29 +0100 Subject: [PATCH 170/600] Hotfix merge --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd95a9d8..b28ea749 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Configured message key gets overwritten by the first fetched message #84 +- NaN ### Added - NaN ### Affected Classes -- [Query::class](src/Query/Query.php) +- NaN ### Breaking changes - NaN @@ -21,9 +21,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Debug line position fixed - Handle incomplete address to string conversion #83 +- Configured message key gets overwritten by the first fetched message #84 ### Affected Classes - [Address::class](src/Address.php) +- [Query::class](src/Query/Query.php) ## [2.4.0] - 2021-01-03 ### Fixed From bb096283143885fac2cb417465ee34512a25a454 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 9 Jan 2021 02:05:52 +0100 Subject: [PATCH 171/600] Attachment::save() return error 'A facade root has not been set' #87 --- CHANGELOG.md | 4 ++-- src/Attachment.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b28ea749..fbab6267 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Attachment::save() return error 'A facade root has not been set' #87 ### Added - NaN ### Affected Classes -- NaN +- [Attachment::class](src/Attachment.php) ### Breaking changes - NaN diff --git a/src/Attachment.php b/src/Attachment.php index 135d9e97..ac2552c3 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -13,7 +13,6 @@ namespace Webklex\PHPIMAP; use Illuminate\Support\Str; -use Illuminate\Support\Facades\File; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; @@ -246,7 +245,7 @@ protected function fetch() { public function save($path, $filename = null) { $filename = $filename ?: $this->getName(); - return File::put($path.$filename, $this->getContent()) !== false; + return file_put_contents($path.$filename, $this->getContent()) !== false; } /** From 207d256cd3cca35b51d239ae57b96690b9062940 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 9 Jan 2021 02:10:32 +0100 Subject: [PATCH 172/600] Unused dependencies removed --- CHANGELOG.md | 3 +++ src/Address.php | 6 ------ src/Attribute.php | 6 ------ 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbab6267..e91bb7fb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,15 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Attachment::save() return error 'A facade root has not been set' #87 +- Unused dependencies removed ### Added - NaN ### Affected Classes - [Attachment::class](src/Attachment.php) +- [Address::class](src/Address.php) +- [Attribute::class](src/Attribute.php) ### Breaking changes - NaN diff --git a/src/Address.php b/src/Address.php index 63cf1c20..644158dd 100644 --- a/src/Address.php +++ b/src/Address.php @@ -12,12 +12,6 @@ namespace Webklex\PHPIMAP; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\File; -use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; -use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; -use Webklex\PHPIMAP\Support\Masks\AttachmentMask; - /** * Class Address * diff --git a/src/Attribute.php b/src/Attribute.php index 15fb65de..e57b2c9d 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -12,12 +12,6 @@ namespace Webklex\PHPIMAP; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\File; -use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; -use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; -use Webklex\PHPIMAP\Support\Masks\AttachmentMask; - /** * Class Attribute * From 74580dbb46020cf5d082bd896951287d9546c726 Mon Sep 17 00:00:00 2001 From: Menno van Hout Date: Sat, 9 Jan 2021 20:05:45 +0100 Subject: [PATCH 173/600] Bug getBoundary() for PHP 8 and CASE error. (#88) * Fix PHP 8 error that changes null back in to an empty string. * Fix regex to be case insensetive --- src/Structure.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Structure.php b/src/Structure.php index a2d7d7c9..fb6a2d44 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -100,7 +100,12 @@ public function findContentType(){ * Determine the message content type */ public function getBoundary(){ - $boundary = $this->header->find("/boundary=\"?([^\"]*)[\";\s]/"); + $boundary = $this->header->find("/boundary=\"?([^\"]*)[\";\s]/i"); + + if ($boundary === null) { + return null; + } + return str_replace('"', '', $boundary); } From cf999b3642f6846074925800e42482c9cbfd6ab1 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 9 Jan 2021 20:07:46 +0100 Subject: [PATCH 174/600] Changelog and doc updated --- CHANGELOG.md | 3 +++ src/Structure.php | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e91bb7fb..62707441 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Attachment::save() return error 'A facade root has not been set' #87 - Unused dependencies removed +- Fix PHP 8 error that changes null back in to an empty string. +- Fix regex to be case insensitive ### Added - NaN @@ -16,6 +18,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Attachment::class](src/Attachment.php) - [Address::class](src/Address.php) - [Attribute::class](src/Attribute.php) +- [Structure::class](src/Structure.php) ### Breaking changes - NaN diff --git a/src/Structure.php b/src/Structure.php index fb6a2d44..2daa3742 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -98,6 +98,8 @@ public function findContentType(){ /** * Determine the message content type + * + * @return string|null */ public function getBoundary(){ $boundary = $this->header->find("/boundary=\"?([^\"]*)[\";\s]/i"); From 7ee17acf5a1955e7789f191e1b36afecfc952837 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 9 Jan 2021 20:14:31 +0100 Subject: [PATCH 175/600] Travis link updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11f11da9..65012298 100755 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Latest Version on Packagist][ico-version]][link-packagist] [![Software License][ico-license]][link-license] -[![Build Status][ico-build]][link-scrutinizer] +[![Build Status][ico-travis]][link-scrutinizer] [![Total Downloads][ico-downloads]][link-downloads] [![Hits][ico-hits]][link-hits] From 53dc02344979f152d6cde4ad2ad99f186d3f1ebb Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 9 Jan 2021 20:38:47 +0100 Subject: [PATCH 176/600] Version information updated --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62707441..541081bc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,23 +6,30 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.4.2] - 2021-01-09 +### Fixed - Attachment::save() return error 'A facade root has not been set' #87 - Unused dependencies removed - Fix PHP 8 error that changes null back in to an empty string. - Fix regex to be case insensitive -### Added -- NaN - ### Affected Classes - [Attachment::class](src/Attachment.php) - [Address::class](src/Address.php) - [Attribute::class](src/Attribute.php) - [Structure::class](src/Structure.php) -### Breaking changes -- NaN - ## [2.4.1] - 2021-01-06 ### Fixed - Debug line position fixed From 54375296a53e01308d7a0f7f77f667fc23237214 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 9 Jan 2021 20:41:19 +0100 Subject: [PATCH 177/600] Credits added --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 541081bc..0cd11825 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Attachment::save() return error 'A facade root has not been set' #87 - Unused dependencies removed -- Fix PHP 8 error that changes null back in to an empty string. -- Fix regex to be case insensitive +- Fix PHP 8 error that changes null back in to an empty string. #88 (thanks @mennovanhout) +- Fix regex to be case insensitive #88 (thanks @mennovanhout) ### Affected Classes - [Attachment::class](src/Attachment.php) From 3540ad6d23385b18596c541a625d0518095b6e19 Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 19 Jan 2021 10:33:16 +0100 Subject: [PATCH 178/600] Timeout handling improved --- CHANGELOG.md | 4 ++-- src/Client.php | 12 +++++++++--- src/Connection/Protocols/Protocol.php | 1 + src/config/imap.php | 3 ++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd11825..b67a25bb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Timeout handling improved ### Added - NaN ### Affected Classes -- NaN +- [Client::class](src/Client.php) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index 31b9d919..59d63a2b 100755 --- a/src/Client.php +++ b/src/Client.php @@ -88,6 +88,12 @@ class Client { 'password' => null, ]; + /** + * Connection timeout + * @var int $timeout + */ + public $timeout; + /** * Account username/ * @@ -149,7 +155,8 @@ class Client { 'request_fulluri' => false, 'username' => null, 'password' => null, - ] + ], + "timeout" => 30 ]; /** @@ -325,9 +332,8 @@ public function connect() { $protocol = strtolower($this->protocol); if ($protocol == "imap") { - $timeout = $this->connection !== false ? $this->connection->getConnectionTimeout() : null; $this->connection = new ImapProtocol($this->validate_cert, $this->encryption); - $this->connection->setConnectionTimeout($timeout); + $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); }else{ if (extension_loaded('imap') === false) { diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 60081453..f29be0f1 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -191,6 +191,7 @@ protected function createStream($transport, $host, $port, $timeout) { STREAM_CLIENT_CONNECT, stream_context_create($this->defaultSocketOptions($transport)) ); + stream_set_timeout($stream, $timeout); if (!$stream) { throw new ConnectionFailedException("Failed to connect to host", 0, $error); diff --git a/src/config/imap.php b/src/config/imap.php index 9147a996..f85f6feb 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -60,7 +60,8 @@ 'request_fulluri' => false, 'username' => null, 'password' => null, - ] + ], + "timeout" => 30 ], /* From 22a33f4dbf7b2d06140a8930ad6043b376a1851a Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 19 Jan 2021 10:33:45 +0100 Subject: [PATCH 179/600] Auto reconnect option added to `Folder::idle()` #89 --- CHANGELOG.md | 3 ++- src/Folder.php | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b67a25bb..bb78d48f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Timeout handling improved ### Added -- NaN +- Auto reconnect option added to `Folder::idle()` #89 ### Affected Classes +- [Folder::class](src/Folder.php) - [Client::class](src/Client.php) ### Breaking changes diff --git a/src/Folder.php b/src/Folder.php index 21909ff2..46db9ae6 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -347,6 +347,7 @@ public function unsubscribe() { * Idle the current connection * @param callable $callback * @param integer $timeout max 1740 seconds - recommended by rfc2177 §3 + * @param boolean $auto_reconnect try to reconnect on connection close * * @throws ConnectionFailedException * @throws Exceptions\InvalidMessageDateException @@ -356,7 +357,7 @@ public function unsubscribe() { * @throws Exceptions\EventNotFoundException * @throws Exceptions\MessageFlagException */ - public function idle(callable $callback, $timeout = 1200) { + public function idle(callable $callback, $timeout = 1200, $auto_reconnect = false) { $this->client->getConnection()->setConnectionTimeout($timeout); $this->client->reconnect(); @@ -387,6 +388,9 @@ public function idle(callable $callback, $timeout = 1200) { if(strpos($e->getMessage(), "connection closed") === false) { throw $e; } + if ($auto_reconnect === true) { + $this->client->reconnect(); + } } } } From de50ce32d8e3cb9445677697161efc380e8d7f9c Mon Sep 17 00:00:00 2001 From: Webklex Date: Tue, 19 Jan 2021 10:34:26 +0100 Subject: [PATCH 180/600] Attachment detection updated #82 #90 --- CHANGELOG.md | 2 ++ src/Part.php | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb78d48f..91683d0b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- Attachment detection updated #82 #90 - Timeout handling improved ### Added @@ -13,6 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Folder::class](src/Folder.php) +- [Part::class](src/Part.php) - [Client::class](src/Client.php) ### Breaking changes diff --git a/src/Part.php b/src/Part.php index 24ec2019..be3c1c41 100644 --- a/src/Part.php +++ b/src/Part.php @@ -299,11 +299,8 @@ private function parseEncoding(){ * @return bool */ public function isAttachment(){ - if ($this->type == IMAP::MESSAGE_TYPE_TEXT && - ($this->ifdisposition == 0 || - (empty($this->disposition) || !in_array(strtolower($this->disposition), ClientManager::get('options.dispositions'))) - ) - ) { + $valid_disposition = in_array(strtolower($this->disposition), ClientManager::get('options.dispositions')); + if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || (empty($this->disposition))) && !$valid_disposition) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "csv", "html"])) && $this->filename == null && $this->name == null) { return false; } From 27cb7e159a065f0948303643975eccbb24e3a833 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 21 Jan 2021 20:18:25 +0100 Subject: [PATCH 181/600] UTF-8 checks added to prevent decoding of unencoded values #76 --- CHANGELOG.md | 2 ++ src/Header.php | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91683d0b..a06ea592 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Attachment detection updated #82 #90 - Timeout handling improved +- Additional utf-8 checks added to prevent decoding of unencoded values #76 ### Added - Auto reconnect option added to `Folder::idle()` #89 @@ -16,6 +17,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Folder::class](src/Folder.php) - [Part::class](src/Part.php) - [Client::class](src/Client.php) +- [Header::class](src/Header.php) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index f1764014..3aef18a0 100644 --- a/src/Header.php +++ b/src/Header.php @@ -406,6 +406,16 @@ public function getEncoding($structure) { return $this->fallback_encoding; } + /** + * Test if a given value is utf-8 encoded + * @param $value + * + * @return bool + */ + private function is_uft8($value) { + return strpos(strtolower($value), '=?utf-8?') === 0; + } + /** * Try to decode a specific header * @param mixed $value @@ -420,9 +430,12 @@ private function decode($value) { $decoder = $this->config['decoder']['message']; if ($value !== null) { + $is_utf8_base = $this->is_uft8($value); + if($decoder === 'utf-8' && extension_loaded('imap')) { $value = \imap_utf8($value); - if (strpos(strtolower($value), '=?utf-8?') === 0) { + $is_utf8_base = $this->is_uft8($value); + if ($is_utf8_base) { $value = mb_decode_mimeheader($value); } if ($this->notDecoded($original_value, $value)) { @@ -433,13 +446,13 @@ private function decode($value) { } } } - }elseif($decoder === 'iconv') { + }elseif($decoder === 'iconv' && $is_utf8_base) { $value = iconv_mime_decode($value); - }else{ + }elseif($is_utf8_base){ $value = mb_decode_mimeheader($value); } - if (strpos(strtolower($value), '=?utf-8?') === 0) { + if ($this->is_uft8($value)) { $value = mb_decode_mimeheader($value); } From 17f7bb147d456e76c438239c59f16b33e188c223 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 21 Jan 2021 20:24:18 +0100 Subject: [PATCH 182/600] Version information updated --- CHANGELOG.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a06ea592..9a1241c6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.4.3] - 2021-01-21 +### Fixed - Attachment detection updated #82 #90 - Timeout handling improved - Additional utf-8 checks added to prevent decoding of unencoded values #76 @@ -19,9 +32,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Client::class](src/Client.php) - [Header::class](src/Header.php) -### Breaking changes -- NaN - ## [2.4.2] - 2021-01-09 ### Fixed - Attachment::save() return error 'A facade root has not been set' #87 From 1beee218d9cae37f5e27f5d2d4b452ddc5d1b166 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 22 Jan 2021 15:37:10 +0100 Subject: [PATCH 183/600] Boundary detection simplified #90 --- CHANGELOG.md | 4 ++-- src/Structure.php | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1241c6..372a502e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Boundary detection simplified #90 ### Added - NaN ### Affected Classes -- NaN +- [Structure::class](src/Structure.php) ### Breaking changes - NaN diff --git a/src/Structure.php b/src/Structure.php index 2daa3742..51f531c8 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -102,13 +102,23 @@ public function findContentType(){ * @return string|null */ public function getBoundary(){ - $boundary = $this->header->find("/boundary=\"?([^\"]*)[\";\s]/i"); + $boundary = $this->header->find("/boundary\=(.*)/i"); if ($boundary === null) { return null; } - return str_replace('"', '', $boundary); + return $this->clearBoundaryString($boundary); + } + + /** + * Remove all unwanted chars from a given boundary + * @param string $str + * + * @return string + */ + private function clearBoundaryString($str) { + return str_replace(['"', '\r', '\n', "\n", "\r", ";", "\s"], "", $str); } /** @@ -131,11 +141,11 @@ public function find_parts(){ if (preg_match("/boundary\=\"?(.*)\"?/", $this->raw, $match) == 1) { if(is_array($match[1])){ foreach($match[1] as $matched){ - $boundaries[] = str_replace('"', '', $matched); + $boundaries[] = $this->clearBoundaryString($matched); } }else{ if(!empty($match[1])) { - $boundaries[] = str_replace('"', '', $match[1]); + $boundaries[] = $this->clearBoundaryString($match[1]); } } } From 0f5e945c4652bc9f763d040683417d9f3b0b612c Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 22 Jan 2021 15:39:17 +0100 Subject: [PATCH 184/600] Prevent potential body overwriting #90 --- CHANGELOG.md | 2 ++ src/Message.php | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 372a502e..cc1e10cc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Boundary detection simplified #90 +- Prevent potential body overwriting #90 ### Added - NaN ### Affected Classes - [Structure::class](src/Structure.php) +- [Message::class](src/Message.php) ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index 316b369e..9e6d244b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -578,7 +578,11 @@ private function fetchPart(Part $part) { $subtype = strtolower($part->subtype); $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; - $this->bodies[$subtype] = $content; + if (isset($this->bodies[$subtype])) { + $this->bodies[$subtype] .= "\n".$content; + }else{ + $this->bodies[$subtype] = $content; + } } } From ab922f32b3972dbd0a7f9b87b81b8ab23233d26d Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 22 Jan 2021 15:40:19 +0100 Subject: [PATCH 185/600] CSV files are no longer regarded as plain body --- CHANGELOG.md | 1 + src/Part.php | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc1e10cc..8d9bba6b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Boundary detection simplified #90 - Prevent potential body overwriting #90 +- CSV files are no longer regarded as plain body ### Added - NaN diff --git a/src/Part.php b/src/Part.php index be3c1c41..a6a67488 100644 --- a/src/Part.php +++ b/src/Part.php @@ -300,8 +300,9 @@ private function parseEncoding(){ */ public function isAttachment(){ $valid_disposition = in_array(strtolower($this->disposition), ClientManager::get('options.dispositions')); + if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || (empty($this->disposition))) && !$valid_disposition) { - if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "csv", "html"])) && $this->filename == null && $this->name == null) { + if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) { return false; } } From b2c749fbfe0671274cdc21053b2dbf0832fbdcf2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 22 Jan 2021 19:57:18 +0100 Subject: [PATCH 186/600] Missing doc added --- src/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Message.php b/src/Message.php index 9e6d244b..a4e97497 100755 --- a/src/Message.php +++ b/src/Message.php @@ -535,7 +535,7 @@ public function parseRawBody($raw_body) { /** * Fetch the Message structure - * @param $structure + * @param Structure $structure * * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException From 5941cee4cde42d88086b99f7d1c2c12cbc136c8e Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 22 Jan 2021 20:06:51 +0100 Subject: [PATCH 187/600] Boundary detection overhauled to support "related" and "alternative" #90 --- CHANGELOG.md | 3 ++ src/Header.php | 25 +++++++++++++ src/Structure.php | 90 +++++++++++++++++++++++++---------------------- 3 files changed, 76 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9bba6b..91fd3e15 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Boundary detection simplified #90 - Prevent potential body overwriting #90 - CSV files are no longer regarded as plain body +- Boundary detection overhauled to support "related" and "alternative" multipart messages #90 ### Added - NaN @@ -16,6 +17,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Structure::class](src/Structure.php) - [Message::class](src/Message.php) +- [Header::class](src/Header.php) +- [Part::class](src/Part.php) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index 3aef18a0..93cad466 100644 --- a/src/Header.php +++ b/src/Header.php @@ -167,6 +167,31 @@ public function find($pattern) { return null; } + /** + * Try to find a boundary if possible + * + * @return string|null + */ + public function getBoundary(){ + $boundary = $this->find("/boundary\=(.*)/i"); + + if ($boundary === null) { + return null; + } + + return $this->clearBoundaryString($boundary); + } + + /** + * Remove all unwanted chars from a given boundary + * @param string $str + * + * @return string + */ + private function clearBoundaryString($str) { + return str_replace(['"', '\r', '\n', "\n", "\r", ";", "\s"], "", $str); + } + /** * Parse the raw headers * diff --git a/src/Structure.php b/src/Structure.php index 51f531c8..a6e65b93 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -86,7 +86,6 @@ protected function parse(){ * Determine the message content type */ public function findContentType(){ - $content_type = $this->header->get("content_type"); $content_type = (is_array($content_type)) ? implode(' ', $content_type) : $content_type; if(stripos($content_type, 'multipart') === 0) { @@ -97,28 +96,51 @@ public function findContentType(){ } /** - * Determine the message content type + * Find all available headers and return the left over body segment + * @var string $context + * @var integer $part_number * - * @return string|null + * @return Part[] + * @throws InvalidMessageDateException */ - public function getBoundary(){ - $boundary = $this->header->find("/boundary\=(.*)/i"); - - if ($boundary === null) { - return null; + private function parsePart($context, $part_number = 0){ + $body = $context; + while (($pos = strpos($body, "\r\n")) > 0) { + $body = substr($body, $pos + 2); } + $headers = substr($context, 0, strlen($body) * -1); + $body = substr($body, 0, -2); - return $this->clearBoundaryString($boundary); + $headers = new Header($headers); + if (($boundary = $headers->getBoundary()) !== null) { + return $this->detectParts($boundary, $body, $part_number); + } + return [new Part($body, $headers, $part_number)]; } /** - * Remove all unwanted chars from a given boundary - * @param string $str + * @param string $boundary + * @param string $context + * @param int $part_number * - * @return string + * @return array + * @throws InvalidMessageDateException */ - private function clearBoundaryString($str) { - return str_replace(['"', '\r', '\n', "\n", "\r", ";", "\s"], "", $str); + private function detectParts($boundary, $context, $part_number = 0){ + $base_parts = explode( $boundary, $context); + $final_parts = []; + foreach($base_parts as $ctx) { + $ctx = substr($ctx, 2); + if ($ctx !== "--" && $ctx != "") { + $parts = $this->parsePart($ctx, $part_number); + foreach ($parts as $part) { + $final_parts[] = $part; + $part_number = $part->part_number; + } + $part_number++; + } + } + return $final_parts; } /** @@ -130,39 +152,23 @@ private function clearBoundaryString($str) { */ public function find_parts(){ if($this->type === IMAP::MESSAGE_TYPE_MULTIPART) { - if (($boundary = $this->getBoundary()) === null) { + if (($boundary = $this->header->getBoundary()) === null) { throw new MessageContentFetchingException("no content found", 0); } - $boundaries = [ - $boundary - ]; - - if (preg_match("/boundary\=\"?(.*)\"?/", $this->raw, $match) == 1) { - if(is_array($match[1])){ - foreach($match[1] as $matched){ - $boundaries[] = $this->clearBoundaryString($matched); - } - }else{ - if(!empty($match[1])) { - $boundaries[] = $this->clearBoundaryString($match[1]); - } - } - } - - $raw_parts = explode( $boundaries[0], str_replace($boundaries, $boundaries[0], $this->raw) ); - $parts = []; - $part_number = 0; - foreach($raw_parts as $part) { - $part = trim(rtrim($part)); - if ($part !== "--") { - $parts[] = new Part($part, null, $part_number); - $part_number++; - } - } - return $parts; + return $this->detectParts($boundary, $this->raw); } return [new Part($this->raw, $this->header)]; } + + /** + * Try to find a boundary if possible + * + * @return string|null + * @Depricated since version 2.4.4 + */ + public function getBoundary(){ + return $this->header->getBoundary(); + } } From 1676bfbc12b2a72f451624bd9bdb90013a9fa70a Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 22 Jan 2021 21:59:30 +0100 Subject: [PATCH 188/600] Version information updated --- CHANGELOG.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91fd3e15..de612e69 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,23 +6,30 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Boundary detection simplified #90 -- Prevent potential body overwriting #90 -- CSV files are no longer regarded as plain body -- Boundary detection overhauled to support "related" and "alternative" multipart messages #90 +- NaN ### Added - NaN +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.4.4] - 2022-01-22 +### Fixed +- Boundary detection simplified #90 +- Prevent potential body overwriting #90 +- CSV files are no longer regarded as plain body +- Boundary detection overhauled to support "related" and "alternative" multipart messages #90 #91 + ### Affected Classes - [Structure::class](src/Structure.php) - [Message::class](src/Message.php) - [Header::class](src/Header.php) - [Part::class](src/Part.php) -### Breaking changes -- NaN - ## [2.4.3] - 2021-01-21 ### Fixed - Attachment detection updated #82 #90 From daa76818f7faa72d05c2416ff29a99602bc055c8 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 23 Jan 2021 23:03:27 +0100 Subject: [PATCH 189/600] Dynamic Attribute access support added --- CHANGELOG.md | 4 ++-- src/Attribute.php | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de612e69..b8b059e4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- Dynamic Attribute access support added (e.g `$message->from[0]`) ### Affected Classes -- NaN +- [Attribute::class](src/Attribute.php) ### Breaking changes - NaN diff --git a/src/Attribute.php b/src/Attribute.php index e57b2c9d..1731911b 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -17,7 +17,7 @@ * * @package Webklex\PHPIMAP */ -class Attribute { +class Attribute implements \ArrayAccess { /** @var string $name */ protected $name; @@ -76,6 +76,51 @@ public function toArray(){ return $this->__serialize(); } + /** + * Determine if a value exists at an offset. + * + * @param mixed $key + * @return bool + */ + public function offsetExists($key) { + return array_key_exists($key, $this->values); + } + + /** + * Get a value at a given offset. + * + * @param mixed $key + * @return mixed + */ + public function offsetGet($key) { + return $this->values[$key]; + } + + /** + * Set the value at a given offset. + * + * @param mixed $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value) { + if (is_null($key)) { + $this->values[] = $value; + } else { + $this->values[$key] = $value; + } + } + + /** + * Unset the value at a given offset. + * + * @param string $key + * @return void + */ + public function offsetUnset($key) { + unset($this->values[$key]); + } + /** * Add one or more values to the attribute * @param array|mixed $value From e920ceda23757c30789e042ad41793195d711eaa Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 25 Jan 2021 18:47:31 +0100 Subject: [PATCH 190/600] Attachment saving filename fixed --- CHANGELOG.md | 3 ++- src/Attachment.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b059e4..40a5b2a2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Attachment saving filename fixed ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) ### Affected Classes - [Attribute::class](src/Attribute.php) +- [Attachment::class](src/Attachment.php) ### Breaking changes - NaN diff --git a/src/Attachment.php b/src/Attachment.php index ac2552c3..4deb2631 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -243,7 +243,7 @@ protected function fetch() { * @return boolean */ public function save($path, $filename = null) { - $filename = $filename ?: $this->getName(); + $filename = $filename ? $filename : $this->getName(); return file_put_contents($path.$filename, $this->getContent()) !== false; } From bdfda44a6491531a40448749542bb1d30fb3443f Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 25 Jan 2021 18:49:13 +0100 Subject: [PATCH 191/600] Unnecessary parameter removed from `Client::getTimeout()` --- CHANGELOG.md | 2 ++ src/Client.php | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a5b2a2..c1564669 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Attachment saving filename fixed +- Unnecessary parameter removed from `Client::getTimeout()` ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) @@ -14,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Attribute::class](src/Attribute.php) - [Attachment::class](src/Attachment.php) +- [Client::class](src/Client.php) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index 59d63a2b..2dfb5f8a 100755 --- a/src/Client.php +++ b/src/Client.php @@ -591,12 +591,11 @@ public function setTimeout($timeout) { } /** - * Get the timeout for a certain operation - * @param $type + * Get the connection timeout * * @return int */ - public function getTimeout($type){ + public function getTimeout(){ return $this->connection->getConnectionTimeout(); } From a2a9c6b452a4f08949f6d80d5629cab4eb13f202 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 25 Jan 2021 18:50:31 +0100 Subject: [PATCH 192/600] Missing encryption variable added --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1564669..1360df4b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Attachment saving filename fixed - Unnecessary parameter removed from `Client::getTimeout()` +- Missing encryption variable added - could have caused problems with unencrypted communications ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 6dee4241..cd8bcf57 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -56,6 +56,7 @@ public function __destruct() { */ public function connect($host, $port = null) { $transport = 'tcp'; + $encryption = ""; if ($this->encryption) { $encryption = strtolower($this->encryption); From e1fb214309a7b8eb00d1c21c5d9a0b16dc61fb09 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 25 Jan 2021 18:51:20 +0100 Subject: [PATCH 193/600] Missing event docs added --- src/Events/Event.php | 16 +++++++++++++++- src/Events/FlagDeletedEvent.php | 16 +++++++++++++++- src/Events/FlagNewEvent.php | 15 +++++++++++++++ src/Events/FolderDeletedEvent.php | 15 +++++++++++++++ src/Events/FolderMovedEvent.php | 15 +++++++++++++++ src/Events/FolderNewEvent.php | 15 +++++++++++++++ src/Events/MessageCopiedEvent.php | 16 +++++++++++++++- src/Events/MessageDeletedEvent.php | 16 +++++++++++++++- src/Events/MessageMovedEvent.php | 15 +++++++++++++++ src/Events/MessageNewEvent.php | 15 +++++++++++++++ src/Events/MessageRestoredEvent.php | 16 +++++++++++++++- 11 files changed, 165 insertions(+), 5 deletions(-) diff --git a/src/Events/Event.php b/src/Events/Event.php index cac69326..921f28b2 100644 --- a/src/Events/Event.php +++ b/src/Events/Event.php @@ -1,8 +1,22 @@ Date: Mon, 25 Jan 2021 18:58:03 +0100 Subject: [PATCH 194/600] Docs and references updated --- src/Attribute.php | 4 +- src/Client.php | 8 +-- src/Connection/Protocols/ImapProtocol.php | 8 +-- .../Protocols/ProtocolInterface.php | 3 ++ src/Folder.php | 6 +-- src/Message.php | 6 ++- src/Query/Query.php | 29 +++++----- src/Query/WhereQuery.php | 53 ++++++++++--------- src/Support/Masks/MessageMask.php | 2 +- src/Support/PaginatedCollection.php | 1 + 10 files changed, 68 insertions(+), 52 deletions(-) diff --git a/src/Attribute.php b/src/Attribute.php index 1731911b..e07d06a3 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -12,12 +12,14 @@ namespace Webklex\PHPIMAP; +use ArrayAccess; + /** * Class Attribute * * @package Webklex\PHPIMAP */ -class Attribute implements \ArrayAccess { +class Attribute implements ArrayAccess { /** @var string $name */ protected $name; diff --git a/src/Client.php b/src/Client.php index 2dfb5f8a..7f02c37c 100755 --- a/src/Client.php +++ b/src/Client.php @@ -12,6 +12,8 @@ namespace Webklex\PHPIMAP; +use ErrorException; +use Exception; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol; use Webklex\PHPIMAP\Connection\Protocols\Protocol; @@ -348,7 +350,7 @@ public function connect() { try { $this->connection->connect($this->host, $this->port); - } catch (\ErrorException $e) { + } catch (ErrorException $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); } catch (Exceptions\RuntimeException $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); @@ -372,7 +374,7 @@ protected function authenticate() { } elseif (!$this->connection->login($this->username, $this->password)) { throw new AuthFailedException(); } - } catch (\Exception $e) { + } catch (Exception $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); } } @@ -581,7 +583,7 @@ public function expunge() { } /** - * Set the imap timeout for a given operation type + * Set the connection timeout * @param $timeout * * @return Protocol diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cd8bcf57..7d7ac654 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -12,8 +12,10 @@ namespace Webklex\PHPIMAP\Connection\Protocols; +use Exception; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Header; @@ -74,7 +76,7 @@ public function connect($host, $port = null) { if ($encryption == "tls") { $this->enableTls(); } - } catch (\Exception $e) { + } catch (Exception $e) { throw new ConnectionFailedException('connection failed', 0, $e); } } @@ -424,7 +426,7 @@ public function logout() { if ($this->stream) { try { $result = $this->requestAndResponse('LOGOUT', [], true); - } catch (\Exception $e) {} + } catch (Exception $e) {} fclose($this->stream); $this->stream = null; } @@ -993,7 +995,7 @@ public function search(array $params, $uid = false) { * * @return array * @throws RuntimeException - * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws InvalidMessageDateException */ public function overview($sequence, $uid = false) { $result = []; diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index eae0ab27..688c6398 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -14,6 +14,7 @@ use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\RuntimeException; /** @@ -291,6 +292,8 @@ public function search(array $params, $uid = false); * @param bool $uid set to true if passing a unique id * * @return array + * @throws RuntimeException + * @throws InvalidMessageDateException */ public function overview($sequence, $uid = false); diff --git a/src/Folder.php b/src/Folder.php index 46db9ae6..3b57b704 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -14,11 +14,8 @@ use Carbon\Carbon; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; -use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; -use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; use Webklex\PHPIMAP\Query\WhereQuery; use Webklex\PHPIMAP\Support\FolderCollection; -use Webklex\PHPIMAP\Support\MessageCollection; use Webklex\PHPIMAP\Traits\HasEvents; /** @@ -32,7 +29,7 @@ class Folder { /** * Client instance * - * @var \Webklex\PHPIMAP\Client + * @var Client */ protected $client; @@ -250,6 +247,7 @@ public function move($new_name, $expunge = true) { * * @return array * @throws ConnectionFailedException + * @throws Exceptions\InvalidMessageDateException * @throws Exceptions\RuntimeException */ public function overview($sequence = null){ diff --git a/src/Message.php b/src/Message.php index a4e97497..e86ad8a5 100755 --- a/src/Message.php +++ b/src/Message.php @@ -13,6 +13,8 @@ namespace Webklex\PHPIMAP; use Carbon\Carbon; +use ReflectionClass; +use ReflectionException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; @@ -257,12 +259,12 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul * @throws Exceptions\EventNotFoundException * @throws InvalidMessageDateException * @throws MessageContentFetchingException - * @throws \ReflectionException + * @throws ReflectionException * @throws MessageFlagException * @throws Exceptions\RuntimeException */ public static function make($uid, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null, $sequence = null){ - $reflection = new \ReflectionClass(self::class); + $reflection = new ReflectionClass(self::class); /** @var self $instance */ $instance = $reflection->newInstanceWithoutConstructor(); diff --git a/src/Query/Query.php b/src/Query/Query.php index 6701a71f..cfb2a9bc 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -13,14 +13,18 @@ namespace Webklex\PHPIMAP\Query; use Carbon\Carbon; +use Exception; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; +use ReflectionException; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; use Webklex\PHPIMAP\Exceptions\RuntimeException; @@ -38,7 +42,7 @@ class Query { /** @var array $query */ protected $query; - /** @var string $raw_query */ + /** @var string $raw_query */ protected $raw_query; /** @var string $charset */ @@ -80,7 +84,7 @@ public function __construct(Client $client, $charset = 'UTF-8') { $this->setClient($client); $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - if(ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); + if (ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); if (ClientManager::get('options.fetch_order') === 'desc') { $this->fetch_order = 'desc'; @@ -98,7 +102,8 @@ public function __construct(Client $client, $charset = 'UTF-8') { /** * Instance boot method for additional functionality */ - protected function boot(){} + protected function boot() { + } /** * Parse a given value @@ -106,29 +111,29 @@ protected function boot(){} * * @return string */ - protected function parse_value($value){ - switch(true){ - case $value instanceof \Carbon\Carbon: + protected function parse_value($value) { + switch (true) { + case $value instanceof Carbon: $value = $value->format($this->date_format); break; } - return (string) $value; + return (string)$value; } /** * Check if a given date is a valid carbon object and if not try to convert it - * @param $date + * @param string|Carbon $date * * @return Carbon * @throws MessageSearchValidationException */ protected function parse_date($date) { - if($date instanceof \Carbon\Carbon) return $date; + if ($date instanceof Carbon) return $date; try { $date = Carbon::parse($date); - } catch (\Exception $e) { + } catch (Exception $e) { throw new MessageSearchValidationException(); } @@ -175,7 +180,7 @@ public function setSequence($sequence) { * @return Collection * @throws GetMessagesFailedException */ - protected function search(){ + protected function search() { $this->generate_query(); try { @@ -388,7 +393,7 @@ public function getClient() { * @return $this */ public function limit($limit, $page = 1) { - if($page >= 1) $this->page = $page; + if ($page >= 1) $this->page = $page; $this->limit = $limit; return $this; diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 3041a746..fea959db 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP\Query; +use Closure; use Illuminate\Support\Str; use Webklex\PHPIMAP\Exceptions\InvalidWhereQueryCriteriaException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; @@ -78,22 +79,22 @@ public function __call($name, $arguments) { $name = Str::camel($name); - if(strtolower(substr($name, 0, 3)) === 'not') { + if (strtolower(substr($name, 0, 3)) === 'not') { $that = $that->whereNot(); $name = substr($name, 3); } - if (strpos(strtolower($name), "where") === false){ - $method = 'where'.ucfirst($name); - }else{ + if (strpos(strtolower($name), "where") === false) { + $method = 'where' . ucfirst($name); + } else { $method = lcfirst($name); } - if(method_exists($this, $method) === true){ + if (method_exists($this, $method) === true) { return call_user_func_array([$that, $method], $arguments); } - throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported'); + throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported'); } /** @@ -108,7 +109,7 @@ protected function validate_criteria($criteria) { if (substr($criteria, 0, 7) === "CUSTOM ") { return substr($criteria, 7); } - if(in_array($criteria, $this->available_criteria) === false) { + if (in_array($criteria, $this->available_criteria) === false) { throw new InvalidWhereQueryCriteriaException(); } @@ -117,26 +118,26 @@ protected function validate_criteria($criteria) { /** * @param mixed $criteria - * @param null $value + * @param null $value * * @return $this * @throws InvalidWhereQueryCriteriaException */ public function where($criteria, $value = null) { - if(is_array($criteria)){ - foreach($criteria as $key => $value){ - if(is_numeric($key)){ + if (is_array($criteria)) { + foreach ($criteria as $key => $value) { + if (is_numeric($key)) { return $this->where($value); } return $this->where($key, $value); } - }else{ + } else { $criteria = $this->validate_criteria($criteria); $value = $this->parse_value($value); - if($value === null || $value === ''){ + if ($value === null || $value === '') { $this->query->push([$criteria]); - }else{ + } else { $this->query->push([$criteria, $value]); } } @@ -145,25 +146,25 @@ public function where($criteria, $value = null) { } /** - * @param \Closure $closure + * @param Closure $closure * * @return $this */ - public function orWhere(\Closure $closure = null) { + public function orWhere(Closure $closure = null) { $this->query->push(['OR']); - if($closure !== null) $closure($this); + if ($closure !== null) $closure($this); return $this; } /** - * @param \Closure $closure + * @param Closure $closure * * @return $this */ - public function andWhere(\Closure $closure = null) { + public function andWhere(Closure $closure = null) { $this->query->push(['AND']); - if($closure !== null) $closure($this); + if ($closure !== null) $closure($this); return $this; } @@ -403,7 +404,7 @@ public function whereUnseen() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereNoXSpam(){ + public function whereNoXSpam() { return $this->where("CUSTOM X-Spam-Flag NO"); } @@ -411,7 +412,7 @@ public function whereNoXSpam(){ * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereIsXSpam(){ + public function whereIsXSpam() { return $this->where("CUSTOM X-Spam-Flag YES"); } @@ -423,7 +424,7 @@ public function whereIsXSpam(){ * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereHeader($header, $value){ + public function whereHeader($header, $value) { return $this->where("CUSTOM HEADER $header $value"); } @@ -434,7 +435,7 @@ public function whereHeader($header, $value){ * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereMessageId($messageId){ + public function whereMessageId($messageId) { return $this->whereHeader("Message-ID", $messageId); } @@ -445,7 +446,7 @@ public function whereMessageId($messageId){ * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereInReplyTo($messageId){ + public function whereInReplyTo($messageId) { return $this->whereHeader("In-Reply-To", $messageId); } @@ -455,7 +456,7 @@ public function whereInReplyTo($messageId){ * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereLanguage($country_code){ + public function whereLanguage($country_code) { return $this->where("Content-Language $country_code"); } } \ No newline at end of file diff --git a/src/Support/Masks/MessageMask.php b/src/Support/Masks/MessageMask.php index 11f3b9ad..d072e8b6 100644 --- a/src/Support/Masks/MessageMask.php +++ b/src/Support/Masks/MessageMask.php @@ -75,7 +75,7 @@ public function getCustomHTMLBody($callback = false) { */ public function getHTMLBodyWithEmbeddedBase64Images() { return $this->getCustomHTMLBody(function($body, $oAttachment){ - /** @var \Webklex\PHPIMAP\Attachment $oAttachment */ + /** @var Attachment $oAttachment */ if ($oAttachment->id) { $body = str_replace('cid:'.$oAttachment->id, 'data:'.$oAttachment->getContentType().';base64, '.base64_encode($oAttachment->getContent()), $body); } diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 4c8bab32..b9764526 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -35,6 +35,7 @@ class PaginatedCollection extends Collection { * @param int $per_page * @param int|null $page * @param string $page_name + * @param boolean $prepaginated * * @return LengthAwarePaginator */ From 1aebda377dfb08adfe836df8d88d8c871cf6e7eb Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 27 Jan 2021 02:18:30 +0100 Subject: [PATCH 195/600] Prefer attachment filename attribute over name attribute #82 --- CHANGELOG.md | 1 + src/Attachment.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1360df4b..1b8809fa 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Attachment saving filename fixed - Unnecessary parameter removed from `Client::getTimeout()` - Missing encryption variable added - could have caused problems with unencrypted communications +- Prefer attachment filename attribute over name attribute #82 ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) diff --git a/src/Attachment.php b/src/Attachment.php index 4deb2631..29c3d442 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -218,11 +218,11 @@ protected function fetch() { $this->size = $this->part->bytes; $this->disposition = $this->part->disposition; - if (($name = $this->part->name) !== null) { - $this->setName($name); - }elseif (($filename = $this->part->filename) !== null) { + if (($filename = $this->part->filename) !== null) { $this->setName($filename); - } else { + } elseif (($name = $this->part->name) !== null) { + $this->setName($name); + }else { $this->setName("undefined"); } From 1972a3c4fc0c45beb0c4d7ee8b662ce6f87a2b70 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 27 Jan 2021 02:23:34 +0100 Subject: [PATCH 196/600] Missing connection settings added to `Folder:idle()` auto mode #89 --- CHANGELOG.md | 2 ++ src/Folder.php | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8809fa..9b176fe5 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Unnecessary parameter removed from `Client::getTimeout()` - Missing encryption variable added - could have caused problems with unencrypted communications - Prefer attachment filename attribute over name attribute #82 +- Missing connection settings added to `Folder:idle()` auto mode #89 ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) @@ -18,6 +19,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Attribute::class](src/Attribute.php) - [Attachment::class](src/Attachment.php) - [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) ### Breaking changes - NaN diff --git a/src/Folder.php b/src/Folder.php index 3b57b704..aba1ac28 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -388,6 +388,10 @@ public function idle(callable $callback, $timeout = 1200, $auto_reconnect = fals } if ($auto_reconnect === true) { $this->client->reconnect(); + $this->client->openFolder($this->path, true); + + $connection = $this->client->getConnection(); + $connection->idle(); } } } From 974452bb60d4e57ecb6b81eb472420ef939ea208 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 27 Jan 2021 22:37:34 +0100 Subject: [PATCH 197/600] Message not found exception added #93 --- CHANGELOG.md | 6 +- src/Connection/Protocols/ImapProtocol.php | 28 +++--- .../Protocols/ProtocolInterface.php | 6 +- src/Exceptions/MessageNotFoundException.php | 24 ++++++ src/Folder.php | 2 + src/Message.php | 15 ++-- src/Query/Query.php | 85 ++++++++----------- 7 files changed, 96 insertions(+), 70 deletions(-) create mode 100644 src/Exceptions/MessageNotFoundException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b176fe5..6d97401c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,15 +14,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) +- Message not found exception added #93 ### Affected Classes -- [Attribute::class](src/Attribute.php) - [Attachment::class](src/Attachment.php) +- [Attribute::class](src/Attribute.php) +- [Query::class](src/Query/Query.php) - [Client::class](src/Client.php) - [Folder::class](src/Folder.php) ### Breaking changes -- NaN +- A new exception can occur if a message can't be fetched (`\Webklex\PHPIMAP\Exceptions\MessageNotFoundException::class`) ## [2.4.4] - 2022-01-22 ### Fixed diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 7d7ac654..8b552388 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -16,6 +16,7 @@ use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Header; @@ -669,21 +670,23 @@ public function flags($uids, $uid = false){ * @param int|null $id message number * * @return array|string message number for given message or all messages as array - * @throws RuntimeException + * @throws MessageNotFoundException */ public function getUid($id = null) { - $uids = $this->fetch('http://github.com/UID', 1, INF); - if ($id == null) { - return $uids; - } + try { + $uids = $this->fetch('http://github.com/UID', 1, INF); + if ($id == null) { + return $uids; + } - foreach ($uids as $k => $v) { - if ($k == $id) { - return $v; + foreach ($uids as $k => $v) { + if ($k == $id) { + return $v; + } } - } + } catch (RuntimeException $e) {} - throw new RuntimeException('unique id not found'); + throw new MessageNotFoundException('unique id not found'); } /** @@ -691,7 +694,7 @@ public function getUid($id = null) { * @param string $id uid * * @return int message number - * @throws RuntimeException + * @throws MessageNotFoundException */ public function getMessageNumber($id) { $ids = $this->getUid(); @@ -701,7 +704,7 @@ public function getMessageNumber($id) { } } - throw new RuntimeException('message number not found'); + throw new MessageNotFoundException('message number not found'); } /** @@ -995,6 +998,7 @@ public function search(array $params, $uid = false) { * * @return array * @throws RuntimeException + * @throws MessageNotFoundException * @throws InvalidMessageDateException */ public function overview($sequence, $uid = false) { diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 688c6398..7a929ee7 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -15,6 +15,7 @@ use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\RuntimeException; /** @@ -126,7 +127,7 @@ public function flags($uids, $uid = false); * @param int|null $id message number * * @return array|string message number for given message or all messages as array - * @throws RuntimeException + * @throws MessageNotFoundException */ public function getUid($id = null); @@ -135,7 +136,7 @@ public function getUid($id = null); * @param string $id uid * * @return int message number - * @throws RuntimeException + * @throws MessageNotFoundException */ public function getMessageNumber($id); @@ -293,6 +294,7 @@ public function search(array $params, $uid = false); * * @return array * @throws RuntimeException + * @throws MessageNotFoundException * @throws InvalidMessageDateException */ public function overview($sequence, $uid = false); diff --git a/src/Exceptions/MessageNotFoundException.php b/src/Exceptions/MessageNotFoundException.php new file mode 100644 index 00000000..2f76875a --- /dev/null +++ b/src/Exceptions/MessageNotFoundException.php @@ -0,0 +1,24 @@ +client->getConnection()->setConnectionTimeout($timeout); diff --git a/src/Message.php b/src/Message.php index e86ad8a5..5b8073e8 100755 --- a/src/Message.php +++ b/src/Message.php @@ -200,6 +200,7 @@ class Message { * @throws MessageContentFetchingException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\MessageNotFoundException */ public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false, $sequence = null) { @@ -262,6 +263,7 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul * @throws ReflectionException * @throws MessageFlagException * @throws Exceptions\RuntimeException + * @throws Exceptions\MessageNotFoundException */ public static function make($uid, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null, $sequence = null){ $reflection = new ReflectionClass(self::class); @@ -868,6 +870,7 @@ protected function fetchThreadByMessageId(&$thread, $message_id, $primary_folder * @throws MessageHeaderFetchingException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\MessageNotFoundException */ public function copy($folder_path, $expunge = false) { $this->client->openFolder($folder_path); @@ -902,6 +905,7 @@ public function copy($folder_path, $expunge = false) { * @throws MessageHeaderFetchingException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException + * @throws Exceptions\MessageNotFoundException */ public function move($folder_path, $expunge = false) { $this->client->openFolder($folder_path); @@ -932,6 +936,7 @@ public function move($folder_path, $expunge = false) { * @return mixed * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException + * @throws Exceptions\MessageNotFoundException * @throws Exceptions\RuntimeException * @throws InvalidMessageDateException * @throws MessageContentFetchingException @@ -1348,9 +1353,9 @@ public function setClient($client){ * Set the message number * @param int $uid * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException * @return $this + * @throws Exceptions\MessageNotFoundException + * @throws Exceptions\ConnectionFailedException */ public function setUid($uid){ $this->uid = $uid; @@ -1365,9 +1370,9 @@ public function setUid($uid){ * @param $msgn * @param int|null $msglist * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException * @return $this + * @throws Exceptions\MessageNotFoundException + * @throws Exceptions\ConnectionFailedException */ public function setMsgn($msgn, $msglist = null){ $this->msgn = $msgn; @@ -1401,7 +1406,7 @@ public function getSequenceId(){ * @param int|null $msglist * * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws Exceptions\MessageNotFoundException */ public function setSequenceId($uid, $msglist = null){ if ($this->getSequence() === IMAP::ST_UID) { diff --git a/src/Query/Query.php b/src/Query/Query.php index cfb2a9bc..a41c2c99 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -26,6 +26,7 @@ use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; +use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\IMAP; @@ -274,8 +275,8 @@ public function get() { /** * Get a new Message instance * @param int $uid - * @param null $msglist - * @param null $sequence + * @param int|null $msglist + * @param int|null $sequence * * @return Message * @throws ConnectionFailedException @@ -283,17 +284,18 @@ public function get() { * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException - * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException - * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException + * @throws EventNotFoundException + * @throws MessageFlagException + * @throws MessageNotFoundException */ - public function getMessage($uid, $msglist = null, $sequence = null){ + public function getMessage($uid, $msglist = null, $sequence = null) { return new Message($uid, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags(), $sequence ? $sequence : $this->sequence); } /** * Get a message by its message number * @param $msgn - * @param null $msglist + * @param int|null $msglist * * @return Message * @throws ConnectionFailedException @@ -301,10 +303,11 @@ public function getMessage($uid, $msglist = null, $sequence = null){ * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException * @throws RuntimeException - * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException - * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException + * @throws EventNotFoundException + * @throws MessageFlagException + * @throws MessageNotFoundException */ - public function getMessageByMsgn($msgn, $msglist = null){ + public function getMessageByMsgn($msgn, $msglist = null) { return $this->getMessage($msgn, $msglist, IMAP::ST_MSGN); } @@ -318,62 +321,46 @@ public function getMessageByMsgn($msgn, $msglist = null){ * @throws MessageContentFetchingException * @throws MessageHeaderFetchingException * @throws RuntimeException - * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException - * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException + * @throws EventNotFoundException + * @throws MessageFlagException + * @throws MessageNotFoundException */ - public function getMessageByUid($uid){ + public function getMessageByUid($uid) { return $this->getMessage($uid, null, IMAP::ST_UID); } /** - * Paginate the current query - * @param int $per_page Results you which to receive per page - * @param int $page The current page you are on (e.g. 0, 1, 2, ...) use `null` to enable auto mode - * @param string $page_name The page name / uri parameter used for the generated links and the auto mode + * Don't mark messages as read when fetching * - * @return LengthAwarePaginator - * @throws GetMessagesFailedException + * @return $this */ - public function paginate($per_page = 5, $page = null, $page_name = 'imap_page'){ - if ( - $page === null - && isset($_GET[$page_name]) - && $_GET[$page_name] > 0 - ) { - $this->page = intval($_GET[$page_name]); - } elseif ($page > 0) { - $this->page = $page; - } - - $this->limit = $per_page; + public function leaveUnread() { + $this->setFetchOptions(IMAP::FT_PEEK); - return $this->get()->paginate($per_page, $this->page, $page_name, true); + return $this; } /** - * Get the raw IMAP search query + * Mark all messages as read when fetching * - * @return string + * @return $this */ - public function generate_query() { - $query = ''; - $this->query->each(function($statement) use(&$query) { - if (count($statement) == 1) { - $query .= $statement[0]; - } else { - if($statement[1] === null){ - $query .= $statement[0]; - }else{ - $query .= $statement[0].' "'.$statement[1].'"'; - } - } - $query .= ' '; + public function markAsRead() { + $this->setFetchOptions(IMAP::FT_UID); - }); + return $this; + } - $this->raw_query = trim($query); + /** + * Set the sequence type + * @param int $sequence + * + * @return $this + */ + public function setSequence($sequence) { + $this->sequence = $sequence != IMAP::ST_MSGN ? IMAP::ST_UID : $sequence; - return $this->raw_query; + return $this; } /** From 32c2f577fe9e0384c1af8664624c7ed123c77bcf Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 27 Jan 2021 22:44:50 +0100 Subject: [PATCH 198/600] Chunked fetching support added --- CHANGELOG.md | 1 + src/Query/Query.php | 231 ++++++++++++++++++++++++++++++-------------- 2 files changed, 160 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d97401c..9076d5e7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) - Message not found exception added #93 +- Chunked fetching support added `Query::chunked()`. Just in case you can't fetch all messages at once ### Affected Classes - [Attachment::class](src/Attachment.php) diff --git a/src/Query/Query.php b/src/Query/Query.php index a41c2c99..d1baafc8 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -142,37 +142,29 @@ protected function parse_date($date) { } /** - * Don't mark messages as read when fetching + * Get the raw IMAP search query * - * @return $this + * @return string */ - public function leaveUnread() { - $this->setFetchOptions(IMAP::FT_PEEK); + public function generate_query() { + $query = ''; + $this->query->each(function($statement) use (&$query) { + if (count($statement) == 1) { + $query .= $statement[0]; + } else { + if ($statement[1] === null) { + $query .= $statement[0]; + } else { + $query .= $statement[0] . ' "' . $statement[1] . '"'; + } + } + $query .= ' '; - return $this; - } + }); - /** - * Mark all messages as read when fetching - * - * @return $this - */ - public function markAsRead() { - $this->setFetchOptions(IMAP::FT_UID); + $this->raw_query = trim($query); - return $this; - } - - /** - * Set the sequence type - * @param int $sequence - * - * @return $this - */ - public function setSequence($sequence) { - $this->sequence = $sequence != IMAP::ST_MSGN ? IMAP::ST_UID : $sequence; - - return $this; + return $this->raw_query; } /** @@ -210,66 +202,161 @@ public function count() { } /** - * Fetch the current query and return all found messages + * Fetch a given id collection + * @param Collection $available_messages * - * @return MessageCollection - * @throws GetMessagesFailedException + * @return array + * @throws ConnectionFailedException + * @throws RuntimeException */ - public function get() { - $messages = MessageCollection::make([]); + protected function fetch($available_messages) { + if ($this->fetch_order === 'desc') { + $available_messages = $available_messages->reverse(); + } - $available_messages = $this->search(); - try { - if (($available_messages_count = $available_messages->count()) > 0) { + $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); + $flags = $this->client->getConnection()->flags($uids, $this->sequence == IMAP::ST_UID); + $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence == IMAP::ST_UID); - $messages->total($available_messages_count); + $contents = []; + if ($this->getFetchBody()) { + $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence == IMAP::ST_UID); + } - if ($this->fetch_order === 'desc') { - $available_messages = $available_messages->reverse(); - } + return [ + "uids" => $uids, + "flags" => $flags, + "headers" => $headers, + "contents" => $contents, + ]; + } - $message_key = ClientManager::get('options.message_key'); + /** + * Populate a given id collection and receive a fully fetched message collection + * @param Collection $available_messages + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws RuntimeException + * @throws ReflectionException + * @throws EventNotFoundException + * @throws MessageFlagException + * @throws MessageNotFoundException + */ + protected function populate($available_messages) { + $messages = MessageCollection::make([]); - $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); + $messages->total($available_messages->count()); + + $message_key = ClientManager::get('options.message_key'); + + $raw_messages = $this->fetch($available_messages); + $flags = $raw_messages["flags"]; + $headers = $raw_messages["headers"]; + $contents = $raw_messages["contents"]; + + $msglist = 0; + foreach ($headers as $uid => $header) { + $content = isset($contents[$uid]) ? $contents[$uid] : ""; + $flag = isset($flags[$uid]) ? $flags[$uid] : []; + + $message = Message::make($uid, $msglist, $this->getClient(), $header, $content, $flag, $this->getFetchOptions(), $this->sequence); + switch ($message_key) { + case 'number': + $key = $message->getMessageNo(); + break; + case 'list': + $key = $msglist; + break; + case 'uid': + $key = $message->getUid(); + break; + default: + $key = $message->getMessageId(); + break; - $raw_flags = $this->client->getConnection()->flags($uids, $this->sequence == IMAP::ST_UID); - $raw_headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence == IMAP::ST_UID); + } + $messages->put("$key", $message); + $msglist++; + } - $raw_contents = []; - if ($this->getFetchBody()) { - $raw_contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence == IMAP::ST_UID); - } + return $messages; + } - $msglist = 0; - foreach ($raw_headers as $uid => $raw_header) { - $raw_content = isset($raw_contents[$uid]) ? $raw_contents[$uid] : ""; - $raw_flag = isset($raw_flags[$uid]) ? $raw_flags[$uid] : []; - - $message = Message::make($uid, $msglist, $this->getClient(), $raw_header, $raw_content, $raw_flag, $this->getFetchOptions(), $this->sequence); - switch ($message_key){ - case 'number': - $key = $message->getMessageNo(); - break; - case 'list': - $key = $msglist; - break; - case 'uid': - $key = $message->getUid(); - break; - default: - $key = $message->getMessageId(); - break; - - } - $messages->put("$key", $message); - $msglist++; + /** + * Fetch the current query and return all found messages + * + * @return MessageCollection + * @throws GetMessagesFailedException + */ + public function get() { + $available_messages = $this->search(); + + try { + if (($available_messages_count = $available_messages->count()) > 0) { + return $this->populate($available_messages); + } + return MessageCollection::make([]); + } catch (Exception $e) { + throw new GetMessagesFailedException($e->getMessage(), 0, $e); + } + } + + /** + * Fetch the current query as chunked requests + * @param callable $callback + * @param int $chunk_size + * @param int $start_chunk + * + * @throws GetMessagesFailedException + */ + public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { + $available_messages = $this->search(); + if (($available_messages_count = $available_messages->count()) > 0) { + $old_limit = $this->limit; + $old_page = $this->page; + + $this->limit = $chunk_size; + $this->page = $start_chunk; + while ($this->limit * $this->page <= $available_messages_count) { + try { + $messages = $this->populate($available_messages); + $callback($messages, $this->page); + } catch (Exception $e) { + throw new GetMessagesFailedException($e->getMessage(), 0, $e); } + $this->page++; } + $this->limit = $old_limit; + $this->page = $old_page; + } + } - return $messages; - } catch (\Exception $e) { - throw new GetMessagesFailedException($e->getMessage(),0, $e); + /** + * Paginate the current query + * @param int $per_page Results you which to receive per page + * @param int|null $page The current page you are on (e.g. 0, 1, 2, ...) use `null` to enable auto mode + * @param string $page_name The page name / uri parameter used for the generated links and the auto mode + * + * @return LengthAwarePaginator + * @throws GetMessagesFailedException + */ + public function paginate($per_page = 5, $page = null, $page_name = 'imap_page') { + if ( + $page === null + && isset($_GET[$page_name]) + && $_GET[$page_name] > 0 + ) { + $this->page = intval($_GET[$page_name]); + } elseif ($page > 0) { + $this->page = $page; } + + $this->limit = $per_page; + + return $this->get()->paginate($per_page, $this->page, $page_name, true); } /** From 17da5553a91279cc70f553485932b404ad9542c1 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 27 Jan 2021 23:20:07 +0100 Subject: [PATCH 199/600] "Soft fail" support added --- CHANGELOG.md | 1 + src/Query/Query.php | 171 ++++++++++++++++++++++++++++++++++++++------ src/config/imap.php | 3 + 3 files changed, 152 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9076d5e7..363a4b45 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Dynamic Attribute access support added (e.g `$message->from[0]`) - Message not found exception added #93 - Chunked fetching support added `Query::chunked()`. Just in case you can't fetch all messages at once +- "Soft fail" support added ### Affected Classes - [Attachment::class](src/Attachment.php) diff --git a/src/Query/Query.php b/src/Query/Query.php index d1baafc8..6e95ed5e 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -76,6 +76,12 @@ class Query { /** @var string $date_format */ protected $date_format; + /** @var bool $soft_fail */ + protected $soft_fail = false; + + /** @var array $errors */ + protected $errors = []; + /** * Query constructor. * @param Client $client @@ -94,6 +100,7 @@ public function __construct(Client $client, $charset = 'UTF-8') { } $this->date_format = ClientManager::get('date_format', 'd M y'); + $this->soft_fail = ClientManager::get('options.soft_fail', false); $this->charset = $charset; $this->query = collect(); @@ -237,13 +244,10 @@ protected function fetch($available_messages) { * * @return MessageCollection * @throws ConnectionFailedException - * @throws InvalidMessageDateException - * @throws MessageContentFetchingException - * @throws RuntimeException - * @throws ReflectionException * @throws EventNotFoundException - * @throws MessageFlagException - * @throws MessageNotFoundException + * @throws GetMessagesFailedException + * @throws ReflectionException + * @throws RuntimeException */ protected function populate($available_messages) { $messages = MessageCollection::make([]); @@ -261,25 +265,44 @@ protected function populate($available_messages) { foreach ($headers as $uid => $header) { $content = isset($contents[$uid]) ? $contents[$uid] : ""; $flag = isset($flags[$uid]) ? $flags[$uid] : []; + $error = null; + + try { + $message = Message::make($uid, $msglist, $this->getClient(), $header, $content, $flag, $this->getFetchOptions(), $this->sequence); + + switch ($message_key) { + case 'number': + $key = $message->getMessageNo(); + break; + case 'list': + $key = $msglist; + break; + case 'uid': + $key = $message->getUid(); + break; + default: + $key = $message->getMessageId(); + break; - $message = Message::make($uid, $msglist, $this->getClient(), $header, $content, $flag, $this->getFetchOptions(), $this->sequence); - switch ($message_key) { - case 'number': - $key = $message->getMessageNo(); - break; - case 'list': - $key = $msglist; - break; - case 'uid': - $key = $message->getUid(); - break; - default: - $key = $message->getMessageId(); - break; + } + $messages->put("$key", $message); + $msglist++; + }catch (MessageNotFoundException $e) { + $this->setError($uid, $e); + }catch (RuntimeException $e) { + $this->setError($uid, $e); + }catch (MessageFlagException $e) { + $this->setError($uid, $e); + }catch (InvalidMessageDateException $e) { + $this->setError($uid, $e); + }catch (MessageContentFetchingException $e) { + $this->setError($uid, $e); + } + if ($this->soft_fail === false && $this->hasError($uid)) { + $error = $this->getError($uid); + throw new GetMessagesFailedException($error->getMessage(), 0, $error); } - $messages->put("$key", $message); - $msglist++; } return $messages; @@ -325,7 +348,9 @@ public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { $messages = $this->populate($available_messages); $callback($messages, $this->page); } catch (Exception $e) { - throw new GetMessagesFailedException($e->getMessage(), 0, $e); + if ($this->soft_fail === false) { + throw new GetMessagesFailedException($e->getMessage(), 0, $e); + } } $this->page++; } @@ -682,4 +707,104 @@ public function setFetchOrderDesc() { public function fetchOrderDesc() { return $this->setFetchOrderDesc(); } + + /** + * @var boolean $state + * + * @return Query + */ + public function softFail($state = true) { + return $this->setSoftFail($state); + } + + /** + * @var boolean $state + * + * @return Query + */ + public function setSoftFail($state = true) { + $this->soft_fail = $state; + + return $this; + } + + /** + * @return boolean + */ + public function getSoftFail() { + return $this->soft_fail; + } + + /** + * Add a new error to the error holder + * @param integer $uid + * @param Exception $error + */ + protected function setError($uid, $error) { + $this->errors[$uid] = $error; + } + + /** + * Check if there are any errors / exceptions present + * @var integer|null $uid + * + * @return boolean + */ + public function hasErrors($uid = null){ + if ($uid !== null) { + return $this->hasError($uid); + } + return count($this->errors) > 0; + } + + /** + * Check if there is an error / exception present + * @var integer $uid + * + * @return boolean + */ + public function hasError($uid){ + return isset($this->errors[$uid]); + } + + /** + * Get all available errors / exceptions + * + * @return array + */ + public function errors(){ + return $this->getErrors(); + } + + /** + * Get all available errors / exceptions + * + * @return array + */ + public function getErrors(){ + return $this->errors; + } + + /** + * Get a specific error / exception + * @var integer $uid + * + * @return Exception|null + */ + public function error($uid){ + return $this->getError($uid); + } + + /** + * Get a specific error / exception + * @var integer $uid + * + * @return Exception|null + */ + public function getError($uid){ + if ($this->hasError($uid)) { + return $this->errors[$uid]; + } + return null; + } } diff --git a/src/config/imap.php b/src/config/imap.php index f85f6feb..33fc511f 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -106,6 +106,8 @@ | Default TRUE | -Flag download option | Default TRUE + | -Soft fail + | Default FALSE - Set to TRUE if you want to ignore certain exception while fetching bulk messages | -Message key identifier option | You can choose between the following: | 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail) @@ -136,6 +138,7 @@ 'sequence' => \Webklex\PHPIMAP\IMAP::ST_MSGN, 'fetch_body' => true, 'fetch_flags' => true, + 'soft_fail' => false, 'message_key' => 'list', 'fetch_order' => 'asc', 'dispositions' => ['attachment', 'inline'], From 52e12cf88cc72d6cdb912a4a52ce23caf3cdd10a Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 27 Jan 2021 23:58:58 +0100 Subject: [PATCH 200/600] Code complexity reduced and missing docs added --- src/Header.php | 2 +- src/Message.php | 4 +- src/Query/Query.php | 146 +++++++++++++++++++++++++++----------------- 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/src/Header.php b/src/Header.php index 93cad466..c8046ea4 100644 --- a/src/Header.php +++ b/src/Header.php @@ -61,7 +61,7 @@ class Header { /** * Header constructor. - * @param $raw_header + * @param string $raw_header * @param boolean $attributize * * @throws InvalidMessageDateException diff --git a/src/Message.php b/src/Message.php index 5b8073e8..4c938a38 100755 --- a/src/Message.php +++ b/src/Message.php @@ -251,7 +251,7 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul * @param Client $client * @param string $raw_header * @param string $raw_body - * @param string $raw_flags + * @param array $raw_flags * @param null $fetch_options * @param null $sequence * @@ -431,7 +431,7 @@ public function parseRawHeader($raw_header){ /** * Parse additional raw flags - * @param $raw_flags + * @param array $raw_flags */ public function parseRawFlags($raw_flags) { $this->flags = FlagCollection::make([]); diff --git a/src/Query/Query.php b/src/Query/Query.php index 6e95ed5e..0527f5c1 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -103,7 +103,7 @@ public function __construct(Client $client, $charset = 'UTF-8') { $this->soft_fail = ClientManager::get('options.soft_fail', false); $this->charset = $charset; - $this->query = collect(); + $this->query = new Collection(); $this->boot(); } @@ -185,17 +185,12 @@ protected function search() { try { $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence == IMAP::ST_UID); + return new Collection($available_messages); } catch (RuntimeException $e) { - $available_messages = false; + throw new GetMessagesFailedException("failed to fetch messages", 0, $e); } catch (ConnectionFailedException $e) { throw new GetMessagesFailedException("failed to fetch messages", 0, $e); } - - if ($available_messages !== false) { - return collect($available_messages); - } - - return collect(); } /** @@ -238,6 +233,66 @@ protected function fetch($available_messages) { ]; } + /** + * Make a new message from given raw components + * @param integer $uid + * @param integer $msglist + * @param string $header + * @param string $content + * @param array $flags + * + * @return Message|null + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws GetMessagesFailedException + * @throws ReflectionException + */ + protected function make($uid, $msglist, $header, $content, $flags){ + try { + return Message::make($uid, $msglist, $this->getClient(), $header, $content, $flags, $this->getFetchOptions(), $this->sequence); + }catch (MessageNotFoundException $e) { + $this->setError($uid, $e); + }catch (RuntimeException $e) { + $this->setError($uid, $e); + }catch (MessageFlagException $e) { + $this->setError($uid, $e); + }catch (InvalidMessageDateException $e) { + $this->setError($uid, $e); + }catch (MessageContentFetchingException $e) { + $this->setError($uid, $e); + } + + $this->handleException($uid); + + return null; + } + + /** + * Get the message key for a given message + * @param string $message_key + * @param integer $msglist + * @param Message $message + * + * @return string + */ + protected function getMessageKey($message_key, $msglist, $message){ + switch ($message_key) { + case 'number': + $key = $message->getMessageNo(); + break; + case 'list': + $key = $msglist; + break; + case 'uid': + $key = $message->getUid(); + break; + default: + $key = $message->getMessageId(); + break; + } + return (string)$key; + } + /** * Populate a given id collection and receive a fully fetched message collection * @param Collection $available_messages @@ -257,52 +312,18 @@ protected function populate($available_messages) { $message_key = ClientManager::get('options.message_key'); $raw_messages = $this->fetch($available_messages); - $flags = $raw_messages["flags"]; - $headers = $raw_messages["headers"]; - $contents = $raw_messages["contents"]; $msglist = 0; - foreach ($headers as $uid => $header) { - $content = isset($contents[$uid]) ? $contents[$uid] : ""; - $flag = isset($flags[$uid]) ? $flags[$uid] : []; - $error = null; - - try { - $message = Message::make($uid, $msglist, $this->getClient(), $header, $content, $flag, $this->getFetchOptions(), $this->sequence); - - switch ($message_key) { - case 'number': - $key = $message->getMessageNo(); - break; - case 'list': - $key = $msglist; - break; - case 'uid': - $key = $message->getUid(); - break; - default: - $key = $message->getMessageId(); - break; + foreach ($raw_messages["headers"] as $uid => $header) { + $content = isset($raw_messages["contents"][$uid]) ? $raw_messages["contents"][$uid] : ""; + $flag = isset($raw_messages["flags"][$uid]) ? $raw_messages["flags"][$uid] : []; - } + $message = $this->make($uid, $msglist, $header, $content, $flag); + if ($message !== null) { + $key = $this->getMessageKey($message_key, $msglist, $message); $messages->put("$key", $message); - $msglist++; - }catch (MessageNotFoundException $e) { - $this->setError($uid, $e); - }catch (RuntimeException $e) { - $this->setError($uid, $e); - }catch (MessageFlagException $e) { - $this->setError($uid, $e); - }catch (InvalidMessageDateException $e) { - $this->setError($uid, $e); - }catch (MessageContentFetchingException $e) { - $this->setError($uid, $e); - } - - if ($this->soft_fail === false && $this->hasError($uid)) { - $error = $this->getError($uid); - throw new GetMessagesFailedException($error->getMessage(), 0, $error); } + $msglist++; } return $messages; @@ -333,7 +354,11 @@ public function get() { * @param int $chunk_size * @param int $start_chunk * + * @throws ConnectionFailedException + * @throws EventNotFoundException * @throws GetMessagesFailedException + * @throws ReflectionException + * @throws RuntimeException */ public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { $available_messages = $this->search(); @@ -344,14 +369,8 @@ public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { $this->limit = $chunk_size; $this->page = $start_chunk; while ($this->limit * $this->page <= $available_messages_count) { - try { - $messages = $this->populate($available_messages); - $callback($messages, $this->page); - } catch (Exception $e) { - if ($this->soft_fail === false) { - throw new GetMessagesFailedException($e->getMessage(), 0, $e); - } - } + $messages = $this->populate($available_messages); + $callback($messages, $this->page); $this->page++; } $this->limit = $old_limit; @@ -735,6 +754,19 @@ public function getSoftFail() { return $this->soft_fail; } + /** + * Handle the exception for a given uid + * @param integer $uid + * + * @throws GetMessagesFailedException + */ + protected function handleException($uid) { + if ($this->soft_fail === false && $this->hasError($uid)) { + $error = $this->getError($uid); + throw new GetMessagesFailedException($error->getMessage(), 0, $error); + } + } + /** * Add a new error to the error holder * @param integer $uid From f8fc4ad7337cabf844d0ac9c701119705aa3b46e Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:30:40 +0100 Subject: [PATCH 201/600] Missing docs added --- src/Client.php | 10 +++++----- src/Connection/Protocols/ImapProtocol.php | 2 +- src/Connection/Protocols/LegacyProtocol.php | 4 ++-- src/Connection/Protocols/Protocol.php | 2 +- src/Connection/Protocols/ProtocolInterface.php | 10 ++++++++++ src/Query/Query.php | 4 ++-- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Client.php b/src/Client.php index 7f02c37c..31114825 100755 --- a/src/Client.php +++ b/src/Client.php @@ -118,11 +118,11 @@ class Client { public $authentication; /** - * Active folder. + * Active folder path. * - * @var Folder + * @var string */ - protected $active_folder = false; + protected $active_folder = null; /** * Default message mask @@ -388,7 +388,7 @@ public function disconnect() { if ($this->isConnected() && $this->connection !== false) { $this->connection->logout(); } - $this->active_folder = false; + $this->active_folder = null; return $this; } @@ -540,7 +540,7 @@ public function checkFolder($folder) { /** * Get the current active folder * - * @return Folder + * @return string */ public function getFolderPath(){ return $this->active_folder; diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 8b552388..07916248 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -25,7 +25,7 @@ * * @package Webklex\PHPIMAP\Connection\Protocols */ -class ImapProtocol extends Protocol implements ProtocolInterface { +class ImapProtocol extends Protocol { /** * Request noun diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index be4ed5e3..3dc57d71 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -23,7 +23,7 @@ * * @package Webklex\PHPIMAP\Connection\Protocols */ -class LegacyProtocol extends Protocol implements ProtocolInterface { +class LegacyProtocol extends Protocol { protected $protocol = "imap"; protected $host = null; @@ -103,7 +103,7 @@ public function login($user, $password) { * @param string $user username * @param string $token access token * - * @return bool + * @return bool|mixed * @throws AuthFailedException */ public function authenticate($user, $token) { diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index f29be0f1..1d70f239 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -19,7 +19,7 @@ * * @package Webklex\PHPIMAP\Connection\Protocols */ -abstract class Protocol { +abstract class Protocol implements ProtocolInterface { /** * Default connection timeout in seconds diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 7a929ee7..c715878e 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -57,6 +57,16 @@ public function connect($host, $port = null); */ public function login($user, $password); + /** + * Authenticate your current session. + * @param string $user username + * @param string $token access token + * + * @return bool|mixed + * @throws AuthFailedException + */ + public function authenticate($user, $token); + /** * Logout of the current server session * diff --git a/src/Query/Query.php b/src/Query/Query.php index 0527f5c1..932ebf5a 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -40,7 +40,7 @@ */ class Query { - /** @var array $query */ + /** @var Collection $query */ protected $query; /** @var string $raw_query */ @@ -518,7 +518,7 @@ public function limit($limit, $page = 1) { } /** - * @return array + * @return Collection */ public function getQuery() { return $this->query; From 67b8b2590b003ed4ed65991647b52b629625f573 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:36:37 +0100 Subject: [PATCH 202/600] Message move / copy expect a folder path #79 --- CHANGELOG.md | 3 +++ src/Message.php | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363a4b45..1c0175e6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing encryption variable added - could have caused problems with unencrypted communications - Prefer attachment filename attribute over name attribute #82 - Missing connection settings added to `Folder:idle()` auto mode #89 +- Message move / copy expect a folder path #79 ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) @@ -22,11 +23,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Attachment::class](src/Attachment.php) - [Attribute::class](src/Attribute.php) - [Query::class](src/Query/Query.php) +- [Message::class](src/Message.php) - [Client::class](src/Client.php) - [Folder::class](src/Folder.php) ### Breaking changes - A new exception can occur if a message can't be fetched (`\Webklex\PHPIMAP\Exceptions\MessageNotFoundException::class`) +- `Message::move()` and `Message::copy()` no longer accept folder names as folder path ## [2.4.4] - 2022-01-22 ### Fixed diff --git a/src/Message.php b/src/Message.php index 4c938a38..f729a1f9 100755 --- a/src/Message.php +++ b/src/Message.php @@ -773,7 +773,7 @@ public function getEncoding($structure) { * @throws Exceptions\RuntimeException */ public function getFolder(){ - return $this->client->getFolder($this->folder_path); + return $this->client->getFolderByPath($this->folder_path); } /** @@ -791,7 +791,7 @@ public function getFolder(){ public function thread($sent_folder = null, &$thread = null, $folder = null){ $thread = $thread ? $thread : MessageCollection::make([]); $folder = $folder ? $folder : $this->getFolder(); - $sent_folder = $sent_folder ? $sent_folder : $this->client->getFolder(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); + $sent_folder = $sent_folder ? $sent_folder : $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ foreach($thread as $message) { @@ -880,7 +880,7 @@ public function copy($folder_path, $expunge = false) { $next_uid = $status["uidnext"]; /** @var Folder $folder */ - $folder = $this->client->getFolder($folder_path); + $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { @@ -915,7 +915,7 @@ public function move($folder_path, $expunge = false) { $next_uid = $status["uidnext"]; /** @var Folder $folder */ - $folder = $this->client->getFolder($folder_path); + $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { From 4a062c7181a6b17b876a499abd65af35473d880f Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:37:19 +0100 Subject: [PATCH 203/600] Client doc updated and variable names fixed --- src/Client.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Client.php b/src/Client.php index 31114825..57fe44ef 100755 --- a/src/Client.php +++ b/src/Client.php @@ -481,21 +481,21 @@ public function getFolders($hierarchical = true, $parent_folder = null) { } /** - * Open folder. - * @param string $folder + * Open a given folder. + * @param string $folder_path * @param boolean $force_select * * @return mixed * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function openFolder($folder, $force_select = false) { - if ($this->active_folder == $folder && $this->isConnected() && $force_select === false) { + public function openFolder($folder_path, $force_select = false) { + if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) { return true; } $this->checkConnection(); - $this->active_folder = $folder; - return $this->connection->selectFolder($folder); + $this->active_folder = $folder_path; + return $this->connection->selectFolder($folder_path); } /** From 5efdd232f294a9ee4565e70c21c7f5df7f382541 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:39:53 +0100 Subject: [PATCH 204/600] `Client::getFolder()` updated to circumvent special edge cases #79 --- CHANGELOG.md | 1 + src/Client.php | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0175e6..e008bd72 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prefer attachment filename attribute over name attribute #82 - Missing connection settings added to `Folder:idle()` auto mode #89 - Message move / copy expect a folder path #79 +- `Client::getFolder()` updated to circumvent special edge cases #79 ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) diff --git a/src/Client.php b/src/Client.php index 57fe44ef..e917a4fa 100755 --- a/src/Client.php +++ b/src/Client.php @@ -396,7 +396,7 @@ public function disconnect() { /** * Get a folder instance by a folder name * @param string $folder_name - * @param string|bool $delimiter + * @param string|bool|null $delimiter * * @return mixed * @throws ConnectionFailedException @@ -404,13 +404,15 @@ public function disconnect() { * @throws Exceptions\RuntimeException */ public function getFolder($folder_name, $delimiter = null) { + if ($delimiter !== false && $delimiter !== null) { + return $this->getFolderByPath($folder_name); + } + // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) - if ($delimiter !== false) { - $delimiter = (is_null($delimiter)) ? ClientManager::get('options.delimiter', "/") : $delimiter; - if (strpos($folder_name, $delimiter) !== false) { - return $this->getFolderByPath($folder_name); - } - } + $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; + if (strpos($folder_name, $delimiter) !== false) { + return $this->getFolderByPath($folder_name); + } return $this->getFolderByName($folder_name); } From 4e42b8c7e8e105c337e163ec4d23e4d85470420e Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:48:28 +0100 Subject: [PATCH 205/600] Missing connection status checks added to various methods --- CHANGELOG.md | 1 + src/Client.php | 8 ++++++-- src/Connection/Protocols/ProtocolInterface.php | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e008bd72..c698a5bb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing connection settings added to `Folder:idle()` auto mode #89 - Message move / copy expect a folder path #79 - `Client::getFolder()` updated to circumvent special edge cases #79 +- Missing connection status checks added to various methods ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) diff --git a/src/Client.php b/src/Client.php index e917a4fa..53efc2b7 100755 --- a/src/Client.php +++ b/src/Client.php @@ -410,7 +410,7 @@ public function getFolder($folder_name, $delimiter = null) { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; - if (strpos($folder_name, $delimiter) !== false) { + if (strpos($folder_name, (string)$delimiter) !== false) { return $this->getFolderByPath($folder_name); } @@ -586,11 +586,13 @@ public function expunge() { /** * Set the connection timeout - * @param $timeout + * @param integer $timeout * * @return Protocol + * @throws ConnectionFailedException */ public function setTimeout($timeout) { + $this->checkConnection(); return $this->connection->setConnectionTimeout($timeout); } @@ -598,8 +600,10 @@ public function setTimeout($timeout) { * Get the connection timeout * * @return int + * @throws ConnectionFailedException */ public function getTimeout(){ + $this->checkConnection(); return $this->connection->getConnectionTimeout(); } diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index c715878e..7949a82f 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -74,6 +74,13 @@ public function authenticate($user, $token); */ public function logout(); + /** + * Check if the current session is connected + * + * @return bool + */ + public function connected(); + /** * Get an array of available capabilities * From c57b4f59ffd7410f52867fd4ad65bb502f347949 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:52:20 +0100 Subject: [PATCH 206/600] Unused parameter `uid` removed --- src/Connection/Protocols/ImapProtocol.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 07916248..be1f0309 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -945,11 +945,10 @@ public function getQuotaRoot($quota_root = 'INBOX') { /** * Send idle command - * @param bool $uid set to true if passing a unique id (depreciated argument: will be removed. CMD UID IDLE is not supported) * * @throws RuntimeException */ - public function idle($uid = false) { + public function idle() { $this->sendRequest("IDLE"); if (!$this->assumedNextLine('+ ')) { throw new RuntimeException('idle failed'); From a588fe6a87c3a5534109b2baa882e98fbf3129be Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:52:42 +0100 Subject: [PATCH 207/600] Shared method added to interface --- src/Connection/Protocols/ProtocolInterface.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 7949a82f..d2802bc0 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -258,6 +258,22 @@ public function deleteFolder($folder); */ public function subscribeFolder($folder); + /** + * Unsubscribe from a folder + * @param string $folder folder name + * + * @return bool success + * @throws RuntimeException + */ + public function unsubscribeFolder($folder); + + /** + * Send idle command + * + * @throws RuntimeException + */ + public function idle(); + /** * Apply session saved changes to the server * From 6aa22d4853052cd38a1d08ff2687160721badab0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:55:21 +0100 Subject: [PATCH 208/600] Shared method "done" added --- src/Connection/Protocols/ProtocolInterface.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index d2802bc0..ff470bbf 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -274,6 +274,12 @@ public function unsubscribeFolder($folder); */ public function idle(); + /** + * Send done command + * @throws RuntimeException + */ + public function done(); + /** * Apply session saved changes to the server * From b1c492debded037877527c159f6022ab65f624e2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Jan 2021 00:55:53 +0100 Subject: [PATCH 209/600] Convert any given query to a collection --- src/Query/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 932ebf5a..4b074f75 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -529,7 +529,7 @@ public function getQuery() { * @return Query */ public function setQuery($query) { - $this->query = $query; + $this->query = new Collection($query); return $this; } From fa91c67c391bfdae3bd597a371a4184c4899950a Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Jan 2021 17:14:15 +0100 Subject: [PATCH 210/600] Count method added --- CHANGELOG.md | 1 + src/Attribute.php | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c698a5bb..38c06053 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Message not found exception added #93 - Chunked fetching support added `Query::chunked()`. Just in case you can't fetch all messages at once - "Soft fail" support added +- Count method added to `Attribute:class` ### Affected Classes - [Attachment::class](src/Attachment.php) diff --git a/src/Attribute.php b/src/Attribute.php index e07d06a3..9b183b58 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -244,10 +244,18 @@ public function first(){ * @return mixed|null */ public function last(){ - $cnt = count($this->values); - if ($cnt > 0) { + if (($cnt = $this->count()) > 0) { return $this->values[$cnt - 1]; } return null; } + + /** + * Get the number of values + * + * @return int + */ + public function count(){ + return count($this->values); + } } \ No newline at end of file From 5616b2c74444e0b7286609cd7eab92c3ca678857 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Jan 2021 17:15:28 +0100 Subject: [PATCH 211/600] Convert an Attribute instance into a Carbon date object #95 --- CHANGELOG.md | 1 + src/Attribute.php | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c06053..cbeee382 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Chunked fetching support added `Query::chunked()`. Just in case you can't fetch all messages at once - "Soft fail" support added - Count method added to `Attribute:class` +- Convert an Attribute instance into a Carbon date object #95 ### Affected Classes - [Attachment::class](src/Attachment.php) diff --git a/src/Attribute.php b/src/Attribute.php index 9b183b58..75e84150 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP; use ArrayAccess; +use Carbon\Carbon; /** * Class Attribute @@ -78,6 +79,18 @@ public function toArray(){ return $this->__serialize(); } + /** + * Convert first value to a date object + * + * @return Carbon|null + */ + public function toDate(){ + $date = $this->first(); + if ($date instanceof Carbon) return $date; + + return Carbon::parse($date); + } + /** * Determine if a value exists at an offset. * From cccf46fa6c1f5d6a1255ab88cb4325a3e6510907 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Jan 2021 17:16:02 +0100 Subject: [PATCH 212/600] Use isset instead of count --- src/Attribute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Attribute.php b/src/Attribute.php index 75e84150..06dc6a79 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -245,7 +245,7 @@ public function all(){ * @return mixed|null */ public function first(){ - if (count($this->values) > 0) { + if ($this->offsetExists(0)) { return $this->values[0]; } return null; From 86f28b6f3fa28a4c4144c16c1d25f2926415741d Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Jan 2021 17:19:11 +0100 Subject: [PATCH 213/600] Class docs updated --- src/Message.php | 80 ++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/src/Message.php b/src/Message.php index f729a1f9..a8f2c22b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -12,7 +12,6 @@ namespace Webklex\PHPIMAP; -use Carbon\Carbon; use ReflectionClass; use ReflectionException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -36,49 +35,36 @@ * @property integer msglist * @property integer uid * @property integer msgn - * @property string subject - * @property string message_id - * @property string message_no - * @property string references - * @property carbon date - * @property array from - * @property array to - * @property array cc - * @property array bcc - * @property array reply_to - * @property array in_reply_to - * @property array sender + * @property Attribute subject + * @property Attribute message_id + * @property Attribute message_no + * @property Attribute references + * @property Attribute date + * @property Attribute from + * @property Attribute to + * @property Attribute cc + * @property Attribute bcc + * @property Attribute reply_to + * @property Attribute in_reply_to + * @property Attribute sender * * @method integer getMsglist() - * @method integer setMsglist(integer $msglist) + * @method integer setMsglist($msglist) * @method integer getUid() * @method integer getMsgn() - * @method integer getPriority() - * @method integer setPriority(integer $priority) - * @method string getSubject() - * @method string setSubject(string $subject) - * @method string getMessageId() - * @method string setMessageId(string $message_id) - * @method string getMessageNo() - * @method string setMessageNo(string $message_no) - * @method string getReferences() - * @method string setReferences(string $references) - * @method carbon getDate() - * @method carbon setDate(carbon $date) - * @method array getFrom() - * @method array setFrom(array $from) - * @method array getTo() - * @method array setTo(array $to) - * @method array getCc() - * @method array setCc(array $cc) - * @method array getBcc() - * @method array setBcc(array $bcc) - * @method array getReplyTo() - * @method array setReplyTo(array $reply_to) - * @method array getInReplyTo() - * @method array setInReplyTo(array $in_reply_to) - * @method array getSender() - * @method array setSender(array $sender) + * @method Attribute getPriority() + * @method Attribute getSubject() + * @method Attribute getMessageId() + * @method Attribute getMessageNo() + * @method Attribute getReferences() + * @method Attribute getDate() + * @method Attribute getFrom() + * @method Attribute getTo() + * @method Attribute getCc() + * @method Attribute getBcc() + * @method Attribute getReplyTo() + * @method Attribute getInReplyTo() + * @method Attribute getSender() */ class Message { use HasEvents; @@ -107,7 +93,7 @@ class Message { /** * Attribute holder * - * @var array $attributes + * @var Attribute[]|mixed[] $attributes */ protected $attributes = [ 'message_no' => null, @@ -338,7 +324,7 @@ public function __set($name, $value) { * Magic getter * @param $name * - * @return mixed|null + * @return Attribute|mixed|null */ public function __get($name) { return $this->get($name); @@ -348,7 +334,7 @@ public function __get($name) { * Get an available message or message header attribute * @param $name * - * @return mixed|null + * @return Attribute|mixed|null */ public function get($name) { if(isset($this->attributes[$name])) { @@ -795,7 +781,7 @@ public function thread($sent_folder = null, &$thread = null, $folder = null){ /** @var Message $message */ foreach($thread as $message) { - if ($message->message_id == $this->message_id) { + if ($message->message_id->first() == $this->message_id->first()) { return $thread; } } @@ -1221,9 +1207,9 @@ public function is(Message $message = null) { } return $this->uid == $message->uid - && $this->message_id == $message->message_id - && $this->subject == $message->subject - && $this->date->eq($message->date); + && $this->message_id->first() == $message->message_id->first() + && $this->subject->first() == $message->subject->first() + && $this->date->toDate()->eq($message->date); } /** From f9b5746f991e36c17ecd8302fb686a219c0ecb36 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Jan 2021 17:19:52 +0100 Subject: [PATCH 214/600] Unused default attribute `message_no` removed --- CHANGELOG.md | 2 ++ src/Message.php | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbeee382..e7430366 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Message move / copy expect a folder path #79 - `Client::getFolder()` updated to circumvent special edge cases #79 - Missing connection status checks added to various methods +- Unused default attribute `message_no` removed from `Message::class` ### Added - Dynamic Attribute access support added (e.g `$message->from[0]`) @@ -34,6 +35,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - A new exception can occur if a message can't be fetched (`\Webklex\PHPIMAP\Exceptions\MessageNotFoundException::class`) - `Message::move()` and `Message::copy()` no longer accept folder names as folder path +- A `Message::class` instance might no longer have a `message_no` attribute ## [2.4.4] - 2022-01-22 ### Fixed diff --git a/src/Message.php b/src/Message.php index a8f2c22b..3514ce92 100755 --- a/src/Message.php +++ b/src/Message.php @@ -95,9 +95,7 @@ class Message { * * @var Attribute[]|mixed[] $attributes */ - protected $attributes = [ - 'message_no' => null, - ]; + protected $attributes = []; /** * The message folder path From cf95d883b40ba96bcc907e6a3661a59cc4a356d7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Jan 2021 17:20:41 +0100 Subject: [PATCH 215/600] Common boot method added --- src/Message.php | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Message.php b/src/Message.php index 3514ce92..2433206f 100755 --- a/src/Message.php +++ b/src/Message.php @@ -153,12 +153,14 @@ class Message { * Message body components * * @var array $bodies - * @var AttachmentCollection|array $attachments - * @var FlagCollection|array $flags */ public $bodies = []; - public $attachments = []; - public $flags = []; + + /** @var AttachmentCollection $attachments */ + public $attachments; + + /** @var FlagCollection $flags */ + public $flags; /** * A list of all available and supported flags @@ -187,6 +189,7 @@ class Message { * @throws Exceptions\MessageNotFoundException */ public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false, $sequence = null) { + $this->boot(); $default_mask = $client->getDefaultMessageMask(); if($default_mask != null) { @@ -197,17 +200,11 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul $this->folder_path = $client->getFolderPath(); - $this->config = ClientManager::get('options'); - $this->available_flags = ClientManager::get('flags'); - $this->setSequence($sequence); $this->setFetchOption($fetch_options); $this->setFetchBodyOption($fetch_body); $this->setFetchFlagsOption($fetch_flags); - $this->attachments = AttachmentCollection::make([]); - $this->flags = FlagCollection::make([]); - $this->client = $client; $this->client->openFolder($this->folder_path); @@ -253,6 +250,7 @@ public static function make($uid, $msglist, Client $client, $raw_header, $raw_bo $reflection = new ReflectionClass(self::class); /** @var self $instance */ $instance = $reflection->newInstanceWithoutConstructor(); + $instance->boot(); $default_mask = $client->getDefaultMessageMask(); if($default_mask != null) { @@ -263,13 +261,9 @@ public static function make($uid, $msglist, Client $client, $raw_header, $raw_bo "flag" => $client->getDefaultEvents("flag"), ]); $instance->setFolderPath($client->getFolderPath()); - $instance->setConfig(ClientManager::get('options')); - $instance->setAvailableFlags(ClientManager::get('flags')); $instance->setSequence($sequence); $instance->setFetchOption($fetch_options); - $instance->setAttachments(AttachmentCollection::make([])); - $instance->setClient($client); $instance->setSequenceId($uid, $msglist); @@ -281,6 +275,19 @@ public static function make($uid, $msglist, Client $client, $raw_header, $raw_bo return $instance; } + /** + * Boot a new instance + */ + public function boot(){ + $this->attributes = []; + + $this->config = ClientManager::get('options'); + $this->available_flags = ClientManager::get('flags'); + + $this->attachments = AttachmentCollection::make([]); + $this->flags = FlagCollection::make([]); + } + /** * Call dynamic attribute setter and getter methods * @param string $method From 461fbc39aa28507470e95925d070a55f07a7b964 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 1 Feb 2021 10:54:18 +0100 Subject: [PATCH 216/600] Version information updated --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7430366..851a451e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.5.0] - 2021-02-01 +### Fixed - Attachment saving filename fixed - Unnecessary parameter removed from `Client::getTimeout()` - Missing encryption variable added - could have caused problems with unencrypted communications @@ -37,7 +50,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::move()` and `Message::copy()` no longer accept folder names as folder path - A `Message::class` instance might no longer have a `message_no` attribute -## [2.4.4] - 2022-01-22 +## [2.4.4] - 2021-01-22 ### Fixed - Boundary detection simplified #90 - Prevent potential body overwriting #90 From ecae826b3a55217764fc84acf461cac8b4f2df1b Mon Sep 17 00:00:00 2001 From: Ricus Swanepoel Date: Tue, 23 Feb 2021 09:58:31 +0200 Subject: [PATCH 217/600] Fixed possible php warning in Message.php class line 435 (#109) Co-authored-by: ricus --- src/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Message.php b/src/Message.php index 2433206f..adcb7ed7 100755 --- a/src/Message.php +++ b/src/Message.php @@ -432,7 +432,7 @@ public function parseRawFlags($raw_flags) { $flag = substr($flag, 1); } $flag_key = strtolower($flag); - if (in_array($flag_key, $this->available_flags) || $this->available_flags === null) { + if ($this->available_flags === null || in_array($flag_key, $this->available_flags)) { $this->flags->put($flag_key, $flag); } } From e1296d18a01027b9d99728a2b3dee4e8b5b4d299 Mon Sep 17 00:00:00 2001 From: Ricus Swanepoel Date: Wed, 24 Feb 2021 11:42:34 +0200 Subject: [PATCH 218/600] Remove duplicate if statement in setEventsFromConfig (#110) * Fixed possible php warning in Message.php class line 435 * removed duplicate if statement in setEventsFromConfig in Client.php Co-authored-by: ricus --- src/Client.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Client.php b/src/Client.php index 53efc2b7..047b3bc2 100755 --- a/src/Client.php +++ b/src/Client.php @@ -220,10 +220,8 @@ private function setAccountConfig($key, $config, $default_config){ protected function setEventsFromConfig($config) { $this->events = ClientManager::get("events"); if(isset($config['events'])){ - if(isset($config['events'])) { - foreach($config['events'] as $section => $events) { - $this->events[$section] = array_merge($this->events[$section], $events); - } + foreach($config['events'] as $section => $events) { + $this->events[$section] = array_merge($this->events[$section], $events); } } } From fc100117349cf85df1e86a4a8151a555f2fcd304 Mon Sep 17 00:00:00 2001 From: shacky Date: Thu, 17 Jun 2021 18:16:17 +0200 Subject: [PATCH 219/600] fix setting default mask from config (#133) --- src/Client.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Client.php b/src/Client.php index 047b3bc2..68853c9f 100755 --- a/src/Client.php +++ b/src/Client.php @@ -251,13 +251,13 @@ protected function setMaskFromConfig($config) { } if(isset($config['masks']['attachment'])) { if(class_exists($config['masks']['attachment'])) { - $this->default_message_mask = $config['masks']['attachment']; + $this->default_attachment_mask = $config['masks']['attachment']; }else{ throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['attachment']); } }else{ if(class_exists($default_config['attachment'])) { - $this->default_message_mask = $default_config['attachment']; + $this->default_attachment_mask = $default_config['attachment']; }else{ throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']); } @@ -270,7 +270,7 @@ protected function setMaskFromConfig($config) { } if(class_exists($default_config['attachment'])) { - $this->default_message_mask = $default_config['attachment']; + $this->default_attachment_mask = $default_config['attachment']; }else{ throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']); } From 10c7c500f916a40c744c9653fbafd22f5ac19902 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Jun 2021 16:32:58 +0200 Subject: [PATCH 220/600] Chunked fetch fails in case of less available mails than page size #114 --- CHANGELOG.md | 3 ++- src/Query/Query.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 851a451e..aa938923 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix setting default mask from config #133 (thanks @shacky) +- Chunked fetch fails in case of less available mails than page size #114 ### Added - NaN diff --git a/src/Query/Query.php b/src/Query/Query.php index 4b074f75..df93b461 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -368,11 +368,11 @@ public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { $this->limit = $chunk_size; $this->page = $start_chunk; - while ($this->limit * $this->page <= $available_messages_count) { + do { $messages = $this->populate($available_messages); $callback($messages, $this->page); $this->page++; - } + } while ($this->limit * $this->page <= $available_messages_count); $this->limit = $old_limit; $this->page = $old_page; } From d10581afa012d0aa99914f9ba23f09b59649ccdb Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Jun 2021 16:46:20 +0200 Subject: [PATCH 221/600] Disable rfc822 header parsing via config option #115 --- CHANGELOG.md | 2 +- src/Header.php | 2 +- src/config/imap.php | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa938923..df9044c1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Chunked fetch fails in case of less available mails than page size #114 ### Added -- NaN +- Disable rfc822 header parsing via config option #115 ### Affected Classes - NaN diff --git a/src/Header.php b/src/Header.php index c8046ea4..d59bfe31 100644 --- a/src/Header.php +++ b/src/Header.php @@ -234,7 +234,7 @@ protected function parse(){ public function rfc822_parse_headers($raw_headers){ $headers = []; $imap_headers = []; - if (extension_loaded('imap')) { + if (extension_loaded('imap') && $this->config["rfc822"]) { $raw_imap_headers = (array) \imap_rfc822_parse_headers($this->raw); foreach($raw_imap_headers as $key => $values) { $key = str_replace("-", "_", $key); diff --git a/src/config/imap.php b/src/config/imap.php index 33fc511f..1b605ee0 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -108,6 +108,9 @@ | Default TRUE | -Soft fail | Default FALSE - Set to TRUE if you want to ignore certain exception while fetching bulk messages + | -RFC822 + | Default TRUE - Set to FALSE to prevent the usage of \imap_rfc822_parse_headers(). + | See https://github.com/Webklex/php-imap/issues/115 for more information. | -Message key identifier option | You can choose between the following: | 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail) @@ -139,6 +142,7 @@ 'fetch_body' => true, 'fetch_flags' => true, 'soft_fail' => false, + 'rfc822' => true, 'message_key' => 'list', 'fetch_order' => 'asc', 'dispositions' => ['attachment', 'inline'], From 4d02761ac25b01ad9fb7312127bfb57ded1c7cdd Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Jun 2021 16:57:34 +0200 Subject: [PATCH 222/600] Protocol::createStream() exception information fixed #137 --- CHANGELOG.md | 1 + src/Connection/Protocols/Protocol.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df9044c1..0c2363a2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Fix setting default mask from config #133 (thanks @shacky) - Chunked fetch fails in case of less available mails than page size #114 +- Protocol::createStream() exception information fixed #137 ### Added - Disable rfc822 header parsing via config option #115 diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 1d70f239..a919dafa 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -194,7 +194,7 @@ protected function createStream($transport, $host, $port, $timeout) { stream_set_timeout($stream, $timeout); if (!$stream) { - throw new ConnectionFailedException("Failed to connect to host", 0, $error); + throw new ConnectionFailedException($errstr, $errno); } if (false === stream_set_timeout($stream, $timeout)) { From ea51bd73119b3de42ae0e494999f998e5d7be0ba Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Jun 2021 17:02:38 +0200 Subject: [PATCH 223/600] Legacy methods (headers, content, flags) fixed #125 --- CHANGELOG.md | 1 + src/Connection/Protocols/LegacyProtocol.php | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2363a2..5644b9e3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix setting default mask from config #133 (thanks @shacky) - Chunked fetch fails in case of less available mails than page size #114 - Protocol::createStream() exception information fixed #137 +- Legacy methods (headers, content, flags) fixed #125 ### Added - Disable rfc822 header parsing via config option #115 diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 3dc57d71..5cf71eb7 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -206,8 +206,8 @@ public function examineFolder($folder = 'INBOX') { public function content($uids, $rfc = "RFC822", $uid = false) { $result = []; $uids = is_array($uids) ? $uids : [$uids]; - foreach ($uids as $uid) { - $result[$uid] = \imap_fetchbody($this->stream, $uid, "", $uid ? IMAP::FT_UID : IMAP::NIL); + foreach ($uids as $id) { + $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::FT_UID : IMAP::NIL); } return $result; } @@ -223,8 +223,8 @@ public function content($uids, $rfc = "RFC822", $uid = false) { public function headers($uids, $rfc = "RFC822", $uid = false){ $result = []; $uids = is_array($uids) ? $uids : [$uids]; - foreach ($uids as $uid) { - $result[$uid] = \imap_fetchheader($this->stream, $uid, $uid ? IMAP::FT_UID : IMAP::NIL); + foreach ($uids as $id) { + $result[$id] = \imap_fetchheader($this->stream, $id, $uid ? IMAP::FT_UID : IMAP::NIL); } return $result; } @@ -239,8 +239,8 @@ public function headers($uids, $rfc = "RFC822", $uid = false){ public function flags($uids, $uid = false){ $result = []; $uids = is_array($uids) ? $uids : [$uids]; - foreach ($uids as $uid) { - $raw_flags = \imap_fetch_overview($this->stream, $uid, $uid ? IMAP::FT_UID : IMAP::NIL); + foreach ($uids as $id) { + $raw_flags = \imap_fetch_overview($this->stream, $id, $uid ? IMAP::FT_UID : IMAP::NIL); $flags = []; if (is_array($raw_flags) && isset($raw_flags[0])) { $raw_flags = (array) $raw_flags[0]; From 8ab4557ee7135860daa8bded7ffdb7d31fea200b Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Jun 2021 17:08:51 +0200 Subject: [PATCH 224/600] Legacy connection cycle fixed #124 --- CHANGELOG.md | 1 + src/Connection/Protocols/LegacyProtocol.php | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5644b9e3..c6eef032 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Chunked fetch fails in case of less available mails than page size #114 - Protocol::createStream() exception information fixed #137 - Legacy methods (headers, content, flags) fixed #125 +- Legacy connection cycle fixed #124 (thanks @zssarkany) ### Added - Disable rfc822 header parsing via config option #115 diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 5cf71eb7..00e24df6 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -138,6 +138,7 @@ protected function getAddress() { */ public function logout() { if ($this->stream) { + $this->stream = false; return \imap_close($this->stream, IMAP::CL_EXPUNGE); } return false; @@ -149,7 +150,7 @@ public function logout() { * @return bool */ public function connected(){ - return !$this->stream; + return boolval($this->stream); } /** From f7db7aae9b86fca7b8d5fa9971b8b8dcfb156baf Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 19 Jun 2021 17:13:15 +0200 Subject: [PATCH 225/600] Version information added --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6eef032..d6344b6e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Fix setting default mask from config #133 (thanks @shacky) -- Chunked fetch fails in case of less available mails than page size #114 -- Protocol::createStream() exception information fixed #137 -- Legacy methods (headers, content, flags) fixed #125 -- Legacy connection cycle fixed #124 (thanks @zssarkany) +- NaN ### Added -- Disable rfc822 header parsing via config option #115 +- NaN ### Affected Classes - NaN @@ -21,6 +17,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [2.5.1] - 2021-06-19 +### Fixed +- Fix setting default mask from config #133 (thanks @shacky) +- Chunked fetch fails in case of less available mails than page size #114 +- Protocol::createStream() exception information fixed #137 +- Legacy methods (headers, content, flags) fixed #125 +- Legacy connection cycle fixed #124 (thanks @zssarkany) + +### Added +- Disable rfc822 header parsing via config option #115 + ## [2.5.0] - 2021-02-01 ### Fixed - Attachment saving filename fixed From 45270d712ed662d299bc273ad65c93e4f97995c9 Mon Sep 17 00:00:00 2001 From: Szymon Janaczek <74879123+szymekjanaczek@users.noreply.github.com> Date: Sun, 25 Jul 2021 20:00:29 +0200 Subject: [PATCH 226/600] Added imap 4 handling. (#146) --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 68853c9f..814f20a4 100755 --- a/src/Client.php +++ b/src/Client.php @@ -331,7 +331,7 @@ public function connect() { $this->disconnect(); $protocol = strtolower($this->protocol); - if ($protocol == "imap") { + if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) { $this->connection = new ImapProtocol($this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); From 5ca52ab98da97ba2e281e71f608805e73815086c Mon Sep 17 00:00:00 2001 From: Webklex Date: Sun, 25 Jul 2021 20:02:19 +0200 Subject: [PATCH 227/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6344b6e..019b790b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- Added imap 4 handling. #146 (thanks @szymekjanaczek) ### Affected Classes - NaN From d071a0be8020bd48beb1f7b1cc36da6711eb5ebe Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Fri, 20 Aug 2021 21:56:02 +0200 Subject: [PATCH 228/600] Added laravel's conditionable methods. (#147) --- src/Query/WhereQuery.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index fea959db..7d78be9d 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -459,4 +459,44 @@ public function whereInReplyTo($messageId) { public function whereLanguage($country_code) { return $this->where("Content-Language $country_code"); } + + /** + * Apply the callback if the given "value" is truthy. + * copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php + * + * @param mixed $value + * @param callable $callback + * @param callable|null $default + + * @return $this|mixed + */ + public function when($value, $callback, $default = null) { + if ($value) { + return $callback($this, $value) ?: $this; + } elseif ($default) { + return $default($this, $value) ?: $this; + } + + return $this; + } + + /** + * Apply the callback if the given "value" is falsy. + * copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php + * + * @param mixed $value + * @param callable $callback + * @param callable|null $default + + * @return $this|mixed + */ + public function unless($value, $callback, $default = null) { + if (! $value) { + return $callback($this, $value) ?: $this; + } elseif ($default) { + return $default($this, $value) ?: $this; + } + + return $this; + } } \ No newline at end of file From c4fc9a3a2470ce3bad274e97ee35f2d4537c1c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Lemesle?= Date: Fri, 20 Aug 2021 21:57:53 +0200 Subject: [PATCH 229/600] POP3 fixes (#151) * Fix pop3 connect, pop3 error in case of empty mailbox * Revert imap change * docbloc about exception * src/Query/Query.php Better handling of empty mailbox search --- src/Client.php | 2 +- src/Connection/Protocols/LegacyProtocol.php | 15 +++++++++++++-- src/Query/Query.php | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Client.php b/src/Client.php index 814f20a4..1ac56467 100755 --- a/src/Client.php +++ b/src/Client.php @@ -372,7 +372,7 @@ protected function authenticate() { } elseif (!$this->connection->login($this->username, $this->password)) { throw new AuthFailedException(); } - } catch (Exception $e) { + } catch (AuthFailedException $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); } } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 00e24df6..99eff330 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -72,6 +72,7 @@ public function connect($host, $port = null) { * * @return bool * @throws AuthFailedException + * @throws RuntimeException */ public function login($user, $password) { try { @@ -79,7 +80,7 @@ public function login($user, $password) { $this->getAddress(), $user, $password, - IMAP::OP_READONLY, + 0, $attempts = 3, ClientManager::get('options.open') ); @@ -95,6 +96,15 @@ public function login($user, $password) { throw new AuthFailedException($message); } + $errors = \imap_errors(); + if(is_array($errors)) { + $status = $this->examineFolder(); + if($status['exists'] !== 0) { + $message = implode("; ", (is_array($errors) ? $errors : array())); + throw new RuntimeException($message); + } + } + return $this->stream; } @@ -138,8 +148,9 @@ protected function getAddress() { */ public function logout() { if ($this->stream) { + $result = \imap_close($this->stream, IMAP::CL_EXPUNGE); $this->stream = false; - return \imap_close($this->stream, IMAP::CL_EXPUNGE); + return $result; } return false; } diff --git a/src/Query/Query.php b/src/Query/Query.php index df93b461..b1806755 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -185,7 +185,7 @@ protected function search() { try { $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence == IMAP::ST_UID); - return new Collection($available_messages); + return $available_messages !== false ? new Collection($available_messages) : new Collection(); } catch (RuntimeException $e) { throw new GetMessagesFailedException("failed to fetch messages", 0, $e); } catch (ConnectionFailedException $e) { @@ -339,7 +339,7 @@ public function get() { $available_messages = $this->search(); try { - if (($available_messages_count = $available_messages->count()) > 0) { + if ($available_messages->count() > 0) { return $this->populate($available_messages); } return MessageCollection::make([]); From ad6b77b68ed51d3962966751477970e026193e66 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 20 Aug 2021 22:04:47 +0200 Subject: [PATCH 230/600] Release information added --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 019b790b..bcd36509 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- Added imap 4 handling. #146 (thanks @szymekjanaczek) +- NaN ### Affected Classes - NaN @@ -17,6 +17,18 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [2.6.0] - 2021-08-20 +### Fixed +- POP3 fixes #151 (thanks @Korko) + +### Added +- Added imap 4 handling. #146 (thanks @szymekjanaczek) +- Added laravel's conditionable methods. #147 (thanks @szymekjanaczek) + +### Affected Classes +- [Query::class](src/Query/Query.php) +- [Client::class](src/Client.php) + ## [2.5.1] - 2021-06-19 ### Fixed - Fix setting default mask from config #133 (thanks @shacky) From ff91bee8b7c31a26049b7168d14f50014c5235eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kleszczy=C5=84ski?= Date: Tue, 24 Aug 2021 17:06:21 +0200 Subject: [PATCH 231/600] Expose message folder path (#154) --- src/Message.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Message.php b/src/Message.php index adcb7ed7..5b501b39 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1265,6 +1265,15 @@ public function mask($mask = null){ throw new MaskNotFoundException("Unknown mask provided: ".$mask); } + /** + * Get the message path aka folder path + * + * @return string + */ + public function getFolderPath(){ + return $this->folder_path; + } + /** * Set the message path aka folder path * @param $folder_path From fa4de50bc1d5d998a719e3e2c8ca20e68204881d Mon Sep 17 00:00:00 2001 From: Oliver-Holz <49278665+Oliver-Holz@users.noreply.github.com> Date: Sat, 4 Sep 2021 11:36:25 +0200 Subject: [PATCH 232/600] Fixes Multi-Line Headers and handles multiple Adresses (#159) * fixes handling of long header lines which are seperated by \r\n\t * fixes to line parsing with multiple addresses * adds mailparse_rfc822_parse_addresses integration --- src/Header.php | 61 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Header.php b/src/Header.php index d59bfe31..9fecd69f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -241,8 +241,7 @@ public function rfc822_parse_headers($raw_headers){ $imap_headers[$key] = $values; } } - - $lines = explode("\r\n", $raw_headers); + $lines = explode("\r\n", str_replace("\r\n\t", ' ', $raw_headers)); $prev_header = null; foreach($lines as $line) { if (substr($line, 0, 1) === "\n") { @@ -539,26 +538,50 @@ private function findPriority() { */ private function decodeAddresses($values) { $addresses = []; - foreach($values as $address) { - $address = trim(rtrim($address)); - if (strpos($address, ",") == strlen($address) - 1) { - $address = substr($address, 0, -1); + + if (extension_loaded('mailparse') && $this->config["rfc822"]) { + foreach ($values as $address) { + foreach (\mailparse_rfc822_parse_addresses($address) as $parsed_address) { + if (isset($parsed_address['address'])) { + $mail_address = explode('@', $parsed_address['address']); + if (count($mail_address) == 2) { + $addresses[] = (object)[ + "personal" => isset($parsed_address['display']) ? $parsed_address['display'] : '', + "mailbox" => $mail_address[0], + "host" => $mail_address[1], + ]; + } + } + } } - if (preg_match( - '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', - $address, - $matches - )){ - $name = trim(rtrim($matches["name"])); - $email = trim(rtrim($matches["email"])); - list($mailbox, $host) = array_pad(explode("@", $email), 2, null); - $addresses[] = (object) [ - "personal" => $name, - "mailbox" => $mailbox, - "host" => $host, - ]; + + return $addresses; + } + + foreach($values as $address) { + foreach (preg_split('/, (?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { + $split_address = trim(rtrim($split_address)); + + if (strpos($split_address, ",") == strlen($split_address) - 1) { + $split_address = substr($split_address, 0, -1); + } + if (preg_match( + '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', + $split_address, + $matches + )) { + $name = trim(rtrim($matches["name"])); + $email = trim(rtrim($matches["email"])); + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + $addresses[] = (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ]; + } } } + return $addresses; } From d2033e7e2ed842880a9e29820adf2e3671e88d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kleszczy=C5=84ski?= Date: Sat, 4 Sep 2021 11:39:14 +0200 Subject: [PATCH 233/600] Added methods for moving and copying multiple messages (#160) * Added moveManyMessages method * Added copyManyMessages method * Updated docblocks to better explain return type --- src/Connection/Protocols/ImapProtocol.php | 40 ++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index be1f0309..ddc77c4d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -824,6 +824,25 @@ public function copyMessage($folder, $from, $to = null, $uid = false) { return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } + /** + * Copy multiple messages to the target folder + * + * @param array $messages List of message identifiers + * @param string $folder Destination folder + * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @return array|bool Tokens if operation successful, false if an error occurred + * + * @throws RuntimeException + */ + public function copyManyMessages($messages, $folder, $uid = false) { + $command = $uid ? 'UID COPY' : 'COPY'; + + $set = implode(',', $messages); + $tokens = [$set, $this->escapeString($folder)]; + + return $this->requestAndResponse($command, $tokens, true); + } + /** * Move a message set from current folder to an other folder * @param string $folder destination folder @@ -845,6 +864,25 @@ public function moveMessage($folder, $from, $to = null, $uid = false) { return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } + /** + * Move multiple messages to the target folder + * + * @param array $messages List of message identifiers + * @param string $folder Destination folder + * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @return array|bool Tokens if operation successful, false if an error occurred + * + * @throws RuntimeException + */ + public function moveManyMessages($messages, $folder, $uid = false) { + $command = $uid ? 'UID MOVE' : 'MOVE'; + + $set = implode(',', $messages); + $tokens = [$set, $this->escapeString($folder)]; + + return $this->requestAndResponse($command, $tokens, true); + } + /** * Create a new folder (and parent folders if needed) * @param string $folder folder name @@ -1032,4 +1070,4 @@ public function enableDebug(){ public function disableDebug(){ $this->debug = false; } -} \ No newline at end of file +} From 7779e0b3fa56fd5e7bc71b7677e71c462b68168b Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 11:42:29 +0200 Subject: [PATCH 234/600] Changelog updated --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd36509..b6d24d92 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,18 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fixes handling of long header lines which are seperated by `\r\n\t` (thanks @Oliver-Holz) +- Fixes to line parsing with multiple addresses (thanks @Oliver-Holz) ### Added -- NaN +- Expose message folder path #154 (thanks @Magiczne) +- Adds mailparse_rfc822_parse_addresses integration (thanks @Oliver-Holz) +- Added moveManyMessages method (thanks @Magiczne) +- Added copyManyMessages method (thanks @Magiczne) ### Affected Classes -- NaN +- [Header::class](src/Header.php) +- [Message::class](src/Message.php) ### Breaking changes - NaN From 7d5a7ff48d8e83864ca2a0524e78ca3caa091935 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 16:55:45 +0200 Subject: [PATCH 235/600] unused variable removed --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ddc77c4d..2de86fdf 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1050,7 +1050,7 @@ public function overview($sequence, $uid = false) { $ids[] = $id; } } - $headers = $this->headers($ids, $rfc = "RFC822", $uid); + $headers = $this->headers($ids, "RFC822", $uid); foreach ($headers as $id => $raw_header) { $result[$id] = (new Header($raw_header, false))->getAttributes(); } From 365905905b257136d56f8a7e8a8d61303345e14f Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 16:57:13 +0200 Subject: [PATCH 236/600] Interface method option name changed to match all others --- src/Connection/Protocols/ProtocolInterface.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index ff470bbf..d3885466 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -161,11 +161,11 @@ public function getMessageNumber($id); * Get a list of available folders * * @param string $reference mailbox reference for list - * @param string $mailbox mailbox name match with wildcards - * @return array mailboxes that matched $mailbox as array(globalName => array('delim' => .., 'flags' => ..)) + * @param string $folder mailbox / folder name match with wildcards + * @return array mailboxes that matched $folder as array(globalName => array('delim' => .., 'flags' => ..)) * @throws RuntimeException */ - public function folders($reference = '', $mailbox = '*'); + public function folders($reference = '', $folder = '*'); /** * Set message flags From a070216b6fb622255a7e15ce47dfbd552de572e5 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 16:57:35 +0200 Subject: [PATCH 237/600] doc tags updated --- src/Connection/Protocols/ImapProtocol.php | 2 +- src/Connection/Protocols/LegacyProtocol.php | 7 +++---- src/Connection/Protocols/Protocol.php | 1 - src/Connection/Protocols/ProtocolInterface.php | 3 ++- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 2de86fdf..f5a86796 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -385,7 +385,7 @@ public function login($user, $password) { * @param string $user username * @param string $token access token * - * @return bool|mixed + * @return bool * @throws AuthFailedException */ public function authenticate($user, $token) { diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 99eff330..6bfa8fba 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -51,7 +51,6 @@ public function __destruct() { * Save the information for a nw connection * @param string $host * @param null $port - * @param bool $encryption */ public function connect($host, $port = null) { if ($this->encryption) { @@ -113,8 +112,8 @@ public function login($user, $password) { * @param string $user username * @param string $token access token * - * @return bool|mixed - * @throws AuthFailedException + * @return bool|resource + * @throws AuthFailedException|RuntimeException */ public function authenticate($user, $token) { return $this->login($user, $token); @@ -189,7 +188,7 @@ public function selectFolder($folder = 'INBOX') { * Examine a given folder * @param string $folder examine this folder * - * @return bool|array see examineOrselect() + * @return bool|array * @throws RuntimeException */ public function examineFolder($folder = 'INBOX') { diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index a919dafa..ef01d46e 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -183,7 +183,6 @@ private function defaultSocketOptions($transport) { * * @return resource|boolean The socket created. * @throws ConnectionFailedException - * @throws \ErrorException */ protected function createStream($transport, $host, $port, $timeout) { $socket = "$transport://$host:$port"; diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index d3885466..a170d546 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; +use ErrorException; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -41,7 +42,7 @@ public function __destruct(); * @param string $host hostname or IP address of IMAP server * @param int|null $port of service server * - * @throws \ErrorException + * @throws ErrorException * @throws ConnectionFailedException * @throws RuntimeException */ From 031dc82508f8aa6eb380c63ac1c7926ed8c76f7f Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 16:58:05 +0200 Subject: [PATCH 238/600] release information added --- CHANGELOG.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6d24d92..3833b9c3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [2.7.0] - 2021-09-04 +### Fixed - Fixes handling of long header lines which are seperated by `\r\n\t` (thanks @Oliver-Holz) - Fixes to line parsing with multiple addresses (thanks @Oliver-Holz) @@ -19,9 +32,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Header::class](src/Header.php) - [Message::class](src/Message.php) -### Breaking changes -- NaN - ## [2.6.0] - 2021-08-20 ### Fixed - POP3 fixes #151 (thanks @Korko) From 622fd5db9a2c8f83c45788d43c5388304f522afd Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 17:14:47 +0200 Subject: [PATCH 239/600] Protocoll interface updated copyManyMessages() and moveManyMessages() added --- .../Protocols/ProtocolInterface.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index a170d546..6770d8d4 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -210,6 +210,18 @@ public function appendMessage($folder, $message, $flags = null, $date = null); */ public function copyMessage($folder, $from, $to = null, $uid = false); + /** + * Copy multiple messages to the target folder + * + * @param array $messages List of message identifiers + * @param string $folder Destination folder + * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @return array|bool Tokens if operation successful, false if an error occurred + * + * @throws RuntimeException + */ + public function copyManyMessages($messages, $folder, $uid = false); + /** * Move a message set from current folder to an other folder * @param string $folder destination folder @@ -222,6 +234,18 @@ public function copyMessage($folder, $from, $to = null, $uid = false); */ public function moveMessage($folder, $from, $to = null, $uid = false); + /** + * Move multiple messages to the target folder + * + * @param array $messages List of message identifiers + * @param string $folder Destination folder + * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @return array|bool Tokens if operation successful, false if an error occurred + * + * @throws RuntimeException + */ + public function moveManyMessages($messages, $folder, $uid = false); + /** * Create a new folder * From bd123fa62fb597ddd9a43d1b05d377fc73bb57a2 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 17:15:14 +0200 Subject: [PATCH 240/600] Legacy support for moveManyMessages and copyManyMessages added --- src/Connection/Protocols/LegacyProtocol.php | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 6bfa8fba..96cc733f 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -392,6 +392,24 @@ public function copyMessage($folder, $from, $to = null, $uid = false) { return \imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::FT_UID : IMAP::NIL); } + /** + * Copy multiple messages to the target folder + * + * @param array $messages List of message identifiers + * @param string $folder Destination folder + * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @return array|bool Tokens if operation successful, false if an error occurred + */ + public function copyManyMessages($messages, $folder, $uid = false) { + foreach($messages as $msg) { + if ($this->copyMessage($folder, $msg, null, $uid) == false) { + return false; + } + } + + return $messages; + } + /** * Move a message set from current folder to an other folder * @param string $folder destination folder @@ -406,6 +424,24 @@ public function moveMessage($folder, $from, $to = null, $uid = false) { return \imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::FT_UID : IMAP::NIL); } + /** + * Move multiple messages to the target folder + * + * @param array $messages List of message identifiers + * @param string $folder Destination folder + * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @return array|bool Tokens if operation successful, false if an error occurred + */ + public function moveManyMessages($messages, $folder, $uid = false) { + foreach($messages as $msg) { + if ($this->moveMessage($folder, $msg, null, $uid) == false) { + return false; + } + } + + return $messages; + } + /** * Create a new folder (and parent folders if needed) * @param string $folder folder name From 47d03dd0ef58e8cced97681f5809faa589a7ee67 Mon Sep 17 00:00:00 2001 From: Webklex Date: Sat, 4 Sep 2021 17:33:12 +0200 Subject: [PATCH 241/600] dont decode IMAP::MESSAGE_ENC_8BIT encoded messages #155 --- src/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Message.php b/src/Message.php index 5b501b39..90ed1a89 100755 --- a/src/Message.php +++ b/src/Message.php @@ -681,9 +681,9 @@ public function decodeString($string, $encoding) { return base64_decode($string); case IMAP::MESSAGE_ENC_BASE64: return base64_decode($string); - case IMAP::MESSAGE_ENC_8BIT: case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE: return quoted_printable_decode($string); + case IMAP::MESSAGE_ENC_8BIT: case IMAP::MESSAGE_ENC_7BIT: case IMAP::MESSAGE_ENC_OTHER: default: From a17b5adb557985797afcd948a873db5c85f4ff94 Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Tue, 7 Sep 2021 15:36:51 +0200 Subject: [PATCH 242/600] Added searching by UID (#161) * Added `UID` as available criteria. * Added methods to simplify searching by UID. --- src/Query/WhereQuery.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 7d78be9d..1084f02a 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -62,7 +62,7 @@ class WhereQuery extends Query { 'OR', 'AND', 'ALL', 'ANSWERED', 'BCC', 'BEFORE', 'BODY', 'CC', 'DELETED', 'FLAGGED', 'FROM', 'KEYWORD', 'NEW', 'NOT', 'OLD', 'ON', 'RECENT', 'SEEN', 'SINCE', 'SUBJECT', 'TEXT', 'TO', - 'UNANSWERED', 'UNDELETED', 'UNFLAGGED', 'UNKEYWORD', 'UNSEEN' + 'UNANSWERED', 'UNDELETED', 'UNFLAGGED', 'UNKEYWORD', 'UNSEEN', 'UID' ]; /** @@ -460,6 +460,33 @@ public function whereLanguage($country_code) { return $this->where("Content-Language $country_code"); } + /** + * Get message be it UID. + * + * @param int|string $uid + * + * @return WhereQuery + * @throws InvalidWhereQueryCriteriaException + */ + public function whereUid($uid) + { + return $this->where('UID', $uid); + } + + /** + * Get messages by their UIDs. + * + * @param array $uids + * + * @return WhereQuery + * @throws InvalidWhereQueryCriteriaException + */ + public function whereUidIn($uids) + { + $uids = implode(',', $uids); + return $this->where('UID', $uids); + } + /** * Apply the callback if the given "value" is truthy. * copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php From dab403669615dc5420ee40ec5e79f8f0fed5cf81 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 8 Sep 2021 17:40:56 +0200 Subject: [PATCH 243/600] release information added --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3833b9c3..b33c76be 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- Added `UID` as available search criteria #161 (thanks @szymekjanaczek) ### Affected Classes - NaN @@ -17,6 +17,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [2.7.1] - 2021-09-08 +### Added +- Added `UID` as available search criteria #161 (thanks @szymekjanaczek) + ## [2.7.0] - 2021-09-04 ### Fixed - Fixes handling of long header lines which are seperated by `\r\n\t` (thanks @Oliver-Holz) From c4bd117e272eac7184d6a5452f1408d7578d05ac Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 8 Sep 2021 17:41:31 +0200 Subject: [PATCH 244/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b33c76be..86bdf6b7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- Added `UID` as available search criteria #161 (thanks @szymekjanaczek) +- NaN ### Affected Classes - NaN From cd87ea698172e7e9892dbded977b895e173a849d Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Sat, 25 Sep 2021 20:20:50 +0200 Subject: [PATCH 245/600] Fixed problem with skipping last line of the response. (#166) --- src/Connection/Protocols/ImapProtocol.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index f5a86796..99da29d3 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -254,9 +254,10 @@ public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false) { public function readResponse($tag, $dontParse = false) { $lines = []; $tokens = null; // define $tokens variable before first use - while (!$this->readLine($tokens, $tag, $dontParse)) { + do { + $readAll = $this->readLine($tokens, $tag, $dontParse); $lines[] = $tokens; - } + } while (!$readAll); if ($dontParse) { // First two chars are still needed for the response code From 301335de24df88d911d59bbc68431e09bda162cf Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 27 Sep 2021 06:15:35 +0200 Subject: [PATCH 246/600] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86bdf6b7..2a2c36f7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [2.7.2] - 2021-09-27 +### Added +- Fixed problem with skipping last line of the response. #166 (thanks @szymekjanaczek) + ## [2.7.1] - 2021-09-08 ### Added - Added `UID` as available search criteria #161 (thanks @szymekjanaczek) From e22be3ed3760a939cd68b2907969c4b2478e8588 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 27 Sep 2021 06:16:22 +0200 Subject: [PATCH 247/600] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2c36f7..a2ce640d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ## [2.7.2] - 2021-09-27 -### Added +### Fixed - Fixed problem with skipping last line of the response. #166 (thanks @szymekjanaczek) ## [2.7.1] - 2021-09-08 From 473129393afa790bbc16eaa0788b6df7a85efc5b Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Oct 2021 23:42:26 +0200 Subject: [PATCH 248/600] Make boundary regex configurable --- CHANGELOG.md | 2 +- src/Header.php | 3 ++- src/config/imap.php | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ce640d..c859d828 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- Make boundary regex configurable #108 #152 (thanks @EthraZa) ### Affected Classes - NaN diff --git a/src/Header.php b/src/Header.php index 9fecd69f..a4bd23d8 100644 --- a/src/Header.php +++ b/src/Header.php @@ -173,7 +173,8 @@ public function find($pattern) { * @return string|null */ public function getBoundary(){ - $boundary = $this->find("/boundary\=(.*)/i"); + $regex = isset($this->config["boundary"]) ? $this->config["boundary"] : "/boundary=(.*?(?=;)|(.*))/i"; + $boundary = $this->find($regex); if ($boundary === null) { return null; diff --git a/src/config/imap.php b/src/config/imap.php index 1b605ee0..d95e637b 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -143,6 +143,7 @@ 'fetch_flags' => true, 'soft_fail' => false, 'rfc822' => true, + 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', 'message_key' => 'list', 'fetch_order' => 'asc', 'dispositions' => ['attachment', 'inline'], From 8cf23675169a22aa9eb7a6a3c15947b7f8d4cf76 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Oct 2021 23:44:12 +0200 Subject: [PATCH 249/600] Extend date parsing error message #173 --- CHANGELOG.md | 2 +- src/Header.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c859d828..ea230d5c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Extend date parsing error message #173 ### Added - Make boundary regex configurable #108 #152 (thanks @EthraZa) diff --git a/src/Header.php b/src/Header.php index a4bd23d8..47f9b687 100644 --- a/src/Header.php +++ b/src/Header.php @@ -734,7 +734,8 @@ private function parseDate($header) { try{ $parsed_date = Carbon::parse($date); } catch (\Exception $_e) { - throw new InvalidMessageDateException("Invalid message date. ID:".$this->get("message_id"), 1100, $e); + $error_message = "Invalid message date. ID:".$this->get("message_id")." Date:".$header->date."/".$date; + throw new InvalidMessageDateException($error_message, 1100, $e); } } From 8a8321c5ac8dbc6935b90d0495906e7752425f23 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 28 Oct 2021 23:52:04 +0200 Subject: [PATCH 250/600] Boundary regex config doc added #169 #150 #126 #121 #111 #152 #108 --- CHANGELOG.md | 2 +- src/config/imap.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea230d5c..2470d7a3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Extend date parsing error message #173 ### Added -- Make boundary regex configurable #108 #152 (thanks @EthraZa) +- Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) ### Affected Classes - NaN diff --git a/src/config/imap.php b/src/config/imap.php index d95e637b..e5ec7f1a 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -111,6 +111,8 @@ | -RFC822 | Default TRUE - Set to FALSE to prevent the usage of \imap_rfc822_parse_headers(). | See https://github.com/Webklex/php-imap/issues/115 for more information. + | -Boundary regex used to detect message boundaries. If you are having problems with empty messages, missing + | attachments or anything like this. Be advised that it likes to break which causes new problems.. | -Message key identifier option | You can choose between the following: | 'id' - Use the MessageID as array key (default, might cause hickups with yahoo mail) From d2e22ae1e021b5237318c59778ea4642517090a4 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Oct 2021 00:14:06 +0200 Subject: [PATCH 251/600] IMAP ID support added #174 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 22 +++++++++++++++++++ src/Connection/Protocols/LegacyProtocol.php | 13 +++++++++++ .../Protocols/ProtocolInterface.php | 11 ++++++++++ 4 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2470d7a3..cd8fc5cd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) +- IMAP ID support added #174 ### Affected Classes - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 99da29d3..1336d905 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -884,6 +884,28 @@ public function moveManyMessages($messages, $folder, $uid = false) { return $this->requestAndResponse($command, $tokens, true); } + /** + * Exchange identification information + * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 + * + * @param null $ids + * @return array|bool|void|null + * + * @throws RuntimeException + */ + public function ID($ids = null) { + $token = "NIL"; + if (is_array($ids) && !empty($ids)) { + $token = "("; + foreach ($ids as $id) { + $token .= '"'.$id.'" '; + } + $token = rtrim($token).")"; + } + + return $this->requestAndResponse("ID", [$token], true); + } + /** * Create a new folder (and parent folders if needed) * @param string $folder folder name diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 96cc733f..f5db288c 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -442,6 +442,19 @@ public function moveManyMessages($messages, $folder, $uid = false) { return $messages; } + /** + * Exchange identification information + * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 + * + * @param null $ids + * @return array|bool|void|null + * + * @throws MethodNotSupportedException + */ + public function ID($ids = null) { + throw new MethodNotSupportedException(); + } + /** * Create a new folder (and parent folders if needed) * @param string $folder folder name diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 6770d8d4..026f4c53 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -246,6 +246,17 @@ public function moveMessage($folder, $from, $to = null, $uid = false); */ public function moveManyMessages($messages, $folder, $uid = false); + /** + * Exchange identification information + * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 + * + * @param null $ids + * @return array|bool|void|null + * + * @throws RuntimeException + */ + public function ID($ids = null); + /** * Create a new folder * From a42b86345406e486243de0cc69769768cd85de31 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 29 Oct 2021 00:20:22 +0200 Subject: [PATCH 252/600] Affected classes added to the changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8fc5cd..acab6150 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - IMAP ID support added #174 ### Affected Classes -- NaN +- [Header::class](src/Header.php) +- [Protocol::class](src/Connection/Protocols/Protocol.php) ### Breaking changes - NaN From 1880684d741de67e35a646a382e6d53ec79c3fc1 Mon Sep 17 00:00:00 2001 From: Webklex Date: Mon, 1 Nov 2021 15:25:33 +0100 Subject: [PATCH 253/600] IMAP ID quick access method added --- src/Client.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 1ac56467..8974e2f2 100755 --- a/src/Client.php +++ b/src/Client.php @@ -13,7 +13,6 @@ namespace Webklex\PHPIMAP; use ErrorException; -use Exception; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol; use Webklex\PHPIMAP\Connection\Protocols\Protocol; @@ -546,6 +545,21 @@ public function getFolderPath(){ return $this->active_folder; } + /** + * Exchange identification information + * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 + * + * @param null|array $ids + * @return array|bool|void|null + * + * @throws ConnectionFailedException + * @throws Exceptions\RuntimeException + */ + public function Id($ids = null) { + $this->checkConnection(); + return $this->connection->ID($ids); + } + /** * Retrieve the quota level settings, and usage statics per mailbox * From 133a7b1050efa11aed59e4b117851ca20a5210f0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:23:06 +0100 Subject: [PATCH 254/600] Fixed 'Where' method replaces the content with uppercase #148 --- CHANGELOG.md | 2 ++ src/Query/WhereQuery.php | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acab6150..aaf57f84 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Extend date parsing error message #173 +- Fixed 'Where' method replaces the content with uppercase #148 ### Added - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) @@ -15,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Affected Classes - [Header::class](src/Header.php) - [Protocol::class](src/Connection/Protocols/Protocol.php) +- [Query::class](src/Query/Query.php) ### Breaking changes - NaN diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 1084f02a..e8cf2577 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -105,11 +105,11 @@ public function __call($name, $arguments) { * @throws InvalidWhereQueryCriteriaException */ protected function validate_criteria($criteria) { - $criteria = strtoupper($criteria); - if (substr($criteria, 0, 7) === "CUSTOM ") { + $command = strtoupper($criteria); + if (substr($command, 0, 7) === "CUSTOM ") { return substr($criteria, 7); } - if (in_array($criteria, $this->available_criteria) === false) { + if (in_array($command, $this->available_criteria) === false) { throw new InvalidWhereQueryCriteriaException(); } From ee64b9b14dc329002a41375bccb2594375cc41d7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:33:51 +0100 Subject: [PATCH 255/600] Enable debug mode via config --- src/Client.php | 4 ++++ src/config/imap.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/Client.php b/src/Client.php index 8974e2f2..8759126f 100755 --- a/src/Client.php +++ b/src/Client.php @@ -345,6 +345,10 @@ public function connect() { $this->connection->setProtocol($protocol); } + if (ClientManager::get('options.debug')) { + $this->connection->enableDebug(); + } + try { $this->connection->connect($this->host, $this->port); } catch (ErrorException $e) { diff --git a/src/config/imap.php b/src/config/imap.php index e5ec7f1a..f8fd6405 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -111,6 +111,7 @@ | -RFC822 | Default TRUE - Set to FALSE to prevent the usage of \imap_rfc822_parse_headers(). | See https://github.com/Webklex/php-imap/issues/115 for more information. + | -Debug enable to trace communication traffic | -Boundary regex used to detect message boundaries. If you are having problems with empty messages, missing | attachments or anything like this. Be advised that it likes to break which causes new problems.. | -Message key identifier option @@ -145,6 +146,7 @@ 'fetch_flags' => true, 'soft_fail' => false, 'rfc822' => true, + 'debug' => false, 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', 'message_key' => 'list', 'fetch_order' => 'asc', From d410c2033eaebb22d0ed65fd8e4abec5378a6834 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:34:05 +0100 Subject: [PATCH 256/600] changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf57f84..97b8b5e6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) - IMAP ID support added #174 +- Enable debug mode via config ### Affected Classes - [Header::class](src/Header.php) From afbe643e62913b50bb365cc99688037209197329 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:35:35 +0100 Subject: [PATCH 257/600] Don't surround numeric search values with quotes --- CHANGELOG.md | 1 + src/Query/Query.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b8b5e6..f55d5954 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Extend date parsing error message #173 - Fixed 'Where' method replaces the content with uppercase #148 +- Don't surround numeric search values with quotes ### Added - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) diff --git a/src/Query/Query.php b/src/Query/Query.php index b1806755..0186d3be 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -162,7 +162,11 @@ public function generate_query() { if ($statement[1] === null) { $query .= $statement[0]; } else { - $query .= $statement[0] . ' "' . $statement[1] . '"'; + if (is_numeric($statement[1])) { + $query .= $statement[0] . ' ' . $statement[1]; + } else { + $query .= $statement[0] . ' "' . $statement[1] . '"'; + } } } $query .= ' '; From 307dc660c5b94878a8cdd0e9f617c01a45125c2d Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:44:37 +0100 Subject: [PATCH 258/600] Custom UID alternative support added --- src/Connection/Protocols/ImapProtocol.php | 79 +++++++++++-------- src/Connection/Protocols/LegacyProtocol.php | 63 +++++++-------- src/Connection/Protocols/Protocol.php | 18 +++++ .../Protocols/ProtocolInterface.php | 61 ++++++++------ src/Query/Query.php | 21 +++-- src/config/imap.php | 2 +- 6 files changed, 148 insertions(+), 96 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 1336d905..b7f71037 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -19,6 +19,7 @@ use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Header; +use Webklex\PHPIMAP\IMAP; /** * Class ImapProtocol @@ -532,7 +533,8 @@ public function examineFolder($folder = 'INBOX') { * @param int|array $from message for items or start message if $to !== null * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return string|array if only one item of one message is fetched it's returned as string * if items of one message are fetched it's returned as (name => value) @@ -540,7 +542,7 @@ public function examineFolder($folder = 'INBOX') { * if items of messages are fetched it's returned as (msgno => (name => value)) * @throws RuntimeException */ - protected function fetch($items, $from, $to = null, $uid = false) { + public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { if (is_array($from)) { $set = implode(',', $from); } elseif ($to === null) { @@ -554,8 +556,7 @@ protected function fetch($items, $from, $to = null, $uid = false) { $items = (array)$items; $itemList = $this->escapeList($items); - $this->sendRequest(($uid ? 'UID ' : '') . 'FETCH', [$set, $itemList], $tag); - + $this->sendRequest(trim($this->getUIDKey($uid) . ' FETCH'), [$set, $itemList], $tag); $result = []; $tokens = null; // define $tokens variable before first use while (!$this->readLine($tokens, $tag)) { @@ -632,12 +633,13 @@ protected function fetch($items, $from, $to = null, $uid = false) { * Fetch message headers * @param array|int $uids * @param string $rfc - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException */ - public function content($uids, $rfc = "RFC822", $uid = false) { + public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID) { return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); } @@ -645,24 +647,26 @@ public function content($uids, $rfc = "RFC822", $uid = false) { * Fetch message headers * @param array|int $uids * @param string $rfc - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException */ - public function headers($uids, $rfc = "RFC822", $uid = false){ + public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID){ return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); } /** * Fetch message flags * @param array|int $uids - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException */ - public function flags($uids, $uid = false){ + public function flags($uids, $uid = IMAP::ST_UID){ return $this->fetch(["FLAGS"], $uids, null, $uid); } @@ -741,16 +745,21 @@ public function folders($reference = '', $folder = '*') { * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. + * @param null|string $item command used to store a flag * * @return bool|array new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false) { - $item = 'FLAGS'; + public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID, $item = null) { + if ($item === null) { + $item = 'FLAGS'; + } if ($mode == '+' || $mode == '-') { $item = $mode . $item; } + if ($silent) { $item .= '.SILENT'; } @@ -810,18 +819,18 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return bool success * @throws RuntimeException */ - public function copyMessage($folder, $from, $to = null, $uid = false) { + public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { $set = (int)$from; if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); } - $command = ($uid ? "UID " : "")."COPY"; - + $command = trim($this->getUIDKey($uid)." COPY"); return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } @@ -830,13 +839,14 @@ public function copyMessage($folder, $from, $to = null, $uid = false) { * * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * @return array|bool Tokens if operation successful, false if an error occurred * * @throws RuntimeException */ - public function copyManyMessages($messages, $folder, $uid = false) { - $command = $uid ? 'UID COPY' : 'COPY'; + public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { + $command = trim($this->getUIDKey($uid)." COPY"); $set = implode(',', $messages); $tokens = [$set, $this->escapeString($folder)]; @@ -850,33 +860,34 @@ public function copyManyMessages($messages, $folder, $uid = false) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return bool success * @throws RuntimeException */ - public function moveMessage($folder, $from, $to = null, $uid = false) { + public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { $set = (int)$from; if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); } - $command = ($uid ? "UID " : "")."MOVE"; + $command = trim($this->getUIDKey($uid)." MOVE"); return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** * Move multiple messages to the target folder - * * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param bool $uid Set to true if you pass message unique identifiers instead of numbers - * @return array|bool Tokens if operation successful, false if an error occurred + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * + * @return array|bool Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function moveManyMessages($messages, $folder, $uid = false) { - $command = $uid ? 'UID MOVE' : 'MOVE'; + public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID) { + $command = trim($this->getUIDKey($uid)." MOVE"); $set = implode(',', $messages); $tokens = [$set, $this->escapeString($folder)]; @@ -1030,13 +1041,14 @@ public function done() { /** * Search for matching messages * @param array $params - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array message ids * @throws RuntimeException */ - public function search(array $params, $uid = false) { - $token = $uid == true ? "UID SEARCH" : "SEARCH"; + public function search(array $params, $uid = IMAP::ST_UID) { + $token = trim($this->getUIDKey($uid)." SEARCH"); $response = $this->requestAndResponse($token, $params); if (!$response) { return $response; @@ -1054,14 +1066,15 @@ public function search(array $params, $uid = false) { /** * Get a message overview * @param string $sequence - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException * @throws MessageNotFoundException * @throws InvalidMessageDateException */ - public function overview($sequence, $uid = false) { + public function overview($sequence, $uid = IMAP::ST_UID) { $result = []; list($from, $to) = explode(":", $sequence); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index f5db288c..24554556 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -210,15 +210,15 @@ public function examineFolder($folder = 'INBOX') { * Fetch message content * @param array|int $uids * @param string $rfc - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return array */ - public function content($uids, $rfc = "RFC822", $uid = false) { + public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID) { $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { - $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::FT_UID : IMAP::NIL); + $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::ST_UID : IMAP::NIL); } return $result; } @@ -227,15 +227,15 @@ public function content($uids, $rfc = "RFC822", $uid = false) { * Fetch message headers * @param array|int $uids * @param string $rfc - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return array */ - public function headers($uids, $rfc = "RFC822", $uid = false){ + public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID){ $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { - $result[$id] = \imap_fetchheader($this->stream, $id, $uid ? IMAP::FT_UID : IMAP::NIL); + $result[$id] = \imap_fetchheader($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL); } return $result; } @@ -243,15 +243,15 @@ public function headers($uids, $rfc = "RFC822", $uid = false){ /** * Fetch message flags * @param array|int $uids - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return array */ - public function flags($uids, $uid = false){ + public function flags($uids, $uid = IMAP::ST_UID){ $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { - $raw_flags = \imap_fetch_overview($this->stream, $id, $uid ? IMAP::FT_UID : IMAP::NIL); + $raw_flags = \imap_fetch_overview($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL); $flags = []; if (is_array($raw_flags) && isset($raw_flags[0])) { $raw_flags = (array) $raw_flags[0]; @@ -298,12 +298,12 @@ public function getMessageNumber($id) { /** * Get a message overview * @param string $sequence uid sequence - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return array */ - public function overview($sequence, $uid = false) { - return \imap_fetch_overview($this->stream, $sequence,$uid ? IMAP::FT_UID : IMAP::NIL); + public function overview($sequence, $uid = IMAP::ST_UID) { + return \imap_fetch_overview($this->stream, $sequence,$uid ? IMAP::ST_UID : IMAP::NIL); } /** @@ -338,17 +338,17 @@ public function folders($reference = '', $folder = '*') { * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return bool|array new flags if $silent is false, else true or false depending on success */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false) { + public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID) { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); if ($mode == "+"){ - $status = \imap_setflag_full($this->stream, $from, $flag, $uid ? IMAP::FT_UID : IMAP::NIL); + $status = \imap_setflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL); }else{ - $status = \imap_clearflag_full($this->stream, $from, $flag, $uid ? IMAP::FT_UID : IMAP::NIL); + $status = \imap_clearflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL); } if ($silent === true) { @@ -384,23 +384,23 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return bool success */ - public function copyMessage($folder, $from, $to = null, $uid = false) { - return \imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::FT_UID : IMAP::NIL); + public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { + return \imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL); } /** * Copy multiple messages to the target folder - * * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * * @return array|bool Tokens if operation successful, false if an error occurred */ - public function copyManyMessages($messages, $folder, $uid = false) { + public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { foreach($messages as $msg) { if ($this->copyMessage($folder, $msg, null, $uid) == false) { return false; @@ -416,23 +416,23 @@ public function copyManyMessages($messages, $folder, $uid = false) { * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return bool success */ - public function moveMessage($folder, $from, $to = null, $uid = false) { - return \imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::FT_UID : IMAP::NIL); + public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { + return \imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL); } /** * Move multiple messages to the target folder - * * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param bool $uid Set to true if you pass message unique identifiers instead of numbers + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * * @return array|bool Tokens if operation successful, false if an error occurred */ - public function moveManyMessages($messages, $folder, $uid = false) { + public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID) { foreach($messages as $msg) { if ($this->moveMessage($folder, $msg, null, $uid) == false) { return false; @@ -544,12 +544,13 @@ public function done() { /** * Search for matching messages - * * @param array $params + * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * * @return array message ids */ - public function search(array $params, $uid = false) { - return \imap_search($this->stream, $params[0], $uid ? IMAP::FT_UID : IMAP::NIL); + public function search(array $params, $uid = IMAP::ST_UID) { + return \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); } /** diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index ef01d46e..936de36e 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\IMAP; /** * Class Protocol @@ -221,4 +222,21 @@ public function setConnectionTimeout($connection_timeout) { return $this; } + /** + * Get the UID key string + * @param int|string $uid + * + * @return string + */ + public function getUIDKey($uid) { + if ($uid == IMAP::ST_UID || $uid == IMAP::FT_UID) { + return "UID"; + } + if (strlen($uid) > 0 && !is_numeric($uid)) { + return (string)$uid; + } + + return ""; + } + } \ No newline at end of file diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 026f4c53..367fd2c4 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -18,6 +18,7 @@ use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\RuntimeException; +use Webklex\PHPIMAP\IMAP; /** * Interface ProtocolInterface @@ -112,33 +113,36 @@ public function examineFolder($folder = 'INBOX'); * Fetch message headers * @param array|int $uids * @param string $rfc - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException */ - public function content($uids, $rfc = "RFC822", $uid = false); + public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID); /** * Fetch message headers * @param array|int $uids * @param string $rfc - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException */ - public function headers($uids, $rfc = "RFC822", $uid = false); + public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID); /** * Fetch message flags * @param array|int $uids - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException */ - public function flags($uids, $uid = false); + public function flags($uids, $uid = IMAP::ST_UID); /** * Get uid for a given id @@ -160,9 +164,9 @@ public function getMessageNumber($id); /** * Get a list of available folders - * * @param string $reference mailbox reference for list * @param string $folder mailbox / folder name match with wildcards + * * @return array mailboxes that matched $folder as array(globalName => array('delim' => .., 'flags' => ..)) * @throws RuntimeException */ @@ -170,27 +174,27 @@ public function folders($reference = '', $folder = '*'); /** * Set message flags - * * @param array $flags flags to set, add or remove * @param int $from message for items or start message if $to !== null * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return bool|array new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = false); + public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID); /** * Append a new message to given folder - * * @param string $folder name of target folder * @param string $message full message content * @param array $flags flags for new message * @param string $date date for new message + * * @return bool success * @throws RuntimeException */ @@ -203,24 +207,25 @@ public function appendMessage($folder, $message, $flags = null, $date = null); * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return bool success * @throws RuntimeException */ - public function copyMessage($folder, $from, $to = null, $uid = false); + public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID); /** * Copy multiple messages to the target folder - * * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param bool $uid Set to true if you pass message unique identifiers instead of numbers - * @return array|bool Tokens if operation successful, false if an error occurred + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * + * @return array|bool Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function copyManyMessages($messages, $folder, $uid = false); + public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID); /** * Move a message set from current folder to an other folder @@ -228,23 +233,25 @@ public function copyManyMessages($messages, $folder, $uid = false); * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return bool success */ - public function moveMessage($folder, $from, $to = null, $uid = false); + public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID); /** * Move multiple messages to the target folder * * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param bool $uid Set to true if you pass message unique identifiers instead of numbers - * @return array|bool Tokens if operation successful, false if an error occurred + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * + * @return array|bool Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function moveManyMessages($messages, $folder, $uid = false); + public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID); /** * Exchange identification information @@ -355,24 +362,26 @@ public function noop(); * Do a search request * * @param array $params - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array message ids * @throws RuntimeException */ - public function search(array $params, $uid = false); + public function search(array $params, $uid = IMAP::ST_UID); /** * Get a message overview * @param string $sequence uid sequence - * @param bool $uid set to true if passing a unique id + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array * @throws RuntimeException * @throws MessageNotFoundException * @throws InvalidMessageDateException */ - public function overview($sequence, $uid = false); + public function overview($sequence, $uid = IMAP::ST_UID); /** * Enable the debug mode diff --git a/src/Query/Query.php b/src/Query/Query.php index 0186d3be..5bc7ea93 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -67,7 +67,7 @@ class Query { /** @var int $fetch_flags */ protected $fetch_flags = true; - /** @var int $sequence */ + /** @var int|string $sequence */ protected $sequence = IMAP::NIL; /** @var string $fetch_order */ @@ -188,7 +188,7 @@ protected function search() { $this->generate_query(); try { - $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence == IMAP::ST_UID); + $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence); return $available_messages !== false ? new Collection($available_messages) : new Collection(); } catch (RuntimeException $e) { throw new GetMessagesFailedException("failed to fetch messages", 0, $e); @@ -223,10 +223,12 @@ protected function fetch($available_messages) { $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); $flags = $this->client->getConnection()->flags($uids, $this->sequence == IMAP::ST_UID); $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence == IMAP::ST_UID); + $flags = $this->client->getConnection()->flags($uids, $this->sequence); + $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence); $contents = []; if ($this->getFetchBody()) { - $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence == IMAP::ST_UID); + $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence); } return [ @@ -411,7 +413,7 @@ public function paginate($per_page = 5, $page = null, $page_name = 'imap_page') * Get a new Message instance * @param int $uid * @param int|null $msglist - * @param int|null $sequence + * @param int|string|null $sequence * * @return Message * @throws ConnectionFailedException @@ -493,11 +495,20 @@ public function markAsRead() { * @return $this */ public function setSequence($sequence) { - $this->sequence = $sequence != IMAP::ST_MSGN ? IMAP::ST_UID : $sequence; + $this->sequence = $sequence; return $this; } + /** + * Get the sequence type + * + * @return int|string + */ + public function getSequence() { + return $this->sequence; + } + /** * @return Client * @throws ConnectionFailedException diff --git a/src/config/imap.php b/src/config/imap.php index f8fd6405..586b164c 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -141,7 +141,7 @@ 'options' => [ 'delimiter' => '/', 'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK, - 'sequence' => \Webklex\PHPIMAP\IMAP::ST_MSGN, + 'sequence' => \Webklex\PHPIMAP\IMAP::ST_UID, 'fetch_body' => true, 'fetch_flags' => true, 'soft_fail' => false, From 1eab98067bc0cfa9be5129e436523b6aae94de8b Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:49:52 +0100 Subject: [PATCH 259/600] Fetch additional extensions --- CHANGELOG.md | 6 +++++- src/Client.php | 8 ++++++++ src/Folder.php | 19 ++++++++++--------- src/Query/Query.php | 46 ++++++++++++++++++++++++++++----------------- src/config/imap.php | 3 ++- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f55d5954..2085f992 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) - IMAP ID support added #174 - Enable debug mode via config +- Custom UID alternative support added +- Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` ### Affected Classes - [Header::class](src/Header.php) @@ -21,7 +23,9 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Query::class](src/Query/Query.php) ### Breaking changes -- NaN +- All protocol methods which had a `boolean` `$uid` option no longer support a boolean. Use `IMAP::ST_UID` or `IMAP::NIL` instead. If you want to use an alternative to `UID` just use the string instead. +- Default config option `options.sequence` changed from `IMAP::ST_MSGN` to `IMAP::ST_UID`. +- `Folder::query()` no longer accepts a charset string. It has been replaced by an extension array, which provides the ability to automatically fetch additional features. ## [2.7.2] - 2021-09-27 ### Fixed diff --git a/src/Client.php b/src/Client.php index 8759126f..fc853504 100755 --- a/src/Client.php +++ b/src/Client.php @@ -109,6 +109,13 @@ class Client { */ public $password; + /** + * Additional data fetched from the server. + * + * @var string + */ + public $extensions; + /** * Account authentication method. * @@ -151,6 +158,7 @@ class Client { 'username' => '', 'password' => '', 'authentication' => null, + "extensions" => [], 'proxy' => [ 'socket' => null, 'request_fulluri' => false, diff --git a/src/Folder.php b/src/Folder.php index 6d9bd794..5c34cd59 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -130,35 +130,36 @@ public function __construct(Client $client, $folder_name, $delimiter, $attribute /** * Get a new search query instance - * @param string $charset + * @param string[] $extensions * * @return WhereQuery * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function query($charset = 'UTF-8'){ + public function query($extensions = []){ $this->getClient()->checkConnection(); $this->getClient()->openFolder($this->path); + $extensions = count($extensions) > 0 ? $extensions : $this->getClient()->extensions; - return new WhereQuery($this->getClient(), $charset); + return new WhereQuery($this->getClient(), $extensions); } /** - * @inheritdoc self::query($charset = 'UTF-8') + * @inheritdoc self::query($extensions = []) * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function search($charset = 'UTF-8'){ - return $this->query($charset); + public function search($extensions = 'UTF-8'){ + return $this->query($extensions); } /** - * @inheritdoc self::query($charset = 'UTF-8') + * @inheritdoc self::query($extensions = []) * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function messages($charset = 'UTF-8'){ - return $this->query($charset); + public function messages($extensions = []){ + return $this->query($extensions); } /** diff --git a/src/Query/Query.php b/src/Query/Query.php index 5bc7ea93..1d2f87d9 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -46,8 +46,8 @@ class Query { /** @var string $raw_query */ protected $raw_query; - /** @var string $charset */ - protected $charset; + /** @var string[] $extensions */ + protected $extensions; /** @var Client $client */ protected $client; @@ -85,9 +85,9 @@ class Query { /** * Query constructor. * @param Client $client - * @param string $charset + * @param string[] $extensions */ - public function __construct(Client $client, $charset = 'UTF-8') { + public function __construct(Client $client, $extensions = []) { $this->setClient($client); $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); @@ -102,7 +102,7 @@ public function __construct(Client $client, $charset = 'UTF-8') { $this->date_format = ClientManager::get('date_format', 'd M y'); $this->soft_fail = ClientManager::get('options.soft_fail', false); - $this->charset = $charset; + $this->setExtensions($extensions); $this->query = new Collection(); $this->boot(); } @@ -221,8 +221,10 @@ protected function fetch($available_messages) { } $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); - $flags = $this->client->getConnection()->flags($uids, $this->sequence == IMAP::ST_UID); - $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence == IMAP::ST_UID); + $extensions = []; + if (empty($this->getExtensions()) === false) { + $extensions = $this->client->getConnection()->fetch($this->getExtensions(), $uids, null, $this->sequence); + } $flags = $this->client->getConnection()->flags($uids, $this->sequence); $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence); @@ -232,10 +234,11 @@ protected function fetch($available_messages) { } return [ - "uids" => $uids, - "flags" => $flags, - "headers" => $headers, - "contents" => $contents, + "uids" => $uids, + "flags" => $flags, + "headers" => $headers, + "contents" => $contents, + "extensions" => $extensions, ]; } @@ -323,8 +326,12 @@ protected function populate($available_messages) { foreach ($raw_messages["headers"] as $uid => $header) { $content = isset($raw_messages["contents"][$uid]) ? $raw_messages["contents"][$uid] : ""; $flag = isset($raw_messages["flags"][$uid]) ? $raw_messages["flags"][$uid] : []; + $extensions = isset($raw_messages["extensions"][$uid]) ? $raw_messages["extensions"][$uid] : []; $message = $this->make($uid, $msglist, $header, $content, $flag); + foreach($extensions as $key => $extension) { + $message->getHeader()->set($key, $extension); + } if ($message !== null) { $key = $this->getMessageKey($message_key, $msglist, $message); $messages->put("$key", $message); @@ -565,18 +572,23 @@ public function setRawQuery($raw_query) { } /** - * @return string + * @return string[] */ - public function getCharset() { - return $this->charset; + public function getExtensions() { + return $this->extensions; } /** - * @param string $charset + * @param string[] $extensions * @return Query */ - public function setCharset($charset) { - $this->charset = $charset; + public function setExtensions($extensions) { + $this->extensions = $extensions; + if (count($this->extensions) > 0) { + if (in_array("UID", $this->extensions) === false) { + $this->extensions[] = "UID"; + } + } return $this; } diff --git a/src/config/imap.php b/src/config/imap.php index 586b164c..c8e1eb06 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -61,7 +61,8 @@ 'username' => null, 'password' => null, ], - "timeout" => 30 + "timeout" => 30, + "extensions" => [] ], /* From 2b5dda2e83a27d1b78098b880f84c0efcdc0e822 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 19:50:14 +0100 Subject: [PATCH 260/600] formatting --- src/Query/Query.php | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 1d2f87d9..aa6c5c11 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -256,18 +256,18 @@ protected function fetch($available_messages) { * @throws GetMessagesFailedException * @throws ReflectionException */ - protected function make($uid, $msglist, $header, $content, $flags){ + protected function make($uid, $msglist, $header, $content, $flags) { try { return Message::make($uid, $msglist, $this->getClient(), $header, $content, $flags, $this->getFetchOptions(), $this->sequence); - }catch (MessageNotFoundException $e) { + } catch (MessageNotFoundException $e) { $this->setError($uid, $e); - }catch (RuntimeException $e) { + } catch (RuntimeException $e) { $this->setError($uid, $e); - }catch (MessageFlagException $e) { + } catch (MessageFlagException $e) { $this->setError($uid, $e); - }catch (InvalidMessageDateException $e) { + } catch (InvalidMessageDateException $e) { $this->setError($uid, $e); - }catch (MessageContentFetchingException $e) { + } catch (MessageContentFetchingException $e) { $this->setError($uid, $e); } @@ -284,7 +284,7 @@ protected function make($uid, $msglist, $header, $content, $flags){ * * @return string */ - protected function getMessageKey($message_key, $msglist, $message){ + protected function getMessageKey($message_key, $msglist, $message) { switch ($message_key) { case 'number': $key = $message->getMessageNo(); @@ -755,18 +755,18 @@ public function fetchOrderDesc() { } /** + * @return Query * @var boolean $state * - * @return Query */ public function softFail($state = true) { return $this->setSoftFail($state); } /** + * @return Query * @var boolean $state * - * @return Query */ public function setSoftFail($state = true) { $this->soft_fail = $state; @@ -805,11 +805,11 @@ protected function setError($uid, $error) { /** * Check if there are any errors / exceptions present + * @return boolean * @var integer|null $uid * - * @return boolean */ - public function hasErrors($uid = null){ + public function hasErrors($uid = null) { if ($uid !== null) { return $this->hasError($uid); } @@ -818,11 +818,11 @@ public function hasErrors($uid = null){ /** * Check if there is an error / exception present + * @return boolean * @var integer $uid * - * @return boolean */ - public function hasError($uid){ + public function hasError($uid) { return isset($this->errors[$uid]); } @@ -831,7 +831,7 @@ public function hasError($uid){ * * @return array */ - public function errors(){ + public function errors() { return $this->getErrors(); } @@ -840,27 +840,27 @@ public function errors(){ * * @return array */ - public function getErrors(){ + public function getErrors() { return $this->errors; } /** * Get a specific error / exception + * @return Exception|null * @var integer $uid * - * @return Exception|null */ - public function error($uid){ + public function error($uid) { return $this->getError($uid); } /** * Get a specific error / exception + * @return Exception|null * @var integer $uid * - * @return Exception|null */ - public function getError($uid){ + public function getError($uid) { if ($this->hasError($uid)) { return $this->errors[$uid]; } From 594562eae06fa50f4dcfdffcec72ac6132f2c215 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 22:09:44 +0100 Subject: [PATCH 261/600] Provider added as suggested context --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 735fbe01..a0571c33 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,6 +26,7 @@ If applicable, add screenshots to help explain your problem. - OS: [e.g. Debian 10] - PHP: [e.g. 5.5.9] - Version [e.g. v2.3.1] +- Provider [e.g. Gmail, Outlook, Dovecot] **Additional context** Add any other context about the problem here. From 23c80eb698e9f9d3c10297a8ff54e1965a58e0fb Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 3 Nov 2021 22:21:03 +0100 Subject: [PATCH 262/600] Optionally move a message during "deletion" instead of just "flagging" it #106 --- CHANGELOG.md | 1 + src/Message.php | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2085f992..172d3b76 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Enable debug mode via config - Custom UID alternative support added - Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` +- Optionally move a message during "deletion" instead of just "flagging" it #106 (thanks @EthraZa) ### Affected Classes - [Header::class](src/Header.php) diff --git a/src/Message.php b/src/Message.php index 90ed1a89..be636e8a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -955,6 +955,8 @@ protected function fetchNewMail($folder, $next_uid, $event, $expunge){ /** * Delete the current Message * @param bool $expunge + * @param string|null $trash_path + * @param boolean $force_move * * @return bool * @throws Exceptions\ConnectionFailedException @@ -962,8 +964,12 @@ protected function fetchNewMail($folder, $next_uid, $event, $expunge){ * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function delete($expunge = true) { + public function delete($expunge = true, $trash_path = null, $force_move = false) { $status = $this->setFlag("Deleted"); + if($force_move) { + $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"]: $trash_path; + $status = $this->move($trash_path); + } if($expunge) $this->client->expunge(); $event = $this->getEvent("message", "deleted"); From 19c4d12d52473039bb012377e2a132cbf652d87f Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 02:37:54 +0100 Subject: [PATCH 263/600] Context added to InvalidWhereQueryCriteriaException --- CHANGELOG.md | 1 + src/Query/WhereQuery.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 172d3b76..598cd525 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Extend date parsing error message #173 - Fixed 'Where' method replaces the content with uppercase #148 - Don't surround numeric search values with quotes +- Context added to `InvalidWhereQueryCriteriaException` ### Added - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index e8cf2577..074466ce 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -110,7 +110,7 @@ protected function validate_criteria($criteria) { return substr($criteria, 7); } if (in_array($command, $this->available_criteria) === false) { - throw new InvalidWhereQueryCriteriaException(); + throw new InvalidWhereQueryCriteriaException("Invalid imap search criteria: $command"); } return $criteria; From 10f58420f342d5c6067af2eef9f7af17561bef56 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 02:39:47 +0100 Subject: [PATCH 264/600] Redundant stream_set_timeout() removed --- CHANGELOG.md | 1 + src/Connection/Protocols/Protocol.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 598cd525..e5931812 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fixed 'Where' method replaces the content with uppercase #148 - Don't surround numeric search values with quotes - Context added to `InvalidWhereQueryCriteriaException` +- Redundant `stream_set_timeout()` removed ### Added - Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 936de36e..7a882ab9 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -191,7 +191,6 @@ protected function createStream($transport, $host, $port, $timeout) { STREAM_CLIENT_CONNECT, stream_context_create($this->defaultSocketOptions($transport)) ); - stream_set_timeout($stream, $timeout); if (!$stream) { throw new ConnectionFailedException($errstr, $errno); From ae26ff560e552ca185dccdbd53aa0ff75d9f5539 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 02:41:44 +0100 Subject: [PATCH 265/600] Where search accepts now almost everything --- CHANGELOG.md | 2 ++ src/Query/WhereQuery.php | 63 +++++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5931812..77971461 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Custom UID alternative support added - Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` - Optionally move a message during "deletion" instead of just "flagging" it #106 (thanks @EthraZa) +- `WhereQuery::where()` accepts now a wide range of criteria / values. ### Affected Classes - [Header::class](src/Header.php) - [Protocol::class](src/Connection/Protocols/Protocol.php) - [Query::class](src/Query/Query.php) +- [WhereQuery::class](src/Query/WhereQuery.php) ### Breaking changes - All protocol methods which had a `boolean` `$uid` option no longer support a boolean. Use `IMAP::ST_UID` or `IMAP::NIL` instead. If you want to use an alternative to `UID` just use the string instead. diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 074466ce..f795974e 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -117,34 +117,55 @@ protected function validate_criteria($criteria) { } /** + * Register search parameters * @param mixed $criteria * @param null $value * * @return $this * @throws InvalidWhereQueryCriteriaException + * + * Examples: + * $query->from("someone@email.tld")->seen(); + * $query->whereFrom("someone@email.tld")->whereSeen(); + * $query->where([["FROM" => "someone@email.tld"], ["SEEN"]]); + * $query->where(["FROM" => "someone@email.tld"])->where(["SEEN"]); + * $query->where(["FROM" => "someone@email.tld", "SEEN"]); + * $query->where("FROM", "someone@email.tld")->where("SEEN"); */ public function where($criteria, $value = null) { if (is_array($criteria)) { foreach ($criteria as $key => $value) { if (is_numeric($key)) { - return $this->where($value); + $this->where($value); + }else{ + $this->where($key, $value); } - return $this->where($key, $value); } } else { - $criteria = $this->validate_criteria($criteria); - $value = $this->parse_value($value); - - if ($value === null || $value === '') { - $this->query->push([$criteria]); - } else { - $this->query->push([$criteria, $value]); - } + $this->push_search_criteria($criteria, $value); } return $this; } + /** + * Push a given search criteria and value pair to the search query + * @param $criteria string + * @param $value mixed + * + * @throws InvalidWhereQueryCriteriaException + */ + protected function push_search_criteria($criteria, $value){ + $criteria = $this->validate_criteria($criteria); + $value = $this->parse_value($value); + + if ($value === null || $value === '') { + $this->query->push([$criteria]); + } else { + $this->query->push([$criteria, $value]); + } + } + /** * @param Closure $closure * @@ -468,8 +489,7 @@ public function whereLanguage($country_code) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUid($uid) - { + public function whereUid($uid) { return $this->where('UID', $uid); } @@ -481,8 +501,7 @@ public function whereUid($uid) * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUidIn($uids) - { + public function whereUidIn($uids) { $uids = implode(',', $uids); return $this->where('UID', $uids); } @@ -491,10 +510,9 @@ public function whereUidIn($uids) * Apply the callback if the given "value" is truthy. * copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php * - * @param mixed $value - * @param callable $callback - * @param callable|null $default - + * @param mixed $value + * @param callable $callback + * @param callable|null $default * @return $this|mixed */ public function when($value, $callback, $default = null) { @@ -511,14 +529,13 @@ public function when($value, $callback, $default = null) { * Apply the callback if the given "value" is falsy. * copied from @url https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Traits/Conditionable.php * - * @param mixed $value - * @param callable $callback - * @param callable|null $default - + * @param mixed $value + * @param callable $callback + * @param callable|null $default * @return $this|mixed */ public function unless($value, $callback, $default = null) { - if (! $value) { + if (!$value) { return $callback($this, $value) ?: $this; } elseif ($default) { return $default($this, $value) ?: $this; From 5008b12a9a1ae900d09050c27026546ee6058530 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 02:42:54 +0100 Subject: [PATCH 266/600] classes simplified --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 66 +++++++++++------------ src/Connection/Protocols/Protocol.php | 4 ++ 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77971461..3b90abe9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [Protocol::class](src/Connection/Protocols/Protocol.php) - [Query::class](src/Query/Query.php) - [WhereQuery::class](src/Query/WhereQuery.php) +- [Message::class](src/Message.php) ### Breaking changes - All protocol methods which had a `boolean` `$uid` option no longer support a boolean. Use `IMAP::ST_UID` or `IMAP::NIL` instead. If you want to use an alternative to `UID` just use the string instead. diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b7f71037..aba80fe6 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -556,7 +556,7 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { $items = (array)$items; $itemList = $this->escapeList($items); - $this->sendRequest(trim($this->getUIDKey($uid) . ' FETCH'), [$set, $itemList], $tag); + $this->sendRequest($this->buildUIDCommand("FETCH", $uid), [$set, $itemList], $tag); $result = []; $tokens = null; // define $tokens variable before first use while (!$this->readLine($tokens, $tag)) { @@ -753,33 +753,20 @@ public function folders($reference = '', $folder = '*') { * @throws RuntimeException */ public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID, $item = null) { - if ($item === null) { - $item = 'FLAGS'; - } - if ($mode == '+' || $mode == '-') { - $item = $mode . $item; - } - - if ($silent) { - $item .= '.SILENT'; - } - $flags = $this->escapeList($flags); - $set = (int)$from; - if ($to !== null) { - $set .= ':' . ($to == INF ? '*' : (int)$to); - } + $set = $this->buildSet($from, $to); - $command = ($uid ? "UID " : "")."STORE"; - $result = $this->requestAndResponse($command, [$set, $item, $flags], $silent); + $command = $this->buildUIDCommand("STORE", $uid); + $item = ($mode == '-' ? "-" : "+").($item === null ? "FLAGS" : $item).($silent ? '.SILENT' : ""); + + $response = $this->requestAndResponse($command, [$set, $item, $flags], $silent); if ($silent) { - return (bool)$result; + return (bool)$response; } - $tokens = $result; $result = []; - foreach ($tokens as $token) { + foreach ($response as $token) { if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') { continue; } @@ -826,11 +813,8 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * @throws RuntimeException */ public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { - $set = (int)$from; - if ($to !== null) { - $set .= ':' . ($to == INF ? '*' : (int)$to); - } - $command = trim($this->getUIDKey($uid)." COPY"); + $set = $this->buildSet($from, $to); + $command = $this->buildUIDCommand("COPY", $uid); return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } @@ -846,7 +830,7 @@ public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { * @throws RuntimeException */ public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { - $command = trim($this->getUIDKey($uid)." COPY"); + $command = $this->buildUIDCommand("COPY", $uid); $set = implode(',', $messages); $tokens = [$set, $this->escapeString($folder)]; @@ -867,11 +851,8 @@ public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { * @throws RuntimeException */ public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { - $set = (int)$from; - if ($to !== null) { - $set .= ':' . ($to == INF ? '*' : (int)$to); - } - $command = trim($this->getUIDKey($uid)." MOVE"); + $set = $this->buildSet($from, $to); + $command = $this->buildUIDCommand("MOVE", $uid); return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } @@ -887,7 +868,7 @@ public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { * @throws RuntimeException */ public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID) { - $command = trim($this->getUIDKey($uid)." MOVE"); + $command = $this->buildUIDCommand("MOVE", $uid); $set = implode(',', $messages); $tokens = [$set, $this->escapeString($folder)]; @@ -1048,8 +1029,8 @@ public function done() { * @throws RuntimeException */ public function search(array $params, $uid = IMAP::ST_UID) { - $token = trim($this->getUIDKey($uid)." SEARCH"); - $response = $this->requestAndResponse($token, $params); + $command = $this->buildUIDCommand("SEARCH", $uid); + $response = $this->requestAndResponse($command, $params); if (!$response) { return $response; } @@ -1106,4 +1087,19 @@ public function enableDebug(){ public function disableDebug(){ $this->debug = false; } + + /** + * Build a valid UID number set + * @param $from + * @param null $to + * + * @return int|string + */ + public function buildSet($from, $to = null) { + $set = (int)$from; + if ($to !== null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + return $set; + } } diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 7a882ab9..5c70dba4 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -238,4 +238,8 @@ public function getUIDKey($uid) { return ""; } + public function buildUIDCommand($command, $uid) { + return trim($this->getUIDKey($uid)." ".$command); + } + } \ No newline at end of file From 017be447e4fe88cf7b5e79e08abd5e53a7bfbd3d Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 02:44:01 +0100 Subject: [PATCH 267/600] issue reference added --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b90abe9..69c4831b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Custom UID alternative support added - Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` - Optionally move a message during "deletion" instead of just "flagging" it #106 (thanks @EthraZa) -- `WhereQuery::where()` accepts now a wide range of criteria / values. +- `WhereQuery::where()` accepts now a wide range of criteria / values. #104 ### Affected Classes - [Header::class](src/Header.php) From 9a1ed6d0379dfbee36e6225d6b46a7efb088eff0 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 16:40:28 +0100 Subject: [PATCH 268/600] Release information added --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c4831b..18064772 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + +## [3.0.0-alpha] - 2021-11-04 +### Fixed - Extend date parsing error message #173 - Fixed 'Where' method replaces the content with uppercase #148 - Don't surround numeric search values with quotes From 43d33dada27acf1dfd8e5440e1104ef256e75dbe Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 4 Nov 2021 18:10:37 +0100 Subject: [PATCH 269/600] Discord link added --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 65012298..16566f69 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Official documentation: [php-imap.com](https://www.php-imap.com/) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) +Discord: https://discord.gg/jCcZWCSq ## Table of Contents - [Documentations](#documentations) From 7574029286292167d44d7f1ceccaa73ee849bc84 Mon Sep 17 00:00:00 2001 From: Webklex Date: Fri, 5 Nov 2021 00:03:12 +0100 Subject: [PATCH 270/600] Discord link updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16566f69..6e04be4f 100755 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Official documentation: [php-imap.com](https://www.php-imap.com/) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) -Discord: https://discord.gg/jCcZWCSq +Discord: https://discord.gg/vUHrbfbDr9 ## Table of Contents - [Documentations](#documentations) From 1907079f19890304d47539d2d3fdb4117f27407e Mon Sep 17 00:00:00 2001 From: netpok Date: Wed, 17 Nov 2021 20:19:39 +0100 Subject: [PATCH 271/600] Fix attribute serialization (#179) --- src/Attribute.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Attribute.php b/src/Attribute.php index 06dc6a79..ebe411a1 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -61,22 +61,13 @@ public function toString(){ return $this->__toString(); } - /** - * Return the serialized attribute - * - * @return array - */ - public function __serialize(){ - return $this->values; - } - /** * Convert instance to array * * @return array */ public function toArray(){ - return $this->__serialize(); + return $this->values; } /** From 14a343229e220313faca277d788d889600582d17 Mon Sep 17 00:00:00 2001 From: netpok Date: Wed, 17 Nov 2021 20:23:41 +0100 Subject: [PATCH 272/600] Use real tls instead of starttls (#180) --- src/Client.php | 2 +- src/Connection/Protocols/ImapProtocol.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Client.php b/src/Client.php index fc853504..536e5947 100755 --- a/src/Client.php +++ b/src/Client.php @@ -65,7 +65,7 @@ class Client { /** * Server encryption. - * Supported: none, ssl, tls, or notls. + * Supported: none, ssl, tls, starttls or notls. * * @var string */ diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index aba80fe6..29a84039 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -60,12 +60,12 @@ public function __destruct() { */ public function connect($host, $port = null) { $transport = 'tcp'; - $encryption = ""; + $encryption = ''; if ($this->encryption) { $encryption = strtolower($this->encryption); - if ($encryption == "ssl") { - $transport = 'ssl'; + if (in_array($encryption, ['ssl', 'tls'])) { + $transport = $encryption; $port = $port === null ? 993 : $port; } } @@ -75,8 +75,8 @@ public function connect($host, $port = null) { if (!$this->assumedNextLine('* OK')) { throw new ConnectionFailedException('connection refused'); } - if ($encryption == "tls") { - $this->enableTls(); + if ($encryption == 'starttls') { + $this->enableStartTls(); } } catch (Exception $e) { throw new ConnectionFailedException('connection failed', 0, $e); @@ -89,7 +89,7 @@ public function connect($host, $port = null) { * @throws ConnectionFailedException * @throws RuntimeException */ - protected function enableTls(){ + protected function enableStartTls(){ $response = $this->requestAndResponse('STARTTLS'); $result = $response && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); if (!$result) { From 3fd20eef1c762c207e9ba8b16cb2af28d4104ac7 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 17 Nov 2021 20:27:31 +0100 Subject: [PATCH 273/600] Tags updated --- README.md | 68 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 6e04be4f..49723fa9 100755 --- a/README.md +++ b/README.md @@ -1,25 +1,28 @@ # IMAP Library for PHP -[![Latest Version on Packagist][ico-version]][link-packagist] +[![Latest release on Packagist][ico-release]][link-packagist] +[![Latest prerelease on Packagist][ico-prerelease]][link-packagist] [![Software License][ico-license]][link-license] -[![Build Status][ico-travis]][link-scrutinizer] +[![Build Status][ico-travis]][link-scrutinizer] [![Total Downloads][ico-downloads]][link-downloads] [![Hits][ico-hits]][link-hits] +[![Discord][ico-discord]][link-discord] +[![Snyk][ico-snyk]][link-snyk] ## Description PHP-IMAP is a wrapper for common IMAP communication without the need to have the php-imap module installed / enabled. -The protocol is completely integrated and therefore supports IMAP IDLE operation and the "new" oAuth authentication +The protocol is completely integrated and therefore supports IMAP IDLE operation and the "new" oAuth authentication process as well. -You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if +You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if you want to use legacy protocols such as pop3. Official documentation: [php-imap.com](https://www.php-imap.com/) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) -Discord: https://discord.gg/vUHrbfbDr9 +Discord: [discord.gg/jCcZWCSq][link-discord] ## Table of Contents - [Documentations](#documentations) @@ -82,41 +85,41 @@ foreach($folders as $folder){ ``` -### Known issues +### Known issues | Error | Solution | | ------------------------------------------------------------------------- | ---------------------------------------------------------- | | Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | - -## Support -If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). -However please be aware that it might take some time to get an answer. -Off topic, rude or abusive issues will be deleted without any notice. - -If you need **commercial** support, feel free to send me a mail at github@webklex.com. - - -##### A little notice + +## Support +If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). +However please be aware that it might take some time to get an answer. +Off topic, rude or abusive issues will be deleted without any notice. + +If you need **commercial** support, feel free to send me a mail at github@webklex.com. + + +##### A little notice If you write source code in your issue, please consider to format it correctly. This makes it so much nicer to read -and people are more likely to comment and help :) - -```php +and people are more likely to comment and help :) -echo 'your php code...'; - -``` - -will turn into: +```php + +echo 'your php code...'; + +``` + +will turn into: ```php echo 'your php code...'; ``` - - -## Features & pull requests + + +## Features & pull requests Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) -first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) -if you're just wishing a feature ;) +first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) +if you're just wishing a feature ;) ## Change log @@ -136,7 +139,8 @@ If you discover any security related issues, please email github@webklex.com ins The MIT License (MIT). Please see [License File][link-license] for more information. -[ico-version]: https://img.shields.io/packagist/v/Webklex/php-imap.svg?style=flat-square +[ico-release]: https://img.shields.io/packagist/v/Webklex/php-imap.svg?style=flat-square&label=version +[ico-prerelease]: https://img.shields.io/github/v/release/webklex/php-imap?include_prereleases&style=flat-square&label=pre-release [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square [ico-travis]: https://img.shields.io/travis/Webklex/php-imap/master.svg?style=flat-square [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/Webklex/php-imap.svg?style=flat-square @@ -145,6 +149,8 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [ico-build]: https://img.shields.io/scrutinizer/build/g/Webklex/php-imap/master?style=flat-square [ico-quality]: https://img.shields.io/scrutinizer/quality/g/Webklex/php-imap/master?style=flat-square [ico-hits]: https://hits.webklex.com/svg/webklex/php-imap +[ico-snyk]: https://snyk-widget.herokuapp.com/badge/composer/webklex/php-imap/badge.svg +[ico-discord]: https://img.shields.io/static/v1?label=discord&message=open&color=5865f2&style=flat-square [link-packagist]: https://packagist.org/packages/Webklex/php-imap [link-travis]: https://travis-ci.org/Webklex/php-imap @@ -157,3 +163,5 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md [link-jetbrains]: https://www.jetbrains.com [link-hits]: https://hits.webklex.com +[link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap +[link-discord]: https://discord.gg/jCcZWCSq From c0b3e843d72b8961992c0638266a18cdbebbc294 Mon Sep 17 00:00:00 2001 From: Webklex Date: Wed, 17 Nov 2021 20:28:48 +0100 Subject: [PATCH 274/600] Pulled changes added --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18064772..22ef0ae0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix attribute serialization #179 (thanks @netpok) +- Use real tls instead of starttls #180 (thanks @netpok) ### Added - NaN From a11e9f8825cbf88e4b1d8ff30823b7af04b80de8 Mon Sep 17 00:00:00 2001 From: Stephen Vanderwarker Date: Fri, 19 Nov 2021 08:59:43 -0500 Subject: [PATCH 275/600] spelling error in readme.md (#181) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49723fa9..7a02da9c 100755 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ foreach($folders as $folder){ //Move the current Message to 'INBOX.read' if($message->move('INBOX.read') == true){ - echo 'Message has ben moved'; + echo 'Message has been moved'; }else{ echo 'Message could not be moved'; } From 073beecc6e927b6101852901e64f08e1a49c9806 Mon Sep 17 00:00:00 2001 From: laurent-rizer <52099563+laurent-rizer@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:26:53 +0100 Subject: [PATCH 276/600] Fix isAttachment that did not properly take in consideration dispositions options (#195) Co-authored-by: Nourisson Laurent --- src/Part.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Part.php b/src/Part.php index a6a67488..2ca0a1b6 100644 --- a/src/Part.php +++ b/src/Part.php @@ -301,7 +301,7 @@ private function parseEncoding(){ public function isAttachment(){ $valid_disposition = in_array(strtolower($this->disposition), ClientManager::get('options.dispositions')); - if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || (empty($this->disposition))) && !$valid_disposition) { + if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) { return false; } From 77c12b84a160c62684716804350fdd378d8e53f3 Mon Sep 17 00:00:00 2001 From: laurent-rizer <52099563+laurent-rizer@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:29:04 +0100 Subject: [PATCH 277/600] Query::chunked does not loop over the last chunk (#196) Co-authored-by: Nourisson Laurent --- src/Query/Query.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index aa6c5c11..75578278 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -381,11 +381,13 @@ public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { $this->limit = $chunk_size; $this->page = $start_chunk; + $handled_messages_count = 0; do { $messages = $this->populate($available_messages); + $handled_messages_count += $messages->count(); $callback($messages, $this->page); $this->page++; - } while ($this->limit * $this->page <= $available_messages_count); + } while ($handled_messages_count < $available_messages_count); $this->limit = $old_limit; $this->page = $old_page; } From 684cc9e5c8aac4145c20f12f6535a2820def9d24 Mon Sep 17 00:00:00 2001 From: laurent-rizer <52099563+laurent-rizer@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:35:24 +0100 Subject: [PATCH 278/600] Allow to fully overwrite default config arrays (#194) * Fix isAttachment that did not properly take in consideration dispositions options * Query::chunked does not loop over the last chunk * Allow to fully overwrite default config arrays Co-authored-by: Nourisson Laurent --- src/ClientManager.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ClientManager.php b/src/ClientManager.php index 3daf9f83..87fcf971 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -232,6 +232,12 @@ private function array_merge_recursive_distinct() { $arrays = func_get_args(); $base = array_shift($arrays); + // From https://stackoverflow.com/a/173479 + $isAssoc = function(array $arr) { + if (array() === $arr) return false; + return array_keys($arr) !== range(0, count($arr) - 1); + }; + if(!is_array($base)) $base = empty($base) ? array() : array($base); foreach($arrays as $append) { @@ -245,7 +251,19 @@ private function array_merge_recursive_distinct() { continue; } - if(is_array($value) or is_array($base[$key])) { + if( + ( + is_array($value) + && $isAssoc($value) + ) + || ( + is_array($base[$key]) + && $isAssoc($base[$key]) + ) + ) { + // If the arrays are not associates we don't want to array_merge_recursive_distinct + // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] + // results in $resultConfig['dispositions'] = ['attachment', 'inline'] $base[$key] = $this->array_merge_recursive_distinct($base[$key], $append[$key]); } else if(is_numeric($key)) { if(!in_array($value, $base)) $base[] = $value; From 6166f2c57edc90adc3caad20cb6e92c1ea8acb5e Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 3 Feb 2022 16:44:06 +0100 Subject: [PATCH 279/600] Recent changes added --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ef0ae0..80c66558 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Fix attribute serialization #179 (thanks @netpok) -- Use real tls instead of starttls #180 (thanks @netpok) +- NaN ### Added - NaN @@ -18,6 +17,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [3.1.0-alpha] - 2022-02-03 +### Fixed +- Fix attribute serialization #179 (thanks @netpok) +- Use real tls instead of starttls #180 (thanks @netpok) +- Allow to fully overwrite default config arrays #194 (thanks @laurent-rizer) +- Query::chunked does not loop over the last chunk #196 (thanks @laurent-rizer) +- Fix isAttachment that did not properly take in consideration dispositions options #195 (thanks @laurent-rizer) + ## [3.0.0-alpha] - 2021-11-04 ### Fixed - Extend date parsing error message #173 From ed93fe43ac3e71ffcc4e9a61d3016f91173d617a Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 3 Feb 2022 16:48:28 +0100 Subject: [PATCH 280/600] Recent changes added --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ef0ae0..0b734dcd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Fix attribute serialization #179 (thanks @netpok) -- Use real tls instead of starttls #180 (thanks @netpok) +- NaN ### Added - NaN @@ -18,6 +17,21 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Breaking changes - NaN +## [3.1.0-alpha] - 2022-02-03 +### Fixed +- Fix attribute serialization #179 (thanks @netpok) +- Use real tls instead of starttls #180 (thanks @netpok) +- Allow to fully overwrite default config arrays #194 (thanks @laurent-rizer) +- Query::chunked does not loop over the last chunk #196 (thanks @laurent-rizer) +- Fix isAttachment that did not properly take in consideration dispositions options #195 (thanks @laurent-rizer) + +### Affected Classes +- [Header::class](src/Header.php) +- [Protocol::class](src/Connection/Protocols/Protocol.php) +- [Query::class](src/Query/Query.php) +- [Part::class](src/Part.php) +- [ClientManager::class](src/ClientManager.php) + ## [3.0.0-alpha] - 2021-11-04 ### Fixed - Extend date parsing error message #173 From af9cf9033ef12b16d07c202cc34482be351e2e6b Mon Sep 17 00:00:00 2001 From: Sebastian B <76404254+HelloSebastian@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:54:52 +0100 Subject: [PATCH 281/600] add search method for messages with UID greater than X (#201) * add method getMessagesGreaterThanUid The getMessagesGreaterThanUid function searches for all messages that have a UID greater than or equal to the one passed. The found messages are initialized and returned as MessageCollection. * fix performance issue because of get each message individually --- src/Query/Query.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Query/Query.php b/src/Query/Query.php index 75578278..962891f1 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -475,6 +475,38 @@ public function getMessageByUid($uid) { return $this->getMessage($uid, null, IMAP::ST_UID); } + /** + * Get messages with UID equal or greater than given UID. + * + * @param int|string $uid + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws MessageNotFoundException + */ + public function getByUidGreaterThanEqual($uid) { + $connection = $this->getClient()->getConnection(); + + $uids = $connection->getUid(); + $available_messages = new Collection(); + $i = 0; + foreach ($uids as $id) { + if ($id >= $uid) { + $available_messages->put($i++, $id); + } + } + + try { + if ($available_messages->count() > 0) { + return $this->populate($available_messages); + } + return MessageCollection::make([]); + } catch (Exception $e) { + throw new GetMessagesFailedException($e->getMessage(), 0, $e); + } + } + /** * Don't mark messages as read when fetching * From 3d516a3c1c130f2717bc16f244136c2e9b54122e Mon Sep 17 00:00:00 2001 From: Catalin Bozan <43291246+catabozan@users.noreply.github.com> Date: Mon, 7 Mar 2022 12:55:32 +0200 Subject: [PATCH 282/600] Fix return type in src/Client::createFolder() docblock (#202) Change return type of src/Client::createFolder() from 'bool' to 'Folder' --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 536e5947..63c6a03b 100755 --- a/src/Client.php +++ b/src/Client.php @@ -514,7 +514,7 @@ public function openFolder($folder_path, $force_select = false) { * @param string $folder * @param boolean $expunge * - * @return bool + * @return Folder * @throws ConnectionFailedException * @throws FolderFetchingException * @throws Exceptions\EventNotFoundException From 4dc4103449373bb4cc04a8d4ad8d6bcec615fc70 Mon Sep 17 00:00:00 2001 From: Sebastian B <76404254+HelloSebastian@users.noreply.github.com> Date: Mon, 7 Mar 2022 17:05:12 +0100 Subject: [PATCH 283/600] add uid cache (#204) improve annotations of client and add uid cache to LegacyProtocol change change change cache asd --- src/Client.php | 4 ++ src/Connection/Protocols/ImapProtocol.php | 37 +++++++++++----- src/Connection/Protocols/LegacyProtocol.php | 11 ++++- src/Connection/Protocols/Protocol.php | 42 ++++++++++++++++++- .../Protocols/ProtocolInterface.php | 20 ++++++++- src/config/imap.php | 1 + 6 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/Client.php b/src/Client.php index 63c6a03b..6b518a23 100755 --- a/src/Client.php +++ b/src/Client.php @@ -357,6 +357,10 @@ public function connect() { $this->connection->enableDebug(); } + if (!ClientManager::get('options.uid_cache')) { + $this->connection->disableUidCache(); + } + try { $this->connection->connect($this->host, $this->port); } catch (ErrorException $e) { diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 29a84039..cf898887 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -432,7 +432,9 @@ public function logout() { } catch (Exception $e) {} fclose($this->stream); $this->stream = null; + $this->uid_cache = null; } + return $result; } @@ -513,6 +515,8 @@ public function examineOrSelect($command = 'EXAMINE', $folder = 'INBOX') { * @throws RuntimeException */ public function selectFolder($folder = 'INBOX') { + $this->uid_cache = null; + return $this->examineOrSelect('SELECT', $folder); } @@ -678,18 +682,31 @@ public function flags($uids, $uid = IMAP::ST_UID){ * @throws MessageNotFoundException */ public function getUid($id = null) { - try { - $uids = $this->fetch('http://github.com/UID', 1, INF); - if ($id == null) { - return $uids; - } - foreach ($uids as $k => $v) { - if ($k == $id) { - return $v; - } + if ($this->enable_uid_cache && $this->uid_cache) { + $uids = $this->uid_cache; + } else { + try { + $uids = $this->fetch('http://github.com/UID', 1, INF); + $this->setUidCache($uids); // set cache for this folder + } catch (RuntimeException $e) {} + } + + if ($id == null) { + return $uids; + } + + foreach ($uids as $k => $v) { + if ($k == $id) { + return $v; } - } catch (RuntimeException $e) {} + } + + // clear uid cache and run method again + if ($this->enable_uid_cache && $this->uid_cache) { + $this->setUidCache(null); + return $this->getUid($id); + } throw new MessageNotFoundException('unique id not found'); } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 24554556..87d3c931 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -149,6 +149,7 @@ public function logout() { if ($this->stream) { $result = \imap_close($this->stream, IMAP::CL_EXPUNGE); $this->stream = false; + $this->uid_cache = null; return $result; } return false; @@ -181,6 +182,7 @@ public function getCapabilities() { */ public function selectFolder($folder = 'INBOX') { \imap_reopen($this->stream, $folder, IMAP::OP_READONLY, 3); + $this->uid_cache = null; return $this->examineFolder($folder); } @@ -275,13 +277,20 @@ public function flags($uids, $uid = IMAP::ST_UID){ */ public function getUid($id = null) { if ($id === null) { + if ($this->enable_uid_cache && $this->uid_cache) { + return $this->uid_cache; + } + $overview = $this->overview("1:*"); $uids = []; foreach($overview as $set){ $uids[$set->msgno] = $set->uid; } + + $this->setUidCache($uids); return $uids; } + return \imap_uid($this->stream, $id); } @@ -618,4 +627,4 @@ public function setProtocol($protocol) { $this->protocol = $protocol; return $this; } -} \ No newline at end of file +} diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 5c70dba4..c622e9ed 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -32,6 +32,11 @@ abstract class Protocol implements ProtocolInterface { */ protected $debug = false; + /** + * @var boolean + */ + protected $enable_uid_cache = true; + /** * @var false|resource */ @@ -60,6 +65,13 @@ abstract class Protocol implements ProtocolInterface { 'password' => null, ]; + /** + * Cache for uid of active folder. + * + * @var null|array + */ + protected $uid_cache = null; + /** * Get an available cryptographic method * @@ -242,4 +254,32 @@ public function buildUIDCommand($command, $uid) { return trim($this->getUIDKey($uid)." ".$command); } -} \ No newline at end of file + /** + * Set the uid cache of current active folder + * + * @param array|null $uids + */ + public function setUidCache($uids) { + if (is_null($uids)) { + $this->uid_cache = null; + return; + } + + $messageNumber = 1; + + $uid_cache = []; + foreach ($uids as $uid) { + $uid_cache[$messageNumber++] = $uid; + } + + $this->uid_cache = $uid_cache; + } + + public function enableUidCache() { + $this->enable_uid_cache = true; + } + + public function disableUidCache() { + $this->enable_uid_cache = false; + } +} diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 367fd2c4..a54ae647 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use ErrorException; +use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -392,4 +393,21 @@ public function enableDebug(); * Disable the debug mode */ public function disableDebug(); -} \ No newline at end of file + + /** + * Enable uid caching + */ + public function enableUidCache(); + + /** + * Disable uid caching + */ + public function disableUidCache(); + + /** + * Set the uid cache of current active folder + * + * @param array|null $uids + */ + public function setUidCache($uids); +} diff --git a/src/config/imap.php b/src/config/imap.php index c8e1eb06..f6a2fb01 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -148,6 +148,7 @@ 'soft_fail' => false, 'rfc822' => true, 'debug' => false, + 'uid_cache' => true, 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', 'message_key' => 'list', 'fetch_order' => 'asc', From cb3afd7b6a15ec2340b2acaccda443abbf797ee6 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 7 Mar 2022 18:05:42 +0100 Subject: [PATCH 284/600] UID filter support added --- src/Query/Query.php | 129 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 26 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 962891f1..bfa33843 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -302,6 +302,24 @@ protected function getMessageKey($message_key, $msglist, $message) { return (string)$key; } + /** + * Currates a given collection aof messages + * @param Collection $available_messages + * + * @return MessageCollection + * @throws GetMessagesFailedException + */ + public function curate_messages($available_messages) { + try { + if ($available_messages->count() > 0) { + return $this->populate($available_messages); + } + return MessageCollection::make([]); + } catch (Exception $e) { + throw new GetMessagesFailedException($e->getMessage(), 0, $e); + } + } + /** * Populate a given id collection and receive a fully fetched message collection * @param Collection $available_messages @@ -349,16 +367,7 @@ protected function populate($available_messages) { * @throws GetMessagesFailedException */ public function get() { - $available_messages = $this->search(); - - try { - if ($available_messages->count() > 0) { - return $this->populate($available_messages); - } - return MessageCollection::make([]); - } catch (Exception $e) { - throw new GetMessagesFailedException($e->getMessage(), 0, $e); - } + return $this->curate_messages($this->search()); } /** @@ -476,35 +485,103 @@ public function getMessageByUid($uid) { } /** - * Get messages with UID equal or greater than given UID. - * - * @param int|string $uid + * Filter all available uids by a given closure and get a curated list of messages + * @param callable $closure * * @return MessageCollection * @throws ConnectionFailedException * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function getByUidGreaterThanEqual($uid) { + public function filter($closure) { $connection = $this->getClient()->getConnection(); $uids = $connection->getUid(); $available_messages = new Collection(); - $i = 0; - foreach ($uids as $id) { - if ($id >= $uid) { - $available_messages->put($i++, $id); + if (is_array($uids)) { + foreach ($uids as $id){ + if ($closure($id)) { + $available_messages->push($id); + } } } - try { - if ($available_messages->count() > 0) { - return $this->populate($available_messages); - } - return MessageCollection::make([]); - } catch (Exception $e) { - throw new GetMessagesFailedException($e->getMessage(), 0, $e); - } + return $this->curate_messages($available_messages); + } + + /** + * Get all messages with an uid greater or equal to a given UID + * @param int $uid + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws MessageNotFoundException + */ + public function getByUidGreaterOrEqual($uid) { + return $this->filter(function($id) use($uid){ + return $id >= $uid; + }); + } + + /** + * Get all messages with an uid greater than a given UID + * @param int $uid + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws MessageNotFoundException + */ + public function getByUidGreater($uid) { + return $this->filter(function($id) use($uid){ + return $id > $uid; + }); + } + + /** + * Get all messages with an uid lower than a given UID + * @param int $uid + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws MessageNotFoundException + */ + public function getByUidLower($uid) { + return $this->filter(function($id) use($uid){ + return $id < $uid; + }); + } + + /** + * Get all messages with an uid lower or equal to a given UID + * @param int $uid + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws MessageNotFoundException + */ + public function getByUidLowerOrEqual($uid) { + return $this->filter(function($id) use($uid){ + return $id <= $uid; + }); + } + + /** + * Get all messages with an uid greater than a given UID + * @param int $uid + * + * @return MessageCollection + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws MessageNotFoundException + */ + public function getByUidLowerThan($uid) { + return $this->filter(function($id) use($uid){ + return $id < $uid; + }); } /** From e794b50b30a5a016f4740263f27e776272a4b362 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 7 Mar 2022 18:10:46 +0100 Subject: [PATCH 285/600] Fallback date support added and code formatted --- src/Header.php | 121 +++++++++++++++++++++++--------------------- src/config/imap.php | 3 ++ 2 files changed, 65 insertions(+), 59 deletions(-) diff --git a/src/Header.php b/src/Header.php index 47f9b687..6f1863b4 100644 --- a/src/Header.php +++ b/src/Header.php @@ -82,16 +82,16 @@ public function __construct($raw_header, $attributize = true) { * @throws MethodNotFoundException */ public function __call($method, $arguments) { - if(strtolower(substr($method, 0, 3)) === 'get') { + if (strtolower(substr($method, 0, 3)) === 'get') { $name = preg_replace('/(.)(?=[A-Z])/u', '$1_', substr(strtolower($method), 3)); - if(in_array($name, array_keys($this->attributes))) { + if (in_array($name, array_keys($this->attributes))) { return $this->attributes[$name]; } } - throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported'); + throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported'); } /** @@ -111,7 +111,7 @@ public function __get($name) { * @return Attribute|mixed */ public function get($name) { - if(isset($this->attributes[$name])) { + if (isset($this->attributes[$name])) { return $this->attributes[$name]; } @@ -127,23 +127,23 @@ public function get($name) { * @return Attribute */ public function set($name, $value, $strict = false) { - if(isset($this->attributes[$name]) && $strict === false) { + if (isset($this->attributes[$name]) && $strict === false) { if ($this->attributize) { $this->attributes[$name]->add($value, true); - }else{ - if(isset($this->attributes[$name])) { + } else { + if (isset($this->attributes[$name])) { if (is_array($this->attributes[$name]) == false) { $this->attributes[$name] = [$this->attributes[$name], $value]; - }else{ + } else { $this->attributes[$name][] = $value; } - }else{ + } else { $this->attributes[$name] = $value; } } - }elseif($this->attributize == false){ + } elseif ($this->attributize == false) { $this->attributes[$name] = $value; - }else{ + } else { $this->attributes[$name] = new Attribute($name, $value); } @@ -159,7 +159,7 @@ public function set($name, $value, $strict = false) { public function find($pattern) { if (preg_match_all($pattern, $this->raw, $matches)) { if (isset($matches[1])) { - if(count($matches[1]) > 0) { + if (count($matches[1]) > 0) { return $matches[1][0]; } } @@ -172,7 +172,7 @@ public function find($pattern) { * * @return string|null */ - public function getBoundary(){ + public function getBoundary() { $regex = isset($this->config["boundary"]) ? $this->config["boundary"] : "/boundary=(.*?(?=;)|(.*))/i"; $boundary = $this->find($regex); @@ -198,7 +198,7 @@ private function clearBoundaryString($str) { * * @throws InvalidMessageDateException */ - protected function parse(){ + protected function parse() { $header = $this->rfc822_parse_headers($this->raw); $this->extractAddresses($header); @@ -216,7 +216,7 @@ protected function parse(){ $this->parseDate($header); foreach ($header as $key => $value) { $key = trim(rtrim(strtolower($key))); - if(!isset($this->attributes[$key])){ + if (!isset($this->attributes[$key])) { $this->set($key, $value); } } @@ -232,19 +232,19 @@ protected function parse(){ * * @return object */ - public function rfc822_parse_headers($raw_headers){ + public function rfc822_parse_headers($raw_headers) { $headers = []; $imap_headers = []; if (extension_loaded('imap') && $this->config["rfc822"]) { - $raw_imap_headers = (array) \imap_rfc822_parse_headers($this->raw); - foreach($raw_imap_headers as $key => $values) { + $raw_imap_headers = (array)\imap_rfc822_parse_headers($this->raw); + foreach ($raw_imap_headers as $key => $values) { $key = str_replace("-", "_", $key); $imap_headers[$key] = $values; } } $lines = explode("\r\n", str_replace("\r\n\t", ' ', $raw_headers)); $prev_header = null; - foreach($lines as $line) { + foreach ($lines as $line) { if (substr($line, 0, 1) === "\n") { $line = substr($line, 1); } @@ -255,7 +255,7 @@ public function rfc822_parse_headers($raw_headers){ if ($prev_header !== null) { $headers[$prev_header][] = $line; } - }elseif (substr($line, 0, 1) === " ") { + } elseif (substr($line, 0, 1) === " ") { $line = substr($line, 1); $line = trim(rtrim($line)); if ($prev_header !== null) { @@ -264,19 +264,19 @@ public function rfc822_parse_headers($raw_headers){ } if (is_array($headers[$prev_header])) { $headers[$prev_header][] = $line; - }else{ + } else { $headers[$prev_header] .= $line; } } - }else{ + } else { if (($pos = strpos($line, ":")) > 0) { $key = trim(rtrim(strtolower(substr($line, 0, $pos)))); $key = str_replace("-", "_", $key); $value = trim(rtrim(substr($line, $pos + 1))); if (isset($headers[$key])) { - $headers[$key][] = $value; - }else{ + $headers[$key][] = $value; + } else { $headers[$key] = [$value]; } $prev_header = $key; @@ -284,10 +284,10 @@ public function rfc822_parse_headers($raw_headers){ } } - foreach($headers as $key => $values) { + foreach ($headers as $key => $values) { if (isset($imap_headers[$key])) continue; $value = null; - switch($key){ + switch ($key) { case 'from': case 'to': case 'cc': @@ -295,14 +295,14 @@ public function rfc822_parse_headers($raw_headers){ case 'reply_to': case 'sender': $value = $this->decodeAddresses($values); - $headers[$key."address"] = implode(", ", $values); + $headers[$key . "address"] = implode(", ", $values); break; case 'subject': $value = implode(" ", $values); break; default: if (is_array($values)) { - foreach($values as $k => $v) { + foreach ($values as $k => $v) { if ($v == "") { unset($values[$k]); } @@ -323,7 +323,7 @@ public function rfc822_parse_headers($raw_headers){ $headers[$key] = $value; } - return (object) array_merge($headers, $imap_headers); + return (object)array_merge($headers, $imap_headers); } /** @@ -334,14 +334,14 @@ public function rfc822_parse_headers($raw_headers){ * @return array The decoded elements are returned in an array of objects, where each * object has two properties, charset and text. */ - public function mime_header_decode($text){ + public function mime_header_decode($text) { if (extension_loaded('imap')) { return \imap_mime_header_decode($text); } $charset = $this->getEncoding($text); return [(object)[ "charset" => $charset, - "text" => $this->convertEncoding($text, $charset) + "text" => $this->convertEncoding($text, $charset) ]]; } @@ -422,9 +422,9 @@ public function getEncoding($structure) { return EncodingAliases::get($parameter->value, $this->fallback_encoding); } } - }elseif (property_exists($structure, 'charset')) { + } elseif (property_exists($structure, 'charset')) { return EncodingAliases::get($structure->charset, $this->fallback_encoding); - }elseif (is_string($structure) === true){ + } elseif (is_string($structure) === true) { return mb_detect_encoding($structure); } @@ -457,7 +457,7 @@ private function decode($value) { if ($value !== null) { $is_utf8_base = $this->is_uft8($value); - if($decoder === 'utf-8' && extension_loaded('imap')) { + if ($decoder === 'utf-8' && extension_loaded('imap')) { $value = \imap_utf8($value); $is_utf8_base = $this->is_uft8($value); if ($is_utf8_base) { @@ -466,14 +466,14 @@ private function decode($value) { if ($this->notDecoded($original_value, $value)) { $decoded_value = $this->mime_header_decode($value); if (count($decoded_value) > 0) { - if(property_exists($decoded_value[0], "text")) { + if (property_exists($decoded_value[0], "text")) { $value = $decoded_value[0]->text; } } } - }elseif($decoder === 'iconv' && $is_utf8_base) { + } elseif ($decoder === 'iconv' && $is_utf8_base) { $value = iconv_mime_decode($value); - }elseif($is_utf8_base){ + } elseif ($is_utf8_base) { $value = mb_decode_mimeheader($value); } @@ -496,7 +496,7 @@ private function decode($value) { * @return array */ private function decodeArray($values) { - foreach($values as $key => $value) { + foreach ($values as $key => $value) { $values[$key] = $this->decode($value); } return $values; @@ -506,8 +506,8 @@ private function decodeArray($values) { * Try to extract the priority from a given raw header string */ private function findPriority() { - if(($priority = $this->get("x_priority")) === null) return; - switch((int)"$priority"){ + if (($priority = $this->get("x_priority")) === null) return; + switch ((int)"$priority") { case IMAP::MESSAGE_PRIORITY_HIGHEST; $priority = IMAP::MESSAGE_PRIORITY_HIGHEST; break; @@ -548,8 +548,8 @@ private function decodeAddresses($values) { if (count($mail_address) == 2) { $addresses[] = (object)[ "personal" => isset($parsed_address['display']) ? $parsed_address['display'] : '', - "mailbox" => $mail_address[0], - "host" => $mail_address[1], + "mailbox" => $mail_address[0], + "host" => $mail_address[1], ]; } } @@ -559,7 +559,7 @@ private function decodeAddresses($values) { return $addresses; } - foreach($values as $address) { + foreach ($values as $address) { foreach (preg_split('/, (?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { $split_address = trim(rtrim($split_address)); @@ -576,8 +576,8 @@ private function decodeAddresses($values) { list($mailbox, $host) = array_pad(explode("@", $email), 2, null); $addresses[] = (object)[ "personal" => $name, - "mailbox" => $mailbox, - "host" => $host, + "mailbox" => $mailbox, + "host" => $host, ]; } } @@ -591,7 +591,7 @@ private function decodeAddresses($values) { * @param object $header */ private function extractAddresses($header) { - foreach(['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key){ + foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) { if (property_exists($header, $key)) { $this->set($key, $this->parseAddresses($header->$key)); } @@ -612,7 +612,7 @@ private function parseAddresses($list) { } foreach ($list as $item) { - $address = (object) $item; + $address = (object)$item; if (!property_exists($address, 'mailbox')) { $address->mailbox = false; @@ -625,7 +625,7 @@ private function parseAddresses($list) { } else { $personalParts = $this->mime_header_decode($address->personal); - if(is_array($personalParts)) { + if (is_array($personalParts)) { $address->personal = ''; foreach ($personalParts as $p) { $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); @@ -637,8 +637,8 @@ private function parseAddresses($list) { } } - $address->mail = ($address->mailbox && $address->host) ? $address->mailbox.'@'.$address->host : false; - $address->full = ($address->personal) ? $address->personal.' <'.$address->mail.'>' : $address->mail; + $address->mail = ($address->mailbox && $address->host) ? $address->mailbox . '@' . $address->host : false; + $address->full = ($address->personal) ? $address->personal . ' <' . $address->mail . '>' : $address->mail; $addresses[] = new Address($address); } @@ -649,23 +649,23 @@ private function parseAddresses($list) { /** * Search and extract potential header extensions */ - private function extractHeaderExtensions(){ + private function extractHeaderExtensions() { foreach ($this->attributes as $key => $value) { if (is_array($value)) { $value = implode(", ", $value); - }else{ + } else { $value = (string)$value; } // Only parse strings and don't parse any attributes like the user-agent if (in_array($key, ["user_agent"]) === false) { - if (($pos = strpos($value, ";")) !== false){ + if (($pos = strpos($value, ";")) !== false) { $original = substr($value, 0, $pos); $this->set($key, trim(rtrim($original)), true); // Get all potential extensions $extensions = explode(";", substr($value, $pos + 1)); - foreach($extensions as $extension) { - if (($pos = strpos($extension, "=")) !== false){ + foreach ($extensions as $extension) { + if (($pos = strpos($extension, "=")) !== false) { $key = substr($extension, 0, $pos); $key = trim(rtrim(strtolower($key))); @@ -708,7 +708,7 @@ private function parseDate($header) { $parsed_date = null; $date = $header->date; - if(preg_match('/\+0580/', $date)) { + if (preg_match('/\+0580/', $date)) { $date = str_replace('+0580', '+0530', $date); } @@ -731,11 +731,14 @@ private function parseDate($header) { $date = trim(array_pop($array)); break; } - try{ + try { $parsed_date = Carbon::parse($date); } catch (\Exception $_e) { - $error_message = "Invalid message date. ID:".$this->get("message_id")." Date:".$header->date."/".$date; - throw new InvalidMessageDateException($error_message, 1100, $e); + if (!isset($this->config["fallback_date"])) { + throw new InvalidMessageDateException("Invalid message date. ID:" . $this->get("message_id") . " Date:" . $header->date . "/" . $date, 1100, $e); + } else { + $parsed_date = Carbon::parse($this->config["fallback_date"]); + } } } diff --git a/src/config/imap.php b/src/config/imap.php index f6a2fb01..590d27cb 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -113,6 +113,8 @@ | Default TRUE - Set to FALSE to prevent the usage of \imap_rfc822_parse_headers(). | See https://github.com/Webklex/php-imap/issues/115 for more information. | -Debug enable to trace communication traffic + | -UID cache enable the UID cache + | -Fallback date is used if the given message date could not be parsed | -Boundary regex used to detect message boundaries. If you are having problems with empty messages, missing | attachments or anything like this. Be advised that it likes to break which causes new problems.. | -Message key identifier option @@ -149,6 +151,7 @@ 'rfc822' => true, 'debug' => false, 'uid_cache' => true, + // 'fallback_date' => "01.01.1970 00:00:00", 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', 'message_key' => 'list', 'fetch_order' => 'asc', From a09a0f1f0511f7d21d7de125f7b9078fb1991ef3 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 7 Mar 2022 18:13:03 +0100 Subject: [PATCH 286/600] Check if IDLE is supported #199 --- src/Folder.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Folder.php b/src/Folder.php index 5c34cd59..a5c9e8c3 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -14,6 +14,7 @@ use Carbon\Carbon; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException; use Webklex\PHPIMAP\Query\WhereQuery; use Webklex\PHPIMAP\Support\FolderCollection; use Webklex\PHPIMAP\Traits\HasEvents; @@ -357,11 +358,15 @@ public function unsubscribe() { * @throws Exceptions\EventNotFoundException * @throws Exceptions\MessageFlagException * @throws Exceptions\MessageNotFoundException + * @throws Exceptions\NotSupportedCapabilityException */ public function idle(callable $callback, $timeout = 1200, $auto_reconnect = false) { $this->client->getConnection()->setConnectionTimeout($timeout); $this->client->reconnect(); + if (!in_array("IDLE", $this->client->getConnection()->getCapabilities())) { + throw new NotSupportedCapabilityException("IMAP server does not support IDLE"); + } $this->client->openFolder($this->path, true); $connection = $this->client->getConnection(); From 8949eccfbad5854fce3e6ce5b37700fd95a5e932 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 7 Mar 2022 18:14:12 +0100 Subject: [PATCH 287/600] Return type fixed to be consistent and prevent type confusion --- src/Connection/Protocols/LegacyProtocol.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 87d3c931..9e1a1480 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -559,7 +559,11 @@ public function done() { * @return array message ids */ public function search(array $params, $uid = IMAP::ST_UID) { - return \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); + $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); + if ($result === false) { + return []; + } + return $result; } /** From 94b17a39b6f3b013bd51897c4d4f16139c50261a Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 7 Mar 2022 18:18:24 +0100 Subject: [PATCH 288/600] New Exception added to handle unsupported capabilities --- .../NotSupportedCapabilityException.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Exceptions/NotSupportedCapabilityException.php diff --git a/src/Exceptions/NotSupportedCapabilityException.php b/src/Exceptions/NotSupportedCapabilityException.php new file mode 100644 index 00000000..27723dd2 --- /dev/null +++ b/src/Exceptions/NotSupportedCapabilityException.php @@ -0,0 +1,24 @@ + Date: Mon, 7 Mar 2022 18:18:52 +0100 Subject: [PATCH 289/600] Release information added --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b734dcd..cf3a3cd6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,60 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - NaN - +- ### Added - NaN - +- ### Affected Classes - NaN ### Breaking changes - NaN + +## [3.2.0] - 2022-03-07 +### Fixed +- Fix attribute serialization #179 (thanks @netpok) +- Use real tls instead of starttls #180 (thanks @netpok) +- Allow to fully overwrite default config arrays #194 (thanks @laurent-rizer) +- Query::chunked does not loop over the last chunk #196 (thanks @laurent-rizer) +- Fix isAttachment that did not properly take in consideration dispositions options #195 (thanks @laurent-rizer) +- Extend date parsing error message #173 +- Fixed 'Where' method replaces the content with uppercase #148 +- Don't surround numeric search values with quotes +- Context added to `InvalidWhereQueryCriteriaException` +- Redundant `stream_set_timeout()` removed + +### Added +- UID Cache added #204 (thanks @HelloSebastian) +- Query::class extended with `getByUidLower`, `getByUidLowerOrEqual` , `getByUidGreaterOrEqual` , `getByUidGreater` to fetch certain ranges of uids #201 (thanks @HelloSebastian) +- Check if IDLE is supported if `Folder::idle()` is called #199 (thanks @HelloSebastian) +- Fallback date support added. The config option `options.fallback_date` is used as fallback date is it is set. Otherwise, an exception will be thrown #198 +- UID filter support added +- Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) +- IMAP ID support added #174 +- Enable debug mode via config +- Custom UID alternative support added +- Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` +- Optionally move a message during "deletion" instead of just "flagging" it #106 (thanks @EthraZa) +- `WhereQuery::where()` accepts now a wide range of criteria / values. #104 + +### Affected Classes +- [Part::class](src/Part.php) +- [Query::class](src/Query/Query.php) +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) +- [Protocol::class](src/Connection/Protocols/Protocol.php) +- [ClientManager::class](src/ClientManager.php) + +### Breaking changes +- If you are using the legacy protocol to search, the results no longer return false if the search criteria could not be interpreted but instead return an empty array. This will ensure it is compatible to the rest of this library and no longer result in a potential type confusion. +- `Folder::idle` will throw an `Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException` exception if IMAP isn't supported by the mail server +- All protocol methods which had a `boolean` `$uid` option no longer support a boolean value. Use `IMAP::ST_UID` or `IMAP::NIL` instead. If you want to use an alternative to `UID` just use the string instead. +- Default config option `options.sequence` changed from `IMAP::ST_MSGN` to `IMAP::ST_UID`. +- `Folder::query()` no longer accepts a charset string. It has been replaced by an extension array, which provides the ability to automatically fetch additional features. + + ## [3.1.0-alpha] - 2022-02-03 ### Fixed - Fix attribute serialization #179 (thanks @netpok) From 354c2773381b8eb68096ac394a0e5670ab9801f8 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 7 Mar 2022 18:21:13 +0100 Subject: [PATCH 290/600] spaces fixed --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3a3cd6..e6be2264 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - NaN -- + ### Added - NaN -- + ### Affected Classes - NaN From f565c3b2510238191981dec92840bfcf3cc8b7e8 Mon Sep 17 00:00:00 2001 From: FreeScout <40499291+freescout-helpdesk@users.noreply.github.com> Date: Fri, 11 Mar 2022 00:05:09 +0300 Subject: [PATCH 291/600] Make package compatible with PHP 8.1 (#214) --- src/Attribute.php | 7 ++++--- src/EncodingAliases.php | 4 ++-- src/Message.php | 4 ++-- src/Part.php | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Attribute.php b/src/Attribute.php index ebe411a1..e526395a 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -88,7 +88,7 @@ public function toDate(){ * @param mixed $key * @return bool */ - public function offsetExists($key) { + public function offsetExists($key): bool { return array_key_exists($key, $this->values); } @@ -98,6 +98,7 @@ public function offsetExists($key) { * @param mixed $key * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->values[$key]; } @@ -109,7 +110,7 @@ public function offsetGet($key) { * @param mixed $value * @return void */ - public function offsetSet($key, $value) { + public function offsetSet($key, $value): void { if (is_null($key)) { $this->values[] = $value; } else { @@ -123,7 +124,7 @@ public function offsetSet($key, $value) { * @param string $key * @return void */ - public function offsetUnset($key) { + public function offsetUnset($key): void { unset($this->values[$key]); } diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 1eb16c13..4d84061d 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -473,8 +473,8 @@ class EncodingAliases { * @return string */ public static function get($encoding, $fallback = null) { - if (isset(self::$aliases[strtolower($encoding)])) { - return self::$aliases[strtolower($encoding)]; + if (isset(self::$aliases[strtolower($encoding ?? '')])) { + return self::$aliases[strtolower($encoding ?? '')]; } return $fallback !== null ? $fallback : $encoding; } diff --git a/src/Message.php b/src/Message.php index be636e8a..7fb7aa45 100755 --- a/src/Message.php +++ b/src/Message.php @@ -570,7 +570,7 @@ private function fetchPart(Part $part) { $content = $this->convertEncoding($content, $encoding); } - $subtype = strtolower($part->subtype); + $subtype = strtolower($part->subtype ?? ''); $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; if (isset($this->bodies[$subtype])) { @@ -719,7 +719,7 @@ public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { // https://en.wikipedia.org/wiki/ASCII // // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. - if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') { + if (strtolower($from ?? '') == 'us-ascii' && $to == 'UTF-8') { return $str; } diff --git a/src/Part.php b/src/Part.php index 2ca0a1b6..028c5575 100644 --- a/src/Part.php +++ b/src/Part.php @@ -299,7 +299,7 @@ private function parseEncoding(){ * @return bool */ public function isAttachment(){ - $valid_disposition = in_array(strtolower($this->disposition), ClientManager::get('options.dispositions')); + $valid_disposition = in_array(strtolower($this->disposition ?? ''), ClientManager::get('options.dispositions')); if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) { From 6e76b3552491e437f37721ae113129682489c141 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 10 Mar 2022 23:38:04 +0100 Subject: [PATCH 292/600] Method return and argument types added #212 --- CHANGELOG.md | 5 +- composer.json | 2 +- src/Address.php | 6 +- src/Attachment.php | 18 ++- src/Attribute.php | 67 +++++---- src/Client.php | 54 +++---- src/ClientManager.php | 40 +++-- src/Connection/Protocols/ImapProtocol.php | 106 ++++++------- src/Connection/Protocols/LegacyProtocol.php | 79 +++++----- .../Protocols/ProtocolInterface.php | 71 ++++----- src/EncodingAliases.php | 4 +- src/Events/Event.php | 2 +- src/Folder.php | 68 +++++---- src/Header.php | 31 ++-- src/Message.php | 142 +++++++++--------- src/Part.php | 8 +- src/Query/Query.php | 134 ++++++++--------- src/Query/WhereQuery.php | 82 +++++----- src/Structure.php | 6 +- src/Support/Masks/AttachmentMask.php | 2 +- src/Support/Masks/Mask.php | 6 +- src/Support/PaginatedCollection.php | 18 +-- src/Traits/HasEvents.php | 4 +- 23 files changed, 481 insertions(+), 474 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6be2264..5956596f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- PHP dependency updated to support php v8.0 #212 #214 (thanks @freescout-helpdesk) +- Method return and argument types added ### Added - NaN @@ -15,7 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Breaking changes -- NaN +- No longer supports php >=5.5.9 but instead requires at least php v7.0.0 ## [3.2.0] - 2022-03-07 diff --git a/composer.json b/composer.json index 9bc3849a..2a62e223 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ } ], "require": { - "php": ">=5.5.9", + "php": ">=7.0.0", "ext-openssl": "*", "ext-json": "*", "ext-mbstring": "*", diff --git a/src/Address.php b/src/Address.php index 644158dd..96e48d94 100644 --- a/src/Address.php +++ b/src/Address.php @@ -52,7 +52,7 @@ public function __construct($object) { * @return string */ public function __toString() { - return $this->full ? $this->full : ""; + return $this->full ?: ""; } /** @@ -75,7 +75,7 @@ public function __serialize(){ * * @return array */ - public function toArray(){ + public function toArray(): array { return $this->__serialize(); } @@ -84,7 +84,7 @@ public function toArray(){ * * @return string */ - public function toString(){ + public function toString(): string { return $this->__toString(); } } \ No newline at end of file diff --git a/src/Attachment.php b/src/Attachment.php index 29c3d442..cff02269 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -119,7 +119,7 @@ public function __construct(Message $oMessage, Part $part) { * @return mixed * @throws MethodNotFoundException */ - public function __call($method, $arguments) { + public function __call(string $method, array $arguments) { if(strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); @@ -242,8 +242,8 @@ protected function fetch() { * * @return boolean */ - public function save($path, $filename = null) { - $filename = $filename ? $filename : $this->getName(); + public function save(string $path, $filename = null): bool { + $filename = $filename ?: $this->getName(); return file_put_contents($path.$filename, $this->getContent()) !== false; } @@ -280,11 +280,13 @@ public function getMimeType(){ public function getExtension(){ $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; if (class_exists($deprecated_guesser) !== false){ + /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ return $deprecated_guesser::getInstance()->guess($this->getMimeType()); } $guesser = "\Symfony\Component\Mime\MimeTypes"; + /** @var Symfony\Component\Mime\MimeTypes $guesser */ $extensions = $guesser::getDefault()->getExtensions($this->getMimeType()); - return isset($extensions[0]) ? $extensions[0] : null; + return $extensions[0] ?? null; } /** @@ -292,14 +294,14 @@ public function getExtension(){ * * @return array */ - public function getAttributes(){ + public function getAttributes(): array { return $this->attributes; } /** * @return Message */ - public function getMessage(){ + public function getMessage(): Message { return $this->oMessage; } @@ -309,7 +311,7 @@ public function getMessage(){ * * @return $this */ - public function setMask($mask){ + public function setMask($mask): Attachment { if(class_exists($mask)){ $this->mask = $mask; } @@ -322,7 +324,7 @@ public function setMask($mask){ * * @return string */ - public function getMask(){ + public function getMask(): string { return $this->mask; } diff --git a/src/Attribute.php b/src/Attribute.php index e526395a..2f3aab0f 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -14,6 +14,7 @@ use ArrayAccess; use Carbon\Carbon; +use ReturnTypeWillChange; /** * Class Attribute @@ -34,10 +35,10 @@ class Attribute implements ArrayAccess { /** * Attribute constructor. - * @param string $name + * @param string $name * @param array|mixed $value */ - public function __construct($name, $value = null) { + public function __construct(string $name, $value = null) { $this->setName($name); $this->add($value); } @@ -57,7 +58,7 @@ public function __toString() { * * @return string */ - public function toString(){ + public function toString(): string { return $this->__toString(); } @@ -66,16 +67,16 @@ public function toString(){ * * @return array */ - public function toArray(){ + public function toArray(): array { return $this->values; } /** * Convert first value to a date object * - * @return Carbon|null + * @return Carbon */ - public function toDate(){ + public function toDate(): Carbon { $date = $this->first(); if ($date instanceof Carbon) return $date; @@ -85,47 +86,49 @@ public function toDate(){ /** * Determine if a value exists at an offset. * - * @param mixed $key + * @param mixed $offset * @return bool */ - public function offsetExists($key): bool { - return array_key_exists($key, $this->values); + public function offsetExists($offset): bool { + return array_key_exists($offset, $this->values); } /** * Get a value at a given offset. * - * @param mixed $key + * @param mixed $offset * @return mixed */ - #[\ReturnTypeWillChange] - public function offsetGet($key) { - return $this->values[$key]; + #[ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->values[$offset]; } /** * Set the value at a given offset. * - * @param mixed $key + * @param mixed $offset * @param mixed $value * @return void */ - public function offsetSet($key, $value): void { - if (is_null($key)) { + #[ReturnTypeWillChange] + public function offsetSet($offset, $value) { + if (is_null($offset)) { $this->values[] = $value; } else { - $this->values[$key] = $value; + $this->values[$offset] = $value; } } /** * Unset the value at a given offset. * - * @param string $key + * @param string $offset * @return void */ - public function offsetUnset($key): void { - unset($this->values[$key]); + #[ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->values[$offset]); } /** @@ -135,7 +138,7 @@ public function offsetUnset($key): void { * * @return Attribute */ - public function add($value, $strict = false) { + public function add($value, bool $strict = false): Attribute { if (is_array($value)) { return $this->merge($value, $strict); }elseif ($value !== null) { @@ -152,11 +155,9 @@ public function add($value, $strict = false) { * * @return Attribute */ - public function merge($values, $strict = false) { - if (is_array($values)) { - foreach ($values as $value) { - $this->attach($value, $strict); - } + public function merge(array $values, bool $strict = false): Attribute { + foreach ($values as $value) { + $this->attach($value, $strict); } return $this; @@ -168,7 +169,7 @@ public function merge($values, $strict = false) { * * @return bool */ - public function contains($value) { + public function contains($value): bool { foreach ($this->values as $v) { if ($v === $value) { return true; @@ -182,7 +183,7 @@ public function contains($value) { * @param $value * @param bool $strict */ - public function attach($value, $strict = false) { + public function attach($value, bool $strict = false) { if ($strict === true) { if ($this->contains($value) === false) { $this->values[] = $value; @@ -198,7 +199,7 @@ public function attach($value, $strict = false) { * * @return Attribute */ - public function setName($name){ + public function setName($name): Attribute { $this->name = $name; return $this; @@ -209,7 +210,7 @@ public function setName($name){ * * @return string */ - public function getName(){ + public function getName(): string { return $this->name; } @@ -218,7 +219,7 @@ public function getName(){ * * @return array */ - public function get(){ + public function get(): array { return $this->values; } @@ -227,7 +228,7 @@ public function get(){ * * @return array */ - public function all(){ + public function all(): array { return $this->get(); } @@ -260,7 +261,7 @@ public function last(){ * * @return int */ - public function count(){ + public function count(): int { return count($this->values); } } \ No newline at end of file diff --git a/src/Client.php b/src/Client.php index 6b518a23..c4abe3a7 100755 --- a/src/Client.php +++ b/src/Client.php @@ -174,7 +174,7 @@ class Client { * * @throws MaskNotFoundException */ - public function __construct($config = []) { + public function __construct(array $config = []) { $this->setConfig($config); $this->setMaskFromConfig($config); $this->setEventsFromConfig($config); @@ -193,7 +193,7 @@ public function __destruct() { * * @return self */ - public function setConfig(array $config) { + public function setConfig(array $config): Client { $default_account = ClientManager::get('default'); $default_config = ClientManager::get("accounts.$default_account"); @@ -210,7 +210,7 @@ public function setConfig(array $config) { * @param array $config * @param array $default_config */ - private function setAccountConfig($key, $config, $default_config){ + private function setAccountConfig(string $key, array $config, array $default_config){ $value = $this->default_account_config[$key]; if(isset($config[$key])) { $value = $config[$key]; @@ -301,8 +301,8 @@ public function getConnection() { * * @return bool */ - public function isConnected() { - return $this->connection ? $this->connection->connected() : false; + public function isConnected(): bool { + return $this->connection && $this->connection->connected(); } /** @@ -334,7 +334,7 @@ public function reconnect() { * @return $this * @throws ConnectionFailedException */ - public function connect() { + public function connect(): Client { $this->disconnect(); $protocol = strtolower($this->protocol); @@ -397,7 +397,7 @@ protected function authenticate() { * * @return $this */ - public function disconnect() { + public function disconnect(): Client { if ($this->isConnected() && $this->connection !== false) { $this->connection->logout(); } @@ -416,7 +416,7 @@ public function disconnect() { * @throws FolderFetchingException * @throws Exceptions\RuntimeException */ - public function getFolder($folder_name, $delimiter = null) { + public function getFolder(string $folder_name, $delimiter = null) { if ($delimiter !== false && $delimiter !== null) { return $this->getFolderByPath($folder_name); } @@ -468,7 +468,7 @@ public function getFolderByPath($folder_path) { * @throws FolderFetchingException * @throws Exceptions\RuntimeException */ - public function getFolders($hierarchical = true, $parent_folder = null) { + public function getFolders(bool $hierarchical = true, string $parent_folder = null): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -500,11 +500,11 @@ public function getFolders($hierarchical = true, $parent_folder = null) { * @param string $folder_path * @param boolean $force_select * - * @return mixed + * @return array|bool * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function openFolder($folder_path, $force_select = false) { + public function openFolder(string $folder_path, bool $force_select = false) { if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) { return true; } @@ -524,7 +524,7 @@ public function openFolder($folder_path, $force_select = false) { * @throws Exceptions\EventNotFoundException * @throws Exceptions\RuntimeException */ - public function createFolder($folder, $expunge = true) { + public function createFolder(string $folder, bool $expunge = true): Folder { $this->checkConnection(); $status = $this->connection->createFolder($folder); @@ -543,7 +543,7 @@ public function createFolder($folder, $expunge = true) { * Check a given folder * @param $folder * - * @return false|object + * @return array|bool * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ @@ -565,13 +565,13 @@ public function getFolderPath(){ * Exchange identification information * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 * - * @param null|array $ids + * @param array|null $ids * @return array|bool|void|null * * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function Id($ids = null) { + public function Id(array $ids = null) { $this->checkConnection(); return $this->connection->ID($ids); } @@ -583,7 +583,7 @@ public function Id($ids = null) { * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function getQuota() { + public function getQuota(): array { $this->checkConnection(); return $this->connection->getQuota($this->username); } @@ -595,7 +595,7 @@ public function getQuota() { * @return array * @throws ConnectionFailedException */ - public function getQuotaRoot($quota_root = 'INBOX') { + public function getQuotaRoot(string $quota_root = 'INBOX'): array { $this->checkConnection(); return $this->connection->getQuotaRoot($quota_root); } @@ -607,7 +607,7 @@ public function getQuotaRoot($quota_root = 'INBOX') { * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function expunge() { + public function expunge(): bool { $this->checkConnection(); return $this->connection->expunge(); } @@ -619,7 +619,7 @@ public function expunge() { * @return Protocol * @throws ConnectionFailedException */ - public function setTimeout($timeout) { + public function setTimeout(int $timeout): Protocol { $this->checkConnection(); return $this->connection->setConnectionTimeout($timeout); } @@ -630,7 +630,7 @@ public function setTimeout($timeout) { * @return int * @throws ConnectionFailedException */ - public function getTimeout(){ + public function getTimeout(): int { $this->checkConnection(); return $this->connection->getConnectionTimeout(); } @@ -640,7 +640,7 @@ public function getTimeout(){ * * @return string */ - public function getDefaultMessageMask(){ + public function getDefaultMessageMask(): string { return $this->default_message_mask; } @@ -650,18 +650,18 @@ public function getDefaultMessageMask(){ * * @return array */ - public function getDefaultEvents($section){ + public function getDefaultEvents($section): array { return $this->events[$section]; } /** * Set the default message mask - * @param $mask + * @param string $mask * * @return $this * @throws MaskNotFoundException */ - public function setDefaultMessageMask($mask) { + public function setDefaultMessageMask(string $mask): Client { if(class_exists($mask)) { $this->default_message_mask = $mask; @@ -676,18 +676,18 @@ public function setDefaultMessageMask($mask) { * * @return string */ - public function getDefaultAttachmentMask(){ + public function getDefaultAttachmentMask(): string { return $this->default_attachment_mask; } /** * Set the default attachment mask - * @param $mask + * @param string $mask * * @return $this * @throws MaskNotFoundException */ - public function setDefaultAttachmentMask($mask) { + public function setDefaultAttachmentMask(string $mask): Client { if(class_exists($mask)) { $this->default_attachment_mask = $mask; diff --git a/src/ClientManager.php b/src/ClientManager.php index 87fcf971..72fe4304 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -43,13 +43,13 @@ public function __construct($config = []) { /** * Dynamically pass calls to the default account. - * @param string $method - * @param array $parameters + * @param string $method + * @param array $parameters * * @return mixed * @throws Exceptions\MaskNotFoundException */ - public function __call($method, $parameters) { + public function __call(string $method, array $parameters) { $callable = [$this->account(), $method]; return call_user_func_array($callable, $parameters); @@ -62,7 +62,7 @@ public function __call($method, $parameters) { * @return Client * @throws Exceptions\MaskNotFoundException */ - public function make($config) { + public function make(array $config): Client { return new Client($config); } @@ -73,7 +73,7 @@ public function make($config) { * * @return mixed|null */ - public static function get($key, $default = null) { + public static function get(string $key, $default = null) { $parts = explode('.', $key); $value = null; foreach($parts as $part) { @@ -97,12 +97,12 @@ public static function get($key, $default = null) { /** * Resolve a account instance. - * @param string $name + * @param string|null $name * * @return Client * @throws Exceptions\MaskNotFoundException */ - public function account($name = null) { + public function account(string $name = null): Client { $name = $name ?: $this->getDefaultAccount(); // If the connection has not been resolved yet we will resolve it now as all @@ -116,14 +116,13 @@ public function account($name = null) { } /** - * Resolve a account. - * - * @param string $name + * Resolve an account. + * @param string $name * * @return Client * @throws Exceptions\MaskNotFoundException */ - protected function resolve($name) { + protected function resolve(string $name): Client { $config = $this->getClientConfig($name); return new Client($config); @@ -131,11 +130,11 @@ protected function resolve($name) { /** * Get the account configuration. - * @param string $name + * @param string|null $name * * @return array */ - protected function getClientConfig($name) { + protected function getClientConfig($name): array { if ($name === null || $name === 'null') { return ['driver' => 'null']; } @@ -148,17 +147,17 @@ protected function getClientConfig($name) { * * @return string */ - public function getDefaultAccount() { + public function getDefaultAccount(): string { return self::$config['default']; } /** * Set the name of the default account. - * @param string $name + * @param string $name * * @return void */ - public function setDefaultAccount($name) { + public function setDefaultAccount(string $name) { self::$config['default'] = $name; } @@ -174,7 +173,7 @@ public function setDefaultAccount($name) { * * @return $this */ - public function setConfig($config) { + public function setConfig($config): ClientManager { if(is_array($config) === false) { $config = require $config; @@ -219,9 +218,6 @@ public function setConfig($config) { * Numeric entries are appended, not replaced, but only if they are * unique * - * @param array $array1 Initial array to merge. - * @param array ... Variable list of arrays to recursively merge. - * * @return array|mixed * * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 @@ -247,7 +243,7 @@ private function array_merge_recursive_distinct() { foreach($append as $key => $value) { if(!array_key_exists($key, $base) and !is_numeric($key)) { - $base[$key] = $append[$key]; + $base[$key] = $value; continue; } @@ -264,7 +260,7 @@ private function array_merge_recursive_distinct() { // If the arrays are not associates we don't want to array_merge_recursive_distinct // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] // results in $resultConfig['dispositions'] = ['attachment', 'inline'] - $base[$key] = $this->array_merge_recursive_distinct($base[$key], $append[$key]); + $base[$key] = $this->array_merge_recursive_distinct($base[$key], $value); } else if(is_numeric($key)) { if(!in_array($value, $base)) $base[] = $value; } else { diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cf898887..b2949033 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -39,7 +39,7 @@ class ImapProtocol extends Protocol { * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct($cert_validation = true, $encryption = false) { + public function __construct(bool $cert_validation = true, $encryption = false) { $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -58,7 +58,7 @@ public function __destruct() { * * @throws ConnectionFailedException */ - public function connect($host, $port = null) { + public function connect(string $host, $port = null) { $transport = 'tcp'; $encryption = ''; @@ -103,7 +103,7 @@ protected function enableStartTls(){ * @return string next line * @throws RuntimeException */ - public function nextLine() { + public function nextLine(): string { $line = fgets($this->stream); if ($line === false) { @@ -120,19 +120,19 @@ public function nextLine() { * @return bool * @throws RuntimeException */ - protected function assumedNextLine($start) { + protected function assumedNextLine(string $start): bool { $line = $this->nextLine(); return strpos($line, $start) === 0; } /** * Get the next line and split the tag - * @param string $tag reference tag + * @param string|null $tag reference tag * * @return string next line * @throws RuntimeException */ - protected function nextTaggedLine(&$tag) { + protected function nextTaggedLine(&$tag): string { $line = $this->nextLine(); list($tag, $line) = explode(' ', $line, 2); @@ -146,7 +146,7 @@ protected function nextTaggedLine(&$tag) { * @return array * @throws RuntimeException */ - protected function decodeLine($line) { + protected function decodeLine(string $line): array { $tokens = []; $stack = []; @@ -158,7 +158,7 @@ protected function decodeLine($line) { continue; } while ($token[0] == '(') { - array_push($stack, $tokens); + $stack[] = $tokens; $tokens = []; $token = substr($token, 1); } @@ -231,7 +231,7 @@ protected function decodeLine($line) { * @return bool * @throws RuntimeException */ - public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false) { + public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse = false): bool { $line = $this->nextTaggedLine($tag); // get next tag if (!$dontParse) { $tokens = $this->decodeLine($line); @@ -252,7 +252,7 @@ public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false) { * @return void|null|bool|array tokens if success, false if error, null if bad request * @throws RuntimeException */ - public function readResponse($tag, $dontParse = false) { + public function readResponse(string $tag, bool $dontParse = false) { $lines = []; $tokens = null; // define $tokens variable before first use do { @@ -272,18 +272,18 @@ public function readResponse($tag, $dontParse = false) { return false; } - return; + return null; } /** * Send a new request * @param string $command * @param array $tokens additional parameters to command, use escapeString() to prepare - * @param string $tag provide a tag otherwise an autogenerated is returned + * @param string|null $tag provide a tag otherwise an autogenerated is returned * * @throws RuntimeException */ - public function sendRequest($command, $tokens = [], &$tag = null) { + public function sendRequest(string $command, array $tokens = [], string &$tag = null) { if (!$tag) { $this->noun++; $tag = 'TAG' . $this->noun; @@ -320,7 +320,7 @@ public function sendRequest($command, $tokens = [], &$tag = null) { * @return void|null|bool|array response as in readResponse() * @throws RuntimeException */ - public function requestAndResponse($command, $tokens = [], $dontParse = false) { + public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false) { $this->sendRequest($command, $tokens, $tag); return $this->readResponse($tag, $dontParse); @@ -354,7 +354,7 @@ public function escapeString($string) { * * @return string escaped list for imap */ - public function escapeList($list) { + public function escapeList(array $list): string { $result = []; foreach ($list as $v) { if (!is_array($v)) { @@ -374,9 +374,10 @@ public function escapeList($list) { * @return bool|mixed * @throws AuthFailedException */ - public function login($user, $password) { + public function login(string $user, string $password): bool { try { - return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); + $response = $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); + return $response !== null && $response !== false; } catch (RuntimeException $e) { throw new AuthFailedException("failed to authenticate", 0, $e); } @@ -390,7 +391,7 @@ public function login($user, $password) { * @return bool * @throws AuthFailedException */ - public function authenticate($user, $token) { + public function authenticate(string $user, string $token): bool { try { $authenticateParams = ['XOAUTH2', base64_encode("user=$user\1auth=Bearer $token\1\1")]; $this->sendRequest('AUTHENTICATE', $authenticateParams); @@ -416,7 +417,6 @@ public function authenticate($user, $token) { } catch (RuntimeException $e) { throw new AuthFailedException("failed to authenticate", 0, $e); } - return false; } /** @@ -424,7 +424,7 @@ public function authenticate($user, $token) { * * @return bool success */ - public function logout() { + public function logout(): bool { $result = false; if ($this->stream) { try { @@ -435,7 +435,7 @@ public function logout() { $this->uid_cache = null; } - return $result; + return $result !== false; } /** @@ -443,7 +443,7 @@ public function logout() { * * @return bool */ - public function connected(){ + public function connected(): bool { return (boolean) $this->stream; } @@ -453,7 +453,7 @@ public function connected(){ * @return array list of capabilities * @throws RuntimeException */ - public function getCapabilities() { + public function getCapabilities(): array { $response = $this->requestAndResponse('CAPABILITY'); if (!$response) return []; @@ -473,7 +473,7 @@ public function getCapabilities() { * @return bool|array * @throws RuntimeException */ - public function examineOrSelect($command = 'EXAMINE', $folder = 'INBOX') { + public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'INBOX') { $this->sendRequest($command, [$this->escapeString($folder)], $tag); $result = []; @@ -514,7 +514,7 @@ public function examineOrSelect($command = 'EXAMINE', $folder = 'INBOX') { * @return bool|array see examineOrselect() * @throws RuntimeException */ - public function selectFolder($folder = 'INBOX') { + public function selectFolder(string $folder = 'INBOX') { $this->uid_cache = null; return $this->examineOrSelect('SELECT', $folder); @@ -527,7 +527,7 @@ public function selectFolder($folder = 'INBOX') { * @return bool|array see examineOrselect() * @throws RuntimeException */ - public function examineFolder($folder = 'INBOX') { + public function examineFolder(string $folder = 'INBOX') { return $this->examineOrSelect('EXAMINE', $folder); } @@ -585,6 +585,7 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { continue; } + $data = ""; // if we only want one item we return that one directly if (count($items) == 1) { @@ -643,7 +644,7 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { * @return array * @throws RuntimeException */ - public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID) { + public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); } @@ -657,7 +658,7 @@ public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID) { * @return array * @throws RuntimeException */ - public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID){ + public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array{ return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); } @@ -670,7 +671,7 @@ public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID){ * @return array * @throws RuntimeException */ - public function flags($uids, $uid = IMAP::ST_UID){ + public function flags($uids, $uid = IMAP::ST_UID): array { return $this->fetch(["FLAGS"], $uids, null, $uid); } @@ -682,6 +683,7 @@ public function flags($uids, $uid = IMAP::ST_UID){ * @throws MessageNotFoundException */ public function getUid($id = null) { + $uids = []; if ($this->enable_uid_cache && $this->uid_cache) { $uids = $this->uid_cache; @@ -718,7 +720,7 @@ public function getUid($id = null) { * @return int message number * @throws MessageNotFoundException */ - public function getMessageNumber($id) { + public function getMessageNumber(string $id): int { $ids = $this->getUid(); foreach ($ids as $k => $v) { if ($v == $id) { @@ -737,7 +739,7 @@ public function getMessageNumber($id) { * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) * @throws RuntimeException */ - public function folders($reference = '', $folder = '*') { + public function folders(string $reference = '', string $folder = '*'): array { $result = []; $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); if (!$list || $list === true) { @@ -769,7 +771,7 @@ public function folders($reference = '', $folder = '*') { * @return bool|array new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID, $item = null) { + public function store(array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null) { $flags = $this->escapeList($flags); $set = $this->buildSet($from, $to); @@ -797,13 +799,13 @@ public function store(array $flags, $from, $to = null, $mode = null, $silent = t * Append a new message to given folder * @param string $folder name of target folder * @param string $message full message content - * @param array $flags flags for new message + * @param array|null $flags flags for new message * @param string $date date for new message * * @return bool success * @throws RuntimeException */ - public function appendMessage($folder, $message, $flags = null, $date = null) { + public function appendMessage(string $folder, string $message, $flags = null, $date = null): bool { $tokens = []; $tokens[] = $this->escapeString($folder); if ($flags !== null) { @@ -829,7 +831,7 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * @return bool success * @throws RuntimeException */ - public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { + public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("COPY", $uid); return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); @@ -838,7 +840,7 @@ public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { /** * Copy multiple messages to the target folder * - * @param array $messages List of message identifiers + * @param array $messages List of message identifiers * @param string $folder Destination folder * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. @@ -846,7 +848,7 @@ public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { * * @throws RuntimeException */ - public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { + public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { $command = $this->buildUIDCommand("COPY", $uid); $set = implode(',', $messages); @@ -867,7 +869,7 @@ public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { * @return bool success * @throws RuntimeException */ - public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { + public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); @@ -876,7 +878,7 @@ public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { /** * Move multiple messages to the target folder - * @param array $messages List of message identifiers + * @param array $messages List of message identifiers * @param string $folder Destination folder * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. @@ -884,7 +886,7 @@ public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { * @return array|bool Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID) { + public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { $command = $this->buildUIDCommand("MOVE", $uid); $set = implode(',', $messages); @@ -922,7 +924,7 @@ public function ID($ids = null) { * @return bool success * @throws RuntimeException */ - public function createFolder($folder) { + public function createFolder(string $folder): bool { return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); } @@ -934,7 +936,7 @@ public function createFolder($folder) { * @return bool success * @throws RuntimeException */ - public function renameFolder($old, $new) { + public function renameFolder(string $old, string $new): bool { return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); } @@ -945,7 +947,7 @@ public function renameFolder($old, $new) { * @return bool success * @throws RuntimeException */ - public function deleteFolder($folder) { + public function deleteFolder(string $folder): bool { return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); } @@ -956,7 +958,7 @@ public function deleteFolder($folder) { * @return bool success * @throws RuntimeException */ - public function subscribeFolder($folder) { + public function subscribeFolder(string $folder): bool { return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); } @@ -967,7 +969,7 @@ public function subscribeFolder($folder) { * @return bool success * @throws RuntimeException */ - public function unsubscribeFolder($folder) { + public function unsubscribeFolder(string $folder): bool { return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); } @@ -977,7 +979,7 @@ public function unsubscribeFolder($folder) { * @return bool success * @throws RuntimeException */ - public function expunge() { + public function expunge(): bool { return $this->requestAndResponse('EXPUNGE'); } @@ -987,7 +989,7 @@ public function expunge() { * @return bool success * @throws RuntimeException */ - public function noop() { + public function noop(): bool { return $this->requestAndResponse('NOOP'); } @@ -998,7 +1000,7 @@ public function noop() { * @return array * @throws RuntimeException */ - public function getQuota($username) { + public function getQuota($username): array { return $this->requestAndResponse("GETQUOTA", ['"#user/'.$username.'"']); } @@ -1009,7 +1011,7 @@ public function getQuota($username) { * @return array * @throws RuntimeException */ - public function getQuotaRoot($quota_root = 'INBOX') { + public function getQuotaRoot(string $quota_root = 'INBOX'): array { return $this->requestAndResponse("QUOTA", [$quota_root]); } @@ -1029,7 +1031,7 @@ public function idle() { * Send done command * @throws RuntimeException */ - public function done() { + public function done(): bool { if (fwrite($this->stream, "DONE\r\n") === false) { throw new RuntimeException('failed to write - connection closed?'); } @@ -1045,7 +1047,7 @@ public function done() { * @return array message ids * @throws RuntimeException */ - public function search(array $params, $uid = IMAP::ST_UID) { + public function search(array $params, $uid = IMAP::ST_UID): array { $command = $this->buildUIDCommand("SEARCH", $uid); $response = $this->requestAndResponse($command, $params); if (!$response) { @@ -1072,7 +1074,7 @@ public function search(array $params, $uid = IMAP::ST_UID) { * @throws MessageNotFoundException * @throws InvalidMessageDateException */ - public function overview($sequence, $uid = IMAP::ST_UID) { + public function overview(string $sequence, $uid = IMAP::ST_UID): array { $result = []; list($from, $to) = explode(":", $sequence); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 9e1a1480..df960e06 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -35,7 +35,7 @@ class LegacyProtocol extends Protocol { * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct($cert_validation = true, $encryption = false) { + public function __construct(bool $cert_validation = true, $encryption = false) { $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -52,7 +52,7 @@ public function __destruct() { * @param string $host * @param null $port */ - public function connect($host, $port = null) { + public function connect(string $host, $port = null) { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { @@ -73,7 +73,7 @@ public function connect($host, $port = null) { * @throws AuthFailedException * @throws RuntimeException */ - public function login($user, $password) { + public function login(string $user, string $password): bool { try { $this->stream = \imap_open( $this->getAddress(), @@ -104,7 +104,7 @@ public function login($user, $password) { } } - return $this->stream; + return $this->stream !== false; } /** @@ -115,7 +115,7 @@ public function login($user, $password) { * @return bool|resource * @throws AuthFailedException|RuntimeException */ - public function authenticate($user, $token) { + public function authenticate(string $user, string $token): bool { return $this->login($user, $token); } @@ -124,7 +124,7 @@ public function authenticate($user, $token) { * * @return string */ - protected function getAddress() { + protected function getAddress(): string { $address = "{".$this->host.":".$this->port."/".$this->protocol; if (!$this->cert_validation) { $address .= '/novalidate-cert'; @@ -145,7 +145,7 @@ protected function getAddress() { * * @return bool success */ - public function logout() { + public function logout(): bool { if ($this->stream) { $result = \imap_close($this->stream, IMAP::CL_EXPUNGE); $this->stream = false; @@ -160,7 +160,7 @@ public function logout() { * * @return bool */ - public function connected(){ + public function connected(): bool { return boolval($this->stream); } @@ -169,7 +169,7 @@ public function connected(){ * * @throws MethodNotSupportedException */ - public function getCapabilities() { + public function getCapabilities(): array { throw new MethodNotSupportedException(); } @@ -180,7 +180,7 @@ public function getCapabilities() { * @return bool|array see examineOrselect() * @throws RuntimeException */ - public function selectFolder($folder = 'INBOX') { + public function selectFolder(string $folder = 'INBOX') { \imap_reopen($this->stream, $folder, IMAP::OP_READONLY, 3); $this->uid_cache = null; return $this->examineFolder($folder); @@ -193,7 +193,7 @@ public function selectFolder($folder = 'INBOX') { * @return bool|array * @throws RuntimeException */ - public function examineFolder($folder = 'INBOX') { + public function examineFolder(string $folder = 'INBOX') { if (strpos($folder, ".") === 0) { throw new RuntimeException("Segmentation fault prevented. Folders starts with an illegal char '.'."); } @@ -216,7 +216,7 @@ public function examineFolder($folder = 'INBOX') { * * @return array */ - public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID) { + public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { @@ -233,7 +233,7 @@ public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID) { * * @return array */ - public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID){ + public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { @@ -249,7 +249,7 @@ public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID){ * * @return array */ - public function flags($uids, $uid = IMAP::ST_UID){ + public function flags($uids, $uid = IMAP::ST_UID): array { $result = []; $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { @@ -300,7 +300,7 @@ public function getUid($id = null) { * * @return int message number */ - public function getMessageNumber($id) { + public function getMessageNumber(string $id): int { return \imap_msgno($this->stream, $id); } @@ -311,7 +311,7 @@ public function getMessageNumber($id) { * * @return array */ - public function overview($sequence, $uid = IMAP::ST_UID) { + public function overview(string $sequence, $uid = IMAP::ST_UID): array { return \imap_fetch_overview($this->stream, $sequence,$uid ? IMAP::ST_UID : IMAP::NIL); } @@ -323,7 +323,7 @@ public function overview($sequence, $uid = IMAP::ST_UID) { * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) * @throws RuntimeException */ - public function folders($reference = '', $folder = '*') { + public function folders(string $reference = '', string $folder = '*'): array { $result = []; $items = \imap_getmailboxes($this->stream, $this->getAddress(), $reference.$folder); @@ -348,10 +348,11 @@ public function folders($reference = '', $folder = '*') { * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param null $item unused attribute * * @return bool|array new flags if $silent is false, else true or false depending on success */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID) { + public function store(array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null) { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); if ($mode == "+"){ @@ -371,12 +372,12 @@ public function store(array $flags, $from, $to = null, $mode = null, $silent = t * Append a new message to given folder * @param string $folder name of target folder * @param string $message full message content - * @param array $flags flags for new message + * @param array|null $flags flags for new message * @param string $date date for new message * * @return bool success */ - public function appendMessage($folder, $message, $flags = null, $date = null) { + public function appendMessage(string $folder, string $message, $flags = null, $date = null): bool { if ($date != null) { if ($date instanceof \Carbon\Carbon){ $date = $date->format('d-M-Y H:i:s O'); @@ -397,19 +398,19 @@ public function appendMessage($folder, $message, $flags = null, $date = null) { * * @return bool success */ - public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { + public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { return \imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL); } /** * Copy multiple messages to the target folder - * @param array $messages List of message identifiers + * @param array $messages List of message identifiers * @param string $folder Destination folder * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return array|bool Tokens if operation successful, false if an error occurred */ - public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { + public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { foreach($messages as $msg) { if ($this->copyMessage($folder, $msg, null, $uid) == false) { return false; @@ -429,19 +430,19 @@ public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID) { * * @return bool success */ - public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID) { + public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { return \imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL); } /** * Move multiple messages to the target folder - * @param array $messages List of message identifiers + * @param array $messages List of message identifiers * @param string $folder Destination folder * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return array|bool Tokens if operation successful, false if an error occurred */ - public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID) { + public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { foreach($messages as $msg) { if ($this->moveMessage($folder, $msg, null, $uid) == false) { return false; @@ -470,7 +471,7 @@ public function ID($ids = null) { * * @return bool success */ - public function createFolder($folder) { + public function createFolder(string $folder): bool { return \imap_createmailbox($this->stream, $folder); } @@ -481,7 +482,7 @@ public function createFolder($folder) { * * @return bool success */ - public function renameFolder($old, $new) { + public function renameFolder(string $old, string $new): bool { return \imap_renamemailbox($this->stream, $old, $new); } @@ -491,7 +492,7 @@ public function renameFolder($old, $new) { * * @return bool success */ - public function deleteFolder($folder) { + public function deleteFolder(string $folder): bool { return \imap_deletemailbox($this->stream, $folder); } @@ -501,7 +502,7 @@ public function deleteFolder($folder) { * * @throws MethodNotSupportedException */ - public function subscribeFolder($folder) { + public function subscribeFolder(string $folder): bool { throw new MethodNotSupportedException(); } @@ -511,7 +512,7 @@ public function subscribeFolder($folder) { * * @throws MethodNotSupportedException */ - public function unsubscribeFolder($folder) { + public function unsubscribeFolder(string $folder): bool { throw new MethodNotSupportedException(); } @@ -520,7 +521,7 @@ public function unsubscribeFolder($folder) { * * @return bool success */ - public function expunge() { + public function expunge(): bool { return \imap_expunge($this->stream); } @@ -529,7 +530,7 @@ public function expunge() { * * @throws MethodNotSupportedException */ - public function noop() { + public function noop(): bool { throw new MethodNotSupportedException(); } @@ -558,7 +559,7 @@ public function done() { * * @return array message ids */ - public function search(array $params, $uid = IMAP::ST_UID) { + public function search(array $params, $uid = IMAP::ST_UID): array { $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); if ($result === false) { return []; @@ -586,7 +587,7 @@ public function disableDebug(){ * * @param $name * - * @return mixed|string + * @return array|false|string|string[]|null */ protected function decodeFolderName($name) { preg_match('#\{(.*)\}(.*)#', $name, $preg); @@ -596,7 +597,7 @@ protected function decodeFolderName($name) { /** * @return string */ - public function getProtocol() { + public function getProtocol(): string { return $this->protocol; } @@ -606,7 +607,7 @@ public function getProtocol() { * * @return array */ - public function getQuota($username) { + public function getQuota($username): array { return \imap_get_quota($this->stream, 'user.'.$username); } @@ -616,7 +617,7 @@ public function getQuota($username) { * * @return array */ - public function getQuotaRoot($quota_root = 'INBOX') { + public function getQuotaRoot(string $quota_root = 'INBOX'): array { return \imap_get_quotaroot($this->stream, $quota_root); } @@ -624,7 +625,7 @@ public function getQuotaRoot($quota_root = 'INBOX') { * @param string $protocol * @return LegacyProtocol */ - public function setProtocol($protocol) { + public function setProtocol(string $protocol): LegacyProtocol { if (($pos = strpos($protocol, "legacy")) > 0) { $protocol = substr($protocol, 0, ($pos + 2) * -1); } diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index a54ae647..a512afe8 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -28,12 +28,6 @@ */ interface ProtocolInterface { - /** - * Protocol constructor. - * @param bool $cert_validation set to false to skip SSL certificate validation - */ - public function __construct($cert_validation = true); - /** * Public destructor */ @@ -48,7 +42,7 @@ public function __destruct(); * @throws ConnectionFailedException * @throws RuntimeException */ - public function connect($host, $port = null); + public function connect(string $host, $port = null); /** * Login to a new session. @@ -58,7 +52,7 @@ public function connect($host, $port = null); * @return bool success * @throws AuthFailedException */ - public function login($user, $password); + public function login(string $user, string $password): bool; /** * Authenticate your current session. @@ -68,21 +62,21 @@ public function login($user, $password); * @return bool|mixed * @throws AuthFailedException */ - public function authenticate($user, $token); + public function authenticate(string $user, string $token); /** * Logout of the current server session * * @return bool success */ - public function logout(); + public function logout(): bool; /** * Check if the current session is connected * * @return bool */ - public function connected(); + public function connected(): bool; /** * Get an array of available capabilities @@ -90,7 +84,7 @@ public function connected(); * @return array list of capabilities * @throws RuntimeException */ - public function getCapabilities(); + public function getCapabilities(): array; /** * Change the current folder @@ -99,7 +93,7 @@ public function getCapabilities(); * @return bool|array see examineOrselect() * @throws RuntimeException */ - public function selectFolder($folder = 'INBOX'); + public function selectFolder(string $folder = 'INBOX'); /** * Examine a given folder @@ -108,7 +102,7 @@ public function selectFolder($folder = 'INBOX'); * @return bool|array * @throws RuntimeException */ - public function examineFolder($folder = 'INBOX'); + public function examineFolder(string $folder = 'INBOX'); /** * Fetch message headers @@ -120,7 +114,7 @@ public function examineFolder($folder = 'INBOX'); * @return array * @throws RuntimeException */ - public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID); + public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array; /** * Fetch message headers @@ -132,7 +126,7 @@ public function content($uids, $rfc = "RFC822", $uid = IMAP::ST_UID); * @return array * @throws RuntimeException */ - public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID); + public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array; /** * Fetch message flags @@ -143,7 +137,7 @@ public function headers($uids, $rfc = "RFC822", $uid = IMAP::ST_UID); * @return array * @throws RuntimeException */ - public function flags($uids, $uid = IMAP::ST_UID); + public function flags($uids, $uid = IMAP::ST_UID): array; /** * Get uid for a given id @@ -161,7 +155,7 @@ public function getUid($id = null); * @return int message number * @throws MessageNotFoundException */ - public function getMessageNumber($id); + public function getMessageNumber(string $id): int; /** * Get a list of available folders @@ -171,7 +165,7 @@ public function getMessageNumber($id); * @return array mailboxes that matched $folder as array(globalName => array('delim' => .., 'flags' => ..)) * @throws RuntimeException */ - public function folders($reference = '', $folder = '*'); + public function folders(string $reference = '', string $folder = '*'): array; /** * Set message flags @@ -183,23 +177,24 @@ public function folders($reference = '', $folder = '*'); * @param bool $silent if false the return values are the new flags for the wanted messages * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. + * @param null|string $item command used to store a flag * * @return bool|array new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, $from, $to = null, $mode = null, $silent = true, $uid = IMAP::ST_UID); + public function store(array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null); /** * Append a new message to given folder * @param string $folder name of target folder * @param string $message full message content - * @param array $flags flags for new message - * @param string $date date for new message + * @param array|null $flags flags for new message + * @param string|null $date date for new message * * @return bool success * @throws RuntimeException */ - public function appendMessage($folder, $message, $flags = null, $date = null); + public function appendMessage(string $folder, string $message, $flags = null, $date = null): bool; /** * Copy message set from current folder to other folder @@ -214,7 +209,7 @@ public function appendMessage($folder, $message, $flags = null, $date = null); * @return bool success * @throws RuntimeException */ - public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID); + public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool; /** * Copy multiple messages to the target folder @@ -226,7 +221,7 @@ public function copyMessage($folder, $from, $to = null, $uid = IMAP::ST_UID); * @return array|bool Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID); + public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID); /** * Move a message set from current folder to an other folder @@ -239,7 +234,7 @@ public function copyManyMessages($messages, $folder, $uid = IMAP::ST_UID); * * @return bool success */ - public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID); + public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool; /** * Move multiple messages to the target folder @@ -252,7 +247,7 @@ public function moveMessage($folder, $from, $to = null, $uid = IMAP::ST_UID); * @return array|bool Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function moveManyMessages($messages, $folder, $uid = IMAP::ST_UID); + public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID); /** * Exchange identification information @@ -272,7 +267,7 @@ public function ID($ids = null); * @return bool success * @throws RuntimeException */ - public function createFolder($folder); + public function createFolder(string $folder): bool; /** * Rename an existing folder @@ -282,7 +277,7 @@ public function createFolder($folder); * @return bool success * @throws RuntimeException */ - public function renameFolder($old, $new); + public function renameFolder(string $old, string $new): bool; /** * Delete a folder @@ -291,7 +286,7 @@ public function renameFolder($old, $new); * @return bool success * @throws RuntimeException */ - public function deleteFolder($folder); + public function deleteFolder(string $folder): bool; /** * Subscribe to a folder @@ -300,7 +295,7 @@ public function deleteFolder($folder); * @return bool success * @throws RuntimeException */ - public function subscribeFolder($folder); + public function subscribeFolder(string $folder): bool; /** * Unsubscribe from a folder @@ -309,7 +304,7 @@ public function subscribeFolder($folder); * @return bool success * @throws RuntimeException */ - public function unsubscribeFolder($folder); + public function unsubscribeFolder(string $folder): bool; /** * Send idle command @@ -330,7 +325,7 @@ public function done(); * @return bool success * @throws RuntimeException */ - public function expunge(); + public function expunge(): bool; /** * Retrieve the quota level settings, and usage statics per mailbox @@ -339,7 +334,7 @@ public function expunge(); * @return array * @throws RuntimeException */ - public function getQuota($username); + public function getQuota($username): array; /** * Retrieve the quota settings per user @@ -349,7 +344,7 @@ public function getQuota($username); * @return array * @throws ConnectionFailedException */ - public function getQuotaRoot($quota_root = 'INBOX'); + public function getQuotaRoot(string $quota_root = 'INBOX'): array; /** * Send noop command @@ -357,7 +352,7 @@ public function getQuotaRoot($quota_root = 'INBOX'); * @return bool success * @throws RuntimeException */ - public function noop(); + public function noop(): bool; /** * Do a search request @@ -369,7 +364,7 @@ public function noop(); * @return array message ids * @throws RuntimeException */ - public function search(array $params, $uid = IMAP::ST_UID); + public function search(array $params, $uid = IMAP::ST_UID): array; /** * Get a message overview @@ -382,7 +377,7 @@ public function search(array $params, $uid = IMAP::ST_UID); * @throws MessageNotFoundException * @throws InvalidMessageDateException */ - public function overview($sequence, $uid = IMAP::ST_UID); + public function overview(string $sequence, $uid = IMAP::ST_UID): array; /** * Enable the debug mode diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 4d84061d..9140640e 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -467,12 +467,12 @@ class EncodingAliases { /** * Returns proper encoding mapping, if exsists. If it doesn't, return unchanged $encoding - * @param string $encoding + * @param string|null $encoding * @param string|null $fallback * * @return string */ - public static function get($encoding, $fallback = null) { + public static function get($encoding, string $fallback = null): string { if (isset(self::$aliases[strtolower($encoding ?? '')])) { return self::$aliases[strtolower($encoding ?? '')]; } diff --git a/src/Events/Event.php b/src/Events/Event.php index 921f28b2..f9e3e8f6 100644 --- a/src/Events/Event.php +++ b/src/Events/Event.php @@ -22,7 +22,7 @@ abstract class Event { /** * Dispatch the event with the given arguments. */ - public static function dispatch() { + public static function dispatch(): Event { return new static(func_get_args()); } } diff --git a/src/Folder.php b/src/Folder.php index a5c9e8c3..1addef49 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -115,7 +115,7 @@ class Folder { * @param string $delimiter * @param string[] $attributes */ - public function __construct(Client $client, $folder_name, $delimiter, $attributes) { + public function __construct(Client $client, string $folder_name, string $delimiter, array $attributes) { $this->client = $client; $this->events["message"] = $client->getDefaultEvents("message"); @@ -137,7 +137,7 @@ public function __construct(Client $client, $folder_name, $delimiter, $attribute * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function query($extensions = []){ + public function query(array $extensions = []): WhereQuery { $this->getClient()->checkConnection(); $this->getClient()->openFolder($this->path); $extensions = count($extensions) > 0 ? $extensions : $this->getClient()->extensions; @@ -146,20 +146,26 @@ public function query($extensions = []){ } /** - * @inheritdoc self::query($extensions = []) + * Get a new search query instance + * @param string[] $extensions + * + * @return WhereQuery * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function search($extensions = 'UTF-8'){ + public function search(array $extensions = []): WhereQuery { return $this->query($extensions); } /** - * @inheritdoc self::query($extensions = []) + * Get a new search query instance + * @param string[] $extensions + * + * @return WhereQuery * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function messages($extensions = []){ + public function messages(array $extensions = []): WhereQuery { return $this->query($extensions); } @@ -168,7 +174,7 @@ public function messages($extensions = []){ * * @return bool */ - public function hasChildren() { + public function hasChildren(): bool { return $this->has_children; } @@ -178,7 +184,7 @@ public function hasChildren() { * * @return self */ - public function setChildren($children = []) { + public function setChildren($children = []): Folder { $this->children = $children; return $this; @@ -189,7 +195,7 @@ public function setChildren($children = []) { * It converts UTF7-IMAP encoding to UTF-8. * @param $name * - * @return mixed|string + * @return array|false|string|string[]|null */ protected function decodeName($name) { return mb_convert_encoding($name, "UTF-8", "UTF7-IMAP"); @@ -213,11 +219,11 @@ protected function getSimpleName($delimiter, $full_name) { * @param $attributes */ protected function parseAttributes($attributes) { - $this->no_inferiors = in_array('\NoInferiors', $attributes) ? true : false; - $this->no_select = in_array('\NoSelect', $attributes) ? true : false; - $this->marked = in_array('\Marked', $attributes) ? true : false; - $this->referral = in_array('\Referral', $attributes) ? true : false; - $this->has_children = in_array('\HasChildren', $attributes) ? true : false; + $this->no_inferiors = in_array('\NoInferiors', $attributes); + $this->no_select = in_array('\NoSelect', $attributes); + $this->marked = in_array('\Marked', $attributes); + $this->referral = in_array('\Referral', $attributes); + $this->has_children = in_array('\HasChildren', $attributes); } /** @@ -231,7 +237,7 @@ protected function parseAttributes($attributes) { * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ - public function move($new_name, $expunge = true) { + public function move(string $new_name, bool $expunge = true): bool { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name); if($expunge) $this->client->expunge(); @@ -253,7 +259,7 @@ public function move($new_name, $expunge = true) { * @throws Exceptions\MessageNotFoundException * @throws Exceptions\RuntimeException */ - public function overview($sequence = null){ + public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN) == IMAP::ST_UID; @@ -263,24 +269,22 @@ public function overview($sequence = null){ /** * Append a string message to the current mailbox * @param string $message - * @param string $options - * @param string $internal_date + * @param array|null $options + * @param string|null|Carbon $internal_date * * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function appendMessage($message, $options = null, $internal_date = null) { + public function appendMessage(string $message, array $options = null, $internal_date = null): bool { /** * Check if $internal_date is parsed. If it is null it should not be set. Otherwise the message can't be stored. * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if ($internal_date != null) { - if ($internal_date instanceof Carbon){ - $internal_date = $internal_date->format('d-M-Y H:i:s O'); - } + if ($internal_date instanceof Carbon){ + $internal_date = $internal_date->format('d-M-Y H:i:s O'); } return $this->client->getConnection()->appendMessage($this->full_name, $message, $options, $internal_date); @@ -297,7 +301,7 @@ public function appendMessage($message, $options = null, $internal_date = null) * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ - public function rename($new_name, $expunge = true) { + public function rename(string $new_name, bool $expunge = true): bool { return $this->move($new_name, $expunge); } @@ -310,7 +314,7 @@ public function rename($new_name, $expunge = true) { * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException */ - public function delete($expunge = true) { + public function delete(bool $expunge = true): bool { $status = $this->client->getConnection()->deleteFolder($this->path); if($expunge) $this->client->expunge(); @@ -327,7 +331,7 @@ public function delete($expunge = true) { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function subscribe() { + public function subscribe(): bool { $this->client->openFolder($this->path); return $this->client->getConnection()->subscribeFolder($this->path); } @@ -339,7 +343,7 @@ public function subscribe() { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function unsubscribe() { + public function unsubscribe(): bool { $this->client->openFolder($this->path); return $this->client->getConnection()->unsubscribeFolder($this->path); } @@ -360,7 +364,7 @@ public function unsubscribe() { * @throws Exceptions\MessageNotFoundException * @throws Exceptions\NotSupportedCapabilityException */ - public function idle(callable $callback, $timeout = 1200, $auto_reconnect = false) { + public function idle(callable $callback, int $timeout = 1200, bool $auto_reconnect = false) { $this->client->getConnection()->setConnectionTimeout($timeout); $this->client->reconnect(); @@ -408,11 +412,11 @@ public function idle(callable $callback, $timeout = 1200, $auto_reconnect = fals /** * Get folder status information * - * @return array|bool + * @return array * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function getStatus() { + public function getStatus(): array { return $this->examine(); } @@ -423,7 +427,7 @@ public function getStatus() { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function examine() { + public function examine(): array { return $this->client->getConnection()->examineFolder($this->path); } @@ -432,7 +436,7 @@ public function examine() { * * @return Client */ - public function getClient() { + public function getClient(): Client { return $this->client; } diff --git a/src/Header.php b/src/Header.php index 6f1863b4..d2962ef4 100644 --- a/src/Header.php +++ b/src/Header.php @@ -66,7 +66,7 @@ class Header { * * @throws InvalidMessageDateException */ - public function __construct($raw_header, $attributize = true) { + public function __construct(string $raw_header, bool $attributize = true) { $this->raw = $raw_header; $this->config = ClientManager::get('options'); $this->attributize = $attributize; @@ -81,7 +81,7 @@ public function __construct($raw_header, $attributize = true) { * @return Attribute|mixed * @throws MethodNotFoundException */ - public function __call($method, $arguments) { + public function __call(string $method, array $arguments) { if (strtolower(substr($method, 0, 3)) === 'get') { $name = preg_replace('/(.)(?=[A-Z])/u', '$1_', substr(strtolower($method), 3)); @@ -126,7 +126,7 @@ public function get($name) { * * @return Attribute */ - public function set($name, $value, $strict = false) { + public function set(string $name, $value, bool $strict = false) { if (isset($this->attributes[$name]) && $strict === false) { if ($this->attributize) { $this->attributes[$name]->add($value, true); @@ -173,7 +173,7 @@ public function find($pattern) { * @return string|null */ public function getBoundary() { - $regex = isset($this->config["boundary"]) ? $this->config["boundary"] : "/boundary=(.*?(?=;)|(.*))/i"; + $regex = $this->config["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; $boundary = $this->find($regex); if ($boundary === null) { @@ -189,7 +189,7 @@ public function getBoundary() { * * @return string */ - private function clearBoundaryString($str) { + private function clearBoundaryString(string $str): string { return str_replace(['"', '\r', '\n', "\n", "\r", ";", "\s"], "", $str); } @@ -334,7 +334,7 @@ public function rfc822_parse_headers($raw_headers) { * @return array The decoded elements are returned in an array of objects, where each * object has two properties, charset and text. */ - public function mime_header_decode($text) { + public function mime_header_decode(string $text): array { if (extension_loaded('imap')) { return \imap_mime_header_decode($text); } @@ -352,7 +352,7 @@ public function mime_header_decode($text) { * * @return bool */ - private function notDecoded($encoded, $decoded) { + private function notDecoded($encoded, $decoded): bool { return 0 === strpos($decoded, '=?') && strlen($decoded) - 2 === strpos($decoded, '?=') && false !== strpos($encoded, $decoded); @@ -415,7 +415,7 @@ public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { * * @return string */ - public function getEncoding($structure) { + public function getEncoding($structure): string { if (property_exists($structure, 'parameters')) { foreach ($structure->parameters as $parameter) { if (strtolower($parameter->attribute) == "charset") { @@ -437,7 +437,7 @@ public function getEncoding($structure) { * * @return bool */ - private function is_uft8($value) { + private function is_uft8($value): bool { return strpos(strtolower($value), '=?utf-8?') === 0; } @@ -495,7 +495,7 @@ private function decode($value) { * * @return array */ - private function decodeArray($values) { + private function decodeArray(array $values): array { foreach ($values as $key => $value) { $values[$key] = $this->decode($value); } @@ -537,7 +537,7 @@ private function findPriority() { * * @return array */ - private function decodeAddresses($values) { + private function decodeAddresses($values): array { $addresses = []; if (extension_loaded('mailparse') && $this->config["rfc822"]) { @@ -547,7 +547,7 @@ private function decodeAddresses($values) { $mail_address = explode('@', $parsed_address['address']); if (count($mail_address) == 2) { $addresses[] = (object)[ - "personal" => isset($parsed_address['display']) ? $parsed_address['display'] : '', + "personal" => $parsed_address['display'] ?? '', "mailbox" => $mail_address[0], "host" => $mail_address[1], ]; @@ -604,7 +604,7 @@ private function extractAddresses($header) { * * @return array */ - private function parseAddresses($list) { + private function parseAddresses($list): array { $addresses = []; if (is_array($list) === false) { @@ -657,7 +657,7 @@ private function extractHeaderExtensions() { $value = (string)$value; } // Only parse strings and don't parse any attributes like the user-agent - if (in_array($key, ["user_agent"]) === false) { + if (($key == "user_agent") === false) { if (($pos = strpos($value, ";")) !== false) { $original = substr($value, 0, $pos); $this->set($key, trim(rtrim($original)), true); @@ -705,7 +705,6 @@ private function extractHeaderExtensions() { private function parseDate($header) { if (property_exists($header, 'date')) { - $parsed_date = null; $date = $header->date; if (preg_match('/\+0580/', $date)) { @@ -751,7 +750,7 @@ private function parseDate($header) { * * @return array */ - public function getAttributes() { + public function getAttributes(): array { return $this->attributes; } diff --git a/src/Message.php b/src/Message.php index 7fb7aa45..0bfc1b34 100755 --- a/src/Message.php +++ b/src/Message.php @@ -55,7 +55,6 @@ * @method Attribute getPriority() * @method Attribute getSubject() * @method Attribute getMessageId() - * @method Attribute getMessageNo() * @method Attribute getReferences() * @method Attribute getDate() * @method Attribute getFrom() @@ -93,7 +92,7 @@ class Message { /** * Attribute holder * - * @var Attribute[]|mixed[] $attributes + * @var Attribute[]|array $attributes */ protected $attributes = []; @@ -177,7 +176,7 @@ class Message { * @param integer|null $fetch_options * @param boolean $fetch_body * @param boolean $fetch_flags - * @param integer $sequence + * @param integer|null $sequence * * @throws Exceptions\ConnectionFailedException * @throws InvalidMessageDateException @@ -188,7 +187,7 @@ class Message { * @throws MessageFlagException * @throws Exceptions\MessageNotFoundException */ - public function __construct($uid, $msglist, Client $client, $fetch_options = null, $fetch_body = false, $fetch_flags = false, $sequence = null) { + public function __construct(int $uid, $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { $this->boot(); $default_mask = $client->getDefaultMessageMask(); @@ -246,7 +245,7 @@ public function __construct($uid, $msglist, Client $client, $fetch_options = nul * @throws Exceptions\RuntimeException * @throws Exceptions\MessageNotFoundException */ - public static function make($uid, $msglist, Client $client, $raw_header, $raw_body, $raw_flags, $fetch_options = null, $sequence = null){ + public static function make(int $uid, $msglist, Client $client, string $raw_header, string $raw_body, array $raw_flags, $fetch_options = null, $sequence = null): Message { $reflection = new ReflectionClass(self::class); /** @var self $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -296,7 +295,7 @@ public function boot(){ * @return mixed * @throws MethodNotFoundException */ - public function __call($method, $arguments) { + public function __call(string $method, array $arguments) { if(strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); return $this->get($name); @@ -354,7 +353,7 @@ public function get($name) { * * @return bool */ - public function hasTextBody() { + public function hasTextBody(): bool { return isset($this->bodies['text']); } @@ -376,7 +375,7 @@ public function getTextBody() { * * @return bool */ - public function hasHTMLBody() { + public function hasHTMLBody(): bool { return isset($this->bodies['html']); } @@ -416,7 +415,7 @@ private function parseHeader() { * * @throws InvalidMessageDateException */ - public function parseRawHeader($raw_header){ + public function parseRawHeader(string $raw_header){ $this->header = new Header($raw_header); } @@ -424,7 +423,7 @@ public function parseRawHeader($raw_header){ * Parse additional raw flags * @param array $raw_flags */ - public function parseRawFlags($raw_flags) { + public function parseRawFlags(array $raw_flags) { $this->flags = FlagCollection::make([]); foreach($raw_flags as $flag) { @@ -473,7 +472,7 @@ private function parseFlags() { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function parseBody() { + public function parseBody(): Message { $this->client->openFolder($this->folder_path); $sequence_id = $this->getSequenceId(); @@ -521,7 +520,7 @@ public function peek(){ * @throws MessageContentFetchingException * @throws Exceptions\RuntimeException */ - public function parseRawBody($raw_body) { + public function parseRawBody(string $raw_body): Message { $this->structure = new Structure($raw_body, $this->header); $this->fetchStructure($this->structure); @@ -535,7 +534,7 @@ public function parseRawBody($raw_body) { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - private function fetchStructure($structure) { + private function fetchStructure(Structure $structure) { $this->client->openFolder($this->folder_path); foreach ($structure->parts as $part) { @@ -585,7 +584,7 @@ private function fetchPart(Part $part) { * Fetch the Message attachment * @param Part $part */ - protected function fetchAttachment($part) { + protected function fetchAttachment(Part $part) { $oAttachment = new Attachment($this, $part); if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { @@ -603,7 +602,7 @@ protected function fetchAttachment($part) { * * @return $this */ - public function setFetchOption($option) { + public function setFetchOption($option): Message { if (is_long($option) === true) { $this->fetch_options = $option; } elseif (is_null($option) === true) { @@ -616,11 +615,11 @@ public function setFetchOption($option) { /** * Set the sequence type - * @param int $sequence + * @param int|null $sequence * * @return $this */ - public function setSequence($sequence) { + public function setSequence($sequence): Message { if (is_long($sequence)) { $this->sequence = $sequence; } elseif (is_null($sequence)) { @@ -637,7 +636,7 @@ public function setSequence($sequence) { * * @return $this */ - public function setFetchBodyOption($option) { + public function setFetchBodyOption($option): Message { if (is_bool($option)) { $this->fetch_body = $option; } elseif (is_null($option)) { @@ -654,7 +653,7 @@ public function setFetchBodyOption($option) { * * @return $this */ - public function setFetchFlagsOption($option) { + public function setFetchFlagsOption($option): Message { if (is_bool($option)) { $this->fetch_flags = $option; } elseif (is_null($option)) { @@ -672,7 +671,7 @@ public function setFetchFlagsOption($option) { * * @return string */ - public function decodeString($string, $encoding) { + public function decodeString($string, $encoding): string { switch ($encoding) { case IMAP::MESSAGE_ENC_BINARY: if (extension_loaded('imap')) { @@ -699,7 +698,7 @@ public function decodeString($string, $encoding) { * * @return mixed|string */ - public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { + public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8") { $from = EncodingAliases::get($from); $to = EncodingAliases::get($to); @@ -735,19 +734,19 @@ public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { /** * Get the encoding of a given abject - * @param object|string $structure + * @param string|object $structure * - * @return string + * @return string|null */ - public function getEncoding($structure) { + public function getEncoding($structure): string { if (property_exists($structure, 'parameters')) { foreach ($structure->parameters as $parameter) { if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value); + return EncodingAliases::get($parameter->value, "ISO-8859-2"); } } }elseif (property_exists($structure, 'charset')){ - return EncodingAliases::get($structure->charset); + return EncodingAliases::get($structure->charset, "ISO-8859-2"); }elseif (is_string($structure) === true){ return mb_detect_encoding($structure); } @@ -779,10 +778,10 @@ public function getFolder(){ * @throws Exceptions\GetMessagesFailedException * @throws Exceptions\RuntimeException */ - public function thread($sent_folder = null, &$thread = null, $folder = null){ - $thread = $thread ? $thread : MessageCollection::make([]); - $folder = $folder ? $folder : $this->getFolder(); - $sent_folder = $sent_folder ? $sent_folder : $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); + public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { + $thread = $thread ?: MessageCollection::make([]); + $folder = $folder ?: $this->getFolder(); + $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ foreach($thread as $message) { @@ -816,8 +815,9 @@ public function thread($sent_folder = null, &$thread = null, $folder = null){ * @throws Exceptions\ConnectionFailedException * @throws Exceptions\GetMessagesFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\FolderFetchingException */ - protected function fetchThreadByInReplyTo(&$thread, $in_reply_to, $primary_folder, $secondary_folder, $sent_folder){ + protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in_reply_to, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder){ $primary_folder->query()->inReplyTo($in_reply_to) ->setFetchBody($this->getFetchBodyOption()) ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ @@ -837,8 +837,9 @@ protected function fetchThreadByInReplyTo(&$thread, $in_reply_to, $primary_folde * @throws Exceptions\ConnectionFailedException * @throws Exceptions\GetMessagesFailedException * @throws Exceptions\RuntimeException + * @throws Exceptions\FolderFetchingException */ - protected function fetchThreadByMessageId(&$thread, $message_id, $primary_folder, $secondary_folder, $sent_folder){ + protected function fetchThreadByMessageId(MessageCollection &$thread, string $message_id, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder){ $primary_folder->query()->messageId($message_id) ->setFetchBody($this->getFetchBodyOption()) ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ @@ -863,7 +864,7 @@ protected function fetchThreadByMessageId(&$thread, $message_id, $primary_folder * @throws MessageFlagException * @throws Exceptions\MessageNotFoundException */ - public function copy($folder_path, $expunge = false) { + public function copy(string $folder_path, bool $expunge = false) { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path); @@ -898,7 +899,7 @@ public function copy($folder_path, $expunge = false) { * @throws MessageFlagException * @throws Exceptions\MessageNotFoundException */ - public function move($folder_path, $expunge = false) { + public function move(string $folder_path, bool $expunge = false) { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path); @@ -924,7 +925,7 @@ public function move($folder_path, $expunge = false) { * @param string $event * @param boolean $expunge * - * @return mixed + * @return Message * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws Exceptions\MessageNotFoundException @@ -934,7 +935,7 @@ public function move($folder_path, $expunge = false) { * @throws MessageFlagException * @throws MessageHeaderFetchingException */ - protected function fetchNewMail($folder, $next_uid, $event, $expunge){ + protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bool $expunge): Message { if($expunge) $this->client->expunge(); $this->client->openFolder($folder->path); @@ -961,10 +962,15 @@ protected function fetchNewMail($folder, $next_uid, $event, $expunge){ * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\EventNotFoundException - * @throws MessageFlagException + * @throws Exceptions\FolderFetchingException + * @throws Exceptions\MessageNotFoundException * @throws Exceptions\RuntimeException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException */ - public function delete($expunge = true, $trash_path = null, $force_move = false) { + public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false) { $status = $this->setFlag("Deleted"); if($force_move) { $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"]: $trash_path; @@ -988,7 +994,7 @@ public function delete($expunge = true, $trash_path = null, $force_move = false) * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function restore($expunge = true) { + public function restore(bool $expunge = true): bool { $status = $this->unsetFlag("Deleted"); if($expunge) $this->client->expunge(); @@ -1008,7 +1014,7 @@ public function restore($expunge = true) { * @throws Exceptions\EventNotFoundException * @throws Exceptions\RuntimeException */ - public function setFlag($flag) { + public function setFlag($flag): bool { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); @@ -1035,7 +1041,7 @@ public function setFlag($flag) { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function unsetFlag($flag) { + public function unsetFlag($flag): bool { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); @@ -1063,7 +1069,7 @@ public function unsetFlag($flag) { * @throws Exceptions\EventNotFoundException * @throws Exceptions\RuntimeException */ - public function addFlag($flag) { + public function addFlag($flag): bool { return $this->setFlag($flag); } @@ -1077,7 +1083,7 @@ public function addFlag($flag) { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function removeFlag($flag) { + public function removeFlag($flag): bool { return $this->unsetFlag($flag); } @@ -1086,7 +1092,7 @@ public function removeFlag($flag) { * * @return AttachmentCollection */ - public function getAttachments() { + public function getAttachments(): AttachmentCollection { return $this->attachments; } @@ -1095,7 +1101,7 @@ public function getAttachments() { * * @return AttachmentCollection */ - public function attachments(){ + public function attachments(): AttachmentCollection { return $this->getAttachments(); } @@ -1104,7 +1110,7 @@ public function attachments(){ * * @return boolean */ - public function hasAttachments() { + public function hasAttachments(): bool { return $this->attachments->isEmpty() === false; } @@ -1139,7 +1145,7 @@ public function getHeader() { * * @return Client */ - public function getClient() { + public function getClient(): Client { return $this->client; } @@ -1175,7 +1181,7 @@ public function getFetchFlagsOption() { * * @return array */ - public function getBodies() { + public function getBodies(): array { return $this->bodies; } @@ -1184,7 +1190,7 @@ public function getBodies() { * * @return FlagCollection */ - public function getFlags() { + public function getFlags(): FlagCollection { return $this->flags; } @@ -1193,7 +1199,7 @@ public function getFlags() { * * @return FlagCollection */ - public function flags(){ + public function flags(): FlagCollection { return $this->getFlags(); } @@ -1212,7 +1218,7 @@ public function getStructure(){ * @param null|Message $message * @return boolean */ - public function is(Message $message = null) { + public function is(Message $message = null): bool { if (is_null($message)) { return false; } @@ -1228,7 +1234,7 @@ public function is(Message $message = null) { * * @return array */ - public function getAttributes(){ + public function getAttributes(): array { return array_merge($this->attributes, $this->header->getAttributes()); } @@ -1238,7 +1244,7 @@ public function getAttributes(){ * * @return $this */ - public function setMask($mask){ + public function setMask($mask): Message { if(class_exists($mask)){ $this->mask = $mask; } @@ -1251,13 +1257,13 @@ public function setMask($mask){ * * @return string */ - public function getMask(){ + public function getMask(): string { return $this->mask; } /** * Get a masked instance by providing a mask name - * @param string|null $mask + * @param string|mixed $mask * * @return mixed * @throws MaskNotFoundException @@ -1276,7 +1282,7 @@ public function mask($mask = null){ * * @return string */ - public function getFolderPath(){ + public function getFolderPath(): string { return $this->folder_path; } @@ -1286,7 +1292,7 @@ public function getFolderPath(){ * * @return $this */ - public function setFolderPath($folder_path){ + public function setFolderPath($folder_path): Message { $this->folder_path = $folder_path; return $this; @@ -1298,7 +1304,7 @@ public function setFolderPath($folder_path){ * * @return $this */ - public function setConfig($config){ + public function setConfig($config): Message { $this->config = $config; return $this; @@ -1310,7 +1316,7 @@ public function setConfig($config){ * * @return $this */ - public function setAvailableFlags($available_flags){ + public function setAvailableFlags($available_flags): Message { $this->available_flags = $available_flags; return $this; @@ -1322,7 +1328,7 @@ public function setAvailableFlags($available_flags){ * * @return $this */ - public function setAttachments($attachments){ + public function setAttachments($attachments): Message { $this->attachments = $attachments; return $this; @@ -1334,7 +1340,7 @@ public function setAttachments($attachments){ * * @return $this */ - public function setFlags($flags){ + public function setFlags($flags): Message { $this->flags = $flags; return $this; @@ -1348,7 +1354,7 @@ public function setFlags($flags){ * @throws Exceptions\RuntimeException * @throws Exceptions\ConnectionFailedException */ - public function setClient($client){ + public function setClient($client): Message { $this->client = $client; $this->client->openFolder($this->folder_path); @@ -1363,7 +1369,7 @@ public function setClient($client){ * @throws Exceptions\MessageNotFoundException * @throws Exceptions\ConnectionFailedException */ - public function setUid($uid){ + public function setUid(int $uid): Message { $this->uid = $uid; $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid); $this->msglist = null; @@ -1373,14 +1379,14 @@ public function setUid($uid){ /** * Set the message number - * @param $msgn + * @param int $msgn * @param int|null $msglist * * @return $this * @throws Exceptions\MessageNotFoundException * @throws Exceptions\ConnectionFailedException */ - public function setMsgn($msgn, $msglist = null){ + public function setMsgn(int $msgn, int $msglist = null): Message { $this->msgn = $msgn; $this->msglist = $msglist; $this->uid = $this->client->getConnection()->getUid($this->msgn); @@ -1393,7 +1399,7 @@ public function setMsgn($msgn, $msglist = null){ * * @return int */ - public function getSequence(){ + public function getSequence(): int { return $this->sequence; } @@ -1402,7 +1408,7 @@ public function getSequence(){ * * @return int */ - public function getSequenceId(){ + public function getSequenceId(): int { return $this->sequence === IMAP::ST_UID ? $this->uid : $this->msgn; } @@ -1414,7 +1420,7 @@ public function getSequenceId(){ * @throws Exceptions\ConnectionFailedException * @throws Exceptions\MessageNotFoundException */ - public function setSequenceId($uid, $msglist = null){ + public function setSequenceId($uid, int $msglist = null){ if ($this->getSequence() === IMAP::ST_UID) { $this->setUid($uid); $this->setMsglist($msglist); diff --git a/src/Part.php b/src/Part.php index 028c5575..4f0985a6 100644 --- a/src/Part.php +++ b/src/Part.php @@ -142,12 +142,12 @@ class Part { /** * Part constructor. * @param $raw_part - * @param Header $header + * @param Header|null $header * @param integer $part_number * * @throws InvalidMessageDateException */ - public function __construct($raw_part, $header = null, $part_number = 0) { + public function __construct($raw_part, Header $header = null, int $part_number = 0) { $this->raw = $raw_part; $this->header = $header; $this->part_number = $part_number; @@ -207,7 +207,7 @@ protected function parse(){ * @return string * @throws InvalidMessageDateException */ - private function findHeaders(){ + private function findHeaders(): string { $body = $this->raw; while (($pos = strpos($body, "\r\n")) > 0) { $body = substr($body, $pos + 2); @@ -298,7 +298,7 @@ private function parseEncoding(){ * * @return bool */ - public function isAttachment(){ + public function isAttachment(): bool { $valid_disposition = in_array(strtolower($this->disposition ?? ''), ClientManager::get('options.dispositions')); if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) { diff --git a/src/Query/Query.php b/src/Query/Query.php index bfa33843..2607c389 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -87,7 +87,7 @@ class Query { * @param Client $client * @param string[] $extensions */ - public function __construct(Client $client, $extensions = []) { + public function __construct(Client $client, array $extensions = []) { $this->setClient($client); $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); @@ -119,7 +119,7 @@ protected function boot() { * * @return string */ - protected function parse_value($value) { + protected function parse_value($value): string { switch (true) { case $value instanceof Carbon: $value = $value->format($this->date_format); @@ -136,7 +136,7 @@ protected function parse_value($value) { * @return Carbon * @throws MessageSearchValidationException */ - protected function parse_date($date) { + protected function parse_date($date): Carbon { if ($date instanceof Carbon) return $date; try { @@ -153,7 +153,7 @@ protected function parse_date($date) { * * @return string */ - public function generate_query() { + public function generate_query(): string { $query = ''; $this->query->each(function($statement) use (&$query) { if (count($statement) == 1) { @@ -184,7 +184,7 @@ public function generate_query() { * @return Collection * @throws GetMessagesFailedException */ - protected function search() { + protected function search(): Collection { $this->generate_query(); try { @@ -203,7 +203,7 @@ protected function search() { * @return int * @throws GetMessagesFailedException */ - public function count() { + public function count(): int { return $this->search()->count(); } @@ -215,7 +215,7 @@ public function count() { * @throws ConnectionFailedException * @throws RuntimeException */ - protected function fetch($available_messages) { + protected function fetch(Collection $available_messages): array { if ($this->fetch_order === 'desc') { $available_messages = $available_messages->reverse(); } @@ -256,7 +256,7 @@ protected function fetch($available_messages) { * @throws GetMessagesFailedException * @throws ReflectionException */ - protected function make($uid, $msglist, $header, $content, $flags) { + protected function make(int $uid, int $msglist, string $header, string $content, array $flags) { try { return Message::make($uid, $msglist, $this->getClient(), $header, $content, $flags, $this->getFetchOptions(), $this->sequence); } catch (MessageNotFoundException $e) { @@ -284,7 +284,7 @@ protected function make($uid, $msglist, $header, $content, $flags) { * * @return string */ - protected function getMessageKey($message_key, $msglist, $message) { + protected function getMessageKey(string $message_key, int $msglist, Message $message): string { switch ($message_key) { case 'number': $key = $message->getMessageNo(); @@ -303,13 +303,13 @@ protected function getMessageKey($message_key, $msglist, $message) { } /** - * Currates a given collection aof messages + * Curates a given collection aof messages * @param Collection $available_messages * * @return MessageCollection * @throws GetMessagesFailedException */ - public function curate_messages($available_messages) { + public function curate_messages(Collection $available_messages): MessageCollection { try { if ($available_messages->count() > 0) { return $this->populate($available_messages); @@ -331,7 +331,7 @@ public function curate_messages($available_messages) { * @throws ReflectionException * @throws RuntimeException */ - protected function populate($available_messages) { + protected function populate(Collection $available_messages): MessageCollection { $messages = MessageCollection::make([]); $messages->total($available_messages->count()); @@ -342,9 +342,9 @@ protected function populate($available_messages) { $msglist = 0; foreach ($raw_messages["headers"] as $uid => $header) { - $content = isset($raw_messages["contents"][$uid]) ? $raw_messages["contents"][$uid] : ""; - $flag = isset($raw_messages["flags"][$uid]) ? $raw_messages["flags"][$uid] : []; - $extensions = isset($raw_messages["extensions"][$uid]) ? $raw_messages["extensions"][$uid] : []; + $content = $raw_messages["contents"][$uid] ?? ""; + $flag = $raw_messages["flags"][$uid] ?? []; + $extensions = $raw_messages["extensions"][$uid] ?? []; $message = $this->make($uid, $msglist, $header, $content, $flag); foreach($extensions as $key => $extension) { @@ -366,7 +366,7 @@ protected function populate($available_messages) { * @return MessageCollection * @throws GetMessagesFailedException */ - public function get() { + public function get(): MessageCollection { return $this->curate_messages($this->search()); } @@ -382,7 +382,7 @@ public function get() { * @throws ReflectionException * @throws RuntimeException */ - public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { + public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1) { $available_messages = $this->search(); if (($available_messages_count = $available_messages->count()) > 0) { $old_limit = $this->limit; @@ -411,7 +411,7 @@ public function chunked($callback, $chunk_size = 10, $start_chunk = 1) { * @return LengthAwarePaginator * @throws GetMessagesFailedException */ - public function paginate($per_page = 5, $page = null, $page_name = 'imap_page') { + public function paginate(int $per_page = 5, $page = null, string $page_name = 'imap_page'): LengthAwarePaginator { if ( $page === null && isset($_GET[$page_name]) @@ -443,7 +443,7 @@ public function paginate($per_page = 5, $page = null, $page_name = 'imap_page') * @throws MessageFlagException * @throws MessageNotFoundException */ - public function getMessage($uid, $msglist = null, $sequence = null) { + public function getMessage(int $uid, $msglist = null, $sequence = null): Message { return new Message($uid, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags(), $sequence ? $sequence : $this->sequence); } @@ -462,7 +462,7 @@ public function getMessage($uid, $msglist = null, $sequence = null) { * @throws MessageFlagException * @throws MessageNotFoundException */ - public function getMessageByMsgn($msgn, $msglist = null) { + public function getMessageByMsgn($msgn, $msglist = null): Message { return $this->getMessage($msgn, $msglist, IMAP::ST_MSGN); } @@ -480,7 +480,7 @@ public function getMessageByMsgn($msgn, $msglist = null) { * @throws MessageFlagException * @throws MessageNotFoundException */ - public function getMessageByUid($uid) { + public function getMessageByUid($uid): Message { return $this->getMessage($uid, null, IMAP::ST_UID); } @@ -493,7 +493,7 @@ public function getMessageByUid($uid) { * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function filter($closure) { + public function filter(callable $closure): MessageCollection { $connection = $this->getClient()->getConnection(); $uids = $connection->getUid(); @@ -518,7 +518,7 @@ public function filter($closure) { * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function getByUidGreaterOrEqual($uid) { + public function getByUidGreaterOrEqual(int $uid): MessageCollection { return $this->filter(function($id) use($uid){ return $id >= $uid; }); @@ -533,7 +533,7 @@ public function getByUidGreaterOrEqual($uid) { * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function getByUidGreater($uid) { + public function getByUidGreater(int $uid): MessageCollection { return $this->filter(function($id) use($uid){ return $id > $uid; }); @@ -548,7 +548,7 @@ public function getByUidGreater($uid) { * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function getByUidLower($uid) { + public function getByUidLower(int $uid): MessageCollection { return $this->filter(function($id) use($uid){ return $id < $uid; }); @@ -563,7 +563,7 @@ public function getByUidLower($uid) { * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function getByUidLowerOrEqual($uid) { + public function getByUidLowerOrEqual(int $uid): MessageCollection { return $this->filter(function($id) use($uid){ return $id <= $uid; }); @@ -578,7 +578,7 @@ public function getByUidLowerOrEqual($uid) { * @throws GetMessagesFailedException * @throws MessageNotFoundException */ - public function getByUidLowerThan($uid) { + public function getByUidLowerThan(int $uid): MessageCollection { return $this->filter(function($id) use($uid){ return $id < $uid; }); @@ -589,7 +589,7 @@ public function getByUidLowerThan($uid) { * * @return $this */ - public function leaveUnread() { + public function leaveUnread(): Query { $this->setFetchOptions(IMAP::FT_PEEK); return $this; @@ -600,7 +600,7 @@ public function leaveUnread() { * * @return $this */ - public function markAsRead() { + public function markAsRead(): Query { $this->setFetchOptions(IMAP::FT_UID); return $this; @@ -612,7 +612,7 @@ public function markAsRead() { * * @return $this */ - public function setSequence($sequence) { + public function setSequence(int $sequence): Query { $this->sequence = $sequence; return $this; @@ -631,7 +631,7 @@ public function getSequence() { * @return Client * @throws ConnectionFailedException */ - public function getClient() { + public function getClient(): Client { $this->client->checkConnection(); return $this->client; } @@ -643,7 +643,7 @@ public function getClient() { * * @return $this */ - public function limit($limit, $page = 1) { + public function limit(int $limit, int $page = 1): Query { if ($page >= 1) $this->page = $page; $this->limit = $limit; @@ -653,7 +653,7 @@ public function limit($limit, $page = 1) { /** * @return Collection */ - public function getQuery() { + public function getQuery(): Collection { return $this->query; } @@ -661,7 +661,7 @@ public function getQuery() { * @param array $query * @return Query */ - public function setQuery($query) { + public function setQuery(array $query): Query { $this->query = new Collection($query); return $this; } @@ -669,7 +669,7 @@ public function setQuery($query) { /** * @return string */ - public function getRawQuery() { + public function getRawQuery(): string { return $this->raw_query; } @@ -677,7 +677,7 @@ public function getRawQuery() { * @param string $raw_query * @return Query */ - public function setRawQuery($raw_query) { + public function setRawQuery(string $raw_query): Query { $this->raw_query = $raw_query; return $this; } @@ -685,7 +685,7 @@ public function setRawQuery($raw_query) { /** * @return string[] */ - public function getExtensions() { + public function getExtensions(): array { return $this->extensions; } @@ -693,7 +693,7 @@ public function getExtensions() { * @param string[] $extensions * @return Query */ - public function setExtensions($extensions) { + public function setExtensions(array $extensions): Query { $this->extensions = $extensions; if (count($this->extensions) > 0) { if (in_array("UID", $this->extensions) === false) { @@ -707,7 +707,7 @@ public function setExtensions($extensions) { * @param Client $client * @return Query */ - public function setClient(Client $client) { + public function setClient(Client $client): Query { $this->client = $client; return $this; } @@ -723,7 +723,7 @@ public function getLimit() { * @param int $limit * @return Query */ - public function setLimit($limit) { + public function setLimit(int $limit): Query { $this->limit = $limit <= 0 ? null : $limit; return $this; } @@ -731,7 +731,7 @@ public function setLimit($limit) { /** * @return int */ - public function getPage() { + public function getPage(): int { return $this->page; } @@ -739,25 +739,25 @@ public function getPage() { * @param int $page * @return Query */ - public function setPage($page) { + public function setPage(int $page): Query { $this->page = $page; return $this; } /** - * @param boolean $fetch_options + * @param int $fetch_options * @return Query */ - public function setFetchOptions($fetch_options) { + public function setFetchOptions(int $fetch_options): Query { $this->fetch_options = $fetch_options; return $this; } /** - * @param boolean $fetch_options + * @param int $fetch_options * @return Query */ - public function fetchOptions($fetch_options) { + public function fetchOptions(int $fetch_options): Query { return $this->setFetchOptions($fetch_options); } @@ -779,7 +779,7 @@ public function getFetchBody() { * @param boolean $fetch_body * @return Query */ - public function setFetchBody($fetch_body) { + public function setFetchBody(bool $fetch_body): Query { $this->fetch_body = $fetch_body; return $this; } @@ -788,7 +788,7 @@ public function setFetchBody($fetch_body) { * @param boolean $fetch_body * @return Query */ - public function fetchBody($fetch_body) { + public function fetchBody(bool $fetch_body): Query { return $this->setFetchBody($fetch_body); } @@ -803,7 +803,7 @@ public function getFetchFlags() { * @param int $fetch_flags * @return Query */ - public function setFetchFlags($fetch_flags) { + public function setFetchFlags(int $fetch_flags): Query { $this->fetch_flags = $fetch_flags; return $this; } @@ -812,7 +812,7 @@ public function setFetchFlags($fetch_flags) { * @param string $fetch_order * @return Query */ - public function setFetchOrder($fetch_order) { + public function setFetchOrder(string $fetch_order): Query { $fetch_order = strtolower($fetch_order); if (in_array($fetch_order, ['asc', 'desc'])) { @@ -826,42 +826,42 @@ public function setFetchOrder($fetch_order) { * @param string $fetch_order * @return Query */ - public function fetchOrder($fetch_order) { + public function fetchOrder(string $fetch_order): Query { return $this->setFetchOrder($fetch_order); } /** * @return string */ - public function getFetchOrder() { + public function getFetchOrder(): string { return $this->fetch_order; } /** * @return Query */ - public function setFetchOrderAsc() { + public function setFetchOrderAsc(): Query { return $this->setFetchOrder('asc'); } /** * @return Query */ - public function fetchOrderAsc() { + public function fetchOrderAsc(): Query { return $this->setFetchOrderAsc(); } /** * @return Query */ - public function setFetchOrderDesc() { + public function setFetchOrderDesc(): Query { return $this->setFetchOrder('desc'); } /** * @return Query */ - public function fetchOrderDesc() { + public function fetchOrderDesc(): Query { return $this->setFetchOrderDesc(); } @@ -870,7 +870,7 @@ public function fetchOrderDesc() { * @var boolean $state * */ - public function softFail($state = true) { + public function softFail(bool $state = true): Query { return $this->setSoftFail($state); } @@ -879,7 +879,7 @@ public function softFail($state = true) { * @var boolean $state * */ - public function setSoftFail($state = true) { + public function setSoftFail(bool $state = true): Query { $this->soft_fail = $state; return $this; @@ -888,7 +888,7 @@ public function setSoftFail($state = true) { /** * @return boolean */ - public function getSoftFail() { + public function getSoftFail(): bool { return $this->soft_fail; } @@ -898,7 +898,7 @@ public function getSoftFail() { * * @throws GetMessagesFailedException */ - protected function handleException($uid) { + protected function handleException(int $uid) { if ($this->soft_fail === false && $this->hasError($uid)) { $error = $this->getError($uid); throw new GetMessagesFailedException($error->getMessage(), 0, $error); @@ -910,7 +910,7 @@ protected function handleException($uid) { * @param integer $uid * @param Exception $error */ - protected function setError($uid, $error) { + protected function setError(int $uid, Exception $error) { $this->errors[$uid] = $error; } @@ -920,7 +920,7 @@ protected function setError($uid, $error) { * @var integer|null $uid * */ - public function hasErrors($uid = null) { + public function hasErrors($uid = null): bool { if ($uid !== null) { return $this->hasError($uid); } @@ -933,7 +933,7 @@ public function hasErrors($uid = null) { * @var integer $uid * */ - public function hasError($uid) { + public function hasError(int $uid): bool { return isset($this->errors[$uid]); } @@ -942,7 +942,7 @@ public function hasError($uid) { * * @return array */ - public function errors() { + public function errors(): array { return $this->getErrors(); } @@ -951,7 +951,7 @@ public function errors() { * * @return array */ - public function getErrors() { + public function getErrors(): array { return $this->errors; } @@ -961,7 +961,7 @@ public function getErrors() { * @var integer $uid * */ - public function error($uid) { + public function error(int $uid) { return $this->getError($uid); } @@ -971,7 +971,7 @@ public function error($uid) { * @var integer $uid * */ - public function getError($uid) { + public function getError(int $uid) { if ($this->hasError($uid)) { return $this->errors[$uid]; } diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index f795974e..b6483f6d 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -74,7 +74,7 @@ class WhereQuery extends Query { * @throws InvalidWhereQueryCriteriaException * @throws MethodNotFoundException */ - public function __call($name, $arguments) { + public function __call(string $name, $arguments) { $that = $this; $name = Str::camel($name); @@ -104,7 +104,7 @@ public function __call($name, $arguments) { * @return string * @throws InvalidWhereQueryCriteriaException */ - protected function validate_criteria($criteria) { + protected function validate_criteria($criteria): string { $command = strtoupper($criteria); if (substr($command, 0, 7) === "CUSTOM ") { return substr($criteria, 7); @@ -132,7 +132,7 @@ protected function validate_criteria($criteria) { * $query->where(["FROM" => "someone@email.tld", "SEEN"]); * $query->where("FROM", "someone@email.tld")->where("SEEN"); */ - public function where($criteria, $value = null) { + public function where($criteria, $value = null): WhereQuery { if (is_array($criteria)) { foreach ($criteria as $key => $value) { if (is_numeric($key)) { @@ -155,7 +155,7 @@ public function where($criteria, $value = null) { * * @throws InvalidWhereQueryCriteriaException */ - protected function push_search_criteria($criteria, $value){ + protected function push_search_criteria(string $criteria, $value){ $criteria = $this->validate_criteria($criteria); $value = $this->parse_value($value); @@ -171,7 +171,7 @@ protected function push_search_criteria($criteria, $value){ * * @return $this */ - public function orWhere(Closure $closure = null) { + public function orWhere(Closure $closure = null): WhereQuery { $this->query->push(['OR']); if ($closure !== null) $closure($this); @@ -183,7 +183,7 @@ public function orWhere(Closure $closure = null) { * * @return $this */ - public function andWhere(Closure $closure = null) { + public function andWhere(Closure $closure = null): WhereQuery { $this->query->push(['AND']); if ($closure !== null) $closure($this); @@ -194,7 +194,7 @@ public function andWhere(Closure $closure = null) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereAll() { + public function whereAll(): WhereQuery { return $this->where('ALL'); } @@ -202,7 +202,7 @@ public function whereAll() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereAnswered() { + public function whereAnswered(): WhereQuery { return $this->where('ANSWERED'); } @@ -212,7 +212,7 @@ public function whereAnswered() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereBcc($value) { + public function whereBcc(string $value): WhereQuery { return $this->where('BCC', $value); } @@ -222,7 +222,7 @@ public function whereBcc($value) { * @throws InvalidWhereQueryCriteriaException * @throws MessageSearchValidationException */ - public function whereBefore($value) { + public function whereBefore($value): WhereQuery { $date = $this->parse_date($value); return $this->where('BEFORE', $date); } @@ -233,7 +233,7 @@ public function whereBefore($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereBody($value) { + public function whereBody(string $value): WhereQuery { return $this->where('BODY', $value); } @@ -243,7 +243,7 @@ public function whereBody($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereCc($value) { + public function whereCc(string $value): WhereQuery { return $this->where('CC', $value); } @@ -251,7 +251,7 @@ public function whereCc($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereDeleted() { + public function whereDeleted(): WhereQuery { return $this->where('DELETED'); } @@ -261,7 +261,7 @@ public function whereDeleted() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereFlagged($value) { + public function whereFlagged(string $value): WhereQuery { return $this->where('FLAGGED', $value); } @@ -271,7 +271,7 @@ public function whereFlagged($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereFrom($value) { + public function whereFrom(string $value): WhereQuery { return $this->where('FROM', $value); } @@ -281,7 +281,7 @@ public function whereFrom($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereKeyword($value) { + public function whereKeyword(string $value): WhereQuery { return $this->where('KEYWORD', $value); } @@ -289,7 +289,7 @@ public function whereKeyword($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereNew() { + public function whereNew(): WhereQuery { return $this->where('NEW'); } @@ -297,7 +297,7 @@ public function whereNew() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereNot() { + public function whereNot(): WhereQuery { return $this->where('NOT'); } @@ -305,7 +305,7 @@ public function whereNot() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereOld() { + public function whereOld(): WhereQuery { return $this->where('OLD'); } @@ -316,7 +316,7 @@ public function whereOld() { * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereOn($value) { + public function whereOn($value): WhereQuery { $date = $this->parse_date($value); return $this->where('ON', $date); } @@ -325,7 +325,7 @@ public function whereOn($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereRecent() { + public function whereRecent(): WhereQuery { return $this->where('RECENT'); } @@ -333,7 +333,7 @@ public function whereRecent() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereSeen() { + public function whereSeen(): WhereQuery { return $this->where('SEEN'); } @@ -344,7 +344,7 @@ public function whereSeen() { * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereSince($value) { + public function whereSince($value): WhereQuery { $date = $this->parse_date($value); return $this->where('SINCE', $date); } @@ -355,7 +355,7 @@ public function whereSince($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereSubject($value) { + public function whereSubject(string $value): WhereQuery { return $this->where('SUBJECT', $value); } @@ -365,7 +365,7 @@ public function whereSubject($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereText($value) { + public function whereText(string $value): WhereQuery { return $this->where('TEXT', $value); } @@ -375,7 +375,7 @@ public function whereText($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereTo($value) { + public function whereTo(string $value): WhereQuery { return $this->where('TO', $value); } @@ -385,7 +385,7 @@ public function whereTo($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUnkeyword($value) { + public function whereUnkeyword(string $value): WhereQuery { return $this->where('UNKEYWORD', $value); } @@ -393,7 +393,7 @@ public function whereUnkeyword($value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUnanswered() { + public function whereUnanswered(): WhereQuery { return $this->where('UNANSWERED'); } @@ -401,7 +401,7 @@ public function whereUnanswered() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUndeleted() { + public function whereUndeleted(): WhereQuery { return $this->where('UNDELETED'); } @@ -409,7 +409,7 @@ public function whereUndeleted() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUnflagged() { + public function whereUnflagged(): WhereQuery { return $this->where('UNFLAGGED'); } @@ -417,7 +417,7 @@ public function whereUnflagged() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUnseen() { + public function whereUnseen(): WhereQuery { return $this->where('UNSEEN'); } @@ -425,7 +425,7 @@ public function whereUnseen() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereNoXSpam() { + public function whereNoXSpam(): WhereQuery { return $this->where("CUSTOM X-Spam-Flag NO"); } @@ -433,7 +433,7 @@ public function whereNoXSpam() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereIsXSpam() { + public function whereIsXSpam(): WhereQuery { return $this->where("CUSTOM X-Spam-Flag YES"); } @@ -445,7 +445,7 @@ public function whereIsXSpam() { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereHeader($header, $value) { + public function whereHeader($header, $value): WhereQuery { return $this->where("CUSTOM HEADER $header $value"); } @@ -456,7 +456,7 @@ public function whereHeader($header, $value) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereMessageId($messageId) { + public function whereMessageId($messageId): WhereQuery { return $this->whereHeader("Message-ID", $messageId); } @@ -467,7 +467,7 @@ public function whereMessageId($messageId) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereInReplyTo($messageId) { + public function whereInReplyTo($messageId): WhereQuery { return $this->whereHeader("In-Reply-To", $messageId); } @@ -477,7 +477,7 @@ public function whereInReplyTo($messageId) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereLanguage($country_code) { + public function whereLanguage($country_code): WhereQuery { return $this->where("Content-Language $country_code"); } @@ -489,7 +489,7 @@ public function whereLanguage($country_code) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUid($uid) { + public function whereUid($uid): WhereQuery { return $this->where('UID', $uid); } @@ -501,7 +501,7 @@ public function whereUid($uid) { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUidIn($uids) { + public function whereUidIn(array $uids): WhereQuery { $uids = implode(',', $uids); return $this->where('UID', $uids); } @@ -515,7 +515,7 @@ public function whereUidIn($uids) { * @param callable|null $default * @return $this|mixed */ - public function when($value, $callback, $default = null) { + public function when($value, callable $callback, $default = null) { if ($value) { return $callback($this, $value) ?: $this; } elseif ($default) { @@ -534,7 +534,7 @@ public function when($value, $callback, $default = null) { * @param callable|null $default * @return $this|mixed */ - public function unless($value, $callback, $default = null) { + public function unless($value, callable $callback, $default = null) { if (!$value) { return $callback($this, $value) ?: $this; } elseif ($default) { diff --git a/src/Structure.php b/src/Structure.php index a6e65b93..682d7bff 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -103,7 +103,7 @@ public function findContentType(){ * @return Part[] * @throws InvalidMessageDateException */ - private function parsePart($context, $part_number = 0){ + private function parsePart(string $context, int $part_number = 0): array { $body = $context; while (($pos = strpos($body, "\r\n")) > 0) { $body = substr($body, $pos + 2); @@ -126,7 +126,7 @@ private function parsePart($context, $part_number = 0){ * @return array * @throws InvalidMessageDateException */ - private function detectParts($boundary, $context, $part_number = 0){ + private function detectParts(string $boundary, string $context, int $part_number = 0): array { $base_parts = explode( $boundary, $context); $final_parts = []; foreach($base_parts as $ctx) { @@ -150,7 +150,7 @@ private function detectParts($boundary, $context, $part_number = 0){ * @throws MessageContentFetchingException * @throws InvalidMessageDateException */ - public function find_parts(){ + public function find_parts(): array { if($this->type === IMAP::MESSAGE_TYPE_MULTIPART) { if (($boundary = $this->header->getBoundary()) === null) { throw new MessageContentFetchingException("no content found", 0); diff --git a/src/Support/Masks/AttachmentMask.php b/src/Support/Masks/AttachmentMask.php index ad93940c..1f9855e8 100644 --- a/src/Support/Masks/AttachmentMask.php +++ b/src/Support/Masks/AttachmentMask.php @@ -34,7 +34,7 @@ public function getContentBase64Encoded() { } /** - * Get an base64 image src string + * Get a base64 image src string * * @return string|null */ diff --git a/src/Support/Masks/Mask.php b/src/Support/Masks/Mask.php index 7483bd5e..1ab17d5a 100755 --- a/src/Support/Masks/Mask.php +++ b/src/Support/Masks/Mask.php @@ -63,7 +63,7 @@ protected function boot(){} * @return mixed * @throws MethodNotFoundException */ - public function __call($method, $arguments) { + public function __call(string $method, array $arguments) { if(strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); @@ -119,7 +119,7 @@ public function __get($name) { /** * Get the parent instance * - * @return mixed + * @return object */ public function getParent(){ return $this->parent; @@ -130,7 +130,7 @@ public function getParent(){ * * @return array */ - public function getAttributes(){ + public function getAttributes(): array { return $this->attributes; } diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index b9764526..36ec4c5a 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -32,17 +32,17 @@ class PaginatedCollection extends Collection { /** * Paginate the current collection. - * @param int $per_page + * @param int $per_page * @param int|null $page - * @param string $page_name - * @param boolean $prepaginated + * @param string $page_name + * @param boolean $prepaginated * * @return LengthAwarePaginator */ - public function paginate($per_page = 15, $page = null, $page_name = 'page', $prepaginated = false) { + public function paginate(int $per_page = 15, $page = null, string $page_name = 'page', bool $prepaginated = false): LengthAwarePaginator { $page = $page ?: Paginator::resolveCurrentPage($page_name); - $total = $this->total ? $this->total : $this->count(); + $total = $this->total ?: $this->count(); $results = !$prepaginated && $total ? $this->forPage($page, $per_page) : $this->all(); @@ -54,15 +54,15 @@ public function paginate($per_page = 15, $page = null, $page_name = 'page', $pre /** * Create a new length-aware paginator instance. - * @param array $items - * @param int $total - * @param int $per_page + * @param array $items + * @param int $total + * @param int $per_page * @param int|null $current_page * @param array $options * * @return LengthAwarePaginator */ - protected function paginator($items, $total, $per_page, $current_page, array $options) { + protected function paginator(array $items, int $total, int $per_page, $current_page, array $options): LengthAwarePaginator { return new LengthAwarePaginator($items, $total, $per_page, $current_page, $options); } diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index bc7ae68e..1852b432 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -58,7 +58,7 @@ public function setEvents($events) { * @return Event * @throws EventNotFoundException */ - public function getEvent($section, $event) { + public function getEvent($section, $event): Event { if (isset($this->events[$section])) { return $this->events[$section][$event]; } @@ -70,7 +70,7 @@ public function getEvent($section, $event) { * * @return array */ - public function getEvents(){ + public function getEvents(): array { return $this->events; } From 0897d64c86eb69cd55d18d2fee2e390868429787 Mon Sep 17 00:00:00 2001 From: Webklex Date: Thu, 11 Aug 2022 18:51:43 +0200 Subject: [PATCH 293/600] Create FUNDING.yml --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..c49bc6e3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +ko_fi: webklex +custom: ['https://www.buymeacoffee.com/webklex'] From 498a40d83d5171aea8ca15ac5224e6960ff6a8fd Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 20:54:08 +0200 Subject: [PATCH 294/600] Issue setting the client timeout fixed --- src/Client.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Client.php b/src/Client.php index c4abe3a7..bda9abac 100755 --- a/src/Client.php +++ b/src/Client.php @@ -317,7 +317,7 @@ public function checkConnection() { } /** - * Force a reconnect + * Force the connection to reconnect * * @throws ConnectionFailedException */ @@ -620,8 +620,12 @@ public function expunge(): bool { * @throws ConnectionFailedException */ public function setTimeout(int $timeout): Protocol { - $this->checkConnection(); - return $this->connection->setConnectionTimeout($timeout); + $this->timeout = $timeout; + if ($this->isConnected()) { + $this->connection->setConnectionTimeout($timeout); + $this->reconnect(); + } + return $this->connection; } /** From 497e3216d34ef600e1c09746a5b6adc2999e2fc8 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 20:54:50 +0200 Subject: [PATCH 295/600] return value set to mixed to allow multiple event types --- src/Traits/HasEvents.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index 1852b432..56a60915 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -55,10 +55,10 @@ public function setEvents($events) { * @param $section * @param $event * - * @return Event + * @return Event|string * @throws EventNotFoundException */ - public function getEvent($section, $event): Event { + public function getEvent($section, $event) { if (isset($this->events[$section])) { return $this->events[$section][$event]; } From 7ba46d19596db7245fe4123f3e32fb8f5726da3f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 20:55:33 +0200 Subject: [PATCH 296/600] Datetime conversion rules extended #189 #173 --- src/Header.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Header.php b/src/Header.php index d2962ef4..3deb1de4 100644 --- a/src/Header.php +++ b/src/Header.php @@ -713,9 +713,15 @@ private function parseDate($header) { $date = trim(rtrim($date)); try { + if(strpos($date, ' ') !== false){ + $date = str_replace(' ', ' ', $date); + } $parsed_date = Carbon::parse($date); } catch (\Exception $e) { switch (true) { + case preg_match('/([0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}\-[0-9]{1,2}\.[0-9]{1,2}.[0-9]{1,2})+$/i', $date) > 0: + $date = Carbon::createFromFormat("Y.m.d-H.i.s", $date); + break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; From f0fcdb728ed8285789e7a3d0cca96f7f93a30ec5 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 20:57:40 +0200 Subject: [PATCH 297/600] IMAP Connection debugging improved --- src/Connection/Protocols/ImapProtocol.php | 50 ++++++++++++++++------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b2949033..74a69704 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -104,13 +104,15 @@ protected function enableStartTls(){ * @throws RuntimeException */ public function nextLine(): string { - $line = fgets($this->stream); - - if ($line === false) { - throw new RuntimeException('failed to read - connection closed?'); + $line = ""; + while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { + $line .= $next_char; } - - return $line; + if ($line === "") { + throw new RuntimeException('empty response'); + } + if ($this->debug) echo "<< ".$line."\n"; + return $line . "\n"; } /** @@ -139,6 +141,19 @@ protected function nextTaggedLine(&$tag): string { return $line; } + /** + * Get the next line and check if it contains a given string and split the tag + * @param string $start + * @param $tag + * + * @return bool + * @throws RuntimeException + */ + protected function assumedNextTaggedLine(string $start, &$tag): bool { + $line = $this->nextTaggedLine($tag); + return strpos($line, $start) >= 0; + } + /** * Split a given line in values. A value is literal of any form or a list * @param string $line @@ -238,7 +253,6 @@ public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse } else { $tokens = $line; } - if ($this->debug) echo "<< ".$line."\n"; // if tag is wanted tag we might be at the end of a multiline response return $tag == $wantedTag; @@ -249,7 +263,7 @@ public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse * @param string $tag request tag * @param bool $dontParse if true every line is returned unparsed instead of the decoded tokens * - * @return void|null|bool|array tokens if success, false if error, null if bad request + * @return array|bool|null tokens if success, false if error, null if bad request * @throws RuntimeException */ public function readResponse(string $tag, bool $dontParse = false) { @@ -293,9 +307,7 @@ public function sendRequest(string $command, array $tokens = [], string &$tag = foreach ($tokens as $token) { if (is_array($token)) { - if (fwrite($this->stream, $line . ' ' . $token[0] . "\r\n") === false) { - throw new RuntimeException('failed to write - connection closed?'); - } + $this->write($line . ' ' . $token[0]); if (!$this->assumedNextLine('+ ')) { throw new RuntimeException('failed to send literal string'); } @@ -304,9 +316,19 @@ public function sendRequest(string $command, array $tokens = [], string &$tag = $line .= ' ' . $token; } } - if ($this->debug) echo ">> ".$line."\n"; + $this->write($line); + } + + /** + * Write data to the current stream + * @param string $data + * @return void + * @throws RuntimeException + */ + public function write(string $data) { + if ($this->debug) echo ">> ".$data ."\n"; - if (fwrite($this->stream, $line . "\r\n") === false) { + if (fwrite($this->stream, $data . "\r\n") === false) { throw new RuntimeException('failed to write - connection closed?'); } } @@ -317,7 +339,7 @@ public function sendRequest(string $command, array $tokens = [], string &$tag = * @param array $tokens parameters as in sendRequest() * @param bool $dontParse if true unparsed lines are returned instead of tokens * - * @return void|null|bool|array response as in readResponse() + * @return array|bool|null response as in readResponse() * @throws RuntimeException */ public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false) { From 8a50a2d39671fa90dcaa56fc89c784def1893faf Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 20:58:26 +0200 Subject: [PATCH 298/600] UID cache loop fixed --- src/Connection/Protocols/ImapProtocol.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 74a69704..a9bd2be2 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -705,16 +705,12 @@ public function flags($uids, $uid = IMAP::ST_UID): array { * @throws MessageNotFoundException */ public function getUid($id = null) { - $uids = []; - - if ($this->enable_uid_cache && $this->uid_cache) { - $uids = $this->uid_cache; - } else { + if (!$this->enable_uid_cache || $this->uid_cache === null || ($this->uid_cache && count($this->uid_cache) <= 0)) { try { - $uids = $this->fetch('http://github.com/UID', 1, INF); - $this->setUidCache($uids); // set cache for this folder + $this->setUidCache($this->fetch('http://github.com/UID', 1, INF)); // set cache for this folder } catch (RuntimeException $e) {} } + $uids = $this->uid_cache; if ($id == null) { return $uids; From f0f6d61f5a26d50fc6cacbbd779ec5d94585c19a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 20:59:53 +0200 Subject: [PATCH 299/600] Idle command reworked and several issues fixed #170 #229 #237 #249 #258 --- src/Connection/Protocols/ImapProtocol.php | 5 ++-- src/Folder.php | 35 +++++++++++++---------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index a9bd2be2..9f592b7c 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1050,8 +1050,9 @@ public function idle() { * @throws RuntimeException */ public function done(): bool { - if (fwrite($this->stream, "DONE\r\n") === false) { - throw new RuntimeException('failed to write - connection closed?'); + $this->write("DONE"); + if (!$this->assumedNextTaggedLine('OK', $tags)) { + throw new RuntimeException('done failed'); } return true; } diff --git a/src/Folder.php b/src/Folder.php index 1addef49..a904b9a9 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -351,8 +351,8 @@ public function unsubscribe(): bool { /** * Idle the current connection * @param callable $callback - * @param integer $timeout max 1740 seconds - recommended by rfc2177 §3 - * @param boolean $auto_reconnect try to reconnect on connection close + * @param integer $timeout max 1740 seconds - recommended by rfc2177 §3. Should not be lower than the servers "* OK Still here" message interval + * @param boolean $auto_reconnect try to reconnect on connection close (@deprecated is no longer required) * * @throws ConnectionFailedException * @throws Exceptions\InvalidMessageDateException @@ -364,25 +364,24 @@ public function unsubscribe(): bool { * @throws Exceptions\MessageNotFoundException * @throws Exceptions\NotSupportedCapabilityException */ - public function idle(callable $callback, int $timeout = 1200, bool $auto_reconnect = false) { - $this->client->getConnection()->setConnectionTimeout($timeout); - - $this->client->reconnect(); + public function idle(callable $callback, int $timeout = 300, bool $auto_reconnect = false) { + $this->client->setTimeout($timeout); if (!in_array("IDLE", $this->client->getConnection()->getCapabilities())) { throw new NotSupportedCapabilityException("IMAP server does not support IDLE"); } $this->client->openFolder($this->path, true); $connection = $this->client->getConnection(); + $connection->idle(); $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - $connection->idle(); while (true) { try { $line = $connection->nextLine(); + if (($pos = strpos($line, "EXISTS")) !== false) { - $msgn = (int) substr($line, 2, $pos -2); $connection->done(); + $msgn = (int) substr($line, 2, $pos -2); $this->client->openFolder($this->path, true); $message = $this->query()->getMessageByMsgn($msgn); @@ -391,20 +390,26 @@ public function idle(callable $callback, int $timeout = 1200, bool $auto_reconne $event = $this->getEvent("message", "new"); $event::dispatch($message); - + $connection->idle(); + } elseif (strpos($line, "OK") === false) { + $connection->done(); $connection->idle(); } }catch (Exceptions\RuntimeException $e) { + if(strpos($e->getMessage(), "empty response") >= 0 && $connection->connected()) { + $connection->done(); + $connection->idle(); + continue; + } if(strpos($e->getMessage(), "connection closed") === false) { throw $e; } - if ($auto_reconnect === true) { - $this->client->reconnect(); - $this->client->openFolder($this->path, true); - $connection = $this->client->getConnection(); - $connection->idle(); - } + $this->client->reconnect(); + $this->client->openFolder($this->path, true); + + $connection = $this->client->getConnection(); + $connection->idle(); } } } From 647d71ae2c58f89153585ab849b84a7affea3456 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 21:00:17 +0200 Subject: [PATCH 300/600] Changelog updated --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5956596f..a8cfe9ac 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,29 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - PHP dependency updated to support php v8.0 #212 #214 (thanks @freescout-helpdesk) - Method return and argument types added +- Imap `DONE` method refactored +- UID cache loop fixed +- `HasEvent::getEvent` return value set to mixed to allow multiple event types +- Protocol line reader changed to `fread` (stream_context timeout issue fixed) +- Issue setting the client timeout fixed +- IMAP Connection debugging improved +- `Folder::idle()` method reworked and several issues fixed #170 #229 #237 #249 #258 +- Datetime conversion rules extended #189 #173 ### Added - NaN ### Affected Classes -- NaN +- [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) +- [ImapProtocol::class](src/Connection/Protocols/ImapProtocol.php) +- [HasEvents::class](src/Traits/HasEvents.php) ### Breaking changes -- No longer supports php >=5.5.9 but instead requires at least php v7.0.0 +- No longer supports php >=5.5.9 but instead requires at least php v7.0.0. +- `HasEvent::getEvent` returns a mixed result. Either an `Event` or a class string representing the event class. +- The error message, if the connection fails to read the next line, is now `empty response` instead of `failed to read - connection closed?`. +- The `$auto_reconnect` used with `Folder::indle()` is deprecated and doesn't serve any purpose anymore. ## [3.2.0] - 2022-03-07 From cf8a4f6dcb22dec92b3030d82cbaff6d942ba850 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 19 Aug 2022 21:02:06 +0200 Subject: [PATCH 301/600] Release information added --- CHANGELOG.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8cfe9ac..28fa5083 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + + +## [4.0.0] - 2022-08-19 +### Fixed - PHP dependency updated to support php v8.0 #212 #214 (thanks @freescout-helpdesk) - Method return and argument types added - Imap `DONE` method refactored @@ -17,9 +31,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Folder::idle()` method reworked and several issues fixed #170 #229 #237 #249 #258 - Datetime conversion rules extended #189 #173 -### Added -- NaN - ### Affected Classes - [Client::class](src/Client.php) - [Folder::class](src/Folder.php) From 33ee4d2eb8bc1645489c0715c0412c42746c90ac Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 21 Aug 2022 11:20:57 +0200 Subject: [PATCH 302/600] Type casting added to several ImapProtocol return values #261 --- CHANGELOG.md | 2 +- src/Connection/Protocols/ImapProtocol.php | 16 ++++++++-------- src/Connection/Protocols/LegacyProtocol.php | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fa5083..b9307a41 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Type casting added to several ImapProtocol return values #261 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 9f592b7c..c455aef0 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -891,7 +891,7 @@ public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); - return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + return (bool)$this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** @@ -943,7 +943,7 @@ public function ID($ids = null) { * @throws RuntimeException */ public function createFolder(string $folder): bool { - return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); + return (bool)$this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); } /** @@ -955,7 +955,7 @@ public function createFolder(string $folder): bool { * @throws RuntimeException */ public function renameFolder(string $old, string $new): bool { - return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); + return (bool)$this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); } /** @@ -966,7 +966,7 @@ public function renameFolder(string $old, string $new): bool { * @throws RuntimeException */ public function deleteFolder(string $folder): bool { - return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); + return (bool)$this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); } /** @@ -977,7 +977,7 @@ public function deleteFolder(string $folder): bool { * @throws RuntimeException */ public function subscribeFolder(string $folder): bool { - return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); + return (bool)$this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); } /** @@ -988,7 +988,7 @@ public function subscribeFolder(string $folder): bool { * @throws RuntimeException */ public function unsubscribeFolder(string $folder): bool { - return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); + return (bool)$this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); } /** @@ -998,7 +998,7 @@ public function unsubscribeFolder(string $folder): bool { * @throws RuntimeException */ public function expunge(): bool { - return $this->requestAndResponse('EXPUNGE'); + return (bool)$this->requestAndResponse('EXPUNGE'); } /** @@ -1008,7 +1008,7 @@ public function expunge(): bool { * @throws RuntimeException */ public function noop(): bool { - return $this->requestAndResponse('NOOP'); + return (bool)$this->requestAndResponse('NOOP'); } /** diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index df960e06..a2a8d964 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -412,7 +412,7 @@ public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U */ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { foreach($messages as $msg) { - if ($this->copyMessage($folder, $msg, null, $uid) == false) { + if (!$this->copyMessage($folder, $msg, null, $uid)) { return false; } } @@ -421,7 +421,7 @@ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::S } /** - * Move a message set from current folder to an other folder + * Move a message set from current folder to another folder * @param string $folder destination folder * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the @@ -444,7 +444,7 @@ public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U */ public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { foreach($messages as $msg) { - if ($this->moveMessage($folder, $msg, null, $uid) == false) { + if (!$this->moveMessage($folder, $msg, null, $uid)) { return false; } } From fbc5b73dcb80e558cbc843470c83f64471a3c016 Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 21 Aug 2022 11:26:12 +0200 Subject: [PATCH 303/600] Class reference added --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9307a41..991f2337 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Affected Classes -- NaN +- [ImapProtocol::class](src/Connection/Protocols/ImapProtocol.php) ### Breaking changes - NaN From 05ab4017327387f4db2a704c638a4a9531f48c5b Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 13:47:56 +0200 Subject: [PATCH 304/600] Remove IMAP::OP_READONLY flag from imap_reopen if POP3 or NNTP protocol is selected #135 --- src/Connection/Protocols/LegacyProtocol.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index a2a8d964..4b00de65 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -181,7 +181,15 @@ public function getCapabilities(): array { * @throws RuntimeException */ public function selectFolder(string $folder = 'INBOX') { - \imap_reopen($this->stream, $folder, IMAP::OP_READONLY, 3); + $flags = IMAP::OP_READONLY; + if (in_array($this->protocol, ["pop3", "nntp"])) { + $flags = IMAP::NIL; + } + if ($this->stream === false) { + throw new RuntimeException("failed to reopen stream."); + } + + \imap_reopen($this->stream, $folder, $flags, 3); $this->uid_cache = null; return $this->examineFolder($folder); } From b0740373511350252b32d3e81996e4f0e6453828 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:22:01 +0200 Subject: [PATCH 305/600] Simplify if statements --- src/Query/Query.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 2607c389..a7556de2 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -120,10 +120,8 @@ protected function boot() { * @return string */ protected function parse_value($value): string { - switch (true) { - case $value instanceof Carbon: - $value = $value->format($this->date_format); - break; + if ($value instanceof Carbon) { + $value = $value->format($this->date_format); } return (string)$value; @@ -189,7 +187,7 @@ protected function search(): Collection { try { $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence); - return $available_messages !== false ? new Collection($available_messages) : new Collection(); + return new Collection($available_messages); } catch (RuntimeException $e) { throw new GetMessagesFailedException("failed to fetch messages", 0, $e); } catch (ConnectionFailedException $e) { From c1ac7826d5ae4182bc65ec4ab3cd26b1460e4801 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:23:52 +0200 Subject: [PATCH 306/600] Check if the Protocol supports the fetch method if extensions are present --- src/Query/Query.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index a7556de2..e50e510c 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -219,9 +219,9 @@ protected function fetch(Collection $available_messages): array { } $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); - $extensions = []; - if (empty($this->getExtensions()) === false) { - $extensions = $this->client->getConnection()->fetch($this->getExtensions(), $uids, null, $this->sequence); + $extensions = $this->getExtensions(); + if (empty($extensions) === false && method_exists($this->client->getConnection(), "fetch")) { + $extensions = $this->client->getConnection()->fetch($extensions, $uids, null, $this->sequence); } $flags = $this->client->getConnection()->flags($uids, $this->sequence); $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence); From 4c832ce2ad197610be4063ab633aaf2e2f1e6dd1 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:25:39 +0200 Subject: [PATCH 307/600] Simplify the contains method --- src/Attribute.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Attribute.php b/src/Attribute.php index 2f3aab0f..627cc315 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -170,12 +170,7 @@ public function merge(array $values, bool $strict = false): Attribute { * @return bool */ public function contains($value): bool { - foreach ($this->values as $v) { - if ($v === $value) { - return true; - } - } - return false; + return in_array($value, $this->values, true); } /** From 3ea650c049ceb0181d5dc7787d4b3f2434708c22 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:26:38 +0200 Subject: [PATCH 308/600] Check if the events for a given section exist - else return an empty array --- src/Client.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index bda9abac..3721f8e7 100755 --- a/src/Client.php +++ b/src/Client.php @@ -655,7 +655,10 @@ public function getDefaultMessageMask(): string { * @return array */ public function getDefaultEvents($section): array { - return $this->events[$section]; + if (isset($this->events[$section])) { + return is_array($this->events[$section]) ? $this->events[$section] : []; + } + return []; } /** From a064414491c642cea3256cbed7b77d2738655a74 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:27:42 +0200 Subject: [PATCH 309/600] Check if a requested account exist before returning or merging the account config --- src/ClientManager.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ClientManager.php b/src/ClientManager.php index 72fe4304..61557ef2 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -105,8 +105,8 @@ public static function get(string $key, $default = null) { public function account(string $name = null): Client { $name = $name ?: $this->getDefaultAccount(); - // If the connection has not been resolved yet we will resolve it now as all - // of the connections are resolved when they are actually needed so we do + // If the connection has not been resolved we will resolve it now as all + // the connections are resolved when they are actually needed, so we do // not make any unnecessary connection to the various queue end-points. if (!isset($this->accounts[$name])) { $this->accounts[$name] = $this->resolve($name); @@ -139,7 +139,7 @@ protected function getClientConfig($name): array { return ['driver' => 'null']; } - return self::$config["accounts"][$name]; + return is_array(self::$config["accounts"][$name]) ? self::$config["accounts"][$name] : []; } /** @@ -187,7 +187,7 @@ public function setConfig($config): ClientManager { if(is_array($config)){ if(isset($config['default'])){ - if(isset($config['accounts']) && $config['default'] != false){ + if(isset($config['accounts']) && $config['default']){ $default_config = $vendor_config['accounts']['default']; if(isset($config['accounts'][$config['default']])){ From d56efbc31466fa14251a28dcd275b7d313ac9db6 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:28:44 +0200 Subject: [PATCH 310/600] Check the response from examineFolder to check if any data was returned --- src/Folder.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Folder.php b/src/Folder.php index a904b9a9..50a2b0a9 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -433,7 +433,8 @@ public function getStatus(): array { * @throws Exceptions\RuntimeException */ public function examine(): array { - return $this->client->getConnection()->examineFolder($this->path); + $result = $this->client->getConnection()->examineFolder($this->path); + return is_array($result) ? $result : []; } /** From 33fdfd9db208a1f43e6ea6d62d8a0dfb49280538 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:30:07 +0200 Subject: [PATCH 311/600] Condition simplified --- src/Header.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Header.php b/src/Header.php index 3deb1de4..e742fccd 100644 --- a/src/Header.php +++ b/src/Header.php @@ -132,7 +132,7 @@ public function set(string $name, $value, bool $strict = false) { $this->attributes[$name]->add($value, true); } else { if (isset($this->attributes[$name])) { - if (is_array($this->attributes[$name]) == false) { + if (!is_array($this->attributes[$name])) { $this->attributes[$name] = [$this->attributes[$name], $value]; } else { $this->attributes[$name][] = $value; @@ -141,7 +141,7 @@ public function set(string $name, $value, bool $strict = false) { $this->attributes[$name] = $value; } } - } elseif ($this->attributize == false) { + } elseif (!$this->attributize) { $this->attributes[$name] = $value; } else { $this->attributes[$name] = new Attribute($name, $value); From 174f88a66714b1ee9979145e05f4fb8aceed15f0 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:31:02 +0200 Subject: [PATCH 312/600] Response checks added to prevent wrong types being returned --- src/Header.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Header.php b/src/Header.php index e742fccd..de653902 100644 --- a/src/Header.php +++ b/src/Header.php @@ -336,7 +336,8 @@ public function rfc822_parse_headers($raw_headers) { */ public function mime_header_decode(string $text): array { if (extension_loaded('imap')) { - return \imap_mime_header_decode($text); + $result = \imap_mime_header_decode($text); + return is_array($result) ? $result : []; } $charset = $this->getEncoding($text); return [(object)[ @@ -425,7 +426,8 @@ public function getEncoding($structure): string { } elseif (property_exists($structure, 'charset')) { return EncodingAliases::get($structure->charset, $this->fallback_encoding); } elseif (is_string($structure) === true) { - return mb_detect_encoding($structure); + $result = mb_detect_encoding($structure); + return $result === false ? $this->fallback_encoding : $result; } return $this->fallback_encoding; From 2c8ce83db3d8b2212a9d2de51546957ae15c334d Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:32:34 +0200 Subject: [PATCH 313/600] Search response type check added #266 --- src/Connection/Protocols/ImapProtocol.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index c455aef0..bf030e97 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1069,9 +1069,7 @@ public function done(): bool { public function search(array $params, $uid = IMAP::ST_UID): array { $command = $this->buildUIDCommand("SEARCH", $uid); $response = $this->requestAndResponse($command, $params); - if (!$response) { - return $response; - } + if (!$response) return []; foreach ($response as $ids) { if ($ids[0] == 'SEARCH') { From 71c5060f42c1a39563b7d8d7773dec7d0201db1e Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:34:19 +0200 Subject: [PATCH 314/600] Additional type casts added --- src/Connection/Protocols/ImapProtocol.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index bf030e97..ec306f96 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -742,7 +742,7 @@ public function getMessageNumber(string $id): int { $ids = $this->getUid(); foreach ($ids as $k => $v) { if ($v == $id) { - return $k; + return (int)$k; } } @@ -834,7 +834,7 @@ public function appendMessage(string $folder, string $message, $flags = null, $d } $tokens[] = $this->escapeString($message); - return $this->requestAndResponse('APPEND', $tokens, true); + return (bool) $this->requestAndResponse('APPEND', $tokens, true); } /** @@ -852,7 +852,7 @@ public function appendMessage(string $folder, string $message, $flags = null, $d public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("COPY", $uid); - return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + return (bool)$this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** From 03919d1a087997d734c6ae65f496c130a9e564cb Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:35:19 +0200 Subject: [PATCH 315/600] Response checks added to prevent methods from returning the wrong type --- src/Connection/Protocols/ImapProtocol.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ec306f96..90fc8dd0 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -667,7 +667,8 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { * @throws RuntimeException */ public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { - return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); + $result = $this->fetch(["$rfc.TEXT"], $uids, null, $uid); + return is_array($result) ? $result : []; } /** @@ -681,7 +682,8 @@ public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): arr * @throws RuntimeException */ public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array{ - return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); + $result = $this->fetch(["$rfc.HEADER"], $uids, null, $uid); + return $result === "" ? [] : $result; } /** @@ -694,7 +696,8 @@ public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): arr * @throws RuntimeException */ public function flags($uids, $uid = IMAP::ST_UID): array { - return $this->fetch(["FLAGS"], $uids, null, $uid); + $result = $this->fetch(["FLAGS"], $uids, null, $uid); + return is_array($result) ? $result : []; } /** @@ -1019,7 +1022,8 @@ public function noop(): bool { * @throws RuntimeException */ public function getQuota($username): array { - return $this->requestAndResponse("GETQUOTA", ['"#user/'.$username.'"']); + $result = $this->requestAndResponse("GETQUOTA", ['"#user/'.$username.'"']); + return is_array($result) ? $result : []; } /** @@ -1030,7 +1034,8 @@ public function getQuota($username): array { * @throws RuntimeException */ public function getQuotaRoot(string $quota_root = 'INBOX'): array { - return $this->requestAndResponse("QUOTA", [$quota_root]); + $result = $this->requestAndResponse("QUOTA", [$quota_root]); + return is_array($result) ? $result : []; } /** From b913ceb1a13205320a87510471dccd7ca19bc2b7 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:36:09 +0200 Subject: [PATCH 316/600] ImapProtocol::assumedNextLine method simplified --- src/Connection/Protocols/ImapProtocol.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 90fc8dd0..ba3fa297 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -123,8 +123,7 @@ public function nextLine(): string { * @throws RuntimeException */ protected function assumedNextLine(string $start): bool { - $line = $this->nextLine(); - return strpos($line, $start) === 0; + return strpos($this->nextLine(), $start) === 0; } /** From d570b344a939a54149d6cd2095b5421f97b2ecc6 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:36:47 +0200 Subject: [PATCH 317/600] Status flag type casts added --- src/Message.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Message.php b/src/Message.php index 0bfc1b34..3b157ff3 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1028,7 +1028,7 @@ public function setFlag($flag): bool { $event = $this->getEvent("flag", "new"); $event::dispatch($this, $flag); - return $status; + return (bool)$status; } /** @@ -1056,7 +1056,7 @@ public function unsetFlag($flag): bool { $event = $this->getEvent("flag", "deleted"); $event::dispatch($this, $flag); - return $status; + return (bool)$status; } /** From 7464e3439bf84c5d0fef2717644112c7d1fa7941 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:37:39 +0200 Subject: [PATCH 318/600] Unnecessary type cast removed --- src/Part.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Part.php b/src/Part.php index 4f0985a6..0c3c9560 100644 --- a/src/Part.php +++ b/src/Part.php @@ -217,7 +217,7 @@ private function findHeaders(): string { $this->header = new Header($headers); - return (string) $body; + return $body; } /** From 5aeb279c41e240aed25a62b7fa8c0f1da6b2c094 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:38:22 +0200 Subject: [PATCH 319/600] Typos fixed and docs extended --- src/Connection/Protocols/ImapProtocol.php | 2 +- src/Connection/Protocols/Protocol.php | 2 +- src/Connection/Protocols/ProtocolInterface.php | 4 ++-- src/Folder.php | 3 ++- src/Header.php | 2 +- src/Message.php | 1 + src/Part.php | 2 +- src/Query/Query.php | 1 + 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ba3fa297..ea3f5a3c 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -563,7 +563,7 @@ public function examineFolder(string $folder = 'INBOX') { * * @return string|array if only one item of one message is fetched it's returned as string * if items of one message are fetched it's returned as (name => value) - * if one items of messages are fetched it's returned as (msgno => value) + * if one item of messages are fetched it's returned as (msgno => value) * if items of messages are fetched it's returned as (msgno => (name => value)) * @throws RuntimeException */ diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index c622e9ed..29d3b0a6 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -38,7 +38,7 @@ abstract class Protocol implements ProtocolInterface { protected $enable_uid_cache = true; /** - * @var false|resource + * @var false|\IMAP\Connection|resource */ public $stream = false; diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index a512afe8..97a2fe3d 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -90,7 +90,7 @@ public function getCapabilities(): array; * Change the current folder * * @param string $folder change to this folder - * @return bool|array see examineOrselect() + * @return bool|array see examineOrSelect() * @throws RuntimeException */ public function selectFolder(string $folder = 'INBOX'); @@ -224,7 +224,7 @@ public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID); /** - * Move a message set from current folder to an other folder + * Move a message set from current folder to another folder * @param string $folder destination folder * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the diff --git a/src/Folder.php b/src/Folder.php index 50a2b0a9..ef42fcb1 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -278,7 +278,7 @@ public function overview(string $sequence = null): array { */ public function appendMessage(string $message, array $options = null, $internal_date = null): bool { /** - * Check if $internal_date is parsed. If it is null it should not be set. Otherwise the message can't be stored. + * Check if $internal_date is parsed. If it is null it should not be set. Otherwise, the message can't be stored. * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ @@ -377,6 +377,7 @@ public function idle(callable $callback, int $timeout = 300, bool $auto_reconnec while (true) { try { + // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand $line = $connection->nextLine(); if (($pos = strpos($line, "EXISTS")) !== false) { diff --git a/src/Header.php b/src/Header.php index de653902..285d6ba4 100644 --- a/src/Header.php +++ b/src/Header.php @@ -347,7 +347,7 @@ public function mime_header_decode(string $text): array { } /** - * Check if a given pair of strings has ben decoded + * Check if a given pair of strings has been decoded * @param $encoded * @param $decoded * diff --git a/src/Message.php b/src/Message.php index 3b157ff3..4137db9a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -55,6 +55,7 @@ * @method Attribute getPriority() * @method Attribute getSubject() * @method Attribute getMessageId() + * @method Attribute getMessageNo() * @method Attribute getReferences() * @method Attribute getDate() * @method Attribute getFrom() diff --git a/src/Part.php b/src/Part.php index 0c3c9560..f29b6348 100644 --- a/src/Part.php +++ b/src/Part.php @@ -202,7 +202,7 @@ protected function parse(){ } /** - * Find all available headers and return the left over body segment + * Find all available headers and return the leftover body segment * * @return string * @throws InvalidMessageDateException diff --git a/src/Query/Query.php b/src/Query/Query.php index e50e510c..361a1cef 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -711,6 +711,7 @@ public function setClient(Client $client): Query { } /** + * Get the set fetch limit * @return int */ public function getLimit() { From d88eddb3e567f24a3630014d0de4457bce0526ab Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 15:40:43 +0200 Subject: [PATCH 320/600] Changelog updated --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 991f2337..aff8fd38 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Type casting added to several ImapProtocol return values #261 +- Remove IMAP::OP_READONLY flag from imap_reopen if POP3 or NNTP protocol is selected #135 (thanks @xianzhe18) +- Several statements optimized and redundant check removed +- Check if the Protocol supports the fetch method if extensions are present +- Typos fixed ### Added - NaN From f0570f313b142ab35bceaffcf2db6e42ff5e605a Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 16:54:43 +0200 Subject: [PATCH 321/600] Detect `NONEXISTENT` errors while selecting or examining a folder #266 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aff8fd38..b9e44c04 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Several statements optimized and redundant check removed - Check if the Protocol supports the fetch method if extensions are present - Typos fixed +- Detect `NONEXISTENT` errors while selecting or examining a folder #266 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ea3f5a3c..d1f54c8c 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -516,6 +516,8 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I case '[UIDNEXT': $result['uidnext'] = (int)$tokens[2]; break; + case '[NONEXISTENT': + throw new RuntimeException("folder doesnt exist"); default: // ignore break; From 483a041b52ad5059e6f88601147a4dea8ae17fda Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 17:43:36 +0200 Subject: [PATCH 322/600] Typo fixed --- src/Connection/Protocols/ImapProtocol.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index d1f54c8c..c6559738 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -516,8 +516,8 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I case '[UIDNEXT': $result['uidnext'] = (int)$tokens[2]; break; - case '[NONEXISTENT': - throw new RuntimeException("folder doesnt exist"); + case '[NONEXISTENT]': + throw new RuntimeException("folder doesn't exist"); default: // ignore break; From 78d847ecd023a7d66e93114afdb4c52f767278db Mon Sep 17 00:00:00 2001 From: Roger Braconier Date: Wed, 24 Aug 2022 19:36:16 +0200 Subject: [PATCH 323/600] Update PaginatedCollection.php (#267) return-value was not an array --- src/Support/PaginatedCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 36ec4c5a..292afd0a 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -44,7 +44,7 @@ public function paginate(int $per_page = 15, $page = null, string $page_name = ' $total = $this->total ?: $this->count(); - $results = !$prepaginated && $total ? $this->forPage($page, $per_page) : $this->all(); + $results = !$prepaginated && $total ? $this->forPage($page, $per_page)->toArray() : $this->all(); return $this->paginator($results, $total, $per_page, $page, [ 'path' => Paginator::resolveCurrentPath(), From 853866bc057a90bbf291936076bcfea6858b6b3b Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 19:41:47 +0200 Subject: [PATCH 324/600] Changelog updated --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e44c04..af83dfd0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Check if the Protocol supports the fetch method if extensions are present - Typos fixed - Detect `NONEXISTENT` errors while selecting or examining a folder #266 +- Missing type cast added to `PaginatedCollection::paginate` #267 (thanks @rogerb87) ### Added - NaN ### Affected Classes +- [Query::class](src/Query/Query.php) - [ImapProtocol::class](src/Connection/Protocols/ImapProtocol.php) +- [LegacyProtocol::class](src/Connection/Protocols/LegacyProtocol.php) +- [PaginatedCollection::class](src/Support/PaginatedCollection.php) ### Breaking changes - NaN From 64ef8f00557ad129238532edf3851576eec8237c Mon Sep 17 00:00:00 2001 From: sulgie-eitea Date: Wed, 24 Aug 2022 20:25:25 +0200 Subject: [PATCH 325/600] Fix multiline header unfolding (#250) According to rfc 822 3.1.1. Long Header Fields, the multiple-line "folding" is properly unfolded by "regarding CRLF immediately followed by a LWSP-char" --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 285d6ba4..df7bd48f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -242,7 +242,7 @@ public function rfc822_parse_headers($raw_headers) { $imap_headers[$key] = $values; } } - $lines = explode("\r\n", str_replace("\r\n\t", ' ', $raw_headers)); + $lines = explode("\r\n", preg_replace("/\r\n\s*/", ' ', $raw_headers)); $prev_header = null; foreach ($lines as $line) { if (substr($line, 0, 1) === "\n") { From 8a522375a3b6336d29c0145950a20bfe637a1639 Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Wed, 24 Aug 2022 20:28:29 +0200 Subject: [PATCH 326/600] Fix problem with illegal offset error. (#226) * Fix problem with illegal offset error. * Switched to more precise comparison method (type detection). --- src/Connection/Protocols/ImapProtocol.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index c6559738..f1de8820 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -600,7 +600,12 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { } else if ($tokens[2][0] == 'UID') { $uidKey = 1; } else { - $uidKey = array_search('UID', $tokens[2]) + 1; + $found = array_search('UID', $tokens[2]); + if ($found === false || $found === -1) { + continue; + } + + $uidKey = $found + 1; } } From 28b2b44ab91ac9d076011b1ace1191a30e810ccb Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 24 Aug 2022 20:30:33 +0200 Subject: [PATCH 327/600] Changelog updated --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af83dfd0..52153dcc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Typos fixed - Detect `NONEXISTENT` errors while selecting or examining a folder #266 - Missing type cast added to `PaginatedCollection::paginate` #267 (thanks @rogerb87) +- Fix multiline header unfolding #250 (thanks @sulgie-eitea) +- Fix problem with illegal offset error #226 (thanks @szymekjanaczek) ### Added - NaN From 969ec69d00954a20971c8b1730d30e59b9b40ac5 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 25 Aug 2022 00:03:18 +0200 Subject: [PATCH 328/600] Release information added --- CHANGELOG.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52153dcc..e3812947 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,29 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Affected Classes +- NaN + +### Breaking changes +- NaN + + +## [4.0.1] - 2022-08-25 +### Fixed - Type casting added to several ImapProtocol return values #261 - Remove IMAP::OP_READONLY flag from imap_reopen if POP3 or NNTP protocol is selected #135 (thanks @xianzhe18) -- Several statements optimized and redundant check removed +- Several statements optimized and redundant checks removed - Check if the Protocol supports the fetch method if extensions are present -- Typos fixed - Detect `NONEXISTENT` errors while selecting or examining a folder #266 - Missing type cast added to `PaginatedCollection::paginate` #267 (thanks @rogerb87) - Fix multiline header unfolding #250 (thanks @sulgie-eitea) - Fix problem with illegal offset error #226 (thanks @szymekjanaczek) - -### Added -- NaN +- Typos fixed ### Affected Classes - [Query::class](src/Query/Query.php) @@ -25,9 +36,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - [LegacyProtocol::class](src/Connection/Protocols/LegacyProtocol.php) - [PaginatedCollection::class](src/Support/PaginatedCollection.php) -### Breaking changes -- NaN - ## [4.0.0] - 2022-08-19 ### Fixed From 438e1d2acd077b5435692ef436bde4f32c53226c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20Br=C3=A4hne?= Date: Thu, 25 Aug 2022 12:27:01 +0200 Subject: [PATCH 329/600] fix #268 - regular expression (#269) --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index df7bd48f..391da44e 100644 --- a/src/Header.php +++ b/src/Header.php @@ -242,7 +242,7 @@ public function rfc822_parse_headers($raw_headers) { $imap_headers[$key] = $values; } } - $lines = explode("\r\n", preg_replace("/\r\n\s*/", ' ', $raw_headers)); + $lines = explode("\r\n", preg_replace("/\r\n\s/", ' ', $raw_headers)); $prev_header = null; foreach ($lines as $line) { if (substr($line, 0, 1) === "\n") { From 0314d9e86def57cb5342ca14c6a49b19981613de Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 25 Aug 2022 12:30:13 +0200 Subject: [PATCH 330/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3812947..2c119cb8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- RFC 822 3.1.1. long header fields regular expression fixed #268 #269 (thanks @) ### Added - NaN From cc3e72f8517b0bf169a91d9fe5831ebd5740874d Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 25 Aug 2022 12:30:40 +0200 Subject: [PATCH 331/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c119cb8..c6a1f04c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- RFC 822 3.1.1. long header fields regular expression fixed #268 #269 (thanks @) +- RFC 822 3.1.1. long header fields regular expression fixed #268 #269 (thanks @hbraehne) ### Added - NaN From d90f4cdfa9dd9697c82a5bf3daa3e3d106377070 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 26 Aug 2022 00:01:23 +0200 Subject: [PATCH 332/600] Release information added --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a1f04c..105e353b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- RFC 822 3.1.1. long header fields regular expression fixed #268 #269 (thanks @hbraehne) +- NaN ### Added - NaN @@ -18,6 +18,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [4.0.2] - 2022-08-26 +### Fixed +- RFC 822 3.1.1. long header fields regular expression fixed #268 #269 (thanks @hbraehne) + + ## [4.0.1] - 2022-08-25 ### Fixed - Type casting added to several ImapProtocol return values #261 From 999d713b8f41445e42a89a8cff2db459f1a1c57d Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Mon, 17 Oct 2022 04:51:10 +0200 Subject: [PATCH 333/600] Added possibility of loading Folder status (#298) * Added missing param. * Added getting folder with its status possibility. --- src/Client.php | 40 +++++++++++++++++++++++ src/Connection/Protocols/ImapProtocol.php | 5 ++- src/Folder.php | 14 ++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 3721f8e7..1799147d 100755 --- a/src/Client.php +++ b/src/Client.php @@ -495,6 +495,46 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu } } + /** + * Get folders list. + * If hierarchical order is set to true, it will make a tree of folders, otherwise it will return flat array. + * + * @param boolean $hierarchical + * @param string|null $parent_folder + * + * @return FolderCollection + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws Exceptions\RuntimeException + */ + public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null): FolderCollection { + $this->checkConnection(); + $folders = FolderCollection::make([]); + + $pattern = $parent_folder.($hierarchical ? '%' : '*'); + $items = $this->connection->folders('', $pattern); + + if(is_array($items)){ + foreach ($items as $folder_name => $item) { + $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); + + if ($hierarchical && $folder->hasChildren()) { + $pattern = $folder->full_name.$folder->delimiter.'%'; + + $children = $this->getFolders(true, $pattern); + $folder->setChildren($children); + } + + $folder->loadStatus(); + $folders->push($folder); + } + + return $folders; + }else{ + throw new FolderFetchingException("failed to fetch any folders"); + } + } + /** * Open a given folder. * @param string $folder_path diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index f1de8820..1a3c2ad2 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -508,7 +508,7 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I switch ($tokens[1]) { case 'EXISTS': case 'RECENT': - $result[strtolower($tokens[1])] = $tokens[0]; + $result[strtolower($tokens[1])] = (int)$tokens[0]; break; case '[UIDVALIDITY': $result['uidvalidity'] = (int)$tokens[2]; @@ -516,6 +516,9 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I case '[UIDNEXT': $result['uidnext'] = (int)$tokens[2]; break; + case '[UNSEEN': + $result['unseen'] = (int)$tokens[2]; + break; case '[NONEXISTENT]': throw new RuntimeException("folder doesn't exist"); default: diff --git a/src/Folder.php b/src/Folder.php index ef42fcb1..a93209a2 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -15,6 +15,7 @@ use Carbon\Carbon; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Query\WhereQuery; use Webklex\PHPIMAP\Support\FolderCollection; use Webklex\PHPIMAP\Traits\HasEvents; @@ -108,6 +109,9 @@ class Folder { */ public $referral; + /** @var array */ + public $status; + /** * Folder constructor. * @param Client $client @@ -426,6 +430,16 @@ public function getStatus(): array { return $this->examine(); } + /** + * @throws RuntimeException + * @throws ConnectionFailedException + */ + public function loadStatus(): Folder + { + $this->status = $this->getStatus(); + return $this; + } + /** * Examine the current folder * From 68eaf30968ee099d78255cc899454809735e38c8 Mon Sep 17 00:00:00 2001 From: Blear <723712241@qq.com> Date: Mon, 17 Oct 2022 10:56:01 +0800 Subject: [PATCH 334/600] Update ImapProtocol.php (#288) fix assumedNextTaggedLine bug --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 1a3c2ad2..e076f5c9 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -150,7 +150,7 @@ protected function nextTaggedLine(&$tag): string { */ protected function assumedNextTaggedLine(string $start, &$tag): bool { $line = $this->nextTaggedLine($tag); - return strpos($line, $start) >= 0; + return strpos($line, $start) !== false; } /** From 0ddfbdd849c8d7d53090268b1130d290792699b7 Mon Sep 17 00:00:00 2001 From: Jeff Bierschbach Date: Sun, 16 Oct 2022 22:00:00 -0500 Subject: [PATCH 335/600] Fix empty response error for blank lines (#274) * Fix empty response error for blank lines * better checking against false --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index e076f5c9..c4bf320d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -108,7 +108,7 @@ public function nextLine(): string { while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { $line .= $next_char; } - if ($line === "") { + if ($line === "" && $next_char === false) { throw new RuntimeException('empty response'); } if ($this->debug) echo "<< ".$line."\n"; From 79464cdb7109787c4413f58a27cd191eabb5418f Mon Sep 17 00:00:00 2001 From: latypoff Date: Mon, 17 Oct 2022 09:02:11 +0600 Subject: [PATCH 336/600] Update ImapProtocol.php (#233) This prevents from erase already parsed tokens. --- src/Connection/Protocols/ImapProtocol.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index c4bf320d..a8476b0d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -616,7 +616,6 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { continue; } - $data = ""; // if we only want one item we return that one directly if (count($items) == 1) { @@ -625,6 +624,7 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { } elseif ($uid && $tokens[2][2] == $items[0]) { $data = $tokens[2][3]; } else { + $expectedResponse = 0; // maybe the server send an other field we didn't wanted $count = count($tokens[2]); // we start with 2, because 0 was already checked @@ -633,8 +633,12 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { continue; } $data = $tokens[2][$i + 1]; + $expectedResponse = 1; break; } + if (!$expectedResponse) { + continue; + } } } else { $data = []; From 410af11bc8dc20bed8c07887572e4f1e1a7f267c Mon Sep 17 00:00:00 2001 From: latypoff Date: Mon, 17 Oct 2022 09:04:00 +0600 Subject: [PATCH 337/600] Update LegacyProtocol.php (#234) imap_reopen as imap_open requires folder argument to start with the bracket, ie address --- src/Connection/Protocols/LegacyProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 4b00de65..dd3d4898 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -189,7 +189,7 @@ public function selectFolder(string $folder = 'INBOX') { throw new RuntimeException("failed to reopen stream."); } - \imap_reopen($this->stream, $folder, $flags, 3); + \imap_reopen($this->stream, $this->getAddress().$folder, $flags, 3); $this->uid_cache = null; return $this->examineFolder($folder); } From 6e6ee1a27aee5007ba4a1d977c75a894b29c67ff Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 17 Oct 2022 05:05:03 +0200 Subject: [PATCH 338/600] Contributions added --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105e353b..6482fc19 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix assumedNextTaggedLine bug #288 (thanks @Blear) +- Fix empty response error for blank lines #274 (thanks @bierpub) +- Fix empty body #233 (thanks @latypoff) +- Fix imap_reopen folder argument #234 (thanks @latypoff) ### Added -- NaN +- Added possibility of loading a Folder status #298 (thanks @szymekjanaczek) ### Affected Classes - NaN From 2609e83f70e6e178c8d8855ddcde2104ebc5195f Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 17 Oct 2022 05:07:12 +0200 Subject: [PATCH 339/600] Discord link updated --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a02da9c..3aef3a4b 100755 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Official documentation: [php-imap.com](https://www.php-imap.com/) Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) -Discord: [discord.gg/jCcZWCSq][link-discord] +Discord: [discord.gg/rd4cN9h6][link-discord] ## Table of Contents - [Documentations](#documentations) @@ -118,7 +118,7 @@ echo 'your php code...'; ## Features & pull requests Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) -first, if you're planning to do bigger changes. Of course you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) +first, if you're planning to do bigger changes. Of course, you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) if you're just wishing a feature ;) @@ -164,4 +164,4 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [link-jetbrains]: https://www.jetbrains.com [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap -[link-discord]: https://discord.gg/jCcZWCSq +[link-discord]: https://discord.gg/rd4cN9h6 From 45843e1554cc280c738278b9e3b6af35a91f8b1f Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 18 Oct 2022 06:17:12 +0200 Subject: [PATCH 340/600] Release information added --- CHANGELOG.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6482fc19..3d8ffd70 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [4.1.0] - 2022-10-18 +### Fixed - Fix assumedNextTaggedLine bug #288 (thanks @Blear) - Fix empty response error for blank lines #274 (thanks @bierpub) - Fix empty body #233 (thanks @latypoff) @@ -14,12 +25,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Added possibility of loading a Folder status #298 (thanks @szymekjanaczek) -### Affected Classes -- NaN - -### Breaking changes -- NaN - ## [4.0.2] - 2022-08-26 ### Fixed From d059b6750dc85d26f08f26af347ff650e1578695 Mon Sep 17 00:00:00 2001 From: Piotr Woszczyk Date: Mon, 14 Nov 2022 09:49:50 +0100 Subject: [PATCH 341/600] Fix for extension recognition (#325) * Remove of deprecated dependency * Suggestion for symfony/mime and use as default guesser --- composer.json | 3 +++ src/Attachment.php | 13 +++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 2a62e223..65104480 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,9 @@ "require-dev": { "phpunit/phpunit": "~4.0" }, + "suggest": { + "symfony/mime": "Recomended for better extension support" + }, "autoload": { "psr-4": { "Webklex\\PHPIMAP\\": "src" diff --git a/src/Attachment.php b/src/Attachment.php index cff02269..d0ce0994 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -278,15 +278,20 @@ public function getMimeType(){ * @return string|null */ public function getExtension(){ + $guesser = "\Symfony\Component\Mime\MimeTypes"; + if (class_exists($guesser) !== false) { + /** @var Symfony\Component\Mime\MimeTypes $guesser */ + $extensions = $guesser::getDefault()->getExtensions($this->getMimeType()); + return $extensions[0] ?? null; + } + $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; if (class_exists($deprecated_guesser) !== false){ /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ return $deprecated_guesser::getInstance()->guess($this->getMimeType()); } - $guesser = "\Symfony\Component\Mime\MimeTypes"; - /** @var Symfony\Component\Mime\MimeTypes $guesser */ - $extensions = $guesser::getDefault()->getExtensions($this->getMimeType()); - return $extensions[0] ?? null; + + return null; } /** From ed017fb266f7638563d92e294704c06899810a45 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 14 Nov 2022 09:50:28 +0100 Subject: [PATCH 342/600] Merge detail added --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8ffd70..0a76a1c3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix for extension recognition #325 (thanks @pwoszczyk) ### Added - NaN From d80e4ae41aba5a4d57dea76ab72c738a5de1eca7 Mon Sep 17 00:00:00 2001 From: Shiv Charan Panjeta Date: Tue, 15 Nov 2022 20:05:32 +0530 Subject: [PATCH 343/600] PHP8 null check (#327) --- src/Structure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Structure.php b/src/Structure.php index 682d7bff..40ddc3a5 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -88,7 +88,7 @@ protected function parse(){ public function findContentType(){ $content_type = $this->header->get("content_type"); $content_type = (is_array($content_type)) ? implode(' ', $content_type) : $content_type; - if(stripos($content_type, 'multipart') === 0) { + if($content_type && stripos($content_type, 'multipart') === 0) { $this->type = IMAP::MESSAGE_TYPE_MULTIPART; }else{ $this->type = IMAP::MESSAGE_TYPE_TEXT; From 41ede1a37f0bc469c3e7044d27ca784aa6fde1f9 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 15 Nov 2022 15:36:35 +0100 Subject: [PATCH 344/600] Merge detail added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a76a1c3..e8b91ea2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Fix for extension recognition #325 (thanks @pwoszczyk) +- Missing null check added #327 (thanks @spanjeta) ### Added - NaN From 246640cbc3dd4107faa0800dc1283b65ab8a0c7f Mon Sep 17 00:00:00 2001 From: Daniel Castilla Date: Tue, 15 Nov 2022 15:40:53 +0100 Subject: [PATCH 345/600] Update ImapProtocol.php (#321) --- src/Connection/Protocols/ImapProtocol.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index a8476b0d..b910c622 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -169,6 +169,7 @@ protected function decodeLine(string $line): array { while (($pos = strpos($line, ' ')) !== false) { $token = substr($line, 0, $pos); if (!strlen($token)) { + $line = substr($line, $pos + 1); continue; } while ($token[0] == '(') { From 8363691d23b8fce41917e68783472361085f6f92 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 15 Nov 2022 15:42:21 +0100 Subject: [PATCH 346/600] Merge detail added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b91ea2..432c0cfd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Fix for extension recognition #325 (thanks @pwoszczyk) - Missing null check added #327 (thanks @spanjeta) +- Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) ### Added - NaN From 33c22e77bbd9cda7bc3298dc37d50c48a20dd9de Mon Sep 17 00:00:00 2001 From: Daniel Castilla Date: Tue, 15 Nov 2022 15:48:31 +0100 Subject: [PATCH 347/600] Fix error when creating folders with special chars (#319) * Fix error when creating folders with special chars * Update return types of getFolder methods to Folder|null --- src/Client.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Client.php b/src/Client.php index 1799147d..c4beecb9 100755 --- a/src/Client.php +++ b/src/Client.php @@ -411,7 +411,7 @@ public function disconnect(): Client { * @param string $folder_name * @param string|bool|null $delimiter * - * @return mixed + * @return Folder|null * @throws ConnectionFailedException * @throws FolderFetchingException * @throws Exceptions\RuntimeException @@ -434,7 +434,7 @@ public function getFolder(string $folder_name, $delimiter = null) { * Get a folder instance by a folder name * @param $folder_name * - * @return mixed + * @return Folder|null * @throws ConnectionFailedException * @throws FolderFetchingException * @throws Exceptions\RuntimeException @@ -447,7 +447,7 @@ public function getFolderByName($folder_name) { * Get a folder instance by a folder path * @param $folder_path * - * @return mixed + * @return Folder|null * @throws ConnectionFailedException * @throws FolderFetchingException * @throws Exceptions\RuntimeException @@ -570,7 +570,7 @@ public function createFolder(string $folder, bool $expunge = true): Folder { if($expunge) $this->expunge(); - $folder = $this->getFolder($folder); + $folder = $this->getFolderByPath($folder); if($status && $folder) { $event = $this->getEvent("folder", "new"); $event::dispatch($folder); From 13931819daead74572b4bb86a7b2778dcb441ed4 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 15 Nov 2022 15:49:12 +0100 Subject: [PATCH 348/600] Merge detail added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 432c0cfd..8d78fbf9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix for extension recognition #325 (thanks @pwoszczyk) - Missing null check added #327 (thanks @spanjeta) - Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) +- Fix error when creating folders with special chars #319 (thanks @thin-k-design) ### Added - NaN From 603447adc543b3fccb556b3771bafd231acdd339 Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Tue, 15 Nov 2022 15:51:46 +0100 Subject: [PATCH 349/600] FIX: Added possibility of loading Folder status #298 (#312) * Added missing param. * Added getting folder with its status possibility. * Fixed wrong method usage in recursion. --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index c4beecb9..5af56423 100755 --- a/src/Client.php +++ b/src/Client.php @@ -521,7 +521,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFolders(true, $pattern); + $children = $this->getFoldersWithStatus(true, $pattern); $folder->setChildren($children); } From 857fb1ff2ca9370a456af912c361301c841af1ba Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 15 Nov 2022 15:53:50 +0100 Subject: [PATCH 350/600] Merge detail added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d78fbf9..c900c23a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Missing null check added #327 (thanks @spanjeta) - Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) - Fix error when creating folders with special chars #319 (thanks @thin-k-design) +- `Client::getFoldersWithStatus()` recursive loading fixed #312 (thanks @szymekjanaczek) ### Added - NaN From f5ae00b5f25d26be18a60424d33941f3588cd623 Mon Sep 17 00:00:00 2001 From: rskrzypczak Date: Tue, 15 Nov 2022 15:55:16 +0100 Subject: [PATCH 351/600] Fix #306 Folder name encoding error in appendMessage() (#307) --- src/Folder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Folder.php b/src/Folder.php index a93209a2..01e59cc8 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -291,7 +291,7 @@ public function appendMessage(string $message, array $options = null, $internal_ $internal_date = $internal_date->format('d-M-Y H:i:s O'); } - return $this->client->getConnection()->appendMessage($this->full_name, $message, $options, $internal_date); + return $this->client->getConnection()->appendMessage($this->path, $message, $options, $internal_date); } /** From 1d82009fcab176cba4a9b7f5c159e2b2118c2325 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 15 Nov 2022 15:56:30 +0100 Subject: [PATCH 352/600] Merge detail added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c900c23a..c04c4191 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) - Fix error when creating folders with special chars #319 (thanks @thin-k-design) - `Client::getFoldersWithStatus()` recursive loading fixed #312 (thanks @szymekjanaczek) +- Fix Folder name encoding error in `Folder::appendMessage()` #306 #307 (thanks @@rskrzypczak) ### Added - NaN From eed8759dd5515bb42f55a0690084f644588c014a Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 16 Nov 2022 08:25:52 +0100 Subject: [PATCH 353/600] Release information added --- CHANGELOG.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c04c4191..ed688510 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN +- +### Added +- NaN + +### Breaking changes +- NaN + + +## [4.1.1] - 2022-11-16 +### Fixed - Fix for extension recognition #325 (thanks @pwoszczyk) - Missing null check added #327 (thanks @spanjeta) - Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) @@ -13,12 +24,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Client::getFoldersWithStatus()` recursive loading fixed #312 (thanks @szymekjanaczek) - Fix Folder name encoding error in `Folder::appendMessage()` #306 #307 (thanks @@rskrzypczak) -### Added -- NaN - -### Breaking changes -- NaN - ## [4.1.0] - 2022-10-18 ### Fixed From 5415d520d235ce8585e7f3c03c2b937bd8662a3e Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 16 Nov 2022 08:27:09 +0100 Subject: [PATCH 354/600] typo fixed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed688510..cfc48e53 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) - Fix error when creating folders with special chars #319 (thanks @thin-k-design) - `Client::getFoldersWithStatus()` recursive loading fixed #312 (thanks @szymekjanaczek) -- Fix Folder name encoding error in `Folder::appendMessage()` #306 #307 (thanks @@rskrzypczak) +- Fix Folder name encoding error in `Folder::appendMessage()` #306 #307 (thanks @rskrzypczak) ## [4.1.0] - 2022-10-18 From 8e957091a4688ce042c70a85b9625d4afa54ba97 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 28 Nov 2022 04:25:22 +0100 Subject: [PATCH 355/600] Attachment ID can return an empty value #318 --- CHANGELOG.md | 4 ++-- src/Attachment.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc48e53..febec329 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN -- +- Attachment ID can return an empty value #318 + ### Added - NaN diff --git a/src/Attachment.php b/src/Attachment.php index d0ce0994..a7c74969 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -213,6 +213,8 @@ protected function fetch() { if (($id = $this->part->id) !== null) { $this->id = str_replace(['<', '>'], '', $id); + }else{ + $this->id = hash("sha256", (string)microtime(true)); } $this->size = $this->part->bytes; From 5748cf1bac188ad45cd9574fe23114d19a5280c7 Mon Sep 17 00:00:00 2001 From: amorebietakoUdala <30795155+amorebietakoUdala@users.noreply.github.com> Date: Wed, 14 Dec 2022 16:43:03 +0100 Subject: [PATCH 356/600] - Fixed an issue with dates. (#345) - I was receiving the following exception with dates: - EXCEPTION: Invalid message date. ID:VI1PR07MB4766210AE35AE91886D2D766E61F9@VI1PR07MB4766.eurprd07.prod.outlook.com Date:Sat, 10, Dec 2022 09:35:19 +0100/Sat, 10, Dec 2022 09:35:19 +0100 - I added a fix for those date to be parsed correctly. Now it works fine. --- src/Header.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Header.php b/src/Header.php index 391da44e..b9bcc057 100644 --- a/src/Header.php +++ b/src/Header.php @@ -728,6 +728,8 @@ private function parseDate($header) { case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; break; + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: + $date = str_replace(',', '', $date); case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: From 94bf93ae8868ac1e073cfbaef377f0ca1acac2bc Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 14 Dec 2022 16:45:15 +0100 Subject: [PATCH 357/600] Release information added --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index febec329..48b41c47 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Attachment ID can return an empty value #318 +- NaN ### Added - NaN @@ -15,6 +15,12 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [4.1.2] - 2022-12-14 +### Fixed +- Attachment ID can return an empty value #318 +- Additional message date format added #345 (thanks @amorebietakoUdala) + + ## [4.1.1] - 2022-11-16 ### Fixed - Fix for extension recognition #325 (thanks @pwoszczyk) From a3fcead0a49b2163153300a8d5f239873b4fc096 Mon Sep 17 00:00:00 2001 From: Szymon Janaczek Date: Wed, 14 Dec 2022 16:51:06 +0100 Subject: [PATCH 358/600] Improve IMAP response handling (#277) * Updated new Imap Protocol to return RAW server responses instead of booleans. * Removed ext-imap. --- src/Connection/Protocols/ImapProtocol.php | 312 ++++++++++++------ .../Protocols/ProtocolInterface.php | 85 +++-- src/Exceptions/ImapBadRequestException.php | 24 ++ src/Exceptions/ImapServerErrorException.php | 24 ++ 4 files changed, 307 insertions(+), 138 deletions(-) create mode 100644 src/Exceptions/ImapBadRequestException.php create mode 100644 src/Exceptions/ImapServerErrorException.php diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b910c622..e81c96e9 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -15,6 +15,8 @@ use Exception; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\RuntimeException; @@ -45,7 +47,9 @@ public function __construct(bool $cert_validation = true, $encryption = false) { } /** - * Public destructor + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException */ public function __destruct() { $this->logout(); @@ -87,6 +91,8 @@ public function connect(string $host, $port = null) { * Enable tls on the current connection * * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ protected function enableStartTls(){ @@ -123,7 +129,7 @@ public function nextLine(): string { * @throws RuntimeException */ protected function assumedNextLine(string $start): bool { - return strpos($this->nextLine(), $start) === 0; + return str_starts_with($this->nextLine(), $start); } /** @@ -260,13 +266,17 @@ public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse /** * Read all lines of response until given tag is found - * @param string $tag request tag + * + * @param string $tag request tag * @param bool $dontParse if true every line is returned unparsed instead of the decoded tokens * - * @return array|bool|null tokens if success, false if error, null if bad request + * @return array + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function readResponse(string $tag, bool $dontParse = false) { + public function readResponse(string $tag, bool $dontParse = false): array { $lines = []; $tokens = null; // define $tokens variable before first use do { @@ -281,12 +291,12 @@ public function readResponse(string $tag, bool $dontParse = false) { // last line has response code if ($tokens[0] == 'OK') { - return $lines ? $lines : true; + return $lines ?: [true]; } elseif ($tokens[0] == 'NO') { - return false; + throw new ImapServerErrorException(); } - return null; + throw new ImapBadRequestException(); } /** @@ -335,14 +345,18 @@ public function write(string $data) { /** * Send a request and get response at once + * * @param string $command - * @param array $tokens parameters as in sendRequest() + * @param array $tokens parameters as in sendRequest() * @param bool $dontParse if true unparsed lines are returned instead of tokens * - * @return array|bool|null response as in readResponse() + * @return array response as in readResponse() + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false) { + public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false): array { $this->sendRequest($command, $tokens, $tag); return $this->readResponse($tag, $dontParse); @@ -357,7 +371,7 @@ public function requestAndResponse(string $command, array $tokens = [], bool $do */ public function escapeString($string) { if (func_num_args() < 2) { - if (strpos($string, "\n") !== false) { + if (str_contains($string, "\n")) { return ['{' . strlen($string) . '}', $string]; } else { return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $string) . '"'; @@ -390,16 +404,21 @@ public function escapeList(array $list): string { /** * Login to a new session. - * @param string $user username + * + * @param string $user username * @param string $password password * - * @return bool|mixed + * @return array * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException */ - public function login(string $user, string $password): bool { + public function login(string $user, string $password): array { try { - $response = $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); - return $response !== null && $response !== false; + $command = 'LOGIN'; + $params = $this->escapeString($user, $password); + + return $this->requestAndResponse($command, $params, true); } catch (RuntimeException $e) { throw new AuthFailedException("failed to authenticate", 0, $e); } @@ -444,20 +463,24 @@ public function authenticate(string $user, string $token): bool { /** * Logout of imap server * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException */ - public function logout(): bool { - $result = false; - if ($this->stream) { - try { - $result = $this->requestAndResponse('LOGOUT', [], true); - } catch (Exception $e) {} - fclose($this->stream); - $this->stream = null; - $this->uid_cache = null; + public function logout(): array { + if (!$this->stream) { + throw new RuntimeException('not connected'); } - return $result !== false; + $result = $this->requestAndResponse('LOGOUT', [], true); + + fclose($this->stream); + $this->stream = null; + $this->uid_cache = null; + + return $result; } /** @@ -473,6 +496,9 @@ public function connected(): bool { * Get an array of available capabilities * * @return array list of capabilities + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ public function getCapabilities(): array { @@ -538,7 +564,7 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I * Change the current folder * @param string $folder change to this folder * - * @return bool|array see examineOrselect() + * @return bool|array see examineOrSelect() * @throws RuntimeException */ public function selectFolder(string $folder = 'INBOX') { @@ -551,7 +577,7 @@ public function selectFolder(string $folder = 'INBOX') { * Examine a given folder * @param string $folder examine this folder * - * @return bool|array see examineOrselect() + * @return bool|array see examineOrSelect() * @throws RuntimeException */ public function examineFolder(string $folder = 'INBOX') { @@ -768,16 +794,21 @@ public function getMessageNumber(string $id): int { /** * Get a list of available folders + * * @param string $reference mailbox reference for list - * @param string $folder mailbox name match with wildcards + * @param string $folder mailbox name match with wildcards * * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ public function folders(string $reference = '', string $folder = '*'): array { $result = []; $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); - if (!$list || $list === true) { + + if ($list[0] === true) { return $result; } @@ -793,20 +824,25 @@ public function folders(string $reference = '', string $folder = '*'): array { /** * Manage flags - * @param array $flags flags to set, add or remove - see $mode - * @param int $from message for items or start message if $to !== null - * @param int|null $to if null only one message ($from) is fetched, else it's the + * + * @param array $flags flags to set, add or remove - see $mode + * @param int $from message for items or start message if $to !== null + * @param null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given - * @param bool $silent if false the return values are the new flags for the wanted messages - * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use - * message numbers instead. - * @param null|string $item command used to store a flag + * @param null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given + * @param bool $silent if false the return values are the new flags for the wanted messages + * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. + * @param null $item command used to store a flag * - * @return bool|array new flags if $silent is false, else true or false depending on success + * @return array new flags if $silent is false, else true or false depending on success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function store(array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null) { + public function store( + array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null + ): array { $flags = $this->escapeList($flags); $set = $this->buildSet($from, $to); @@ -816,7 +852,7 @@ public function store(array $flags, int $from, $to = null, $mode = null, bool $s $response = $this->requestAndResponse($command, [$set, $item, $flags], $silent); if ($silent) { - return (bool)$response; + return $response; } $result = []; @@ -832,15 +868,19 @@ public function store(array $flags, int $from, $to = null, $mode = null, bool $s /** * Append a new message to given folder - * @param string $folder name of target folder + * + * @param string $folder name of target folder * @param string $message full message content - * @param array|null $flags flags for new message - * @param string $date date for new message + * @param null $flags flags for new message + * @param null $date date for new message * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function appendMessage(string $folder, string $message, $flags = null, $date = null): bool { + public function appendMessage(string $folder, string $message, $flags = null, $date = null): array { $tokens = []; $tokens[] = $this->escapeString($folder); if ($flags !== null) { @@ -851,39 +891,47 @@ public function appendMessage(string $folder, string $message, $flags = null, $d } $tokens[] = $this->escapeString($message); - return (bool) $this->requestAndResponse('APPEND', $tokens, true); + return $this->requestAndResponse('APPEND', $tokens, true); } /** - * Copy a message set from current folder to an other folder - * @param string $folder destination folder + * Copy a message set from current folder to another folder + * + * @param string $folder destination folder * @param $from - * @param int|null $to if null only one message ($from) is fetched, else it's the - * last message, INF means last message available - * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use - * message numbers instead. + * @param null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message available + * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { + public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("COPY", $uid); - return (bool)$this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + + return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** * Copy multiple messages to the target folder * * @param array $messages List of message identifiers - * @param string $folder Destination folder - * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use - * message numbers instead. - * @return array|bool Tokens if operation successful, false if an error occurred + * @param string $folder Destination folder + * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. + * + * @return array Tokens if operation successful, false if an error occurred * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { + public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array { $command = $this->buildUIDCommand("COPY", $uid); $set = implode(',', $messages); @@ -893,35 +941,43 @@ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::S } /** - * Move a message set from current folder to an other folder - * @param string $folder destination folder + * Move a message set from current folder to another folder + * + * @param string $folder destination folder * @param $from - * @param int|null $to if null only one message ($from) is fetched, else it's the + * @param null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use - * message numbers instead. + * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { + public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); - return (bool)$this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); } /** * Move multiple messages to the target folder + * * @param array $messages List of message identifiers - * @param string $folder Destination folder - * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use - * message numbers instead. + * @param string $folder Destination folder + * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. + * + * @return array success * - * @return array|bool Tokens if operation successful, false if an error occurred + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { + public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array { $command = $this->buildUIDCommand("MOVE", $uid); $set = implode(',', $messages); @@ -934,12 +990,14 @@ public function moveManyMessages(array $messages, string $folder, $uid = IMAP::S * Exchange identification information * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 * - * @param null $ids - * @return array|bool|void|null + * @param array|null $ids + * @return array * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function ID($ids = null) { + public function ID($ids = null): array { $token = "NIL"; if (is_array($ids) && !empty($ids)) { $token = "("; @@ -954,102 +1012,132 @@ public function ID($ids = null) { /** * Create a new folder (and parent folders if needed) + * * @param string $folder folder name + * @return array success * - * @return bool success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function createFolder(string $folder): bool { - return (bool)$this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); + public function createFolder(string $folder): array { + return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); } /** * Rename an existing folder + * * @param string $old old name * @param string $new new name * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function renameFolder(string $old, string $new): bool { - return (bool)$this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); + public function renameFolder(string $old, string $new): array { + return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); } /** * Delete a folder + * * @param string $folder folder name + * @return array success * - * @return bool success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function deleteFolder(string $folder): bool { - return (bool)$this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); + public function deleteFolder(string $folder): array { + return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); } /** * Subscribe to a folder + * * @param string $folder folder name + * @return array success * - * @return bool success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function subscribeFolder(string $folder): bool { - return (bool)$this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); + public function subscribeFolder(string $folder): array { + return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); } /** * Unsubscribe from a folder + * * @param string $folder folder name + * @return array success * - * @return bool success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function unsubscribeFolder(string $folder): bool { - return (bool)$this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); + public function unsubscribeFolder(string $folder): array { + return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); } /** * Apply session saved changes to the server * - * @return bool success + * @return array success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function expunge(): bool { - return (bool)$this->requestAndResponse('EXPUNGE'); + public function expunge(): array { + return $this->requestAndResponse('EXPUNGE'); } /** * Send noop command * - * @return bool success + * @return array success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function noop(): bool { - return (bool)$this->requestAndResponse('NOOP'); + public function noop(): array { + return $this->requestAndResponse('NOOP'); } /** * Retrieve the quota level settings, and usage statics per mailbox - * @param $username * + * @param $username * @return array + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ public function getQuota($username): array { - $result = $this->requestAndResponse("GETQUOTA", ['"#user/'.$username.'"']); - return is_array($result) ? $result : []; + $command = "GETQUOTA"; + $params = ['"#user/' . $username . '"']; + + return $this->requestAndResponse($command, $params); } /** * Retrieve the quota settings per user - * @param string $quota_root * + * @param string $quota_root * @return array + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ public function getQuotaRoot(string $quota_root = 'INBOX'): array { - $result = $this->requestAndResponse("QUOTA", [$quota_root]); - return is_array($result) ? $result : []; + $command = "QUOTA"; + $params = [$quota_root]; + + return $this->requestAndResponse($command, $params); } /** @@ -1078,24 +1166,28 @@ public function done(): bool { /** * Search for matching messages + * * @param array $params - * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use - * message numbers instead. + * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * message numbers instead. * * @return array message ids + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ public function search(array $params, $uid = IMAP::ST_UID): array { $command = $this->buildUIDCommand("SEARCH", $uid); $response = $this->requestAndResponse($command, $params); - if (!$response) return []; foreach ($response as $ids) { - if ($ids[0] == 'SEARCH') { + if ($ids[0] === 'SEARCH') { array_shift($ids); + return $ids; } } + return []; } diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 97a2fe3d..db65578a 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -16,6 +16,8 @@ use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\RuntimeException; @@ -30,6 +32,10 @@ interface ProtocolInterface { /** * Public destructor + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException */ public function __destruct(); @@ -49,10 +55,14 @@ public function connect(string $host, $port = null); * * @param string $user username * @param string $password password - * @return bool success + * + * @return array success + * * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException */ - public function login(string $user, string $password): bool; + public function login(string $user, string $password): array; /** * Authenticate your current session. @@ -67,9 +77,13 @@ public function authenticate(string $user, string $token); /** * Logout of the current server session * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException */ - public function logout(): bool; + public function logout(): array; /** * Check if the current session is connected @@ -191,10 +205,10 @@ public function store(array $flags, int $from, $to = null, $mode = null, bool $s * @param array|null $flags flags for new message * @param string|null $date date for new message * - * @return bool success + * @return array success * @throws RuntimeException */ - public function appendMessage(string $folder, string $message, $flags = null, $date = null): bool; + public function appendMessage(string $folder, string $message, $flags = null, $date = null): array; /** * Copy message set from current folder to other folder @@ -206,10 +220,10 @@ public function appendMessage(string $folder, string $message, $flags = null, $d * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return bool success + * @return array success * @throws RuntimeException */ - public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool; + public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array; /** * Copy multiple messages to the target folder @@ -218,10 +232,10 @@ public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array|bool Tokens if operation successful, false if an error occurred + * @return array Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID); + public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array; /** * Move a message set from current folder to another folder @@ -232,9 +246,9 @@ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::S * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return bool success + * @return array success */ - public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool; + public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array; /** * Move multiple messages to the target folder @@ -244,10 +258,10 @@ public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array|bool Tokens if operation successful, false if an error occurred + * @return array Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID); + public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array; /** * Exchange identification information @@ -264,47 +278,56 @@ public function ID($ids = null); * Create a new folder * * @param string $folder folder name - * @return bool success + * @return array success * @throws RuntimeException */ - public function createFolder(string $folder): bool; + public function createFolder(string $folder): array; /** * Rename an existing folder * * @param string $old old name * @param string $new new name - * @return bool success + * @return array success * @throws RuntimeException */ - public function renameFolder(string $old, string $new): bool; + public function renameFolder(string $old, string $new): array; /** * Delete a folder * * @param string $folder folder name - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function deleteFolder(string $folder): bool; + public function deleteFolder(string $folder): array; /** * Subscribe to a folder * * @param string $folder folder name - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function subscribeFolder(string $folder): bool; + public function subscribeFolder(string $folder): array; /** * Unsubscribe from a folder + * * @param string $folder folder name + * @return array success * - * @return bool success + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function unsubscribeFolder(string $folder): bool; + public function unsubscribeFolder(string $folder): array; /** * Send idle command @@ -322,10 +345,13 @@ public function done(); /** * Apply session saved changes to the server * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function expunge(): bool; + public function expunge(): array; /** * Retrieve the quota level settings, and usage statics per mailbox @@ -349,10 +375,13 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): array; /** * Send noop command * - * @return bool success + * @return array success + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException */ - public function noop(): bool; + public function noop(): array; /** * Do a search request diff --git a/src/Exceptions/ImapBadRequestException.php b/src/Exceptions/ImapBadRequestException.php new file mode 100644 index 00000000..511718d5 --- /dev/null +++ b/src/Exceptions/ImapBadRequestException.php @@ -0,0 +1,24 @@ + Date: Mon, 2 Jan 2023 01:34:41 +0100 Subject: [PATCH 359/600] travis ci file removed --- .travis.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 26e564ef..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: php - -os: linux -dist: xenial - -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - hhvm - -jobs: - fast_finish: true - allow_failures: - - php: 7.3 - - php: 7.4 - - php: hhvm - -before_install: - - COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-source --no-interaction --dev - -install: - - COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction - -script: - - ./vendor/bin/phpunit - -notifications: - email: - on_success: always - on_failure: always From 630b401beb98ad191f7c9b0fc0b29adeb53ba99e Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 01:50:28 +0100 Subject: [PATCH 360/600] Strict attribute and return types introduced --- src/Address.php | 14 +-- src/Attachment.php | 23 +++-- src/Attribute.php | 8 +- src/Client.php | 86 ++++++++--------- src/ClientManager.php | 20 ++-- src/EncodingAliases.php | 6 +- src/Folder.php | 81 ++++++++-------- src/Header.php | 47 ++++------ src/Message.php | 201 ++++++++++++++++++++-------------------- src/Part.php | 66 ++++++------- src/Structure.php | 12 +-- 11 files changed, 273 insertions(+), 291 deletions(-) diff --git a/src/Address.php b/src/Address.php index 96e48d94..ba186d1e 100644 --- a/src/Address.php +++ b/src/Address.php @@ -27,17 +27,17 @@ class Address { * @var string $mail * @var string $full */ - public $personal = ""; - public $mailbox = ""; - public $host = ""; - public $mail = ""; - public $full = ""; + public string $personal = ""; + public string $mailbox = ""; + public string $host = ""; + public string $mail = ""; + public string $full = ""; /** * Address constructor. - * @param object $object + * @param object $object */ - public function __construct($object) { + public function __construct(object $object) { if (property_exists($object, "personal")){ $this->personal = $object->personal; } if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox; } if (property_exists($object, "host")){ $this->host = $object->host; } diff --git a/src/Attachment.php b/src/Attachment.php index a7c74969..cf261ac4 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -54,24 +54,24 @@ class Attachment { /** * @var Message $oMessage */ - protected $oMessage; + protected Message $oMessage; /** * Used config * * @var array $config */ - protected $config = []; + protected array $config = []; /** @var Part $part */ - protected $part; + protected Part $part; /** * Attribute holder * * @var array $attributes */ - protected $attributes = [ + protected array $attributes = [ 'content' => null, 'type' => null, 'part_number' => 0, @@ -88,7 +88,7 @@ class Attachment { * * @var string $mask */ - protected $mask = AttachmentMask::class; + protected string $mask = AttachmentMask::class; /** * Attachment constructor. @@ -204,8 +204,7 @@ protected function findType() { /** * Fetch the given attachment */ - protected function fetch() { - + protected function fetch(): void { $content = $this->part->content; $this->content_type = $this->part->content_type; @@ -244,7 +243,7 @@ protected function fetch() { * * @return boolean */ - public function save(string $path, $filename = null): bool { + public function save(string $path, string $filename = null): bool { $filename = $filename ?: $this->getName(); return file_put_contents($path.$filename, $this->getContent()) !== false; @@ -254,7 +253,7 @@ public function save(string $path, $filename = null): bool { * Set the attachment name and try to decode it * @param $name */ - public function setName($name) { + public function setName($name): void { $decoder = $this->config['decoder']['attachment']; if ($name !== null) { if($decoder === 'utf-8' && extension_loaded('imap')) { @@ -270,7 +269,7 @@ public function setName($name) { * * @return string|null */ - public function getMimeType(){ + public function getMimeType(): ?string { return (new \finfo())->buffer($this->getContent(), FILEINFO_MIME_TYPE); } @@ -279,7 +278,7 @@ public function getMimeType(){ * * @return string|null */ - public function getExtension(){ + public function getExtension(): ?string { $guesser = "\Symfony\Component\Mime\MimeTypes"; if (class_exists($guesser) !== false) { /** @var Symfony\Component\Mime\MimeTypes $guesser */ @@ -342,7 +341,7 @@ public function getMask(): string { * @return mixed * @throws MaskNotFoundException */ - public function mask($mask = null){ + public function mask(string $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; if(class_exists($mask)){ return new $mask($this); diff --git a/src/Attribute.php b/src/Attribute.php index 627cc315..fc7a40a9 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -24,21 +24,21 @@ class Attribute implements ArrayAccess { /** @var string $name */ - protected $name; + protected string $name; /** * Value holder * * @var array $values */ - protected $values = []; + protected array $values = []; /** * Attribute constructor. * @param string $name - * @param array|mixed $value + * @param mixed|null $value */ - public function __construct(string $name, $value = null) { + public function __construct(string $name, mixed $value = null) { $this->setName($name); $this->add($value); } diff --git a/src/Client.php b/src/Client.php index 5af56423..0069b92b 100755 --- a/src/Client.php +++ b/src/Client.php @@ -38,16 +38,16 @@ class Client { /** * Connection resource * - * @var boolean|Protocol|ProtocolInterface + * @var ?ProtocolInterface */ - public $connection = false; + public ?ProtocolInterface $connection = null; /** * Server hostname. * * @var string */ - public $host; + public string $host; /** * Server port. @@ -59,9 +59,9 @@ class Client { /** * Service protocol. * - * @var int + * @var string */ - public $protocol; + public string $protocol; /** * Server encryption. @@ -69,20 +69,20 @@ class Client { * * @var string */ - public $encryption; + public string $encryption; /** * If server has to validate cert. * * @var bool */ - public $validate_cert = true; + public bool $validate_cert = true; /** * Proxy settings * @var array */ - protected $proxy = [ + protected array $proxy = [ 'socket' => null, 'request_fulluri' => false, 'username' => null, @@ -93,63 +93,63 @@ class Client { * Connection timeout * @var int $timeout */ - public $timeout; + public int $timeout; /** - * Account username/ + * Account username * - * @var mixed + * @var string */ - public $username; + public string $username; /** * Account password. * * @var string */ - public $password; + public string $password; /** * Additional data fetched from the server. * - * @var string + * @var array */ - public $extensions; + public array $extensions; /** * Account authentication method. * - * @var string + * @var ?string */ - public $authentication; + public ?string $authentication; /** * Active folder path. * - * @var string + * @var ?string */ - protected $active_folder = null; + protected ?string $active_folder = null; /** * Default message mask * * @var string $default_message_mask */ - protected $default_message_mask = MessageMask::class; + protected string $default_message_mask = MessageMask::class; /** * Default attachment mask * * @var string $default_attachment_mask */ - protected $default_attachment_mask = AttachmentMask::class; + protected string $default_attachment_mask = AttachmentMask::class; /** * Used default account values * * @var array $default_account_config */ - protected $default_account_config = [ + protected array $default_account_config = [ 'host' => 'localhost', 'port' => 993, 'protocol' => 'imap', @@ -210,7 +210,7 @@ public function setConfig(array $config): Client { * @param array $config * @param array $default_config */ - private function setAccountConfig(string $key, array $config, array $default_config){ + private function setAccountConfig(string $key, array $config, array $default_config): void { $value = $this->default_account_config[$key]; if(isset($config[$key])) { $value = $config[$key]; @@ -224,7 +224,7 @@ private function setAccountConfig(string $key, array $config, array $default_con * Look for a possible events in any available config * @param $config */ - protected function setEventsFromConfig($config) { + protected function setEventsFromConfig($config): void { $this->events = ClientManager::get("events"); if(isset($config['events'])){ foreach($config['events'] as $section => $events) { @@ -291,7 +291,7 @@ protected function setMaskFromConfig($config) { * @return bool|Protocol|ProtocolInterface * @throws ConnectionFailedException */ - public function getConnection() { + public function getConnection(): ProtocolInterface { $this->checkConnection(); return $this->connection; } @@ -321,7 +321,7 @@ public function checkConnection() { * * @throws ConnectionFailedException */ - public function reconnect() { + public function reconnect(): void { if ($this->isConnected()) { $this->disconnect(); } @@ -409,18 +409,14 @@ public function disconnect(): Client { /** * Get a folder instance by a folder name * @param string $folder_name - * @param string|bool|null $delimiter + * @param string|null $delimiter * * @return Folder|null * @throws ConnectionFailedException * @throws FolderFetchingException * @throws Exceptions\RuntimeException */ - public function getFolder(string $folder_name, $delimiter = null) { - if ($delimiter !== false && $delimiter !== null) { - return $this->getFolderByPath($folder_name); - } - + public function getFolder(string $folder_name, ?string $delimiter = null): ?Folder { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; if (strpos($folder_name, (string)$delimiter) !== false) { @@ -439,7 +435,7 @@ public function getFolder(string $folder_name, $delimiter = null) { * @throws FolderFetchingException * @throws Exceptions\RuntimeException */ - public function getFolderByName($folder_name) { + public function getFolderByName($folder_name): ?Folder { return $this->getFolders(false)->where("name", $folder_name)->first(); } @@ -452,7 +448,7 @@ public function getFolderByName($folder_name) { * @throws FolderFetchingException * @throws Exceptions\RuntimeException */ - public function getFolderByPath($folder_path) { + public function getFolderByPath($folder_path): ?Folder { return $this->getFolders(false)->where("path", $folder_path)->first(); } @@ -544,9 +540,9 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function openFolder(string $folder_path, bool $force_select = false) { + public function openFolder(string $folder_path, bool $force_select = false): array { if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) { - return true; + return []; } $this->checkConnection(); $this->active_folder = $folder_path; @@ -564,7 +560,7 @@ public function openFolder(string $folder_path, bool $force_select = false) { * @throws Exceptions\EventNotFoundException * @throws Exceptions\RuntimeException */ - public function createFolder(string $folder, bool $expunge = true): Folder { + public function createFolder(string $folder_path, bool $expunge = true): Folder { $this->checkConnection(); $status = $this->connection->createFolder($folder); @@ -581,13 +577,13 @@ public function createFolder(string $folder, bool $expunge = true): Folder { /** * Check a given folder - * @param $folder + * @param string $folder_path * - * @return array|bool + * @return array * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function checkFolder($folder) { + public function checkFolder(string $folder_path): array { $this->checkConnection(); return $this->connection->examineFolder($folder); } @@ -597,7 +593,7 @@ public function checkFolder($folder) { * * @return string */ - public function getFolderPath(){ + public function getFolderPath(): string { return $this->active_folder; } @@ -606,12 +602,12 @@ public function getFolderPath(){ * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 * * @param array|null $ids - * @return array|bool|void|null + * @return array * * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function Id(array $ids = null) { + public function Id(array $ids = null): array { $this->checkConnection(); return $this->connection->ID($ids); } @@ -643,11 +639,11 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): array { /** * Delete all messages marked for deletion * - * @return bool + * @return array * @throws ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function expunge(): bool { + public function expunge(): array { $this->checkConnection(); return $this->connection->expunge(); } diff --git a/src/ClientManager.php b/src/ClientManager.php index 61557ef2..fd136600 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -26,18 +26,18 @@ class ClientManager { * * @var array $config */ - public static $config = []; + public static array $config = []; /** * @var array $accounts */ - protected $accounts = []; + protected array $accounts = []; /** * ClientManager constructor. * @param array|string $config */ - public function __construct($config = []) { + public function __construct(array|string $config = []) { $this->setConfig($config); } @@ -69,11 +69,11 @@ public function make(array $config): Client { /** * Get a dotted config parameter * @param string $key - * @param null $default + * @param null $default * * @return mixed|null */ - public static function get(string $key, $default = null) { + public static function get(string $key, $default = null): mixed { $parts = explode('.', $key); $value = null; foreach($parts as $part) { @@ -134,8 +134,8 @@ protected function resolve(string $name): Client { * * @return array */ - protected function getClientConfig($name): array { - if ($name === null || $name === 'null') { + protected function getClientConfig(?string $name): array { + if ($name === null || $name === 'null' || $name === "") { return ['driver' => 'null']; } @@ -157,7 +157,7 @@ public function getDefaultAccount(): string { * * @return void */ - public function setDefaultAccount(string $name) { + public function setDefaultAccount(string $name): void { self::$config['default'] = $name; } @@ -173,7 +173,7 @@ public function setDefaultAccount(string $name) { * * @return $this */ - public function setConfig($config): ClientManager { + public function setConfig(array|string $config): ClientManager { if(is_array($config) === false) { $config = require $config; @@ -223,7 +223,7 @@ public function setConfig($config): ClientManager { * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 * @author Mark Roduner */ - private function array_merge_recursive_distinct() { + private function array_merge_recursive_distinct(): mixed { $arrays = func_get_args(); $base = array_shift($arrays); diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 9140640e..40d5ecd3 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -25,7 +25,7 @@ class EncodingAliases { * * @var array */ - private static $aliases = [ + private static array $aliases = [ /* |-------------------------------------------------------------------------- | Email encoding aliases @@ -472,11 +472,11 @@ class EncodingAliases { * * @return string */ - public static function get($encoding, string $fallback = null): string { + public static function get(?string $encoding, string $fallback = null): string { if (isset(self::$aliases[strtolower($encoding ?? '')])) { return self::$aliases[strtolower($encoding ?? '')]; } - return $fallback !== null ? $fallback : $encoding; + return $fallback ?: $encoding; } } diff --git a/src/Folder.php b/src/Folder.php index 01e59cc8..b425bf79 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -33,57 +33,57 @@ class Folder { * * @var Client */ - protected $client; + protected Client $client; /** * Folder full path * * @var string */ - public $path; + public string $path; /** * Folder name * * @var string */ - public $name; + public string $name; /** - * Folder fullname + * Folder full name * * @var string */ - public $full_name; + public string $full_name; /** * Children folders * - * @var FolderCollection|array + * @var FolderCollection */ - public $children = []; + public FolderCollection $children; /** * Delimiter for folder * * @var string */ - public $delimiter; + public string $delimiter; /** - * Indicates if folder can't containg any "children". + * Indicates if folder can't contain any "children". * CreateFolder won't work on this folder. * * @var boolean */ - public $no_inferiors; + public bool $no_inferiors; /** * Indicates if folder is only container, not a mailbox - you can't open it. * * @var boolean */ - public $no_select; + public bool $no_select; /** * Indicates if folder is marked. This means that it may contain new messages since the last time it was checked. @@ -91,26 +91,26 @@ class Folder { * * @var boolean */ - public $marked; + public bool $marked; /** - * Indicates if folder containg any "children". + * Indicates if folder contains any "children". * Not provided by all IMAP servers. * * @var boolean */ - public $has_children; + public bool $has_children; /** - * Indicates if folder refers to other. + * Indicates if folder refers to others. * Not provided by all IMAP servers. * * @var boolean */ - public $referral; + public bool $referral; /** @var array */ - public $status; + public array $status; /** * Folder constructor. @@ -184,11 +184,11 @@ public function hasChildren(): bool { /** * Set children. - * @param FolderCollection|array $children + * @param FolderCollection $children * - * @return self + * @return Folder */ - public function setChildren($children = []): Folder { + public function setChildren(FolderCollection $children): Folder { $this->children = $children; return $this; @@ -199,9 +199,9 @@ public function setChildren($children = []): Folder { * It converts UTF7-IMAP encoding to UTF-8. * @param $name * - * @return array|false|string|string[]|null + * @return string|array|bool|string[]|null */ - protected function decodeName($name) { + protected function decodeName($name): string|array|bool|null { return mb_convert_encoding($name, "UTF-8", "UTF7-IMAP"); } @@ -210,9 +210,9 @@ protected function decodeName($name) { * @param $delimiter * @param $full_name * - * @return mixed + * @return string|bool */ - protected function getSimpleName($delimiter, $full_name) { + protected function getSimpleName($delimiter, $full_name): string|bool { $arr = explode($delimiter, $full_name); return end($arr); @@ -222,7 +222,7 @@ protected function getSimpleName($delimiter, $full_name) { * Parse attributes and set it to object properties. * @param $attributes */ - protected function parseAttributes($attributes) { + protected function parseAttributes($attributes): void { $this->no_inferiors = in_array('\NoInferiors', $attributes); $this->no_select = in_array('\NoSelect', $attributes); $this->marked = in_array('\Marked', $attributes); @@ -235,13 +235,13 @@ protected function parseAttributes($attributes) { * @param string $new_name * @param boolean $expunge * - * @return bool + * @return array * @throws ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ - public function move(string $new_name, bool $expunge = true): bool { + public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name); if($expunge) $this->client->expunge(); @@ -274,13 +274,13 @@ public function overview(string $sequence = null): array { * Append a string message to the current mailbox * @param string $message * @param array|null $options - * @param string|null|Carbon $internal_date + * @param string|Carbon|null $internal_date * * @return bool * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function appendMessage(string $message, array $options = null, $internal_date = null): bool { + public function appendMessage(string $message, array $options = null, Carbon|string $internal_date = null): array { /** * Check if $internal_date is parsed. If it is null it should not be set. Otherwise, the message can't be stored. * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a @@ -299,13 +299,13 @@ public function appendMessage(string $message, array $options = null, $internal_ * @param string $new_name * @param boolean $expunge * - * @return bool + * @return array * @throws ConnectionFailedException * @throws Exceptions\EventNotFoundException * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ - public function rename(string $new_name, bool $expunge = true): bool { + public function rename(string $new_name, bool $expunge = true): array { return $this->move($new_name, $expunge); } @@ -318,9 +318,9 @@ public function rename(string $new_name, bool $expunge = true): bool { * @throws Exceptions\RuntimeException * @throws Exceptions\EventNotFoundException */ - public function delete(bool $expunge = true): bool { - $status = $this->client->getConnection()->deleteFolder($this->path); - if($expunge) $this->client->expunge(); + public function delete(bool $expunge = true): array { + $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); + if ($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); $event::dispatch($this); @@ -335,7 +335,7 @@ public function delete(bool $expunge = true): bool { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function subscribe(): bool { + public function subscribe(): array { $this->client->openFolder($this->path); return $this->client->getConnection()->subscribeFolder($this->path); } @@ -347,7 +347,7 @@ public function subscribe(): bool { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - public function unsubscribe(): bool { + public function unsubscribe(): array { $this->client->openFolder($this->path); return $this->client->getConnection()->unsubscribeFolder($this->path); } @@ -368,7 +368,7 @@ public function unsubscribe(): bool { * @throws Exceptions\MessageNotFoundException * @throws Exceptions\NotSupportedCapabilityException */ - public function idle(callable $callback, int $timeout = 300, bool $auto_reconnect = false) { + public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); if (!in_array("IDLE", $this->client->getConnection()->getCapabilities())) { throw new NotSupportedCapabilityException("IMAP server does not support IDLE"); @@ -434,8 +434,7 @@ public function getStatus(): array { * @throws RuntimeException * @throws ConnectionFailedException */ - public function loadStatus(): Folder - { + public function loadStatus(): Folder { $this->status = $this->getStatus(); return $this; } @@ -465,8 +464,8 @@ public function getClient(): Client { * Set the delimiter * @param $delimiter */ - public function setDelimiter($delimiter){ - if(in_array($delimiter, [null, '', ' ', false]) === true) { + public function setDelimiter($delimiter): void { + if (in_array($delimiter, [null, '', ' ', false]) === true) { $delimiter = ClientManager::get('options.delimiter', '/'); } diff --git a/src/Header.php b/src/Header.php index b9bcc057..ae1b0217 100644 --- a/src/Header.php +++ b/src/Header.php @@ -29,44 +29,36 @@ class Header { * * @var string $raw */ - public $raw = ""; + public string $raw = ""; /** * Attribute holder * * @var Attribute[]|array $attributes */ - protected $attributes = []; + protected array $attributes = []; /** * Config holder * * @var array $config */ - protected $config = []; + protected array $config = []; /** * Fallback Encoding * * @var string */ - public $fallback_encoding = 'UTF-8'; - - /** - * Convert parsed values to attributes - * - * @var bool - */ - protected $attributize = false; + public string $fallback_encoding = 'UTF-8'; /** * Header constructor. * @param string $raw_header - * @param boolean $attributize * * @throws InvalidMessageDateException */ - public function __construct(string $raw_header, bool $attributize = true) { + public function __construct(string $raw_header) { $this->raw = $raw_header; $this->config = ClientManager::get('options'); $this->attributize = $attributize; @@ -108,9 +100,10 @@ public function __get($name) { * Get a specific header attribute * @param $name * - * @return Attribute|mixed + * @return Attribute */ - public function get($name) { + public function get($name): Attribute { + $name = str_replace(["-", " "], "_", strtolower($name)); if (isset($this->attributes[$name])) { return $this->attributes[$name]; } @@ -124,9 +117,9 @@ public function get($name) { * @param array|mixed $value * @param boolean $strict * - * @return Attribute + * @return Attribute|array */ - public function set(string $name, $value, bool $strict = false) { + public function set(string $name, mixed $value, bool $strict = false): Attribute|array { if (isset($this->attributes[$name]) && $strict === false) { if ($this->attributize) { $this->attributes[$name]->add($value, true); @@ -156,7 +149,7 @@ public function set(string $name, $value, bool $strict = false) { * * @return mixed|null */ - public function find($pattern) { + public function find($pattern): mixed { if (preg_match_all($pattern, $this->raw, $matches)) { if (isset($matches[1])) { if (count($matches[1]) > 0) { @@ -172,7 +165,7 @@ public function find($pattern) { * * @return string|null */ - public function getBoundary() { + public function getBoundary(): ?string { $regex = $this->config["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; $boundary = $this->find($regex); @@ -198,7 +191,7 @@ private function clearBoundaryString(string $str): string { * * @throws InvalidMessageDateException */ - protected function parse() { + protected function parse(): void { $header = $this->rfc822_parse_headers($this->raw); $this->extractAddresses($header); @@ -232,7 +225,7 @@ protected function parse() { * * @return object */ - public function rfc822_parse_headers($raw_headers) { + public function rfc822_parse_headers($raw_headers): object { $headers = []; $imap_headers = []; if (extension_loaded('imap') && $this->config["rfc822"]) { @@ -367,7 +360,7 @@ private function notDecoded($encoded, $decoded): bool { * * @return mixed|string */ - public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { + public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { $from = EncodingAliases::get($from, $this->fallback_encoding); $to = EncodingAliases::get($to, $this->fallback_encoding); @@ -416,7 +409,7 @@ public function convertEncoding($str, $from = "ISO-8859-2", $to = "UTF-8") { * * @return string */ - public function getEncoding($structure): string { + public function getEncoding(object|string $structure): string { if (property_exists($structure, 'parameters')) { foreach ($structure->parameters as $parameter) { if (strtolower($parameter->attribute) == "charset") { @@ -449,7 +442,7 @@ private function is_uft8($value): bool { * * @return mixed */ - private function decode($value) { + private function decode(mixed $value): mixed { if (is_array($value)) { return $this->decodeArray($value); } @@ -592,7 +585,7 @@ private function decodeAddresses($values): array { * Extract a given part as address array from a given header * @param object $header */ - private function extractAddresses($header) { + private function extractAddresses(object $header): void { foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) { if (property_exists($header, $key)) { $this->set($key, $this->parseAddresses($header->$key)); @@ -651,7 +644,7 @@ private function parseAddresses($list): array { /** * Search and extract potential header extensions */ - private function extractHeaderExtensions() { + private function extractHeaderExtensions(): void { foreach ($this->attributes as $key => $value) { if (is_array($value)) { $value = implode(", ", $value); @@ -704,7 +697,7 @@ private function extractHeaderExtensions() { * * @throws InvalidMessageDateException */ - private function parseDate($header) { + private function parseDate(object $header): void { if (property_exists($header, 'date')) { $date = $header->date; diff --git a/src/Message.php b/src/Message.php index 4137db9a..1f6b00eb 100755 --- a/src/Message.php +++ b/src/Message.php @@ -72,102 +72,102 @@ class Message { /** * Client instance * - * @var Client + * @var ?Client */ - private $client = Client::class; + private ?Client $client = null; /** * Default mask * * @var string $mask */ - protected $mask = MessageMask::class; + protected string $mask = MessageMask::class; /** * Used config * * @var array $config */ - protected $config = []; + protected array $config = []; /** * Attribute holder * * @var Attribute[]|array $attributes */ - protected $attributes = []; + protected array $attributes = []; /** * The message folder path * * @var string $folder_path */ - protected $folder_path; + protected string $folder_path; /** * Fetch body options * - * @var integer + * @var ?integer */ - public $fetch_options = null; + public ?int $fetch_options = null; /** * @var integer */ - protected $sequence = IMAP::NIL; + protected int $sequence = IMAP::NIL; /** * Fetch body options * * @var bool */ - public $fetch_body = null; + public bool $fetch_body = true; /** * Fetch flags options * * @var bool */ - public $fetch_flags = null; + public bool $fetch_flags = true; /** - * @var Header $header + * @var ?Header $header */ - public $header = null; + public ?Header $header = null; /** * Raw message body * - * @var null|string $raw_body + * @var string $raw_body */ - public $raw_body = null; + protected string $raw_body = ""; /** * Message structure * - * @var Structure $structure + * @var ?Structure $structure */ - protected $structure = null; + protected ?Structure $structure = null; /** * Message body components * - * @var array $bodies + * @var array $bodies */ - public $bodies = []; + public array $bodies = []; /** @var AttachmentCollection $attachments */ - public $attachments; + public AttachmentCollection $attachments; /** @var FlagCollection $flags */ - public $flags; + public FlagCollection $flags; /** * A list of all available and supported flags * - * @var array $available_flags + * @var ?array $available_flags */ - private $available_flags = null; + private ?array $available_flags = null; /** * Message constructor. @@ -188,7 +188,7 @@ class Message { * @throws MessageFlagException * @throws Exceptions\MessageNotFoundException */ - public function __construct(int $uid, $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { + public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { $this->boot(); $default_mask = $client->getDefaultMessageMask(); @@ -246,7 +246,7 @@ public function __construct(int $uid, $msglist, Client $client, int $fetch_optio * @throws Exceptions\RuntimeException * @throws Exceptions\MessageNotFoundException */ - public static function make(int $uid, $msglist, Client $client, string $raw_header, string $raw_body, array $raw_flags, $fetch_options = null, $sequence = null): Message { + public static function make(int $uid, ?int $msglist, Client $client, string $raw_header, string $raw_body, array $raw_flags, $fetch_options = null, $sequence = null): Message { $reflection = new ReflectionClass(self::class); /** @var self $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -278,7 +278,7 @@ public static function make(int $uid, $msglist, Client $client, string $raw_head /** * Boot a new instance */ - public function boot(){ + public function boot(): void { $this->attributes = []; $this->config = ClientManager::get('options'); @@ -361,11 +361,11 @@ public function hasTextBody(): bool { /** * Get the Message text body * - * @return mixed + * @return string */ - public function getTextBody() { + public function getTextBody(): string { if (!isset($this->bodies['text'])) { - return null; + return ""; } return $this->bodies['text']; @@ -383,11 +383,11 @@ public function hasHTMLBody(): bool { /** * Get the Message html body * - * @return string|null + * @return string */ - public function getHTMLBody() { + public function getHTMLBody(): string { if (!isset($this->bodies['html'])) { - return null; + return ""; } return $this->bodies['html']; @@ -401,7 +401,7 @@ public function getHTMLBody() { * @throws InvalidMessageDateException * @throws MessageHeaderFetchingException */ - private function parseHeader() { + private function parseHeader(): void { $sequence_id = $this->getSequenceId(); $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); if (!isset($headers[$sequence_id])) { @@ -416,7 +416,7 @@ private function parseHeader() { * * @throws InvalidMessageDateException */ - public function parseRawHeader(string $raw_header){ + public function parseRawHeader(string $raw_header): void { $this->header = new Header($raw_header); } @@ -424,7 +424,7 @@ public function parseRawHeader(string $raw_header){ * Parse additional raw flags * @param array $raw_flags */ - public function parseRawFlags(array $raw_flags) { + public function parseRawFlags(array $raw_flags): void { $this->flags = FlagCollection::make([]); foreach($raw_flags as $flag) { @@ -446,7 +446,7 @@ public function parseRawFlags(array $raw_flags) { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - private function parseFlags() { + private function parseFlags(): void { $this->client->openFolder($this->folder_path); $this->flags = FlagCollection::make([]); @@ -465,9 +465,12 @@ private function parseFlags() { /** * Parse the Message body * - * @return $this - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\MessageContentFetchingException + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws Exceptions\EventNotFoundException * @throws MessageFlagException @@ -501,7 +504,7 @@ public function parseBody(): Message { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function peek(){ + public function peek(): void { if ($this->fetch_options == IMAP::FT_PEEK) { if ($this->getFlags()->get("seen") == null) { $this->unsetFlag("Seen"); @@ -515,8 +518,11 @@ public function peek(){ * Parse a given message body * @param string $raw_body * - * @return $this - * @throws Exceptions\ConnectionFailedException + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws Exceptions\RuntimeException @@ -535,8 +541,8 @@ public function parseRawBody(string $raw_body): Message { * @throws Exceptions\ConnectionFailedException * @throws Exceptions\RuntimeException */ - private function fetchStructure(Structure $structure) { - $this->client->openFolder($this->folder_path); + private function fetchStructure(Structure $structure): void { + $this->client?->openFolder($this->folder_path); foreach ($structure->parts as $part) { $this->fetchPart($part); @@ -547,7 +553,7 @@ private function fetchStructure(Structure $structure) { * Fetch a given part * @param Part $part */ - private function fetchPart(Part $part) { + private function fetchPart(Part $part): void { if ($part->isAttachment()) { $this->fetchAttachment($part); }else{ @@ -585,7 +591,7 @@ private function fetchPart(Part $part) { * Fetch the Message attachment * @param Part $part */ - protected function fetchAttachment(Part $part) { + protected function fetchAttachment(Part $part): void { $oAttachment = new Attachment($this, $part); if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { @@ -601,7 +607,7 @@ protected function fetchAttachment(Part $part) { * Fail proof setter for $fetch_option * @param $option * - * @return $this + * @return Message */ public function setFetchOption($option): Message { if (is_long($option) === true) { @@ -618,9 +624,9 @@ public function setFetchOption($option): Message { * Set the sequence type * @param int|null $sequence * - * @return $this + * @return Message */ - public function setSequence($sequence): Message { + public function setSequence(?int $sequence): Message { if (is_long($sequence)) { $this->sequence = $sequence; } elseif (is_null($sequence)) { @@ -635,7 +641,7 @@ public function setSequence($sequence): Message { * Fail proof setter for $fetch_body * @param $option * - * @return $this + * @return Message */ public function setFetchBodyOption($option): Message { if (is_bool($option)) { @@ -652,7 +658,7 @@ public function setFetchBodyOption($option): Message { * Fail proof setter for $fetch_flags * @param $option * - * @return $this + * @return Message */ public function setFetchFlagsOption($option): Message { if (is_bool($option)) { @@ -699,7 +705,7 @@ public function decodeString($string, $encoding): string { * * @return mixed|string */ - public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8") { + public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { $from = EncodingAliases::get($from); $to = EncodingAliases::get($to); @@ -735,11 +741,11 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = /** * Get the encoding of a given abject - * @param string|object $structure + * @param object|string $structure * - * @return string|null + * @return string */ - public function getEncoding($structure): string { + public function getEncoding(object|string $structure): string { if (property_exists($structure, 'parameters')) { foreach ($structure->parameters as $parameter) { if (strtolower($parameter->attribute) == "charset") { @@ -763,7 +769,7 @@ public function getEncoding($structure): string { * @throws Exceptions\FolderFetchingException * @throws Exceptions\RuntimeException */ - public function getFolder(){ + public function getFolder(): ?Folder { return $this->client->getFolderByPath($this->folder_path); } @@ -818,7 +824,7 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = * @throws Exceptions\RuntimeException * @throws Exceptions\FolderFetchingException */ - protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in_reply_to, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder){ + protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in_reply_to, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder): void { $primary_folder->query()->inReplyTo($in_reply_to) ->setFetchBody($this->getFetchBodyOption()) ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ @@ -840,7 +846,7 @@ protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in * @throws Exceptions\RuntimeException * @throws Exceptions\FolderFetchingException */ - protected function fetchThreadByMessageId(MessageCollection &$thread, string $message_id, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder){ + protected function fetchThreadByMessageId(MessageCollection &$thread, string $message_id, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder): void { $primary_folder->query()->messageId($message_id) ->setFetchBody($this->getFetchBodyOption()) ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ @@ -865,7 +871,7 @@ protected function fetchThreadByMessageId(MessageCollection &$thread, string $me * @throws MessageFlagException * @throws Exceptions\MessageNotFoundException */ - public function copy(string $folder_path, bool $expunge = false) { + public function copy(string $folder_path, bool $expunge = false): ?Message { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path); @@ -900,7 +906,7 @@ public function copy(string $folder_path, bool $expunge = false) { * @throws MessageFlagException * @throws Exceptions\MessageNotFoundException */ - public function move(string $folder_path, bool $expunge = false) { + public function move(string $folder_path, bool $expunge = false): ?Message { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path); @@ -971,7 +977,7 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo * @throws MessageFlagException * @throws MessageHeaderFetchingException */ - public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false) { + public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false): bool { $status = $this->setFlag("Deleted"); if($force_move) { $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"]: $trash_path; @@ -1007,7 +1013,7 @@ public function restore(bool $expunge = true): bool { /** * Set a given flag - * @param string|array $flag + * @param array|string $flag * * @return bool * @throws Exceptions\ConnectionFailedException @@ -1015,7 +1021,7 @@ public function restore(bool $expunge = true): bool { * @throws Exceptions\EventNotFoundException * @throws Exceptions\RuntimeException */ - public function setFlag($flag): bool { + public function setFlag(array|string $flag): bool { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); @@ -1034,7 +1040,7 @@ public function setFlag($flag): bool { /** * Unset a given flag - * @param string|array $flag + * @param array|string $flag * * @return bool * @throws Exceptions\ConnectionFailedException @@ -1042,7 +1048,7 @@ public function setFlag($flag): bool { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function unsetFlag($flag): bool { + public function unsetFlag(array|string $flag): bool { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); @@ -1062,7 +1068,7 @@ public function unsetFlag($flag): bool { /** * Set a given flag - * @param string|array $flag + * @param array|string $flag * * @return bool * @throws Exceptions\ConnectionFailedException @@ -1070,13 +1076,13 @@ public function unsetFlag($flag): bool { * @throws Exceptions\EventNotFoundException * @throws Exceptions\RuntimeException */ - public function addFlag($flag): bool { + public function addFlag(array|string $flag): bool { return $this->setFlag($flag); } /** * Unset a given flag - * @param string|array $flag + * @param array|string $flag * * @return bool * @throws Exceptions\ConnectionFailedException @@ -1084,7 +1090,7 @@ public function addFlag($flag): bool { * @throws MessageFlagException * @throws Exceptions\RuntimeException */ - public function removeFlag($flag): bool { + public function removeFlag(array|string $flag): bool { return $this->unsetFlag($flag); } @@ -1119,13 +1125,9 @@ public function hasAttachments(): bool { * Get the raw body * * @return string - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException */ - public function getRawBody() { - if ($this->raw_body === null) { - $this->client->openFolder($this->folder_path); - + public function getRawBody(): string { + if ($this->raw_body === "") { $this->raw_body = $this->structure->raw; } @@ -1135,27 +1137,27 @@ public function getRawBody() { /** * Get the message header * - * @return Header + * @return ?Header */ - public function getHeader() { + public function getHeader(): ?Header { return $this->header; } /** * Get the current client * - * @return Client + * @return ?Client */ - public function getClient(): Client { + public function getClient(): ?Client { return $this->client; } /** * Get the used fetch option * - * @return integer + * @return ?integer */ - public function getFetchOptions() { + public function getFetchOptions(): ?int { return $this->fetch_options; } @@ -1164,7 +1166,7 @@ public function getFetchOptions() { * * @return boolean */ - public function getFetchBodyOption() { + public function getFetchBodyOption(): bool { return $this->fetch_body; } @@ -1173,7 +1175,7 @@ public function getFetchBodyOption() { * * @return boolean */ - public function getFetchFlagsOption() { + public function getFetchFlagsOption(): bool { return $this->fetch_flags; } @@ -1209,14 +1211,14 @@ public function flags(): FlagCollection { * * @return Structure|null */ - public function getStructure(){ + public function getStructure(): ?Structure { return $this->structure; } /** * Check if a message matches an other by comparing basic attributes * - * @param null|Message $message + * @param null|Message $message * @return boolean */ public function is(Message $message = null): bool { @@ -1243,7 +1245,7 @@ public function getAttributes(): array { * Set the message mask * @param $mask * - * @return $this + * @return Message */ public function setMask($mask): Message { if(class_exists($mask)){ @@ -1264,12 +1266,12 @@ public function getMask(): string { /** * Get a masked instance by providing a mask name - * @param string|mixed $mask + * @param mixed|null $mask * * @return mixed * @throws MaskNotFoundException */ - public function mask($mask = null){ + public function mask(mixed $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; if(class_exists($mask)){ return new $mask($this); @@ -1291,7 +1293,7 @@ public function getFolderPath(): string { * Set the message path aka folder path * @param $folder_path * - * @return $this + * @return Message */ public function setFolderPath($folder_path): Message { $this->folder_path = $folder_path; @@ -1303,7 +1305,7 @@ public function setFolderPath($folder_path): Message { * Set the config * @param $config * - * @return $this + * @return Message */ public function setConfig($config): Message { $this->config = $config; @@ -1315,7 +1317,7 @@ public function setConfig($config): Message { * Set the available flags * @param $available_flags * - * @return $this + * @return Message */ public function setAvailableFlags($available_flags): Message { $this->available_flags = $available_flags; @@ -1327,7 +1329,7 @@ public function setAvailableFlags($available_flags): Message { * Set the attachment collection * @param $attachments * - * @return $this + * @return Message */ public function setAttachments($attachments): Message { $this->attachments = $attachments; @@ -1339,7 +1341,7 @@ public function setAttachments($attachments): Message { * Set the flag collection * @param $flags * - * @return $this + * @return Message */ public function setFlags($flags): Message { $this->flags = $flags; @@ -1366,9 +1368,7 @@ public function setClient($client): Message { * Set the message number * @param int $uid * - * @return $this - * @throws Exceptions\MessageNotFoundException - * @throws Exceptions\ConnectionFailedException + * @return Message */ public function setUid(int $uid): Message { $this->uid = $uid; @@ -1383,9 +1383,7 @@ public function setUid(int $uid): Message { * @param int $msgn * @param int|null $msglist * - * @return $this - * @throws Exceptions\MessageNotFoundException - * @throws Exceptions\ConnectionFailedException + * @return Message */ public function setMsgn(int $msgn, int $msglist = null): Message { $this->msgn = $msgn; @@ -1417,11 +1415,8 @@ public function getSequenceId(): int { * Set the sequence id * @param $uid * @param int|null $msglist - * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\MessageNotFoundException */ - public function setSequenceId($uid, int $msglist = null){ + public function setSequenceId($uid, int $msglist = null): void { if ($this->getSequence() === IMAP::ST_UID) { $this->setUid($uid); $this->setMsglist($msglist); diff --git a/src/Part.php b/src/Part.php index f29b6348..594e900d 100644 --- a/src/Part.php +++ b/src/Part.php @@ -27,117 +27,117 @@ class Part { * * @var string $raw */ - public $raw = ""; + public string $raw = ""; /** * Part type * * @var int $type */ - public $type = IMAP::MESSAGE_TYPE_TEXT; + public int $type = IMAP::MESSAGE_TYPE_TEXT; /** * Part content * * @var string $content */ - public $content = ""; + public string $content = ""; /** * Part subtype * - * @var string $subtype + * @var ?string $subtype */ - public $subtype = null; + public ?string $subtype = null; /** * Part charset - if available * * @var string $charset */ - public $charset = "utf-8"; + public string $charset = "utf-8"; /** * Part encoding method * * @var int $encoding */ - public $encoding = IMAP::MESSAGE_ENC_OTHER; + public int $encoding = IMAP::MESSAGE_ENC_OTHER; /** * Alias to check if the part is an attachment * * @var boolean $ifdisposition */ - public $ifdisposition = false; + public bool $ifdisposition = false; /** * Indicates if the part is an attachment * - * @var string $disposition + * @var ?string $disposition */ - public $disposition = null; + public ?string $disposition = null; /** * Alias to check if the part has a description * * @var boolean $ifdescription */ - public $ifdescription = false; + public bool $ifdescription = false; /** * Part description if available * - * @var string $description + * @var ?string $description */ - public $description = null; + public ?string $description = null; /** * Part filename if available * - * @var string $filename + * @var ?string $filename */ - public $filename = null; + public ?string $filename = null; /** * Part name if available * - * @var string $name + * @var ?string $name */ - public $name = null; + public ?string $name = null; /** * Part id if available * - * @var string $id + * @var ?string $id */ - public $id = null; + public ?string $id = null; /** * The part number of the current part * * @var integer $part_number */ - public $part_number = 0; + public int $part_number = 0; /** * Part length in bytes * * @var integer $bytes */ - public $bytes = null; + public int $bytes; /** * Part content type * * @var string|null $content_type */ - public $content_type = null; + public ?string $content_type = null; /** - * @var Header $header + * @var ?Header $header */ - private $header = null; + private ?Header $header; /** * Part constructor. @@ -159,7 +159,7 @@ public function __construct($raw_part, Header $header = null, int $part_number = * * @throws InvalidMessageDateException */ - protected function parse(){ + protected function parse(): void { if ($this->header === null) { $body = $this->findHeaders(); }else{ @@ -224,9 +224,9 @@ private function findHeaders(): string { * Try to parse the subtype if any is present * @param $content_type * - * @return string + * @return ?string */ - private function parseSubtype($content_type){ + private function parseSubtype($content_type): ?string { if (is_array($content_type)) { foreach ($content_type as $part){ if ((strpos($part, "/")) !== false){ @@ -244,9 +244,9 @@ private function parseSubtype($content_type){ /** * Try to parse the disposition if any is present */ - private function parseDisposition(){ - $content_disposition = $this->header->get("content_disposition"); - if($content_disposition !== null) { + private function parseDisposition(): void { + $content_disposition = $this->header->get("content_disposition")->first(); + if($content_disposition) { $this->ifdisposition = true; $this->disposition = (is_array($content_disposition)) ? implode(' ', $content_disposition) : $content_disposition; } @@ -255,9 +255,9 @@ private function parseDisposition(){ /** * Try to parse the description if any is present */ - private function parseDescription(){ - $content_description = $this->header->get("content_description"); - if($content_description !== null) { + private function parseDescription(): void { + $content_description = $this->header->get("content_description")->first(); + if($content_description) { $this->ifdescription = true; $this->description = $content_description; } diff --git a/src/Structure.php b/src/Structure.php index 40ddc3a5..fdf9f64a 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -28,33 +28,33 @@ class Structure { * * @var string $raw */ - public $raw = ""; + public string $raw = ""; /** * @var Header $header */ - private $header = null; + private Header $header; /** * Message type (if multipart or not) * * @var int $type */ - public $type = IMAP::MESSAGE_TYPE_TEXT; + public int $type = IMAP::MESSAGE_TYPE_TEXT; /** * All available parts * * @var Part[] $parts */ - public $parts = []; + public array $parts = []; /** * Config holder * * @var array $config */ - protected $config = []; + protected array $config = []; /** * Structure constructor. @@ -77,7 +77,7 @@ public function __construct($raw_structure, Header $header) { * @throws MessageContentFetchingException * @throws InvalidMessageDateException */ - protected function parse(){ + protected function parse(): void { $this->findContentType(); $this->parts = $this->find_parts(); } From 99001f8495e322cb69755957e001e507cb7267c5 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 01:59:10 +0100 Subject: [PATCH 361/600] Syntax updated to support php 8 features --- src/Attachment.php | 42 ++++++++++----------------------- src/Client.php | 8 +++---- src/Header.php | 51 ++++++++++++++++------------------------ src/Message.php | 4 ++-- src/Part.php | 35 +++++++++------------------ src/Query/Query.php | 34 +++++++-------------------- src/Query/WhereQuery.php | 4 ++-- 7 files changed, 58 insertions(+), 120 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index cf261ac4..4ef7b17e 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -169,36 +169,18 @@ public function __get($name) { /** * Determine the structure type */ - protected function findType() { - switch ($this->part->type) { - case IMAP::ATTACHMENT_TYPE_MESSAGE: - $this->type = 'message'; - break; - case IMAP::ATTACHMENT_TYPE_APPLICATION: - $this->type = 'application'; - break; - case IMAP::ATTACHMENT_TYPE_AUDIO: - $this->type = 'audio'; - break; - case IMAP::ATTACHMENT_TYPE_IMAGE: - $this->type = 'image'; - break; - case IMAP::ATTACHMENT_TYPE_VIDEO: - $this->type = 'video'; - break; - case IMAP::ATTACHMENT_TYPE_MODEL: - $this->type = 'model'; - break; - case IMAP::ATTACHMENT_TYPE_TEXT: - $this->type = 'text'; - break; - case IMAP::ATTACHMENT_TYPE_MULTIPART: - $this->type = 'multipart'; - break; - default: - $this->type = 'other'; - break; - } + protected function findType(): void { + $this->type = match ($this->part->type) { + IMAP::ATTACHMENT_TYPE_MESSAGE => 'message', + IMAP::ATTACHMENT_TYPE_APPLICATION => 'application', + IMAP::ATTACHMENT_TYPE_AUDIO => 'audio', + IMAP::ATTACHMENT_TYPE_IMAGE => 'image', + IMAP::ATTACHMENT_TYPE_VIDEO => 'video', + IMAP::ATTACHMENT_TYPE_MODEL => 'model', + IMAP::ATTACHMENT_TYPE_TEXT => 'text', + IMAP::ATTACHMENT_TYPE_MULTIPART => 'multipart', + default => 'other', + }; } /** diff --git a/src/Client.php b/src/Client.php index 0069b92b..1a101168 100755 --- a/src/Client.php +++ b/src/Client.php @@ -347,7 +347,7 @@ public function connect(): Client { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); } $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption); - if (strpos($protocol, "legacy-") === 0) { + if (str_starts_with($protocol, "legacy-")) { $protocol = substr($protocol, 7); } $this->connection->setProtocol($protocol); @@ -363,9 +363,7 @@ public function connect(): Client { try { $this->connection->connect($this->host, $this->port); - } catch (ErrorException $e) { - throw new ConnectionFailedException("connection setup failed", 0, $e); - } catch (Exceptions\RuntimeException $e) { + } catch (ErrorException|RuntimeException $e) { throw new ConnectionFailedException("connection setup failed", 0, $e); } $this->authenticate(); @@ -419,7 +417,7 @@ public function disconnect(): Client { public function getFolder(string $folder_name, ?string $delimiter = null): ?Folder { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; - if (strpos($folder_name, (string)$delimiter) !== false) { + if (str_contains($folder_name, (string)$delimiter)) { return $this->getFolderByPath($folder_name); } diff --git a/src/Header.php b/src/Header.php index ae1b0217..05084682 100644 --- a/src/Header.php +++ b/src/Header.php @@ -238,17 +238,17 @@ public function rfc822_parse_headers($raw_headers): object { $lines = explode("\r\n", preg_replace("/\r\n\s/", ' ', $raw_headers)); $prev_header = null; foreach ($lines as $line) { - if (substr($line, 0, 1) === "\n") { + if (str_starts_with($line, "\n")) { $line = substr($line, 1); } - if (substr($line, 0, 1) === "\t") { + if (str_starts_with($line, "\t")) { $line = substr($line, 1); $line = trim(rtrim($line)); if ($prev_header !== null) { $headers[$prev_header][] = $line; } - } elseif (substr($line, 0, 1) === " ") { + } elseif (str_starts_with($line, " ")) { $line = substr($line, 1); $line = trim(rtrim($line)); if ($prev_header !== null) { @@ -347,9 +347,9 @@ public function mime_header_decode(string $text): array { * @return bool */ private function notDecoded($encoded, $decoded): bool { - return 0 === strpos($decoded, '=?') + return str_starts_with($decoded, '=?') && strlen($decoded) - 2 === strpos($decoded, '?=') - && false !== strpos($encoded, $decoded); + && str_contains($encoded, $decoded); } /** @@ -394,7 +394,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = return mb_convert_encoding($str, $to, $from); } } catch (\Exception $e) { - if (strstr($from, '-')) { + if (str_contains($from, '-')) { $from = str_replace('-', '', $from); return $this->convertEncoding($str, $from, $to); } else { @@ -433,7 +433,7 @@ public function getEncoding(object|string $structure): string { * @return bool */ private function is_uft8($value): bool { - return strpos(strtolower($value), '=?utf-8?') === 0; + return str_starts_with(strtolower($value), '=?utf-8?'); } /** @@ -500,28 +500,17 @@ private function decodeArray(array $values): array { /** * Try to extract the priority from a given raw header string */ - private function findPriority() { - if (($priority = $this->get("x_priority")) === null) return; - switch ((int)"$priority") { - case IMAP::MESSAGE_PRIORITY_HIGHEST; - $priority = IMAP::MESSAGE_PRIORITY_HIGHEST; - break; - case IMAP::MESSAGE_PRIORITY_HIGH; - $priority = IMAP::MESSAGE_PRIORITY_HIGH; - break; - case IMAP::MESSAGE_PRIORITY_NORMAL; - $priority = IMAP::MESSAGE_PRIORITY_NORMAL; - break; - case IMAP::MESSAGE_PRIORITY_LOW; - $priority = IMAP::MESSAGE_PRIORITY_LOW; - break; - case IMAP::MESSAGE_PRIORITY_LOWEST; - $priority = IMAP::MESSAGE_PRIORITY_LOWEST; - break; - default: - $priority = IMAP::MESSAGE_PRIORITY_UNKNOWN; - break; - } + private function findPriority(): void { + $priority = $this->get("x_priority"); + + $priority = match ((int)"$priority") { + IMAP::MESSAGE_PRIORITY_HIGHEST => IMAP::MESSAGE_PRIORITY_HIGHEST, + IMAP::MESSAGE_PRIORITY_HIGH => IMAP::MESSAGE_PRIORITY_HIGH, + IMAP::MESSAGE_PRIORITY_NORMAL => IMAP::MESSAGE_PRIORITY_NORMAL, + IMAP::MESSAGE_PRIORITY_LOW => IMAP::MESSAGE_PRIORITY_LOW, + IMAP::MESSAGE_PRIORITY_LOWEST => IMAP::MESSAGE_PRIORITY_LOWEST, + default => IMAP::MESSAGE_PRIORITY_UNKNOWN, + }; $this->set("priority", $priority); } @@ -627,7 +616,7 @@ private function parseAddresses($list): array { } } - if (strpos($address->personal, "'") === 0) { + if (str_starts_with($address->personal, "'")) { $address->personal = str_replace("'", "", $address->personal); } } @@ -708,7 +697,7 @@ private function parseDate(object $header): void { $date = trim(rtrim($date)); try { - if(strpos($date, ' ') !== false){ + if (str_contains($date, ' ')) { $date = str_replace(' ', ' ', $date); } $parsed_date = Carbon::parse($date); diff --git a/src/Message.php b/src/Message.php index 1f6b00eb..a6c8129d 100755 --- a/src/Message.php +++ b/src/Message.php @@ -427,8 +427,8 @@ public function parseRawHeader(string $raw_header): void { public function parseRawFlags(array $raw_flags): void { $this->flags = FlagCollection::make([]); - foreach($raw_flags as $flag) { - if (strpos($flag, "\\") === 0){ + foreach ($raw_flags as $flag) { + if (str_starts_with($flag, "\\")) { $flag = substr($flag, 1); } $flag_key = strtolower($flag); diff --git a/src/Part.php b/src/Part.php index 594e900d..e369b93f 100644 --- a/src/Part.php +++ b/src/Part.php @@ -266,30 +266,17 @@ private function parseDescription(): void { /** * Try to parse the encoding if any is present */ - private function parseEncoding(){ - $encoding = $this->header->get("content_transfer_encoding"); - if($encoding !== null) { - switch (strtolower($encoding)) { - case "quoted-printable": - $this->encoding = IMAP::MESSAGE_ENC_QUOTED_PRINTABLE; - break; - case "base64": - $this->encoding = IMAP::MESSAGE_ENC_BASE64; - break; - case "7bit": - $this->encoding = IMAP::MESSAGE_ENC_7BIT; - break; - case "8bit": - $this->encoding = IMAP::MESSAGE_ENC_8BIT; - break; - case "binary": - $this->encoding = IMAP::MESSAGE_ENC_BINARY; - break; - default: - $this->encoding = IMAP::MESSAGE_ENC_OTHER; - break; - - } + private function parseEncoding(): void { + $encoding = $this->header->get("content_transfer_encoding")->first(); + if($encoding) { + $this->encoding = match (strtolower($encoding)) { + "quoted-printable" => IMAP::MESSAGE_ENC_QUOTED_PRINTABLE, + "base64" => IMAP::MESSAGE_ENC_BASE64, + "7bit" => IMAP::MESSAGE_ENC_7BIT, + "8bit" => IMAP::MESSAGE_ENC_8BIT, + "binary" => IMAP::MESSAGE_ENC_BINARY, + default => IMAP::MESSAGE_ENC_OTHER, + }; } } diff --git a/src/Query/Query.php b/src/Query/Query.php index 361a1cef..6de567a0 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -188,9 +188,7 @@ protected function search(): Collection { try { $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence); return new Collection($available_messages); - } catch (RuntimeException $e) { - throw new GetMessagesFailedException("failed to fetch messages", 0, $e); - } catch (ConnectionFailedException $e) { + } catch (RuntimeException|ConnectionFailedException $e) { throw new GetMessagesFailedException("failed to fetch messages", 0, $e); } } @@ -257,15 +255,7 @@ protected function fetch(Collection $available_messages): array { protected function make(int $uid, int $msglist, string $header, string $content, array $flags) { try { return Message::make($uid, $msglist, $this->getClient(), $header, $content, $flags, $this->getFetchOptions(), $this->sequence); - } catch (MessageNotFoundException $e) { - $this->setError($uid, $e); - } catch (RuntimeException $e) { - $this->setError($uid, $e); - } catch (MessageFlagException $e) { - $this->setError($uid, $e); - } catch (InvalidMessageDateException $e) { - $this->setError($uid, $e); - } catch (MessageContentFetchingException $e) { + } catch (RuntimeException|MessageFlagException|InvalidMessageDateException|MessageContentFetchingException $e) { $this->setError($uid, $e); } @@ -283,20 +273,12 @@ protected function make(int $uid, int $msglist, string $header, string $content, * @return string */ protected function getMessageKey(string $message_key, int $msglist, Message $message): string { - switch ($message_key) { - case 'number': - $key = $message->getMessageNo(); - break; - case 'list': - $key = $msglist; - break; - case 'uid': - $key = $message->getUid(); - break; - default: - $key = $message->getMessageId(); - break; - } + $key = match ($message_key) { + 'number' => $message->getMessageNo(), + 'list' => $msglist, + 'uid' => $message->getUid(), + default => $message->getMessageId(), + }; return (string)$key; } diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index b6483f6d..5c9340fc 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -84,7 +84,7 @@ public function __call(string $name, $arguments) { $name = substr($name, 3); } - if (strpos(strtolower($name), "where") === false) { + if (!str_contains(strtolower($name), "where")) { $method = 'where' . ucfirst($name); } else { $method = lcfirst($name); @@ -106,7 +106,7 @@ public function __call(string $name, $arguments) { */ protected function validate_criteria($criteria): string { $command = strtoupper($criteria); - if (substr($command, 0, 7) === "CUSTOM ") { + if (str_starts_with($command, "CUSTOM ")) { return substr($criteria, 7); } if (in_array($command, $this->available_criteria) === false) { From 2d4789ab32b1ceaa7fb21fb870c618cfd492e857 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 02:58:57 +0100 Subject: [PATCH 362/600] Strict attribute and return types introduced --- src/Connection/Protocols/ImapProtocol.php | 7 +- src/Connection/Protocols/LegacyProtocol.php | 13 ++- src/Connection/Protocols/Protocol.php | 93 +++++++++++-------- .../Protocols/ProtocolInterface.php | 2 +- src/Events/FlagNewEvent.php | 9 +- src/Events/FolderMovedEvent.php | 8 +- src/Events/FolderNewEvent.php | 5 +- src/Events/MessageMovedEvent.php | 8 +- src/Events/MessageNewEvent.php | 5 +- src/Query/Query.php | 88 +++++++++--------- src/Query/WhereQuery.php | 30 +++--- src/Support/Masks/AttachmentMask.php | 6 +- src/Support/Masks/Mask.php | 12 +-- src/Support/Masks/MessageMask.php | 10 +- src/Support/PaginatedCollection.php | 10 +- src/Traits/HasEvents.php | 20 ++-- 16 files changed, 174 insertions(+), 152 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index e81c96e9..8792e9a8 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -34,14 +34,14 @@ class ImapProtocol extends Protocol { * Request noun * @var int */ - protected $noun = 0; + protected int $noun = 0; /** * Imap constructor. * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, $encryption = false) { + public function __construct(bool $cert_validation = true, mixed $encryption = false) { $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -62,7 +62,7 @@ public function __destruct() { * * @throws ConnectionFailedException */ - public function connect(string $host, $port = null) { + public function connect(string $host, int $port = null): bool { $transport = 'tcp'; $encryption = ''; @@ -85,6 +85,7 @@ public function connect(string $host, $port = null) { } catch (Exception $e) { throw new ConnectionFailedException('connection failed', 0, $e); } + return true; } /** diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index dd3d4898..d4a92a94 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -25,17 +25,16 @@ */ class LegacyProtocol extends Protocol { - protected $protocol = "imap"; - protected $host = null; - protected $port = null; - protected $encryption = null; + protected string $protocol = "imap"; + protected string $host = "localhost"; + protected int $port = 993; /** * Imap constructor. * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, $encryption = false) { + public function __construct(bool $cert_validation = true, mixed $encryption = false) { $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -50,9 +49,9 @@ public function __destruct() { /** * Save the information for a nw connection * @param string $host - * @param null $port + * @param int|null $port */ - public function connect(string $host, $port = null) { + public function connect(string $host, int $port = null) { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 29d3b0a6..04cba85a 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -25,40 +25,40 @@ abstract class Protocol implements ProtocolInterface { /** * Default connection timeout in seconds */ - protected $connection_timeout = 30; + protected int $connection_timeout = 30; /** * @var boolean */ - protected $debug = false; + protected bool $debug = false; /** * @var boolean */ - protected $enable_uid_cache = true; + protected bool $enable_uid_cache = true; /** - * @var false|\IMAP\Connection|resource + * @var resource */ public $stream = false; /** * Connection encryption method - * @var mixed $encryption + * @var string $encryption */ - protected $encryption = false; + protected string $encryption = ""; /** * Set to false to ignore SSL certificate validation * @var bool */ - protected $cert_validation = true; + protected bool $cert_validation = true; /** * Proxy settings * @var array */ - protected $proxy = [ + protected array $proxy = [ 'socket' => null, 'request_fulluri' => false, 'username' => null, @@ -68,16 +68,16 @@ abstract class Protocol implements ProtocolInterface { /** * Cache for uid of active folder. * - * @var null|array + * @var array */ - protected $uid_cache = null; + protected array $uid_cache = []; /** * Get an available cryptographic method * * @return int */ - public function getCryptoMethod() { + public function getCryptoMethod(): int { // Allow the best TLS version(s) we can $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT; @@ -95,18 +95,18 @@ public function getCryptoMethod() { /** * Enable SSL certificate validation * - * @return $this + * @return Protocol */ - public function enableCertValidation() { + public function enableCertValidation(): Protocol { $this->cert_validation = true; return $this; } /** * Disable SSL certificate validation - * @return $this + * @return Protocol */ - public function disableCertValidation() { + public function disableCertValidation(): Protocol { $this->cert_validation = false; return $this; } @@ -115,9 +115,9 @@ public function disableCertValidation() { * Set SSL certificate validation * @var int $cert_validation * - * @return $this + * @return Protocol */ - public function setCertValidation($cert_validation) { + public function setCertValidation(int $cert_validation): Protocol { $this->cert_validation = $cert_validation; return $this; } @@ -127,7 +127,7 @@ public function setCertValidation($cert_validation) { * * @return bool */ - public function getCertValidation() { + public function getCertValidation(): bool { return $this->cert_validation; } @@ -135,9 +135,9 @@ public function getCertValidation() { * Set connection proxy settings * @var array $options * - * @return $this + * @return Protocol */ - public function setProxy($options) { + public function setProxy(array $options): Protocol { foreach ($this->proxy as $key => $val) { if (isset($options[$key])) { $this->proxy[$key] = $options[$key]; @@ -152,19 +152,19 @@ public function setProxy($options) { * * @return array */ - public function getProxy() { + public function getProxy(): array { return $this->proxy; } /** * Prepare socket options - * @var string $transport - * * @return array + *@var string $transport + * */ - private function defaultSocketOptions($transport) { + private function defaultSocketOptions(string $transport): array { $options = []; - if ($this->encryption != false) { + if ($this->encryption) { $options["ssl"] = [ 'verify_peer_name' => $this->getCertValidation(), 'verify_peer' => $this->getCertValidation(), @@ -194,10 +194,10 @@ private function defaultSocketOptions($transport) { * @param int $port of IMAP server, default is 143 (993 for ssl) * @param int $timeout timeout in seconds for initiating session * - * @return resource|boolean The socket created. + * @return resource The socket created. * @throws ConnectionFailedException */ - protected function createStream($transport, $host, $port, $timeout) { + public function createStream($transport, string $host, int $port, int $timeout) { $socket = "$transport://$host:$port"; $stream = stream_socket_client($socket, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, @@ -218,7 +218,7 @@ protected function createStream($transport, $host, $port, $timeout) { /** * @return int */ - public function getConnectionTimeout() { + public function getConnectionTimeout(): int { return $this->connection_timeout; } @@ -226,10 +226,8 @@ public function getConnectionTimeout() { * @param int $connection_timeout * @return Protocol */ - public function setConnectionTimeout($connection_timeout) { - if ($connection_timeout !== null) { - $this->connection_timeout = $connection_timeout; - } + public function setConnectionTimeout(int $connection_timeout): Protocol { + $this->connection_timeout = $connection_timeout; return $this; } @@ -239,7 +237,7 @@ public function setConnectionTimeout($connection_timeout) { * * @return string */ - public function getUIDKey($uid) { + public function getUIDKey(int|string $uid): string { if ($uid == IMAP::ST_UID || $uid == IMAP::FT_UID) { return "UID"; } @@ -250,7 +248,14 @@ public function getUIDKey($uid) { return ""; } - public function buildUIDCommand($command, $uid) { + /** + * Build a UID / MSGN command + * @param string $command + * @param int|string $uid + * + * @return string + */ + public function buildUIDCommand(string $command, int|string $uid): string { return trim($this->getUIDKey($uid)." ".$command); } @@ -259,9 +264,9 @@ public function buildUIDCommand($command, $uid) { * * @param array|null $uids */ - public function setUidCache($uids) { + public function setUidCache(?array $uids) { if (is_null($uids)) { - $this->uid_cache = null; + $this->uid_cache = []; return; } @@ -269,17 +274,27 @@ public function setUidCache($uids) { $uid_cache = []; foreach ($uids as $uid) { - $uid_cache[$messageNumber++] = $uid; + $uid_cache[$messageNumber++] = (int)$uid; } $this->uid_cache = $uid_cache; } - public function enableUidCache() { + /** + * Enable the uid cache + * + * @return void + */ + public function enableUidCache(): void { $this->enable_uid_cache = true; } - public function disableUidCache() { + /** + * Disable the uid cache + * + * @return void + */ + public function disableUidCache(): void { $this->enable_uid_cache = false; } } diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index db65578a..46fb3035 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -48,7 +48,7 @@ public function __destruct(); * @throws ConnectionFailedException * @throws RuntimeException */ - public function connect(string $host, $port = null); + public function connect(string $host, ?int $port = null); /** * Login to a new session. diff --git a/src/Events/FlagNewEvent.php b/src/Events/FlagNewEvent.php index 3fea518e..18e264fb 100644 --- a/src/Events/FlagNewEvent.php +++ b/src/Events/FlagNewEvent.php @@ -22,17 +22,18 @@ class FlagNewEvent extends Event { /** @var Message $message */ - public $message; + public Message $message; /** @var string $flag */ - public $flag; + public string $flag; /** * Create a new event instance. - * @var mixed[] $arguments + * @var array $arguments + * * @return void */ - public function __construct($arguments) { + public function __construct(array $arguments) { $this->message = $arguments[0]; $this->flag = $arguments[1]; } diff --git a/src/Events/FolderMovedEvent.php b/src/Events/FolderMovedEvent.php index b8b9c7cb..746ad123 100644 --- a/src/Events/FolderMovedEvent.php +++ b/src/Events/FolderMovedEvent.php @@ -22,16 +22,18 @@ class FolderMovedEvent extends Event { /** @var Folder $old_folder */ - public $old_folder; + public Folder $old_folder; + /** @var Folder $new_folder */ - public $new_folder; + public Folder $new_folder; /** * Create a new event instance. * @var Folder[] $folders + * * @return void */ - public function __construct($folders) { + public function __construct(array $folders) { $this->old_folder = $folders[0]; $this->new_folder = $folders[1]; } diff --git a/src/Events/FolderNewEvent.php b/src/Events/FolderNewEvent.php index d16bbbd6..0c576cad 100644 --- a/src/Events/FolderNewEvent.php +++ b/src/Events/FolderNewEvent.php @@ -22,14 +22,15 @@ class FolderNewEvent extends Event { /** @var Folder $folder */ - public $folder; + public Folder $folder; /** * Create a new event instance. * @var Folder[] $folders + * * @return void */ - public function __construct($folders) { + public function __construct(array $folders) { $this->folder = $folders[0]; } } diff --git a/src/Events/MessageMovedEvent.php b/src/Events/MessageMovedEvent.php index 261ea019..ace645d7 100644 --- a/src/Events/MessageMovedEvent.php +++ b/src/Events/MessageMovedEvent.php @@ -22,16 +22,18 @@ class MessageMovedEvent extends Event { /** @var Message $old_message */ - public $old_message; + public Message $old_message; + /** @var Message $new_message */ - public $new_message; + public Message $new_message; /** * Create a new event instance. * @var Message[] $messages + * * @return void */ - public function __construct($messages) { + public function __construct(array $messages) { $this->old_message = $messages[0]; $this->new_message = $messages[1]; } diff --git a/src/Events/MessageNewEvent.php b/src/Events/MessageNewEvent.php index 1487e28d..38892c49 100644 --- a/src/Events/MessageNewEvent.php +++ b/src/Events/MessageNewEvent.php @@ -22,14 +22,15 @@ class MessageNewEvent extends Event { /** @var Message $message */ - public $message; + public Message $message; /** * Create a new event instance. * @var Message[] $messages + * * @return void */ - public function __construct($messages) { + public function __construct(array $messages) { $this->message = $messages[0]; } } diff --git a/src/Query/Query.php b/src/Query/Query.php index 6de567a0..39e1572a 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -41,46 +41,46 @@ class Query { /** @var Collection $query */ - protected $query; + protected Collection $query; /** @var string $raw_query */ - protected $raw_query; + protected string $raw_query; /** @var string[] $extensions */ - protected $extensions; + protected array $extensions; /** @var Client $client */ - protected $client; + protected Client $client; - /** @var int $limit */ - protected $limit = null; + /** @var ?int $limit */ + protected ?int $limit = null; /** @var int $page */ - protected $page = 1; + protected int $page = 1; - /** @var int $fetch_options */ - protected $fetch_options = null; + /** @var ?int $fetch_options */ + protected ?int $fetch_options = null; - /** @var int $fetch_body */ - protected $fetch_body = true; + /** @var boolean $fetch_body */ + protected bool $fetch_body = true; - /** @var int $fetch_flags */ - protected $fetch_flags = true; + /** @var boolean $fetch_flags */ + protected bool $fetch_flags = true; /** @var int|string $sequence */ - protected $sequence = IMAP::NIL; + protected mixed $sequence = IMAP::NIL; /** @var string $fetch_order */ - protected $fetch_order; + protected string $fetch_order; /** @var string $date_format */ - protected $date_format; + protected string $date_format; /** @var bool $soft_fail */ - protected $soft_fail = false; + protected bool $soft_fail = false; /** @var array $errors */ - protected $errors = []; + protected array $errors = []; /** * Query constructor. @@ -110,7 +110,7 @@ public function __construct(Client $client, array $extensions = []) { /** * Instance boot method for additional functionality */ - protected function boot() { + protected function boot(): void { } /** @@ -119,7 +119,7 @@ protected function boot() { * * @return string */ - protected function parse_value($value): string { + protected function parse_value(mixed $value): string { if ($value instanceof Carbon) { $value = $value->format($this->date_format); } @@ -129,12 +129,12 @@ protected function parse_value($value): string { /** * Check if a given date is a valid carbon object and if not try to convert it - * @param string|Carbon $date + * @param mixed $date * * @return Carbon * @throws MessageSearchValidationException */ - protected function parse_date($date): Carbon { + protected function parse_date(mixed $date): Carbon { if ($date instanceof Carbon) return $date; try { @@ -252,7 +252,7 @@ protected function fetch(Collection $available_messages): array { * @throws GetMessagesFailedException * @throws ReflectionException */ - protected function make(int $uid, int $msglist, string $header, string $content, array $flags) { + protected function make(int $uid, int $msglist, string $header, string $content, array $flags): ?Message { try { return Message::make($uid, $msglist, $this->getClient(), $header, $content, $flags, $this->getFetchOptions(), $this->sequence); } catch (RuntimeException|MessageFlagException|InvalidMessageDateException|MessageContentFetchingException $e) { @@ -362,7 +362,7 @@ public function get(): MessageCollection { * @throws ReflectionException * @throws RuntimeException */ - public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1) { + public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1): void { $available_messages = $this->search(); if (($available_messages_count = $available_messages->count()) > 0) { $old_limit = $this->limit; @@ -410,8 +410,8 @@ public function paginate(int $per_page = 5, $page = null, string $page_name = 'i /** * Get a new Message instance * @param int $uid - * @param int|null $msglist - * @param int|string|null $sequence + * @param null $msglist + * @param null $sequence * * @return Message * @throws ConnectionFailedException @@ -430,7 +430,7 @@ public function getMessage(int $uid, $msglist = null, $sequence = null): Message /** * Get a message by its message number * @param $msgn - * @param int|null $msglist + * @param null $msglist * * @return Message * @throws ConnectionFailedException @@ -603,7 +603,7 @@ public function setSequence(int $sequence): Query { * * @return int|string */ - public function getSequence() { + public function getSequence(): int|string { return $this->sequence; } @@ -694,9 +694,9 @@ public function setClient(Client $client): Query { /** * Get the set fetch limit - * @return int + * @return ?int */ - public function getLimit() { + public function getLimit(): ?int { return $this->limit; } @@ -743,16 +743,16 @@ public function fetchOptions(int $fetch_options): Query { } /** - * @return int + * @return ?int */ - public function getFetchOptions() { + public function getFetchOptions(): ?int { return $this->fetch_options; } /** * @return boolean */ - public function getFetchBody() { + public function getFetchBody(): bool { return $this->fetch_body; } @@ -774,17 +774,17 @@ public function fetchBody(bool $fetch_body): Query { } /** - * @return int + * @return bool */ - public function getFetchFlags() { + public function getFetchFlags(): bool { return $this->fetch_flags; } /** - * @param int $fetch_flags + * @param bool $fetch_flags * @return Query */ - public function setFetchFlags(int $fetch_flags): Query { + public function setFetchFlags(bool $fetch_flags): Query { $this->fetch_flags = $fetch_flags; return $this; } @@ -879,7 +879,7 @@ public function getSoftFail(): bool { * * @throws GetMessagesFailedException */ - protected function handleException(int $uid) { + protected function handleException(int $uid): void { if ($this->soft_fail === false && $this->hasError($uid)) { $error = $this->getError($uid); throw new GetMessagesFailedException($error->getMessage(), 0, $error); @@ -891,17 +891,17 @@ protected function handleException(int $uid) { * @param integer $uid * @param Exception $error */ - protected function setError(int $uid, Exception $error) { + protected function setError(int $uid, Exception $error): void { $this->errors[$uid] = $error; } /** * Check if there are any errors / exceptions present * @return boolean - * @var integer|null $uid + * @var ?integer $uid * */ - public function hasErrors($uid = null): bool { + public function hasErrors(?int $uid = null): bool { if ($uid !== null) { return $this->hasError($uid); } @@ -942,17 +942,17 @@ public function getErrors(): array { * @var integer $uid * */ - public function error(int $uid) { + public function error(int $uid): ?Exception { return $this->getError($uid); } /** * Get a specific error / exception - * @return Exception|null + * @return ?Exception * @var integer $uid * */ - public function getError(int $uid) { + public function getError(int $uid): ?Exception { if ($this->hasError($uid)) { return $this->errors[$uid]; } diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 5c9340fc..b9903aca 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -58,7 +58,7 @@ class WhereQuery extends Query { /** * @var array $available_criteria */ - protected $available_criteria = [ + protected array $available_criteria = [ 'OR', 'AND', 'ALL', 'ANSWERED', 'BCC', 'BEFORE', 'BODY', 'CC', 'DELETED', 'FLAGGED', 'FROM', 'KEYWORD', 'NEW', 'NOT', 'OLD', 'ON', 'RECENT', 'SEEN', 'SINCE', 'SUBJECT', 'TEXT', 'TO', @@ -74,7 +74,7 @@ class WhereQuery extends Query { * @throws InvalidWhereQueryCriteriaException * @throws MethodNotFoundException */ - public function __call(string $name, $arguments) { + public function __call(string $name, ?array $arguments) { $that = $this; $name = Str::camel($name); @@ -119,7 +119,7 @@ protected function validate_criteria($criteria): string { /** * Register search parameters * @param mixed $criteria - * @param null $value + * @param mixed $value * * @return $this * @throws InvalidWhereQueryCriteriaException @@ -132,7 +132,7 @@ protected function validate_criteria($criteria): string { * $query->where(["FROM" => "someone@email.tld", "SEEN"]); * $query->where("FROM", "someone@email.tld")->where("SEEN"); */ - public function where($criteria, $value = null): WhereQuery { + public function where(mixed $criteria, mixed $value = null): WhereQuery { if (is_array($criteria)) { foreach ($criteria as $key => $value) { if (is_numeric($key)) { @@ -155,11 +155,11 @@ public function where($criteria, $value = null): WhereQuery { * * @throws InvalidWhereQueryCriteriaException */ - protected function push_search_criteria(string $criteria, $value){ + protected function push_search_criteria(string $criteria, mixed $value){ $criteria = $this->validate_criteria($criteria); $value = $this->parse_value($value); - if ($value === null || $value === '') { + if ($value === '') { $this->query->push([$criteria]); } else { $this->query->push([$criteria, $value]); @@ -167,7 +167,7 @@ protected function push_search_criteria(string $criteria, $value){ } /** - * @param Closure $closure + * @param Closure|null $closure * * @return $this */ @@ -179,7 +179,7 @@ public function orWhere(Closure $closure = null): WhereQuery { } /** - * @param Closure $closure + * @param Closure|null $closure * * @return $this */ @@ -222,7 +222,7 @@ public function whereBcc(string $value): WhereQuery { * @throws InvalidWhereQueryCriteriaException * @throws MessageSearchValidationException */ - public function whereBefore($value): WhereQuery { + public function whereBefore(mixed $value): WhereQuery { $date = $this->parse_date($value); return $this->where('BEFORE', $date); } @@ -316,7 +316,7 @@ public function whereOld(): WhereQuery { * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereOn($value): WhereQuery { + public function whereOn(mixed $value): WhereQuery { $date = $this->parse_date($value); return $this->where('ON', $date); } @@ -344,7 +344,7 @@ public function whereSeen(): WhereQuery { * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereSince($value): WhereQuery { + public function whereSince(mixed $value): WhereQuery { $date = $this->parse_date($value); return $this->where('SINCE', $date); } @@ -489,7 +489,7 @@ public function whereLanguage($country_code): WhereQuery { * @return WhereQuery * @throws InvalidWhereQueryCriteriaException */ - public function whereUid($uid): WhereQuery { + public function whereUid(int|string $uid): WhereQuery { return $this->where('UID', $uid); } @@ -513,9 +513,9 @@ public function whereUidIn(array $uids): WhereQuery { * @param mixed $value * @param callable $callback * @param callable|null $default - * @return $this|mixed + * @return $this|null */ - public function when($value, callable $callback, $default = null) { + public function when(mixed $value, callable $callback, ?callable $default = null): mixed { if ($value) { return $callback($this, $value) ?: $this; } elseif ($default) { @@ -534,7 +534,7 @@ public function when($value, callable $callback, $default = null) { * @param callable|null $default * @return $this|mixed */ - public function unless($value, callable $callback, $default = null) { + public function unless(mixed $value, callable $callback, ?callable $default = null): mixed { if (!$value) { return $callback($this, $value) ?: $this; } elseif ($default) { diff --git a/src/Support/Masks/AttachmentMask.php b/src/Support/Masks/AttachmentMask.php index 1f9855e8..d79b948a 100644 --- a/src/Support/Masks/AttachmentMask.php +++ b/src/Support/Masks/AttachmentMask.php @@ -22,14 +22,14 @@ class AttachmentMask extends Mask { /** @var Attachment $parent */ - protected $parent; + protected mixed $parent; /** * Get the attachment content base64 encoded * * @return string|null */ - public function getContentBase64Encoded() { + public function getContentBase64Encoded(): ?string { return base64_encode($this->parent->content); } @@ -38,7 +38,7 @@ public function getContentBase64Encoded() { * * @return string|null */ - public function getImageSrc() { + public function getImageSrc(): ?string { return 'data:'.$this->parent->content_type.';base64,'.$this->getContentBase64Encoded(); } } \ No newline at end of file diff --git a/src/Support/Masks/Mask.php b/src/Support/Masks/Mask.php index 1ab17d5a..2101f574 100755 --- a/src/Support/Masks/Mask.php +++ b/src/Support/Masks/Mask.php @@ -27,14 +27,14 @@ class Mask { * * @var array $attributes */ - protected $attributes = []; + protected array $attributes = []; /** * Parent instance * - * @var object $parent + * @var mixed $parent */ - protected $parent; + protected mixed $parent; /** * Mask constructor. @@ -53,7 +53,7 @@ public function __construct($parent) { /** * Boot method made to be used by any custom mask */ - protected function boot(){} + protected function boot(): void {} /** * Call dynamic attribute setter and getter methods and inherit the parent calls @@ -119,9 +119,9 @@ public function __get($name) { /** * Get the parent instance * - * @return object + * @return mixed */ - public function getParent(){ + public function getParent(): mixed { return $this->parent; } diff --git a/src/Support/Masks/MessageMask.php b/src/Support/Masks/MessageMask.php index d072e8b6..4cc3d5c0 100644 --- a/src/Support/Masks/MessageMask.php +++ b/src/Support/Masks/MessageMask.php @@ -23,7 +23,7 @@ class MessageMask extends Mask { /** @var Message $parent */ - protected $parent; + protected mixed $parent; /** * Get the message html body @@ -44,15 +44,15 @@ public function getHtmlBody(){ /** * Get the Message html body filtered by an optional callback - * @param callable|bool $callback + * @param callable|null $callback * * @return string|null */ - public function getCustomHTMLBody($callback = false) { + public function getCustomHTMLBody(?callable $callback = null): ?string { $body = $this->getHtmlBody(); if($body === null) return null; - if ($callback !== false) { + if ($callback !== null) { $aAttachment = $this->parent->getAttachments(); $aAttachment->each(function($oAttachment) use(&$body, $callback) { /** @var Attachment $oAttachment */ @@ -73,7 +73,7 @@ public function getCustomHTMLBody($callback = false) { * * @return string|null */ - public function getHTMLBodyWithEmbeddedBase64Images() { + public function getHTMLBodyWithEmbeddedBase64Images(): ?string { return $this->getCustomHTMLBody(function($body, $oAttachment){ /** @var Attachment $oAttachment */ if ($oAttachment->id) { diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 292afd0a..27e7fd0d 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -28,7 +28,7 @@ class PaginatedCollection extends Collection { * * @var int $total */ - protected $total; + protected int $total; /** * Paginate the current collection. @@ -39,7 +39,7 @@ class PaginatedCollection extends Collection { * * @return LengthAwarePaginator */ - public function paginate(int $per_page = 15, $page = null, string $page_name = 'page', bool $prepaginated = false): LengthAwarePaginator { + public function paginate(int $per_page = 15, ?int $page = null, string $page_name = 'page', bool $prepaginated = false): LengthAwarePaginator { $page = $page ?: Paginator::resolveCurrentPage($page_name); $total = $this->total ?: $this->count(); @@ -57,12 +57,12 @@ public function paginate(int $per_page = 15, $page = null, string $page_name = ' * @param array $items * @param int $total * @param int $per_page - * @param int|null $current_page + * @param int|null $current_page * @param array $options * * @return LengthAwarePaginator */ - protected function paginator(array $items, int $total, int $per_page, $current_page, array $options): LengthAwarePaginator { + protected function paginator(array $items, int $total, int $per_page, ?int $current_page, array $options): LengthAwarePaginator { return new LengthAwarePaginator($items, $total, $per_page, $current_page, $options); } @@ -72,7 +72,7 @@ protected function paginator(array $items, int $total, int $per_page, $current_p * * @return int|null */ - public function total($total = null) { + public function total($total = null): ?int { if($total === null) { return $this->total; } diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index 56a60915..3b6902ed 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -28,15 +28,15 @@ trait HasEvents { * * @var array $events */ - protected $events = []; + protected array $events = []; /** * Set a specific event - * @param $section - * @param $event - * @param $class + * @param string $section + * @param string $event + * @param mixed $class */ - public function setEvent($section, $event, $class) { + public function setEvent(string $section, string $event, mixed $class): void { if (isset($this->events[$section])) { $this->events[$section][$event] = $class; } @@ -44,21 +44,21 @@ public function setEvent($section, $event, $class) { /** * Set all events - * @param $events + * @param array $events */ - public function setEvents($events) { + public function setEvents(array $events): void { $this->events = $events; } /** * Get a specific event callback - * @param $section - * @param $event + * @param string $section + * @param string $event * * @return Event|string * @throws EventNotFoundException */ - public function getEvent($section, $event) { + public function getEvent(string $section, string $event): Event|string { if (isset($this->events[$section])) { return $this->events[$section][$event]; } From 35071bae09e79892fa0bfa229fdc59e1a659459d Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 02:59:55 +0100 Subject: [PATCH 363/600] Static mask config accessor added `ClientManager::getMask()` added --- src/ClientManager.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ClientManager.php b/src/ClientManager.php index fd136600..67160b3c 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -95,6 +95,22 @@ public static function get(string $key, $default = null): mixed { return $value === null ? $default : $value; } + /** + * Get the mask for a given section + * @param string $section section name such as "message" or "attachment" + * + * @return string|null + */ + public static function getMask(string $section): ?string { + $default_masks = ClientManager::get("masks"); + if (isset($default_masks[$section])) { + if (class_exists($default_masks[$section])) { + return $default_masks[$section]; + } + } + return null; + } + /** * Resolve a account instance. * @param string|null $name From 581db7b0c9333957ac1e3e3e650abc39f684489a Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:00:55 +0100 Subject: [PATCH 364/600] Load the default mask from the shared default configuration if the message client isn't set --- src/Attachment.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 4ef7b17e..dcde4329 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -102,9 +102,16 @@ public function __construct(Message $oMessage, Part $part) { $this->part = $part; $this->part_number = $part->part_number; - $default_mask = $this->oMessage->getClient()->getDefaultAttachmentMask(); - if($default_mask != null) { - $this->mask = $default_mask; + if ($this->oMessage->getClient()) { + $default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask(); + if($default_mask != null) { + $this->mask = $default_mask; + } + }else{ + $default_mask = ClientManager::getMask("attachment"); + if($default_mask != ""){ + $this->mask =$default_mask; + } } $this->findType(); From 50902ca901bf13a7945ec4d10641003251f3c2de Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:02:14 +0100 Subject: [PATCH 365/600] Get the extension from the filename if no mimetype detection library is available --- src/Attachment.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Attachment.php b/src/Attachment.php index dcde4329..ff280b1b 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -281,7 +281,8 @@ public function getExtension(): ?string { return $deprecated_guesser::getInstance()->guess($this->getMimeType()); } - return null; + $extensions = explode(".", $this->part->filename ?: $this->part->name); + return end($extensions); } /** From 94bcd6f352035c100989388ef9b542e62f32a32b Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:04:35 +0100 Subject: [PATCH 366/600] Array accessors added --- src/Attribute.php | 169 ++++++++++++++++++++++++++++++---------------- 1 file changed, 112 insertions(+), 57 deletions(-) diff --git a/src/Attribute.php b/src/Attribute.php index fc7a40a9..7e77a469 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -14,7 +14,6 @@ use ArrayAccess; use Carbon\Carbon; -use ReturnTypeWillChange; /** * Class Attribute @@ -43,6 +42,26 @@ public function __construct(string $name, mixed $value = null) { $this->add($value); } + /** + * Handle class invocation calls + * + * @return array|string + */ + public function __invoke(): array|string { + if ($this->count() > 1) { + return $this->toArray(); + } + return $this->toString(); + } + + /** + * Return the serialized address + * + * @return array + */ + public function __serialize(){ + return $this->values; + } /** * Return the stringified attribute @@ -68,7 +87,7 @@ public function toString(): string { * @return array */ public function toArray(): array { - return $this->values; + return $this->__serialize(); } /** @@ -84,51 +103,72 @@ public function toDate(): Carbon { } /** - * Determine if a value exists at an offset. + * Determine if a value exists at a given key. * - * @param mixed $offset + * @param int|string $key * @return bool */ - public function offsetExists($offset): bool { - return array_key_exists($offset, $this->values); + public function has(mixed $key = 0): bool { + return array_key_exists($key, $this->values); } /** - * Get a value at a given offset. + * Determine if a value exists at a given key. * - * @param mixed $offset + * @param int|string $key + * @return bool + */ + public function exist(mixed $key = 0): bool { + return $this->has($key); + } + + /** + * Check if the attribute contains the given value + * @param mixed $value + * + * @return bool + */ + public function contains(mixed $value): bool { + return in_array($value, $this->values, true); + } + + /** + * Get a value by a given key. + * + * @param int|string $key * @return mixed */ - #[ReturnTypeWillChange] - public function offsetGet($offset) { - return $this->values[$offset]; + public function get(int|string $key = 0): mixed { + return $this->values[$key] ?? null; } /** - * Set the value at a given offset. + * Set the value by a given key. * - * @param mixed $offset - * @param mixed $value - * @return void + * @param mixed $key + * @param mixed $value + * @return Attribute */ - #[ReturnTypeWillChange] - public function offsetSet($offset, $value) { - if (is_null($offset)) { + public function set(mixed $value, mixed $key = 0): Attribute { + if (is_null($key)) { $this->values[] = $value; } else { - $this->values[$offset] = $value; + $this->values[$key] = $value; } + return $this; } /** - * Unset the value at a given offset. + * Unset a value by a given key. * - * @param string $offset - * @return void + * @param int|string $key + * @return Attribute */ - #[ReturnTypeWillChange] - public function offsetUnset($offset) { - unset($this->values[$offset]); + public function remove(int|string $key = 0): Attribute { + if (isset($this->values[$key])) { + unset($this->values[$key]); + } + return $this; } /** @@ -138,7 +178,7 @@ public function offsetUnset($offset) { * * @return Attribute */ - public function add($value, bool $strict = false): Attribute { + public function add(mixed $value, bool $strict = false): Attribute { if (is_array($value)) { return $this->merge($value, $strict); }elseif ($value !== null) { @@ -163,22 +203,13 @@ public function merge(array $values, bool $strict = false): Attribute { return $this; } - /** - * Check if the attribute contains the given value - * @param mixed $value - * - * @return bool - */ - public function contains($value): bool { - return in_array($value, $this->values, true); - } - /** * Attach a given value to the current value array * @param $value * @param bool $strict + * @return Attribute */ - public function attach($value, bool $strict = false) { + public function attach($value, bool $strict = false): Attribute { if ($strict === true) { if ($this->contains($value) === false) { $this->values[] = $value; @@ -186,6 +217,7 @@ public function attach($value, bool $strict = false) { }else{ $this->values[] = $value; } + return $this; } /** @@ -214,17 +246,9 @@ public function getName(): string { * * @return array */ - public function get(): array { - return $this->values; - } - - /** - * Alias method for self::get() - * - * @return array - */ public function all(): array { - return $this->get(); + reset($this->values); + return $this->values; } /** @@ -232,11 +256,8 @@ public function all(): array { * * @return mixed|null */ - public function first(){ - if ($this->offsetExists(0)) { - return $this->values[0]; - } - return null; + public function first(): mixed { + return reset($this->values); } /** @@ -244,11 +265,8 @@ public function first(){ * * @return mixed|null */ - public function last(){ - if (($cnt = $this->count()) > 0) { - return $this->values[$cnt - 1]; - } - return null; + public function last(): mixed { + return end($this->values); } /** @@ -259,4 +277,41 @@ public function last(){ public function count(): int { return count($this->values); } + + /** + * @see ArrayAccess::offsetExists + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool { + return $this->has($offset); + } + + /** + * @see ArrayAccess::offsetGet + * @param mixed $offset + * @return mixed + */ + public function offsetGet(mixed $offset): mixed { + return $this->get($offset); + } + + /** + * @see ArrayAccess::offsetSet + * @param mixed $offset + * @param mixed $value + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void { + $this->set($value, $offset); + } + + /** + * @see ArrayAccess::offsetUnset + * @param mixed $offset + * @return void + */ + public function offsetUnset(mixed $offset): void { + $this->remove($offset); + } } \ No newline at end of file From d4f8541acb40c5e242c3b0b8d9779bbaa8ccf6d2 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:06:13 +0100 Subject: [PATCH 367/600] Get the current client account configuration --- src/Client.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Client.php b/src/Client.php index 1a101168..1766e493 100755 --- a/src/Client.php +++ b/src/Client.php @@ -204,6 +204,19 @@ public function setConfig(array $config): Client { return $this; } + /** + * Get the current config + * + * @return array + */ + public function getConfig(): array { + $config = []; + foreach($this->default_account_config as $key => $value) { + $config[$key] = $this->$key; + } + return $config; + } + /** * Set a specific account config * @param string $key @@ -220,6 +233,21 @@ private function setAccountConfig(string $key, array $config, array $default_con $this->$key = $value; } + /** + * Get the current account config + * + * @return array + */ + public function getAccountConfig(): array { + $config = []; + foreach($this->default_account_config as $key => $value) { + if(property_exists($this, $key)) { + $config[$key] = $this->$key; + } + } + return $config; + } + /** * Look for a possible events in any available config * @param $config From aa0989295598972bf1339c9c2619e068c0901290 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:06:45 +0100 Subject: [PATCH 368/600] Clone a client instance --- src/Client.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Client.php b/src/Client.php index 1766e493..a4192336 100755 --- a/src/Client.php +++ b/src/Client.php @@ -187,6 +187,26 @@ public function __destruct() { $this->disconnect(); } + /** + * Clone the current Client instance + * + * @return Client + */ + public function clone(): Client { + $client = new self(); + $client->events = $this->events; + $client->timeout = $this->timeout; + $client->active_folder = $this->active_folder; + $client->default_account_config = $this->default_account_config; + $config = $this->getAccountConfig(); + foreach($config as $key => $value) { + $client->setAccountConfig($key, $config, $this->default_account_config); + } + $client->default_message_mask = $this->default_message_mask; + $client->default_attachment_mask = $this->default_message_mask; + return $client; + } + /** * Set the Client configuration * @param array $config From 021402a0f394676d82c265d20942cf4a7aadf2b8 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:07:44 +0100 Subject: [PATCH 369/600] Strict attribute and return types introduced where ever possible --- src/Client.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Client.php b/src/Client.php index a4192336..709e128e 100755 --- a/src/Client.php +++ b/src/Client.php @@ -54,7 +54,7 @@ class Client { * * @var int */ - public $port; + public int $port; /** * Service protocol. @@ -287,8 +287,7 @@ protected function setEventsFromConfig($config): void { * * @throws MaskNotFoundException */ - protected function setMaskFromConfig($config) { - $default_config = ClientManager::get("masks"); + protected function setMaskFromConfig($config): void { if(isset($config['masks'])){ if(isset($config['masks']['message'])) { From 0c53c6ab966d301a980791af712194f03e874806 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:26:17 +0100 Subject: [PATCH 370/600] Protocol Response class introduced to handle and unify all protocol requests --- src/Client.php | 177 ++++-- src/Connection/Protocols/ImapProtocol.php | 370 ++++++------ src/Connection/Protocols/LegacyProtocol.php | 553 +++++++++++------- .../Protocols/ProtocolInterface.php | 137 +++-- src/Connection/Protocols/Response.php | 372 ++++++++++++ src/Exceptions/ResponseException.php | 91 +++ src/Folder.php | 162 +++-- src/Message.php | 290 ++++++--- src/Query/Query.php | 128 +++- 9 files changed, 1623 insertions(+), 657 deletions(-) create mode 100644 src/Connection/Protocols/Response.php create mode 100644 src/Exceptions/ResponseException.php diff --git a/src/Client.php b/src/Client.php index 709e128e..dd7f1213 100755 --- a/src/Client.php +++ b/src/Client.php @@ -15,13 +15,17 @@ use ErrorException; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Connection\Protocols\LegacyProtocol; -use Webklex\PHPIMAP\Connection\Protocols\Protocol; use Webklex\PHPIMAP\Connection\Protocols\ProtocolInterface; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\FolderFetchingException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\ProtocolNotSupportedException; +use Webklex\PHPIMAP\Exceptions\ResponseException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Support\FolderCollection; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; use Webklex\PHPIMAP\Support\Masks\MessageMask; @@ -182,6 +186,10 @@ public function __construct(array $config = []) { /** * Client destructor + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException */ public function __destruct() { $this->disconnect(); @@ -335,8 +343,13 @@ protected function setMaskFromConfig($config): void { /** * Get the current imap resource * - * @return bool|Protocol|ProtocolInterface + * @return ProtocolInterface * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getConnection(): ProtocolInterface { $this->checkConnection(); @@ -356,6 +369,11 @@ public function isConnected(): bool { * Determine if connection was established and connect if not. * * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function checkConnection() { if (!$this->isConnected()) { @@ -367,6 +385,11 @@ public function checkConnection() { * Force the connection to reconnect * * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function reconnect(): void { if ($this->isConnected()) { @@ -380,6 +403,11 @@ public function reconnect(): void { * * @return $this * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function connect(): Client { $this->disconnect(); @@ -421,19 +449,18 @@ public function connect(): Client { /** * Authenticate the current session * - * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException */ - protected function authenticate() { - try { - if ($this->authentication == "oauth") { - if (!$this->connection->authenticate($this->username, $this->password)) { - throw new AuthFailedException(); - } - } elseif (!$this->connection->login($this->username, $this->password)) { + protected function authenticate(): void { + if ($this->authentication == "oauth") { + if (!$this->connection->authenticate($this->username, $this->password)->validatedData()) { throw new AuthFailedException(); } - } catch (AuthFailedException $e) { - throw new ConnectionFailedException("connection setup failed", 0, $e); + } elseif (!$this->connection->login($this->username, $this->password)->validatedData()) { + throw new AuthFailedException(); } } @@ -441,9 +468,12 @@ protected function authenticate() { * Disconnect from server. * * @return $this + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException */ public function disconnect(): Client { - if ($this->isConnected() && $this->connection !== false) { + if ($this->isConnected()) { $this->connection->logout(); } $this->active_folder = null; @@ -457,9 +487,13 @@ public function disconnect(): Client { * @param string|null $delimiter * * @return Folder|null - * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getFolder(string $folder_name, ?string $delimiter = null): ?Folder { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) @@ -476,9 +510,13 @@ public function getFolder(string $folder_name, ?string $delimiter = null): ?Fold * @param $folder_name * * @return Folder|null - * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getFolderByName($folder_name): ?Folder { return $this->getFolders(false)->where("name", $folder_name)->first(); @@ -489,9 +527,13 @@ public function getFolderByName($folder_name): ?Folder { * @param $folder_path * * @return Folder|null - * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getFolderByPath($folder_path): ?Folder { return $this->getFolders(false)->where("path", $folder_path)->first(); @@ -507,16 +549,20 @@ public function getFolderByPath($folder_path): ?Folder { * @return FolderCollection * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getFolders(bool $hierarchical = true, string $parent_folder = null): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); $pattern = $parent_folder.($hierarchical ? '%' : '*'); - $items = $this->connection->folders('', $pattern); + $items = $this->connection->folders('', $pattern)->validatedData(); - if(is_array($items)){ + if(!empty($items)){ foreach ($items as $folder_name => $item) { $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); @@ -544,18 +590,22 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * @param string|null $parent_folder * * @return FolderCollection - * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); $pattern = $parent_folder.($hierarchical ? '%' : '*'); - $items = $this->connection->folders('', $pattern); + $items = $this->connection->folders('', $pattern)->validatedData(); - if(is_array($items)){ + if(!empty($items)){ foreach ($items as $folder_name => $item) { $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); @@ -581,9 +631,13 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f * @param string $folder_path * @param boolean $force_select * - * @return array|bool + * @return array * @throws ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function openFolder(string $folder_path, bool $force_select = false): array { if ($this->active_folder == $folder_path && $this->isConnected() && $force_select === false) { @@ -591,7 +645,7 @@ public function openFolder(string $folder_path, bool $force_select = false): arr } $this->checkConnection(); $this->active_folder = $folder_path; - return $this->connection->selectFolder($folder_path); + return $this->connection->selectFolder($folder_path)->validatedData(); } /** @@ -601,17 +655,21 @@ public function openFolder(string $folder_path, bool $force_select = false): arr * * @return Folder * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws EventNotFoundException * @throws FolderFetchingException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\RuntimeException + * @throws ResponseException */ public function createFolder(string $folder_path, bool $expunge = true): Folder { $this->checkConnection(); - $status = $this->connection->createFolder($folder); + $status = $this->connection->createFolder($folder_path)->validatedData(); if($expunge) $this->expunge(); - $folder = $this->getFolderByPath($folder); + $folder = $this->getFolderByPath($folder_path); if($status && $folder) { $event = $this->getEvent("folder", "new"); $event::dispatch($folder); @@ -626,11 +684,15 @@ public function createFolder(string $folder_path, bool $expunge = true): Folder * * @return array * @throws ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function checkFolder(string $folder_path): array { $this->checkConnection(); - return $this->connection->examineFolder($folder); + return $this->connection->examineFolder($folder_path)->validatedData(); } /** @@ -650,11 +712,15 @@ public function getFolderPath(): string { * @return array * * @throws ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function Id(array $ids = null): array { $this->checkConnection(); - return $this->connection->ID($ids); + return $this->connection->ID($ids)->validatedData(); } /** @@ -662,11 +728,15 @@ public function Id(array $ids = null): array { * * @return array * @throws ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getQuota(): array { $this->checkConnection(); - return $this->connection->getQuota($this->username); + return $this->connection->getQuota($this->username)->validatedData(); } /** @@ -675,10 +745,15 @@ public function getQuota(): array { * * @return array * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getQuotaRoot(string $quota_root = 'INBOX'): array { $this->checkConnection(); - return $this->connection->getQuotaRoot($quota_root); + return $this->connection->getQuotaRoot($quota_root)->validatedData(); } /** @@ -686,21 +761,30 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): array { * * @return array * @throws ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function expunge(): array { $this->checkConnection(); - return $this->connection->expunge(); + return $this->connection->expunge()->validatedData(); } /** * Set the connection timeout * @param integer $timeout * - * @return Protocol + * @return ProtocolInterface * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ - public function setTimeout(int $timeout): Protocol { + public function setTimeout(int $timeout): ProtocolInterface { $this->timeout = $timeout; if ($this->isConnected()) { $this->connection->setConnectionTimeout($timeout); @@ -714,6 +798,11 @@ public function setTimeout(int $timeout): Protocol { * * @return int * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getTimeout(): int { $this->checkConnection(); diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 8792e9a8..2e8edf43 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -19,6 +19,7 @@ use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; +use Webklex\PHPIMAP\Exceptions\ResponseException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Header; use Webklex\PHPIMAP\IMAP; @@ -75,8 +76,9 @@ public function connect(string $host, int $port = null): bool { } $port = $port === null ? 143 : $port; try { + $response = new Response(0, $this->debug); $this->stream = $this->createStream($transport, $host, $port, $this->connection_timeout); - if (!$this->assumedNextLine('* OK')) { + if (!$this->stream || !$this->assumedNextLine($response, '* OK')) { throw new ConnectionFailedException('connection refused'); } if ($encryption == 'starttls') { @@ -96,9 +98,9 @@ public function connect(string $host, int $port = null): bool { * @throws ImapServerErrorException * @throws RuntimeException */ - protected function enableStartTls(){ + protected function enableStartTls() { $response = $this->requestAndResponse('STARTTLS'); - $result = $response && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); + $result = $response->successful() && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); if (!$result) { throw new ConnectionFailedException('failed to enable TLS'); } @@ -110,7 +112,7 @@ protected function enableStartTls(){ * @return string next line * @throws RuntimeException */ - public function nextLine(): string { + public function nextLine(Response $response): string { $line = ""; while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { $line .= $next_char; @@ -118,19 +120,22 @@ public function nextLine(): string { if ($line === "" && $next_char === false) { throw new RuntimeException('empty response'); } - if ($this->debug) echo "<< ".$line."\n"; - return $line . "\n"; + $line .= "\n"; + $response->addResponse($line); + if ($this->debug) echo "<< " . $line; + return $line; } /** * Get the next line and check if it starts with a given string + * @param Response $response * @param string $start * * @return bool * @throws RuntimeException */ - protected function assumedNextLine(string $start): bool { - return str_starts_with($this->nextLine(), $start); + protected function assumedNextLine(Response $response, string $start): bool { + return str_starts_with($this->nextLine($response), $start); } /** @@ -140,8 +145,8 @@ protected function assumedNextLine(string $start): bool { * @return string next line * @throws RuntimeException */ - protected function nextTaggedLine(&$tag): string { - $line = $this->nextLine(); + protected function nextTaggedLine(Response $response, ?string &$tag): string { + $line = $this->nextLine($response); list($tag, $line) = explode(' ', $line, 2); return $line; @@ -149,25 +154,26 @@ protected function nextTaggedLine(&$tag): string { /** * Get the next line and check if it contains a given string and split the tag + * @param Response $response * @param string $start * @param $tag * * @return bool * @throws RuntimeException */ - protected function assumedNextTaggedLine(string $start, &$tag): bool { - $line = $this->nextTaggedLine($tag); - return strpos($line, $start) !== false; + protected function assumedNextTaggedLine(Response $response, string $start, &$tag): bool { + return str_contains($this->nextTaggedLine($response, $tag), $start); } /** * Split a given line in values. A value is literal of any form or a list + * @param Response $response * @param string $line * * @return array * @throws RuntimeException */ - protected function decodeLine(string $line): array { + protected function decodeLine(Response $response, string $line): array { $tokens = []; $stack = []; @@ -246,6 +252,7 @@ protected function decodeLine(string $line): array { /** * Read abd decode a response "line" + * @param Response $response * @param array|string $tokens to decode * @param string $wantedTag targeted tag * @param bool $dontParse if true only the unparsed line is returned in $tokens @@ -253,10 +260,10 @@ protected function decodeLine(string $line): array { * @return bool * @throws RuntimeException */ - public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse = false): bool { - $line = $this->nextTaggedLine($tag); // get next tag + public function readLine(Response $response, array|string &$tokens = [], string $wantedTag = '*', bool $dontParse = false): bool { + $line = $this->nextTaggedLine($response, $tag); // get next tag if (!$dontParse) { - $tokens = $this->decodeLine($line); + $tokens = $this->decodeLine($response, $line); } else { $tokens = $line; } @@ -267,8 +274,8 @@ public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse /** * Read all lines of response until given tag is found - * - * @param string $tag request tag + * @param Response $response + * @param string $tag request tag * @param bool $dontParse if true every line is returned unparsed instead of the decoded tokens * * @return array @@ -277,11 +284,11 @@ public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse * @throws ImapServerErrorException * @throws RuntimeException */ - public function readResponse(string $tag, bool $dontParse = false): array { + public function readResponse(Response $response, string $tag, bool $dontParse = false): array { $lines = []; - $tokens = null; // define $tokens variable before first use + $tokens = ""; // define $tokens variable before first use do { - $readAll = $this->readLine($tokens, $tag, $dontParse); + $readAll = $this->readLine($response, $tokens, $tag, $dontParse); $lines[] = $tokens; } while (!$readAll); @@ -306,9 +313,10 @@ public function readResponse(string $tag, bool $dontParse = false): array { * @param array $tokens additional parameters to command, use escapeString() to prepare * @param string|null $tag provide a tag otherwise an autogenerated is returned * + * @return Response * @throws RuntimeException */ - public function sendRequest(string $command, array $tokens = [], string &$tag = null) { + public function sendRequest(string $command, array $tokens = [], string &$tag = null): Response { if (!$tag) { $this->noun++; $tag = 'TAG' . $this->noun; @@ -316,10 +324,12 @@ public function sendRequest(string $command, array $tokens = [], string &$tag = $line = $tag . ' ' . $command; + $response = new Response($this->noun, $this->debug); + foreach ($tokens as $token) { if (is_array($token)) { - $this->write($line . ' ' . $token[0]); - if (!$this->assumedNextLine('+ ')) { + $this->write($response, $line . ' ' . $token[0]); + if (!$this->assumedNextLine($response, '+ ')) { throw new RuntimeException('failed to send literal string'); } $line = $token[1]; @@ -327,19 +337,26 @@ public function sendRequest(string $command, array $tokens = [], string &$tag = $line .= ' ' . $token; } } - $this->write($line); + $this->write($response, $line); + + return $response; } /** * Write data to the current stream + * @param Response $response * @param string $data + * * @return void * @throws RuntimeException */ - public function write(string $data) { - if ($this->debug) echo ">> ".$data ."\n"; + public function write(Response $response, string $data): void { + $command = $data . "\r\n"; + if ($this->debug) echo ">> " . $command . "\n"; + + $response->addCommand($command); - if (fwrite($this->stream, $data . "\r\n") === false) { + if (fwrite($this->stream, $command) === false) { throw new RuntimeException('failed to write - connection closed?'); } } @@ -348,19 +365,19 @@ public function write(string $data) { * Send a request and get response at once * * @param string $command - * @param array $tokens parameters as in sendRequest() + * @param array $tokens parameters as in sendRequest() * @param bool $dontParse if true unparsed lines are returned instead of tokens * - * @return array response as in readResponse() - * + * @return Response response as in readResponse() * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false): array { - $this->sendRequest($command, $tokens, $tag); + public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false): Response { + $response = $this->sendRequest($command, $tokens, $tag); + $response->setResult($this->readResponse($response, $tag, $dontParse)); - return $this->readResponse($tag, $dontParse); + return $response; } /** @@ -409,12 +426,12 @@ public function escapeList(array $list): string { * @param string $user username * @param string $password password * - * @return array + * @return Response * @throws AuthFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException */ - public function login(string $user, string $password): array { + public function login(string $user, string $password): Response { try { $command = 'LOGIN'; $params = $this->escapeString($user, $password); @@ -430,29 +447,29 @@ public function login(string $user, string $password): array { * @param string $user username * @param string $token access token * - * @return bool + * @return Response * @throws AuthFailedException */ - public function authenticate(string $user, string $token): bool { + public function authenticate(string $user, string $token): Response { try { $authenticateParams = ['XOAUTH2', base64_encode("user=$user\1auth=Bearer $token\1\1")]; - $this->sendRequest('AUTHENTICATE', $authenticateParams); + $response = $this->sendRequest('AUTHENTICATE', $authenticateParams); while (true) { - $response = ""; - $is_plus = $this->readLine($response, '+', true); + $tokens = ""; + $is_plus = $this->readLine($response, $tokens, '+', true); if ($is_plus) { // try to log the challenge somewhere where it can be found error_log("got an extra server challenge: $response"); // respond with an empty response. - $this->sendRequest(''); + $response->stack($this->sendRequest('')); } else { - if (preg_match('/^NO /i', $response) || - preg_match('/^BAD /i', $response)) { - error_log("got failure response: $response"); - return false; - } else if (preg_match("/^OK /i", $response)) { - return true; + if (preg_match('/^NO /i', $tokens) || + preg_match('/^BAD /i', $tokens)) { + error_log("got failure response: $tokens"); + return $response->addError("got failure response: $tokens"); + } else if (preg_match("/^OK /i", $tokens)) { + return $response->setResult(is_array($tokens) ? $tokens : [$tokens]); } } } @@ -464,20 +481,34 @@ public function authenticate(string $user, string $token): bool { /** * Logout of imap server * - * @return array success - * - * @throws ImapBadRequestException - * @throws ImapServerErrorException - * @throws RuntimeException + * @return Response */ - public function logout(): array { + public function logout(): Response { if (!$this->stream) { - throw new RuntimeException('not connected'); + $this->reset(); + return new Response(0, $this->debug); + }elseif ($this->meta()["timed_out"]) { + $this->reset(); + return new Response(0, $this->debug); } - $result = $this->requestAndResponse('LOGOUT', [], true); + $result = null; + try { + $result = $this->requestAndResponse('LOGOUT', [], true); + fclose($this->stream); + } catch (\Throwable) {} - fclose($this->stream); + $this->reset(); + + return $result ?? new Response(0, $this->debug); + } + + /** + * Reset the current stream and uid cache + * + * @return void + */ + public function reset(): void { $this->stream = null; $this->uid_cache = null; @@ -496,22 +527,18 @@ public function connected(): bool { /** * Get an array of available capabilities * - * @return array list of capabilities + * @return Response list of capabilities * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function getCapabilities(): array { + public function getCapabilities(): Response { $response = $this->requestAndResponse('CAPABILITY'); - if (!$response) return []; + if (!$response->getResponse()) return $response; - $capabilities = []; - foreach ($response as $line) { - $capabilities = array_merge($capabilities, $line); - } - return $capabilities; + return $response->setResult($response->validatedData()[0]); } /** @@ -519,15 +546,15 @@ public function getCapabilities(): array { * @param string $command can be 'EXAMINE' or 'SELECT' * @param string $folder target folder * - * @return bool|array + * @return Response * @throws RuntimeException */ - public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'INBOX') { - $this->sendRequest($command, [$this->escapeString($folder)], $tag); + public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'INBOX'): Response { + $response = $this->sendRequest($command, [$this->escapeString($folder)], $tag); $result = []; - $tokens = null; // define $tokens variable before first use - while (!$this->readLine($tokens, $tag)) { + $tokens = []; // define $tokens variable before first use + while (!$this->readLine($response, $tokens, $tag, false)) { if ($tokens[0] == 'FLAGS') { array_shift($tokens); $result['flags'] = $tokens; @@ -555,21 +582,23 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I } } + $response->setResult($result); + if ($tokens[0] != 'OK') { - return false; + $response->addError("request failed"); } - return $result; + return $response; } /** * Change the current folder * @param string $folder change to this folder * - * @return bool|array see examineOrSelect() + * @return Response see examineOrSelect() * @throws RuntimeException */ - public function selectFolder(string $folder = 'INBOX') { - $this->uid_cache = null; + public function selectFolder(string $folder = 'INBOX'): Response { + $this->uid_cache = []; return $this->examineOrSelect('SELECT', $folder); } @@ -578,10 +607,10 @@ public function selectFolder(string $folder = 'INBOX') { * Examine a given folder * @param string $folder examine this folder * - * @return bool|array see examineOrSelect() + * @return Response see examineOrSelect() * @throws RuntimeException */ - public function examineFolder(string $folder = 'INBOX') { + public function examineFolder(string $folder = 'INBOX'): Response { return $this->examineOrSelect('EXAMINE', $folder); } @@ -594,13 +623,13 @@ public function examineFolder(string $folder = 'INBOX') { * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return string|array if only one item of one message is fetched it's returned as string + * @return Response if only one item of one message is fetched it's returned as string * if items of one message are fetched it's returned as (name => value) * if one item of messages are fetched it's returned as (msgno => value) * if items of messages are fetched it's returned as (msgno => (name => value)) * @throws RuntimeException */ - public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { + public function fetch(array|string $items, array|int $from, mixed $to = null, int|string $uid = IMAP::ST_UID): Response { if (is_array($from)) { $set = implode(',', $from); } elseif ($to === null) { @@ -614,10 +643,10 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { $items = (array)$items; $itemList = $this->escapeList($items); - $this->sendRequest($this->buildUIDCommand("FETCH", $uid), [$set, $itemList], $tag); + $response = $this->sendRequest($this->buildUIDCommand("FETCH", $uid), [$set, $itemList], $tag); $result = []; - $tokens = null; // define $tokens variable before first use - while (!$this->readLine($tokens, $tag)) { + $tokens = []; // define $tokens variable before first use + while (!$this->readLine($response, $tokens, $tag)) { // ignore other responses if ($tokens[1] != 'FETCH') { continue; @@ -679,9 +708,8 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { // if we want only one message we can ignore everything else and just return if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { // we still need to read all lines - while (!$this->readLine($tokens, $tag)) - - return $data; + while (!$this->readLine($response, $tokens, $tag)) + return $response->setResult($data); } if ($uid) { $result[$tokens[2][$uidKey]] = $data; @@ -694,7 +722,7 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { throw new RuntimeException('the single id was not found in response'); } - return $result; + return $response->setResult($result); } /** @@ -704,12 +732,11 @@ public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException */ - public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { - $result = $this->fetch(["$rfc.TEXT"], $uids, null, $uid); - return is_array($result) ? $result : []; + public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { + return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); } /** @@ -719,12 +746,11 @@ public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): arr * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException */ - public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array{ - $result = $this->fetch(["$rfc.HEADER"], $uids, null, $uid); - return $result === "" ? [] : $result; + public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { + return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); } /** @@ -733,36 +759,36 @@ public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): arr * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException */ - public function flags($uids, $uid = IMAP::ST_UID): array { - $result = $this->fetch(["FLAGS"], $uids, null, $uid); - return is_array($result) ? $result : []; + public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { + return $this->fetch(["FLAGS"], $uids, null, $uid); } /** * Get uid for a given id * @param int|null $id message number * - * @return array|string message number for given message or all messages as array + * @return Response message number for given message or all messages as array * @throws MessageNotFoundException */ - public function getUid($id = null) { - if (!$this->enable_uid_cache || $this->uid_cache === null || ($this->uid_cache && count($this->uid_cache) <= 0)) { + public function getUid(?int $id = null): Response { + if (!$this->enable_uid_cache || empty($this->uid_cache) || count($this->uid_cache) <= 0) { try { - $this->setUidCache($this->fetch('http://github.com/UID', 1, INF)); // set cache for this folder - } catch (RuntimeException $e) {} + $this->setUidCache((array)$this->fetch('http://github.com/UID', 1, INF)->data()); // set cache for this folder + } catch (RuntimeException) { + } } $uids = $this->uid_cache; if ($id == null) { - return $uids; + return Response::empty($this->debug)->setResult($uids); } foreach ($uids as $k => $v) { if ($k == $id) { - return $v; + return Response::empty($this->debug)->setResult($v); } } @@ -779,14 +805,14 @@ public function getUid($id = null) { * Get a message number for a uid * @param string $id uid * - * @return int message number + * @return Response message number * @throws MessageNotFoundException */ - public function getMessageNumber(string $id): int { + public function getMessageNumber(string $id): Response { $ids = $this->getUid(); foreach ($ids as $k => $v) { if ($v == $id) { - return (int)$k; + return Response::empty($this->debug)->setResult((int)$k); } } @@ -799,28 +825,27 @@ public function getMessageNumber(string $id): int { * @param string $reference mailbox reference for list * @param string $folder mailbox name match with wildcards * - * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) + * @return Response folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function folders(string $reference = '', string $folder = '*'): array { - $result = []; - $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); + public function folders(string $reference = '', string $folder = '*'): Response { + $response = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); + $list = $response->data(); - if ($list[0] === true) { - return $result; - } - - foreach ($list as $item) { - if (count($item) != 4 || $item[0] != 'LIST') { - continue; + $result = []; + if ($list[0] !== true) { + foreach ($list as $item) { + if (count($item) != 4 || $item[0] != 'LIST') { + continue; + } + $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]]; } - $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]]; } - return $result; + return $response->setResult($result); } /** @@ -836,15 +861,15 @@ public function folders(string $reference = '', string $folder = '*'): array { * message numbers instead. * @param null $item command used to store a flag * - * @return array new flags if $silent is false, else true or false depending on success + * @return Response new flags if $silent is false, else true or false depending on success * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ public function store( - array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null - ): array { - $flags = $this->escapeList($flags); + array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null + ): Response { + $flags = $this->escapeList(is_array($flags) ? $flags : [$flags]); $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("STORE", $uid); @@ -864,7 +889,8 @@ public function store( $result[$token[0]] = $token[2][1]; } - return $result; + + return $response->setResult($result); } /** @@ -875,13 +901,13 @@ public function store( * @param null $flags flags for new message * @param null $date date for new message * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function appendMessage(string $folder, string $message, $flags = null, $date = null): array { + public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response { $tokens = []; $tokens[] = $this->escapeString($folder); if ($flags !== null) { @@ -905,13 +931,13 @@ public function appendMessage(string $folder, string $message, $flags = null, $d * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array { + public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("COPY", $uid); @@ -926,13 +952,13 @@ public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array Tokens if operation successful, false if an error occurred + * @return Response Tokens if operation successful, false if an error occurred * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array { + public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { $command = $this->buildUIDCommand("COPY", $uid); $set = implode(',', $messages); @@ -951,13 +977,13 @@ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::S * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array { + public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); @@ -972,13 +998,13 @@ public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array { + public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { $command = $this->buildUIDCommand("MOVE", $uid); $set = implode(',', $messages); @@ -992,13 +1018,13 @@ public function moveManyMessages(array $messages, string $folder, $uid = IMAP::S * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 * * @param array|null $ids - * @return array + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function ID($ids = null): array { + public function ID($ids = null): Response { $token = "NIL"; if (is_array($ids) && !empty($ids)) { $token = "("; @@ -1015,13 +1041,13 @@ public function ID($ids = null): array { * Create a new folder (and parent folders if needed) * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function createFolder(string $folder): array { + public function createFolder(string $folder): Response { return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); } @@ -1031,13 +1057,13 @@ public function createFolder(string $folder): array { * @param string $old old name * @param string $new new name * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function renameFolder(string $old, string $new): array { + public function renameFolder(string $old, string $new): Response { return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); } @@ -1045,13 +1071,13 @@ public function renameFolder(string $old, string $new): array { * Delete a folder * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function deleteFolder(string $folder): array { + public function deleteFolder(string $folder): Response { return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); } @@ -1059,13 +1085,13 @@ public function deleteFolder(string $folder): array { * Subscribe to a folder * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function subscribeFolder(string $folder): array { + public function subscribeFolder(string $folder): Response { return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); } @@ -1073,25 +1099,25 @@ public function subscribeFolder(string $folder): array { * Unsubscribe from a folder * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function unsubscribeFolder(string $folder): array { + public function unsubscribeFolder(string $folder): Response { return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); } /** * Apply session saved changes to the server * - * @return array success + * @return Response * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function expunge(): array { + public function expunge(): Response { return $this->requestAndResponse('EXPUNGE'); } @@ -1103,7 +1129,7 @@ public function expunge(): array { * @throws ImapServerErrorException * @throws RuntimeException */ - public function noop(): array { + public function noop(): Response { return $this->requestAndResponse('NOOP'); } @@ -1111,13 +1137,13 @@ public function noop(): array { * Retrieve the quota level settings, and usage statics per mailbox * * @param $username - * @return array + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function getQuota($username): array { + public function getQuota($username): Response { $command = "GETQUOTA"; $params = ['"#user/' . $username . '"']; @@ -1128,13 +1154,13 @@ public function getQuota($username): array { * Retrieve the quota settings per user * * @param string $quota_root - * @return array + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function getQuotaRoot(string $quota_root = 'INBOX'): array { + public function getQuotaRoot(string $quota_root = 'INBOX'): Response { $command = "QUOTA"; $params = [$quota_root]; @@ -1147,8 +1173,8 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): array { * @throws RuntimeException */ public function idle() { - $this->sendRequest("IDLE"); - if (!$this->assumedNextLine('+ ')) { + $response = $this->sendRequest("IDLE"); + if (!$this->assumedNextLine($response, '+ ')) { throw new RuntimeException('idle failed'); } } @@ -1158,8 +1184,9 @@ public function idle() { * @throws RuntimeException */ public function done(): bool { - $this->write("DONE"); - if (!$this->assumedNextTaggedLine('OK', $tags)) { + $response = new Response($this->noun, $this->debug); + $this->write($response, "DONE"); + if (!$this->assumedNextTaggedLine($response, 'OK', $tags)) { throw new RuntimeException('done failed'); } return true; @@ -1172,24 +1199,24 @@ public function done(): bool { * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array message ids + * @return Response message ids * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function search(array $params, $uid = IMAP::ST_UID): array { + public function search(array $params, int|string $uid = IMAP::ST_UID): Response { $command = $this->buildUIDCommand("SEARCH", $uid); $response = $this->requestAndResponse($command, $params); - foreach ($response as $ids) { + foreach ($response->data() as $ids) { if ($ids[0] === 'SEARCH') { array_shift($ids); - return $ids; + return $response->setResult($ids); } } - return []; + return $response; } /** @@ -1198,28 +1225,29 @@ public function search(array $params, $uid = IMAP::ST_UID): array { * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException * @throws MessageNotFoundException * @throws InvalidMessageDateException */ - public function overview(string $sequence, $uid = IMAP::ST_UID): array { + public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response { $result = []; list($from, $to) = explode(":", $sequence); - $uids = $this->getUid(); + $response = $this->getUid(); $ids = []; - foreach ($uids as $msgn => $v) { + foreach ($response->data() as $msgn => $v) { $id = $uid ? $v : $msgn; if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){ $ids[] = $id; } } $headers = $this->headers($ids, "RFC822", $uid); - foreach ($headers as $id => $raw_header) { + $response->stack($headers); + foreach ($headers->data() as $id => $raw_header) { $result[$id] = (new Header($raw_header, false))->getAttributes(); } - return $result; + return $response->setResult($result); } /** diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index d4a92a94..bb8e957d 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -14,6 +14,7 @@ use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Exceptions\AuthFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; use Webklex\PHPIMAP\Exceptions\MethodNotSupportedException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\IMAP; @@ -68,25 +69,26 @@ public function connect(string $host, int $port = null) { * @param string $user username * @param string $password password * - * @return bool - * @throws AuthFailedException - * @throws RuntimeException - */ - public function login(string $user, string $password): bool { - try { - $this->stream = \imap_open( - $this->getAddress(), - $user, - $password, - 0, - $attempts = 3, - ClientManager::get('options.open') - ); - } catch (\ErrorException $e) { - $errors = \imap_errors(); - $message = $e->getMessage().'. '.implode("; ", (is_array($errors) ? $errors : array())); - throw new AuthFailedException($message); - } + * @return Response + */ + public function login(string $user, string $password): Response { + return $this->response()->wrap(function($response) use($user, $password){ + /** @var Response $response */ + try { + $this->stream = \imap_open( + $this->getAddress(), + $user, + $password, + 0, + $attempts = 3, + ClientManager::get('options.open') + ); + $response->addCommand("imap_open"); + } catch (\ErrorException $e) { + $errors = \imap_errors(); + $message = $e->getMessage() . '. ' . implode("; ", (is_array($errors) ? $errors : array())); + throw new AuthFailedException($message); + } if(!$this->stream) { $errors = \imap_errors(); @@ -94,16 +96,24 @@ public function login(string $user, string $password): bool { throw new AuthFailedException($message); } - $errors = \imap_errors(); - if(is_array($errors)) { - $status = $this->examineFolder(); - if($status['exists'] !== 0) { - $message = implode("; ", (is_array($errors) ? $errors : array())); - throw new RuntimeException($message); + $errors = \imap_errors(); + $response->addCommand("imap_errors"); + if (is_array($errors)) { + $status = $this->examineFolder(); + $response->stack($status); + if ($status->data()['exists'] !== 0) { + $message = implode("; ", $errors); + throw new RuntimeException($message); + } + } + + if ($this->stream !== false) { + return ["TAG".$response->Noun()." OK [] Logged in\r\n"]; } - } - return $this->stream !== false; + $response->addError("failed to login"); + return []; + }); } /** @@ -111,10 +121,11 @@ public function login(string $user, string $password): bool { * @param string $user username * @param string $token access token * - * @return bool|resource - * @throws AuthFailedException|RuntimeException + * @return Response + * @throws AuthFailedException + * @throws RuntimeException */ - public function authenticate(string $user, string $token): bool { + public function authenticate(string $user, string $token): Response { return $this->login($user, $token); } @@ -142,25 +153,25 @@ protected function getAddress(): string { /** * Logout of the current session * - * @return bool success - */ - public function logout(): bool { - if ($this->stream) { - $result = \imap_close($this->stream, IMAP::CL_EXPUNGE); - $this->stream = false; - $this->uid_cache = null; - return $result; - } - return false; - } - - /** - * Check if the current session is connected - * - * @return bool - */ - public function connected(): bool { - return boolval($this->stream); + * @return Response + */ + public function logout(): Response { + return $this->response()->wrap(function($response){ + /** @var Response $response */ + if ($this->stream) { + $this->uid_cache = []; + $response->addCommand("imap_close"); + if (\imap_close($this->stream, IMAP::CL_EXPUNGE)) { + $this->stream = false; + return [ + 0 => "BYE Logging out\r\n", + 1 => "TAG".$response->Noun()." OK Logout completed (0.001 + 0.000 secs).\r\n", + ]; + } + $this->stream = false; + } + return []; + }); } /** @@ -168,7 +179,7 @@ public function connected(): bool { * * @throws MethodNotSupportedException */ - public function getCapabilities(): array { + public function getCapabilities(): Response { throw new MethodNotSupportedException(); } @@ -176,10 +187,10 @@ public function getCapabilities(): array { * Change the current folder * @param string $folder change to this folder * - * @return bool|array see examineOrselect() + * @return Response see examineOrselect() * @throws RuntimeException */ - public function selectFolder(string $folder = 'INBOX') { + public function selectFolder(string $folder = 'INBOX'): Response { $flags = IMAP::OP_READONLY; if (in_array($this->protocol, ["pop3", "nntp"])) { $flags = IMAP::NIL; @@ -188,31 +199,42 @@ public function selectFolder(string $folder = 'INBOX') { throw new RuntimeException("failed to reopen stream."); } - \imap_reopen($this->stream, $this->getAddress().$folder, $flags, 3); - $this->uid_cache = null; - return $this->examineFolder($folder); + return $this->response("imap_reopen")->wrap(function($response)use($folder, $flags){ + /** @var Response $response */ + \imap_reopen($this->stream, $this->getAddress() . $folder, $flags, 3); + $this->uid_cache = []; + + $status = $this->examineFolder($folder); + $response->stack($status); + + return $status->data(); + }); } /** * Examine a given folder * @param string $folder examine this folder * - * @return bool|array + * @return Response * @throws RuntimeException */ - public function examineFolder(string $folder = 'INBOX') { - if (strpos($folder, ".") === 0) { + public function examineFolder(string $folder = 'INBOX'): Response { + if (str_starts_with($folder, ".")) { throw new RuntimeException("Segmentation fault prevented. Folders starts with an illegal char '.'."); } - $folder = $this->getAddress().$folder; - $status = \imap_status($this->stream, $folder, IMAP::SA_ALL); - return [ - "flags" => [], - "exists" => $status->messages, - "recent" => $status->recent, - "unseen" => $status->unseen, - "uidnext" => $status->uidnext, - ]; + $folder = $this->getAddress() . $folder; + return $this->response("imap_status")->wrap(function($response)use($folder){ + /** @var Response $response */ + $status = \imap_status($this->stream, $folder, IMAP::SA_ALL); + + return $status ? [ + "flags" => [], + "exists" => $status->messages, + "recent" => $status->recent, + "unseen" => $status->unseen, + "uidnext" => $status->uidnext, + ] : []; + }); } /** @@ -221,15 +243,21 @@ public function examineFolder(string $folder = 'INBOX') { * @param string $rfc * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array + * @return Response */ - public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { - $result = []; - $uids = is_array($uids) ? $uids : [$uids]; - foreach ($uids as $id) { - $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::ST_UID : IMAP::NIL); - } - return $result; + public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { + return $this->response()->wrap(function($response)use($uids, $uid){ + /** @var Response $response */ + + $result = []; + $uids = is_array($uids) ? $uids : [$uids]; + foreach ($uids as $id) { + $response->addCommand("imap_fetchbody"); + $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::ST_UID : IMAP::NIL); + } + + return $result; + }); } /** @@ -238,15 +266,21 @@ public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): arr * @param string $rfc * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array + * @return Response */ - public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { - $result = []; - $uids = is_array($uids) ? $uids : [$uids]; - foreach ($uids as $id) { - $result[$id] = \imap_fetchheader($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL); - } - return $result; + public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { + return $this->response()->wrap(function($response)use($uids, $uid){ + /** @var Response $response */ + + $result = []; + $uids = is_array($uids) ? $uids : [$uids]; + foreach ($uids as $id) { + $response->addCommand("imap_fetchheader"); + $result[$id] = \imap_fetchheader($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL); + } + + return $result; + }); } /** @@ -254,24 +288,28 @@ public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): arr * @param array|int $uids * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array - */ - public function flags($uids, $uid = IMAP::ST_UID): array { - $result = []; - $uids = is_array($uids) ? $uids : [$uids]; - foreach ($uids as $id) { - $raw_flags = \imap_fetch_overview($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL); - $flags = []; - if (is_array($raw_flags) && isset($raw_flags[0])) { - $raw_flags = (array) $raw_flags[0]; - foreach($raw_flags as $flag => $value) { - if ($value === 1 && in_array($flag, ["size", "uid", "msgno", "update"]) === false){ - $flags[] = "\\".ucfirst($flag); + * @return Response + */ + public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { + return $this->response()->wrap(function($response)use($uids, $uid){ + /** @var Response $response */ + + $result = []; + $uids = is_array($uids) ? $uids : [$uids]; + foreach ($uids as $id) { + $response->addCommand("imap_fetch_overview"); + $raw_flags = \imap_fetch_overview($this->stream, $id, $uid ? IMAP::ST_UID : IMAP::NIL); + $flags = []; + if (is_array($raw_flags) && isset($raw_flags[0])) { + $raw_flags = (array)$raw_flags[0]; + foreach ($raw_flags as $flag => $value) { + if ($value === 1 && in_array($flag, ["size", "uid", "msgno", "update"]) === false) { + $flags[] = "\\" . ucfirst($flag); + } } } + $result[$id] = $flags; } - $result[$uid] = $flags; - } return $result; } @@ -280,35 +318,48 @@ public function flags($uids, $uid = IMAP::ST_UID): array { * Get uid for a given id * @param int|null $id message number * - * @return array|string message number for given message or all messages as array + * @return Response message number for given message or all messages as array */ - public function getUid($id = null) { - if ($id === null) { - if ($this->enable_uid_cache && $this->uid_cache) { - return $this->uid_cache; - } + public function getUid(int $id = null): Response { + return $this->response()->wrap(function($response)use($id){ + /** @var Response $response */ + if ($id === null) { + if ($this->enable_uid_cache && $this->uid_cache) { + return $this->uid_cache; + } + + $overview = $this->overview("1:*"); + $response->stack($overview); + $uids = []; + foreach ($overview->data() as $set) { + $uids[$set->msgno] = $set->uid; + } - $overview = $this->overview("1:*"); - $uids = []; - foreach($overview as $set){ - $uids[$set->msgno] = $set->uid; + $this->setUidCache($uids); + return $uids; } - $this->setUidCache($uids); - return $uids; - } + $response->addCommand("imap_uid"); + $uid = \imap_uid($this->stream, $id); + if($uid) { + return $uid; + } - return \imap_uid($this->stream, $id); + return []; + }); } /** * Get a message number for a uid * @param string $id uid * - * @return int message number + * @return Response message number */ - public function getMessageNumber(string $id): int { - return \imap_msgno($this->stream, $id); + public function getMessageNumber(string $id): Response { + return $this->response("imap_msgno")->wrap(function($response)use($id){ + /** @var Response $response */ + return \imap_msgno($this->stream, $id); + }); } /** @@ -316,10 +367,13 @@ public function getMessageNumber(string $id): int { * @param string $sequence uid sequence * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array + * @return Response */ - public function overview(string $sequence, $uid = IMAP::ST_UID): array { - return \imap_fetch_overview($this->stream, $sequence,$uid ? IMAP::ST_UID : IMAP::NIL); + public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response { + return $this->response("imap_fetch_overview")->wrap(function($response)use($sequence, $uid){ + /** @var Response $response */ + return \imap_fetch_overview($this->stream, $sequence, $uid ? IMAP::ST_UID : IMAP::NIL) ?: []; + }); } /** @@ -327,23 +381,25 @@ public function overview(string $sequence, $uid = IMAP::ST_UID): array { * @param string $reference mailbox reference for list * @param string $folder mailbox name match with wildcards * - * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) - * @throws RuntimeException + * @return Response folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) */ - public function folders(string $reference = '', string $folder = '*'): array { - $result = []; + public function folders(string $reference = '', string $folder = '*'): Response { + return $this->response("imap_getmailboxes")->wrap(function($response)use($reference, $folder){ + /** @var Response $response */ + $result = []; - $items = \imap_getmailboxes($this->stream, $this->getAddress(), $reference.$folder); - if(is_array($items)){ - foreach ($items as $item) { - $name = $this->decodeFolderName($item->name); - $result[$name] = ['delimiter' => $item->delimiter, 'flags' => []]; + $items = \imap_getmailboxes($this->stream, $this->getAddress(), $reference . $folder); + if (is_array($items)) { + foreach ($items as $item) { + $name = $this->decodeFolderName($item->name); + $result[$name] = ['delimiter' => $item->delimiter, 'flags' => []]; + } + } else { + throw new RuntimeException(\imap_last_error()); } - }else{ - throw new RuntimeException(\imap_last_error()); - } - return $result; + return $result; + }); } /** @@ -357,22 +413,33 @@ public function folders(string $reference = '', string $folder = '*'): array { * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * @param null $item unused attribute * - * @return bool|array new flags if $silent is false, else true or false depending on success + * @return Response new flags if $silent is false, else true or false depending on success */ - public function store(array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null) { + public function store(array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null): Response { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); - if ($mode == "+"){ - $status = \imap_setflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL); - }else{ - $status = \imap_clearflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL); - } + return $this->response()->wrap(function($response)use($mode, $from, $flag, $uid, $silent){ + /** @var Response $response */ - if ($silent === true) { - return $status; - } + if ($mode == "+") { + $response->addCommand("imap_setflag_full"); + $status = \imap_setflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL); + } else { + $response->addCommand("imap_clearflag_full"); + $status = \imap_clearflag_full($this->stream, $from, $flag, $uid ? IMAP::ST_UID : IMAP::NIL); + } + + if ($silent === true) { + if ($status){ + return [ + "TAG".$response->Noun()." OK Store completed (0.001 + 0.000 secs).\r\n" + ]; + } + return []; + } - return $this->flags($from); + return $this->flags($from); + }); } /** @@ -382,17 +449,28 @@ public function store(array $flags, int $from, $to = null, $mode = null, bool $s * @param array|null $flags flags for new message * @param string $date date for new message * - * @return bool success + * @return Response */ - public function appendMessage(string $folder, string $message, $flags = null, $date = null): bool { - if ($date != null) { - if ($date instanceof \Carbon\Carbon){ - $date = $date->format('d-M-Y H:i:s O'); - } - return \imap_append($this->stream, $folder, $message, $flags, $date); - } + public function appendMessage(string $folder, string $message, array $flags = null, mixed $date = null): Response { + return $this->response("imap_append")->wrap(function($response)use($folder, $message, $flags, $date){ + /** @var Response $response */ - return \imap_append($this->stream, $folder, $message, $flags); + if ($date != null) { + if ($date instanceof \Carbon\Carbon) { + $date = $date->format('d-M-Y H:i:s O'); + } + if(\imap_append($this->stream, $folder, $message, $flags, $date)) { + return [ + "TAG".$response->Noun()." OK Append completed (0.001 + 0.000 secs).\r\n" + ]; + } + } else if (\imap_append($this->stream, $folder, $message, $flags)){ + return [ + "TAG".$response->Noun()." OK Append completed (0.001 + 0.000 secs).\r\n" + ]; + } + return []; + }); } /** @@ -403,10 +481,19 @@ public function appendMessage(string $folder, string $message, $flags = null, $d * last message, INF means last message available * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return bool success + * @return Response */ - public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { - return \imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL); + public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + return $this->response("imap_mail_copy")->wrap(function($response)use($from, $folder, $uid){ + /** @var Response $response */ + + if (\imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { + return [ + "TAG".$response->Noun()." OK Copy completed (0.001 + 0.000 secs).\r\n" + ]; + } + throw new ImapBadRequestException("Invalid ID $from"); + }); } /** @@ -415,16 +502,25 @@ public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param string $folder Destination folder * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array|bool Tokens if operation successful, false if an error occurred - */ - public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { - foreach($messages as $msg) { - if (!$this->copyMessage($folder, $msg, null, $uid)) { - return false; + * @return Response Tokens if operation successful, false if an error occurred + */ + public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { + return $this->response()->wrap(function($response)use($messages, $folder, $uid){ + /** @var Response $response */ + foreach ($messages as $msg) { + $copy_response = $this->copyMessage($folder, $msg, null, $uid); + $response->stack($copy_response); + if (empty($copy_response->data())) { + return [ + "TAG".$response->Noun()." BAD Copy failed (0.001 + 0.000 secs).\r\n", + "Invalid ID $msg\r\n" + ]; + } } - } - - return $messages; + return [ + "TAG".$response->Noun()." OK Copy completed (0.001 + 0.000 secs).\r\n" + ]; + }); } /** @@ -435,10 +531,17 @@ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::S * last message, INF means last message available * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return bool success + * @return Response success */ - public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): bool { - return \imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL); + public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + return $this->response("imap_mail_move")->wrap(function($response)use($from, $folder, $uid){ + if (\imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { + return [ + "TAG".$response->Noun()." OK Move completed (0.001 + 0.000 secs).\r\n" + ]; + } + throw new ImapBadRequestException("Invalid ID $from"); + }); } /** @@ -447,16 +550,25 @@ public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param string $folder Destination folder * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array|bool Tokens if operation successful, false if an error occurred - */ - public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID) { - foreach($messages as $msg) { - if (!$this->moveMessage($folder, $msg, null, $uid)) { - return false; + * @return Response Tokens if operation successful, false if an error occurred + * @throws ImapBadRequestException + */ + public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { + return $this->response()->wrap(function($response)use($messages, $folder, $uid){ + foreach ($messages as $msg) { + $move_response = $this->moveMessage($folder, $msg, null, $uid); + $response = $response->include($response); + if (empty($move_response->data())) { + return [ + "TAG".$response->Noun()." BAD Move failed (0.001 + 0.000 secs).\r\n", + "Invalid ID $msg\r\n" + ]; + } } - } - - return $messages; + return [ + "TAG".$response->Noun()." OK Move completed (0.001 + 0.000 secs).\r\n" + ]; + }); } /** @@ -464,11 +576,11 @@ public function moveManyMessages(array $messages, string $folder, $uid = IMAP::S * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 * * @param null $ids - * @return array|bool|void|null + * @return Response * * @throws MethodNotSupportedException */ - public function ID($ids = null) { + public function ID($ids = null): Response { throw new MethodNotSupportedException(); } @@ -476,10 +588,14 @@ public function ID($ids = null) { * Create a new folder (and parent folders if needed) * @param string $folder folder name * - * @return bool success + * @return Response */ - public function createFolder(string $folder): bool { - return \imap_createmailbox($this->stream, $folder); + public function createFolder(string $folder): Response { + return $this->response("imap_createmailbox")->wrap(function($response)use($folder){ + return \imap_createmailbox($this->stream, $folder) ? [ + 0 => "TAG".$response->Noun()." OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", + ] : []; + }); } /** @@ -487,20 +603,28 @@ public function createFolder(string $folder): bool { * @param string $old old name * @param string $new new name * - * @return bool success + * @return Response */ - public function renameFolder(string $old, string $new): bool { - return \imap_renamemailbox($this->stream, $old, $new); + public function renameFolder(string $old, string $new): Response { + return $this->response("imap_renamemailbox")->wrap(function($response)use($old, $new){ + return \imap_renamemailbox($this->stream, $old, $new) ? [ + 0 => "TAG".$response->Noun()." OK Move completed (0.004 + 0.000 + 0.003 secs).\r\n", + ] : []; + }); } /** * Delete a folder * @param string $folder folder name * - * @return bool success + * @return Response */ - public function deleteFolder(string $folder): bool { - return \imap_deletemailbox($this->stream, $folder); + public function deleteFolder(string $folder): Response { + return $this->response("imap_deletemailbox")->wrap(function($response)use($folder){ + return \imap_deletemailbox($this->stream, $folder) ? [ + 0 => "TAG".$response->Noun()." OK Delete completed (0.004 + 0.000 + 0.003 secs).\r\n", + ] : []; + }); } /** @@ -509,7 +633,7 @@ public function deleteFolder(string $folder): bool { * * @throws MethodNotSupportedException */ - public function subscribeFolder(string $folder): bool { + public function subscribeFolder(string $folder): Response { throw new MethodNotSupportedException(); } @@ -519,17 +643,21 @@ public function subscribeFolder(string $folder): bool { * * @throws MethodNotSupportedException */ - public function unsubscribeFolder(string $folder): bool { + public function unsubscribeFolder(string $folder): Response { throw new MethodNotSupportedException(); } /** * Apply session saved changes to the server * - * @return bool success + * @return Response */ - public function expunge(): bool { - return \imap_expunge($this->stream); + public function expunge(): Response { + return $this->response("imap_expunge")->wrap(function($response){ + return \imap_expunge($this->stream) ? [ + 0 => "TAG".$response->Noun()." OK Expunge completed (0.001 + 0.000 secs).\r\n", + ] : []; + }); } /** @@ -537,7 +665,7 @@ public function expunge(): bool { * * @throws MethodNotSupportedException */ - public function noop(): bool { + public function noop(): Response { throw new MethodNotSupportedException(); } @@ -564,14 +692,13 @@ public function done() { * @param array $params * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * - * @return array message ids + * @return Response message ids */ - public function search(array $params, $uid = IMAP::ST_UID): array { - $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); - if ($result === false) { - return []; - } - return $result; + public function search(array $params, int|string $uid = IMAP::ST_UID): Response { + return $this->response("imap_search")->wrap(function($response)use($params, $uid){ + $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); + return $result ?: []; + }); } /** @@ -612,20 +739,26 @@ public function getProtocol(): string { * Retrieve the quota level settings, and usage statics per mailbox * @param $username * - * @return array + * @return Response */ - public function getQuota($username): array { - return \imap_get_quota($this->stream, 'user.'.$username); + public function getQuota($username): Response { + return $this->response("imap_get_quota")->wrap(function($response)use($username){ + $result = \imap_get_quota($this->stream, 'user.' . $username); + return $result ?: []; + }); } /** * Retrieve the quota settings per user * @param string $quota_root * - * @return array + * @return Response */ - public function getQuotaRoot(string $quota_root = 'INBOX'): array { - return \imap_get_quotaroot($this->stream, $quota_root); + public function getQuotaRoot(string $quota_root = 'INBOX'): Response { + return $this->response("imap_get_quotaroot")->wrap(function($response)use($quota_root){ + $result = \imap_get_quotaroot($this->stream, $quota_root); + return $result ?: []; + }); } /** @@ -639,4 +772,14 @@ public function setProtocol(string $protocol): LegacyProtocol { $this->protocol = $protocol; return $this; } + + /** + * Create a new Response instance + * @param string|null $command + * + * @return Response + */ + protected function response(?string $command = ""): Response { + return Response::make(0, $command == "" ? [] : [$command], [], $this->debug); + } } diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 46fb3035..0093e513 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -13,7 +13,6 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use ErrorException; -use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; @@ -56,34 +55,34 @@ public function connect(string $host, ?int $port = null); * @param string $user username * @param string $password password * - * @return array success + * @return Response * * @throws AuthFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException */ - public function login(string $user, string $password): array; + public function login(string $user, string $password): Response; /** * Authenticate your current session. * @param string $user username * @param string $token access token * - * @return bool|mixed + * @return Response * @throws AuthFailedException */ - public function authenticate(string $user, string $token); + public function authenticate(string $user, string $token): Response; /** * Logout of the current server session * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function logout(): array; + public function logout(): Response; /** * Check if the current session is connected @@ -95,95 +94,95 @@ public function connected(): bool; /** * Get an array of available capabilities * - * @return array list of capabilities + * @return Response containing a list of capabilities * @throws RuntimeException */ - public function getCapabilities(): array; + public function getCapabilities(): Response; /** * Change the current folder - * * @param string $folder change to this folder - * @return bool|array see examineOrSelect() + * + * @return Response see examineOrSelect() * @throws RuntimeException */ - public function selectFolder(string $folder = 'INBOX'); + public function selectFolder(string $folder = 'INBOX'): Response; /** * Examine a given folder - * * @param string $folder - * @return bool|array + * + * @return Response * @throws RuntimeException */ - public function examineFolder(string $folder = 'INBOX'); + public function examineFolder(string $folder = 'INBOX'): Response; /** * Fetch message headers - * @param array|int $uids + * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException */ - public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array; + public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response; /** * Fetch message headers - * @param array|int $uids + * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException */ - public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array; + public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response; /** * Fetch message flags - * @param array|int $uids + * @param int|array $uids * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException */ - public function flags($uids, $uid = IMAP::ST_UID): array; + public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response; /** * Get uid for a given id * @param int|null $id message number * - * @return array|string message number for given message or all messages as array + * @return Response containing a message number for given message or all messages as array * @throws MessageNotFoundException */ - public function getUid($id = null); + public function getUid(?int $id = null): Response; /** * Get a message number for a uid * @param string $id uid * - * @return int message number + * @return Response containing the message number * @throws MessageNotFoundException */ - public function getMessageNumber(string $id): int; + public function getMessageNumber(string $id): Response; /** * Get a list of available folders * @param string $reference mailbox reference for list * @param string $folder mailbox / folder name match with wildcards * - * @return array mailboxes that matched $folder as array(globalName => array('delim' => .., 'flags' => ..)) + * @return Response containing mailboxes that matched $folder as array(globalName => array('delim' => .., 'flags' => ..)) * @throws RuntimeException */ - public function folders(string $reference = '', string $folder = '*'): array; + public function folders(string $reference = '', string $folder = '*'): Response; /** * Set message flags - * @param array $flags flags to set, add or remove + * @param array|string $flags flags to set, add or remove * @param int $from message for items or start message if $to !== null * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available @@ -191,12 +190,12 @@ public function folders(string $reference = '', string $folder = '*'): array; * @param bool $silent if false the return values are the new flags for the wanted messages * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. - * @param null|string $item command used to store a flag + * @param string|null $item command used to store a flag * - * @return bool|array new flags if $silent is false, else true or false depending on success + * @return Response containing the new flags if $silent is false, else true or false depending on success * @throws RuntimeException */ - public function store(array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null); + public function store(array|string $flags, int $from, ?int $to = null, ?string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, ?string $item = null): Response; /** * Append a new message to given folder @@ -205,10 +204,10 @@ public function store(array $flags, int $from, $to = null, $mode = null, bool $s * @param array|null $flags flags for new message * @param string|null $date date for new message * - * @return array success + * @return Response * @throws RuntimeException */ - public function appendMessage(string $folder, string $message, $flags = null, $date = null): array; + public function appendMessage(string $folder, string $message, ?array $flags = null, ?string $date = null): Response; /** * Copy message set from current folder to other folder @@ -220,10 +219,10 @@ public function appendMessage(string $folder, string $message, $flags = null, $d * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array success + * @return Response * @throws RuntimeException */ - public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array; + public function copyMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response; /** * Copy multiple messages to the target folder @@ -232,10 +231,10 @@ public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array Tokens if operation successful, false if an error occurred + * @return Response Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array; + public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response; /** * Move a message set from current folder to another folder @@ -246,9 +245,9 @@ public function copyManyMessages(array $messages, string $folder, $uid = IMAP::S * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array success + * @return Response */ - public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array; + public function moveMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response; /** * Move multiple messages to the target folder @@ -258,76 +257,76 @@ public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_U * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array Tokens if operation successful, false if an error occurred + * @return Response Tokens if operation successful, false if an error occurred * @throws RuntimeException */ - public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array; + public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response; /** * Exchange identification information * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 * * @param null $ids - * @return array|bool|void|null + * @return Response * * @throws RuntimeException */ - public function ID($ids = null); + public function ID($ids = null): Response; /** * Create a new folder * * @param string $folder folder name - * @return array success + * @return Response * @throws RuntimeException */ - public function createFolder(string $folder): array; + public function createFolder(string $folder): Response; /** * Rename an existing folder * * @param string $old old name * @param string $new new name - * @return array success + * @return Response * @throws RuntimeException */ - public function renameFolder(string $old, string $new): array; + public function renameFolder(string $old, string $new): Response; /** * Delete a folder * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function deleteFolder(string $folder): array; + public function deleteFolder(string $folder): Response; /** * Subscribe to a folder * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function subscribeFolder(string $folder): array; + public function subscribeFolder(string $folder): Response; /** * Unsubscribe from a folder * * @param string $folder folder name - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function unsubscribeFolder(string $folder): array; + public function unsubscribeFolder(string $folder): Response; /** * Send idle command @@ -345,43 +344,43 @@ public function done(); /** * Apply session saved changes to the server * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function expunge(): array; + public function expunge(): Response; /** * Retrieve the quota level settings, and usage statics per mailbox * @param $username * - * @return array + * @return Response * @throws RuntimeException */ - public function getQuota($username): array; + public function getQuota($username): Response; /** * Retrieve the quota settings per user * * @param string $quota_root * - * @return array + * @return Response * @throws ConnectionFailedException */ - public function getQuotaRoot(string $quota_root = 'INBOX'): array; + public function getQuotaRoot(string $quota_root = 'INBOX'): Response; /** * Send noop command * - * @return array success + * @return Response * * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException */ - public function noop(): array; + public function noop(): Response; /** * Do a search request @@ -390,10 +389,10 @@ public function noop(): array; * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array message ids + * @return Response containing the message ids * @throws RuntimeException */ - public function search(array $params, $uid = IMAP::ST_UID): array; + public function search(array $params, int|string $uid = IMAP::ST_UID): Response; /** * Get a message overview @@ -401,12 +400,12 @@ public function search(array $params, $uid = IMAP::ST_UID): array; * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * - * @return array + * @return Response * @throws RuntimeException * @throws MessageNotFoundException * @throws InvalidMessageDateException */ - public function overview(string $sequence, $uid = IMAP::ST_UID): array; + public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response; /** * Enable the debug mode @@ -433,5 +432,5 @@ public function disableUidCache(); * * @param array|null $uids */ - public function setUidCache($uids); + public function setUidCache(?array $uids); } diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php new file mode 100644 index 00000000..6f9a2753 --- /dev/null +++ b/src/Connection/Protocols/Response.php @@ -0,0 +1,372 @@ +debug = $debug; + $this->noun = $noun > 0 ? $noun : (int)str_replace(".", "", (string)microtime(true)); + } + + /** + * Make a new response instance + * @param int $noun + * @param array $commands + * @param array $responses + * @param bool $debug + * + * @return Response + */ + public static function make(int $noun, array $commands = [], array $responses = [], bool $debug = false): Response { + return (new self($noun, $debug))->setCommands($commands)->setResponse($responses); + } + + /** + * Create a new empty response + * @param bool $debug + * + * @return Response + */ + public static function empty(bool $debug = false): Response { + return (new self(0, $debug)); + } + + /** + * Stack another response + * @param Response $response + * + * @return void + */ + public function stack(Response $response): void { + $this->response_stack[] = $response; + } + + /** + * Get the associated response stack + * + * @return array + */ + public function getStack(): array { + return $this->response_stack; + } + + /** + * Get all assigned commands + * + * @return array + */ + public function getCommands(): array { + return $this->commands; + } + + /** + * Add a new command + * @param string $command + * + * @return Response + */ + public function addCommand(string $command): Response { + $this->commands[] = $command; + return $this; + } + + /** + * Set and overwrite all commands + * @param array $commands + * + * @return Response + */ + public function setCommands(array $commands): Response { + $this->commands = $commands; + return $this; + } + + /** + * Get all set errors + * + * @return array + */ + public function getErrors(): array { + $errors = $this->errors; + foreach($this->getStack() as $response) { + $errors = array_merge($errors, $response->getErrors()); + } + return $errors; + } + + /** + * Set and overwrite all existing errors + * @param array $errors + * + * @return Response + */ + public function setErrors(array $errors): Response { + $this->errors = $errors; + return $this; + } + + /** + * Set the response + * @param string $error + * + * @return Response + */ + public function addError(string $error): Response { + $this->errors[] = $error; + return $this; + } + + /** + * Set the response + * @param array $response + * + * @return Response + */ + public function addResponse(mixed $response): Response { + $this->response[] = $response; + return $this; + } + + /** + * Set the response + * @param array $response + * + * @return Response + */ + public function setResponse(array $response): Response { + $this->response = $response; + return $this; + } + + /** + * Get the assigned response + * + * @return array + */ + public function getResponse(): array { + return $this->response; + } + + /** + * Set the result data + * @param mixed $result + * + * @return Response + */ + public function setResult(mixed $result): Response { + $this->result = $result; + return $this; + } + + /** + * Wrap a result bearing action + * @param callable $callback + * + * @return Response + */ + public function wrap(callable $callback): Response { + $this->result = call_user_func($callback, $this); + return $this; + } + + /** + * Get the response data + * + * @return mixed + */ + public function data(): mixed { + if ($this->result !== null) { + return $this->result; + } + return $this->getResponse(); + } + + /** + * Get the response data as array + * + * @return array + */ + public function array(): array { + $data = $this->data(); + if(is_array($data)){ + return $data; + } + return [$data]; + } + + /** + * Get the response data as string + * + * @return string + */ + public function string(): string { + $data = $this->data(); + if(is_array($data)){ + return implode(" ", $data); + } + return (string)$data; + } + + /** + * Get the response data as integer + * + * @return int + */ + public function integer(): int { + $data = $this->data(); + if(is_array($data) && isset($data[0])){ + return (int)$data[0]; + } + return (int)$data; + } + + /** + * Get the response data as boolean + * + * @return bool + */ + public function boolean(): bool { + return (bool)$this->data(); + } + + /** + * Validate and retrieve the response data + * @throws ResponseException + */ + public function validatedData(): mixed { + return $this->validate()->data(); + } + + /** + * Validate the response date + * @throws ResponseException + */ + public function validate(): Response { + if ($this->failed()) { + throw ResponseException::make($this, $this->debug); + } + return $this; + } + + /** + * Check if the Response can be considered successful + * + * @return bool + */ + public function successful(): bool { + foreach(array_merge($this->getResponse(), $this->array()) as $data) { + if (!$this->verify_data($data)) { + return false; + } + } + foreach($this->getStack() as $response) { + if (!$response->successful()) { + return false; + } + } + return $this->boolean() && !$this->getErrors(); + } + + + public function verify_data(mixed $data): bool { + if (is_array($data)) { + foreach ($data as $line) { + if (is_array($line)) { + if(!$this->verify_data($line)){ + return false; + } + }else{ + if (!$this->verify_line((string)$line)) { + return false; + } + } + } + }else{ + if (!$this->verify_line((string)$data)) { + return false; + } + } + return true; + } + + public function verify_line(string $line): bool { + return !str_starts_with($line, "TAG".$this->noun." BAD ") && !str_starts_with($line, "TAG".$this->noun." NO "); + } + + /** + * Check if the Response can be considered failed + * + * @return bool + */ + public function failed(): bool { + return !$this->successful(); + } + + public function Noun(): int { + return $this->noun; + } +} \ No newline at end of file diff --git a/src/Exceptions/ResponseException.php b/src/Exceptions/ResponseException.php new file mode 100644 index 00000000..1cea32bd --- /dev/null +++ b/src/Exceptions/ResponseException.php @@ -0,0 +1,91 @@ +getErrors() as $error) { + $message .= "\t- $error\n"; + } + + if(!$response->data()) { + $message .= "\t- Empty response\n"; + } + + if ($debug) { + $message .= self::debug_message($response); + } + + foreach($response->getStack() as $_response) { + $exception = self::make($_response, $debug, $exception); + } + + return new self($message."Error occurred", 0, $exception); + } + + /** + * Generate a debug message containing all commands send and responses received + * @param Response $response + * + * @return string + */ + protected static function debug_message(Response $response): string { + $commands = $response->getCommands(); + $message = "Commands send:\n"; + if ($commands) { + foreach($commands as $command) { + $message .= "\t".str_replace("\r\n", "\\r\\n", $command)."\n"; + } + }else{ + $message .= "\tNo command send!\n"; + } + + $responses = $response->getResponse(); + $message .= "Responses received:\n"; + if ($responses) { + foreach($responses as $_response) { + if (is_array($_response)) { + foreach($_response as $value) { + $message .= "\t".str_replace("\r\n", "\\r\\n", "$value")."\n"; + } + }else{ + $message .= "\t".str_replace("\r\n", "\\r\\n", "$_response")."\n"; + } + } + }else{ + $message .= "\tNo responses received!\n"; + } + + return $message; + } +} diff --git a/src/Folder.php b/src/Folder.php index b425bf79..732d229d 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -13,8 +13,17 @@ namespace Webklex\PHPIMAP; use Carbon\Carbon; +use Webklex\PHPIMAP\Connection\Protocols\Response; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\EventNotFoundException; +use Webklex\PHPIMAP\Exceptions\FolderFetchingException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException; +use Webklex\PHPIMAP\Exceptions\ResponseException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Query\WhereQuery; use Webklex\PHPIMAP\Support\FolderCollection; @@ -138,8 +147,12 @@ public function __construct(Client $client, string $folder_name, string $delimit * @param string[] $extensions * * @return WhereQuery - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ResponseException */ public function query(array $extensions = []): WhereQuery { $this->getClient()->checkConnection(); @@ -154,8 +167,12 @@ public function query(array $extensions = []): WhereQuery { * @param string[] $extensions * * @return WhereQuery - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ResponseException */ public function search(array $extensions = []): WhereQuery { return $this->query($extensions); @@ -166,8 +183,12 @@ public function search(array $extensions = []): WhereQuery { * @param string[] $extensions * * @return WhereQuery - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ResponseException */ public function messages(array $extensions = []): WhereQuery { return $this->query($extensions); @@ -237,14 +258,18 @@ protected function parseAttributes($attributes): void { * * @return array * @throws ConnectionFailedException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); - $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name); - if($expunge) $this->client->expunge(); + $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData(); + if ($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); $event = $this->getEvent("folder", "moved"); @@ -259,15 +284,20 @@ public function move(string $new_name, bool $expunge = true): array { * * @return array * @throws ConnectionFailedException - * @throws Exceptions\InvalidMessageDateException - * @throws Exceptions\MessageNotFoundException - * @throws Exceptions\RuntimeException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws InvalidMessageDateException + * @throws MessageNotFoundException + * @throws ResponseException */ public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN) == IMAP::ST_UID; - return $this->client->getConnection()->overview($sequence, $uid); + $response = $this->client->getConnection()->overview($sequence, $uid); + return $response->validatedData(); } /** @@ -276,9 +306,13 @@ public function overview(string $sequence = null): array { * @param array|null $options * @param string|Carbon|null $internal_date * - * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @return array + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function appendMessage(string $message, array $options = null, Carbon|string $internal_date = null): array { /** @@ -291,7 +325,7 @@ public function appendMessage(string $message, array $options = null, Carbon|str $internal_date = $internal_date->format('d-M-Y H:i:s O'); } - return $this->client->getConnection()->appendMessage($this->path, $message, $options, $internal_date); + return $this->client->getConnection()->appendMessage($this->path, $message, $options, $internal_date)->validatedData(); } /** @@ -301,9 +335,13 @@ public function appendMessage(string $message, array $options = null, Carbon|str * * @return array * @throws ConnectionFailedException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws RuntimeException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws ResponseException */ public function rename(string $new_name, bool $expunge = true): array { return $this->move($new_name, $expunge); @@ -313,10 +351,14 @@ public function rename(string $new_name, bool $expunge = true): array { * Delete the current folder * @param boolean $expunge * - * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException - * @throws Exceptions\EventNotFoundException + * @return array + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws EventNotFoundException + * @throws AuthFailedException + * @throws ResponseException */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); @@ -331,25 +373,33 @@ public function delete(bool $expunge = true): array { /** * Subscribe the current folder * - * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @return array + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function subscribe(): array { $this->client->openFolder($this->path); - return $this->client->getConnection()->subscribeFolder($this->path); + return $this->client->getConnection()->subscribeFolder($this->path)->validatedData(); } /** * Unsubscribe the current folder * - * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @return array + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function unsubscribe(): array { $this->client->openFolder($this->path); - return $this->client->getConnection()->unsubscribeFolder($this->path); + return $this->client->getConnection()->unsubscribeFolder($this->path)->validatedData(); } /** @@ -359,19 +409,18 @@ public function unsubscribe(): array { * @param boolean $auto_reconnect try to reconnect on connection close (@deprecated is no longer required) * * @throws ConnectionFailedException - * @throws Exceptions\InvalidMessageDateException - * @throws Exceptions\MessageContentFetchingException - * @throws Exceptions\MessageHeaderFetchingException - * @throws Exceptions\RuntimeException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\MessageFlagException - * @throws Exceptions\MessageNotFoundException - * @throws Exceptions\NotSupportedCapabilityException + * @throws RuntimeException + * @throws AuthFailedException + * @throws NotSupportedCapabilityException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException */ public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); - if (!in_array("IDLE", $this->client->getConnection()->getCapabilities())) { - throw new NotSupportedCapabilityException("IMAP server does not support IDLE"); + + if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { + throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } $this->client->openFolder($this->path, true); $connection = $this->client->getConnection(); @@ -423,16 +472,24 @@ public function idle(callable $callback, int $timeout = 300): void { * Get folder status information * * @return array - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function getStatus(): array { return $this->examine(); } /** - * @throws RuntimeException * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function loadStatus(): Folder { $this->status = $this->getStatus(); @@ -443,12 +500,15 @@ public function loadStatus(): Folder { * Examine the current folder * * @return array - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException */ public function examine(): array { - $result = $this->client->getConnection()->examineFolder($this->path); - return is_array($result) ? $result : []; + return $this->client->getConnection()->examineFolder($this->path)->validatedData(); } /** diff --git a/src/Message.php b/src/Message.php index a6c8129d..59904922 100755 --- a/src/Message.php +++ b/src/Message.php @@ -14,12 +14,22 @@ use ReflectionClass; use ReflectionException; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; +use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\EventNotFoundException; +use Webklex\PHPIMAP\Exceptions\FolderFetchingException; +use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; +use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; +use Webklex\PHPIMAP\Exceptions\ResponseException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Support\AttachmentCollection; use Webklex\PHPIMAP\Support\FlagCollection; use Webklex\PHPIMAP\Support\Masks\MessageMask; @@ -179,14 +189,17 @@ class Message { * @param boolean $fetch_flags * @param integer|null $sequence * - * @throws Exceptions\ConnectionFailedException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException - * @throws Exceptions\RuntimeException - * @throws MessageHeaderFetchingException * @throws MessageContentFetchingException - * @throws Exceptions\EventNotFoundException * @throws MessageFlagException - * @throws Exceptions\MessageNotFoundException + * @throws MessageHeaderFetchingException + * @throws RuntimeException + * @throws ResponseException */ public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { $this->boot(); @@ -237,18 +250,21 @@ public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_ * @param null $sequence * * @return Message - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException - * @throws ReflectionException * @throws MessageFlagException - * @throws Exceptions\RuntimeException - * @throws Exceptions\MessageNotFoundException + * @throws ReflectionException + * @throws RuntimeException + * @throws ResponseException */ public static function make(int $uid, ?int $msglist, Client $client, string $raw_header, string $raw_body, array $raw_flags, $fetch_options = null, $sequence = null): Message { $reflection = new ReflectionClass(self::class); - /** @var self $instance */ + /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); $instance->boot(); @@ -294,7 +310,14 @@ public function boot(): void { * @param array $arguments * * @return mixed + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MessageNotFoundException * @throws MethodNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function __call(string $method, array $arguments) { if(strtolower(substr($method, 0, 3)) === 'get') { @@ -330,6 +353,13 @@ public function __set($name, $value) { * @param $name * * @return Attribute|mixed|null + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function __get($name) { return $this->get($name); @@ -396,14 +426,18 @@ public function getHTMLBody(): string { /** * Parse all defined headers * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException * @throws InvalidMessageDateException * @throws MessageHeaderFetchingException + * @throws ResponseException */ private function parseHeader(): void { $sequence_id = $this->getSequenceId(); - $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); + $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID)->validatedData(); if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } @@ -442,9 +476,13 @@ public function parseRawFlags(array $raw_flags): void { * Parse additional flags * * @return void - * @throws Exceptions\ConnectionFailedException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ private function parseFlags(): void { $this->client->openFolder($this->folder_path); @@ -452,7 +490,7 @@ private function parseFlags(): void { $sequence_id = $this->getSequenceId(); try { - $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID); + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be fetched", 0, $e); } @@ -472,16 +510,17 @@ private function parseFlags(): void { * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws InvalidMessageDateException - * @throws Exceptions\EventNotFoundException + * @throws MessageContentFetchingException * @throws MessageFlagException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function parseBody(): Message { $this->client->openFolder($this->folder_path); $sequence_id = $this->getSequenceId(); try { - $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID); + $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageContentFetchingException("failed to fetch content", 0); } @@ -499,17 +538,21 @@ public function parseBody(): Message { /** * Handle auto "Seen" flag handling * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function peek(): void { if ($this->fetch_options == IMAP::FT_PEEK) { if ($this->getFlags()->get("seen") == null) { $this->unsetFlag("Seen"); } - }elseif ($this->getFlags()->get("seen") != null) { + } elseif ($this->getFlags()->get("seen") != null) { $this->setFlag("Seen"); } } @@ -525,7 +568,8 @@ public function peek(): void { * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function parseRawBody(string $raw_body): Message { $this->structure = new Structure($raw_body, $this->header); @@ -538,8 +582,12 @@ public function parseRawBody(string $raw_body): Message { * Fetch the Message structure * @param Structure $structure * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ private function fetchStructure(Structure $structure): void { $this->client?->openFolder($this->folder_path); @@ -764,10 +812,14 @@ public function getEncoding(object|string $structure): string { /** * Get the messages folder * - * @return mixed - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\RuntimeException + * @return ?Folder + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getFolder(): ?Folder { return $this->client->getFolderByPath($this->folder_path); @@ -779,15 +831,19 @@ public function getFolder(): ?Folder { * @param MessageCollection|null $thread * @param Folder|null $folder * - * @return MessageCollection|null - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\GetMessagesFailedException - * @throws Exceptions\RuntimeException + * @return MessageCollection + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { $thread = $thread ?: MessageCollection::make([]); - $folder = $folder ?: $this->getFolder(); + $folder = $folder ?: $this->getFolder(); $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ @@ -819,10 +875,14 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = * @param Folder $secondary_folder * @param Folder $sent_folder * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\GetMessagesFailedException - * @throws Exceptions\RuntimeException - * @throws Exceptions\FolderFetchingException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in_reply_to, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder): void { $primary_folder->query()->inReplyTo($in_reply_to) @@ -841,10 +901,14 @@ protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in * @param Folder $secondary_folder * @param Folder $sent_folder * - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\GetMessagesFailedException - * @throws Exceptions\RuntimeException - * @throws Exceptions\FolderFetchingException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws GetMessagesFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ protected function fetchThreadByMessageId(MessageCollection &$thread, string $message_id, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder): void { $primary_folder->query()->messageId($message_id) @@ -861,19 +925,23 @@ protected function fetchThreadByMessageId(MessageCollection &$thread, string $me * @param boolean $expunge * * @return null|Message - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException - * @throws MessageHeaderFetchingException - * @throws Exceptions\EventNotFoundException * @throws MessageFlagException - * @throws Exceptions\MessageNotFoundException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function copy(string $folder_path, bool $expunge = false): ?Message { $this->client->openFolder($folder_path); - $status = $this->client->getConnection()->examineFolder($folder_path); + $status = $this->client->getConnection()->examineFolder($folder_path)->validatedData(); if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; @@ -882,7 +950,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { + if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID)->validatedData()) { return $this->fetchNewMail($folder, $next_uid, "copied", $expunge); } } @@ -896,19 +964,23 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { * @param boolean $expunge * * @return Message|null - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException - * @throws MessageHeaderFetchingException - * @throws Exceptions\EventNotFoundException * @throws MessageFlagException - * @throws Exceptions\MessageNotFoundException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function move(string $folder_path, bool $expunge = false): ?Message { $this->client->openFolder($folder_path); - $status = $this->client->getConnection()->examineFolder($folder_path); + $status = $this->client->getConnection()->examineFolder($folder_path)->validatedData(); if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; @@ -917,7 +989,7 @@ public function move(string $folder_path, bool $expunge = false): ?Message { $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID) == true) { + if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID)->validatedData()) { return $this->fetchNewMail($folder, $next_uid, "moved", $expunge); } } @@ -933,24 +1005,28 @@ public function move(string $folder_path, bool $expunge = false): ?Message { * @param boolean $expunge * * @return Message - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\MessageNotFoundException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws MessageFlagException * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bool $expunge): Message { - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $this->client->openFolder($folder->path); if ($this->sequence === IMAP::ST_UID) { $sequence_id = $next_uid; - }else{ - $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid); + } else { + $sequence_id = $this->client->getConnection()->getMessageNumber($next_uid)->validatedData(); } $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); @@ -967,15 +1043,19 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo * @param boolean $force_move * * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\FolderFetchingException - * @throws Exceptions\MessageNotFoundException - * @throws Exceptions\RuntimeException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException * @throws MessageFlagException * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false): bool { $status = $this->setFlag("Deleted"); @@ -996,10 +1076,14 @@ public function delete(bool $expunge = true, string $trash_path = null, bool $fo * @param boolean $expunge * * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function restore(bool $expunge = true): bool { $status = $this->unsetFlag("Deleted"); @@ -1016,17 +1100,21 @@ public function restore(bool $expunge = true): bool { * @param array|string $flag * * @return bool - * @throws Exceptions\ConnectionFailedException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function setFlag(array|string $flag): bool { $this->client->openFolder($this->folder_path); $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be set", 0, $e); } @@ -1043,10 +1131,14 @@ public function setFlag(array|string $flag): bool { * @param array|string $flag * * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function unsetFlag(array|string $flag): bool { $this->client->openFolder($this->folder_path); @@ -1054,7 +1146,7 @@ public function unsetFlag(array|string $flag): bool { $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be removed", 0, $e); } @@ -1071,10 +1163,14 @@ public function unsetFlag(array|string $flag): bool { * @param array|string $flag * * @return bool - * @throws Exceptions\ConnectionFailedException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\EventNotFoundException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function addFlag(array|string $flag): bool { return $this->setFlag($flag); @@ -1085,10 +1181,14 @@ public function addFlag(array|string $flag): bool { * @param array|string $flag * * @return bool - * @throws Exceptions\ConnectionFailedException - * @throws Exceptions\EventNotFoundException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageFlagException - * @throws Exceptions\RuntimeException + * @throws RuntimeException + * @throws ResponseException */ public function removeFlag(array|string $flag): bool { return $this->unsetFlag($flag); @@ -1353,9 +1453,13 @@ public function setFlags($flags): Message { * Set the client * @param $client * - * @return $this - * @throws Exceptions\RuntimeException - * @throws Exceptions\ConnectionFailedException + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function setClient($client): Message { $this->client = $client; diff --git a/src/Query/Query.php b/src/Query/Query.php index 39e1572a..8baef153 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -19,15 +19,19 @@ use ReflectionException; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; use Webklex\PHPIMAP\Exceptions\MessageSearchValidationException; +use Webklex\PHPIMAP\Exceptions\ResponseException; use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\IMAP; use Webklex\PHPIMAP\Message; @@ -181,12 +185,16 @@ public function generate_query(): string { * * @return Collection * @throws GetMessagesFailedException + * @throws AuthFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException */ protected function search(): Collection { $this->generate_query(); try { - $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence); + $available_messages = $this->client->getConnection()->search([$this->getRawQuery()], $this->sequence)->validatedData(); return new Collection($available_messages); } catch (RuntimeException|ConnectionFailedException $e) { throw new GetMessagesFailedException("failed to fetch messages", 0, $e); @@ -197,7 +205,11 @@ protected function search(): Collection { * Count all available messages matching the current search criteria * * @return int + * @throws AuthFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException */ public function count(): int { return $this->search()->count(); @@ -208,8 +220,12 @@ public function count(): int { * @param Collection $available_messages * * @return array + * @throws AuthFailedException * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws RuntimeException + * @throws ResponseException */ protected function fetch(Collection $available_messages): array { if ($this->fetch_order === 'desc') { @@ -219,14 +235,14 @@ protected function fetch(Collection $available_messages): array { $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); $extensions = $this->getExtensions(); if (empty($extensions) === false && method_exists($this->client->getConnection(), "fetch")) { - $extensions = $this->client->getConnection()->fetch($extensions, $uids, null, $this->sequence); + $extensions = $this->client->getConnection()->fetch($extensions, $uids, null, $this->sequence)->validatedData(); } - $flags = $this->client->getConnection()->flags($uids, $this->sequence); - $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence); + $flags = $this->client->getConnection()->flags($uids, $this->sequence)->validatedData(); + $headers = $this->client->getConnection()->headers($uids, "RFC822", $this->sequence)->validatedData(); $contents = []; if ($this->getFetchBody()) { - $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence); + $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence)->validatedData(); } return [ @@ -247,10 +263,14 @@ protected function fetch(Collection $available_messages): array { * @param array $flags * * @return Message|null + * @throws AuthFailedException * @throws ConnectionFailedException * @throws EventNotFoundException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ReflectionException + * @throws ResponseException */ protected function make(int $uid, int $msglist, string $header, string $content, array $flags): ?Message { try { @@ -305,11 +325,15 @@ public function curate_messages(Collection $available_messages): MessageCollecti * @param Collection $available_messages * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws EventNotFoundException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ReflectionException * @throws RuntimeException + * @throws ResponseException */ protected function populate(Collection $available_messages): MessageCollection { $messages = MessageCollection::make([]); @@ -327,7 +351,7 @@ protected function populate(Collection $available_messages): MessageCollection { $extensions = $raw_messages["extensions"][$uid] ?? []; $message = $this->make($uid, $msglist, $header, $content, $flag); - foreach($extensions as $key => $extension) { + foreach ($extensions as $key => $extension) { $message->getHeader()->set($key, $extension); } if ($message !== null) { @@ -344,7 +368,11 @@ protected function populate(Collection $available_messages): MessageCollection { * Fetch the current query and return all found messages * * @return MessageCollection + * @throws AuthFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException */ public function get(): MessageCollection { return $this->curate_messages($this->search()); @@ -356,11 +384,15 @@ public function get(): MessageCollection { * @param int $chunk_size * @param int $start_chunk * + * @throws AuthFailedException * @throws ConnectionFailedException * @throws EventNotFoundException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ReflectionException * @throws RuntimeException + * @throws ResponseException */ public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1): void { $available_messages = $this->search(); @@ -389,7 +421,11 @@ public function chunked(callable $callback, int $chunk_size = 10, int $start_chu * @param string $page_name The page name / uri parameter used for the generated links and the auto mode * * @return LengthAwarePaginator + * @throws AuthFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException */ public function paginate(int $per_page = 5, $page = null, string $page_name = 'imap_page'): LengthAwarePaginator { if ( @@ -414,17 +450,20 @@ public function paginate(int $per_page = 5, $page = null, string $page_name = 'i * @param null $sequence * * @return Message + * @throws AuthFailedException * @throws ConnectionFailedException - * @throws RuntimeException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException - * @throws MessageHeaderFetchingException - * @throws EventNotFoundException * @throws MessageFlagException - * @throws MessageNotFoundException + * @throws MessageHeaderFetchingException + * @throws RuntimeException + * @throws ResponseException */ public function getMessage(int $uid, $msglist = null, $sequence = null): Message { - return new Message($uid, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags(), $sequence ? $sequence : $this->sequence); + return new Message($uid, $msglist, $this->getClient(), $this->getFetchOptions(), $this->getFetchBody(), $this->getFetchFlags(), $sequence ?: $this->sequence); } /** @@ -433,14 +472,17 @@ public function getMessage(int $uid, $msglist = null, $sequence = null): Message * @param null $msglist * * @return Message + * @throws AuthFailedException * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException + * @throws MessageFlagException * @throws MessageHeaderFetchingException * @throws RuntimeException - * @throws EventNotFoundException - * @throws MessageFlagException - * @throws MessageNotFoundException + * @throws ResponseException */ public function getMessageByMsgn($msgn, $msglist = null): Message { return $this->getMessage($msgn, $msglist, IMAP::ST_MSGN); @@ -451,14 +493,17 @@ public function getMessageByMsgn($msgn, $msglist = null): Message { * @param $uid * * @return Message + * @throws AuthFailedException * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException * @throws MessageContentFetchingException + * @throws MessageFlagException * @throws MessageHeaderFetchingException * @throws RuntimeException - * @throws EventNotFoundException - * @throws MessageFlagException - * @throws MessageNotFoundException + * @throws ResponseException */ public function getMessageByUid($uid): Message { return $this->getMessage($uid, null, IMAP::ST_UID); @@ -469,17 +514,22 @@ public function getMessageByUid($uid): Message { * @param callable $closure * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function filter(callable $closure): MessageCollection { $connection = $this->getClient()->getConnection(); - $uids = $connection->getUid(); + $uids = $connection->getUid()->validatedData(); $available_messages = new Collection(); if (is_array($uids)) { - foreach ($uids as $id){ + foreach ($uids as $id) { if ($closure($id)) { $available_messages->push($id); } @@ -494,12 +544,17 @@ public function filter(callable $closure): MessageCollection { * @param int $uid * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function getByUidGreaterOrEqual(int $uid): MessageCollection { - return $this->filter(function($id) use($uid){ + return $this->filter(function($id) use ($uid) { return $id >= $uid; }); } @@ -509,12 +564,17 @@ public function getByUidGreaterOrEqual(int $uid): MessageCollection { * @param int $uid * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function getByUidGreater(int $uid): MessageCollection { - return $this->filter(function($id) use($uid){ + return $this->filter(function($id) use ($uid) { return $id > $uid; }); } @@ -524,12 +584,17 @@ public function getByUidGreater(int $uid): MessageCollection { * @param int $uid * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function getByUidLower(int $uid): MessageCollection { - return $this->filter(function($id) use($uid){ + return $this->filter(function($id) use ($uid) { return $id < $uid; }); } @@ -539,12 +604,17 @@ public function getByUidLower(int $uid): MessageCollection { * @param int $uid * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function getByUidLowerOrEqual(int $uid): MessageCollection { - return $this->filter(function($id) use($uid){ + return $this->filter(function($id) use ($uid) { return $id <= $uid; }); } @@ -554,12 +624,17 @@ public function getByUidLowerOrEqual(int $uid): MessageCollection { * @param int $uid * * @return MessageCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws GetMessagesFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ public function getByUidLowerThan(int $uid): MessageCollection { - return $this->filter(function($id) use($uid){ + return $this->filter(function($id) use ($uid) { return $id < $uid; }); } @@ -609,7 +684,12 @@ public function getSequence(): int|string { /** * @return Client + * @throws AuthFailedException * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException */ public function getClient(): Client { $this->client->checkConnection(); From 761120602e1e80b590d36f243be7f7a967f578b6 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:27:34 +0100 Subject: [PATCH 371/600] Unit tests added #347 #242 --- .github/workflows/tests.yaml | 38 +++ .gitignore | 2 + composer.json | 9 +- phpunit.xml | 54 ++-- tests/AddressTest.php | 72 ++++++ tests/AttributeTest.php | 75 ++++++ tests/ClientManagerTest.php | 91 +++++++ tests/ClientTest.php | 308 +++++++++++++++++++++++ tests/HeaderTest.php | 140 +++++++++++ tests/ImapProtocolTest.php | 39 +++ tests/MessageTest.php | 224 +++++++++++++++++ tests/PartTest.php | 94 +++++++ tests/StructureTest.php | 55 ++++ tests/messages/1366671050@github.com.eml | 108 ++++++++ tests/messages/example_attachment.eml | 56 +++++ 15 files changed, 1331 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 tests/AddressTest.php create mode 100644 tests/AttributeTest.php create mode 100644 tests/ClientManagerTest.php create mode 100644 tests/ClientTest.php create mode 100644 tests/HeaderTest.php create mode 100644 tests/ImapProtocolTest.php create mode 100644 tests/MessageTest.php create mode 100644 tests/PartTest.php create mode 100644 tests/StructureTest.php create mode 100644 tests/messages/1366671050@github.com.eml create mode 100644 tests/messages/example_attachment.eml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..d4146065 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,38 @@ +name: Tests + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +permissions: + contents: read + +jobs: + phpunit: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: ['8.0', 8.1, 8.2] + + name: PHP ${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: openssl, json, mbstring, iconv, fileinfo, libxml, zip + coverage: none + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 77d608c3..21a1fd47 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ vendor composer.lock .idea /build/ +test.php +.phpunit.result.cache \ No newline at end of file diff --git a/composer.json b/composer.json index 65104480..f20fb95b 100644 --- a/composer.json +++ b/composer.json @@ -19,18 +19,21 @@ } ], "require": { - "php": ">=7.0.0", + "php": "^8.0.2", "ext-openssl": "*", "ext-json": "*", "ext-mbstring": "*", "ext-iconv": "*", + "ext-libxml": "*", + "ext-zip": "*", "ext-fileinfo": "*", - "nesbot/carbon": ">=1.0", + "nesbot/carbon": "^2.62.1", "symfony/http-foundation": ">=2.8.0", "illuminate/pagination": ">=5.0.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^9.5.10", + "symfony/var-dumper": "^6.2" }, "suggest": { "symfony/mime": "Recomended for better extension support" diff --git a/phpunit.xml b/phpunit.xml index bfbb0b09..e7cbae72 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,32 +1,24 @@ - - - - tests - - - - - src/ - - - - - - - - - - - - - \ No newline at end of file + + + + src/ + + + + + + + + + + tests + + + + + + + + + diff --git a/tests/AddressTest.php b/tests/AddressTest.php new file mode 100644 index 00000000..442462fb --- /dev/null +++ b/tests/AddressTest.php @@ -0,0 +1,72 @@ + "Username", + "mailbox" => "info", + "host" => "domain.tld", + "mail" => "info@domain.tld", + "full" => "Username ", + ]; + + /** + * Address test + * + * @return void + */ + public function testAddress(): void { + $address = new Address((object)$this->data); + + self::assertSame("Username", $address->personal); + self::assertSame("info", $address->mailbox); + self::assertSame("domain.tld", $address->host); + self::assertSame("info@domain.tld", $address->mail); + self::assertSame("Username ", $address->full); + } + + /** + * Test Address to string conversion + * + * @return void + */ + public function testAddressToStringConversion(): void { + $address = new Address((object)$this->data); + + self::assertSame("Username ", (string)$address); + } + + /** + * Test Address serialization + * + * @return void + */ + public function testAddressSerialization(): void { + $address = new Address((object)$this->data); + + foreach($address as $key => $value) { + self::assertSame($this->data[$key], $value); + } + + } +} \ No newline at end of file diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php new file mode 100644 index 00000000..8374894f --- /dev/null +++ b/tests/AttributeTest.php @@ -0,0 +1,75 @@ +toString()); + self::assertSame("foo", $attribute->getName()); + self::assertSame("foos", $attribute->setName("foos")->getName()); + } + + /** + * Date Attribute test + * + * @return void + */ + public function testDateAttribute(): void { + $attribute = new Attribute("foo", "2022-12-26 08:07:14 GMT-0800"); + + self::assertInstanceOf(Carbon::class, $attribute->toDate()); + self::assertSame("2022-12-26 08:07:14 GMT-0800", $attribute->toDate()->format("Y-m-d H:i:s T")); + } + + /** + * Array Attribute test + * + * @return void + */ + public function testArrayAttribute(): void { + $attribute = new Attribute("foo", ["bar"]); + + self::assertSame("bar", $attribute->toString()); + + $attribute->add("bars"); + self::assertSame(true, $attribute->has(1)); + self::assertSame("bars", $attribute->get(1)); + self::assertSame(true, $attribute->contains("bars")); + self::assertSame("foo, bars", $attribute->set("foo", 0)->toString()); + + $attribute->remove(0); + self::assertSame("bars", $attribute->toString()); + + self::assertSame("bars, foos", $attribute->merge(["foos", "bars"], true)->toString()); + self::assertSame("bars, foos, foos, donk", $attribute->merge(["foos", "donk"], false)->toString()); + + self::assertSame(4, $attribute->count()); + + self::assertSame("donk", $attribute->last()); + self::assertSame("bars", $attribute->first()); + + self::assertSame(["bars", "foos", "foos", "donk"], array_values($attribute->all())); + } +} \ No newline at end of file diff --git a/tests/ClientManagerTest.php b/tests/ClientManagerTest.php new file mode 100644 index 00000000..80ec4ceb --- /dev/null +++ b/tests/ClientManagerTest.php @@ -0,0 +1,91 @@ +cm = new ClientManager(); + } + + /** + * Test if the config can be accessed + * + * @return void + */ + public function testConfigAccessorAccount(): void { + self::assertSame("default", ClientManager::get("default")); + self::assertSame("d-M-Y", ClientManager::get("date_format")); + self::assertSame(IMAP::FT_PEEK, ClientManager::get("options.fetch")); + self::assertSame([], ClientManager::get("options.open")); + } + + /** + * Test creating a client instance + * + * @throws MaskNotFoundException + */ + public function testMakeClient(): void { + self::assertInstanceOf(Client::class, $this->cm->make([])); + } + + /** + * Test accessing accounts + * + * @throws MaskNotFoundException + */ + public function testAccountAccessor(): void { + self::assertSame("default", $this->cm->getDefaultAccount()); + self::assertNotEmpty($this->cm->account("default")); + + $this->cm->setDefaultAccount("foo"); + self::assertSame("foo", $this->cm->getDefaultAccount()); + $this->cm->setDefaultAccount("default"); + } + + /** + * Test setting a config + * + * @throws MaskNotFoundException + */ + public function testSetConfig(): void { + $config = [ + "default" => "foo", + "options" => [ + "fetch" => IMAP::ST_MSGN, + "open" => "foo" + ] + ]; + $cm = new ClientManager($config); + + self::assertSame("foo", $cm->getDefaultAccount()); + self::assertInstanceOf(Client::class, $cm->account("foo")); + self::assertSame(IMAP::ST_MSGN, $cm->get("options.fetch")); + self::assertSame(false, is_array($cm->get("options.open"))); + + } +} \ No newline at end of file diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 00000000..8d9f8e66 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,308 @@ +client = new Client([ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]); + } + + /** + * Client test + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + */ + public function testClient(): void { + $this->createNewProtocolMockup(); + + self::assertInstanceOf(ImapProtocol::class, $this->client->getConnection()); + self::assertSame(true, $this->client->isConnected()); + self::assertSame(false, $this->client->checkConnection()); + self::assertSame(30, $this->client->getTimeout()); + self::assertSame(MessageMask::class, $this->client->getDefaultMessageMask()); + self::assertSame(AttachmentMask::class, $this->client->getDefaultAttachmentMask()); + self::assertArrayHasKey("new", $this->client->getDefaultEvents("message")); + } + + public function testClientLogout(): void { + $this->createNewProtocolMockup(); + + $this->protocol->expects($this->any())->method('logout')->willReturn(Response::empty()->setResponse([ + 0 => "BYE Logging out\r\n", + 1 => "OK Logout completed (0.001 + 0.000 secs).\r\n", + ])); + self::assertInstanceOf(Client::class, $this->client->disconnect()); + + } + + public function testClientExpunge(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('expunge')->willReturn(Response::empty()->setResponse([ + 0 => "OK", + 1 => "Expunge", + 2 => "completed", + 3 => [ + 0 => "0.001", + 1 => "+", + 2 => "0.000", + 3 => "secs).", + ], + ])); + self::assertNotEmpty($this->client->expunge()); + + } + + public function testClientFolders(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('expunge')->willReturn(Response::empty()->setResponse([ + 0 => "OK", + 1 => "Expunge", + 2 => "completed", + 3 => [ + 0 => "0.001", + 1 => "+", + 2 => "0.000", + 3 => "secs).", + ], + ])); + + $this->protocol->expects($this->any())->method('selectFolder')->willReturn(Response::empty()->setResponse([ + "flags" => [ + 0 => [ + 0 => "\Answered", + 1 => "\Flagged", + 2 => "\Deleted", + 3 => "\Seen", + 4 => "\Draft", + 5 => "NonJunk", + 6 => "unknown-1", + ], + ], + "exists" => 139, + "recent" => 0, + "unseen" => 94, + "uidvalidity" => 1488899637, + "uidnext" => 278, + ])); + self::assertNotEmpty($this->client->openFolder("INBOX")); + self::assertSame("INBOX", $this->client->getFolderPath()); + + $this->protocol->expects($this->any())->method('examineFolder')->willReturn(Response::empty()->setResponse([ + "flags" => [ + 0 => [ + 0 => "\Answered", + 1 => "\Flagged", + 2 => "\Deleted", + 3 => "\Seen", + 4 => "\Draft", + 5 => "NonJunk", + 6 => "unknown-1", + ], + ], + "exists" => 139, + "recent" => 0, + "unseen" => 94, + "uidvalidity" => 1488899637, + "uidnext" => 278, + ])); + self::assertNotEmpty($this->client->checkFolder("INBOX")); + + $this->protocol->expects($this->any())->method('folders')->with($this->identicalTo(""), $this->identicalTo("*"))->willReturn(Response::empty()->setResponse([ + "INBOX" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasChildren", + ], + ], + "INBOX.new" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.9AL56dEMTTgUKOAz" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.U9PsHCvXxAffYvie" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.Trash" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Trash", + ], + ], + "INBOX.processing" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.Sent" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Sent", + ], + ], + "INBOX.OzDWCXKV3t241koc" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.5F3bIVTtBcJEqIVe" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.8J3rll6eOBWnTxIU" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.Junk" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Junk", + ], + ], + "INBOX.Drafts" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Drafts", + ], + ], + "INBOX.test" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + ])); + + $this->protocol->expects($this->any())->method('createFolder')->willReturn(Response::empty()->setResponse([ + 0 => "OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", + ])); + self::assertNotEmpty($this->client->createFolder("INBOX.new")); + + $this->protocol->expects($this->any())->method('deleteFolder')->willReturn(Response::empty()->setResponse([ + 0 => "OK Delete completed (0.007 + 0.000 + 0.006 secs).\r\n", + ])); + self::assertNotEmpty($this->client->deleteFolder("INBOX.new")); + + self::assertInstanceOf(Folder::class, $this->client->getFolderByPath("INBOX.new")); + self::assertInstanceOf(Folder::class, $this->client->getFolderByName("new")); + self::assertInstanceOf(Folder::class, $this->client->getFolder("INBOX.new", ".")); + self::assertInstanceOf(Folder::class, $this->client->getFolder("new")); + } + + public function testClientId(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('ID')->willReturn(Response::empty()->setResponse([ + 0 => "ID (\"name\" \"Dovecot\")\r\n", + 1 => "OK ID completed (0.001 + 0.000 secs).\r\n" + + ])); + self::assertSame("ID (\"name\" \"Dovecot\")\r\n", $this->client->Id()[0]); + + } + + public function testClientConfig(): void { + $config = $this->client->getConfig(); + self::assertSame("foo@domain.tld", $config["username"]); + self::assertSame("bar", $config["password"]); + self::assertSame("localhost", $config["host"]); + self::assertSame(true, $config["validate_cert"]); + self::assertSame(993, $config["port"]); + + $this->client->setConfig([ + "host" => "domain.tld", + 'password' => 'bar', + ]); + $config = $this->client->getConfig(); + self::assertSame("bar", $config["password"]); + self::assertSame("domain.tld", $config["host"]); + self::assertSame(true, $config["validate_cert"]); + } + + protected function createNewProtocolMockup() { + $this->protocol = $this->createMock(ImapProtocol::class); + + $this->protocol->expects($this->any())->method('connected')->willReturn(true); + $this->protocol->expects($this->any())->method('getConnectionTimeout')->willReturn(30); + + $this->protocol + ->expects($this->any()) + ->method('createStream') + //->will($this->onConsecutiveCalls(true)); + ->willReturn(true); + + $this->client->connection = $this->protocol; + } +} \ No newline at end of file diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php new file mode 100644 index 00000000..29ed8167 --- /dev/null +++ b/tests/HeaderTest.php @@ -0,0 +1,140 @@ +get("subject"); + $returnPath = $header->get("Return-Path"); + /** @var Carbon $date */ + $date = $header->get("date")->first(); + /** @var Address $from */ + $from = $header->get("from")->first(); + /** @var Address $to */ + $to = $header->get("to")->first(); + + self::assertSame($raw_header, $header->raw); + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$header->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("-4.299", (string)$header->get("X-Spam-Score")); + self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$header->get("Message-ID")); + self::assertSame(6, $header->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$header->get("priority")()); + + self::assertSame("Username", $from->personal); + self::assertSame("notifications", $from->mailbox); + self::assertSame("github.com", $from->host); + self::assertSame("notifications@github.com", $from->mail); + self::assertSame("Username ", $from->full); + + self::assertSame("Webklex/php-imap", $to->personal); + self::assertSame("php-imap", $to->mailbox); + self::assertSame("noreply.github.com", $to->host); + self::assertSame("php-imap@noreply.github.com", $to->mail); + self::assertSame("Webklex/php-imap ", $to->full); + + self::assertInstanceOf(Carbon::class, $date); + self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); + + self::assertSame(48, count($header->getAttributes())); + } + + public function testRfc822ParseHeaders() { + $mock = $this->getMockBuilder(Header::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + + $config = new \ReflectionProperty($mock, 'config'); + $config->setValue($mock, ['rfc822' => true]); + + $mockHeader = "Content-Type: text/csv; charset=WINDOWS-1252; name*0=\"TH_Is_a_F ile name example 20221013.c\"; name*1=sv\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Disposition: attachment; filename*0=\"TH_Is_a_F ile name example 20221013.c\"; filename*1=\"sv\"\r\n"; + + $expected = new \stdClass(); + $expected->content_type = 'text/csv; charset=WINDOWS-1252; name*0="TH_Is_a_F ile name example 20221013.c"; name*1=sv'; + $expected->content_transfer_encoding = 'quoted-printable'; + $expected->content_disposition = 'attachment; filename*0="TH_Is_a_F ile name example 20221013.c"; filename*1="sv"'; + + $this->assertEquals($expected, $mock->rfc822_parse_headers($mockHeader)); + } + + public function testExtractHeaderExtensions() { + $mock = $this->getMockBuilder(Header::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + + $method = new \ReflectionMethod($mock, 'extractHeaderExtensions'); + $method->setAccessible(true); + + $mockAttributes = [ + 'content_type' => new Attribute('content_type', 'text/csv; charset=WINDOWS-1252; name*0="TH_Is_a_F ile name example 20221013.c"; name*1=sv'), + 'content_transfer_encoding' => new Attribute('content_transfer_encoding', 'quoted-printable'), + 'content_disposition' => new Attribute('content_disposition', 'attachment; filename*0="TH_Is_a_F ile name example 20221013.c"; filename*1="sv"; attribute_test=attribute_test_value'), + ]; + + $attributes = new \ReflectionProperty($mock, 'attributes'); + $attributes->setValue($mock, $mockAttributes); + + $method->invoke($mock); + + $this->assertArrayHasKey('filename', $mock->getAttributes()); + $this->assertArrayNotHasKey('filename*0', $mock->getAttributes()); + $this->assertEquals('TH_Is_a_F ile name example 20221013.csv', $mock->get('filename')); + + $this->assertArrayHasKey('name', $mock->getAttributes()); + $this->assertArrayNotHasKey('name*0', $mock->getAttributes()); + $this->assertEquals('TH_Is_a_F ile name example 20221013.csv', $mock->get('name')); + + $this->assertArrayHasKey('content_type', $mock->getAttributes()); + $this->assertEquals('text/csv', $mock->get('content_type')->last()); + + $this->assertArrayHasKey('charset', $mock->getAttributes()); + $this->assertEquals('WINDOWS-1252', $mock->get('charset')->last()); + + $this->assertArrayHasKey('content_transfer_encoding', $mock->getAttributes()); + $this->assertEquals('quoted-printable', $mock->get('content_transfer_encoding')); + + $this->assertArrayHasKey('content_disposition', $mock->getAttributes()); + $this->assertEquals('attachment', $mock->get('content_disposition')->last()); + $this->assertEquals('quoted-printable', $mock->get('content_transfer_encoding')); + + $this->assertArrayHasKey('attribute_test', $mock->getAttributes()); + $this->assertEquals('attribute_test_value', $mock->get('attribute_test')); + } +} \ No newline at end of file diff --git a/tests/ImapProtocolTest.php b/tests/ImapProtocolTest.php new file mode 100644 index 00000000..4d744332 --- /dev/null +++ b/tests/ImapProtocolTest.php @@ -0,0 +1,39 @@ +getCertValidation()); + self::assertSame("", $protocol->getEncryption()); + + $protocol->setCertValidation(true); + $protocol->setEncryption("ssl"); + + self::assertSame(true, $protocol->getCertValidation()); + self::assertSame("ssl", $protocol->getEncryption()); + } +} \ No newline at end of file diff --git a/tests/MessageTest.php b/tests/MessageTest.php new file mode 100644 index 00000000..9ade7239 --- /dev/null +++ b/tests/MessageTest.php @@ -0,0 +1,224 @@ +client = new Client([ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]); + } + + /** + * Message test + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageNotFoundException + * @throws ReflectionException + * @throws RuntimeException + * @throws ResponseException + */ + public function testMessage(): void { + $this->createNewProtocolMockup(); + + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"])); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $this->protocol->expects($this->any())->method('getUid')->willReturn(Response::empty()->setResult(22)); + $this->protocol->expects($this->any())->method('getMessageNumber')->willReturn(Response::empty()->setResult(21)); + $this->protocol->expects($this->any())->method('flags')->willReturn(Response::empty()->setResult([22 => [0 => "\\Seen"]])); + + self::assertNotEmpty($this->client->openFolder("INBOX")); + + $message = Message::make(22, null, $this->client, $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID); + + self::assertInstanceOf(Client::class, $message->getClient()); + self::assertSame(22, $message->uid); + self::assertSame(21, $message->msgn); + self::assertContains("Seen", $message->flags()->toArray()); + + $subject = $message->get("subject"); + $returnPath = $message->get("Return-Path"); + + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); + self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); + self::assertSame(6, $message->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); + } + + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ImapServerErrorException + * @throws ConnectionFailedException + * @throws AuthFailedException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws InvalidMessageDateException + * @throws ReflectionException + */ + public function testLoadMessageFromFile(): void { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"]); + $message = Message::fromFile($filename); + + $subject = $message->get("subject"); + $returnPath = $message->get("Return-Path"); + + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); + self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); + self::assertSame(6, $message->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); + + self::assertNull($message->getClient()); + self::assertSame(0, $message->uid); + + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "example_attachment.eml"]); + $message = Message::fromFile($filename); + + $subject = $message->get("subject"); + $returnPath = $message->get("Return-Path"); + + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", $subject->toString()); + self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", (string)$message->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("1.103", (string)$message->get("X-Spam-Score")); + self::assertSame("d3a5e91963cb805cee975687d5acb1c6@swift.generated", (string)$message->get("Message-ID")); + self::assertSame(5, $message->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_HIGHEST, (int)$message->get("priority")()); + + self::assertNull($message->getClient()); + self::assertSame(0, $message->uid); + self::assertSame(1, $message->getAttachments()->count()); + + /** @var Attachment $attachment */ + $attachment = $message->getAttachments()->first(); + self::assertSame("attachment", $attachment->disposition); + self::assertSame("znk551MP3TP3WPp9Kl1gnLErrWEgkJFAtvaKqkTgrk3dKI8dX38YT8BaVxRcOERN", $attachment->content); + self::assertSame("application/octet-stream", $attachment->content_type); + self::assertSame("6mfFxiU5Yhv9WYJx.txt", $attachment->name); + self::assertSame(1, $attachment->part_number); + self::assertSame("text", $attachment->type); + self::assertNotEmpty($attachment->id); + self::assertSame(90, $attachment->size); + self::assertSame("txt", $attachment->getExtension()); + self::assertInstanceOf(Message::class, $attachment->getMessage()); + self::assertSame("text/plain", $attachment->getMimeType()); + + } + + protected function createNewProtocolMockup() { + $this->protocol = $this->createMock(ImapProtocol::class); + + $this->protocol->expects($this->any())->method('createStream')->willReturn(true); + $this->protocol->expects($this->any())->method('connected')->willReturn(true); + $this->protocol->expects($this->any())->method('getConnectionTimeout')->willReturn(30); + $this->protocol->expects($this->any())->method('logout')->willReturn(Response::empty()->setResponse([ + 0 => "BYE Logging out\r\n", + 1 => "OK Logout completed (0.001 + 0.000 secs).\r\n", + ])); + $this->protocol->expects($this->any())->method('selectFolder')->willReturn(Response::empty()->setResponse([ + "flags" => [ + 0 => [ + 0 => "\Answered", + 1 => "\Flagged", + 2 => "\Deleted", + 3 => "\Seen", + 4 => "\Draft", + 5 => "NonJunk", + 6 => "unknown-1", + ], + ], + "exists" => 139, + "recent" => 0, + "unseen" => 94, + "uidvalidity" => 1488899637, + "uidnext" => 278, + ])); + + $this->client->connection = $this->protocol; + } +} \ No newline at end of file diff --git a/tests/PartTest.php b/tests/PartTest.php new file mode 100644 index 00000000..f4653519 --- /dev/null +++ b/tests/PartTest.php @@ -0,0 +1,94 @@ +charset); + self::assertSame("text/plain", $part->content_type); + self::assertSame(12, $part->bytes); + self::assertSame(0, $part->part_number); + self::assertSame(false, $part->ifdisposition); + self::assertSame(false, $part->isAttachment()); + self::assertSame("Any updates?", $part->content); + self::assertSame(IMAP::MESSAGE_TYPE_TEXT, $part->type); + self::assertSame(IMAP::MESSAGE_ENC_7BIT, $part->encoding); + } + + /** + * Test parsing a html Part + * @throws InvalidMessageDateException + */ + public function testHTMLPart(): void { + $raw_headers = "Content-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; + $raw_body = "\r\n

\r\n

Any updates?

"; + + $headers = new Header($raw_headers); + $part = new Part($raw_body, $headers, 0); + + self::assertSame("UTF-8", $part->charset); + self::assertSame("text/html", $part->content_type); + self::assertSame(39, $part->bytes); + self::assertSame(0, $part->part_number); + self::assertSame(false, $part->ifdisposition); + self::assertSame(false, $part->isAttachment()); + self::assertSame("

\r\n

Any updates?

", $part->content); + self::assertSame(IMAP::MESSAGE_TYPE_TEXT, $part->type); + self::assertSame(IMAP::MESSAGE_ENC_7BIT, $part->encoding); + } + + /** + * Test parsing a html Part + * @throws InvalidMessageDateException + */ + public function testBase64Part(): void { + $raw_headers = "Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt\r\n"; + $raw_body = "em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW\r\neFJjT0VSTg=="; + + $headers = new Header($raw_headers); + $part = new Part($raw_body, $headers, 0); + + self::assertSame("", $part->charset); + self::assertSame("application/octet-stream", $part->content_type); + self::assertSame(90, $part->bytes); + self::assertSame(0, $part->part_number); + self::assertSame("znk551MP3TP3WPp9Kl1gnLErrWEgkJFAtvaKqkTgrk3dKI8dX38YT8BaVxRcOERN", base64_decode($part->content)); + self::assertSame(true, $part->ifdisposition); + self::assertSame("attachment", $part->disposition); + self::assertSame("6mfFxiU5Yhv9WYJx.txt", $part->name); + self::assertSame("6mfFxiU5Yhv9WYJx.txt", $part->filename); + self::assertSame(true, $part->isAttachment()); + self::assertSame(IMAP::MESSAGE_TYPE_TEXT, $part->type); + self::assertSame(IMAP::MESSAGE_ENC_BASE64, $part->encoding); + } +} \ No newline at end of file diff --git a/tests/StructureTest.php b/tests/StructureTest.php new file mode 100644 index 00000000..a1df098b --- /dev/null +++ b/tests/StructureTest.php @@ -0,0 +1,55 @@ +parts)); + + $textPart = $structure->parts[0]; + + self::assertSame("UTF-8", $textPart->charset); + self::assertSame("text/plain", $textPart->content_type); + self::assertSame(278, $textPart->bytes); + + $htmlPart = $structure->parts[1]; + + self::assertSame("UTF-8", $htmlPart->charset); + self::assertSame("text/html", $htmlPart->content_type); + self::assertSame(1478, $htmlPart->bytes); + } +} \ No newline at end of file diff --git a/tests/messages/1366671050@github.com.eml b/tests/messages/1366671050@github.com.eml new file mode 100644 index 00000000..91f51cf8 --- /dev/null +++ b/tests/messages/1366671050@github.com.eml @@ -0,0 +1,108 @@ +Return-Path: +Delivered-To: someone@domain.tld +Received: from mx.domain.tld + by localhost with LMTP + id SABVMNfGqWP+PAAA0J78UA + (envelope-from ) + for ; Mon, 26 Dec 2022 17:07:51 +0100 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id C3828140227 + for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: -4.299 +X-Spam-Level: +X-Spam-Status: No, score=-4.299 required=6.31 tests=[BAYES_00=-1.9, + DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, + DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_IMAGE_ONLY_16=1.092, + HTML_MESSAGE=0.001, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_MED=-2.3, + RCVD_IN_MSPIKE_H2=-0.001, T_KAM_HTML_FONT_INVALID=0.01] + autolearn=ham autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id JcIS9RuNBTNx for ; + Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 + for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) + by smtp.github.com (Postfix) with ESMTP id 176985E0200 + for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=github.com; + s=pf2014; t=1672070834; + bh=v91TPiLpM/cpUb4lgt2NMIUfM4HOIxCEWMR1+JTco+Q=; + h=Date:From:Reply-To:To:Cc:In-Reply-To:References:Subject:List-ID: + List-Archive:List-Post:List-Unsubscribe:From; + b=jW4Tac9IjWAPbEImyiYf1bzGLzY3ceohVbBg1V8BlpMTQ+o+yY3YB0eOe20hAsqZR + jrDjArx7rKQcslqBFL/b2B1C51rHuCBrz2cccLEERu9l/u0mTGCxTNtCRXHbCKbnR1 + VLWBeFLjATHth83kK6Kt7lkVuty+G3V1B6ZKPhCI= +Date: Mon, 26 Dec 2022 08:07:14 -0800 +From: Username +Reply-To: Webklex/php-imap +To: Webklex/php-imap +Cc: Subscribed +Message-ID: +In-Reply-To: +References: +Subject: Re: [Webklex/php-imap] Read all folders? (Issue #349) +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="--==_mimepart_63a9c6b293fe_65b5c71014155a"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +Precedence: list +X-GitHub-Sender: consigliere23 +X-GitHub-Recipient: Webklex +X-GitHub-Reason: subscribed +List-ID: Webklex/php-imap +List-Archive: https://github.com/Webklex/php-imap +List-Post: +List-Unsubscribe: , + +X-Auto-Response-Suppress: All +X-GitHub-Recipient-Address: someone@domain.tld + + +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Any updates? + +-- +Reply to this email directly or view it on GitHub: +https://github.com/Webklex/php-imap/issues/349#issuecomment-1365266070 +You are receiving this because you are subscribed to this thread. + +Message ID: +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/html; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +

+

Any updates?

+ +


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <Webklex/php-imap/issues/349/1365266070@github.com>

+ +----==_mimepart_63a9c6b293fe_65b5c71014155a-- diff --git a/tests/messages/example_attachment.eml b/tests/messages/example_attachment.eml new file mode 100644 index 00000000..a452ae79 --- /dev/null +++ b/tests/messages/example_attachment.eml @@ -0,0 +1,56 @@ +Return-Path: +Delivered-To: +Received: from mx.domain.tld + by localhost (Dovecot) with LMTP id T7mwLn3ddlvKWwAA0J78UA + for ; Fri, 17 Aug 2018 16:36:45 +0200 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id B642913BA0BE2 + for ; Fri, 17 Aug 2018 16:36:45 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: 1.103 +X-Spam-Level: * +X-Spam-Status: No, score=1.103 required=6.31 tests=[ALL_TRUSTED=-1, + OBFU_TEXT_ATTACH=1, TRACKER_ID=1.102, TVD_SPACE_RATIO=0.001] + autolearn=no autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id L8E9vyX80d44 for ; + Fri, 17 Aug 2018 16:36:39 +0200 (CEST) +Received: from [127.0.0.1] (ip.dynamic.domain.tld [192.168.0.24]) + (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPSA id EF01E13BA0BD7 + for ; Fri, 17 Aug 2018 16:36:38 +0200 (CEST) +Sender: testsender +Message-ID: +Date: Fri, 17 Aug 2018 14:36:24 +0000 +Subject: ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk +From: testfrom +Reply-To: testreply_to +To: testnameto +Cc: testnamecc +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_" +X-Priority: 1 (Highest) + + + + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +n1IaXFkbeqKyg4lYToaJ3u1Ond2EDrN3UWuiLFNjOLJEAabSYagYQaOHtV5QDlZE + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt + +em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW +eFJjT0VSTg== + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_-- + From 78700eaba152c7cd8e63cb734e479a1c54d34723 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:27:51 +0100 Subject: [PATCH 372/600] old test removed --- tests/InitialTest.php | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 tests/InitialTest.php diff --git a/tests/InitialTest.php b/tests/InitialTest.php deleted file mode 100644 index fb854db4..00000000 --- a/tests/InitialTest.php +++ /dev/null @@ -1,26 +0,0 @@ -cm = new ClientManager(); - } - - public function testConfigDefaultAccount() { - $this->assertEquals("default", ClientManager::get("default")); - } -} \ No newline at end of file From 4e05e515ef91ccf48508969d53796ed81424f075 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:29:55 +0100 Subject: [PATCH 373/600] Protocol attribute accessors added to retrieve the resource stream, meta information, encryption and stream state --- src/Connection/Protocols/Protocol.php | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 04cba85a..3f4b83ba 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -297,4 +297,66 @@ public function enableUidCache(): void { public function disableUidCache(): void { $this->enable_uid_cache = false; } + + /** + * Set the encryption method + * @param string $encryption + * + * @return void + */ + public function setEncryption(string $encryption): void { + $this->encryption = $encryption; + } + + /** + * Get the encryption method + * @return string + */ + public function getEncryption(): string { + return $this->encryption; + } + + /** + * Check if the current session is connected + * + * @return bool + */ + public function connected(): bool { + return (bool)$this->stream; + } + + /** + * Retrieves header/meta data from the resource stream + * + * @return array + */ + public function meta(): array { + if (!$this->stream) { + return [ + "crypto" => [ + "protocol" => "", + "cipher_name" => "", + "cipher_bits" => 0, + "cipher_version" => "", + ], + "timed_out" => true, + "blocked" => true, + "eof" => true, + "stream_type" => "tcp_socket/unknown", + "mode" => "c", + "unread_bytes" => 0, + "seekable" => false, + ]; + } + return stream_get_meta_data($this->stream); + } + + /** + * Get the resource stream + * + * @return mixed + */ + public function getStream(): mixed { + return $this->stream; + } } From a9650805ebd4d65d04445f3c6d7037fdb0a35d5b Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:35:50 +0100 Subject: [PATCH 374/600] Formatting updated --- src/Client.php | 41 ++++++----- src/ClientManager.php | 49 ++++++------- src/Connection/Protocols/ImapProtocol.php | 32 +++++---- src/Connection/Protocols/LegacyProtocol.php | 20 +++--- src/Folder.php | 14 ++-- src/Header.php | 1 + src/Message.php | 76 ++++++++++----------- src/Part.php | 20 +++--- src/Query/Query.php | 10 +-- src/Structure.php | 6 +- 10 files changed, 137 insertions(+), 132 deletions(-) diff --git a/src/Client.php b/src/Client.php index dd7f1213..0ab9d0a2 100755 --- a/src/Client.php +++ b/src/Client.php @@ -305,39 +305,42 @@ protected function setMaskFromConfig($config): void { throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']); } }else{ - if(class_exists($default_config['message'])) { - $this->default_message_mask = $default_config['message']; + $default_mask = ClientManager::getMask("message"); + if($default_mask != ""){ + $this->default_message_mask = $default_mask; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$default_config['message']); + throw new MaskNotFoundException("Unknown message mask provided"); } } if(isset($config['masks']['attachment'])) { if(class_exists($config['masks']['attachment'])) { $this->default_attachment_mask = $config['masks']['attachment']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['attachment']); + throw new MaskNotFoundException("Unknown mask provided: ". $config['masks']['attachment']); } }else{ - if(class_exists($default_config['attachment'])) { - $this->default_attachment_mask = $default_config['attachment']; + $default_mask = ClientManager::getMask("attachment"); + if($default_mask != ""){ + $this->default_attachment_mask = $default_mask; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']); + throw new MaskNotFoundException("Unknown attachment mask provided"); } } }else{ - if(class_exists($default_config['message'])) { - $this->default_message_mask = $default_config['message']; + $default_mask = ClientManager::getMask("message"); + if($default_mask != ""){ + $this->default_message_mask = $default_mask; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$default_config['message']); + throw new MaskNotFoundException("Unknown message mask provided"); } - if(class_exists($default_config['attachment'])) { - $this->default_attachment_mask = $default_config['attachment']; + $default_mask = ClientManager::getMask("attachment"); + if($default_mask != ""){ + $this->default_attachment_mask = $default_mask; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$default_config['attachment']); + throw new MaskNotFoundException("Unknown attachment mask provided"); } } - } /** @@ -375,10 +378,16 @@ public function isConnected(): bool { * @throws RuntimeException * @throws ResponseException */ - public function checkConnection() { - if (!$this->isConnected()) { + public function checkConnection(): bool { + try { + if (!$this->isConnected()) { + $this->connect(); + return true; + } + } catch (\Throwable) { $this->connect(); } + return false; } /** diff --git a/src/ClientManager.php b/src/ClientManager.php index 67160b3c..7f724ffe 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -76,17 +76,17 @@ public function make(array $config): Client { public static function get(string $key, $default = null): mixed { $parts = explode('.', $key); $value = null; - foreach($parts as $part) { - if($value === null) { - if(isset(self::$config[$part])) { + foreach ($parts as $part) { + if ($value === null) { + if (isset(self::$config[$part])) { $value = self::$config[$part]; - }else{ + } else { break; } - }else{ - if(isset($value[$part])) { + } else { + if (isset($value[$part])) { $value = $value[$part]; - }else{ + } else { break; } } @@ -154,8 +154,9 @@ protected function getClientConfig(?string $name): array { if ($name === null || $name === 'null' || $name === "") { return ['driver' => 'null']; } + $account = self::$config["accounts"][$name] ?? []; - return is_array(self::$config["accounts"][$name]) ? self::$config["accounts"][$name] : []; + return is_array($account) ? $account : []; } /** @@ -191,27 +192,27 @@ public function setDefaultAccount(string $name): void { */ public function setConfig(array|string $config): ClientManager { - if(is_array($config) === false) { + if (is_array($config) === false) { $config = require $config; } $config_key = 'imap'; - $path = __DIR__.'/config/'.$config_key.'.php'; + $path = __DIR__ . '/config/' . $config_key . '.php'; $vendor_config = require $path; $config = $this->array_merge_recursive_distinct($vendor_config, $config); - if(is_array($config)){ - if(isset($config['default'])){ - if(isset($config['accounts']) && $config['default']){ + if (is_array($config)) { + if (isset($config['default'])) { + if (isset($config['accounts']) && $config['default']) { $default_config = $vendor_config['accounts']['default']; - if(isset($config['accounts'][$config['default']])){ + if (isset($config['accounts'][$config['default']])) { $default_config = array_merge($default_config, $config['accounts'][$config['default']]); } - if(is_array($config['accounts'])){ - foreach($config['accounts'] as $account_key => $account){ + if (is_array($config['accounts'])) { + foreach ($config['accounts'] as $account_key => $account) { $config['accounts'][$account_key] = array_merge($default_config, $account); } } @@ -250,20 +251,20 @@ private function array_merge_recursive_distinct(): mixed { return array_keys($arr) !== range(0, count($arr) - 1); }; - if(!is_array($base)) $base = empty($base) ? array() : array($base); + if (!is_array($base)) $base = empty($base) ? array() : array($base); - foreach($arrays as $append) { + foreach ($arrays as $append) { - if(!is_array($append)) $append = array($append); + if (!is_array($append)) $append = array($append); - foreach($append as $key => $value) { + foreach ($append as $key => $value) { - if(!array_key_exists($key, $base) and !is_numeric($key)) { + if (!array_key_exists($key, $base) and !is_numeric($key)) { $base[$key] = $value; continue; } - if( + if ( ( is_array($value) && $isAssoc($value) @@ -277,8 +278,8 @@ private function array_merge_recursive_distinct(): mixed { // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] // results in $resultConfig['dispositions'] = ['attachment', 'inline'] $base[$key] = $this->array_merge_recursive_distinct($base[$key], $value); - } else if(is_numeric($key)) { - if(!in_array($value, $base)) $base[] = $value; + } else if (is_numeric($key)) { + if (!in_array($value, $base)) $base[] = $value; } else { $base[$key] = $value; } diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 2e8edf43..37fc6ba3 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -191,7 +191,7 @@ protected function decodeLine(Response $response, string $line): array { $token = substr($token, 1); } if ($token[0] == '"') { - if (preg_match('%^\(*"((.|\\\\|\\")*?)" *%', $line, $matches)) { + if (preg_match('%^\(*"((.|\\\\|\")*?)" *%', $line, $matches)) { $tokens[] = $matches[1]; $line = substr($line, strlen($matches[0])); continue; @@ -203,14 +203,14 @@ protected function decodeLine(Response $response, string $line): array { if (is_numeric($chars)) { $token = ''; while (strlen($token) < $chars) { - $token .= $this->nextLine(); + $token .= $this->nextLine($response); } $line = ''; if (strlen($token) > $chars) { $line = substr($token, $chars); $token = substr($token, 0, $chars); } else { - $line .= $this->nextLine(); + $line .= $this->nextLine($response); } $tokens[] = $token; $line = trim($line) . ' '; @@ -229,7 +229,7 @@ protected function decodeLine(Response $response, string $line): array { } $token = $tokens; $tokens = array_pop($stack); - // special handline if more than one closing brace + // special handling if more than one closing brace while ($braces-- > 0) { $tokens[] = $token; $token = $tokens; @@ -633,11 +633,11 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in if (is_array($from)) { $set = implode(',', $from); } elseif ($to === null) { - $set = (int)$from; - } elseif ($to === INF) { - $set = (int)$from . ':*'; + $set = $from; + } elseif ($to == INF) { + $set = $from . ':*'; } else { - $set = (int)$from . ':' . (int)$to; + $set = $from . ':' . (int)$to; } $items = (array)$items; @@ -652,6 +652,9 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in continue; } + $uidKey = 0; + $data = []; + // find array key of UID value; try the last elements, or search for it if ($uid) { $count = count($tokens[2]); @@ -682,7 +685,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in $data = $tokens[2][3]; } else { $expectedResponse = 0; - // maybe the server send an other field we didn't wanted + // maybe the server send another field we didn't wanted $count = count($tokens[2]); // we start with 2, because 0 was already checked for ($i = 2; $i < $count; $i += 2) { @@ -698,7 +701,6 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } } } else { - $data = []; while (key($tokens[2]) !== null) { $data[current($tokens[2])] = next($tokens[2]); next($tokens[2]); @@ -713,7 +715,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } if ($uid) { $result[$tokens[2][$uidKey]] = $data; - }else{ + } else { $result[$tokens[0]] = $data; } } @@ -873,7 +875,7 @@ public function store( $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("STORE", $uid); - $item = ($mode == '-' ? "-" : "+").($item === null ? "FLAGS" : $item).($silent ? '.SILENT' : ""); + $item = ($mode == '-' ? "-" : "+") . ($item === null ? "FLAGS" : $item) . ($silent ? '.SILENT' : ""); $response = $this->requestAndResponse($command, [$set, $item, $flags], $silent); @@ -1029,9 +1031,9 @@ public function ID($ids = null): Response { if (is_array($ids) && !empty($ids)) { $token = "("; foreach ($ids as $id) { - $token .= '"'.$id.'" '; + $token .= '"' . $id . '" '; } - $token = rtrim($token).")"; + $token = rtrim($token) . ")"; } return $this->requestAndResponse("ID", [$token], true); @@ -1238,7 +1240,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp $ids = []; foreach ($response->data() as $msgn => $v) { $id = $uid ? $v : $msgn; - if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){ + if (($to >= $id && $from <= $id) || ($to === "*" && $from <= $id)) { $ids[] = $id; } } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index bb8e957d..086869b6 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -90,11 +90,11 @@ public function login(string $user, string $password): Response { throw new AuthFailedException($message); } - if(!$this->stream) { - $errors = \imap_errors(); - $message = implode("; ", (is_array($errors) ? $errors : array())); - throw new AuthFailedException($message); - } + if (!$this->stream) { + $errors = \imap_errors(); + $message = implode("; ", (is_array($errors) ? $errors : array())); + throw new AuthFailedException($message); + } $errors = \imap_errors(); $response->addCommand("imap_errors"); @@ -135,12 +135,12 @@ public function authenticate(string $user, string $token): Response { * @return string */ protected function getAddress(): string { - $address = "{".$this->host.":".$this->port."/".$this->protocol; + $address = "{" . $this->host . ":" . $this->port . "/" . $this->protocol; if (!$this->cert_validation) { $address .= '/novalidate-cert'; } - if (in_array($this->encryption,['tls', 'notls', 'ssl'])) { - $address .= '/'.$this->encryption; + if (in_array($this->encryption, ['tls', 'notls', 'ssl'])) { + $address .= '/' . $this->encryption; } elseif ($this->encryption === "starttls") { $address .= '/tls'; } @@ -704,14 +704,14 @@ public function search(array $params, int|string $uid = IMAP::ST_UID): Response /** * Enable the debug mode */ - public function enableDebug(){ + public function enableDebug() { $this->debug = true; } /** * Disable the debug mode */ - public function disableDebug(){ + public function disableDebug() { $this->debug = false; } diff --git a/src/Folder.php b/src/Folder.php index 732d229d..63cfd59c 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -135,9 +135,9 @@ public function __construct(Client $client, string $folder_name, string $delimit $this->events["folder"] = $client->getDefaultEvents("folder"); $this->setDelimiter($delimiter); - $this->path = $folder_name; - $this->full_name = $this->decodeName($folder_name); - $this->name = $this->getSimpleName($this->delimiter, $this->full_name); + $this->path = $folder_name; + $this->full_name = $this->decodeName($folder_name); + $this->name = $this->getSimpleName($this->delimiter, $this->full_name); $this->parseAttributes($attributes); } @@ -245,9 +245,9 @@ protected function getSimpleName($delimiter, $full_name): string|bool { */ protected function parseAttributes($attributes): void { $this->no_inferiors = in_array('\NoInferiors', $attributes); - $this->no_select = in_array('\NoSelect', $attributes); - $this->marked = in_array('\Marked', $attributes); - $this->referral = in_array('\Referral', $attributes); + $this->no_select = in_array('\NoSelect', $attributes); + $this->marked = in_array('\Marked', $attributes); + $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); } @@ -321,7 +321,7 @@ public function appendMessage(string $message, array $options = null, Carbon|str * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if ($internal_date instanceof Carbon){ + if ($internal_date instanceof Carbon) { $internal_date = $internal_date->format('d-M-Y H:i:s O'); } diff --git a/src/Header.php b/src/Header.php index 05084682..0f8566b3 100644 --- a/src/Header.php +++ b/src/Header.php @@ -712,6 +712,7 @@ private function parseDate(object $header): void { break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: $date = str_replace(',', '', $date); + break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: diff --git a/src/Message.php b/src/Message.php index 59904922..cd9dc6c4 100755 --- a/src/Message.php +++ b/src/Message.php @@ -205,7 +205,7 @@ public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_ $this->boot(); $default_mask = $client->getDefaultMessageMask(); - if($default_mask != null) { + if ($default_mask != null) { $this->mask = $default_mask; } $this->events["message"] = $client->getDefaultEvents("message"); @@ -269,13 +269,13 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw $instance->boot(); $default_mask = $client->getDefaultMessageMask(); - if($default_mask != null) { + if ($default_mask != null) { $instance->setMask($default_mask); } $instance->setEvents([ - "message" => $client->getDefaultEvents("message"), - "flag" => $client->getDefaultEvents("flag"), - ]); + "message" => $client->getDefaultEvents("message"), + "flag" => $client->getDefaultEvents("flag"), + ]); $instance->setFolderPath($client->getFolderPath()); $instance->setSequence($sequence); $instance->setFetchOption($fetch_options); @@ -320,19 +320,19 @@ public function boot(): void { * @throws ResponseException */ public function __call(string $method, array $arguments) { - if(strtolower(substr($method, 0, 3)) === 'get') { + if (strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); return $this->get($name); - }elseif (strtolower(substr($method, 0, 3)) === 'set') { + } elseif (strtolower(substr($method, 0, 3)) === 'set') { $name = Str::snake(substr($method, 3)); - if(in_array($name, array_keys($this->attributes))) { + if (in_array($name, array_keys($this->attributes))) { return $this->__set($name, array_pop($arguments)); } } - throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported'); + throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported'); } /** @@ -604,7 +604,7 @@ private function fetchStructure(Structure $structure): void { private function fetchPart(Part $part): void { if ($part->isAttachment()) { $this->fetchAttachment($part); - }else{ + } else { $encoding = $this->getEncoding($part); $content = $this->decodeString($part->content, $part->encoding); @@ -628,8 +628,8 @@ private function fetchPart(Part $part): void { $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; if (isset($this->bodies[$subtype])) { - $this->bodies[$subtype] .= "\n".$content; - }else{ + $this->bodies[$subtype] .= "\n" . $content; + } else { $this->bodies[$subtype] = $content; } } @@ -778,7 +778,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = } if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') { - return @iconv($from, $to.'//IGNORE', $str); + return @iconv($from, $to . '//IGNORE', $str); } else { if (!$from) { return mb_convert_encoding($str, $to); @@ -800,9 +800,9 @@ public function getEncoding(object|string $structure): string { return EncodingAliases::get($parameter->value, "ISO-8859-2"); } } - }elseif (property_exists($structure, 'charset')){ + } elseif (property_exists($structure, 'charset')) { return EncodingAliases::get($structure->charset, "ISO-8859-2"); - }elseif (is_string($structure) === true){ + } elseif (is_string($structure) === true) { return mb_detect_encoding($structure); } @@ -847,7 +847,7 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ - foreach($thread as $message) { + foreach ($thread as $message) { if ($message->message_id->first() == $this->message_id->first()) { return $thread; } @@ -858,7 +858,7 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = $this->fetchThreadByInReplyTo($thread, $this->message_id, $sent_folder, $folder, $sent_folder); if (is_array($this->in_reply_to)) { - foreach($this->in_reply_to as $in_reply_to) { + foreach ($this->in_reply_to as $in_reply_to) { $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder); $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder); } @@ -886,11 +886,11 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = */ protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in_reply_to, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder): void { $primary_folder->query()->inReplyTo($in_reply_to) - ->setFetchBody($this->getFetchBodyOption()) - ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ - /** @var Message $message */ - $message->thread($sent_folder, $thread, $secondary_folder); - }); + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use (&$thread, $secondary_folder, $sent_folder) { + /** @var Message $message */ + $message->thread($sent_folder, $thread, $secondary_folder); + }); } /** @@ -912,11 +912,11 @@ protected function fetchThreadByInReplyTo(MessageCollection &$thread, string $in */ protected function fetchThreadByMessageId(MessageCollection &$thread, string $message_id, Folder $primary_folder, Folder $secondary_folder, Folder $sent_folder): void { $primary_folder->query()->messageId($message_id) - ->setFetchBody($this->getFetchBodyOption()) - ->leaveUnread()->get()->each(function($message) use(&$thread, $secondary_folder, $sent_folder){ - /** @var Message $message */ - $message->thread($sent_folder, $thread, $secondary_folder); - }); + ->setFetchBody($this->getFetchBodyOption()) + ->leaveUnread()->get()->each(function($message) use (&$thread, $secondary_folder, $sent_folder) { + /** @var Message $message */ + $message->thread($sent_folder, $thread, $secondary_folder); + }); } /** @@ -1059,11 +1059,11 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo */ public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false): bool { $status = $this->setFlag("Deleted"); - if($force_move) { - $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"]: $trash_path; - $status = $this->move($trash_path); + if ($force_move) { + $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"] : $trash_path; + $this->move($trash_path); } - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $event = $this->getEvent("message", "deleted"); $event::dispatch($this); @@ -1087,7 +1087,7 @@ public function delete(bool $expunge = true, string $trash_path = null, bool $fo */ public function restore(bool $expunge = true): bool { $status = $this->unsetFlag("Deleted"); - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $event = $this->getEvent("message", "restored"); $event::dispatch($this); @@ -1111,7 +1111,7 @@ public function restore(bool $expunge = true): bool { */ public function setFlag(array|string $flag): bool { $this->client->openFolder($this->folder_path); - $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); + $flag = "\\" . trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID)->validatedData(); @@ -1143,7 +1143,7 @@ public function setFlag(array|string $flag): bool { public function unsetFlag(array|string $flag): bool { $this->client->openFolder($this->folder_path); - $flag = "\\".trim(is_array($flag) ? implode(" \\", $flag) : $flag); + $flag = "\\" . trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID)->validatedData(); @@ -1348,7 +1348,7 @@ public function getAttributes(): array { * @return Message */ public function setMask($mask): Message { - if(class_exists($mask)){ + if (class_exists($mask)) { $this->mask = $mask; } @@ -1373,11 +1373,11 @@ public function getMask(): string { */ public function mask(mixed $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; - if(class_exists($mask)){ + if (class_exists($mask)) { return new $mask($this); } - throw new MaskNotFoundException("Unknown mask provided: ".$mask); + throw new MaskNotFoundException("Unknown mask provided: " . $mask); } /** @@ -1524,7 +1524,7 @@ public function setSequenceId($uid, int $msglist = null): void { if ($this->getSequence() === IMAP::ST_UID) { $this->setUid($uid); $this->setMsglist($msglist); - }else{ + } else { $this->setMsgn($uid, $msglist); } } diff --git a/src/Part.php b/src/Part.php index e369b93f..b5c25032 100644 --- a/src/Part.php +++ b/src/Part.php @@ -170,33 +170,29 @@ protected function parse(): void { $this->parseDescription(); $this->parseEncoding(); - $this->charset = $this->header->get("charset"); + $this->charset = $this->header->get("charset")->first(); $this->name = $this->header->get("name"); $this->filename = $this->header->get("filename"); - if(!empty($this->header->get("id"))) { + if($this->header->get("id")->exist()) { $this->id = $this->header->get("id"); - } else if(!empty($this->header->get("x_attachment_id"))){ + } else if($this->header->get("x_attachment_id")->exist()){ $this->id = $this->header->get("x_attachment_id"); - } else if(!empty($this->header->get("content_id"))){ + } else if($this->header->get("content_id")->exist()){ $this->id = strtr($this->header->get("content_id"), [ '<' => '', '>' => '' ]); } - $content_types = $this->header->get("content_type"); + $content_types = $this->header->get("content_type")->all(); if(!empty($content_types)){ $this->subtype = $this->parseSubtype($content_types); - $content_type = $content_types; - if (is_array($content_types)) { - $content_type = $content_types[0]; - } + $content_type = $content_types[0]; $parts = explode(';', $content_type); $this->content_type = trim($parts[0]); } - $this->content = trim(rtrim($body)); $this->bytes = strlen($this->content); } @@ -236,7 +232,7 @@ private function parseSubtype($content_type): ?string { return null; } if (($pos = strpos($content_type, "/")) !== false){ - return substr($content_type, $pos + 1); + return substr(explode(";", $content_type)[0], $pos + 1); } return null; } @@ -248,7 +244,7 @@ private function parseDisposition(): void { $content_disposition = $this->header->get("content_disposition")->first(); if($content_disposition) { $this->ifdisposition = true; - $this->disposition = (is_array($content_disposition)) ? implode(' ', $content_disposition) : $content_disposition; + $this->disposition = (is_array($content_disposition)) ? implode(' ', $content_disposition) : explode(";", $content_disposition)[0]; } } diff --git a/src/Query/Query.php b/src/Query/Query.php index 8baef153..6a67f91d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -143,7 +143,7 @@ protected function parse_date(mixed $date): Carbon { try { $date = Carbon::parse($date); - } catch (Exception $e) { + } catch (Exception) { throw new MessageSearchValidationException(); } @@ -428,14 +428,10 @@ public function chunked(callable $callback, int $chunk_size = 10, int $start_chu * @throws ResponseException */ public function paginate(int $per_page = 5, $page = null, string $page_name = 'imap_page'): LengthAwarePaginator { - if ( - $page === null - && isset($_GET[$page_name]) - && $_GET[$page_name] > 0 - ) { + if ($page === null && isset($_GET[$page_name]) && $_GET[$page_name] > 0) { $this->page = intval($_GET[$page_name]); } elseif ($page > 0) { - $this->page = $page; + $this->page = (int)$page; } $this->limit = $per_page; diff --git a/src/Structure.php b/src/Structure.php index fdf9f64a..5ac4a65d 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -85,9 +85,8 @@ protected function parse(): void { /** * Determine the message content type */ - public function findContentType(){ - $content_type = $this->header->get("content_type"); - $content_type = (is_array($content_type)) ? implode(' ', $content_type) : $content_type; + public function findContentType(): void { + $content_type = $this->header->get("content_type")->first(); if($content_type && stripos($content_type, 'multipart') === 0) { $this->type = IMAP::MESSAGE_TYPE_MULTIPART; }else{ @@ -115,6 +114,7 @@ private function parsePart(string $context, int $part_number = 0): array { if (($boundary = $headers->getBoundary()) !== null) { return $this->detectParts($boundary, $body, $part_number); } + return [new Part($body, $headers, $part_number)]; } From 1774dbbbc5d0d3979d43ae5b33d1f5c5f16a58dd Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:36:37 +0100 Subject: [PATCH 375/600] Prevent the structure parsing from parsing an empty part --- src/Structure.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Structure.php b/src/Structure.php index 5ac4a65d..745a6234 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -95,7 +95,7 @@ public function findContentType(): void { } /** - * Find all available headers and return the left over body segment + * Find all available headers and return the leftover body segment * @var string $context * @var integer $part_number * @@ -131,7 +131,7 @@ private function detectParts(string $boundary, string $context, int $part_number $final_parts = []; foreach($base_parts as $ctx) { $ctx = substr($ctx, 2); - if ($ctx !== "--" && $ctx != "") { + if ($ctx !== "--" && $ctx != "" && $ctx != "\r\n") { $parts = $this->parsePart($ctx, $part_number); foreach ($parts as $part) { $final_parts[] = $part; @@ -161,14 +161,4 @@ public function find_parts(): array { return [new Part($this->raw, $this->header)]; } - - /** - * Try to find a boundary if possible - * - * @return string|null - * @Depricated since version 2.4.4 - */ - public function getBoundary(){ - return $this->header->getBoundary(); - } } From 440417f39e125fc4b8db4f6b7bf68bf938620a6a Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:40:14 +0100 Subject: [PATCH 376/600] Comments and docs updated --- src/Client.php | 3 +- src/Connection/Protocols/ImapProtocol.php | 92 ++++++++++----------- src/Connection/Protocols/LegacyProtocol.php | 39 ++++----- src/EncodingAliases.php | 2 +- src/Folder.php | 3 +- src/Header.php | 3 +- src/Message.php | 2 +- src/Query/Query.php | 2 +- 8 files changed, 69 insertions(+), 77 deletions(-) diff --git a/src/Client.php b/src/Client.php index 0ab9d0a2..1560cc5e 100755 --- a/src/Client.php +++ b/src/Client.php @@ -370,6 +370,7 @@ public function isConnected(): bool { /** * Determine if connection was established and connect if not. + * Returns true if the connection was closed and has been reopened. * * @throws ConnectionFailedException * @throws AuthFailedException @@ -659,7 +660,7 @@ public function openFolder(string $folder_path, bool $force_select = false): arr /** * Create a new Folder - * @param string $folder + * @param string $folder_path * @param boolean $expunge * * @return Folder diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 37fc6ba3..e8cd71d8 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -48,9 +48,7 @@ public function __construct(bool $cert_validation = true, mixed $encryption = fa } /** - * @throws ImapBadRequestException - * @throws ImapServerErrorException - * @throws RuntimeException + * Handle the class destruction / tear down */ public function __destruct() { $this->logout(); @@ -382,12 +380,12 @@ public function requestAndResponse(string $command, array $tokens = [], bool $do /** * Escape one or more literals i.e. for sendRequest - * @param string|array $string the literal/-s + * @param array|string $string the literal/-s * * @return string|array escape literals, literals with newline ar returned * as array('{size}', 'string'); */ - public function escapeString($string) { + public function escapeString(array|string $string): array|string { if (func_num_args() < 2) { if (str_contains($string, "\n")) { return ['{' . strlen($string) . '}', $string]; @@ -423,7 +421,7 @@ public function escapeList(array $list): string { /** * Login to a new session. * - * @param string $user username + * @param string $user username * @param string $password password * * @return Response @@ -460,7 +458,7 @@ public function authenticate(string $user, string $token): Response { $is_plus = $this->readLine($response, $tokens, '+', true); if ($is_plus) { // try to log the challenge somewhere where it can be found - error_log("got an extra server challenge: $response"); + error_log("got an extra server challenge: $tokens"); // respond with an empty response. $response->stack($this->sendRequest('')); } else { @@ -510,18 +508,7 @@ public function logout(): Response { */ public function reset(): void { $this->stream = null; - $this->uid_cache = null; - - return $result; - } - - /** - * Check if the current session is connected - * - * @return bool - */ - public function connected(): bool { - return (boolean) $this->stream; + $this->uid_cache = []; } /** @@ -532,6 +519,7 @@ public function connected(): bool { * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException + * @throws ResponseException */ public function getCapabilities(): Response { $response = $this->requestAndResponse('CAPABILITY'); @@ -616,8 +604,8 @@ public function examineFolder(string $folder = 'INBOX'): Response { /** * Fetch one or more items of one or more messages - * @param string|array $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] - * @param int|array $from message for items or start message if $to !== null + * @param array|string $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] + * @param array|int $from message for items or start message if $to !== null * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use @@ -729,7 +717,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in /** * Fetch message headers - * @param array|int $uids + * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. @@ -743,7 +731,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid /** * Fetch message headers - * @param array|int $uids + * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. @@ -757,7 +745,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid /** * Fetch message flags - * @param array|int $uids + * @param int|array $uids * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * @@ -825,7 +813,7 @@ public function getMessageNumber(string $id): Response { * Get a list of available folders * * @param string $reference mailbox reference for list - * @param string $folder mailbox name match with wildcards + * @param string $folder mailbox name match with wildcards * * @return Response folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) * @@ -853,15 +841,15 @@ public function folders(string $reference = '', string $folder = '*'): Response /** * Manage flags * - * @param array $flags flags to set, add or remove - see $mode - * @param int $from message for items or start message if $to !== null - * @param null $to if null only one message ($from) is fetched, else it's the + * @param array|string $flags flags to set, add or remove - see $mode + * @param int $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given - * @param bool $silent if false the return values are the new flags for the wanted messages - * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given + * @param bool $silent if false the return values are the new flags for the wanted messages + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. - * @param null $item command used to store a flag + * @param string|null $item command used to store a flag * * @return Response new flags if $silent is false, else true or false depending on success * @throws ImapBadRequestException @@ -898,10 +886,10 @@ public function store( /** * Append a new message to given folder * - * @param string $folder name of target folder + * @param string $folder name of target folder * @param string $message full message content - * @param null $flags flags for new message - * @param null $date date for new message + * @param array|null $flags flags for new message + * @param string|null $date date for new message * * @return Response * @@ -926,11 +914,11 @@ public function appendMessage(string $folder, string $message, array $flags = nu /** * Copy a message set from current folder to another folder * - * @param string $folder destination folder + * @param string $folder destination folder * @param $from - * @param null $to if null only one message ($from) is fetched, else it's the + * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * * @return Response @@ -950,8 +938,8 @@ public function copyMessage(string $folder, $from, int $to = null, int|string $u * Copy multiple messages to the target folder * * @param array $messages List of message identifiers - * @param string $folder Destination folder - * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * @param string $folder Destination folder + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * * @return Response Tokens if operation successful, false if an error occurred @@ -972,11 +960,11 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui /** * Move a message set from current folder to another folder * - * @param string $folder destination folder + * @param string $folder destination folder * @param $from - * @param null $to if null only one message ($from) is fetched, else it's the + * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * * @return Response @@ -996,8 +984,8 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u * Move multiple messages to the target folder * * @param array $messages List of message identifiers - * @param string $folder Destination folder - * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * @param string $folder Destination folder + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * * @return Response @@ -1126,7 +1114,7 @@ public function expunge(): Response { /** * Send noop command * - * @return array success + * @return Response * @throws ImapBadRequestException * @throws ImapServerErrorException * @throws RuntimeException @@ -1198,7 +1186,7 @@ public function done(): bool { * Search for matching messages * * @param array $params - * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use + * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use * message numbers instead. * * @return Response message ids @@ -1254,15 +1242,19 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp /** * Enable the debug mode + * + * @return void */ - public function enableDebug(){ + public function enableDebug(): void { $this->debug = true; } /** * Disable the debug mode + * + * @return void */ - public function disableDebug(){ + public function disableDebug(): void { $this->debug = false; } @@ -1273,7 +1265,7 @@ public function disableDebug(){ * * @return int|string */ - public function buildSet($from, $to = null) { + public function buildSet($from, $to = null): int|string { $set = (int)$from; if ($to !== null) { $set .= ':' . ($to == INF ? '*' : (int)$to); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 086869b6..e8323372 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -239,9 +239,9 @@ public function examineFolder(string $folder = 'INBOX'): Response { /** * Fetch message content - * @param array|int $uids + * @param int|array $uids * @param string $rfc - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response */ @@ -262,9 +262,9 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid /** * Fetch message headers - * @param array|int $uids + * @param int|array $uids * @param string $rfc - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response */ @@ -285,8 +285,8 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid /** * Fetch message flags - * @param array|int $uids - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|array $uids + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response */ @@ -311,7 +311,8 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response $result[$id] = $flags; } - return $result; + return $result; + }); } /** @@ -365,7 +366,7 @@ public function getMessageNumber(string $id): Response { /** * Get a message overview * @param string $sequence uid sequence - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response */ @@ -404,14 +405,14 @@ public function folders(string $reference = '', string $folder = '*'): Response /** * Manage flags - * @param array $flags flags to set, add or remove - see $mode + * @param array|string $flags flags to set, add or remove - see $mode * @param int $from message for items or start message if $to !== null * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given * @param bool $silent if false the return values are the new flags for the wanted messages - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. - * @param null $item unused attribute + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param string|null $item unused attribute * * @return Response new flags if $silent is false, else true or false depending on success */ @@ -447,7 +448,7 @@ public function store(array|string $flags, int $from, int $to = null, string $mo * @param string $folder name of target folder * @param string $message full message content * @param array|null $flags flags for new message - * @param string $date date for new message + * @param mixed $date date for new message * * @return Response */ @@ -479,7 +480,7 @@ public function appendMessage(string $folder, string $message, array $flags = nu * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response */ @@ -500,7 +501,7 @@ public function copyMessage(string $folder, $from, int $to = null, int|string $u * Copy multiple messages to the target folder * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response Tokens if operation successful, false if an error occurred */ @@ -529,7 +530,7 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * @param $from * @param int|null $to if null only one message ($from) is fetched, else it's the * last message, INF means last message available - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response success */ @@ -548,7 +549,7 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u * Move multiple messages to the target folder * @param array $messages List of message identifiers * @param string $folder Destination folder - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response Tokens if operation successful, false if an error occurred * @throws ImapBadRequestException @@ -690,7 +691,7 @@ public function done() { /** * Search for matching messages * @param array $params - * @param int $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. * * @return Response message ids */ @@ -723,8 +724,8 @@ public function disableDebug() { * * @return array|false|string|string[]|null */ - protected function decodeFolderName($name) { - preg_match('#\{(.*)\}(.*)#', $name, $preg); + protected function decodeFolderName($name): array|bool|string|null { + preg_match('#\{(.*)}(.*)#', $name, $preg); return mb_convert_encoding($preg[2], "UTF-8", "UTF7-IMAP"); } diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 40d5ecd3..274a8010 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -466,7 +466,7 @@ class EncodingAliases { ]; /** - * Returns proper encoding mapping, if exsists. If it doesn't, return unchanged $encoding + * Returns proper encoding mapping, if exists. If it doesn't, return unchanged $encoding * @param string|null $encoding * @param string|null $fallback * diff --git a/src/Folder.php b/src/Folder.php index 63cfd59c..e9ba8956 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -404,9 +404,8 @@ public function unsubscribe(): array { /** * Idle the current connection - * @param callable $callback + * @param callable $callback function(Message $message) gets called if a new message is received * @param integer $timeout max 1740 seconds - recommended by rfc2177 §3. Should not be lower than the servers "* OK Still here" message interval - * @param boolean $auto_reconnect try to reconnect on connection close (@deprecated is no longer required) * * @throws ConnectionFailedException * @throws RuntimeException diff --git a/src/Header.php b/src/Header.php index 0f8566b3..31cec761 100644 --- a/src/Header.php +++ b/src/Header.php @@ -61,7 +61,6 @@ class Header { public function __construct(string $raw_header) { $this->raw = $raw_header; $this->config = ClientManager::get('options'); - $this->attributize = $attributize; $this->parse(); } @@ -670,7 +669,7 @@ private function extractHeaderExtensions(): void { /** * Exception handling for invalid dates * - * Currently known invalid formats: + * Known bad and "invalid" formats: * ^ Datetime ^ Problem ^ Cause * | Mon, 20 Nov 2017 20:31:31 +0800 (GMT+8:00) | Double timezone specification | A Windows feature * | Thu, 8 Nov 2018 08:54:58 -0200 (-02) | diff --git a/src/Message.php b/src/Message.php index cd9dc6c4..831a7649 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1316,7 +1316,7 @@ public function getStructure(): ?Structure { } /** - * Check if a message matches an other by comparing basic attributes + * Check if a message matches another by comparing basic attributes * * @param null|Message $message * @return boolean diff --git a/src/Query/Query.php b/src/Query/Query.php index 6a67f91d..db356337 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -417,7 +417,7 @@ public function chunked(callable $callback, int $chunk_size = 10, int $start_chu /** * Paginate the current query * @param int $per_page Results you which to receive per page - * @param int|null $page The current page you are on (e.g. 0, 1, 2, ...) use `null` to enable auto mode + * @param null $page The current page you are on (e.g. 0, 1, 2, ...) use `null` to enable auto mode * @param string $page_name The page name / uri parameter used for the generated links and the auto mode * * @return LengthAwarePaginator From f049aca26bac5f20c12e81454c847e284c5f1283 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:40:28 +0100 Subject: [PATCH 377/600] Delete folder method added --- src/Client.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Client.php b/src/Client.php index 1560cc5e..f5fa4909 100755 --- a/src/Client.php +++ b/src/Client.php @@ -688,6 +688,34 @@ public function createFolder(string $folder_path, bool $expunge = true): Folder return $folder; } + /** + * Delete a given folder + * @param string $folder_path + * @param boolean $expunge + * + * @return array + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException + */ + public function deleteFolder(string $folder_path, bool $expunge = true): array { + $this->checkConnection(); + + $folder = $this->getFolderByPath($folder_path); + $status = $this->getConnection()->deleteFolder($folder_path)->validatedData(); + if ($expunge) $this->expunge(); + + $event = $this->getEvent("folder", "deleted"); + $event::dispatch($folder); + + return $status; + } + /** * Check a given folder * @param string $folder_path From af3ad97ec4c98fc9b8d7815501c13a2fc4e7aa4f Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:40:44 +0100 Subject: [PATCH 378/600] Examples updated --- examples/custom_attachment_mask.php | 4 ++-- examples/custom_message_mask.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/custom_attachment_mask.php b/examples/custom_attachment_mask.php index 32b7b91e..5a6323a3 100644 --- a/examples/custom_attachment_mask.php +++ b/examples/custom_attachment_mask.php @@ -16,7 +16,7 @@ class CustomAttachmentMask extends \Webklex\PHPIMAP\Support\Masks\AttachmentMask * New custom method which can be called through a mask * @return string */ - public function token(){ + public function token(): string { return implode('-', [$this->id, $this->getMessage()->getUid(), $this->name]); } @@ -24,7 +24,7 @@ public function token(){ * Custom attachment saving method * @return bool */ - public function custom_save() { + public function custom_save(): bool { $path = "foo".DIRECTORY_SEPARATOR."bar".DIRECTORY_SEPARATOR; $filename = $this->token(); diff --git a/examples/custom_message_mask.php b/examples/custom_message_mask.php index 25d05667..187eeed4 100644 --- a/examples/custom_message_mask.php +++ b/examples/custom_message_mask.php @@ -16,7 +16,7 @@ class CustomMessageMask extends \Webklex\PHPIMAP\Support\Masks\MessageMask { * New custom method which can be called through a mask * @return string */ - public function token(){ + public function token(): string { return implode('-', [$this->message_id, $this->uid, $this->message_no]); } @@ -24,7 +24,7 @@ public function token(){ * Get number of message attachments * @return integer */ - public function getAttachmentCount() { + public function getAttachmentCount(): int { return $this->getAttachments()->count(); } From 5f33f184b9a83c0705a6416a8efe184fe89e5184 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:41:57 +0100 Subject: [PATCH 379/600] Support "parallel" message processing during idle #338 --- src/Folder.php | 73 +++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index e9ba8956..af15476d 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -421,48 +421,53 @@ public function idle(callable $callback, int $timeout = 300): void { if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } - $this->client->openFolder($this->path, true); - $connection = $this->client->getConnection(); - $connection->idle(); + + $idle_client = $this->client->clone(); + $idle_client->connect(); + $idle_client->openFolder($this->path, true); + $idle_client->getConnection()->idle(); + + $last_action = Carbon::now()->addSeconds($timeout); $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); while (true) { - try { - // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand - $line = $connection->nextLine(); - - if (($pos = strpos($line, "EXISTS")) !== false) { - $connection->done(); - $msgn = (int) substr($line, 2, $pos -2); - - $this->client->openFolder($this->path, true); - $message = $this->query()->getMessageByMsgn($msgn); - $message->setSequence($sequence); - $callback($message); - - $event = $this->getEvent("message", "new"); - $event::dispatch($message); - $connection->idle(); - } elseif (strpos($line, "OK") === false) { - $connection->done(); - $connection->idle(); - } - }catch (Exceptions\RuntimeException $e) { - if(strpos($e->getMessage(), "empty response") >= 0 && $connection->connected()) { - $connection->done(); - $connection->idle(); - continue; - } - if(strpos($e->getMessage(), "connection closed") === false) { - throw $e; + // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand + $line = $idle_client->getConnection()->nextLine(Response::empty()); + + if (($pos = strpos($line, "EXISTS")) !== false) { + $msgn = (int)substr($line, 2, $pos - 2); + + // Check if the stream is still alive or should be considered stale + if (!$this->client->isConnected() || $last_action->isBefore(Carbon::now())) { + // Reset the connection before interacting with it. Otherwise, the resource might be stale which + // would result in a stuck interaction. If you know of a way of detecting a stale resource, please + // feel free to improve this logic. I tried a lot but nothing seem to work reliably... + // Things that didn't work: + // - Closing the resource with fclose() + // - Verifying the resource with stream_get_meta_data() + // - Bool validating the resource stream (e.g.: (bool)$stream) + // - Sending a NOOP command + // - Sending a null package + // - Reading a null package + // - Catching the fs warning + + // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand + $this->client->getConnection()->reset(); + // Establish a new connection + $this->client->connect(); } + $last_action = Carbon::now()->addSeconds($timeout); - $this->client->reconnect(); + // Always reopen the folder - otherwise the new message number isn't known to the current remote session $this->client->openFolder($this->path, true); - $connection = $this->client->getConnection(); - $connection->idle(); + $message = $this->query()->getMessageByMsgn($msgn); + $message->setSequence($sequence); + $callback($message); + + $event = $this->getEvent("message", "new"); + $event::dispatch($message); } } } From 54eb26530ba1a7916245608963ba2e6af0908d17 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:43:43 +0100 Subject: [PATCH 380/600] Convert all non address header values to Attribute instances --- src/Header.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Header.php b/src/Header.php index 31cec761..c0c7a5fc 100644 --- a/src/Header.php +++ b/src/Header.php @@ -107,7 +107,7 @@ public function get($name): Attribute { return $this->attributes[$name]; } - return null; + return new Attribute($name); } /** @@ -120,21 +120,7 @@ public function get($name): Attribute { */ public function set(string $name, mixed $value, bool $strict = false): Attribute|array { if (isset($this->attributes[$name]) && $strict === false) { - if ($this->attributize) { - $this->attributes[$name]->add($value, true); - } else { - if (isset($this->attributes[$name])) { - if (!is_array($this->attributes[$name])) { - $this->attributes[$name] = [$this->attributes[$name], $value]; - } else { - $this->attributes[$name][] = $value; - } - } else { - $this->attributes[$name] = $value; - } - } - } elseif (!$this->attributize) { - $this->attributes[$name] = $value; + $this->attributes[$name]->add($value, true); } else { $this->attributes[$name] = new Attribute($name, $value); } From 95925dad6a86936253dba82a22a7d267d951878c Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:44:21 +0100 Subject: [PATCH 381/600] The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 --- src/Message.php | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Message.php b/src/Message.php index 831a7649..7217a86e 100755 --- a/src/Message.php +++ b/src/Message.php @@ -370,12 +370,28 @@ public function __get($name) { * @param $name * * @return Attribute|mixed|null + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MessageNotFoundException + * @throws RuntimeException + * @throws ResponseException */ - public function get($name) { - if(isset($this->attributes[$name])) { + public function get($name): mixed { + if (isset($this->attributes[$name]) && $this->attributes[$name] !== null) { return $this->attributes[$name]; } + switch ($name){ + case "uid": + $this->attributes[$name] = $this->client->getConnection()->getUid($this->msgn)->validate()->integer(); + return $this->attributes[$name]; + case "msgn": + $this->attributes[$name] = $this->client->getConnection()->getMessageNumber($this->uid)->validate()->integer(); + return $this->attributes[$name]; + } + return $this->header->get($name); } @@ -1476,7 +1492,7 @@ public function setClient($client): Message { */ public function setUid(int $uid): Message { $this->uid = $uid; - $this->msgn = $this->client->getConnection()->getMessageNumber($this->uid); + $this->msgn = null; $this->msglist = null; return $this; @@ -1492,7 +1508,7 @@ public function setUid(int $uid): Message { public function setMsgn(int $msgn, int $msglist = null): Message { $this->msgn = $msgn; $this->msglist = $msglist; - $this->uid = $this->client->getConnection()->getUid($this->msgn); + $this->uid = null; return $this; } From 96f6ca65dc81cf3dc02d77d30670a0cc333d85bb Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:45:08 +0100 Subject: [PATCH 382/600] Save an entire message (including its headers) --- src/Message.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Message.php b/src/Message.php index 7217a86e..ea00ce77 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1544,4 +1544,14 @@ public function setSequenceId($uid, int $msglist = null): void { $this->setMsgn($uid, $msglist); } } + + /** + * Safe the entire message in a file + * @param $filename + * + * @return bool|int + */ + public function save($filename): bool|int { + return file_put_contents($filename, $this->header->raw."\r\n\r\n".$this->structure->raw); + } } From 5548db5f8e22617c23d95338f60d160efeae2a91 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:45:23 +0100 Subject: [PATCH 383/600] Restore a message from a local or remote file --- src/Message.php | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/Message.php b/src/Message.php index ea00ce77..c3eabb0a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -291,6 +291,48 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw return $instance; } + /** + * Create a new message instance by reading and loading a file or remote location + * + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws InvalidMessageDateException + * @throws ConnectionFailedException + * @throws ImapServerErrorException + * @throws ReflectionException + * @throws AuthFailedException + * @throws MaskNotFoundException + */ + public static function fromFile($filename): Message { + $reflection = new ReflectionClass(self::class); + /** @var Message $instance */ + $instance = $reflection->newInstanceWithoutConstructor(); + $instance->boot(); + + $default_mask = ClientManager::getMask("message"); + if($default_mask != ""){ + $instance->setMask($default_mask); + }else{ + throw new MaskNotFoundException("Unknown message mask provided"); + } + + $email = file_get_contents($filename); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $instance->parseRawHeader($raw_header); + $instance->parseRawBody($raw_body); + + $instance->setUid(0); + + return $instance; + } + /** * Boot a new instance */ From a6b765b39ce0f7462ce2d96a68ed342d1ecfb6d1 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:46:55 +0100 Subject: [PATCH 384/600] Fix undefined attachment name when headers use "filename*=" format #301 --- src/Header.php | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/Header.php b/src/Header.php index c0c7a5fc..2f9e0017 100644 --- a/src/Header.php +++ b/src/Header.php @@ -214,7 +214,7 @@ public function rfc822_parse_headers($raw_headers): object { $headers = []; $imap_headers = []; if (extension_loaded('imap') && $this->config["rfc822"]) { - $raw_imap_headers = (array)\imap_rfc822_parse_headers($this->raw); + $raw_imap_headers = (array)\imap_rfc822_parse_headers($raw_headers); foreach ($raw_imap_headers as $key => $values) { $key = str_replace("-", "_", $key); $imap_headers[$key] = $values; @@ -594,11 +594,9 @@ private function parseAddresses($list): array { } else { $personalParts = $this->mime_header_decode($address->personal); - if (is_array($personalParts)) { - $address->personal = ''; - foreach ($personalParts as $p) { - $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); - } + $address->personal = ''; + foreach ($personalParts as $p) { + $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); } if (str_starts_with($address->personal, "'")) { @@ -629,15 +627,41 @@ private function extractHeaderExtensions(): void { if (($key == "user_agent") === false) { if (($pos = strpos($value, ";")) !== false) { $original = substr($value, 0, $pos); - $this->set($key, trim(rtrim($original)), true); + $this->set($key, trim(rtrim($original))); // Get all potential extensions $extensions = explode(";", substr($value, $pos + 1)); + $previousKey = null; + $previousValue = ''; + foreach ($extensions as $extension) { if (($pos = strpos($extension, "=")) !== false) { $key = substr($extension, 0, $pos); $key = trim(rtrim(strtolower($key))); + $matches = []; + + if (preg_match('/^(?P\w+)\*/', $key, $matches) !== 0) { + $key = $matches['key_name']; + $previousKey = $key; + + $value = substr($extension, $pos + 1); + $value = str_replace('"', "", $value); + $previousValue .= trim(rtrim($value)); + + continue; + } + + if ( + $previousKey !== null + && $previousKey !== $key + && isset($this->attributes[$previousKey]) === false + ) { + $this->set($previousKey, $previousValue); + + $previousValue = ''; + } + if (isset($this->attributes[$key]) === false) { $value = substr($extension, $pos + 1); $value = str_replace('"', "", $value); @@ -645,8 +669,13 @@ private function extractHeaderExtensions(): void { $this->set($key, $value); } + + $previousKey = $key; } } + if ($previousValue !== '') { + $this->set($previousKey, $previousValue); + } } } } From 8b5a4cb82358cfa3d48d5719ff3e2771573c8da7 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:47:16 +0100 Subject: [PATCH 385/600] Compatibility information added --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3aef3a4b..60bfa12a 100755 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] ## Table of Contents - [Documentations](#documentations) +- [Compatibility](#compatibility) - [Basic usage example](#basic-usage-example) - [Known issues](#known-issues) - [Support](#support) @@ -40,14 +41,25 @@ Discord: [discord.gg/rd4cN9h6][link-discord] - Core documentation: [php-imap.com](https://www.php-imap.com/) +## Compatibility +| Version | PHP 5.6 | PHP 7 | PHP 8 | +|:--------|:-------:|:-----:|:-----:| +| v5.x | / | / | X | +| v4.x | / | X | X | +| v3.x | / | X | / | +| v2.x | X | X | / | +| v1.x | X | / | / | + ## Basic usage example This is a basic example, which will echo out all Mails within all imap folders and will move every message into INBOX.read. Please be aware that this should not be -tested in real life and is only meant to gives an impression on how things work. +tested in real life and is only meant to give an impression on how things work. ```php use Webklex\PHPIMAP\ClientManager; +require_once "vendor/autoload.php"; + $cm = new ClientManager('path/to/config/imap.php'); /** @var \Webklex\PHPIMAP\Client $client */ @@ -86,15 +98,15 @@ foreach($folders as $folder){ ### Known issues -| Error | Solution | -| ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | +| Error | Solution | +|:---------------------------------------------------------------------------|:----------------------------------------------------------------------------------------| +| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | ## Support If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). -However please be aware that it might take some time to get an answer. -Off topic, rude or abusive issues will be deleted without any notice. +However, please be aware that it might take some time to get an answer. +Off-topic, rude or abusive issues will be deleted without any notice. If you need **commercial** support, feel free to send me a mail at github@webklex.com. @@ -116,7 +128,7 @@ echo 'your php code...'; ## Features & pull requests -Everyone can contribute to this project. Every pull request will be considered but it can also happen to be declined. +Everyone can contribute to this project. Every pull request will be considered, but it can also happen to be declined. To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) first, if you're planning to do bigger changes. Of course, you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) if you're just wishing a feature ;) From 15b2b8f05f8684c1f657bfacb708b127a5789f00 Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 03:48:01 +0100 Subject: [PATCH 386/600] Changelog updated --- CHANGELOG.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b41c47..3004a165 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,41 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 (thanks @szymekjanaczek) +- Fix undefined attachment name when headers use "filename*=" format #301 (thanks @JulienChavee) +- Fixed `ImapProtocol::logout` always throws 'not connected' Exception after upgraded to 4.1.2 #351 +- Protocol interface and methods unified +- Strict attribute and return types introduced where ever possible +- Parallel messages during idle #338 +- Idle timeout / stale resource stream issue fixed +- Syntax updated to support php 8 features +- Get the attachment file extension from the filename if no mimetype detection library is available +- Prevent the structure parsing from parsing an empty part ### Added -- NaN +- Unit tests added #347 #242 (thanks @sergiy-petrov, @boekkooi-lengoo) +- `Client::clone()` method added to clone a client instance +- Save an entire message (including its headers) `Message::save()` +- Restore a message from a local or remote file `Message::fromFile()` +- Protocol resource stream accessor added `Protocol::getStream()` +- Protocol resource stream meta data accessor added `Protocol::meta()` +- ImapProtocol resource stream reset method added `ImapProtocol::reset()` +- Protocol `Response::class` introduced to handle and unify all protocol requests +- Static mask config accessor added `ClientManager::getMask()` added +- An `Attribute::class` instance can be treated as array +- Get the current client account configuration via `Client::getConfig()` +- Delete a folder via `Client::deleteFolder()` ### Breaking changes -- NaN +- PHP ^8.0.2 required +- `nesbot/carbon` version bumped to ^2.62.1 +- `phpunit/phpunit` version bumped to ^9.5.10 +- `Header::get()` always returns an `Attribute::class` instance +- `Attribute::class` accessor methods renamed to shorten their names and improve the readability +- All protocol methods that used to return `array|bool` will now always return a `Response::class` instance. +- `ResponseException::class` gets thrown if a response is empty or contains errors +- Message client is optional and can be null (e.g. if used in combination with `Message::fromFile()`) +- The message text or html body is now "" if its empty and not `null` ## [4.1.2] - 2022-12-14 From 081343736585bce3e6d45148f9feedd01fe0e38e Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 2 Jan 2023 04:25:21 +0100 Subject: [PATCH 387/600] symfony/var-dumper added as suggested library --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f20fb95b..37efc1c5 100644 --- a/composer.json +++ b/composer.json @@ -32,11 +32,11 @@ "illuminate/pagination": ">=5.0.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.10", - "symfony/var-dumper": "^6.2" + "phpunit/phpunit": "^9.5.10" }, "suggest": { - "symfony/mime": "Recomended for better extension support" + "symfony/mime": "Recomended for better extension support", + "symfony/var-dumper": "Usefull tool for debugging" }, "autoload": { "psr-4": { From 12414a0972cfb9bbcbf10428951db1ac2d0f1c1c Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 3 Jan 2023 03:13:07 +0100 Subject: [PATCH 388/600] Fix attribute accessibility issue --- tests/HeaderTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 29ed8167..7afec065 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -81,6 +81,7 @@ public function testRfc822ParseHeaders() { ->getMock(); $config = new \ReflectionProperty($mock, 'config'); + $config->setAccessible(true); $config->setValue($mock, ['rfc822' => true]); $mockHeader = "Content-Type: text/csv; charset=WINDOWS-1252; name*0=\"TH_Is_a_F ile name example 20221013.c\"; name*1=sv\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Disposition: attachment; filename*0=\"TH_Is_a_F ile name example 20221013.c\"; filename*1=\"sv\"\r\n"; @@ -109,6 +110,7 @@ public function testExtractHeaderExtensions() { ]; $attributes = new \ReflectionProperty($mock, 'attributes'); + $attributes->setAccessible(true); $attributes->setValue($mock, $mockAttributes); $method->invoke($mock); From 903bcbc4924dc42111a71917527d1ca95e7d2bf4 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 3 Jan 2023 03:17:28 +0100 Subject: [PATCH 389/600] Header attribute and config setters added --- src/Header.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Header.php b/src/Header.php index 2f9e0017..d4a20e12 100644 --- a/src/Header.php +++ b/src/Header.php @@ -761,4 +761,26 @@ public function getAttributes(): array { return $this->attributes; } + /** + * Set all header attributes + * @param array $attributes + * + * @return Header + */ + public function setAttributes(array $attributes): Header { + $this->attributes = $attributes; + return $this; + } + + /** + * Set the configuration used for parsing a raw header + * @param array $config + * + * @return Header + */ + public function setConfig(array $config): Header { + $this->config = $config; + return $this; + } + } From 26e564f54dea1200e75df13baed3c8c86a6b4823 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 3 Jan 2023 06:29:52 +0100 Subject: [PATCH 390/600] Test case for issue #348 added --- tests/MessageTest.php | 20 + tests/messages/issue-348.eml | 1261 ++++++++++++++++++++++++++++++++++ 2 files changed, 1281 insertions(+) create mode 100644 tests/messages/issue-348.eml diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 9ade7239..eaf6737c 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -187,7 +187,27 @@ public function testLoadMessageFromFile(): void { self::assertSame("txt", $attachment->getExtension()); self::assertInstanceOf(Message::class, $attachment->getMessage()); self::assertSame("text/plain", $attachment->getMimeType()); + } + + public function testIssue348() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "issue-348.eml"]); + $message = Message::fromFile($filename); + self::assertSame(1, $message->getAttachments()->count()); + + /** @var Attachment $attachment */ + $attachment = $message->getAttachments()->first(); + + self::assertSame("attachment", $attachment->disposition); + self::assertSame("application/pdf", $attachment->content_type); + self::assertSame("Kelvinsong—Font_test_page_bold.pdf", $attachment->name); + self::assertSame(1, $attachment->part_number); + self::assertSame("text", $attachment->type); + self::assertNotEmpty($attachment->id); + self::assertSame(92384, $attachment->size); + self::assertSame("pdf", $attachment->getExtension()); + self::assertInstanceOf(Message::class, $attachment->getMessage()); + self::assertSame("application/pdf", $attachment->getMimeType()); } protected function createNewProtocolMockup() { diff --git a/tests/messages/issue-348.eml b/tests/messages/issue-348.eml new file mode 100644 index 00000000..6c9d385d --- /dev/null +++ b/tests/messages/issue-348.eml @@ -0,0 +1,1261 @@ +MIME-Version: 1.0 +Date: Fri, 18 Nov 2022 16:52:06 +0100 +From: sender@testaddres.com +To: receiver@testaddres.com +Subject: 2 - test with a strange attachement +In-Reply-To: <30fdd5117dd9408487d74e9b50dedc27@testaddres.com> +References: + <30fdd5117dd9408487d74e9b50dedc27@testaddres.com> +User-Agent: REDACTED +Message-ID: <0a5495c56acb49d56545a2017dd1f931@testaddres.com> +X-Sender: sender@testaddres.com +Content-Type: multipart/mixed; + boundary="=_be6bc52b94449e229e651d02e56a25f1" + +--=_be6bc52b94449e229e651d02e56a25f1 +Content-Transfer-Encoding: 8bit +Content-Type: text/plain; charset=UTF-8; + format=flowed + + + +-------- Original Message -------- +Subject: 2 - test with a strange attachement +Date: 2022-11-09 08:36 + From: anotherSender@testaddres.com> +To: "receiver@testaddres.com" + +--- First Text as body test 80 column new line linux style 0x0A --- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu +fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. + +--------------------------------------------------------- + + +--- Second Text as body test no column limit (865 characters) one single line ending with 0x0A --- + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur + + +--------------------------------------------------------- + +Da: From Test sender in another language (IT) +Inviato: mercoledì 9 novembre 2022 03:17 +A: To in another language (IT) +Oggetto: Subject in another language + +--- Third Text as body test 80 column new line linux style 0x0A --- + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis +praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias +excepturi sint occaecati +cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia +animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et +expedita distinctio. Nam +libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo +minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, +omnis dolor repellendus. +Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus +saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. +Itaque earum rerum hic tenetur a +sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut +perferendis doloribus asperiores repellat. +--=_be6bc52b94449e229e651d02e56a25f1 +Content-Transfer-Encoding: base64 +Content-Type: application/pdf; + name="Kelvinsong—Font_test_page_bold.pdf" +Content-Disposition: attachment; + filename="Kelvinsong—Font_test_page_bold.pdf"; + size=35648 + +JVBERi0xLjUKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29k +ZT4+CnN0cmVhbQp4nK1Y+3PbxhEe4wAcSXEMgC9IlEJDskWKNAHi/YjTmlbixMm400mqmf4Q9IdO +mqRJY3rMdqb/fr+9A0BSsltl0jiWgLu9vX18++3C7xzfC2PHpz/1w3dvOqtvMufHf3bEsrP9sfOu +E3tBmqaZWNh//u6Nc30D+dzJvTx3bn7AocBxA2yFfuQloRMHiRcFgXPzpnP1QGGqpvNWu9zwI95l +5ZY/NMyu1dN431TZQDF75XbIR/bxyfzm504QOjevO1fjU3s04mcPPprwR065OcfRC7Xc9h7PZ52X +N52vO4Hzb/z9Cn9/rmz+5otOFudOkgZenAYO/s/iyNl+3/nTPXwJ4ILv+YfeBIXvRaETRIXnZztv +FE3XhD9k734YgsKT1j+cOE8m/BKGT/uTxuIwdMIgSWGdE6ehE0WJtC7H5WFchLRRFE6e+Lv1IPKT +aj32M7lOXtbr8LJZF/JBLrwn+TjcrddRwXoTFaGnkic9+/JBFkTVvVmW7+RrOyHf2Cn0VPIU9Vr+ +XSfNIliaihviwikAie33zp+dzT0zEh6mIwu8KHWi0MsSPxXpAKDK7UNDw0/NtHr9cjsYHmkjZmvH +x+bRyfj07PQjypJIwNe/EQf7F/PJoxabOK0R0NAfTdTWeQ2GzCuE2VkYEizCyPNj5+ZvnW+vHsI6 +ppTbuRv6fubF8dVs/pebr+StNYbcNECAc8dNU69I6eBVt9U+Z5OLiTJq3UacG8DaGCbnXhYK4XKr +l1flnC1U9rSci9o7GhhmOW9burlEGbmua93dIMVJFpPnUeCludBle8cn9sxe+YFP+whE6iXSptWJ +PR5zHn40RVk/iuK9AoWkW6lyU8EwdMAJIYiC4LPZSbCyIY5wGKrFLxIsggusS4vjshP7dGSvTk73 +DMrCWklC7NHCNSkzFW3I/dmQ48fBlXFeSz/RehbPVifc9sc+H53ivzHuvSy3+eF6YJ/IwO7lIZLa +gtCvI2spjyfnrb1sJzEikvt1mp70DT681AY2s6d2FS95ckYZGS/O7mTvbpiSmh+ZouPxgCQFR8KB +29zY2L6PXHlzH0EeGhaMemCbdlMJ7wj9eQL3UKsgHyrTMBJlKjoBTn/29r71crcJIHZwJwSzRE25 +PJV0GWWkBULCOgG7EEhP42rJUkbt1tORFHbDMKXCk1s9e2i64kAu0F7p8Cbn5aYpDpRW5lX6v71a +6ZbZ1bR5mCcwMrwanCtUsL4oOxmJThEHDavFVErpr+KoD3mfFLkXFLJprPTh0fD/R0MBPIyE5m+v +hqzctANUc7lRFdaau1GQo1sFRBrqZFRFRlu22Dk2izBKKMSPBZtMtMcT7UG5QWoEkVVktIfPMI/p +0ZWNDdEuyk2/D/Hw7OyC4+Xi44lAdZIWXlan1WE6CHmOMuVRuVV01Dmh2rP5YMCfXfLLgmfc54KY +Y/IoK2rGaZfbBRULId8Cqes4/NQt55qhfmIx9U6VijBF0jZ40g4jtNyWFqMqzu/2Z2qn9U0X0+SC +HOH0i4dH5MwZt8fBjJ963sgO7tzlAkShk8Y1BVZ8MFMYGIFiv5teBkFLRUa8ybSF7h/74gjWh61p +65EyUrBYSc7KTUJv0q7XB40l3YkZqPPJND0YJWrMYpRoWvFvgmycpV5YzTmI4DRSdyFs0Jj6OBNV +cLCHI0m+hWBJt+HK0UyQjRuHxI7N8tQR9Ys+HtcFPxsfz2bHo9Ht9ukmkU92+tQMSfCs3C6BorZm +cB3gUvH70ex0uTwnHDoAJf/dA+fJ74e8P3mGThLMfM8Dw99xwM0Ep7iN4mflxkbjGVd9JAJt1bYJ +J5IiIZsqShmPh1h2iyxKvDS9su19Lgn8YjdSBRgYwqQmk4OtN/I1imIxNP2CzCGpmJSQVT+u5q4i +yu+b1PdN4kAH3EzywAsSmdOLZds0VFN03IaNwPu7W2lAvP+t74NSBPpFQWeJl1S3LhbZaTkXoQ0K +3ILLajgMFVWUaz3FldvY1Iz2+TSqhuswIaCIynmfaLkdVbIoFKk7pe5CunfXSjcL0FMd/jjKgPns +3h8HuZMeOBkjqIfjYEuQnG7ypWbo7Dlfg8DQu08t2+bycTaiZ63ema2sVbMV1M8vmLJmBktTU34o +rbUe9f1n5gA8igfF4qO9CaCcD0w5Alh8qvVQF9uFyRVT4SDc+ULHjAe1eFm0NfDR1tA4jWiMmwaZ +y/VPmLbGPUuTrxnXFgvNoAPdLluUWxX70IRRkVMUrwbMNBQSpDc5m76mcVNRmc7apsUnUik0lfPr +ayyaH4jD6kNxIDGLI5kCW/IOFKzoEU0Lso4eAgJ5uT3CbKMVxkODmx9jqK7qXBwRJ9yomar6pnKt +GUQf6xemsWaK9pxTYPXdOiJdbeG5qxHPdDWFiYDwB9OLJ2Gfa4pqmByWjsYIofQB0UW3Y0vsmQda +FioCat5O6/8Yz812u5zLO5cayV/DVdLBlU/FuHosx9XPOFNEVmTKEfOl7LQw4rZBh37W5klfWI+D +SJfS3nL7kvWYKlZxQkU+VbwtGFRCyvicVvUekEV+ML7QFOyJUPEBUKVdXwNmTEZBDvez1SjguqVb +EBZWmMZSE/60DQ3nAVBpj0g7xgABKqRroQ0A+OecVfYJXOn1OqXIBBhpga01At5CAlhJaWpm+KxQ +eBMbXEVTtSC/S4MCZQ4RQGgxqLJkjiqD/ZktDB5ynCFZhZzldX3DN36JQkCYkCaKzAJVRJesSRal +ReFYM3VXvUQJhJU5aYOXV3Ca9Q6qiEK9kOSs8WumSNjIxDUeiwWBqtrkHazoSWZf2UHww0EoNwup +ViZ759AXdI82EBuNCcs261WfXI0Z+ifamqxCfna2P7MKi6/sI9s7Gtn2ichpEIjvXzg5BZv0JQSY ++vlzSsAOvSJGxFwNSKqaa5sqgAfjVcR0Lutx32Kyprh7WtIeUkMRBf0ttFupM7tdZJ+KhUiQiA8u +HQajbZDQWkQbFmDqaAuokSrMH3CuSaCgfRJblHNz72a5Ullg0aenqCi40DW5ORCwRnGpIgiVx5JU +CH16XcXANshIFXMx0WsPRQE6rkE3pOqSnixUgvZ7vNAQRbLG3AUSO9C5XYoy3CF1P44EV6rzVEJO +jrhicILTuMMU8NBUUvFcEEFPRh7fq/MKMTBYTvQLlOaCSWrBZ5lB75wiAqwqoihuswLwuNNPgBFd +UVsIEmJoA5WrIkSR2TMJ07sOUs+SqRw58oi+UaoeMjP40tS6bjVQHA4KzWyNQeFXzNZ507P2RgXx +hZ/GRTMQPWHLlwaB45IZ9URLXzqiRYuYHfQyNw1SD99geVx3s3Lz37iX3mryHc+O8GPEdf0QE2vC +iqA3kZ5tlxKnEQx7gqbXjOyQvbxpWGSyWGgTpEQdANY6uxbM0+AICoABahI9QOJVw+WvhvzVIQW8 +gsJXInt9SrLoZyLTryy+y8f9/z06iJ1M/DOsWMWY5xVFkRbNbwr+zTdfvnjt/OGPn710XOfTX376 +7h/O37/HiP7D263z5i0eftrg8c1f//XT283eF/t/AGaEuvFlbmRzdHJlYW0KZW5kb2JqCjYgMCBv +YmoKMjY0NgplbmRvYmoKNCAwIG9iago8PC9UeXBlL1BhZ2UvTWVkaWFCb3ggWzAgMCA1OTUgODQy +XQovUm90YXRlIDkwL1BhcmVudCAzIDAgUgovUmVzb3VyY2VzPDwvUHJvY1NldFsvUERGIC9JbWFn +ZUMgL1RleHRdCi9FeHRHU3RhdGUgMTYgMCBSCi9YT2JqZWN0IDE3IDAgUgovRm9udCAxOCAwIFIK +Pj4KL0Fubm90c1sxMyAwIFJdL0NvbnRlbnRzIDUgMCBSCj4+CmVuZG9iagozIDAgb2JqCjw8IC9U +eXBlIC9QYWdlcyAvS2lkcyBbCjQgMCBSCl0gL0NvdW50IDEKPj4KZW5kb2JqCjEgMCBvYmoKPDwv +VHlwZSAvQ2F0YWxvZyAvUGFnZXMgMyAwIFIKL01ldGFkYXRhIDI1IDAgUgo+PgplbmRvYmoKNyAw +IG9iago8PC9UeXBlL0V4dEdTdGF0ZQovT1BNIDE+PmVuZG9iagoxMyAwIG9iago8PC9UeXBlL0Fu +bm90Ci9SZWN0IFswIDAgNTk0Ljk2IDE1XQovQm9yZGVyIFswIDAgMl0KL0MgWzEgMCAwXQovQTw8 +L1VSSShodHRwOi8vd3d3LmJ1bGx6aXAuY29tL2Rpc3BhdGNoLz9hY3Rpb249VHJpYWwmcHJvZHVj +dGlkPXBkZiZ2PTExLjExLjI4MDQmZj1Qcm9mZXNzaW9uYWwlMjBmZWF0dXJlczolMjBVc2VyJTIw +aXMlMjBub3QlMjBpbnRlcmFjdGl2ZSZyZD10cmlhbCZmbD0mcmZsPVBSTykKL1MvVVJJPj4KL1N1 +YnR5cGUvTGluaz4+ZW5kb2JqCjE2IDAgb2JqCjw8L1I3CjcgMCBSPj4KZW5kb2JqCjE3IDAgb2Jq +Cjw8L1IxMgoxMiAwIFI+PgplbmRvYmoKMTIgMCBvYmoKPDwvU3VidHlwZS9JbWFnZQovQ29sb3JT +cGFjZS9EZXZpY2VSR0IKL1dpZHRoIDQ4NQovSGVpZ2h0IDE3OAovQml0c1BlckNvbXBvbmVudCA4 +Ci9GaWx0ZXIvRmxhdGVEZWNvZGUKL0RlY29kZVBhcm1zPDwvUHJlZGljdG9yIDE1Ci9Db2x1bW5z +IDQ4NQovQ29sb3JzIDM+Pi9MZW5ndGggODU5Nj4+c3RyZWFtCnic7Z1PrF/Fdcfv29kKErY3cfAi +buxKkQKKI2ErSOFPFk1MKwVbIqHOJhg2UIjAKZGC8w/yx1RKGpME4mzAzqJxaSr5gdTEJgv8cKVU +BglXGDULE6gUE7qxXYnG3rmHN2RyfGbu3Lm/371zvnfe+SiKzHu/97tz5893Zs6ZOWfh8uXLDTAX +3rl052NHnv3335Z86NXvW/Xmv+xZc9Wqtg+8+fYFKtXSqTcLFupdXnnq3i2b1/v/XDzxWyrG//7f +pcLFMAyjPC/8cPcCuF7f8sDB8rJIHPzKjjtv/Vjbb9f89WMqKrn//u0PfvYG9+9Dv3pl9z8sli+D +YRgqfGH7Fmi9pmXsX9yxv/xzP/j+NbS+bvutolCe/7eH/apfayYzDEMF2l5D6zVt9n929FT5537z +zlse2f3Jtt9qCSXNroce3un+rTWTGYahws1bNh5HtodceOfSxs/tVzE7vPHMno3r10R/pSiUL/xw +9y1bNrp/P/jjX/3wX/9DpRiGYZTHWWhx9VrL7HDbJz68+N1dbb/VEkpuolGcyQzDKM/V71t14ZcP +0z9w9XrL3Qf+88zb5Z975Du7dtz44eivFIWS+z/N02gYK4oHbv/441+8tYHV6+On3vzkAwfLPxfT +0yjOF2rNZIZhqOAttKB6jelp1BJK7mk8debtj919oHwZDMNQgVtoEfX6wjuX1v7NYyqP5gfmBIpC +ya/JUOVsuevAf//PBZWSGIZRkgdu/zitIL0oIer147/4zZ4njpZ/Ll/Ghmgt+T+6ef2pp+4t/1zD +MNBA1OuNn9uvsn7kB+YEIJ5GwzBWMnB6bZ5Gjj/H46Bp45GDL5wyZ6Nh1M6aq1aFB4vh9HrHVw8X +ju7k4KE5QrSW/P4cT7NsQN+x97BZrg1jhRCKEpZeK94eTHgatZb8zZU3LbUM6IZhqBBaaLH0mjb7 +jx46Xv65mJ5GFzHA/VvxzIxhGOWJWmix9FrL7CDiSnMUhZJ7GrVmMsMwVIgeNADS68UTv935tcPl +n5s+MKd1uFDMrlozmWEY5WlLmQKk15ipCbSEkt+01JrJDMNQoc1Ci6LXWp7GdOovRaHknkZLTWAY +K4o2Cy2KXmvFKU17GrUOF/KIAZaawDBWFPyggQBFr7UyIiY8jYpCyWO6mjHEMFYOtONf3Ler7aI1 +hF5r3R5Mexq1jmSE53ge/8VvLrxj2QkMo34e/OwNbebZBkSvzdPISd+0NAxjxaKv11pxSkVoDoGi +FULctKT6scW1YVTPxvVr2tLGevT1Wuv2IA/NEYKQBN1Fd7K8uoaxEkifVXMo67UlQRfwiAEWMMQw +VhSJkM4OZb3W8jQmTsw0lgTdMIzipEM6O5T1WisjYtrTqHW4kJdK6x68YRgqpJPHOjT1WitOadrT +CJIE3QKGGMaKIhHS2aOp15hJ0BE8jYoRtw3DKE/6orVHTa8V45Riehr5TUvzNBrGiqLT0+hQ02st ++ywPzRGCkATdAoYYxooix9PoUNNrLfssD80hAEmCbp5Gw1hRpI8/cHT02pKgc4T/U+vCp2EYhaGx +/8juW/LjT+jotVac0rSnUetwYfqmpWEYhkNBrxXtswlPo+KqNlEqwzAMj4JeWxJ0TvqmpWEYhkdB +r7U8jYkTM4qexoT/05gKtDmj+b6wMc3MaCuQ0nqtFac07WkESYLuoMnjVMGRv+aqVW0ZdowcFC83 +XV56VOW5Wrz59gX6n3YphiEnempIab3Wuj2YTgKAkARdMXrqPBkSii0tw2RAJJR37jtSuOHCYmhZ +0oR9T2WNPx60lDn19L38fjYtqh45eLyaCGg50VNDiuq1oqcxcTdfcX3EPY26dxpnM8sUblBeXSA3 +UUFC7yreFh4P/oJVXiLLP3btKarXmEnQtYSS37RUH2+zGUNLNqhwzGr1JbG+BknyWd8dK/GCWs09 +Hvl3GjlF9VorTmna06gllLxU6uMtnXq4jZINyhcjIDdRGz1LGkgxxkPY6LSkYzxyoqeGlNNrrduD +k/A0Ioy3nHCOnJINKq6AgtxE1XKeC9OnYq7R8eC9Uau5R2W2Wxfl9NqSoHP48gEkempfE3bJ66DC +XANyExUh9G6jd1t4PMQLatXzeKSjziUopNeKSdATTliQJOgg0VN7mbALNyhfjIDcRDWH53hwU2GV +4XQyo6eGFNJrkDNPAq2FCS8Vznjr5QAp2aBiMQJyE1XLAyaKoeXwHA/hSgFZzQzIbJ5GRwm9VvQO +8ZWIAOQkFtR4y7SpFXbSckONon9Y2ItU9JqG+uK+XbxL1+eIA3Esj8c81x1K6LWWuyB95kFLKEWp +EDyNnswDoSWdtCAH18I1EUkJFaZkGdZctYpahxv36nPECQNmfS/Y9Hfsc0roNWYSdISTWGie/cwk +ciWrThx7QriJikN9jjjRA6FWM4OQOcTaGF2vFZOgJzyNIEnQ0Tz7OZa1wnMMX4yA3EQFAcfzMSC8 +nkHOTQ1LwkKbw+h6reUuSJ92QDiJhTneOoWp5BwjFiMgXmsX6aVkTC6CBrnoz/U54oQrtb4XnO1W +GmdcvbYk6AI+u0J5Gj1pI1LhqgOJICEidWy564DKJp13niodccLTWF9ElBkChgjG1Wst71A6CQDI +SSxMz37avlay6oRxZoVH6hDFqM8RJ+6OYq5m5mG2gHyCcfUawacXoiWUvFSw4y1twtYKGNJg3ERV +LIZweGr58McDxLE8HoPklxhRrxU9jXyiFoCEnkD27Le5RAoHDEEIkSGKoegB437XKq/8cQMm2rmp +QRjEZT2iXmsdfsD0NPJSYXoaPW3n+Uuu6UAiSGA6POtzxIkrrGjnpuZnqDStY+m1JUEX8FKBB/ON +BqMpXHUgITJAilF9agJ+dxR8NTMb83saHWPptZa7IB34CiT0BKan0RM1KJWsOpBIHZaaoAyWmiCf +sfRay12QCAoKkgQd1tPIESbswms6sRhB8A83MM7z+hxxlpogn1H0GjM1AUipJuHZF0Oo5JoOMzUB +yIXYKq/8WWqCfEbRay3vUHoe0xJKXqqpePaFWankmg4zNYHWJl106foccSCO5fGYOTVBlOH12jyN +Apwk6Pnw1WXhw1WAqQmaZYsQaWXhMpBS86j2VTriQK6wjkfftE1phtdrrZUIpqdRJEGf0B1ib8Iu +uaYDiSAx1OmrYanvyp+lJujL8HptSdA5vFTTss05E3bhJY9ITYDgH8ahPk+jpSboy8B6DeLTE4DE +fJiEp9Hj1piKAUNAWq1Z9vIdf+WNkmXYuH6NOK47rck+B0tNMAMD67WWJKXnMYTQE1P07F9eerTk +bgkkgoQohpZ9TxyprM8RJzyN01rN5DBnaoIoQ+q1oncoMY+BxHyYom2OVpolFRMkVj2gw7M+Rxx1 +reM/2o3Q3CNBuwd6wXlSE0QZUq9BoisIEEpV5R3iYQEJkQFSDOE8V7zyd3OLT2geSMVoB8MXWFr1 +TNPGxg8MnzaI5qHHv3jrsJYQx2B6reguwPQ08v1sfXeIBwfkXBdIpA4Ev+tHN6+nqWvwFWKIVj0/ +cPvHxbSBz2B6jelptCTok8BSEySKoTWyiqWsVKlnzCObnQym1wg+vRCEmA/12eYGByQnAGYxVBxx +JeVMpZ4xj2x2MoxeYyZBBwlyP0VPY0ksNYEAITXBUPE/O1Fp7sGvsRRjGL0G8Q4JtIIt4CdBhwIk +goQoBkLnafRG1uWlR8s8SKWeBwyYV5gB9BrEpycACWNS3x3iwQHJCQBSDBCH5/77t2/5yw8M+520 +aRCjVauei5nmB2cAvdZyywifnkBLKIXhzzyNaUQjgqQmAOnS9R0rEgsslUE6xjWWYgyg1wg+vRCE +UlWZNnRYMFMTqFiNb/vEh0lHuDOmvskeITVB4vgvPvPqNYh3SAAS5L6+aMXDgpMTIMx/RpJ94Z1y +UhLaCqo8VsQNESrNPV1Po2NevQZxywgsCfokwExNAEJ9x4oQHMvFzr2MxFx6DeLTE4CUyjyNnQBG +6nAcL6sj9HRRgCone/UrrOlN+SSYS69BfHoCkIQJ9aUNHRbM1ASkI7RlLLzMD3WkvskeIQn6pD2N +jrn0GsGnF6IllJNLgq4LSKx6hEgdzQpLgq5Vz9M9xueZXa+hvEMekDAm9UUrHhaL1MERXbrKY0Xq +SdAnGjBEMLteI/j0QhBSs1dpfBwWzNQEWg5PUYz6JnuE1ART9zQ6ZtRrEJ+eAMRnpRiteCpYaoK2 +YlQ52XNPo0o9T/0Yn2dGvdaSpPSmBiSMiXka0wjHLEirWWqCkRBaqVLP0w0YIphRrxF8egKQhAnm +aewEJEQGSDFEl65vsheeRpV6rsDT6JhFr0F8egKQUtVnfBwWS02QKEaVkz33NKrUcwXH+Dyz6DWI +W0aAkJpd0QY6FTBzAoAUo/oc4Sr1POmAIYLeeg3ilhGARJev7w7x4PDqAgk+A9J5qpzseUA+lXqu +xtPo6K3XIG4ZAYLPStGAPhUwcwJo2WTEydT6JnsRHlalues4xufpp9cg99AEIAkTqjQ+DgtITgAR +hZn6D0lJ4TI8ePsNvD9XOdlzrbSAIYPQT69BfHoCkOjy9RkfhwUzNQEI9aUmEFppqQkGoZ9ea7ll +EncaaWGy5a4D6mFMqjQ+DgtmagIQ6gsYguBprOYYn6eHXiu6ZWh9fWhvZJ588w/nH/zxUZVhb0nQ +eyGqCyRSR7M80S6e+K+SZSAFERNGfakJxFEuCxgyFD302iSJIzyNWgb0qYAQq74JNmpankZxwkzR +hy8ykI2EpSYYily9NkkS8K1WfcbHwbFIHW3F0BpZxa5oq9RzZcf4PLl6bZLEsSTovcBMTQASA0dr +jc+Pfo+KSj1XEzBEkKvXJkkcvtWqz/g4OJaagIOQmqDkwQkVx3J9nkZHll5XGUB9ZoTPysz6aUCS +oINE6gBJTVDsirZKPdd3jM+Tpdda99Aw4T4rM+t3YqkJEsXQGlkibsl4qNQzzc2nnr63pmsynm69 +rjKA+jxYEvReWGqCtmLUN7LEsr2+SwniZmx5uvXaJIkjwpiYWT+NpSbgVJ+aAKS5xyOdjLAA3Xpt +ksThPqvCxkcSncn1fuHis9QEdacmAHEsj4d6aNYOvbYYRhzhsyppfHSPnlYyBEtNkChGfSNLuFLr +OwGMcKa7Q6+nJRBjo5gE3T16WrYpzJwAKlYI0rLFfbv40qy+kSVsBfXty4s5aROk9Lo+f8ic8CsG +haXT+ammddYbMDVBs7xPp6Vf4WLQOOdlqM8R18A4lkcCJDRrSq/r84fMg2K8Me7GWbj5m2UeOieY +qQlAqM8RV72nEaQjpfS6Pn/IPCgmQed+qqmchefVhZOaAIEqHXG8i1a5LwfpSK16XZ8/ZB4Uk6CD +uMt6AZKaIIyo+cjBF46XtRpvXL9GrMvqG1kgjuXxwMlx0arXli2Fw10NhZcPwssxCdMnZmoCrS4t +DoHV54gDucI6HjihWeN6PQlRKIZwNRReLYZx1MDtVJipCRSLAZKLfTwQHMvjEea4UCSu1/W5C+ZB +uBpKymXUywFuwsZMTaBVDJCrleMB0tzjoX6nkRPR6yr9IfOgmAQ9ep8K3ITNq8sidSCkJhgVEMfy +eECFZo3oNbgcFEa4GhQ9jR5kaxVIagKQSB2iNuobWaKL1ncCGC0JZESv63MXzINiEvSElwPWhA0S +QcJSE5RBOMNhu+XMiJAv6ki9rtIfMjOKSdDT96kwzaAgLj6QSB0gqQnGA8SxPB4IAUMEUq8xhUAL +kZqg5DItfZ8Kc2wIzwxITgCQYoB7iWdAdNH6TgADJoG8Qq+r9IfMA3c1FJbI9H0qTMcOSAQJfrzM +HJ7jAeJYHg8oT6PjCr2u72LSPAhXQ8nlQ859KjRjKEgECZDzc6I26htZoovWty8HCRgiuEKv0SRA +F0xPowdthACmJmiUjmSQWNNQ576H+hxxII7l8VBPTRDlz3pdnz9kHhSToGdGboQyYYMEOYk6iAoH +DKGGE4YsqJYaBBDH8ngAehodf9br+vwh88BdDYVXi5n3qaBMoiARJHDiPHCqv/JX374cITVBlPf0 +GmrwI8BdDYVXi/leDhyPPEIECZCI8oIqRxaIY3k8wqA9ILyn1/X5Q+ZBMQl6r/tUINfJMFMTnDrz +9p2PHSk8n4WOYjQ3w/yAXGEdD0xPo+M9va7PHzIP3HVWePnQa0cP4nIAiSDBj5eBFKNKRxyIY3k8 +QFITRHlXr+tzF8yDcDUU9jT2ityIMFpAYtWLRZ9WMcT6ur6RBdLc44GTmiDKu3pdnz9kHhQ9jTPc +p1I3YWOmJgBxeKq3zuCAOJbHA9Nl7Vl44w/n6/OHzAN3NcB6Gj26JmyQCBIgx8tWQmoC3kVBzHED +gumy5iw88KNfIvisQABJgp6P7pgBiVVvqQnKAOJYHg+o1ARRFq6+dV9l/pB54K6zwuuj2SI36pqw +QVx8IJE6hJ9q4eZvqhRjPEAcy+MBGDBEYOvr96CtEK0duGKSFG6560CB9XX46F6cOvP2jr2HC5sR +P/j+NYf27hQXdsuba8NigKQmaOq6JhPtojW9YLQ/A9KaH90wJgdOagLDGAPTa6MeQByehjESptdG +PWjt0AED2xtVYnpt1EP50zK0sn5k9y2YsYGM+jC9NgzDmAam14ZhGNPA9NowDGMamF4bhmFMA9Nr +wzCMaWB6bRiGMQ1Mrw3DMKaB6bVhGMY0ML02DMOYBqbXhmEY08D02jAMYxqYXhuGYUyDhd///uzi +kWf9f2/btnXrtuvFhy5evHjs6K/Pnj2b/hjx4tKJV1897f9z8+ZNn97+qaHLXDmLR57zVb1hw4Yd +Oz8z+CPOnTt/7OjzZ8++Rfgfbtr0odWrV1933bXRljWMYlDP7BSllcnCw1/52qVLV8R3333XF2jQ ++v+kunv6qUPnz58Xfyk+Rjz5xIHXX/+d+NjfP7Rnw4ZrBi1zzYR1uGvXHcN2VppQDz79s7bfbt16 +/a7P3zHg4wyjF9Q/D//8GS5Kq1ateujLX1q3bm308y+dfPno0edpabgS+u3CngcfEj/69Kf/ii+K +oyocfoxq7fDhZ8Rn1q5d+/Vv7B2utJVDU+M/fl/mxBtWr2mr9O1v7RMz9HiPM4y+kFi/9NLL4off +3fct2vzxn1BPPv3qa6TUbil5zTXXPPTlPeVKqUSHXkcVJPwYQSoQrsFt8PciOud97et721YWQz2C +Ew4MwyhJqCThso90idaRfNlx0003jmE5RKNDr6NznYNXUHSLvUJmvAEJa5t2gvse+/aoj2j+NK2e +OfM6rVmEjcswSkI98Kt7vyF+KGx00T3iClkaRvTaG6bPnTv/nW/va/vLTZs+dN/997p/R20mf3ff +PZs3bxq0tJUTriyuvfYjd91956iPMJuVgUN05ce1mFbWtObgfnLHCvG7RPTa62xicd0wvY7aTLia +O+hjtBmnRVxY1/Rhdyyh706cZloqJP0//Zv+lhos/AZ64otLJ9xniB07bxP+z/D0y67P/y03QThL +GfUk+ioxq3eWnF755MmX3L/pM0586atcVdC3kVxu3/4p+oboymLHjs/cdPON/j/pD6kk586d8z9Z +t25dZjd1R3fCaZWW8FQh9Bb8Qc3ybE1/ErYXFZi6BxW4bTKmF1k88pwvpHPu0w+Xq+Jl921Ub6KS +Q3jVNbGGC59FUA3ztnDPDV+c3prKnz4M01kA8QHalfI64Z2ToOqlx7nO5ms1HCZtHDv6PP2V+7c/ +NeRe7fTp15rl7Sz9kBfA/ZbGXThDuxZPL0hdZ6OHij9PVx19norq/zM82pH+ALXmiy+eEN/pzyyE +ZhAO1QCNhXB0tw1eVw/ULhNaVrbqdXpx3bCuFpV1vrh2gyoh/Q7qB1TdvfbjTz91yHVWR2iBoZ5x +7NivxVO4r5lek3qA6JHcIkSadfTo8wkHnftOkomw1cOnk/6SmggLsrNQR1cWvhrbTulkLiuiw0Dg +R0Vme1EHEOLo+P739guJp28Wa6LOYofdLzwkQCOQ6iSxL6b6X1o6kW47GrT0FuFMENaYsE1Fu5b/ +AJX/+9/7gTjkQPuYJ5/4Ka+HTKtrWBh6TXp93kZ8n9S2CBXQeLnv/nvCFmzrbJ1/HvZh0WrRTs6P +kIU7dV+r9L4/efKnnUXiChAeNYkyobV5q16nF9fNn/Q6Kut81ZDZ9uLpmR8OW/cH+7/n/93WwHxI +R03G1O9dL+ysBP5X9Mpi2IfKRf1J/MSP2Kikutdp63b5Nru9D3+9s9e6YZNewgjCCTJa59QfRDN1 +OlGjuw3RN8LWJ82i8lDb0Z8LZUwQPS4WGo7Ey4aNy/U66tcVrc97Wpov7fly+qsa1hk6Xcrie4Tm +5siiRyidWD85+JCMfoC7uMM39SbBnDUHV55e9SBOT8AS12vaZYerG6pT3oNd1YSrjIaNq3CVwf+c +pCH8VS9zatiEfK4OR5TDt03aBJEv1tGSR79cwEdsqD6JGm5bFbbRqddOjDpP+4WIjh4trSBzORMq +JtfrqKx410vbIdQ2hF0iugrhxY42Lnc25IhLpkbkCKjve20i5axe0Trp1FyqHFftJ0++HC68uByH +3UxUbPgBPgtG39TXUs545JdCot4gqoSLFy+dPPlSaBuZhBcncl+GRgW1uqgaqjWqTd7ebjscDu9O +PyR9FQmiE6lotw5v4rTx4tKJxcXnROFd30rMrr4HhOLie0+061Cn37nztrXr1p4/d56aPHw1vuBN +X0sRJWliKwtaer9rfg36KHW7qKU+QaIw9FLbtm11a/zo8oca1LXX6VdfCzu6sBJEv0GQeUIx/Cpe +XW3TWxPrFc3ygNy+/VPUdm+dfWtp6USoO3ym7/R6RT/AnQ1tawVP/uI6Zwp0ZYsuj1yndSWn7kQF +S2huOA8JPXWmZzfErtlwDZV/w/L/Ny1+LN5knbNgevGXXky4Xup1IywMfxA1H3USV3j/IpOwYi88 +8eOfiH5Pchkaoahv0fgRek3jPNREX79RyRMHHqJNmL83CR/hnp5uWleG6Gd84Tvvakb/nL9d2zCj +enOOKepSJFuur+dvQmdeCESVlLtocvzG0VmQL3vbFvLUplu3baX3padkbgtC2fUdI31QPVxYhU6L +dK+LLiP4NNOpLNGtFRWD5oxrr7t29epVVIbMemibAqmzUYGdCct9VeKwpv/PdAtGf0vzEClap5xF +p0m+9op+OS9epzklOk1G/V5R7wLNW/Qik75uHdHr0NrourKQMPoY9TkxMPjwjta+a3v+k8QOqJNw +YLiel96NttkZvCk5qlzhLj6xxIv+tmk/n5ezhhKF7EtaYpoWkRIm47QFqe12Vf6GiRPOYb5jhIrs +fxUd0mGPCte/vGnStummpXG9srTNvrPFZohOgdExEn7SecL5T2h7Eapqeo3lcQcq6JM05YQ7pM57 +idEP8DpJW0uajCW8Jzp5ZL4IMhG9FviNW45JLmep1Ukv2794CmnHps2bRKPSUBTHSO67/x6xOub7 +02hjh4MtrdehfSOxBc4xI/h3me1EdufiJXyd0J0YHTBer6MLqHmc76IOXfWGT/FuxqZlWgovbSaO +unfapsOCNVc2fbQMs3m0oluB6B6rl59QwIdtpyXHEa4bOj200X2PnwWjXUs8pXN758n3xEzrYmS3 +XiesvQKxdw77dCa9rioJlQnt7E4lxQikR4iGF+fAwjfl0uYIJyQ/qqODJ6Fcvea22W6od/qCogZ0 +0ZXThw47F1B9EdrhChwOe75+z5l1oors+3m07bhtOqoaadt6M2urtRkoxGH5tmJnwuczqhxh+WyD +d49O23T0A3wWjL6p2JllhhZxZB5qbCZ1N7JDr/mSsFOvxUQXjn9aF+R02ejNiDbEqr/tnJMY5FSS +8KyL/8+cNVra49Rpy+MkgrREmWFFkLN4SRyl8qQHTLg0m9PtLh7nbieJihWFTBzg9aSlodNwFJVj +3rjR43ezxWaIVnhU+qN6Tc9dvXpV51PCOzvUYU4v3zGJHuJy8Irt9NBGX6Qz9IV40+gB2XTF0iuc +fvW1s2fPJlQu/9aSOh16zSs0rdfhO4e9doxz6ZmlSh/wEmvA6HeKRU102+i7V6fxgdN2lMXJU3hb +Z4agIjmLl2jELj5LpR3IOZEf+hJOe/Tu6Uib0WHP3zS6U+ZVmnA1J24S+YrKt7HmkD8Fpg84zgO9 +9fLJkF+HHd536fSQabMm81mwM1JCjp0qDa3xSbjDqwz16HWnT9wTWpFCzRKjy0cl93+4afOmde/S +Y9uY3gZ6IU4YiMOxFB1yzr987XUfuXjxEhU7/DauTZ2eE05UYnxHjFZ73x1czuIl+hl/miV6mbBh +7d55xG0GOvf4Ydu1xfV1l/5pxNJbhLrDvyc6b7lTj+FxRv/9Xll6ba3S9JoCox8Oj/fQK/hzbO+O +tHVr3YE8l7/C3Z53a2ohl+n7nNFJzp2HiZ6edCTOEYZv2tYZaJtIo/L1M697s62/uO+KJCoh7OeV +6LWor4ReR1+47QjzzcuHec8tH2FOjP9MEvfm+X6/rfDcVcWJDtoE3HAULVLCiBH1w/hvi04efffX +OWGeZjCAdh6enT9bRcILEl1pznDlR9zxm8Hvwuuhl401Td8psG3GdVNFW/QY19PajjO5Z5Eahjf7 ++Vv3vaDUXCkaOW/aeXbFdYaoK8hpOvWN18/8Ljw0MSX79c//6Z/bbg2J9VeivtpEtu8VwdkqLjrA +xGGMNr1uW/j0Ei9xGb3znCknJ8xTtAvmS2H+Mq1Xe4lJOsdwPAOJ4wptva7XReQwkEDfqbq5snE7 +j0nk03cK7DtX8XfvVWlNMMnlH3DydC6nwjdNuOV9f84/GuuY0OKaWDj6q2PR1wvXg21uscQLuxBl +OQ3Jb2H1JdpXhD5mBhHkUA8+cuTZzt4fRgWL9pi24wHRiUH01OgL5nsdOw88cDIlO3x64rTMPLSd +Ik1/eab6RENWJY7uut1hWllyjtlkErUopqfA/HA9YUiD/Nk6rLf0+oY6W+LQd5M92Se02PfnXqFj +SOWpaSaUoGPhj3/8o5DUhHSKYeC6b6cjhUd9DKF+s23b9e7y20yvIE8ghYElw8I7y1qnadXlpaXC +R1WbJCMaVVLc+o2GguLwYdkW6i8dKaUTMfLT+xh3Wze6vaXi0StTi4eNJfrGUCMhqrzphH4OUi56 +izYBcjv9NstbNDiff3Gua2HjCk2Z53iviL1FHZv6Rucwoe5HLx6N9eG/h4Zt2wBfWjqREDuqN/rD +6JY0ur7xlzDF6BMKk/mmibOG4u5xugZcayZiAsOycPny5WIP834Ajw8+AI53xXgm19J9ofelt+Y/ +8X6qkrSFoMk/bhH2upy2E68/xeYOW7CZ6d0dma3Pa3uk0U2rKB7xvGl/qeiL9D3RAEVRvTaMTtzJ +303LQWCiy/yphFIzjMExvTaw6HstyzBWDqbXBhbpMDXTivZgGMNiem1gkTjJO6G8TYYxBqbXBhbR +9XXmSSTDqBvTawMRfiBH5VyKYQDy/x/DPzUKZW5kc3RyZWFtCmVuZG9iagoxOCAwIG9iago8PC9S +OAo4IDAgUi9SMTAKMTAgMCBSL1IxNAoxNCAwIFI+PgplbmRvYmoKMjIgMCBvYmoKPDwvRmlsdGVy +L0ZsYXRlRGVjb2RlL0xlbmd0aCA1ODU+PnN0cmVhbQp4nF2UvW7cMBCE+3sKvcFxl5RkAwYbp3GR +IEjyAjqJMq6wTpDPRd4+M7NxihRjYI4/u9+szPPzy5eX7Xrvzt+P2/yz3bv1ui1He799HHPrLu31 +up3Mu+U63/86/Z3fpv10fv467b9+763DhraG/za9tfMPj18szsy3pb3v09yOaXttp6eU6tO61lPb +lv+WRo8Tl/Vzq9VQ6vsK6zWUhkabaygNj7SlhtJYaPsaSoPODjWUhkw71lDyRPtQQ6nX6mMNpVGr +Uw2lYrSXGkq+0M41lMaBdqmhNGhzq6HU66q1htLosIYsKGxeacFq4nUCGlgteGdasJp4C5s0sJp4 +h5EWrCZe12awmnizCoHVxJvZhoHVxOuqC1YTb2aSBlYTb2aSBlYTb36gBauJt6gQWE28RW2A1cRb +dDNYTbzOswhbQhrsysHqwcu6DlYXb5EFq4t3lAWri7dnVw5WF29h7A5WF29hdA5WD15OAcUlXCUL +Vo/58ttwsLp4RwbrYHXxFmaFOCXkzHFj5hI2kxeDklBIbYDVxZt5FcKWYFkIvUiwPIvJSCikVbDm +mC/DQfYSeCdasGbx9iyEahLa0CpYc3zPF1qw5uBl7JiqhJuJkMGa43vWZrDm4GWwGaw5eHUWrFm8 +vXoGa475ahWsOXjZBgKTcJZX4d9FQlc8CywJq0TAVyyldGHOKC7hKnaFSCRsZhoYspRS02awFvE2 +toFeJBRyvSyfTwgfGb5Wn49TN38cR9vuetL0ZPGpum7t36u333ae6qDTH+WnOLQKZW5kc3RyZWFt +CmVuZG9iago4IDAgb2JqCjw8L0Jhc2VGb250L0dBVkNCTytBcmlhbC9Gb250RGVzY3JpcHRvciA5 +IDAgUi9Ub1VuaWNvZGUgMjIgMCBSL1R5cGUvRm9udAovRmlyc3RDaGFyIDEvTGFzdENoYXIgNzIv +V2lkdGhzWyA3MjIgNTU2IDIyMiAyNzggNTU2IDUwMCAyNzggNjY3IDU1NiA2NjcgMzMzIDUwMCA1 +NTYgNjY3IDMzMwo1NTYgMjc4IDIyMiA3MjIgNTU2IDI3OCA1NTYgNTU2IDI3OCA1NTYgNTU2IDU1 +NiA1NTYgNzc4IDc3OCAzMzMKNzIyIDMzMyAyNzggNTAwIDYxMSA2MTEgNzIyIDU1NiA1NTYgNTU2 +IDUwMCAxMDE1IDgzMyA3MjIgNTU2IDU1Ngo1NTYgNTU2IDY2NyA2NjcgNjExIDY2NyA1MDAgNTg0 +IDUwMCA4MzMgNjY3IDcyMiA1NTYgOTQ0IDcyMiAyNzgKNTU2IDE5MSAyNzggNDAwIDI3OCA1NTYg +NTU2IDU1NiAzNTVdCi9TdWJ0eXBlL1RydWVUeXBlPj4KZW5kb2JqCjIzIDAgb2JqCjw8L0ZpbHRl +ci9GbGF0ZURlY29kZS9MZW5ndGggNTAyPj5zdHJlYW0KeJxd1E1u2zAQBeC9T6EbmDP8kQME3KSb +LFoUbS8gU1SgRWRBcRa9fd+8qbvo4gV4EWnyo0yfX16/vG7rfTh/P27tZ78Py7rNR/+4fR6tD9f+ +tm4n0WFe2/1v49/2Pu2n88vXaf/1e+8DBvTF+7fpvZ9/aOR/xOe029w/9qn1Y9re+uk5hPq8LPXU +t/m/R/niM67LY6hUT8ihomr1hLJYjdUTRrWaqieMyWqunqCcW6onKOeO1RMy516qJ5Rs9al6whit +TtUTili9Vk9I/KhWPSFx3bl6Qnqy2qsnpG51qZ6QbCHBWVhQ7ZMFVqE32boCq9Cb+RRWoTdzLqxC +b+ZgWIXebNsQWIXeYusKrEKvssIq9EbjC6xCr85WYRV6IxeCVeiN3AasQm8sVmEVeiO3AavQW4wv +sAq9xQbjVTCotpDCqvQWm6uwKr3jZBVWpbeYSGFV945WYVV608UqrOreZhVWpTdxXVjVvRwMq9Ib +7buhsKp7uRCsSm/hQrCqv1/uGValV/kUVnWvnSS+XJ4w2Tbw8QwG20ni/BgAbV2cPYNqB4vzY1A5 +GNboXiPgVTB4agScPYM921lFWKO/X64La6Q38ymskd5ke8YMBrXxHj4unF1Ju9uPqzy0z+Po250/ +ALzgdrHXrf/7jdhvu80akNMfVk8LMQplbmRzdHJlYW0KZW5kb2JqCjEwIDAgb2JqCjw8L0Jhc2VG +b250L1FaUEVYSitBcmlhbCxCb2xkL0ZvbnREZXNjcmlwdG9yIDExIDAgUi9Ub1VuaWNvZGUgMjMg +MCBSL1R5cGUvRm9udAovRmlyc3RDaGFyIDEvTGFzdENoYXIgNTgvV2lkdGhzWyA2NjcgNjExIDM4 +OSAzMzMgMjc4IDI3OCA3MjIgNTU2IDU1NiA1NTYgNzc4IDcyMiAyNzggNzIyIDY2Nwo3MjIgNzIy +IDc3OCA3MjIgNjY3IDYxMSA2MTEgMjc4IDU1NiAzMzMgNTU2IDU1NiA1NTYgNTU2IDI3OCAzMzMK +ODg5IDU1NiA1MDAgNjExIDk0NCA3MjIgMjc4IDYxMSA1NTYgNTU2IDU1NiA2MTEgODMzIDIzOCA2 +MTEgNTU2Cjg4OSA2MTEgNjExIDYxMSA2NjcgNTU2IDMzMyAyNzggNjExIDc3OCA2MTFdCi9TdWJ0 +eXBlL1RydWVUeXBlPj4KZW5kb2JqCjI0IDAgb2JqCjw8L0ZpbHRlci9GbGF0ZURlY29kZS9MZW5n +dGggMzEyPj5zdHJlYW0KeJxdksFugzAMhu88Rd6AkBLTSpUv7aWHTdO2F4BgKg4NiNLD3n6/TbvD +Dr+lDxL5s5zydDlf8ri68mOZ0pesbhhzv8h9eixJXCfXMRdVcP2Y1idZTbd2LsrTWzt//8zicECG +jd/bm5SfobYv1XYnTb3c5zbJ0uarFEfv+TgMXEju//0KcbvRDc+jAUc13qMCe94SqgCsK7Z4jwrc +sQW4U6zZAqwVI1uAUfHAFuBBMbEFmBTRRAPsFQe2AOF9jIEt3kfViGgSrVHURgQjMitSK4IRmRWp +FUGBTINUg4gtQFLcswW4V4QgmSSpJHVsAXaK8CVzJnUm+JI5kzqTsAUoitAnG4F0hAb6jY3Q6AgN +9BsbAVUX89qA7kiX/dqtS49lkbzai7CN66bHLH+PZp5mveWQ4heSYaHqCmVuZHN0cmVhbQplbmRv +YmoKMTQgMCBvYmoKPDwvQmFzZUZvbnQvRk1ZRExOK0M6XFdpbmRvd3NcRm9udHNcYXJpYWwudHRm +L0ZvbnREZXNjcmlwdG9yIDE1IDAgUi9Ub1VuaWNvZGUgMjQgMCBSL1R5cGUvRm9udAovRmlyc3RD +aGFyIDMyL0xhc3RDaGFyIDExNi9XaWR0aHNbCjI3OCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAz +MzMgMCAwCjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAKMCA2NjcgMCA3MjIgNzIyIDY2 +NyAwIDAgMCAyNzggMCAwIDU1NiA4MzMgMCA3NzgKMCAwIDcyMiAwIDYxMSAwIDAgMCAwIDAgMCAw +IDAgMCAwIDAKMCA1NTYgMCA1MDAgMCA1NTYgMjc4IDAgNTU2IDIyMiAwIDUwMCAyMjIgODMzIDU1 +NiA1NTYKMCAwIDMzMyAwIDI3OF0KL1N1YnR5cGUvVHJ1ZVR5cGU+PgplbmRvYmoKOSAwIG9iago8 +PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0dBVkNCTytBcmlhbC9Gb250QkJveFswIC0y +MTAgOTc5IDcyOV0vRmxhZ3MgNAovQXNjZW50IDcyOQovQ2FwSGVpZ2h0IDcyOQovRGVzY2VudCAt +MjEwCi9JdGFsaWNBbmdsZSAwCi9TdGVtViAxNDYKL01pc3NpbmdXaWR0aCA3NTAKL0ZvbnRGaWxl +MiAxOSAwIFI+PgplbmRvYmoKMTkgMCBvYmoKPDwvRmlsdGVyL0ZsYXRlRGVjb2RlCi9MZW5ndGgx +IDYzMDI4L0xlbmd0aCAyODM2OT4+c3RyZWFtCnicpL0JfBTl/TD+PM+cuzu7O7vJHtnd7M5mkw3J +AoEkEAKRDLeISBAICRIJcgooBPA+CCqHEQVtS9VaxaviQd0cQDhaqPLTqqXaam21VanF89cotUg9 +yO77fZ7ZWTbV/t++n/9Onuf5zjPPzHN97+eZCcIIITtqRxxqmDGrohKx3+8uhKhx0RUL1xjnry5H +CL+86Or12p4j790FGX9BSLxy6ZplV4x63JtASILytuSyVdctNcpH4L5tLyxfsnDx/05r/A1CX56F +zJHLIcNdFViAkJueFy+/Yv21mfoeg9ORq1YvWmicPzMKodiwKxZeu8Z3p+VZhPI0yNSuXHjFkkz5 +aoh8a1avW2+cf/kXen3N2iVrHu76+FMoryOkVAgHUQGEgPAEKuDjyI9Q+iMIH9M0dXn6Y3qdpgTu +QL2ZgNButAdfjvagI+g5fAruehYdQD3o18iHJqIH0I3oh2gLEtE8yLkdXQyHAPk/xAXpHlSBHoZx +fBgdh7Jz0c3oIPJif/oTtAFt4l6HuzbBSBehcagBrUZ34gvTV6H56D3+VlSDLkRXojW4Pd2Uvit9 +T/ox9Dg6wP063Y9sKIAWwXE8/Znwp/Rf0BC440foPvQevseyF+lQSzuU/Clai+7nWnicXpb+BloQ +RddAG3g0HR3HR0kCnr4EfYT9+EZuAjzl0XQyfQxKhVALWo7uRwfxCDyFRIX56enp48gLdVwLT70P +daF9cPSiX6C3sSKcSj+WPoUK0GA0FfrTg36Lj3Kp/o2pehgxAUapDNXCldXol+hF9BqO4V+R1YIi +VAq6cH36DZSPhqM50Non4M4P8b/IzXBs4F7gJ6fHIweMy910tNH/oL/iAK7AM3AjKSOryYPcWiRD +jcPhWIwuh/G+F57+Lk7gfUQhr3KP8k/z34qFqRNpB8xIHP0E/RT9Ctuhpxpeh2/Bb+K/kQlkAfkJ +eZ/7If8k/3tpIfT6UnQFuhM9jf6F3XgUnokvwcvxjXgLvhvfh4/j1/DHZByZTVaSz7nlXBv3C348 +HLP4dfytwmbhDvHjVFPqWOp3qX+lK9Ob0UzAh43Q+h+hB6FnB9Cr6C043kPvYwHbsAMODUfxHHwD +HDfjO/EjeDd+EvdALa/h9/En+Av8Jf6WIDhEEiRRUgRHjKwl15AfkgfIq3C8Rv5OvuZ8XBGX4EZw +dVwztxpatYXbAcde7q98gH+VT8M4Vwo7hYeE3cLTwnPCKVGRbpGR/Juzj/aX97+bQqmtqZ2prlRP ++q/IA3MYgFGIoDpo/UI4VsB87wSMexa9jhUYuwAux2PxhTAyC/AK3IavhZG8Dd+PH2dt/zk+DKP0 +R/w5tNlOQqzNQ8kIMp7MgONSsoS0kR3kHtJD3iTfcBJn45ychyvnpnAt3BJuPXcdt5NLcr/h3uHe +585wZ+FI81Y+whfxcT7BT+EX8FfxD/If8R8J84VXhA9Eq3iFuFnsFf8hjZTGSg3STKlF2i7tk96Q +WwE7n0d70X6U88MnuI3cJG4vuotU8QXkt+S3gM8L0GJuOgFMJbvxVnIT7iHFwrXiGDIGX4RO8XEY +6xfIQ+QMGcNNx9PwLLSCDDeeJubzT0FSxz+P+vjD0LffwpOvFRV8M/lcVFAXRqQW6vwfbhif4F5B +b3PvYYl/GP2Zt2If7iNPcA2ABb/gxwpNKMo9gH7OteGb0F4yCSHrt/I2wOOL8FPAF2bjSvwVl0Yc +uQiwqIb7G7oVrSR/Qn1Ax1vRj/Fifhm6C1XhG9FH6GdAFWXClWK56MEvkcv5DpKHexDhn4Te1eJi +zAn56Dbcwt0vfk7eQlehV3krepd7Blr/Kvk5N50/JVyMlwMF3IQ2o7b0RnSd0MT/Hi9DHG5EJfwJ +4G43cpV8FNINwFXmA0/bB9R9EPjAOG465PgBcy4EvJgDHOJ+OO4FPsEDBl0OND4XuNhvUY84m/Si +ZYIDA9dBiH8ldTGal/4Zui+9DF2ZvgcNAX6wJX0jPHE3+gBtR7vxptQNaA0KA+W8iy8UJpNXhcnp +IaSDvEVmkZ0D5xdGuwT70adw/BxOxgqHUAf/RzQL1ae3pf8A2D0IOOx96DJ0AToJvfwMajifO4qq +UheRzvRkbg309z00M/1EOoKtaHl6FZqBDqPHJQEtlBIwx0n8e+jvDWgJuTi9nluSuhzGYTuMgg6j +dRXwn9v5Nv5W/mu0DWh+J/CbXUA3TwHlUNpH+iWb1q9b27Zm9ZVXrFq54vLly5YuuaylaW7jnNkz +Lhqn1489r27M6NpRNSOqqyqHD6sYOmRworxsUGm8pDhWFNUi4cJQMFDg93k9+Xlul+p02BWb1SJL +osBzBKPBk2KTW7VkvDXJx2Pnnz+EnscWQsbCnIzWpAZZkweWSWqtrJg2sKQOJZf+W0ndKKlnS2JV +q0N1QwZrk2Ja8vjEmNaL581sAvjOibFmLdnH4OkM3sFgO8DRKNygTfIvn6glcas2KTn56uUdk1on +wuM6bdYJsQlLrEMGo06rDUAbQElfbE0n9o3FDCC+SaM7CZLt0KhkIDZxUrIgNpG2IMmVTFq4ONkw +s2nSxGA02jxkcBJPWBS7LIli45POBCuCJrBqkuKEpMSq0S6nvUF3aJ2Dj3Zs61XRZa0JZXFs8cL5 +TUluYTOtw5WAeicmfdef9J87hYe7JzRtyb0a5Dom+S/X6GlHxxYtuWtmU+7VKI2bm+EZcC8pmdza +MRmq3gaDOG2WBrWRTc1NSbwJqtRoT2ivjP4tiU2iOa0rtKQlNj62vGNFK0xNoCOJLr4u2hUI6AfS +J1BgktYxuykWTdYHY80LJ4Y681HHxdd1F+hawcArQwZ3qi5jYDsdzgyg2HOBJdlrDGLFKTTt4uzI +Ytqi2FRAiKS2SIOWNMWgT6NotGQU6lg0CorBrxnDXcnFMCOXJy0TWjvU0TSf3p8UStSY1vElAgyI +9f19YM7CTI5Yon6JKEjxJItqcN2Ek4lEsrycoog0AeYU2jiWnY8YMvjqXhKLrVE1SGD4UAOM7cLm +0RUw/NEoneA7enV0GZwk22c2GecauizYhfSKRHOStNIrR80rnjn0Srt5JXt7awwwuQdRFdmTlOPZ +P6fqzZu0fHQSe/8/Li8xrk+bFZs2c16TNqmjNTO202YPODOuj8pey0DJvAlNXJBkIBLk2FVAyvnZ +wvSkSUnyJfAnMqRe3CvJgJUsB2uTk2rr+UbcbI1G/8ubetOn6F0sOXdbppnJ0YmB52MGnA9ontLB +QYNBvE6bPa+jwzrgGqCaUeHUTAIYj2Y3RbUJSTQHKLME/nrTR0fR0BxM6jBkE2gBwD8jK3M6oGAw +AzfDj2LnkMGTgdF1dEyOaZM7WjsW9qbbL4tpaqzjAHmOPNexZlKriTi96YN3BJOTtzXDWC3Ho4cM +jtErHR2LOxFXAtXowU7MgJoJdzQnZySaY8nLErForGkJ9KVzNFKis1snAETQ+M4Y3jqzU8dbZ81r +OqCCVbJ1dlMXwWRC6/jmzmK41nQAjBmd5RKaSzPpiUZP0DQMQ9NFZFY+eADsmHZ2lWcZ7HxRL0Ys +TzbzMFrUS4w81agozirSQbFc1MsbV3SzNA95spHXbpQelCktwxWVXjmIQOIgdtH4dcLJ7CbdWqOP +1sfoY0k9gRGhWV2QcxDKjsGoeyyux8FOeObFLLsXt3eO0YMH2JMuzpRsh5I0rz2bBy2nxXIeBPUZ +HZ9zrgdz5jV1j0XwfBZDifH0RzktNCKXhhhjong+N9GkkI5pswAD6UXrqKA157JGb0ziWHJB7Noo +7V2yMXZdFDJjSQ24NRTqRFNCzR0dGhwxGJVFjU1GTC/hwSF4UnOy/TKzbDAEOHHuVIFbGV51hygP +ydZ2g1nbWqiNAh1mdclF31sbtD6JL6Ex+2PN7xyJYkb9IKWNSjvmd8wDfIwmC2nFmXbAqSPUzJ4A +LbmXtQQz4bQIdIKllJY0yuSATcYu6CQXJViKWdpxQWzSYihBAwjdETBZUW1xMy0Vo0RDEf8/FsI5 +haggYQ/vUMeYZzhzZpBvR3LZwNPl2dPJNICOUjLUYBPQF0ay0eSKYHJVcyJbZCHtcwfQ9mhK4KPZ +zVNoaAWxMyXZvmghNBHkzdRFMci4ADK0psuMEaSCuoNqTosWwm10lDM1Ja9MDHgk8AQMLAoeRLuT +bG/QWpu1VuAheCYMdlBLCpBqS0F9ii2kfKPB6E8DMH9IFnbMgnsRnbZgUgJ+tnThkhhlrkmK78bo +0zby0Do0qymJgh0dMcAhaGLJZCgMj48nxfhUmsDfmkRs4RKq2S2lit0SQ+WA5rLRoU8LTopFm6EI +KWFjCQMHhHYZjRZ1UL2xpTUBI+HqcHdotR1A8C3Aq/j4osZW4Guaqk3W2FQvDMIZDMJUetYMDzIK +WkpoQbif/cWTVyQ6W6SScznsb3XCKCyzpzIlItlgFpHYHwBtiSTxjYKLtPP44nlMLsBE0cETSqbC +8OqAVUF6N1DR7IzYMO6fSm8NmhNm3AY5zaYAAHzvLMFbG3I54fyke9rFlwRhYIdQyS2NTV2EJqjo +m2e/uR7aigfaGug2muP6MdkL1vLLSIKnqMwWQIIvnUYCIp2zN42zcYPpQYpQIYqAmV4OhSNceZdY +GOnlBnXH/ZHXDnNl6AQEwpV1JQojB7hSrrBrTETv5WLdbk+lc9wQToOqKlisQbwawrMQjkDg0QIu +DPkqxBsgtEN4FsIRCK9BEBGCmF7VIKyG8BCEE/QKV8iFurSIOq6UK4B7C6ADTs6HPoeQhsBBO31Q +qw/NgLAAwnYID0EQWTmasxrCBghHIJxiV3TO13VPFbTd13UHS7pXrKpkpwuN0/kt7LR7brORTp9p +pBOnGsVGG8WGVxvZQ8cbaelgI3WXVLbT1GqvPDrOy3mhk15o+BqIMTmGnBiDDbqL86AkBMKJmRyd +c3cXxysfOsLxCHOEw2gxiqSPcrjL7qocZyVp8jlyowj5jPQZV0hft8NV+dC4C8j76FkIRyBw5H04 +/kr+ijaQE3TMIa6H8BCEIxBehfA5BJGcgOM9ON4l7yIneQdVQKiHsADCQxCOQPgcgkTegVglf6Go +xGIK10Mg5C8Qq+TP0K0/Q+wkbwP0NnkbmvZ6V01t5QEGJCoyQKQkA/iCGcDtrewlv+/6ugwwKg4z +DRh1iCtCY1EVV9RVMhzQz99Vd3mkl/ytW0tEdo0bRt5ASQgEWvIG1PwG0iA0QGiFsAaCCNCbAL2J +2iHsgLALQhICYBnEKgSNvAzhNxDeRMMg6BAaIMjktS6oppe82hUfHxnnJb8lLyIfjPhx8muW/oa8 +wNJXyP+w9CVIw5C+TF7oCkfQOBtcR3CPCqkKaQVcF8ivuovdkfQ4FzkCYxeBuAJCPYQZEBZA2A5B +JEdIUdfiiBsecgi9LCMo2YU+YenP0CMy0ldE9PgEQECNRvHR5wEE0UPaQ3Gix3feB6c0it91D0A0 +it+2DSAaxa/fCBCN4quuBohG8cUrAKJRfN4CgGgUnzEbIIh6yYP7i0sjNTNWYm2ck1wDo3QNjNI1 +MErXIJ5cQw/0NU/b9pOu8nIYsfv1RFl5pB30o8O4/WLc/ghuX4Lbb8btG3F7HW6/FLcncHsIt4dx +u47bD+FRMBTtWO8ZcFqr+3H7y7h9D25fh9vjuL0Etxfjdg3X6L0k2jW1iiWTWNI9jhIdpOeNBe7j +JFEY0SjgfBR4whGIX4WQZmc6FNKKjMIFYZoWdZfXG+dDR1euBvJ5Hm58HqbhefQeBB4m6HlAo+fh +Ic/DA5wQ10NYAOEohM8hpCGIULoIGr6dxU6IKyDUQ1gAYQOEzyGIrDmfQyBodaaJz7KG0UZXZBo+ +AwJPnoeDelCjJKoXqiE1oZ7PbQ9hZxjPCKfDpAZ5vcDT3S7Z1Yvt+/5l/+pfdmQZZyF3ke2UdZMd +mXR719fAuvG9XfFDkXEe/GMU5gHzcC2K4xJIR6F17HwECsk0rUYh8jSklV2hRrjN2RUfHDmIHfSu +fZGvQycjn4R6CYAfhw5F/qj18rgr8gfIeXpf5I3Q7ZGXKnplyDkc78WQHNRY0QOhUZE9L7OiG+HC +/V2Rm2myL3JTaEpkZYhdWGJcuHQdnOnOyMXxeZHz4XkTQ5dF9HXwzH2R+tClkTqj1Ah6z77IMGhC +wgDLobFlIVZpLAw5PZERc+bU9OLl+mBpp9QkzZBGSpXSYCkqRaRCKSjly25ZlR2yIltlWRZlXiYy +kvN70yf0BBWc+SKTnyJPY57BKqExQUyuEiwTdAFK5nHTyLRZ4/G05NFFaNplWvLMrFgvtoLxKMTG +Y5DOaNrs8clRiWm9UvriZE1iWlJquKSpE+O7miE3SbaC+TO7qRenadamIHXTHEAYuzbdGaTpoE13 +Njcjv/fqen+9e6yrdvLE74laM3Hi3M8/AC4cn9w5bVZT14innioc35ysZHA6DfC05A+oO+cA/gKf +mjTxAP4HTZqbDnBj8ReTLqb53NiJzc3TenEjK4c0/A8oB6jzD1ZOBilNyyFNDhvl7jfKlcD9UK6Y +JlDOYkElrFyJxcLK8ZiW61xXPGliZ3ExK+PT0DpWZp1Pyy3zcgmUKSlhZbzt6GVW5mVvOy2THMuK +hEJQJBxiRXAAhViREA6wIo3nilRkityeLXI7q4nD58qEjDL2E2YZ+wkok/hvf0vGJxK4e0zzovnU +FdYam7QEQmvyjquX+6lWr3Uuas74yOKtly1aTlPQa5tjSyYmF8Umap1j5n/P5fn08pjYxE40f9Ls +ps75+pKJXWP0MZNiCyc2d09pqK4ZUNft2bqqG77nYQ30YdW0rik133O5hl6eQuuqoXXV0Lqm6FNY +XYihekNTp4zGN0+Yb6TdxGYFtG0FW2C8V10zluHwmKj/5uBBUF12I1uiOanExiftEOilIeOGjKOX +gLToJQf1d2Yu+W8eEw0exLszl1TIdsXGo8T6q9ZdhfyTLp9o/K2DH2Stv4oOuBEn1v2nH1yblNQX +TqSrq9OS5bOmJevBgO6UJMhtpV1KjjbzbLZJvemjRuZQyBxNMzkuW5Dm1dE8iyVT8Lvzf1UmnUCp +oJ0c6sZ6GK9H65q5ZHjabAIcYXbGsXQQFCsqK9Y1QwfX4QReZz4j0+xEAhnniPbZDOuvykCZsVif +SY074ZZ15pBkf3SwEtkRW88ey4YzMb9pnIMbyVWgcaA7D4N0CKRDIK2EtJKr0N3xCEdqIha5JmKz +ToxI4sSI+dTmBGKOFTAcBNDZwZ4Y30PwSVHqJffpeUjgT3LIKvEnMSqQReEk4Q6T4ciC78NDkT+h +nqnrr7tIPV03vb8O1QOsnoVo+LCoK+oqgQiYLjqrcUfP6gL6Fmn8UagLXcp1k2uEg1CdDf0guSkB +I4zSX3UXlVQLvemv9KJ4WbVNtEoCAoEmCKLtM4sscxxBklxndVraLcQCs6Z77M5qy7uY4+sI1u2u +alygtD3hT0BjErQ1an+ipY41SoWjvw4i7HLX1tIwfBhOJIK6gnnJigQRRAaIA399vXrMVztseHMe +N6LKw1WxeEfl8SHvDD8+jOvGvlOnUp8YMZUd8yCrlPWijvZB9yCBw8JnBHEbNbwDE7xCpO1Rz7T0 +ofo+bNQb3At90lltgeO15yrbOpRV4f7yy9Rn8OwbUzNJq/A62HUXsWdbS50guNySrKq9uKobPeSQ +IdVd0kOOSxGnchrHcc+4frqNVdd/pk89A3XW1cNE4JZgN3JKmHYPasNx4qquGVlTJUpweFSM3/vR +b6fPO7zxutLzYgmcSM08jL/Cjs/e7v/2teaOnYd+kYqkNDSgRVezFimDyCCVWKwqRm4LbZP1IQ5D +2gOG2qWO3vSpHlUlcwD4qsfpZMDJHrudAX/XnVYrmeN0RBzE8Yw702qKit9pOXZazJbnxZCrujQO +R5XX5/WopH8jTGLReaXXbzw8b/qrqZn4BP7r4QM7O+b9/tv+tz9LfZGSabvXoj5+NL8PMK2ZtTuC +rrSQr2XuSkESLVdaeevXAr6ynswghBQoc+cx/GmZfrqur049WVeHKk4D6pyGedtPycMqcaSXq+pc +xSF/RaKqsrKqogKaVuKKjoi6qlxRT9RFcKoNb38Kb0+19eF7dtN0d+pK2pKnUu/iW9FxZEWLaUv2 +WoHUnhZ7cYMex1wdIdiK65AVLESuDomjpNEz0AK0Gm1Au6DmXbaH74VROt1y+qQKLQOsprHap/Yz +xKJ4JYmY+kopXlUch0ZVAVrli1LpyJE1+443zK2sHckdP952R3x6wcJLoDXjcC9ZQa4Aej+PjUvB +GrKGI9PxdGhIDJGAsAYKFfBr7qQjcrJF/RBVTO8bPgy1wbR0IR1mpSJAq8kbEfWMI2W4d+9euOEg +dHQL9JFDNeypfkK7VGd05FnE74Iyu3jWlzMtjC6g6d3ZhmeaffD48eNMO0t/RGoB5zhjxA4gLv1u +V34t6U2/q2v5tT/mMOEe4p4Fq/pqhPPhDmBjwKu4jxH5GHDxyb0I8d3XQ1116uk+1cCrLcLQRMtN +6jGKX8ADuoGTmRjmwVUYP7kj1VQg/P2bfGAJc9If8S7hKOB8IU7RFnQSw+EcCPNCfthu9wEj+pjh +NwX0AorgFhdSaA7yKgrECs1DFYDcxyE6Dj1mfe4Uv/uk0/AkkT7pQ6AUBnymF9hsIn2kSnOQqig0 +pnnZR557pn4RL24hW21bnS85BItk85NJeRd6LiiYEJydN98zv+Di4EpppW1R3irPyoLW4HXkGvFq +2/XOLeK90k71Jf/b5E3xTdufnYFsk8ap6dNIQQpMTyPypb8AGrJl4K+QHdmxrrsafessejRWPQzo +1KICZx5nhZvMgpb0x0bB/Y2WHRGXoii9YB42uhw2mwHIdjsA3Y2udYiydAWepCHqFjOLIjlTFBlF +9zWiHeEX76D4A11vSfRBTMGWNgZmhgK3tKGWJJmQ1BuaekStQA0BQ+oimu2X6RPIC8ENwQlhFP1h +CM3NzcFOe34vV9Gzym7nAwB0reIFwIxEfYKiueoeWVXp9bqB8YixotJ4nuqtqhzpUuOxIkmcs/L1 +XVd3rR+/4vWH37ju7gNP3njjk0/efOMFLeR1zOPznlnQnUq/nUqlnt9z737809SPPz+Fl+MVn12+ +GXD8PRCI3wKOWbGDYli3NdtzE7Cao4VMwGqMRXZQ9GgjRyXgSn4D2U7uk/lneGxBokA4i4AVgl+2 +stG10nlCmDrzwD5i/BmAT3UXQ9cQQ1cHQ1cYLb2AIqOJcQz7Aoqgg8wV6LMc9FkC1gRdIEKB7SCu +w5uQwSrajBlhPzgxNIN6Xy12UbnbglpAzLAfCGCCLaIuCBasWOhY17trgasAQ4MRj8ZcoiiNAPZV +Rb7tGff67B+/X7Gev2HsjZGfT3l5AfShDqhbgpELk1JGmwZFWVyq3Z+XJ86xU4JyuRjwmW5RVYDC ++UKYEqqPFgiH6dVwyAFXwgrtYbiXHII2WX0+LaK6CNEi0JSKN2iDKo6jCopgiXoaH6ukJEyyFSpu +N2EV6hani5j1nNBt7jwyJ5xP8+izu+DRlGHYbGSOj8pBNtrfVxulalofrY1Vpk8ZI4wRDwlHxEPS +i/JLIWmq0qzMdqxUFjuud1+fd7v7sPuDwAfBUwHliG1/HglbVVkUXw4F8kOhgBwKAKeUAyHOHlZ7 +yWPdM1zY1Yv9e2k7EW1YNyaKdQC5W3PI3Zold3ujdZ3vdWC0lOTxIbIRaUjFo3TFtbeeLCCryQbC +k4OkGEXw9k5GpC3AeM8kKP9l1AnCtL6vv+Wky03xAaItjqEJB7BjQ4plSFa3BNWQWqiGVfGX6VNI +AkKVIbVAMOl1VDNqwS1rgWrp1NqDkmQnYRDNPauIkm9n1JufoV5XrasKhpTKaU80XgMINXLkiGqg +WyYcgahBTIIuJEq8dLaG+Eoevf/z3ffdcMsD+EDeV797/cz5Tzz3yPzwnj3j6hYdvfnYB0tX/uCB +jrxX3/p0T9NThx/bunA4YGJj+kPeC5iYwGdypIStwK/T+fWHEKYkk1DgBJfFrHan4gxbrWWecIgP +l4WEMnvMrvgLQJHSVEqEmhSnWEKLxysojweBDgdy14KGCnIMOtP3gvqCu1Y9lqikgeLHMMHutU+y +b7bzk1xzXVcHuYu9q9QV+Yu9V9mvy99s78i/Pfi43WpT7A5ewlAfpohAF2kPYbpN0o5H9CiKh/cf +JI+hArJct0DrBGie3T0AL9w5eOHOEQPudQu01RrR/JSOtHZpwE1Szk1Szk3SujiTHXGM4mqcQK9P +76f3x3cM8ffiUV0Fr+ODeBSoAUd1W1Yy7Bjci+/JIFeij6FXhvmfTrRkZUD/SUpGoB9RXDNQLYte +XYLGAXUCGjVTdoTbKBIhjPmYYndaAXf2rnI6Q2U8QPtXldkL/P6Qh2FUiGFUZUUVRSqq/9VCUkWl +Q42XSgOGVVJNFjQRjGKYRGMUK4o39kR+tHLDs4/cVHVhvtu2rnfzisu35fdEP/35tS+vXLr4lh2p +j9/8VRrf6r9vS/KWGx/Of5Bce9OiW267Tdv74rKuxQseGBr+xV1HU19+SO2oAHBAFSwQK7KTEMW8 +w0hJf2MMe0+jXcwIEMGUJKIJWLKyxQQEU7aIJmDJShsTkORMYdkEJFM6y3K2TEY0ySYgmIBoAhYT +yMgxvabR3aQsV+5XnlReUoQLuQvtP+Q5N7AspIicJFhtnATS0G5/mePzOY7n7Igodl7iDpFDoDgS +vEu3Ip6HIuhlK99Llu4XBKteGKm2mmLOauhUDPiMKVfWXlyj2yW9KFYttUdHSDuchNKozZ5fjYhK +NAKKPtxM7wHg5D56D9nr6MXbGOr9neoeVMqdpjKhTv1QZUIO7OAzda7aWmbsbRma4IGzOZ1OEHvM +jWAH9dVdC3LiDd1WVcsVDanl+MLCOmaEAyJCGT1f0W21SntDraLHa5WiEKRDag0zHX+PlwolgvsU +3iJydrBLKvdT1QUpvClKE1VVlYYsBQMFV7mqPDEX58JkZ/9t5Kc/eOGFntQIvOBxbt/ZCx5PPQyc ++0f9K4EhUK03KvwM5KrENJI8E0fcJpCnZGbbbQJ5SmZK3QAcoIRuMMEDCMOo2ukw4pDDGvZ4Qm4q +ZG1Ong+H7A6MJD+oIEyFZgBjmFT8UYZHCRm60X8MmBzlcdVuJqadLJ4WuK6wo3Bn3hN5zytvKn8O +ypY8v6M8wOVZPe68vJcdznxHXr7DaQc+p+fRqnXHLrA3HU7dgzPN2O/k8euUB4Iw1F20Qa4F6mp1 +g7pd5dX/mof5GQ/zgxWh+onf5GH+HZr7MB6BnPhHUHJUl2Pv9/GyyEBeNoCbtVArD/gXG4MW4DQt +wPxPbpGHJgRAK5QrMHssw4RhtoMgJznG1yhna2uhrh1T0UIoZM9zgL7BewwO5/E4QzxTd0N2pxsk +Z9cqJ28KzAoaAF9chtzMZW/A0/LAzuWAryFPvgS6cHzOLzz3rbqlZ8+2udsGPXkXeat//4zb7j6K +5fV3nv51P25XO+449sj9XTPqveQfz6Sunp8687sX7+46Ad2fDpjmAblZiMrxJzmSM+LEEbwAczg4 +KKzbsd0O6lRQKArn261hjEpUqmgxW0sN+1SKOj4mN33M1vJlDKPjbxxX/8dEoZY+9VgLRaEhKwvw +REn3TCyYqM1zz9ZWcoulxfIK92JtvXxVaJO8OfSm/IbXJWl0DksNFiDOiVFlLkihKLtAm9VgJ9Cw +IH6d6qK9VGKajcRUdqG9JQPwpyQHf0py8KdkncrwR8VIBVYFfTu1n+rc6o7BwKNGdYdNogubbDgM +XPMQe04Y1+r2et8C32rfBh/vUzMFYDQYW3U0+rz0UT4vbbOvlxR3J7KmkyErc/GtzxCcTGDCgGWR +6wBVwHpKtZgW7TWxiz6Ays7m4F6MBat9EMMpuz2YX8RwKt8eFJjIDArncKrSwCYsxUuZ1SRKVDq6 +qfoVK0IutYbKSpyfg2vct93+wVNXNo6bcxkZd3hZT/81r93219TJn97+8Z53+mtm3HXR2sceueH6 +p/hZjhXDpg8b+9lfFrWm/vX7jr6b8TR8I37yV7ufO/tOy1PNvQ/e++yzMEsLQV56hSdg7O9g3gnH +MTvm4Y/IvAWECmVMwwjmLYp9HccROi0zmFbLkYBTXmf5XzQDsHIB4eohWY03gG1X4MgQMPUatdVN +P913kXqG2jzU20C1XdAQDNUW6DHYY1E4wBVKa5jRWlV9xoMiIk6UYiPd7pqF3N5tqb5pI50HuFv+ +eTv/zZ5tP0q5U9/2/nkP/hS/+ADi0CygmgKgGh+KoWHkhXN006OgYHgoFWNg35A5Q4e6o2FRGBR2 +28NU4DMnxel9zEeRcFIPHSUdp2mQUIBddPo5033HmaW4LMlxxR6FFvewJ3oYyXnO+SIGOjqoDOpj +bk/DOtvPGiKaDRGNhpxkfg+nKWYz9dM8AM7qRTSTVkvv9DDe72E9Pdc/szKoC1dkGmAGSvXTR3hx +mXeqd2r8Q+WTYYJlGL4J3YRv5NfLbba1ylX26313oA68jd8sb7Tdpmy23+n7jeuFPLeCwn6kQE27 +huKcwRxA1+Ecug6bdL2vMbzuiAVbxrnJMpTIKZ3IKZ3I4QKJdU5dAy7gxMipOomzF9/dU+k3Sd9v +kr7fdIL41yU5zPWSZd3FZqFis1Cx6VQpXucxTXXNo3uIZ8fwF01ZwwQMc56czsqbrPLsrm1hQ2m4 +rLNsoCh9oiukBYAJdGlaBU2GaKCzn+gs0xhXMOROy9o21AZ2WTeM3FDGFoJB0T2IsQW3XYwytiDm +sAXmBcfx+IjqjDFmqsoIcvLyc7hBLmvAK9as+vDI0U9XXrHlztSZt95Knbn7ss0rl2+6femyraOn +7pi1cfeeWzY8wQXL7l2x6+33di39cdngY1sPp0HNP7r9V3j28ttuXbBoy21n09N3zPhZ+y1P7UYZ +fx+lrDAqJ/PO+RT22yIg3UtcINvPMLSkQp7JBT91lAyieOl3McR0MX+Jy+8anLANClMP9wwH53Dk +owaMmRFoV13iHExVjSJqfNPRPpZoqWQct5INOOAsJSKVyq93/ifrZ8hpxDl1SS9n+pKL0eJ/qHVg +Xf9WVUVuRfqU0YELvXrsEu/c2FJulfeKwLLY9YGbwtsCd4Tv9z4ZOBz41PuhdkbLO8/7oHePlxtd +tlgkg8IzHAuoXhWileDXGwxp2EOrjYwrzcH9SA7uR0zcpzCuRbaccrb0mWw5W045Gx6luwYqWzsG +U1m7F2StSQUlJhWUmFRQss6VpQKX7iKuHYkBVAAiMEMBGfzPqlznROAhVAq6VSx9ojuqiZrpf2jD +Lc1MAPI2hyEAYcyzShWThLleiKwANNSpsWREdSmVfJAiQHy3i3kW45iht4fh/Zo93hsXzrqpYSQe +eeiKfWex9ML2vhuu/8cjz7xNXnl8/bVdT95408N4lnr9lRdu+NMaxd+4Est/eg+r96f+lvoi9VGq +++dHuOqf7Dv2wDYQf4DfBxDCm/k4W+Mz1pU0sBVEyULEOp6rwyJvJXWgdiNCfYQPy5k1hzYqy/pU +Ywkrs4ol8LK5CFBvLANUeehC1oHjx49zzcePn33i+HGoka15sBod6CFWY8U62622H9getZ2yCTCl +cWuNdbK10brEutf6vlWyWR0SbYlUJ4qCg7c9baXrIzGhjmeN24iQIEp1vHWUbbRQwdfzROMx/7DT +bGjd6ZNgiNGFEWqM9ff3qcYqCWs6Ul+iYhitbQvut1kHdKAitwvZRZPjmWUTsz/m4gnd85OaKf1B ++AOagubiEaxfc/moqnmj0ZIR9irHJMdU/8To5OLJU6c0znZcX+bwlpThuKW8MF42IjCydkJJo7+5 +8JJoY1nj1ObGJf4lJUvLrg5cX7i2eJP/tsC2wjuiW+IFDrXBgbhZVCmxOkuH2RpsxCZ5D5Hz0QQ0 +jRzqmTCas0aoETMaa4k1CZI4iKejUnJoX8X5xU4JS73kVt2pNoxFxe5dzuJh6hpQLg/iJ1GQPNhT +P6q8GMpbUIw8qFu0EXhEQdPcbZm1r75+aom09J3uh8Fs6UMVfX0twDtOwjDWt5wEyshoNNQBF9SD +5eUVo52lFU6Hc9Ysm807ehonI693ghwZTddVqurBjKhnhkSVu7ayvqoiY1OUUNSnTJ/51X01VZyB +8zUj3SOqSXGsiCeefDdfpRXXVIkiHysqLi6F0jVuFK3k6fofMz1K4zg/Q0tANQ7C3z7u4ZnNuy9/ +9Iu1cx+sLereES4rHNG4dtPTqT3HP03d9Ic/4B98iUV8WdPeqq9ST/3j3dTtqa8mzF58Pf4V1r/C +d6xd+Jt9f5o0J9+e8t4ye9SNbedvWai3rdAfnXbJ8j9tfAjX77qk5Sf9C7c5g6XnNWD79idw0c// +nFr26ZepB59M3nz52xvWfvCjX/z59DvYibVXXtrzSurdv75cXlqAL7z93gm3vbJ0685xO34L2JPu +BzRuFg4CFTpIB8WecYVgJ3+V43w9m4UtOflCDsybcI47RuSzfhlF+WXmlm8MZgrFRJvtl5l7T5uZ +RDEz8blM0Wp6c7zm0oNp6NtMp5LVanqOTMDiMJth5khGzv5G7HCqzJHyRU8G+IrJT0LVymamETLt +TmBxhTpMXSYvt7SqW7kd6kvCC+JR9ZRqk4Vm3Ega1OW2pPpP5Z/2fzosvMLbeQdns1oEnlfsDlmU +JAVgWVQkjBDdTOBkyxqapOTDJcJxNM9D8ziNV/LhLktYEOSwyIm9ZI1uQbLyiU4wIQexDXQFm+5W +NLRE4i5u4F/l3+O5HcBvejHWbQ3KUek9hduhYIWeq07pVYlskNolIv3A+eYfDX5UAAH+/EA6gQK1 +rw8Ioi4ApFRHF3H76FJkAkz5LUP9LDW2J9TWblGPHXMcO7ZFMFJgWtOStlnTkuGZ8wx5NK+ph3dy +snQwfYpunDD0rrVtLd/jIMr+gp2y2MsN15VVsowwMD9ZwYQRaD1bcACSjOEqHOOiXF6Ui5eKEkeq +fkea3nm6/ycPv4X/cd/kolCVcPCbyfhwaiKZh3ceuObOO4Cv7wSb6hPAZRez4t811mkBwfQyukrJ +85NjjbGlsXWW2yzi5YGrhDUW4P/CrTax1Gvh/KXlYW+hBfTpj3Pw/ePvLh/q/kaLJc8dLi8vK0Oh +wjBMUCQcdiHZD/emsvf6czQMP2gRCrvX2uiPiwo1Z8Te9Id6CVWeRDdVnESRIoIo05aKDPXEfIqW +4uySAc8daLubz1UbS+JKiD5XsdKnKRSZFfosJTAY2vgdu91qmuVhjS3CaZkVuDNMn2NAZvXtmx6G +tQYgGutxVrYG15IYM9+fXV9rqeun7seL2Pl0wwdu/M4tt0CA+a0DuUhVS7oa4q7FzB3O1uKCXRZ3 +OZike1e53RgZSyhIxoWGlUpy1HS6OO+K5nizHSSGo5XGako8FoVrNYwRA7yTxHe/sm7psk3b57b/ +alvqB/i8jaMumDb5lgdTf8ZXXBqfMG/07B9tS+0RDjYfWHLpz6pKD7cv62wdzl3s8i6dPnV12be7 +JGXUyskXX0dXV5amPxKuFl5HhbiC7ZtYRFYUEmyYrWxsPtYXUEhDlfZFaA1aX9iObivcge4XnuYe +tx/geuwv2l9DJwv/WehyuAtdhYVcuTjIVR7SIlPsjflzPY0Fy4WVhTe473Dfz93nuD+0Gz9Gdrv+ +4MhD+Sig5qsBnm446BpUyxR4bVCt6gQCCuaFFS4Y5i1q3HkBimugaQciPnPSfeak+zKTbm30xTUZ +A19mp/ZGmWGKXBBeNN/Ys5RoYRMIcwlAxhnj8hlblVroenYigdcGdSvwNd6pqgof7OUqe1bxFiUP +gK5VCmfMFZW0mWUJ7GOCE+bEXVwFUlOKU9WSylWqXPI9z52Xev6DvtQff/IsnvDcX/DgMUeqnvvB +k3+bf8WHmx99n5Dhn3/7K3zl7z/AczpPvDJk1z2PpD6/+1Dqk47DVAN6EGTYPKB7J8yLoUu6tQie +IBvU6VLDTiT7BlDRwB0EJhVF6MBYcIQtlVkYSVisbEeFn+UwomISIhApVM1hVa0Zf7NqKP5AVOp/ +TVT/MonqK5Oowt9DVJnTlgGUNHzYhOv0kVxQkkVZkHmZFwv8AT8RbVbgAVZQYbz53jwvJwY5XxS7 +HRD55VAUe62uKErQde9y+G0EvakTqd9Pahkq83l9XrcnnwCNlUQrM0uWpUBZD+Kvn553c/P6dRdd +f/fxTalOXHv348MnTf/xqov2pH4jHPQUXnhZ6tVjT6RSTy6s3DNy+KRPfvbhv8rDdDfNI4A49Osy +NtRn7FcThbAsSxLieDplVkvYhmSJ4ni+6q6WZnMXaFbNTqwBO28hWRlvrg5lmZnl/4GZWSz/gasp +Yy7JUEFmCqabjK1l+umT3+Fk1CoWZMa1BAEjizmU/He4ljGcnmgmPMIXn32QS5z9A3ebcHBPqv6Z +lH0PHRsw//lNMDYW9DobmyI2NttBjTaHB4bmAY1oNkICtv+f46HbjJ1BGfaV+s5oWMfM/4+jcdLw +K1L7dMBI7Gcj8W9D4P73EdjNvXP2A5Lsb6C9H72nfym09ArgrweAv5bgJ1jfA8H8oIe0luJL5Tzs +5oqLUdTtIyUoTBgD9NDWYiz6wg4uGhYtGMdLS4oHUHpxDqUXZynd3liscRyMYWkrW107yUaGKYWZ +Zba3GaYwpdBBayFr20txaaE52IXmYBdmmWphXLNia5apWpn7xVoQX3TJAKY6XW05kxlJlQ0lVXmy +HkoYTjg3lolrqWUDND6RjwVDgVBBiBOVuFriiUficgkfj5X47YVR5HXmRaFwfp4mwVmRUBLFIRsQ +e74LorAlGkXFHERs2y8QPbVCs4oYJX/UEtzP6cXFUQdzB+9dhbGDesYq968SLe68PIePsXQHN2C9 +2cV2x1C+PqLENYCze33SUAKsnW7ppEYTMAwXdyG5YnvqtV1/Sj3U040b/vwQxvfEn41etm/1pueu +iY7agsndN58aS+qfwf0n1q47gC/905t4Xc+y3h8OW9M+feZtM7Y+dCz1VfvCGuwCHHkMuH0R5R14 +sqHh2QETvHmeap4LW6y7rK9ZiVUgxCYDVxyACnIOKsgmKuxtlDVJEulaLFPGAAV0G1PI2DqOSFdb +PEwpw0wpa2m3YzuxmXhgM/HAZuDB/kabltnVdFS3QqP+C+KTM8SXIwu8GVak2bFmb7C32tfY+THN +/kRLW3Y7U1Y2GOiUqDOwiW0srG2pYAICg6rNWWFWdfsqjkMY1G1ZIIwg68/p2nRxjW4ZjUH82HPk +m+ee6xeFg/0/I/O+mUy6+6dDb44AY9oIY87hBrYOS8z+cyZApMxAcACMs2fMuK+zA45MGIoKiiEw +OQCyRb815oMVzcD7GimvI3STV/eo89hmr+6qaiMdMsxIB5UZaazESAvDRuoPGJvDyu1qtSbsEJ4V +gN5BX9mOdqEk4iuQjhrQe+gUEtwaZO5AnGAsutO58Wfm7O/mnH1mztkZXTWMODZnj/BvNucI6wnz +m7rawVJraW5bW9efNYHoajxTnbL2TzcwR5LZ6UlH/8hz1JqBcQYLRriY4jaZzfhfmCuqqZUto0ut +I8SR1inWudxm7o+cdLX1Le4tEPGUOzHVZJCwje8QnuI/lQUrj0fwb/J0X/oJ3eKOVnMajUBt7FZq +3TS3G87lTMrTtJClR7vdXpr/rj6hAOosKTlPthQUnMeX+/3jwTyRLFaLbBU4ntcEa74gwBlQjgim +rGi1IoHwGFAA8MvKERtGfC8ZrTuHCXiXkBSOCicEXrhApnm2YRLWwDRNSpzUSzZ3/0dKAhZq0/5f +TZMvzgnx3dTsTZzTm/pb2vqo/5AywDpKJnV1NAANUNuX7vuC1M+2SUiyWifXgaXrB0s3eM7Spcbk +n0Y1G554enKqW3HRoT2l+wAQVYerWlYdarWFQlYVUC/ztkVz4hwC0K0VLksRjPHgglqehqJgLSDf +u/u8AHprRToFNnetXJRfy+v5tXRK9pYA6KnNsaGb6ZNx29qWBKLGdhAeKYo8B1PBqJwfQOVVIFKq +MriGoxj+JNfO58ifsNR/H7kljfrPnAKCLyN/7P/52XvJh5+meAMX+XL2TsFjxi57TIDDCkimfs5e +8sReiWSpnzMnj8vqHtx/rYud+Y4OLH6fDvxhi6F6UTUDcaaaxXqZ5WIe6NXvgZb+yTSpexESndAD +lTO4llxuMxgPAWDAzhAQBwYzlh12FxP7QPEACHS/5iAKKW56WXAqnAVhIltsDiRbiNUm0v7ZVNon +G/RpHy1lUxHd5JPp+Vdmz8/2DNh/Tdcl648eVV977SjdEpbIoAgy92NHJMZkRBZzLOZZLLBYptge +oxBh2hsoB1RNcZzzYFlZLJkOLpkOcITtaROwolnd1U4WCWCtYQfo2zCYbE8SfRoD2EMOkUbkRipp +1O0ZNVE0p4s9FtFVzsTpitPMNAGSMjrTksPxDCII6hsQccr5JCjzVyublV/DUCpTlalOrowvsQ92 +NHGX8Ffbr3Vsscs2Isi19pGOGWQaN1HS5en28Q7rveQ+bqe0U97NPSGJbuJ0OIYJBBgRkRW7fZgg +AygrFzsvxjomRJYtVhswc4dDpfPU6m53E/dBshvEyvAuQZN78fC9isVqOhczHkTd0mjVdGWDDdsO +Qrcd2AZlSS8kTozGWXMWgxDTHxQq8JHmXKNitZc07teEVqFdAElCdne7qLwuoK9JtNT5+ykCM78b +nAVyTk+2UPytY+8FmUdA7WP+uC03MXccJMOHoazbrekXSAEBKaffRCT9JnO3TUsqcG1QLqOyp7/q +dFjpxcxurzf2RWsdg6Nsx9e+mlpHZQ0D9w6B3MyurkTz2rYW4CZ0KQnR+bJTfi7QF1qJM+Ogo4ch +tcCS9I2swVFQG3AMu+7FxfiSYd6CEXgBFg6lGp9NNQkHv/3i7vMbfsKd/WYy/8q3I/gT31KO8ADI +uAi1bwhhVMn5TQexbPreuhrdNlOBkv2Kly36f9yTAU7rMZdr/BxZYTEBuS7JIIZkInGcbOEJsUgy +z4Hq921W9eNyVD/OzN8LPEoUBVPMC1nVTzBoHTQyPcAIrkWzYc3WYGu1rbG12wSbnGtrZawvzdD5 +7NDk/87m4r+r9mVtrhytItGSqGP40tJ2+t/1PLb+UVu7hWfIYsolLn1iP4gjWYMIsY1Xw4dR3R8w +oUfWJ9fCEB7dN7lW1isNsLJWAmlEHUv7CgCsNECaGzPeb7HFaiVHPoQ8en56Xx6AhQZYCKCHgl91 +ZsUTzqF6A5EUTkZY+nexZHjxqjBVPbHrgRc5cvDFsynAmo38BsCY9m/b6TdJwCJ8R3gDOVAQGzrR +tIAT56v5+UFfMMjzKp9v89mC/JO+fY4XHJzP5w8SrVB3zcib4dMDTUKTZa46x7Ugb55vgb8xMDd4 +h+8+ohaEOc4dtlk8A+wDTw6SeEz7YF+jJ66B7f3LnFdJJMBFOr2SqRhKdJMInVSJrojTeZXMZXmJ +TjhjylKgvRAXOk2R6TRRyJm1IJ1xijnZt0wypmReIxJz+G5BaNE5i9z007VkkWX6v7960tLSFuy0 +uZmDzmbhCpg1x3E5b5PQVSzq32FWXI2KqiqRq5rEY0VoEd6KR76CJz/dk9p35NXUwd2/xoV//DMO +XvfJ3b9N/ZG8jK/AP30u9fhf3kvt2vtrPO+XqX+lXsXVONiNbT9IfWB45vh+oHU78uOhhka7xLUy +n0xTp+Vfol6Sz9uUMDBy5PMbfg33gAn53g3e3Y3uuHwIpsfwxjsaZebxltWMUDytu+k4yQEtgOEv +4LebI243R9yeVVLs/68Oku+6iwpydZVzXvA2Y0oy02H6i5hRRk3uTofCHCUOB3WU+L/fUVLpCxOY +l2jUBXDW6UbK7pm+6p7mz1IvpbbiGw4/2HLh8NtStwsHHe4l+644lOrvf4bD2zbMv9Vjp77Rh4Hb +7oEZ8KMisoHNQNRtc2D3yNC8yFL5ighvYS/UyCyWWFwMTIFhM3tthQKKCdhMwN2bfr/bHaiG9FR3 +UWm1i54XllarmdSZSeH6n7oL48Z1KK9mUnpdnwpAieOC0AXaLNv80BWhtZZrHdc5N1m3On9sf9LZ +6/zY8ZFTBdrRXM58l8vpcioWd5BEA16r6KZvugh+i8XrCxSEfb9MH83x5x41LHafD0WLGF75/U6n +Qw4PQK6Bm6Wy3oBw3PGAaL5JJ5qYwNwABcwhILJVmRateE1xezFXXOQn39kZlUUv/3+LXuJ/lAUx +ash81/+WofiCk/6MT5gqDhksA1sHTmor2HstxmstQvYtw5wfytisulXWnbVOdbTLPZqybNzGdAYH +cP5AQa0LZIMbgkMP1apgkqhFEQhZZt8c7LIUUBeRbltVUICwE7g8LmJMJoPPhp/h35ZsfF5fXowb +SgCjYwy72Vaq6MOk49hvrn/59emD5lyYPv3cnCvnDolO+yt+eNPOi378aGqYcHDGr6974M3CkuKL +rkq14eG3bRtlk/qv4qpqrpuynL7BNj/9Ef+/wutoGDeOrWW7UGnOrp14Dpzd9wrzpWZmsMAEAgCM +i7By9pw1QCUHtuXAoRw4aMJgC/kzCEFMABuAPqhxEbeIX8et5/mS0hFcbWgCN1W6sHBSZGLx5NJZ +XLM0v3DuoNvzHDHqkKTIU2wCJSYQN4FSE4gxvDIKG0CJCcRNoJR6MCZTaJA9XkyKudKSkc7q2MSS +SRXztMbYnJJVthX2lY6l+Uv819mut1/vvEm9qnhdyWauw3a7vcN5p7qp+NaSe+w7nTs94YyZMiQa +dwfjAUu8DMcRKgu4+crhcbQEWI99yHXB24MkWOK1DwmXluASwStkl1CE8BBLOOzlmKii7sQWw+9J +kxb2jkxFn3EE9SElxQ67TYiGCsNBWQIrl4i4pLgI8kQhHBwS0CkNbQde3+dFQ5hjmClwKtZwA27F +a/AOLOJenNSVIWEtL2/8HFqxQEnaTs9oU6AHF1gGbKa05PAHy7nNlJY4KsNlVMw7HGROGe0PI+Gy +QGXU3PgVNTlB1NwjCWOE426qadK73CYHcGd3BbhnU0ZRMDzjLG6ZfjJBd0RnVuFMuc6W4uiLeGp/ +S4LuiEmcpiMFRE7VKbqS2kz3wrSdo3Gce8IoPrgfB/GQoHeIwEzoITZvmEkfL2euVAC5GlvDwqSq +MrPYU1zKNkWyN4gya3eefJ+X9zF6FkFziM/fb1/w65tWPzWrYf6Y1KqZly+7+YsfPvr1ZuGgc8+T +yYdrR+G3mtqv3/ztT19M/fM+/Ef1yjvnjl83cdKymG9houbRJat/tfjy32x03HHXxktmVFWtHDRm +79VXvbpuPfu6wTDQIg7SPSrYxywG0WS5kgmIpvdR+r96H0XT+yj9X7yPwL8FEgZkQ+wT05Zesq5b +M7Zc7Bc1TCrorleM9+KMv/dj3cb4vJxh8l+Y/o33TW5/1uTuKcNypk+U992X6+qAqQcF/2TLhyp7 +s74+48bN/oI9SBbZi/+gxDH+WlmfeamGvfGflyrkO1JBwb5nzzf/pGP3MOjP1Fuej8cYX2+IO5v4 +Jvklmff2Zvzm1fwYeTJ/gXy182fCx05JQcRFXxAVLfkDBGZ+DkHkmwKzuzE/TkwLimQtKKJmVk9O +GBYUadG8WPM2eEmrd4233ct5/6NWtq/RzhZSTPvPqmXemjKkp9WkHWtWelr5jMfCkJ7WrPS0tnio +JXVOehqewOkq6MS52lmf8YmHBNXLRBeM7b5VogURm2GqUGUMV7ky+vEIsFaMvb8uvvW5xalv3/ht +6ps1z03Zc9Ob+4SDZzvfSZ199C5s/4SbcbbryN7LnmPfAkAW0MIm07fjwECnGCwMNl9xM0UDD0BG +5MgD1iy+ycJogAmSu5bxRVbkmINJHFk6sKQ/zUo92YR7Gq35dvsvM8/90MzExZm1bGIC1oBpndNi +mV1euNj0+gOQ40jT3XTdj2nyViRYZAEToeKd4+o7x11VVciw+ujm3uIKAZejQVyJtUIZprQqt8u3 +W3YoR5VTik1TGhTCE5tMMhueLVixMT9ffT3blAR3Wy0WTRbyZVlAQHxEyCdEsEBVn2hWJFuWyHgJ +JVBERi0xLjUKJbXtrvsKMyAwIG9iago8PCAvTGVuZ3RoIDQgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRl +RGVjb2RlCj4+CnN0cmVhbQp4nNVaa5vjNhX+7l8h7jZLXEuybrA7sEsLpdBC6UApk6U4sTOT7kyy +TWZ3OuXy23mPZE+cSEm3tHxg9tE6kXXec9HR0TlSPs8qRv82l+ytpmKX2+zZeca578ND2IqZqi4r ++uPs/CZ7azGpJnjHzhfZRT4VQtfFROgKHzUvnp+/F0bwfsTjM7zkj4n67OHlA3mg1CAV+Kisxwud +A6oUZkDl+4Q8bwqdz4tDWFEq7YQRjJdc6krU7LzN8k8f/orzz7KJKJ0UrrJscmTUaLAsgYZ/NFZa +brnzY0XjkXZvRSlUrWqPdAHBVL4uJi7fFhPFHalmCtk/SWZRVkpoZ1lVKkK1HpUrQh3bkIwnzobO +XknQ8MrUwstUBYHMIaUuuZZWCnCAgNwKGkZw8hBumIppLiJr56siGo0Zk4HiUZgKNS08ofOe45jU +phQ064JpJfb95yRyQH0SHsGhID3n5Iylpj/jlbVEunPU2hIPbi1T0pTSWGOI20X+cWF43rEZzUR3 +uVwFcdcFr/JXG7YtXH6Ltt4UE1nl92y5Yh90d+yTQlPni5KdX3WbIqsBsV7N4W8du2u2rGGXy801 +ewHS1fpuxWb3rHvdEQjP79erjjWrlq3WoOlK9m63YVddg7dcZPktC0qFmbPOueAvkBAydSTY9Xp1 +2bXslj6v2XZ90xHk3dWazQvTy+7HtSt4k7h9UKO5L6cZiXzPrtevAdE1cxp4xTwRDYE6bFNAH//i +xXW33V7fl4HmZbMkrjSIvYTWMNlyTmNL9hG60nK3zYpcPchLlvUzt1hvLrtbdrW8KWGUHDZg7YaE +fw3oZksj8bajGbD5LXgBAeMf5F0ti4nNL68KrJzbwP6mmJi8aTu22HhTLbtVu/WG7lbdzbLbkqVh +8WlG6Nf3R+Tdkic0d545uQOEu1pug9QQETPc3Gy9QVY0faxt7jGWzZsbeBHEszl5xXKa3SyJeHXJ +luQPi1cea3UfDLhcXQbFn726HVyjCq5B/pMW7a7x6oJ+vodB2l/3sYSmkcwKj7Y5sLd4t1hj+CsY +AjptuyIj11h4y5DveIOu74JcxUTngYuXYyfZvtQXXD73MiIq+RVmscKYkXq0ki/yc0/6yR9gvCp/ +51e/L/D4wH85D8vsPLz5qP/qQx/ivMvfb8gHvZZFlt40KgpUNZo6snHw/CLab4hGHwa3/LmPFWGj +w2MXKwxXZSWV0+YwNI0DB7yJIgfzVu491i9NmE64EDTIaiFwmGoIHHD3bgNvFMpHD79ZGR9A8CCf +2XmBCA7QBxWT0+D9uOKdx8efhh6ILrS/BNghxtAybMK6vY3B9wMMmIQYY0OIwbPzvRRoiPs41rhR +qKHlVQ3RptdyCDeeJ+03+IcHXEbU1dHkoQm2pejRdovLq+VnL65vVuuXXs7PNz6ovXp998X9lw+6 +BE2ekqM9+yWsz/O3C+97v373N+/99nc0C+9/QO7tfRGOiY8fFhj2Rz/4o/M//dmjf/yXT/4aHG/P +wbkZO3iO3UvWShvrDnfWC3I00zuoRrNo7mwiqb9Bm6HN0VpyYLQu4cRT7MmU+vjNNF4EoFpAEpKG +00aKJtHq9HJQhvBqNIWmv/Pd731/CpIf/PBH03xa/PjRT8g2k/Ktn/6MPjx+cvbzX1xMp8//9unf +//HPf/07cpapqCFbDdlqiQbMGmlabaZCVWh4p/BO4Z0CT4V3Cu90RVkgGvo0aDSldug36DcYbzDe +mCnUQONoAk2i1WgKTaMZODDmaio5RnGM4hjFJYmNJ0ZyjOS6/w40URXcUqbIadLxFOSheEqEQgIS +dd+hivBdU1o5lRIMJBhIMJAQQwJcAlzq2B5SgqCuKH7iyfunoD0JT9l/r/un6vvBqDYFdcFsEmaT +MJtUEIyEh+mkAj+YT8J8SEPReFCMMmJHT9k/a++4+KD6DhDBthK2lfAjCftK2FfCByR8QBq8NyaO +riOt4GIczsnhwNxQRERzaHBgPgvOzOHEHE7MO7RF6KO8TsAlBT4IeZaFTqwE+EYFH6gw3xUqhwoJ +diUAJrAaBIAEgASABPxaUhoKEMxehYmoYNtKZgCAFSooVUkASABIAEgASADINkggASIXYfXVPPTV +AKoBVKOzVhn+A1ANoNr2AwBWA6wGWA2wuj07mGUaBOR6EdYsMmxk6GgyACggK7xQAFYAVpBQAVQB +VAFUzftxGfRUAFKLEBw0gDSANMTTANEA0Tq1jjHzAi6OsJMMCBpcNbhqcNVN4KbBWUMdDaYaTPUi +RCYDpkZETIgBVqIUqZBm5Jm09ISQRvWfoYEBoLEJcxkIYqC+gRAGQhgIYSCEWYSQaCGEheYWmluA +WmhuAWgBaG0yJkqT2tUtGFkwsmBk53H5Aq0Qa/qFe6iW7d3GdkNiYSGfq5LGqVKGcdDDQQ8HPVwd +0Bx0cdDFmbQeVVIWB7O4fqdwUMjRTkHIsJ5LeaSDOd2CdpUkG9TL0vjMCo3THo2wL5Ksm1QJDgoE +ZURIcr0qdjuii6pPIkOsFhWYVirNTCaZ8fQZAVmjgX80qWQPVBBRpvfIBjPQpGaAsfR42L+BMzWw +fQPbN7B7A69tYOZmETbuGWZ7hjmZYbZnFGPoidmegdeM1lZGAwA0A9AMQDNQzQA0A9AMQLNFYiIp +HZiDcC6CvnOgzoE+V/13oM+BPgfwHMBzAM8BPAfhHMBzAM+zPgZTXtECrIWYLYDa3ilbiNkCqAVQ +C6AWQG0fKVqAtQBrQdx2iJAt9O1itwoZG0dkRV4sZKmltbUN++D/tJweVUW0HfTZ3gsC/1pVNtWW +fd6+K6dRGIesPc3lSJU9znwP8l5s00Pii8XjS9fMxaW2olLb7SrtNPtU/X1QfvtCW+8KbZaosadZ +XGWnGYba21Ht/U1K72m2X3zHpffYxsnSm71J1W1C1X0DMuQbuS+600x8Ke7BR6U4leGeX/AfqtqG +UhzVmKiiojs7WnUbqrqP6BdqcXKch2L869fi2bgY9yXK7pTLCab1+Dg2v17e9BtWdDy6iI5G/RvK +kfP4lO+LxCnf/ple7fSpw7wv4o1zOPxtsOGh7H9M0fLsQCXDS4c/7RhC1gibBs/ijecpGext+u9Z ++rTZs5nzgTUwYis8pYPYUD8+iywxnOjWbHe2SyeOL9P6UToxm58dogyT8PkJKZvmCFmVbzzZhMuy +pjNlxSbhgPpp+sSU508Kjvoi/zRiB/Mii+V0HI46y/L+DHieSKLY/h9FtXgTJdoF0Q71OHW0/z1Y +W/3/gHXfAIzHR/jacM0Nk6VDTatUOP/3lwpYCLW2RvkIw8N1w6H7YZAYDSD5bMwDdRjK7hGOv19w +JWpHVETUjxzW8f7S4jV7kpZ+ER2qMfkTVrPEZUVXxWM9zyGMcOevBmrJlFElx+JX8uF0aJR2urO9 +I37hg58tDQis60NE7MbDjCQki/02mtAT1PEM2lJjaVrJaq9Q2AZCTn4QkvPZsZuON742i0LELBHe +h5s0t3eTFiMcvTlDLa6P3Jz192ZWazqwPnVvdurSLLJMc+xwVhtZchTtysRHh99+Ghqd+Oxd89g3 +TEAjlFMZafqCx371oWvE5fDGh/s01B1moScvfMJJ0Rj1ZZ/s+eudUQ6qTuegBxloljqL/tayzwj5 +MB2tKB3derqHzHPIOutx1hkhpdJQt0tDhyx0nG9GGMfugg4S0NO3PlkeZfKDGU/eA71Z6hnP+y4Z +JTGPXQyNfiOgSyqSEQoUclMKz8pF2eEiFVJR7sdJZ5ybXiC3TKWyEhkqNqy9m/XhWVfp7wf9bPCh +MT8kXXTipVnlA3wd8r9FMoS/eTZNMUrkj478mgGTk4rnxcPNmU4TItQfyfP1183zx8PoRxQSVhj/ +voBdhTv3EruwxiSWdGIcXn15CJpjXkTpUpPuoi174oibrmkj9QZ38U8oDtUZXfhF+/Kg/qNo+Ghj +jY9GB3M//Pyg54+Fs00j+Sz+xC9bRgqkJamOSXK4iW8RcU5AHBfhOOERK0zqkg45K5pgIWladhs3 +0lIkk7W0o3xSpvJSg1H1V+emtaCNfoelD7EqpBuK0kPKkQckN2RxI/RU3+zYuLEUUNLZGgFr4BAS +ZMrApa2scqfEm8Q6HFNVcVJjh+WvFN85zz7M/gOSUtWRCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9i +agogICAzMDk0CmVuZG9iagoyIDAgb2JqCjw8CiAgIC9FeHRHU3RhdGUgPDwKICAgICAgL2EwIDw8 +IC9DQSAxIC9jYSAxID4+CiAgID4+CiAgIC9Gb250IDw8CiAgICAgIC9mLTAtMCA1IDAgUgogICAg +ICAvZi0wLTEgNiAwIFIKICAgICAgL2YtMS0wIDcgMCBSCiAgICAgIC9mLTEtMSA4IDAgUgogICA+ +Pgo+PgplbmRvYmoKOSAwIG9iago8PCAvVHlwZSAvUGFnZQogICAvUGFyZW50IDEgMCBSCiAgIC9N +ZWRpYUJveCBbIDAgMCA2MTIgNzkyIF0KICAgL0NvbnRlbnRzIDMgMCBSCiAgIC9Hcm91cCA8PAog +ICAgICAvVHlwZSAvR3JvdXAKICAgICAgL1MgL1RyYW5zcGFyZW5jeQogICAgICAvSSB0cnVlCiAg +ICAgIC9DUyAvRGV2aWNlUkdCCiAgID4+CiAgIC9SZXNvdXJjZXMgMiAwIFIKPj4KZW5kb2JqCjEw +IDAgb2JqCjw8IC9MZW5ndGggMTEgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCiAgIC9TdWJ0 +eXBlIC9UeXBlMUMKPj4Kc3RyZWFtCnicnXoHQBTH9/9QllvuuLOuXQ5UxK7Yuyg2bBgRRYqgFKkK +SBUQRKwvIlKVIoiIIhYEERVBwF4Se4mJ3fSYqPHrLM6Z/b89jTHffH/tL7LsvJl5OzOvfd7MGBBj +Y2JgYKBxnG8/ee7E8QsGTVgW5E0MjAkhBvg7XGxGxI4GYidDsbOR2NpYZ96JLepk4vV2LNcZW4Q0 +wyfJaY5Po/Yt5HdLfHT/piUxNjAw7zNkhN2ykJgw/yV+4RY9vHpaDBxgM6SPRaBPUKT/0j59+nz8 +poX80Y+lSRFhy5DTzazBh4ZW3PEYuMhr2fJsmyHDelT27jlm+Hwfi8VL/JdGhIXHzIpaENhvrl9Q +pPfU4LQQR98JroPcZ0+a7DAzIDR6xXi7iVPsp02f8dkcp3nOLgNGHDtRW2fZpWu3RKvu1r369O0/ +ctToseNs3RZ6xsbFr9xRtHNXye49pXvL9h04WH646kj10eM1J+sbGk+dPnP23PkLFy9d/uLLK1ev +Xb9x89btu1/d+/qb+w8ePnr85Omzb7/7/ocff/r5l+e//vbi5avfX//rDRWb3jLduz+kz/M25hSk +bF+/dUtSekby2nVrNkAuIcuIQPJIW1JM9hBb0opMIBPJbDKXbCAtSRvSjpiSUmJGlMSctCfDyWSU +gh2ZSqYRBzKPOBIn4kL6E2cykswgzcgQYk9mkvlkEulJZhGefEYGkymkKxlNWpOxxIZ0J9ZkAOlH +ppM5ZAFxJV1IN2JFepBepDfpQ/qSgWQQGUqGkRFkFGlBOpAssp3kkp3EkBgRY8IZbCQmREH8iIqo +iYY0Jx1JJ9KZaIkFsSRjyDgSR9yIO1lIPIgnWUQWE29SSHzJarKXAAkg2WQXSSQ7SDzZSpaTMLKG +bCbRJJYcIGVkHykn+8lBspEcIlWkglSSw+QYOUKqyVGSQ46TOlJDTpCTpJZsIqdJA2kkp8gZkk8y +ySVynlwgX5CL5DJJJV+S6+QKuUqukdvkBrlJbpHd5A75mtwlX5FvyD1SQNLJY/KAPCSPyBNSRJ6S +FJJB6sl9cpacI8/ISpJAxpMI4k+iSCQJIaEkhqwggSSI3DLEpe+Ci+SMxJP40e/JS4NxBrMM5hv4 +GCw1iDXYaiAadjCcbnjW8J6RyqidkaVRb6PBRsOMNhm3MLYw7mk80HiU8UTj08b3jJ8Z/84Zcmac +wGm5HtwcbhEXwH3H/WbSyqSjSbBJrcktxQJFmGKv4qjiNW/Me/Ah/JemVqZ9TceYTjada+pu6msa +YrrKdKNpmWmV6UnTX5STlbOUq5VXlfeUL1W8Kk21V3Vcdd/M3uyI2T11sDpSXa1uUF9U31Q/Vf+u +0Wi6az5v9lmzRc1imiU1u93safPhzf2ar2qe3fznFpNbuLd0aenX8lgru1aOrYJb1bY621rRunlr +j9ZrW6e23tb6futfhDnCEmGlkC7sFc4Ij9vYt2Ftm7e1ajui7Zy2i9qGtE1sm9l2Z9uGtnfaPm9n +3K5ju2HtN7Tf1r60/fH2V9t/26FXh5cduY4dOvbvOKmjZ8cVnSw6ne90p9P3nd50VnXu0LlX55Gd +Z3c+0Lmu8/XO33fWmbcw720+Qbtam6Yt0pZrT2tvakULM4swi58s3lqaWnazHGQ5wXK65VxLd0sf +y+WWqyxTLIssT1he7WLQRdVFgFrRptagtpbW1xrVthG7iZm6bia1Oi9BtKH1OhuFpoD6CaJntc7T +RPOQXmwyFViRziUjUowAOK8bBdSJPgLd59PCIIYmAuTPzgXxMacJrdopdFfSkbRJYNeq6TUF82X2 +SNGUUju6Wygp2plaBnxdnbdV30DvOW4u52hnLdAAqqCGVHWejy3h2JCZzJZpgS3lmc0VW9qJtvz6 +W2r6q8MVZmQ+B3xjgwN5zURq1+Qm+Ck1offpG4Hms/y9e1k+zefYCWOKf4OwxPI5DVthLK6v1q03 +0VAP8YogkVHGthLpyeq97PALo3uxdsyGmfJbFolqyKOtI1NDvFkEcI8vUBs6APD/688uW8F0cIpy +9OWzQ7nKzPwtx4HXbKCrRXthqrKaJQn2Ss08uk20EKYpNWU0V+wqLFCmHRJclBpHGout3JUa0UVe +zbftqv9oZ6IZRQsOCSD+xhbEQsyjBNCd6CMv4glcRE7jVpsg0Fm06xXaWwtXg6pm7fLI/2zzaOCL +cE3dYOs3yVtZPCxkFzOjuSsgEesbP0nSu31dJWLZp40kvXK8L5G+msuS9Dz4lERULwdL0u20MbCE +H+xhy8aZa4bkiceEzBX0AHATmb0dGzSATeW/CqFRTYax29kTwCWbSHFBBaCuDbTN3Vt8XCHXf3I3 +Zg3MG/8/Z9a/TOZzCrmxt5xoG2zEa7rRW+IKASrDdy7OlaSXk8ZIhDuZLEk/HccRcLZxUZ9X5hwr +gzPQ4LdvCsyDoLglEXx6BFe0ZVtKIeyH4hUQBO4rli4K4VMiucKNWRsLEiTSrmcWMksskQh/O0CS +fklwgRB+wVJvJpj7KmhXqqz+UasJpd7iGEFnVi2ayYrmKysajt9eHj9zdWJtxo7ncyK576++otZA +vfF/T2rd6yofF8ndHd/A2mAjHlWWdpF11sS4uyzARvpE6I0i8xWPiD8JdkqND00SuwkzkXOk5hp1 +brIXmD1K7noCvDOjZntWneHoFdpeoN2mnBltPhjcnf28eTfF7vJ9FSeAP1kRyFqwzk5OjlpfiEiJ +2Mozs6XckchLa24DT6depAO+02q20HixhzBLSe2sBAcle6cbK0y87vcvan3/7i1zuIZL2XuJRHr1 +6SlJWTe3SaTNjTs8E3ZzgWmxGbANslMzd+/kQ7M5r3BPZ/CDhUXhjfF82WqqDuEqk7bHQjDvEebV +e5xbaZ05HM7NqtnHB6ZzkauGB8IMnp4QNwuzcXK2dNl+YY5ytnhfcFRqXtMgcYrgrETbdxfHCx5K +zVSkTBTclBorNKVqYaFSE3aEaYRVyav6coVr8zbsg1Ooj92bOklkUsBgiYw2eSaRjMzLPB0qWl3M +h3w2EiDmTJw129qdZlfnIiEYCSfj6WidFRftIklvL9dJ0ottO1DugyZI0lNfD3zLDZekZ/2PS9J3 +3p9Lknh1cjjEgW9WXAqvocF0zBFhpJKeo9bCKOU9FNdopYbmiZPF/oKnUvNGXI+kicpi5iJMUmqu +VjeNEJyUtDe7IMzDCUehnrcXLJSaL+hjbDceu6poGIr3M6wcKTeer9SAOFZsL3wcfmNcT7a9O83X +Dz8UCfXxdIyu5xG0m4Zq6ipMV2L7JGEG8kppbGopDEZFUje1RC8wFPzdlzjxmV5cSeWusirgj+xN +7Ic+KGJOBBp/5HsPSjdxL1zrh5jrRm4Q2ERUsi8SgP06VXYPsdjCCVvkciVVxaX/Q//hEOrp58xr +2NnHTZOFRKXmnphHowRXpS6ezhX0bHW3p4Zjx83YcXwuNPGcZiflGwRbnHt7erVplDCCaWzZFO1G +mBoBsVcA2C+T5HFkYYfR+KV/UQJwiOGjgoPLn+/M3J+1NTNlJxTAsZictXzOlm2PuJjUqJQA9Ja0 +/xlcC4i6FQG6mrFhEEVLgKOzrl+mGi08H3e0e8bFzDsn4SlPDZwqWT/zcN2Pgp3YUGtyrzFsYPf5 +wUO07ouvmPzaGNizl4tHd62GDaA5TeMF1ufdTdq/yGTUVmj6/O5WTuMlWtAFAlzxr3DYEZG+fPPS +z2uLrp+Hx3DPuXYYzATXqHm+fE4YdyAnO7UKjkHZciROi3S0W8SnBnJHN0qkxTY0L9KsuUTMfZeg +13nxAh1r4A1J+tlMIxGzjL6S9Oj3V7CQl6Q9fzQ6SdIJg7FsqrmfgrpR63O0pzy2Kzg0CL+7Et59 +PmolhNP+AE03Oc1Nukb8Rag1eVa52KqL//Qu2jWBaSZ0/MMnVENNZ59ivbWsJMpEv0Tj5SUqBCgY +gRHhFd0g/iDYKGmh8UClZjdde0gQG+lMRcHAqhSgrXWT4cPC6nvtwl7cEHpAoB1gh+MhANrXByIc +WQfUUJopthRfC2OUfu+IMBY1c3RTC5QLm6wLZXZiqPUc6q2rO4fRWGzgqEInB6n6q7lQxPxhj1MK +gO7Q9HD8RIr8iXfDiwTxQKrit9NsBUD49VjQ1Y+Sa+fKtRo2moaLv6EjYVPYDGEuKiAtQSNg1sN7 +sy6s15NZT7QXoXrHgUP8fsXS0LBEN+AXOh6g5rRV+ckr2iooWFucxFOzEm55hk3+CPSUzu/UOJxd +hXcjgZmFZLqDrlCvjuv1UXMzjaJJAgbzhdNikGiFRP9cakCnctvTBUQWYy7K9toW7XVH/GBmzS1C +h5G/DUmOSDq1gpoyl4ds6MoE7DsT+zrmM4Eu4zTO9MeDwiK05Vm0samd0Ee5W9dR6IvlmWL3BKEf +vqQ1tumvpHPE28IALC0ToUoYpNRYUkCRDUFTUtJkfBuKddP1b8OU9Ix4Uhiu1NzRi3WEUpbKF5ME +tqkhxoMutc6Ffbc55oBe6KOYaDe95ozDt4XiTrpBmIBvg8XL4s/CzXwoZAsBIm9Hs166hcxGXHhm +O5JQRJGXY2mYrqlBlntfeomKAlvMrkyKgkg6FaBwfiGdSo9wO3F5RtCsK3KnIdjpYBTTsnhuAF0l +HMpFWjjS6uKpPTtNe7KC8ETsvQh7L9jKfOkrTvNQXFgtTFZqvqV1OJnbu7owB3RJLZLdwpzD/ePj +PMAW/F4DdQDaYktNcV3R/p3bqoEP1b0Weik1mfQBpUJIePTaQOCd3fe//LGk4vTxageGiJAFMAUz +ZKpZfG4IR4dcoLYUYeJSntrMuM06sZa2iNp6nJvxL/PTUJZbWoaergONEc2FQFyaN02W9JoQrNSt +/xrL9IHYRViqJ2ua8vBtjA4QOLJ31PJD6/dk9gcr+gTE2MogJlEGMfUyiEn8/wUxiTKIqZdBTKIM +Ymz/CWLoT2wMjlbD3O41aYS9utFfpu8Rx8Zt4Vx02+ToxAbTeVhhqWSxsfjUuGRinBG5Oh1nUkPb +fHije+hQoYuS2WfgU5NIuetC9LceGzaz5tyR+PqkS0C7wpWb8B08tz9qcWL0weBiaIQLR/achEoo +Scpfw+cOP7oJqDEXkeqUPRmYEqbYwwjoc8r5jctjr8IwmAOzPPxmgw8s3eKVz78UJeFdXmlTnkJD +O4vdaYiwn91VOMdALCeqqPKvEv2WvRK+h2sF+3P4NGrskhLbl1uWFL7eHXjWaforOp4Orv+NttKe +gEOrS+L5zCFVkEqVnP+26dvGYBN7ZoDuwo7NpwaWdIRWI9o1DaGXhK7KWHZH6KbU9RLffiyIbelb +wQolGkCX0G8FOoNFcm5+oatmI3xfcBYuQkPWibpjfOQOwW7OlBkwBGzO21IDeAr1R+rP8jdsUMGg +0PlgnzfcsS+rf8o/y5+UjWIOGsD9KG6dZ3C/xQ48ujVX7mx1+SH4gm/0Pze01/xpo81h1mHXfT78 +nVph2T6fo3CKP5q/59DBwqWO5rAo3iPAF5Xzy0q9BDWe1EfcKlg8iN0Y6pYeR1t7Mm78+hUHffJS +vqbkIGuTk3S8KCVvDDXkaIc1W7OS6+PSWGuv47Rlcs78zIQETgYtZrStYI1qMesodRN6yHpD85om +CD3fIxpxBOoMO2xs8UkxiR3R9xhJV4gTBObCXCj+7tvPxqDtj6bDke3Ib7EmwJ+OYSPYaDa8LIBi +Gw4TGP8mc4Qu9nOXhfDbQ7iKkr3bGqABqoJgLvhLUlNNgyT9GEDwMXGcJH2bRfhuTAVh30zZvi25 +5vELTiJa2YB69cJHZ9moet6yhbNww+s6aw18zHsNp2niALFMYHPZWNaHebBgOtyGDtPehUc7Go7w +ccWcg8+MtdNQHdox7U06nzpgtqiivWmPn2yZNTNymOSujYbkrC0H+Zyfy6gFh6P5AT9lUlwvSdeu +mUnE2NUWaZMPI4v+3ZkFMxlXFVuhPQoV+w8/5VfNwQ7iQDTPazRRIu2/wLff+MQaDDom1PhHOoB2 +7vEGgVAUmy/kQd7qrER+XSgXYOE1eTnw4cm51dczv72Dcd9tB0phURxNfOdXlcNp1qC/bxDoYNZ8 +dxmbTWey2Wzm7kBOM4RObxosTJHzlEaaLZxNKvBNHsonMUMuyaJy3tNkPqWQ806Pq0h/xmf8zG2l +3JyDi3Krck8fXEk5PulnbvWznIqKdH59ITco2WtehgWfgV3Thkb5zk5CRTvwVVOW8Edczds4hZwI +pdMyYXZGVHnaIz4DlSrjjVf9oDR+fSRXsTrHe/VAPqk3t5Jxp5dUxS6KnbNkK+P4jN5c+sA4b+/V +sht7mlZZn/SGT8KuyY8Kys9mYLIkjqV7BL+lccneMAs8y6eCCziFuzry2yO4A3ty0irgPBzx/RJO +QENRzSle1k4v5iEEiaOnrl6qG5uzhjshbhMWKzUBtELMFCx0z0vWvxGfc+7MRM6fAzGQvBLs3kNP +iJ2I0PM6Qs8tCIGjxJaYojdzT40exgYirkl7hfiT69Zoguhzd/r+rPTszTL6PPgP9PlS3CTWCZdl +a56A1twYxTi2kqlpQqVMQvQSeT6WTmM/nAFu+GkhyEPvuX4CcPSAD2/jJ8jQKhuRzfjt8DV3f/+I +U/i710Reb9pT0DWrEZspXJsMBPa8hj5XaBKoD3qggJ2ca6StE0wAzIZGbHoiSRdOzpNIQJi5JH1h +u/fx4r3LwBlmuAbMgVBwSPMq4hMeeqHLVnFHVjQmostuB/fuw334WpLqxnaXyODjnSXp84Z7Ehk2 +6akklc/ZiunWMDNJerzfD5Hr+ddwCc4eyt+fzx9i8UJlKHds+zf1cA++laRz0/qjEdr+Kklbqq5J +ZOjJltj/O1OJWNtdRWzbnkqkw4kBUA2ny/cfhxI4tSp/FZ85pnITvOEiMhZsnQxdYbwtzmSsRPo8 +ccOZWLeViE/nYkm6fPfHR567V4A9uIcGu6zkUcEjxXuCl5INa+OFtp0gGoulQhGUrM1Zw+dM3A9b +KOFi0pakLwa+C1OPZwOZ9ctJT7Q1cCb7yDF+ZR4XGbcwwQf4Ufa3qZqafVF9TsueMHvBWxncxkfO +Ph0xpZkLgfHeoTKW37l16+YS4Ct3rZinjVRErg/a6Lpq3qoVrhACfkWLTmJorszbUcmnzKVt8mDH +kpTks/A79/WC3Q7m29lowUexyjcqPhYiYVUm7IRijPXP0FW1qCQS6dpRdiIz8U3983KJGCakYBhv +vUkiBolh2zbxaOEX6UrBV8nuUhv8o0kX+9BAATNH6qvQ9fMQREOortb1kwFYAjYdJ9C3NYq64si5 +E3yiJ2kRavZgi78FyA4uY22oDVebcboMvuKveR0YZM58ok0uLAf2vX57bbaMcdlXmNp8WAMfqkLf +sgR9iCft0tRV2FcaNlcLy9YmxK9IXLUqORwCIFAO8Lu3ZG/Ly8rMSisCPpnNEb6srblxe3rpoqku +bna2F4OqtCisYPRG/krNI+qFaT6bsBpiLsWDbtwqNikrjnK7g6g/ty2ucHUOPIC9R8rreBhAbQs3 +5rmkJVwHeuvPDmwrF+LhHyDD+MhdD7SaclqFkPWy7pej4g8mt3ZAAUsCjnWdOoyZMtXjmbS9lmUc +RReVga68UY4BQ2hz5ikEKDX9qGuTlXBwx8LhWnBaHxIXHpeQvC4SlsDyYziEhtSSnKKc7DTEXHw8 +2y9Qg7MXXlMD20N2zGDOrC7M4Kb3DS1K5i1dIQQp5zMbfGIUC2bLhEfwRePuEj66hPMOCYp3AidY +tBfXaD/KeCIKuk0WCrpNDQq/QwDhX1EVFI+8Eh2f5jqkO8aHRxi7pFu38PEE45l0u1c9zAa7yokU +o1j+e4eGSUL1W1P07AMGfZIpzBJVSPRXMkuc3idk+qBpl7BMeX/c39ouaborhCjZDDb2b211TQf/ +NKjXrPe/sXkuhCp//UwIU7JWrP9fdSLXlIPJnS4r0uRW5AP4kHiLnpBFWy9OXTYQkzoQT9DpIOeR +HNvAXAR0ONmOR+SU0RMSHFk7lNgb1lmgxUdYgQldhC7NGViernZUBKYB8xEtORShbs+kj3FqW0K5 +q2mXKqEODq8ssM3jJdLpayf0OZ1XIwZwSeiIVvMLeqkJLxiG2PXTJWJxOEMimuALfDo1c0pZYc2F +JPonu2CE7jDkJXWiU0/9Qi0QFjbGlSTwmQOOo9tozfnnOOQPArYMmDHrBmw+SNJAfoJEfC9skqSM +UR0kUtMpU5KogyHm51fbP8D0PLgoM4YrT8qNgeUQsSJ00WoeE+oyvZwwj+j0Ma9+RaX3cprFUj8h +XtdLqZw1/4T24i9pnGOuHyssaW892zXIdYhcTNAzdER++uIcPSs6CnnpyyF/saFzkM8QGQgdoFeE +5Uoa2CYcS8Hi6vf68Pxd9j+3uTLpI/335iPDv3LFTHGw/rNWLPlvVMX7r6uZ8d/Iqe8/YMUW/41s +9cnYujGHv9W9bRotSNL54RjPKmML0DYuv+qF4rx/UpJOq3eisyzZidXnPgPmA6wZawmYRErShlVn +JNJj6TtJOtQjAMNh99OSdGXYNJTThQBEjlXm60ojMTC1miiRaf2iUWdsbGzkvZbO2HY84stXDwIl +MgSDNfUB2grNjiJXsnyHTpLuJqKizfScjl/lHSQyYF06at/wKDckRnbnWI6zgNxMN2Lz3qsOIkid +9QJRae5xSfqyLoCX/njilST9EW0zD2fyw6qW2P+0C+Zsr1dIxLb5POw045dCTF93HN1Xt3f/4aIb +gMw3TzOSpNTufshYcFiLE6mIkoiZb6QkNQzsj5M9/PoVZsExHgHOQf7ey+2QcuVUP3zeqL6N2n/T +WJIetn4ukf7H5/OY09Ov9XJ8rovVZ/YLxf56CdbofvtQNnwvu526ZR8IFp+I57juuEx9Q3/T93rE +Uv7a8bxPv8N0oy987nMOuHuVS3pZ+7v00cYG5pnQgU9vUFNqPKWOddL2/1q4JLvoFICom/JOkh7u +pCPcmbyd0/xOzzbZCiOY8Sh5N3Aj6FoA8Pq9l6noQbw5uLypInN/VnF+bhlcgQO+mPO7rAuPjXAJ +dfFBJzkT57kY/eY9f3SyE/LRed7IqucblmTHgi8sCY/yx0CdmBqdwcffcl23pRt3IHFnci28hOOX +4Bpcl6Sbf8ipw0vsV2eAHHppkNflM6dQ43Z1kKTvRo5Br+3yHWKW4q2FO3N3H87eDyf5h66Xhpmz +jqxWOOSgsEMQyWk20M/0uHv+iA/HNhFKpO3SRz8rXK5PiNHiVb2tTPqEKBo0Gb23lOGfNs36C+qg +oVj9jUtTuz898yDW868azKwOIm4IXue5YXL8uLiwGeANC8pm34Y7UN9Q0rBvUq3P73AV6op2F/PJ ++dzSlcGrl8IwcGhYQ5vzNH5P2gnuN7ea0eYzEXAtWbCy1mXXNJgJblHO+q3Ng9sLMqqAb6hcOlQ7 +yXieIs7eK3Apqt6zEwSfzyeSXDgGe7ZVHs0o25KxqTSFpwuoo7CvIMBW6xneK57b9G1h4yUEzfOo +03/06uYDhiEEbGaMFtp7XDrCJR7d+MDB9yXSbIS9RCxdXRA4HdTqcdxKn2RP4EcuuCLjuGOI44pg +5zpEgYW21WnJvyMIXIwgkLUAZjoR2BAcXmDv9hJxTUlFv/3YSyL+r9A9/PGmWHbmCX5I1D36hzP3 +oW56sU5nKvmIxoeW6yXKTFjC+/Lq98I0sHhf3PGJwNqxafpjndHUTM9j/WAhUl/01POYh3nL+/Kg +9zzG2n4of/YJEyfWS6Y20hhxkUBn06ERuzDVG8pRB+YuHFD4F/tvnYpRrfXcAVaMPzeTdqLqB/dp +Ly1Q50nUkPWlxnxMBEcNGFfYF1gk3y/Y3tHR5/SLH4uOnzMvh5NJ5ct4zWtx2nv1W8NGyocyZTTi +w6zNPh42ltELf0498RNi3of5M/IXEf27npsNW/hJy+N/W5rpH2tCqzBP9RDNqhVsHRuETeguYy+Z +Sx/RHyPBvdBhaJGzeqP/fDz3Dvr/M2sk6b5TF9SNXz6TpG+eXQNmBUMHy090jfY9JNKWaCXp0qpn +qCqtMV7f3/EORXzt5xmSVJ9G5TjQ5946iYw5kYta1tVdwty96hGafoyTJL3ZtBbfZnUDagWPn8lP +iThb5+G3A2PR67eaL0nXn2OWZVHhgtradTfmPTYtKzlm54LOuoP9DGzeZnwYOvh3qGrNj2Aq9t04 +jAKSW7qzJGmb3cFZvJ3qLZF2v42UJN1ThCydGryxk4URD9S0+MJ92f87mvaVpIrATchS6fAFfnuN +Nc52t0KSvnLncJLXKyajUkfOGIHvDyKm4/PpGQxHbS2mogkWYojr2T+F1zykyXohMksb+XztIb37 +Xn6OGIz15TK96MrGvC9d+Es8jayvTAujR9/3uPZugxD16SGPOO/9Ic+7u58e8rxVctin9C8+E9/N +lfuJrUUi/iCc0p0YaWLt+IxiLkHbZB+UA96+Agx4mVUC+4xpmIotY1G03wRqrBWXfsSRvnocORU+ +nEdwbC27J1Dhb6cPgpwEFaK37Cqc22+HjExjp49w8LlFVdS04MJ9LSoasxo1GL3rkF8dX1GrO8+o +FR3Sv8FSS2vbsOZDBiF8bnPWhlprG+Cs7BhXo2NMXLJuKfBD3Rqolprl1Fyt2B5so10Y3x3AnyZB +1hnupf3REeaanTSrqVaIUbLUd+fl4y1HGoaTjVF2Z+Hy0fwb+uiTaPqQLfgrfD6kFfqeF95dlze5 +nTGxlTumvuPkQ4Eh1FUvH3z5Xm9OGnb2R3GOkCC/PH4tn7o5Yp4mE6hbmwS5dEl/GMfc2ujrLv3+ +sRBKvbHq3eTqpsnyOXaIfI6NZjdRf5VCDPi3GjGr6TLtIO/zfdXmw0PT1Pi29tOdUTN657/fJmXz +qEafvspvY/XpkiYhWxyTTd2z92wzYStAob+d1kpc31pcIWw3U35upiEb5Ds+rYiW9CaziR8JJ7Fk +B9lDDpOrBn0MnAzcDfYYnDP4ypAYtjL0MSw1PG34o5GxUXejKUZeRiFGa4zKja4atzQuN37AtePm +cXFcCneUO22iMplistbkpMlNRSuFlWKYwk6xTlGgOKS4rviRV/KWvA8fy6fzl00501Wm1aavlb2V +TsolyhhlrrJWeUV5W9VCZa6aovJQhao2q3JV+1S3VT+o3pp1MRtlFmWWojZVK9Ud1Z3UndXmaq3a +Qj1LHaeOV69UJ6gT1avUSerV6mT1GvVa9Tr1evUG9UY1qD9Xb1KnqDerU9Vb1GnqdHWGOlOdpc5W +b1VvU+eoc9V56nz1dnWBulC9Q12k3qku9g4O9vYuDaqo2Lu3oiKo1FurKmRTT6Zvp9PitnAq1lpX +dziJthbrOFVd6bw+WpiZFBoTsiIueXUAOIN/PfwEF7JL8kvyctLT9gGvosY3rlMz2sLmyCRmPHES +M2Mtnnlc06qY9E6xL4pKTQpOhe5YZ2ai8g1d5uu9L6xCm5IM8s+SIA+XYD4rivuqsu4LeMKfCt67 +yFxFfS8xXxMVk7cm42hikx+non2+R/mH0zjG3+jL+vTFj4SzOKoc/6NWVbnjm4vwDX938Xk0elOH +0aPNgdnS/AXA1PykJV6zZy2svGcOd4v2X6jgo/ZwM4IGToQRfLdrC2gz2vLeVz+bAx3Ddu1B58yf +3n34aF3pkonmsCBh7oJgXsWCdS/tI9EnzUKfNG3HWTqR+sIFNtkhCmlr5bO3PBYj/gtXbSZ8R4s4 +1c9wvWR/zv89S+TRWWGWyOazQGrMCPXQquhWExpBW1IFDaELxvyLlWnZ5g8nrmPDONVlEzr17AQ2 +h4WPnMT6aieZqDDJ/fOUdTZ8cnwtBn/0fhGAa9ntb86uG5JWrl26fokc9ge/+p+GGcKMPgzTCIfp +qX0KV/NO78do41WzA/LZBoCYhxGc6tNDXhmaT9kOtIimAmbkU/6kceO3g9ig327BYtSVCGDrGTah +668gjVPdlGu2/g3lY6lgwvZ/q7rOwYyVzGDCdD57BUe5L25TI6AKXnVKbrQGG92Q+9vJ/Tdh/+nY +/+p2KGYTAcJqoth0Fu3hSkPYNdoLBepexZLo8Q9fyP7z4/pl9IVC2to9NXwgi8Y1EwM2KeppC4Zp +RaR8ntxo+/E8WcWuiIHzWAv6s24rp7pWhJwckNPFcNDZhrGx97D3b3dxHfWnj3/lXdYwXB65w9/z +NxX1kTNIvfIP+oq1Yz5ymqpX//6jaDut6obJtV3eo5mRj0cXLSRSN3aVWuBEFh1lUyhwp/Ke1QCi +tt8cMXcyZ+WfntlvwcHa53OqyN/c1qeyzlxN7JlV14EOgjvfwAugBpJUPTsREckMTGfWH8S3oZWY +zuxuwsSmh3xG8dAEk52OxfVwHq4eLzsDR6EsqSCZz+93HJWwORe2ZfbWscDawrgJMAAsJTLOExmd ++xU7RRZg9wstkJGHFabLN14g8zZf49vTZokwA6a4+tjDIvBPW1iA87eGgj5Vm9ZS8yiYv1angfmp +UbCWmVdtKPwJ60BXOz6MQ9UoUMiawWzmLexr7XSKdkW7fnNeliLg2l9H5foX4qovRtFJdNilX+gQ +rH51Wa6eo6/mVDOAmetmMnNx5qeH0ov0h9JAu4pzP9fSrjqnBqDzRfrxKPt6NGunm4Hm+MmGvZql +MhXdUvHphr2OXJZtL51xbJPC7s8NehzvbqA3xEvxJeyZfmLxzPL4hvwfaSywa7qL2cvEubCdmrun +cqopvhBJLZlyD7qZWeD8IYOq3L4jHTOoy0U+tlo/xT/TtBo4W7z3NA+j6didG/NcNiecBzoXAfys +in7mKgdFnKN/UCAshegCOAR7JGK4q0QiWvGeRKxcMDdSK7ojuNxJJMJno9i6uOObkZ8tJmKq/5Qd +8awFZkdsCOv1w9Q32hq4/uce+T9yK5X+VoX5f3urojS5eBV/WBGR7pMehKy7yJcxsOOft9m6fdzZ +5VTV2wP7aeck9ZXB1EZI14OpwUzVf6iMxC73o9bmH5CYnKIm+uqR2Hw9EiuouaRV1We7zRiduNTF +NebIpTtZB2q1qidTa7p3HzdtyOAvXV+8uHPxKUYlLie1elUmM/XgPo5B9+Ifl532lB4H/sju/3zZ +aaH+slOAu68rrwKxC5u48sOdqb8uh6jkiPxphDZXvb8AZYReLoYZfvRyd6ghQnxeVChUH8aiZxKD +TFxyockaR9mFjaL9WDDtjLSFR9hQ6vovqv6yEIowt4Fw7m/95I9P/9Dvv77KtV9/let/MbtzFRNk +3IxJhYOnHjdjLoKysxqqR82/y6j5sR41WyFqVtEedheGm48CjwX+XvrLiCUH64GvPBGEjq6z14Ip +Wl8ISQlL470V+Wt2r9n5/ipi38fI8OMK6oeaId/2ygO6hdt1eFdGOZRC/pqdyXzOlqwfuRWpKzYv +hbng6xY4HwO7av7ciVqwq/F77Iu52250TN/GoDtq7YyO6dXaevAD96AoR/wTnfHnbg8j3IFVJ1bX +YroBDY1wD75y3jWoQpIuBqFptK3Djl+nI4tW4+vhABwsLjgB+6EgOWc1v93ucMqGV1xwhmemOzr0 +3t4wPBQWR0DslxgP93H994ScNT8Jp/LL9/ErC7nolTFrIoH3Xll0SksLqOEhZkg1CtXHswvgLCtc +vzH/Ck4cK6/5bw44FvkFo0UtXl74QKtyAEbYvn5067E/LzLWx9OB8k1AytNGqmaN+NZPvvjI5bOR +MYrGuN5sB2rCZKDR9I+kErYR5uqdE1+9Lu8eBkoWzd6lLaOvIZfynuicKkpLKyqC93p7BwV5e+8N +rtCqgovDSkuLi0tLw4qDg8PCgtGCptOOaHKOdDbVMi3G3elM/jsbf8yRPt1cxVxpH9aXeuJPX/xx +pZ6sB+2BEc+TIZ25Io8OdIBvKZv5/oy6Yh+6VN+jMlrcvS8aE0HbyHGsfy9+SyAthjxqqL8g3Q64 +W+cffQnP4IbXcTuYBQGxrn781lDu4La81D0oDyvWd084taRKKORU/76HVN6wfHtCtOPof9/KQfs1 +YcPjMHSg9n05MgyiZROSA1AHNqA8mM6ksxn+egcgxYWdSI+mPwGcZFUY3OlsYC8K4UYhiF05lW7D +u7cVUeKGprc4F8NpRcWsHaKKdqxFIYK8pZEJa6LRlQft9YRl4J3g7CyfXO8pzN6yHSpgb9AR2A0V +2XV1vGpf+rIAn6TooODE3WUH0or2aVUNPgdmz/ZavMCp3O/s2cMVJ81VL1mRydj3MIr7z9CSrbeS +o9TGy4q/g0taQg3KTf5jvKiX40W9HC9s5Xhh+1/Fi//g7Kbncqp/XMgl8oVc5DJaQeQLufX/lwu5 +9fKF3Hr5Qq6tfCHXVr6QmyhfyK2XL+QmyhdybT9eyFWpmOHl8DCKS01xySOLOZX+X/nOXfsrgou9 +tOvTIAXScFAdjBIL8KlLPFHKJxZwY7x8Z4IDP78kuMpc5bs81N+7NKxSTmnWY0qDfkSXGIVPo8Q/ +U5vyC3COPxlSiqnN//bf/wPDaOSsCmVuZHN0cmVhbQplbmRvYmoKMTEgMCBvYmoKICAgMTA4MjkK +ZW5kb2JqCjEyIDAgb2JqCjw8IC9MZW5ndGggMTMgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2Rl +Cj4+CnN0cmVhbQp4nGWWy24jNxBF9/qKXk4WA0tssikBgoFgsvEiD8TJB/BRnBEQy4KsWfjv07eO +MBlMFnaXqKrLU5cUmw+fnn55Op9u08Mf19f2bLdpnM79am+vX6/NpmqfT+fNLkz91G73T/6/vZTL +5mEtfn5/u9nL03m8bo7H6eHP9cu32/V9+vBzf63202aapoffr92up/Pn6cPfn54Zev56ufxjL3a+ +TdvN4+PUbaxyv5bLb+XFpgcv/vjU1+9Pt/ePa9l/GX+9X2wK/nkHUnvt9nYpza7l/Nk2x+32cTqO +8bixc//hu912S00d7Uu5bo6HZc0N2938uDnOcY232/WxOdadx+tjHV8YXzQeGA9r3LPH62NzTMPj +9bHmkzMrZ0Fnkc4yE2uuhfxF+ZnxrPFDhkcMM7WzMyTipJh5Z80b9h6vj5UNnSqdUBmvig/EB9V2 +artiamfVJjSTNBfmWjRX2FK71Th9Ld4X+Yvno79IfzFiU1/oZOlkarNqMz5n9ZipzaqN1EavRT+7 +t+Qk16evxfsiPyg/oZmkudDXor6WRtykyTpmreNC/qL8SH70fPxZ5M8hsBaaN+NDlg8Jn5OvI5qL +NCM9RvWY4EzinMmflZ/QT9JPaCZpRryK8iqiGV2TfRK1TyK1UbVLYd4iNvZJ1j7J9JLVSyYnKyeS +E5UT4YniifgQ3Qc8j/I84nn0dUEnug59RfUV8TbK24R+kn7Ch+Q+0FdSX4m+kvpKcCbfe+gn6c94 +MsuTGYZZDJW4esy8VfNWPKnypLIfqvZDgCeIJ8ATxBPoPfhvhN6Det9vfa2LNAPMwfc/zEHMAX+C +/+5gDmIO8ASvhSeIJ7B2wc8E8mfvEQ9n3xvUzqqd4Z/FP1M7+7rjyfrQiXY/uf53kiWUkq8GSklK +C9WLHM3MnDVzJj/7LwPqLOpMbVZtwcUiFwsuFrlYcLHIxYJbRW4V3Cpyq+BWkVuF1S5a7cJKFq1k +wZUiVwpsRWwFNl+NAk9xHlwpcqXSV1VflZWsfnLDU8VT4al+csNTxVPhqX5qwlDFUGGoYqjMVTVX +Y66muRqeNHnS8KTJk4YnTZ40eJp4GjxNPA2eJp4GTxNPg6eJp+FPkz8Ntia2BlsTW8OfJn8aa9e0 +dg2vmrxq8Dfxd/i7+Dv8Xfwd/u5vM/i7+Dv8Xfwd/i7+Dn8Xf4e5i7nD3MXcYe5i7jB3MXeYu5g7 +zF3MHeYu5g5zF7PBbGI2mE3MBrOJ2WA2MRvMJmaD2cRsMJuYDc9Nnhv8Jn6D38Rv8Jv4DX4Tv8Fv +4jf4TfwGv4nf4DfxD/iH+Af8Q/wD/iH+Af8Q/4B/iH/AP8Q/4B/iH/AP8Q/4h/gH/EP8A/4h/gH/ +EP+Af4h/wD/EP+Af4h/w6+503Hv+zk/xwz0W5971d36eHO6x+jp47c7fPHsj9rfoPZb+3pmD9v93 +J1n44UpGkm+Uw86P5Z2L3l/HauwwE3tOJNZk+3vsk3G186vLnmuVvw72iViG7g8e++tmX4ll4uEe +l+9BdZ3UvffbPbV9vV7XK6pfjv1uqlvp6Wzf7s+X14uq/O9f/gio4gplbmRzdHJlYW0KZW5kb2Jq +CjEzIDAgb2JqCiAgIDExNjkKZW5kb2JqCjE0IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3JpcHRv +cgogICAvRm9udE5hbWUgL1JLSUFUTStTV0lGVERBWTNCb2xkCiAgIC9Gb250RmFtaWx5IChTV0lG +VERBWTMpCiAgIC9GbGFncyA0CiAgIC9Gb250QkJveCBbIC0zOTQgLTExODMgMTYzNSA5MzggXQog +ICAvSXRhbGljQW5nbGUgMAogICAvQXNjZW50IDc3MAogICAvRGVzY2VudCAtNDMwCiAgIC9DYXBI +ZWlnaHQgOTM4CiAgIC9TdGVtViA4MAogICAvU3RlbUggODAKICAgL0ZvbnRGaWxlMyAxMCAwIFIK +Pj4KZW5kb2JqCjUgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL1R5cGUxCiAgIC9C +YXNlRm9udCAvUktJQVRNK1NXSUZUREFZM0JvbGQKICAgL0ZpcnN0Q2hhciAzMgogICAvTGFzdENo +YXIgMjU1CiAgIC9Gb250RGVzY3JpcHRvciAxNCAwIFIKICAgL0VuY29kaW5nIC9XaW5BbnNpRW5j +b2RpbmcKICAgL1dpZHRocyBbIDI2MCAyODYgMzI2IDQzNyA0NzUgOTE3IDcwMCAxNzIgMzEwIDMx +MCA0NDMgNDYwIDI1OSAzMDAgMjU5IDI4NiA1NzYgNDA2IDUwMCA0NDYgNTcwIDQzOSA1MTYgNDAw +IDUxOCA0OTYgMjU5IDI1OSAzMjAgNDYwIDMyMCA0MzAgNzQwIDY3MiA2NDEgNjcxIDc0MCA1ODkg +NTE0IDcwNSA3MzYgMzgwIDM2OCA2NjkgNTYyIDkxMCA3MDMgNzQzIDU1OSA3NDMgNjMwIDUxMCA1 +NTIgNjg4IDY0NiA5NTIgNjM5IDU5MyA1NzAgMzMwIDI4NiAzMzAgNDgwIDUwMCA0MDAgNDg0IDU1 +NiA0MzMgNTQwIDQ0OCAzNTMgNDkyIDYwNSAzMTQgMzE4IDU1NSAzMjAgODYzIDU5MSA1MTggNTY1 +IDU2NSA0MTkgNDMwIDM4NCA1NzAgNDg2IDc1MCA0OTAgNDYxIDQzNSAzMTAgMjA0IDMxMCA0NjAg +MCA2MjMgMCAxODAgMCA0MzAgMTAwMCA0NjAgNDYwIDQwMCAxMzAwIDUxMCAyOTIgODcwIDAgNTcw +IDAgMCAxODAgMTgwIDQzMCA0MzAgMCA1MDAgOTQwIDQwMCAwIDQzMCAyOTIgNzU0IDAgNDM1IDU5 +MyAwIDI4NiA0NTMgNDU0IDAgNTg4IDIwNCA0NzQgNDAwIDcyMSAzODQgNDgwIDQ5NCAwIDcyMSA0 +NDggMjgyIDQ2MCA0MTUgMzgzIDQwMCA1NzEgNTIzIDIzMCA0MDAgMzUyIDM5MyA0ODAgODQwIDg0 +MCA4NDAgNDMwIDY3MiA2NzIgNjcyIDY3MiA2NzIgNjcyIDg1NSA2NzEgNTg5IDU4OSA1ODkgNTg5 +IDM4MCAzODAgMzgwIDM4MCA3NDAgNzAzIDc0MyA3NDMgNzQzIDc0MyA3NDMgNDYwIDc0MyA2ODgg +Njg4IDY4OCA2ODggNTkzIDU2OCA1OTAgNDg0IDQ4NCA0ODQgNDg0IDQ4NCA0ODQgNjg0IDQzMyA0 +NDggNDQ4IDQ0OCA0NDggMzE4IDMxOCAzMTggMzE4IDU0MSA1OTEgNTE4IDUxOCA1MTggNTE4IDUx +OCA0NjAgNTE4IDU3MCA1NzAgNTcwIDU3MCA0NjEgNTY1IDQ2MSBdCiAgICAvVG9Vbmljb2RlIDEy +IDAgUgo+PgplbmRvYmoKMTUgMCBvYmoKPDwgL0xlbmd0aCAxNiAwIFIKICAgL0ZpbHRlciAvRmxh +dGVEZWNvZGUKICAgL1N1YnR5cGUgL0NJREZvbnRUeXBlMEMKPj4Kc3RyZWFtCnicnXoHXFRH1/dS +7u7lLrsKchWNsmpEjb1hizHYsCsBCyAgKigoXTqogKjoSRBRilJFLChYsGAXe+wt0WhMjKlvuqZ4 +7jqL+51715Z8yfM+38fvt7sz8z9n2qkzg5XK1lZlZWWl954+1mPKyGG+fYdHhQerrGxVKpUVfdyl +N6TWYO8iNVFJbawkF2vJYCM52ZpcWrNZrdVzng7lMN6+fRuVqo2vfQf6Ue2xd5V/aprI34ccVJyV +lVrfddDwYcFRs0PGBodExoXFJY+Iik6ODZsXGte285y32vbp1dutW9sFIeEJYZHdunV7OZO28lRe +1lQqa5rRAxtlWqqHqodytb2qi2qD6rrqrupL1Y8qo9Vyq3yrUqstVjut6q3M1rx1kvVR689tBtss +tllns92mweZj2z6252zv2H5ri5yGa8F15Ppzo7gALp5byv2qtlE7qjuq3dRj1T7qfE2GpkCzRVOv +uav5nrfn2/Du/CQ+hs/gc3m087abbRdrl2G3xq7Cbrfd78JgYZwwRQgRlgrrhCrhiHBD66htp+2j +HaZ9TxukjdIu1uZoa7VXtT9pTfbO9p3sR9lPtZ9rf17XQzdK56uL0i3VlesO6S7q7ul+0El6O31L +/Vv6gfrR+mn6EH2cPku/Vl+pr9Nf0H+hf9RE1cShSdsmvZu4N5naZH6TjCYFTa419W4a3DSu6aqm +65tua1rf9HzTOw7tHbo5uDkMdZjoMNUh0GGeQ7RDssMKhwKHK47Wjh6ORY4/Of7p+KyZXTPHZm2b +dWk2v1las+XNcpvtbfaxk52To1Nrp5lOFU43nB6ITUV3MUBcJ/4s/imam09uvqj5582lFk4t+rSY +1SKvRXGLnS1utnjYwuzcxtndeZZzvPNK5wrneue7zt+05Fp2bNmn5dSWoS3jW2a3LGlZ3fJYy/ut +7Ft1bzWolV+rVa2qW51sdQ+OSb2PWR07hg3HbI41lzpI+aYO6mOmOaLUGxtMvTX678/gz+JyQf8N +vrdcrLid/9UH+B4/C1ZO5sxm1jfCbI5W3zSbT11eazbXGntEhvK5iRw+wW/FdmyHGuZtSC7N4Esz +sPXmVZsnf5C4i7uLxWLJg9TC0DS2H7jR4B85azZfGstt31DzwVng9wOqs58Ap8cSvIebRNZgis6L +NTaHIuwcnBvnYcoCPCDNgo0slSyjC5T32Z8D6GQilqxYTt8b10ktxDJBn4+fYg9xs6CfjSMkvVgu +6DFPaieWCImm7mKp3Jx6QzSr2s9oMJt/qnU3qxyr3M3mr7wawI9nmremM2eXeRqchD1vYlcDXJ93 +YMJGs/n7b4lQU08sD1s2mFU2Y9wj36+v+vocPIR7vsfehhEwO3HqXH5DLFdXUpJbAwdgVyxMgOGR +M4bO4leHcQfBrGq16rLZ/NRqgFnVOr6f2fzrswu8/jTuxTyxQghHa3GjoA88xlLESkFfjSNwq7hJ +0LfbjI/EKkG/A0/hl2JXQR+CScaF4sGNE5jWAENXhSSGpaYuWREPgRBzEVALn6yuK68tLi7K2Qh8 +oqmFiOqbX2BbbDfw9Aim9hjA2rJ2971uGfRdMdJYKEYIetbhGD4UsTnaHvrZAAcX7gksMZu/+G6c +WaU9Mctsvu18gJbrXj4F+PVfYSfIv5JZyIbCfBZclModoFWJnp+bzeaPp5lVzrvam81Pumyn9XU0 +mM1/XHjPrLK/+5B2uf8uiOQ9o0OZvYu+I74pFYoVm4vzK4GvLY4Z7Z8QGTlv4YFvDYCdztb8cIhf +vI1jWn+m7gmsGe96Yuo3X145de/K3EPuLnNhQUpCEq9nYzcYPcX8FAwEzp9xke8NYm34y5HYVWqT +WsaWkg71w8XSVbFm/ak9cJY/HVI7sHegj48LMD1OSYJB/MzY5OA5URuPukD9tq0Hq/iUMs4vcUAg +TOZHNUR/9+PZ4xdcAO2Z7xr4id+xfmPt9or42S4wPzM0IpGnFWRLf4gJMakZccCHpVZcO1yxbduu +TSG9DMA6TQvpFsAXRXKoPYxW3wE24x9PP9m736QpQ8bunX3bpQa2llSU8XrSrzhjgThPuMveFbsJ +W01viN0F/UrUSyepEe3YIHGMUM8yxXhBpn1gPCwmCZGs/+u0ydI31JjPer1GOlH6tPm5UnVACUuW +/hwLLNz0eGwCJOMkgNJxG9FNSsR3TMmJc7kxQf5B4AVT94ddhX2wq3RTMZ9RxiVnLloRDfyCuM11 +53ZdQQeDqRuGi3h2NzurphF34QxxrDATM8VJ4JPoo+j73rKNa/cDf7kyxN0QqolYEbTSY9G7abET +IBh8d3jehiNwrmr7GT7fg4YMVYY8rAy57y9DhkH4Zqgjderx3u+kNaYOZlXbh2RoI2zum1VvHLjC +f2pWtRPTV5WErs7YBxjKodWkuh4u8oInPbUTwwSmanwk9hD00/CYsR/V32RLxXHKhrQzbhajBNQ0 +RlvwXOm6XNcyGwsB/mjcJc4RzjUeseB+0j2qXmSTnvO/aTwmJgttnwkWOE/6H6p2YKUKnId/KrCp +W+MzsaeAXtJtsRe1zzfaGMPFFMH0pWmTOF6QhkqZ4gSZfideExcKuKB5nKA/WU/7ybp8OelLl4tQ +v3HnHh680L1iVYlf3pKbILmdAtgzBQBbsLGpkHxzCfT3Am5MPKRiU4AOXPzakLXhwLP2A7uy9ga8 +zuzEyJjYdH/gA713ogs2233immE/VGdVZfD63zBU0bcaFiX2FrDCto/sS1oq6vYHCxQnCkRyS1Gz +YWzuayRZipatZFMtJCXKZiWxGa+RTFR2rIGNU0ik2OaT3v+P+rUA4mRhn9u88+J2ni2iDX3Zl9kq +sHGLWSW07UEu2oFfRk63Sm1W6UvyzSr+6S1CAsgR27kb+axSLjJ97opI4PtPP4kGAy6HtWe5x2MP +9mPanv1ZO9b8cg/s5GJWWQ1YRXplNbCXmfKYZ3LpKJW4H83mSz6rzOa7VZ+ZzaRk5MOWimA2X73m ++h9nbVb1Si0yq1zfJD/X/cN2ZtXbd01mVef78bzZ/Hb6VurowU2au03T+WbVzM59KLzMpBl3vHPV +rBrqyANft2lBD4NXJkd7+ZGiOmwi2/7aZi5T9ItxbL28mxj1Qn/DGweLfQX9GnI+sv7+0kGcJOAI +V3GywBpNQ8VUAY9Kq0VPhaW9Isc/GnMtHCsVES5h7/07y3MjwH6Nyyw8XopMDzCP/zDM2Dmk4ace +WBhCpXVimrCDWf8rQw7+oSyFTR4k9qO6Fs8Yr1B96xjxPRk+ZXQUIfHjeHhrEXiTlt8AMPUdFguJ +uBagfHQZYCXmAgesZPSLxmFlIA26TGYyDuDnjzdCOaPWRO4a9cFyqekaNXE4gVbBVCwXVRp5VFup +QgRT/JhY8oup5BenFgMWc1v2b6s+DPyBrek9yI/He8UTmkCoJ6EfcI8CG9xcBsL8gLkzeBz77Z5e +2ELz0ipXgGyUnL4dfmr8RIwWOrNI0Y0i6xA8arShelf2jphAsfyBYoE4gExQgRsszn4wG6rA6K2I +7TSZn4W7nyK3c8xNhtuierEYOCsmmEL95KPh1+AYHHyhn1lJ2eS1584qPXOurAE7G6B8Bm3MSWVD +CmlDPooH0xFOz9yxBznHtsJlliWykYsh+coSYL8kTeH+6359i+RltrYoiw15ActMo6SR8hwxXvqU +1Le/wL6WdsgQdpR2Sr+K3gIbzSaIUwR23dRe9BI8pc+oTc8EHGdR7iGsKXHJPfWyeOcR7uIiYh8v +tZV1jL1NqjFAwLPSCXGgoL+OPymtl4xzxKkCdmUXxGmC/iKNPkxkw5dC8qVFtKxRsnyTSYJ+xRzq +pDdEiKtOZu3Z29iDRWAbAgIPsP4440/UXa2ASk5/B9OVUOLJcsVBNJnBGPa0txxaMtg8cbpA+M/K +sDY0GQs+T6l70DRkWPIwjpTr/r1lOEZSKRGAPfCzMF+TVLRpozBNRn2xDr+jKltO+bIC35QcRNiZ +sZo5TmDNvCFs3XJ0usQ1YKUyVnPcLdlZcJspzHaGgnOnOSbi7zI/RkidLHHwVOMhcbCA57GT+LZw +T/pBHCLo/8BflKDI/Ji/6KNQOytTT/niH2i/U6BWUoyFFE0WN3Wy8dY/EJ+2uCtflqBQO+MKaTSV +2DA8T70Qe71s1Qk3U8F0yj2OzJYCWvmESpDmfwB42bQAPDkYtyopMyw9LXXFQkikc0NaLp+2LPNt +rnRZybIa4D9D/if0MYC0/UYl6fNk0ueLcWByZ9ekBeRFJVtprPjcMgM0W/bXVu9/bsWsRvPfdY3R +GsW+WfflIhvxwiwU/5Ci+Ac6k+RjqcV67dgc8R0htFElDpXT8j7PI+hM0VfI2yP60ZLzJV4x5Hzm +9zpl6fNAOu11Sv+nanGxwOIL6DubFb9O/9jY+yWUxgpfcXXAZxZRv8feF98V9GNwr0W6BpqFv0wg +OSki7GFsbsFD5WqWFGlBKcwrAvVkWyxwtkWGLixZJtiEJy3dr2ncKboLem9audy9Ky0nQIbXKTYb +0djDgkZbLFY3yoJOYEHifGE0FljQTkr1gVGU0Ss4R6mio/GAOEzQu2IbpX7FuFQMlOEdysy82TYL ++oXRXV5HW/aGAkuT8Kq4hx3SyNGBwyzcbSH7iGxqD4t73jyAMiyixkAJFG+ca7oiDhf0D6R+ijNm +i5m7OFPGscgi0XLTVYUAhymy3MqGWXDJRhEj9jOdteA1igSHsIHPcWvjz2KM8Mt7YqxwwXTcQrPD +6PyijY1ivZ9TTn9NzNipUW2ZkOo1CfekzbfQTt0u9ov4r/Knfcc+Pszfxflyb1+gv+z9hj33fsC1 +q5tx3+UuHD20+whfOPi/TL69VHLyPUxOvo/LyfcJOfk+9bfk+/eXPraIi54VGkEJ5+zYis8Nemmu +lG7x5wOZXhxBqymRWlq0o2dXMUjQP8FTFvxDwkcKVaRRowR9LIJC1L3RTkyU9/AzRf8+bDwoegh6 +H9yoqN9q04/iLBlNsQSfpMalFniokqngu6YDMs7elFoY+4rJuYk584EfxPTubLQBTI5KNCggax5S +DNJwDi6/vym/tqAoP2cTlMOh5A3L+Q1rODaOItqetzWWcE7RKIbiGsUhN/RWshsqvG90kf2aGwZY +jMZ6HH3r3X5QVIea45TJUmEMFUbLBX9Fa6jgqkyUCjNe6QjVtr6mBXpa3VvkME2/Jao/loP2y/TH +Qw7ouJhSoiMer7IfTonynP5/sAbPiCyF7ciLlcJe3JiwO4ALsBA2soGYikte3piMt9yYfCN1x5Nk +OAs0QWkQx2EnPCLe3kyJ0mRKgOyz/GJ94sIWpc0Edwj9A3AyoMOaI1XHK2s3ra8HPsb0h9hF0F/C +1UtE7HDvAbY0wOXQwz5beHSA57c804FjfdzcWBfW+ZdpaGs4hJJ4OqZoBgwBj6XzF85PSEpZGgYj +IPi4cnNRfvZWPq8/gWuNHuKzbnuedtOMYwfEZVs41ipyeGfozLNeKHqjNfZ/8gjbYi9mf4LZuGTM +4W4f85BvQ/olhURHJiQlZs2iKY+4pfRZdObBIV7PJh1Ef7Gz8KpQjqGiFFRvClLr2bnvacAM2Vnd +UgrMvznV2DrU4ClKlvStbRtac0fwkvimkMruiB0EU3O886pi/3qli/T0ZUVqgU9FV+qp7KFxlOjP +QmoXPcFJSzdx+qeOtlJ2vSlbrTelYKqxRIwkyaulM5grmqZmk4d2iaihY4En0mfHLuaKLaQp2RSE +1HvlaU/NYR2YS/V8Orp6MvosmIeurIVpSg5RbDG2wXki4a3/houmWqCOW/+tY1Hu9TDmiFnKDI5h +hLhMKZ3A7qLJLxsN6EQsI5gH0odY2lIq5ZvNmfJwoSj55VDMcaKRRqAHow+N1JbpTL7yTNQ4V9oi +vuCsrmHUk+SXDfUm32ySno7Dzmy8GDEfqQeTXw7MlHxzyNPrdsyTe7Ms1kekZq+18nJqwmk547PK +OXYfqduJbHxeopQP1J3XUnldlnX40Dqybj5fwhkSn2lkdmKI5JPNTbM1+WSXh0gjae6zborYC3u+ +vhWW8fqL0sic8j0mnxyuwVbyyUncYxqZw0mzRoqsF+v5+o4S/TTpSXOWaXQeCyy90Zk7u1vEUcr1 +SiKBV42LKLg0eihm/MxVMeNSMtkhZfC0K94Eahvyok0246ddZTNmjhizSZS2av7RPa3/gvurWzNt +lROchRlh6YszsuUEJ6BUSXAW9edKl5dnKwmO/jaONuhj9uMCsSOpoj8d4IeLbwn679EkPRPrwzd5 +utSW1WymmLB7QdU8CIe4zKhkviCJKy/Iz6mAUihOhTgIWZQ2I4pfm8qdrIwNCwiKG2qI1qDthf2X +Dew02yxfEn+PhcZYkc0q1NTSgWl2OnCZV8XSJA0dxP0L0jAKYDubQlGJnZepMU1a8YX44Iszj38d +d3Cw4V2Y9E78HL4klqs+VnP0GPB3tk2c7J86x9/AeHLbuWwc8AP6e7l2uhR433AHLtzduJdPqeIi +fMP8fIF/N+rC+cPFew8bkIdSOuURsX4Z/klpIbti6lsaLzUlNWFfAO7CRk4fc6xEXJOynfwgPkFn +9vgUrCtiQ6QhUUCY1FPqLS4rCQfmxJ4wZ3w8BTIX4xDTkK2E+p5fJVZlF+YDruZQfXD7jeu7gjq5 +ACs4/KJRP/M0Hdw1M0cwKxe9r2QvDRVjc5ZkAFvNMXVg+CiPeQd+dQEs8H/RqK/3Rg419beQGGKk +3lghrhBkabUnad217ShIGmyuNOERiZLTDiav7RmU33lxbDf75e80xB9EhVR0t7DUyFvgbBKJxVkS +ucds4etotYw6KqgjoayM7aEOUW0r60mvvc1xRT1bocZQ/EVkoawP0mfLLrLyJjgc9dxrJNurRaLq +w+gTTcbLmrDhjAikAmm49K2YLbC7ts+/qM14mc49bana/PkXteHw9dSEd/Huqx+ZNNtb4ab6yx99 +109ZinhZDcTVCX/6GZvySaUcs2M6NtzkBKalvL4rupLpjXuWdllN1FSkViSQMncdX1bKvYVNGbFS +DzLtReoOpKVsuOTE7FDHJyVzP7OmlLb+BCYLwavOLGU1AYzwt1hTviyZQ+JC4qZOKNCM/FWe3lMf +9SdFUMnak9awkTjoZReEU/lZGrav1Awtgqc+Cj6XeJ6msfZxmk8Ww7Pnba/xPK/4qIcuhjiUOx2E +7xDPFeMcNWDAH/vOlfLLK7i+S1ibAcCm8y9g/JJgddjOWcX7i2t3UpHHr8cykwbcFs/zzOQTNecy +i+aBm0IveytfDXFjmwFfLeFzKzjP0vaU7AXwxjkaIlhCBGOfpV1Ry2Xq3EjEOJ21edC3kF+ewJ1L +/mM2sAC+8RUxo9GZujZ0f+qs1LBQKvLs6ytIoz8s2nVuHV+u8Vy3eBc85F/QN9JiWED72Z7JfG4C +91UhtnlAI9Cezj6KVSK2NvU9tJ61lFyxpdQhYD1t0mxKajaJrLXUN2ARtjS5spamDocWcfpXT6j2 +8ktoO/mJNMpxSaH0TiEGFG5br2YpoFGQZlK2k5QiltkL79vrVSvlZ81mKieVqBqs8lIFqkJUc1UR +qmRVvuqxVSur9lYRVu9blVmdtfrEuot1pPVK653WF6x/tLG36WzjZjPOJt4m3+aObbztJtvPuDe4 +YZw3t4hbyV1X26vHq7PU69WfqH9QmzS8ZoImWJOgKdTs1FzTfMt34d15H96Xz7PT2420y7A7aPel +3Vd2f9o9sUM7yc4sqASd0F8YIAwUpgmzhYVCqbBLOCl8r7XWDtVO1V7X/mZvY9/BPsd+l/11+/v2 +n9l/rrPX6XR6XTtdP52nLkX3vu4DXY5utS5Xt0aXp1urW6fL1xXoCnVFuvW6DbpiXYmuVFemK9ed +032uM+r76Yfpp+h366UmYpMZTfybBDQJDI6ICA6uDq+r2769ri68Otig1WqPV0/rZoCJmTHJ0Slp +WUvngw+ENcAPcKFwS+mWkg1r8yj0aNH21k20R4feB0Yx25GjmD1z+HrmDYOWmRs1NYloNmo4rWRf +b7JXa7U49xKbq9ZSIA3dn4bpxlBOi92+Qz3GYRrjb3Vn3boTfxxLQ2HY9wbt3o33L8J9/pPZdI5g +dpOHDCEv7I6lvkAWPmreHM9JgXvvucAnlbUX6vjEbdyE8D4jYRDf4YYvOS/He3d/JB/8Dtu8DdCV +P7N138Hj1fNGuoDvkim+EbyWRfzlyeQckvnBBeYxOZHallObt/K2wmnZRPgWKzntj3BzS+0Gfi3a +T81J6cRFp4dl+QHPWrk9xqk45vRP2NZwFE6lbVnC5/c6DGvQiQvbMLm0L5FEMVtKbaazBWjLVDjT +oMUiNcajIyWe0ej7zp9sh4Gtfn4QGBrLaS+rccy54cyLxQ0exbobRqm1OP1lUu8Jyt2pTAmJUgQU +oNPs3Kg+LB5oLzvARu89ANg9BOK9WQdqWrw8MnsezcGp32//2zSjmc3zadrQNIMMX8H1kjO1POCc +Ixsp4K4ESH4Qz2mfD/9/Xdi+fl/LDXt5Lylf1F6LB5YtX9RitnJTq/1IRope3FgOl9moVj687G/Q +TQ4mLGZWw8fzhSkcclduow2ghteelomWEdEtmX+EzP8B8Y8n/utlUMVGAsQeSWTjWdLMGRjNbmAX +EmjAfpaJh5+P8PK6VNnGuVCBTgG5cX1YEu2ZNP8DTQM6sDHw9wst0oZr0oJpzAF/NBVx2r9eVMWy +ofeI+9dPaB/lo5VD8sz5PuFhwQtHQCcYKM9cPlttPFhzfHvtvspbsumEYDPKT2Tl73uXObMQiqmO +ivr3fBudDdpb6hubg4cwm5CZ7Q2Qjv7sOralhcw6yEYjcKdLvj4CaMv/6n2ctXZhu18/Sq6hyY4t +5bQJv/pn57I23JHUsxk3AfvCnfvwCNDKbK73TDerek9oMJuzd1Gp/153s3mr0d2s6jyDSg/UDWbV +G1UN8CFcP7zjLByEHZnlWXxpj8OkhE252DWeRUOBtYB3h0MvaGdWvRtEHZ3/hZgSyon9ggN1NNNV +ZTbfekSdN/+USl81SYcJMHpGyFiYBWF5geW0/k5Q3m3/B8vRJRGmLzfpYXpuIixnLvtXVvxAGJiO +DYvlSDXKNbJmsN7TArt3mnoa3yS7fvKhLEWgvb9JyvUnx0ZceRtH4YBLP6Ebwb9dlmEvBea0E4C5 +mCYyF2ni2TKoYDkk18upOMtkPAkc4JvSlPcN+KZp6kmKQxJ+VEokgbLokyipmkDmKPMMp4ZTiZR+ +5DItrqmTm1Ko6cNUHGdSXZZtby2lhB9oFF0stKjKVsBb0qVFW9jXysIWsXaHV5Z+j6nAbpguFkZJ +U6AMXQJyOe3ouZCA7ZiwjdzM//OLKwzBoZtWlfitXvIh4JTnD6bayZo077DwBRAJSeWwB7aZVdab +t5hVBumeWeXq52dW6TQdzSrnTSqzii8ksbUPoJJNqHt1Dq+thE0rNizjK9zr87J+55LzZq+dTe7B +gdmNZHTI/58xTwxH4GbhgUP84hIuYXFIVhDwg32vUXpkf6j+PDn+1186azX/+la5T/PXl03ti4eW +xg4vX0c5bX2Z8o7WHSAMV/3j099JOFe5tervD4VoX37kkkHbUOg/YUh6pN+M5AOX7hTsPGbQfjnm +SMeO745z63d1xqNHdy5+5aJFbkNufUY+s5vJvZyD6dH/97uRFqT2r15dlAupbCLktHJYfT3MumgV +GpMNeblkZv3Sy91Ba0A7XtJotM/n8tobBxg70Sz/9XmDuQLEcX/hkwcf/5xvZOrrt96vr+7Vffr/ +srrzdcOZltklTBg0Oehj1KJd1YXPSHau/fsxV+b2u/dv6Prwa3RFN9eT7SiJeLUTypDr5Ku5EsA1 +3OZ9m9fthmooXbYpi069Bd9zKbkpqyNhCsz1XzCdArR2+pSRBhhxJPThXLPqna3kYL5JJrfi5EMO +5rflDRAKAeGJ3vSTtC5pHb/o4xkr1jAVtzPj6NJjgM3h5Cm4B3d9NvetM5svhpOKtzhOjJ+upS6a +DWuAnbCrqvwo1EJ51oalfNmIfTkrf+Mi1gXlB5Bj7hoMA2NgdjykXqW4VsP13BZ9zuUEnC7dXcMv +ruCSFicvSwA+eHHlaQOWo/UeZo16jfblfelfb2Wh16vHfvz4Hy5VF1Z8btBOBqZiNT2w6FAxBdsI +CrYNi7CP6a0DgDyeQh07RaUekuvFUq6UDU7WnErryjaSRD0Ak/BZ5ha2CqYoToavX1FyjwIeS2KN +eVH4BxQjH0RORhtRFVtdXVVVXR1bFRERGxvhQhkeZWQH5YxM68q6b4vDdihABaddE8Ndz7u0F47D +vsXl7iX87pMLy5YkeQ/JT+Z2ZxYnw0KIT4mZtZQnfubHjq5Nwh8ATrD9FN7QE9ijCrhVAdKbnNa0 +svFpXaK00viUHKT1uMoqOms7UKBzqKA0JzJhybIkcmbh24MgCoKX+PjwZfHctorCNWVQB9vDD8BW +qCs8fpzX1qyNmh+SmRQekb51x868yhqD9mTITk/PObN9p+4OPXduX90JF+1jVqkeakkkuH9Orli2 +q+ynV13W/DW9wi1otVv9jx6zQfaYDbLHdJc9pvu/ecx/MPfxxZw2IyujO1exvGRlDZwGs6qjkThG +zadehmiotC6/gcf+skRJ4INJ4GfTOrGijlhY/0IDTizCISZXLsnPbH56mfT/0Xr6etxX/je2ufT1 +uDjdbP66J7V9G0wl6TrlKWkwtyCNZjQNwovjd/Ex6VzF5PrAq3AV6g9WnOdjCrj4eeGp00jHmfXl +uFgkWSDJJKGK046awxVH58VCPEQuT06O5QuSudO7dtbDOdiaXplRyGtH3gz9Ezt99snHLnDDbP6h +K0X2Lt1oHgUfyTH+VgPPxK3cgrzUdbAeCnPzt27iYwq5OXFBPmSjgZVxpxbxO5aiLprbm1mWChH8 +zNg5Xd/1rz7uAvuKC47U8AvWcgkZAxfABF57Yy+XuiWrCjbCttzS0io+vZTznhc6EzwhqiAuf8mr +Df8336kNYh47l3yHfbMoEGu1s6JDpxtg8m6vu3P4gnLOrzqRshtTOsmkpU065EHOB3n83u1VdbUL +N8+VDz5ZiTiRja/Zzjpga8lrKdRz2rxyNhHHzw/HDqy1yWstUKw4W4J6k6YO8Mun70Tks4ONx/vg +4bQ81moMpw2NiVwWSeelGXu9SMVnp08L4otjuJ2bt63ZRmZ1ZM4ZUvF9BQ0HeG1wdQRFg2o6dG2n +6BAerJjl3/+ulo1U/hEwMygxLCFpyfIoGANxlqvvwgPltRVlRau3ygll+wdfYxfs1e5kb9Z+UD/W +hfX63fsbg/ZGOWkUud7kOwnAfnSTdXQb6ejbpKP7a1K9DDB4Sn/m2pZfswBvQwk6JeRGB7NBwN2/ +8Msn8A1cDzo4EMZBQKK3JTEpKVlTS6OZ3tmk+fI4SO8mkF3titGwA6bu6xKkUIAPTV1kU3v5z5Xz +6ydtnlk6evUQ4DehB86EwvtZRSwdAtkX+ZncXflJ6iypsSlRtqvBpFI/ZZOVuB6j0g8ryVR0wwn9 +KlEFQXyHEZNZTxftlspN+TuAP75vpmv36GAv/5nnsY0BcD6ZvTVqP+RTt3DMbSJzZxTOInnW+6I7 +tkbH29+g3S++HzEbFy+YmxCxgNfmp1CuzHmwsR6srxsbw9+NxgijdWoZO0sLiI5btHwB8D6zax9/ +v7HuzOG6yYwGYfPbs2ZMO4kvjubQ7QL2x16AkTz2nnCbtWA6997MrvP5CY9czsCW4uodspPU/h8t +ahH9CmVuZHN0cmVhbQplbmRvYmoKMTYgMCBvYmoKICAgODc5MgplbmRvYmoKMTcgMCBvYmoKPDwg +L0xlbmd0aCAxOCAwIFIKICAgL0ZpbHRlciAvRmxhdGVEZWNvZGUKPj4Kc3RyZWFtCnicbdfLbttG +FIDhvZ6Cy3QRiHPmSsAwUKQbL3pB3T7A3OgKqCWBlhd++1Lzn6ZB0AAJ8EfikN/wMuLxy9NPT+fT +bTr+tl3qc79N6+nctv52ed9qn0p/OZ0PRqZ2qjet8W99zdfDcd/4+ePt1l+fzuvl8PAwHX/fP3y7 +bR/Tpx/bpfQfDtM0HX/dWt9O55fp059fnvmv5/fr9e/+2s+3aT48Pk6tr/twP+frL/m1T8ex8een +tn9+un183jf77xt/fFz7JKMNh1Qvrb9dc+1bPr/0w8O8/3mcHtb9z+Ohn9t3n5t5Zruy1r/yNr5v +9u+LmPw4SvaabZ0py2eGcveRo9Xy1EIFSqhI6SiJstRCOSpTnipUoCoVqUYlqt+PbI66h5ViTDNT +jGkMxZhGKMY0lmJM4yhE5u7bx2IPJlDYTaTQmkThMwulx5IpPZZC6bFUSo+lUXosndJjWSnOkcxU +oQxVKaEaZalOOWql8Bl8gk/PtOAz+ASfwSf4DD7BZ/AJPoNP8Bl8gs/gE3wGn+DTa9DiM/gsPoPP +4jP4LD6Dz+Iz+Cw+wWfxCT6LT/BZfILP4hN8Fp/gs/gEn8Un+Cw+wWfxCT6LT/A5fILP4RN8Dp/g +c/gEn8Mn+Bw+vcccPr03HT69Gx0+vf8cPr3/HD69/xw+vf8cPr3/HD69/xw+vfsdPovP47P4PD6L +z+Oz+Dw+i8/js/g8PofP43P4PD6Hz+Nz+Dw+h8/jc/g8PofP43P4PD6Hz+Nz+Dw+hy/gc/gCPocv +4HP4Aj6HL+BzPJn1Cfw/T+SA3CMPyD3ygNyjC8g9uoDcowvIPbqA3KMLyD26gNyrDrlXHXKPLiL3 +6CJyjy4i95y9iDxwhiLywBmK+AKGiC9giPgChogvYIj4AoaIL2CI+AKGiC9giPiCGvAFNeALGBK+ +gCHh0zUm4Yuch4Qv4kv4Ir6ET1ejhE9Xo4RPV6OET1ejhE/Xn4QvIkr4IqKELyJK+KIe9fCJ3rVp ++KSw3TJTbLcMn1S+uQjF3hdLsffFUTrK8EnjPCyB0u0ipdslirlehs9m5nPJFHO2DJ8tOubw2aqf +DZ9tzODCyq/r1sLKr+tWZuXXdSuz8us6kln5dXXIrPz67M6s/Pr0zJ7SUQLFFZLjKH265LtPzMKx +5IXS7TKl3yyjCjOR6yj9lZUbpd/sFFdIXikMZaY4f2X4TGOUIhR7L/xy018axVHYy/CJrj9l+ETn +rESKuS6J0m8ulO49U8xLKYypx1IpZrc0inNbOsX1UlaK66UOn+haUfllqs/uKhRjVnz6tK749Gld +8QWOpeLTp03Fp0+biq/3b5/I4r5/IFfgnemrwFcdHvjKpVMH3C5MWG2UcjrFJVBXiulrM8Ue2oD/ +e6M0odhDsxSnqzmKG6V5CmoLFNQWKS6Iligmsy0Uk9kyxWS2QnGCGr6Mr+HL+Bq+jK8NX+yM2Wdq ++Xai768n93epr+8+9X3b9tee8cI13nfubzqnc//6Tna9XO9bjb//AJ+bDlkKZW5kc3RyZWFtCmVu +ZG9iagoxOCAwIG9iagogICAxMTM1CmVuZG9iagoxOSAwIG9iago8PCAvVHlwZSAvRm9udERlc2Ny +aXB0b3IKICAgL0ZvbnROYW1lIC9QR0RSSVkrU1dJRlREQVkzQm9sZAogICAvRm9udEZhbWlseSAo +U1dJRlREQVkzKQogICAvRmxhZ3MgNAogICAvRm9udEJCb3ggWyAtMzk0IC0xMTgzIDE2MzUgOTM4 +IF0KICAgL0l0YWxpY0FuZ2xlIDAKICAgL0FzY2VudCA3NzAKICAgL0Rlc2NlbnQgLTQzMAogICAv +Q2FwSGVpZ2h0IDkzOAogICAvU3RlbVYgODAKICAgL1N0ZW1IIDgwCiAgIC9Gb250RmlsZTMgMTUg +MCBSCj4+CmVuZG9iagoyMCAwIG9iago8PCAvVHlwZSAvRm9udAogICAvU3VidHlwZSAvQ0lERm9u +dFR5cGUwCiAgIC9CYXNlRm9udCAvUEdEUklZK1NXSUZUREFZM0JvbGQKICAgL0NJRFN5c3RlbUlu +Zm8KICAgPDwgL1JlZ2lzdHJ5IChBZG9iZSkKICAgICAgL09yZGVyaW5nIChJZGVudGl0eSkKICAg +ICAgL1N1cHBsZW1lbnQgMAogICA+PgogICAvRm9udERlc2NyaXB0b3IgMTkgMCBSCiAgIC9XIFsw +IFsgNTAwIDU4MCA1NzYgNzUwIDM5NiA0OTYgNDQ1IDUwMCA0NDUgNTQyIDQ0MSA1MTYgMzgwIDUx +OCA0NDggMzkwIDM0MiAzODUgMzA1IDM5OSAzODUgNjcyIDQ4NCA2NzIgNDg0IDY3MSA0ODQgNjcx +IDQzMyA2NzEgNDMzIDY3MSA0MzMgNjcxIDQzMyA3NDAgNzAwIDc0MCA1NDAgNTg5IDQ0OCA1ODkg +NDQ4IDU4OSA0NDggNTg5IDQ0OCA1ODkgNDQ4IDcwNSA0OTIgNzA1IDQ5MiA3MDUgNDkyIDcwNSA0 +OTIgNzM2IDYwNSA3MzYgNjA1IDM4MCAzMTggMzgwIDMxOCAzODAgMzE4IDM3OSAzMTQgMzgwIDMx +OCA3MTEgNjMyIDM2OCAzMTggNjY5IDU1NSA1NDkgNTYyIDMyMCA1NjIgMzIwIDU2MiA0NjAgNTYy +IDQzNiA1NjIgMzU2IDcwMyA1OTEgNzAzIDU5MSA3MDMgNTkxIDYxOCA3MDMgNTg5IDc0MyA1MTgg +NzQzIDUxOCA3NDMgNTE4IDYzMCA0MTkgNjMwIDQxOSA2MzAgNDE5IDUxMCA0MzAgNTEwIDQzMCA1 +MTAgNDMwIDU1MiAzODQgNTUyIDM4NCA1NTIgMzg0IDY4OCA1NzAgNjg4IDU3MCA2ODggNTcwIDY4 +OCA1NzAgNjg4IDU3MCA2ODggNTcxIDk1MiA3NTAgNTkzIDQ2MSA1NzAgNDM1IDU3MCA0MzUgMzQz +IDMxOCA0MDAgNDAwIDQwMCA0MDAgNDAwIDQwMCA0MDAgNDAwIDQwMCA0MDAgNzE2IDU3OSA1NzYg +NTUwIDUzOCAzMDAgMzAwIDUwMCAxODAgNDMwIDIzMCAxNzAyIDIyMCA1MDAgMCA2MDAgNjAwIDc2 +MCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDY4MCA4MDAgMzYwIDQ2MCAyODYgNTgw +IDU4MCA3MTggNDgwIDQ2MCA0NjAgNDM2IDQzNiA0NjAgNDYwIDQ2MCA0NjAgNDYwIDQ2MCAzMzAg +MzMwIDEwMDAgMTAwMCAxMDAwIDEwMDAgMzkwIDM5MCAzOTAgMzkwIDM5MCAzOTAgMzEwIDMxMCAz +MTAgMzEwIDMxMCAzMTAgNDA0IDQwNCA0MDQgNDA0IDQwNCA0MDQgNDA0IDI4MCAyODAgXV0KPj4K +ZW5kb2JqCjYgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL1R5cGUwCiAgIC9CYXNl +Rm9udCAvUEdEUklZK1NXSUZUREFZM0JvbGQKICAgL0VuY29kaW5nIC9JZGVudGl0eS1ICiAgIC9E +ZXNjZW5kYW50Rm9udHMgWyAyMCAwIFJdCiAgIC9Ub1VuaWNvZGUgMTcgMCBSCj4+CmVuZG9iagoy +MSAwIG9iago8PCAvTGVuZ3RoIDIyIDAgUgogICAvRmlsdGVyIC9GbGF0ZURlY29kZQogICAvU3Vi +dHlwZSAvVHlwZTFDCj4+CnN0cmVhbQp4nH1WCVhU1R4/d4bjud6xMYWrqeCgLW5UEpoKZbIKiKzK +GigOiyP7AOOwyKKC2onYF0GQfZMQEc20cd/KNCJ7fWal9arXy77K972+c+nM93pnBkNt8c439zv3 +v/7+5/yXwwELC8BxnFVQiJfHOjfnMAeXlMQYr4zoRI0acBYAAI79naTJQJrFSdYyyUYuWVkYeWsa +bT3B7deV0Iax351senc8zt6ccoppPYe9LEamFk6SATnHzXZ4yTUlNUurid+cYTtfvcD2hcX2S+1s +E2ITdZpkOzu7cd+2Jue2Y97Hiczc3Gh1ckis7aZ4TUqmNj0jy3drWMJz6zYn6mI8kyri9KlpztkA +uAA34AeWAw8G2RV4Am/gD4JBEFgH1oNw8DwIBY7AB0wGS4EXWAtCgDtYAHwBD1aDMBAAAsFcEAGe +lDHluWAxWMHM+TDFUFACGsAB8DY4C66Aj8E34EcgcTJuCjeHW8St4Fw4H66Ma+beki2RrZT5yi7L +eflGeZo8GxskewNnMJDTBrlhmvSUVG18aoLBqBYle3LaaI+U3dJ0yUv0FFwiRC9BuYz8xr7WCMpT +pJctwgSltFD6RfpedBWUgeQrRvIVlLRD2UPeG/UQqW9GSfqFYuy/wHgd9hW8t6sb8yTqLnEhdneD +m2NUftjZbR0VeDLrP6K3oGyUbJmBAJOrQGYlgKSNLhFtBQPNF9cJyiLyLeOGCzERYoSgPEta2ddG +QbmYdLPFq0x+OrnAVpFs5UT2SQNilDBM54obBGULqWaMTYKSLCO2o1NFJ7baQGoYLVpQ/kO6xeC7 +CcU0XHQXGPCPGPBggZSTO2IIs5VOniEzGArlLfK19KO4SiBzpzkLynPkOpMLYgLh5BJbhQrKLLNJ +6spiPl9c6p9Ha+Fuz4H8g5jIMfEg9ZhMx43F7flVvK50YRSs0VdE4xT8pCNdqMFZeEd1yQCvPCQZ +mDEfgVoSg7iW4UwfXSN9IzoI9IB0Ulwi9JJWcSkjnzBvP11r9oX9tXRRDHUrLyCgpf4WvEhcDd8d +xF24Vr9TzWdQq7/i1el3qPl4GphHZaX5xLV1T1tAGb54mKTC+tvhPZF4OY4L8qMqvv7mPcKLOMbP +l87jlfQCCZfeE/NZ7E9KS0ZdRT9hEz0t+gvKRNJoQrUqpST9DO7ccpK+cpQGEpeuPW3rcWrferIG +ukeIaynISU/CmTi7TFfO5+3MWA5rt9fvbsb9uLe9ubutr7JpmEzkySqiEzfjNK0uOSMxX+s/j++b +cO6H+pZu3IJri5uL+LqK1i9gflXuGzrMKw+Sjgf3I4xeDaa55pgHSBiseq0SV5hS76cHUm/VWOpZ +m1OvyZx6VE3l1IXaPX1aN6i6hEc+OEsE3t1ozTbIn22QF9ugK5J8iLjALTWBpcmYZ5najrsK6gv4 +HbEwhDqvckzAfEZ+1SkytY9Y/qxSEhsSSRLFRQLxleaIdoLyeTKZOVILytx6yQuTyNqBNybQbIzM +3chS2m0lFYr7JgmvT1KCIlORWwIrIIJpYDp4AswAM8EsYA1swDzWEfxY3zD1A1PnSAGvgSpQDWpA +LagDe8H74Cq4Bj4Aw+Cf4H/gN86R28ClcxlcJtfJnee+kqlkobKtsnfkU+U28kQ5ll+X/yy/azHR +wsUiyqLL4pTFR3AS3ARfg9Xw1oSQCQY0ASHEo4lIQAo0CT2GlGgyehxNQVORJbJCIpqGpqMn0Aw0 +E81C1sgGzUYqZIvmIFfkhtyRB1qNPJEX8kZrkA9ai3yRH/JHASgQBaF1aD0KRiEoFIWhcBSBXkWR +KAptQBtRNNqE1CgGxaI4FI82xyQlxcT0JA4O9vYODib2xKgU9x8yjMsH6DCGCnrNOOqdjbeSQIz3 +uzeSbCn+a2Oct55RihglqIFaSb/Ce2o32vF+WoHx1it6B+NJFy0TYl/7PZqg4vrvrGET6+XfWWsY +i/Tj8qO0H9+zgnXv618wnnPKwjoSjXHzK42fSbuI3njoJr4j/fT+PtxMAzHEuk+y3Y0+NFLSjuk5 +GM+YrdaaHX4tXYKKiqePlewlc5JxWDGlOLwsCSfSOW/vGbjLNPwwDTF6+Ese55qYwRyMdRdyvjGO +vsMsk2gp/HXVNWOEAbNWSEYamYA/E7isp2uNblBxxaTxIiMc30odaN1LZO+QiZLCKGdyyGNGcImF +MkQqKcRqWuJqAlXKQJlgUvXT5nI4YS6Hq+ZySKQrS/KI971+0U6eOXK/HBQPFYPX/WJoMxWDorW2 +MIROjaGWjqoErKnNreHL++Ep4nL1iyNMl1rTBdSO/eD5hE9V1/CHp89f4nUtMF4TqYnGfOSWti8J +aPr3HdWbuFPbso2vK+u/AbMbdNUmx+OTJoPOg9vfKCzJwzxdSR+nnvSZe9V8/cMx+Kvuw38HMsSd +efU5fHGyCfHLy8cRdxJLAlQKOpHOpPOoPYXvJnzMQN0YB7VeE4L5+KSOm2R2+5cj++oLtCocWhBM +J+XzSYjMrP/hLP6UP775UOhsxWas3RaXFpXg7J9FZfwKYwnMcyzQJ+FUrKnX1vB5xanLYUNxx64W +1gLb9w50HDvw8cVGIuM/l0pg3Wc1TT24E7+Z217AQu78AuaUpZVmspArDtEN2Ddzdw7LejW1+FPf +2kQLYe6SjO1BOAqv3hfTw2/bnr4Sdmwdyj+Mz+PmskNNx9sId6qV2PBDpOhvT/GVh06RyFX399rb +OP/hyXLSPFkaHjFZQsyDRG0eJAq65l67jqWah4VZ8o178TLOjxvz0lx7G7LNTE3PSMpMLsp1ms8n +JfxCk4gDGzDhOK5vIfGCFz+tqe5kI6KxiI2IytKO23B7pa5c/2CKvGyM+6O3vx+kYyHtfURIYQ+G +5ECdFlMNhSMJH35FXvqWaAh0PeChUgx1bVxKxbhnV0enHvmSiAe/H1Y95NQc4S/N9bdhGwEjZNYZ +vB/X7Wko5KvLev8Fd1Rsq8zE3jjCJ4jO5BWL6cQAexWO7Is/G3cutjELJ+Kk3J1x7Jw9hzIb+Kzi +nOWwqcCQcwLfwv+9gD/HF7YcDBjc2K3Zj7txV2tVOz6Gj6e35PCNZS2fQ31VWpnp1BdluhjtYfaC +Lv0ZfBJf6+vv4wuboD4voSALb8E791ef40ek+dCUd1ocO5Z33mMROMfTGJj7oiHpbXwbH750mcxj +mxX0UJuYP0jcimooyMxdBv2pa+hz8ezuk9dUcZh/kwQyS3++NZ3Hl68cucPvcnzgFuX8t7co57Fb +VKHpFlVHgmFZS/lR3MHSYEzSblzy2WV0STLOG5P8YwRnzRH0myOIpDWsfrLN9eP8QP0cN9dPX8nB +luPd351oJdasfIoHifsjolPTGS27YNvhzsqj2/mbuXQF1S61b0yHR3s6KnuLeOKYcAy/RWZklsJ0 +ddbO6CqeTukgTkT7yY2sNrgxJac4vYynC7qg4hEPjtYkRKXxtTr40eGrJ/FNfiilL06tzdDMxvHN +ukOF/KOU7z//B39W2BAKZW5kc3RyZWFtCmVuZG9iagoyMiAwIG9iagogICAyNjEzCmVuZG9iagoy +MyAwIG9iago8PCAvTGVuZ3RoIDI0IDAgUgogICAvRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJl +YW0KeJxdks9ugzAMxu88RY7doYLSgFcJVZq6Sw/7o3V7AEhMh7QGlNJD3352vqqTdoD8cL7PNonz +3f55H4bZ5O9xdAeeTT8EH/k8XqJj0/FxCNmqNH5w8+0rvd2pnbJczIfreebTPvRj1jQm/5DN8xyv +ZvHkx44fMmNM/hY9xyEczeJrd0DocJmmHz5xmE2RbbfGcy/pXtrptT2xyZN5ufeyP8zXpdj+FJ/X +iU2ZvldoyY2ez1PrOLbhyFlTFFvT9P024+D/7a0tLF3vvtuYNfVKpEUhi/AavFZmMAtXlFgWiVeI +V8JlkVgWiZeIl8rQ10m/AW+Ue7A01hDykOYheEm9hB5IeyALtsrIQ5rHojervRFqkdaqoKlSrQ61 +Ou0T+jL9C3JWmrN+hOZR2YGd5qyRs9Y49LXqLfQ26T3iXnij/ZfFKtWFt1YvQU+qJ5wV6VkRzpz0 +zC3YKlOLeJsu7nZDeoU6a/fZcJcYZSzSQKZ50EkYAt9ndhondaXnF1p7vskKZW5kc3RyZWFtCmVu +ZG9iagoyNCAwIG9iagogICAzODMKZW5kb2JqCjI1IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3Jp +cHRvcgogICAvRm9udE5hbWUgL0hRUUtGUCtTV0lGVERBWTNCb2xkSXRhbGljCiAgIC9Gb250RmFt +aWx5IChTV0lGVERBWTMpCiAgIC9GbGFncyA0CiAgIC9Gb250QkJveCBbIC0zNzIgLTExODMgMTYw +NCA5MzggXQogICAvSXRhbGljQW5nbGUgMAogICAvQXNjZW50IDc3MAogICAvRGVzY2VudCAtNDMw +CiAgIC9DYXBIZWlnaHQgOTM4CiAgIC9TdGVtViA4MAogICAvU3RlbUggODAKICAgL0ZvbnRGaWxl +MyAyMSAwIFIKPj4KZW5kb2JqCjcgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL1R5 +cGUxCiAgIC9CYXNlRm9udCAvSFFRS0ZQK1NXSUZUREFZM0JvbGRJdGFsaWMKICAgL0ZpcnN0Q2hh +ciAzMgogICAvTGFzdENoYXIgMTQ2CiAgIC9Gb250RGVzY3JpcHRvciAyNSAwIFIKICAgL0VuY29k +aW5nIC9XaW5BbnNpRW5jb2RpbmcKICAgL1dpZHRocyBbIDIyMCAwIDAgMCAwIDAgMCAwIDAgMCAw +IDAgMCAwIDI3MCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA2MzAgMCAwIDAg +MCAwIDAgNzE2IDAgMCAwIDAgMCA2NTYgMCAwIDAgMCAwIDU3NyAwIDAgODk5IDAgNTY4IDAgMCAw +IDAgMCAwIDAgNTIyIDUyMyA0MDkgNTMxIDQzNiAzNTAgNTExIDU0OSAzMDQgMCA1MjMgMjk1IDc4 +OCA1NDUgNDg3IDUyOSA1MTIgNDAyIDM2MiAzMjYgNTQ4IDQ3NSA2OTYgNDYyIDUxNSA0MDEgMCAw +IDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDE4OCBdCiAgICAvVG9V +bmljb2RlIDIzIDAgUgo+PgplbmRvYmoKMjYgMCBvYmoKPDwgL0xlbmd0aCAyNyAwIFIKICAgL0Zp +bHRlciAvRmxhdGVEZWNvZGUKICAgL1N1YnR5cGUgL0NJREZvbnRUeXBlMEMKPj4Kc3RyZWFtCnic +Y2RgYWFgZGQUCQ73dAtxcYw0dsrPSfEsSczJTGZgZGFgYGAEYqcf0j9kunnkfvAz/JBl/CHH9EOe ++YcIyx8Omd+JMmwuv+xY+3iUZIFqu3hUgBTDAR5VELWbH0SeEGzkYWJgZWRk49N28HRMyU9K9UxJ +zSvJLKl0zi+oLMpMzyhR0EjWVDAyMDTVUchOzSnLzNPR0YG7SAHkJAWIm+CCDAxMQKcxMoPdx8DM +wMzIqJS3p3vvD8O9jHv3fj+wl3mv2A+VH1P/qLDt/ZMs+sPw+4E/hux8u783f38u6l3aXf19Xvek +jaxm3yeJHp3j8Vvod0zAb2vDwIbz34W+xxz4bvVUftEO0d8Wv/l/x/4u+6263va7+HeL7/zfY7+X +fVdNv/lbXJ5v2fc7P5xFq/JY97eviOk25fitHGFo65Gy/aFc95tZt27s46hbwPpbPCpAqVuR4zfr +lahP3wW/G7z5HvKdwX6nhtys7/2iv03+yW1s/W7yS46VDxHaPKBQUwQF5wKhmlk/PLu/x07f0Mf2 +u6qbHSwj/KND5Eej6Bwerh4ePoZWRkYmZhZWNnYOTi5uHl4+fgFBIWERUTFxCUkpaRlZOXkFRSVl +FVU1dQ1NLW0dXT19A0MjYxNTM3MLSytrG1s7ewdHJ2cXVzd3D08vbx9fP/+AwKDgkNCw8IjIqOiY +2Lj4hMSk5JTUtPSMzKzsnNy8/ILCouKS0rLyisqq6prauvqGxqbmltY27sEAAJhry3MKZW5kc3Ry +ZWFtCmVuZG9iagoyNyAwIG9iagogICA1NjUKZW5kb2JqCjI4IDAgb2JqCjw8IC9MZW5ndGggMjkg +MCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nF2QwWrDMAyG734KHbtDcZJS +2CEYRnfJYetotgdwbDkzLLZRnEPefo5SOpjAht/6P/Fb8tK9dsFnkB8UTY8ZnA+WcI4LGYQBRx9E +3YD1Jt8V32bSScgC9+ucceqCi6JtQd5Kc860wuHFxgGfBADIK1kkH0Y4fF36/alfUvrBCUOGSigF +Fl0Z96bTu54QJMPHzpa+z+uxYH+OzzUhNKzrPZKJFuekDZIOI4q2KqWgdaWUwGD/9ZudGpz51sTu +urir0/CsWDWs7JnZu2ubsn35EdEsRCUd74VjbYF8wMfqUkwbxecX6UZzeQplbmRzdHJlYW0KZW5k +b2JqCjI5IDAgb2JqCiAgIDIzMwplbmRvYmoKMzAgMCBvYmoKPDwgL1R5cGUgL0ZvbnREZXNjcmlw +dG9yCiAgIC9Gb250TmFtZSAvUFdDREVaK1NXSUZUREFZM0JvbGRJdGFsaWMKICAgL0ZvbnRGYW1p +bHkgKFNXSUZUREFZMykKICAgL0ZsYWdzIDQKICAgL0ZvbnRCQm94IFsgLTM3MiAtMTE4MyAxNjA0 +IDkzOCBdCiAgIC9JdGFsaWNBbmdsZSAwCiAgIC9Bc2NlbnQgNzcwCiAgIC9EZXNjZW50IC00MzAK +ICAgL0NhcEhlaWdodCA5MzgKICAgL1N0ZW1WIDgwCiAgIC9TdGVtSCA4MAogICAvRm9udEZpbGUz +IDI2IDAgUgo+PgplbmRvYmoKMzEgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL0NJ +REZvbnRUeXBlMAogICAvQmFzZUZvbnQgL1BXQ0RFWitTV0lGVERBWTNCb2xkSXRhbGljCiAgIC9D +SURTeXN0ZW1JbmZvCiAgIDw8IC9SZWdpc3RyeSAoQWRvYmUpCiAgICAgIC9PcmRlcmluZyAoSWRl +bnRpdHkpCiAgICAgIC9TdXBwbGVtZW50IDAKICAgPj4KICAgL0ZvbnREZXNjcmlwdG9yIDMwIDAg +UgogICAvVyBbMCBbIDUwMCA1NDEgNTIwIF1dCj4+CmVuZG9iago4IDAgb2JqCjw8IC9UeXBlIC9G +b250CiAgIC9TdWJ0eXBlIC9UeXBlMAogICAvQmFzZUZvbnQgL1BXQ0RFWitTV0lGVERBWTNCb2xk +SXRhbGljCiAgIC9FbmNvZGluZyAvSWRlbnRpdHktSAogICAvRGVzY2VuZGFudEZvbnRzIFsgMzEg +MCBSXQogICAvVG9Vbmljb2RlIDI4IDAgUgo+PgplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFn +ZXMKICAgL0tpZHMgWyA5IDAgUiBdCiAgIC9Db3VudCAxCj4+CmVuZG9iagozMiAwIG9iago8PCAv +Q3JlYXRvciAoY2Fpcm8gMS4xMy4xIChodHRwOi8vY2Fpcm9ncmFwaGljcy5vcmcpKQogICAvUHJv +ZHVjZXIgKGNhaXJvIDEuMTMuMSAoaHR0cDovL2NhaXJvZ3JhcGhpY3Mub3JnKSkKPj4KZW5kb2Jq +CjMzIDAgb2JqCjw8IC9UeXBlIC9DYXRhbG9nCiAgIC9QYWdlcyAxIDAgUgo+PgplbmRvYmoKeHJl +ZgowIDM0CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAzNDYzNSAwMDAwMCBuIAowMDAwMDAzMjA5 +IDAwMDAwIG4gCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMzE4NiAwMDAwMCBuIAowMDAwMDE2 +MDg0IDAwMDAwIG4gCjAwMDAwMjg3MzUgMDAwMDAgbiAKMDAwMDAzMjM5NSAwMDAwMCBuIAowMDAw +MDM0NDY2IDAwMDAwIG4gCjAwMDAwMDMzNzUgMDAwMDAgbiAKMDAwMDAwMzU4OSAwMDAwMCBuIAow +MDAwMDE0NTE3IDAwMDAwIG4gCjAwMDAwMTQ1NDIgMDAwMDAgbiAKMDAwMDAxNTc5MCAwMDAwMCBu +IAowMDAwMDE1ODE0IDAwMDAwIG4gCjAwMDAwMTcxNzIgMDAwMDAgbiAKMDAwMDAyNjA3MCAwMDAw +MCBuIAowMDAwMDI2MDk0IDAwMDAwIG4gCjAwMDAwMjczMDggMDAwMDAgbiAKMDAwMDAyNzMzMiAw +MDAwMCBuIAowMDAwMDI3NjAyIDAwMDAwIG4gCjAwMDAwMjg4OTggMDAwMDAgbiAKMDAwMDAzMTYx +MCAwMDAwMCBuIAowMDAwMDMxNjM0IDAwMDAwIG4gCjAwMDAwMzIwOTYgMDAwMDAgbiAKMDAwMDAz +MjExOSAwMDAwMCBuIAowMDAwMDMyOTEzIDAwMDAwIG4gCjAwMDAwMzM1ODQgMDAwMDAgbiAKMDAw +MDAzMzYwNyAwMDAwMCBuIAowMDAwMDMzOTE5IDAwMDAwIG4gCjAwMDAwMzM5NDIgMDAwMDAgbiAK +MDAwMDAzNDIxOCAwMDAwMCBuIAowMDAwMDM0NzAwIDAwMDAwIG4gCjAwMDAwMzQ4MjggMDAwMDAg +biAKdHJhaWxlcgo8PCAvU2l6ZSAzNAogICAvUm9vdCAzMyAwIFIKICAgL0luZm8gMzIgMCBSCj4+ +CnN0YXJ0eHJlZgozNDg4MQolJUVPRgo= +--=_be6bc52b94449e229e651d02e56a25f1-- From c0618441a722c8414bc489500fb42ecdde01bbc6 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 5 Jan 2023 14:02:58 +0100 Subject: [PATCH 391/600] Convert all header keys to their lower case representation --- CHANGELOG.md | 1 + src/Header.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3004a165..28781f6c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Syntax updated to support php 8 features - Get the attachment file extension from the filename if no mimetype detection library is available - Prevent the structure parsing from parsing an empty part +- Convert all header keys to their lower case representation ### Added - Unit tests added #347 #242 (thanks @sergiy-petrov, @boekkooi-lengoo) diff --git a/src/Header.php b/src/Header.php index d4a20e12..e7ac1f4f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -216,7 +216,7 @@ public function rfc822_parse_headers($raw_headers): object { if (extension_loaded('imap') && $this->config["rfc822"]) { $raw_imap_headers = (array)\imap_rfc822_parse_headers($raw_headers); foreach ($raw_imap_headers as $key => $values) { - $key = str_replace("-", "_", $key); + $key = strtolower(str_replace("-", "_", $key)); $imap_headers[$key] = $values; } } @@ -249,7 +249,7 @@ public function rfc822_parse_headers($raw_headers): object { } else { if (($pos = strpos($line, ":")) > 0) { $key = trim(rtrim(strtolower(substr($line, 0, $pos)))); - $key = str_replace("-", "_", $key); + $key = strtolower(str_replace("-", "_", $key)); $value = trim(rtrim(substr($line, $pos + 1))); if (isset($headers[$key])) { From d45956126492c0da9fe42c17a49653a5a1de6a01 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 10 Jan 2023 10:40:11 +0100 Subject: [PATCH 392/600] Restructure the decode function (#355) * Restructure the decode function * simplified if-statement --- src/Header.php | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/Header.php b/src/Header.php index e7ac1f4f..77e3de11 100644 --- a/src/Header.php +++ b/src/Header.php @@ -435,26 +435,19 @@ private function decode(mixed $value): mixed { $decoder = $this->config['decoder']['message']; if ($value !== null) { - $is_utf8_base = $this->is_uft8($value); - if ($decoder === 'utf-8' && extension_loaded('imap')) { - $value = \imap_utf8($value); - $is_utf8_base = $this->is_uft8($value); - if ($is_utf8_base) { - $value = mb_decode_mimeheader($value); + $decoded_values = $this->mime_header_decode($value); + $tempValue = ""; + foreach ($decoded_values as $decoded_value) { + $tempValue .= $this->convertEncoding($decoded_value->text, $decoded_value->charset); } - if ($this->notDecoded($original_value, $value)) { - $decoded_value = $this->mime_header_decode($value); - if (count($decoded_value) > 0) { - if (property_exists($decoded_value[0], "text")) { - $value = $decoded_value[0]->text; - } - } + if ($tempValue) { + $value = $tempValue; + } else { + $value = \imap_utf8($value); } - } elseif ($decoder === 'iconv' && $is_utf8_base) { + } elseif ($decoder === 'iconv' && $this->is_uft8($value)) { $value = iconv_mime_decode($value); - } elseif ($is_utf8_base) { - $value = mb_decode_mimeheader($value); } if ($this->is_uft8($value)) { From f2731f09a49adf360b7f748603276824932ed404 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 10 Jan 2023 10:47:54 +0100 Subject: [PATCH 393/600] Local phpunit.xml removed and replaced by phpunit.xml.dist --- .gitignore | 3 ++- phpunit.xml => phpunit.xml.dist | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) rename phpunit.xml => phpunit.xml.dist (95%) diff --git a/.gitignore b/.gitignore index 21a1fd47..d1816ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.lock .idea /build/ test.php -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +phpunit.xml \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 95% rename from phpunit.xml rename to phpunit.xml.dist index e7cbae72..6caa954a 100644 --- a/phpunit.xml +++ b/phpunit.xml.dist @@ -13,6 +13,7 @@ tests + tests/issues From 9fcb46c7f1ab4ee3893b319509bd904edea0225d Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 10 Jan 2023 10:48:22 +0100 Subject: [PATCH 394/600] Merge changes added --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28781f6c..9f0c2a94 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Get the attachment file extension from the filename if no mimetype detection library is available - Prevent the structure parsing from parsing an empty part - Convert all header keys to their lower case representation +- Restructure the decode function #355 (thanks @istid) ### Added - Unit tests added #347 #242 (thanks @sergiy-petrov, @boekkooi-lengoo) From 7a76a3ca82b6ab94903948fbf90ef8cea123d471 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 10 Jan 2023 10:49:53 +0100 Subject: [PATCH 395/600] Test for pr #355 added --- tests/issues/Issue355Test.php | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/issues/Issue355Test.php diff --git a/tests/issues/Issue355Test.php b/tests/issues/Issue355Test.php new file mode 100644 index 00000000..365e2997 --- /dev/null +++ b/tests/issues/Issue355Test.php @@ -0,0 +1,39 @@ +cm = new ClientManager(); + } + + public function testIssue() { + $raw_header = "Subject: =?UTF-8?Q?Re=3A_Uppdaterat_=C3=A4rende_=28447899=29=2C_kostnader_f=C3=B6r_hj=C3=A4?= =?UTF-8?Q?lp_med_stadge=C3=A4ndring_enligt_ny_lagstiftning?=\r\n"; + + $header = new Header($raw_header); + $subject = $header->get("subject"); + + $this->assertEquals("Re: Uppdaterat ärende (447899), kostnader för hjälp med stadgeändring enligt ny lagstiftning", $subject->toString()); + } + +} \ No newline at end of file From 1c533ae4dd722ca4a16973e641e2120455bc48b5 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 10 Jan 2023 10:52:05 +0100 Subject: [PATCH 396/600] Unused ClientManager instance removed --- tests/issues/Issue355Test.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/issues/Issue355Test.php b/tests/issues/Issue355Test.php index 365e2997..a61cd07a 100644 --- a/tests/issues/Issue355Test.php +++ b/tests/issues/Issue355Test.php @@ -13,20 +13,10 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; -use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Header; class Issue355Test extends TestCase { - /** - * Setup the test environment. - * - * @return void - */ - public function setUp(): void { - $this->cm = new ClientManager(); - } - public function testIssue() { $raw_header = "Subject: =?UTF-8?Q?Re=3A_Uppdaterat_=C3=A4rende_=28447899=29=2C_kostnader_f=C3=B6r_hj=C3=A4?= =?UTF-8?Q?lp_med_stadge=C3=A4ndring_enligt_ny_lagstiftning?=\r\n"; From f018867e6ae459574fd204fd5fe17617bc77812d Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 17 Jan 2023 18:13:17 +0100 Subject: [PATCH 397/600] Test for issue #275 added --- tests/issues/Issue275Test.php | 28 +++++ tests/messages/issue-275.eml | 208 ++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 tests/issues/Issue275Test.php create mode 100644 tests/messages/issue-275.eml diff --git a/tests/issues/Issue275Test.php b/tests/issues/Issue275Test.php new file mode 100644 index 00000000..5d004254 --- /dev/null +++ b/tests/issues/Issue275Test.php @@ -0,0 +1,28 @@ +subject); + self::assertSame("testing123 this is a body", $message->getTextBody()); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-275.eml b/tests/messages/issue-275.eml new file mode 100644 index 00000000..642d6a42 --- /dev/null +++ b/tests/messages/issue-275.eml @@ -0,0 +1,208 @@ +Received: from FR0P281MB2649.DEUP281.PROD.OUTLOOK.COM (2603:10a6:d10:50::12) + by BEZP281MB2374.DEUP281.PROD.OUTLOOK.COM with HTTPS; Tue, 17 Jan 2023 + 09:25:19 +0000 +Received: from DB6P191CA0002.EURP191.PROD.OUTLOOK.COM (2603:10a6:6:28::12) by + FR0P281MB2649.DEUP281.PROD.OUTLOOK.COM (2603:10a6:d10:50::12) with Microsoft + SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id + 15.20.5986.23; Tue, 17 Jan 2023 09:25:18 +0000 +Received: from DB5EUR02FT011.eop-EUR02.prod.protection.outlook.com + (2603:10a6:6:28:cafe::3b) by DB6P191CA0002.outlook.office365.com + (2603:10a6:6:28::12) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6002.19 via Frontend + Transport; Tue, 17 Jan 2023 09:25:18 +0000 +Authentication-Results: spf=pass (sender IP is 80.241.56.171) + smtp.mailfrom=*****; dkim=pass (signature was verified) + header.d=jankoppe.de;dmarc=pass action=none + header.from=jankoppe.de;compauth=pass reason=100 +Received-SPF: Pass (protection.outlook.com: domain of **** designates + 80.241.56.171 as permitted sender) receiver=protection.outlook.com; + client-ip=80.241.56.171; helo=mout-p-201.mailbox.org; pr=C +Received: from **** + DB5EUR02FT011.mail.protection.outlook.com (10.13.58.70) with Microsoft SMTP + Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id + 15.20.6002.13 via Frontend Transport; Tue, 17 Jan 2023 09:25:18 +0000 +Received: from **** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P384) id 15.1.2375.34; Tue, 17 + Jan 2023 10:25:18 +0100 +Received: from ***** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.1.2375.34; Tue, 17 Jan + 2023 10:25:16 +0100 +Received: from **** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id 15.1.2375.34 via Frontend + Transport; Tue, 17 Jan 2023 10:25:16 +0100 +Received: from ***** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P521) id 15.1.2375.34; Tue, 17 + Jan 2023 10:25:18 +0100 +IronPort-SDR: qF6UeVYj8pb73gXrskSNOtfmUEgr2JLTtqbIK+/6ymuIu+hw8DzzyKrOGqm7zNPwG/4zr+ma5y + XERFv6Zyf/cxrWoVjWXswWrkCVjqQuHglLaONZt1Mg4okFzEByeEZsyg3/3n6kI4O+kgViBgLW + nNMzGlNLSqYBX+EDhOfE1GEWB/4iRbwDY32SnTr5BYqR0HwgHf+0M0E3b23/NqV6iYrF3KETah + cLtLRt/b5JY6f5KvmKoz0Y395r7MwryWR1eLKAU2j3w+ioNYBUw36K/hsIRojcquvQk9w3Qtfu + Yz1BUNwOEOwfxr0VjQScHirO +X-IRON-External-Mail-Tag: Extern +X-IronPort-MID: 60840111 +X-IPAS-Result: =?us-ascii?q?A0G0CQCSaMZjmKs48VBaglqEBz5Fk0mfZoFqEw8BAQEBA?= + =?us-ascii?q?QEBAQEJRAQBAQQDihcCJToEDQECBAEBAQEDAgMBAQEBBQEBAQQBAQECAQEGA?= + =?us-ascii?q?wEBAQIQAQEBAQEBAQEVCRkFDhAFLw1XDV0LgUQLgXQLAzENgjgiggQsgXYnA?= + =?us-ascii?q?QE4hE6DIwetVIEBgggBAQaCY5xECYFBi12ESYEhHD8BgU2EP4VPhXKaXIE7f?= + =?us-ascii?q?IEnDoFIgSk3A0QdQAMLbQpANRZKKxobB4EJKigVAwQEAwIGEwMiAg0oMRQEK?= + =?us-ascii?q?RMNJyZpCQIDImYDAwQoLQkgHwcmJDwHVj0CDx83BgMJAwIhToEgJAUDCxUqR?= + =?us-ascii?q?wQINgUGUhICCA8SDyxDDkI3NBMGgQYLDhEDUIFOBIENfwpXxUKgLYIqgVChG?= + =?us-ascii?q?oNmAZMikiGXS6gMgXwKFoFcTSQUgyJPAQIBAQENAQIBAQMBAgEBAQkBAQEBj?= + =?us-ascii?q?jaEDIosQzE7AgcLAQEDCYwjAQE?= +IronPort-PHdr: A9a23:3aIm3RBfYL78XRTcz0LxUyQUT0MY04WdBeb1wqQuh78GSKm/5ZOqZ + BWZua8wygaRAM6CsKgMotGVmp6jcFRI2YyGvnEGfc4EfD4+ouJSoTYdBtWYA1bwNv/gYn9yN + s1DUFh44yPzahANS47xaFLIv3K98yMZFAnhOgppPOT1HZPZg9iq2+yo9JDffQVFiCCgbb9uL + Bi6ohjdu8cIjYB/Nqs/1xzFr2dHdOhR2W5mP0+YkQzm5se38p5j8iBQtOwk+sVdT6j0fLk2Q + KJBAjg+PG87+MPktR/YTQuS/XQcSXkZkgBJAwfe8h73WIr6vzbguep83CmaOtD2TawxVD+/4 + apnVAPkhSEaPDM/7WrZiNF/jLhDrRyiuhJxw5Dab52aOvRwcazQZs8aSGhbU8pNSyBNHp+wY + o0SBOQBJ+ZYqIz9qkMKoxSkAwmnGebhyjhQhn/uw6IxzuMsERnB3Aw7A9IDq3bUo8/zNKcRV + uC11LHIwivZY/xLxzjw8Y7FeQ0urv+QR7x/a9bRyVUxGAPfiFWdsZDpMjKV2OkTvWWV7+RtW + OOhhmMmqAx8oDuiy8Uih4fGh48Y1FPJ+Th4zYs6J9C0VFJ3bN64HZZftiyXKoR7Tt4kTmp1u + yg60qULtJ2ncCQQ1pgqyAPTZ+aHfoWJ+B7vSeScLSp+iXl4YrywnQyy/lKlyuDkVsm7zlJKr + i1dn9nJsXANygDT5tGfSvdk4EutxSuD2xrW6u5eIEA0kbHUK5kuw7IqkZoTq0vDEjf3mEXwk + qCWal0p9+u05+j9fLnrqYKQO5V0hwz/KKgih86yDfkgPggLRWeb+OC81LP5/U3+RbVHluU2k + q7CsJDGPskbpLS2AwlW0oYk8xa/Fymp3M4FknYZNF5FfgmIgJDzO17SOPD4Eeu/g1O0nTt23 + /zGJKHuAo3RLnjfl7fsZbJ960lTyAoyy9BT/olUCqwZIPLrXU/xrsDYAwQiPAyp2eboFc9y2 + poQWWKIGK+YPrndsUWV6e41PuaDetxdhDGoL/8q5virlmIhgVgHYYGjwIEbYTW2Ge55Kl+VJ + 3bh0fkbFmJfnAM4BM/tkEWPGWpLYG2ud6A14DI8EJqrS4vOENP+yIed1Tu2S8UFLltNDUqBR + C+ASg== +IronPort-Data: A9a23:VhC+4aKGZF33Q9F0FE+RvpMlxSXFcZb7ZxGr2PjKsXjdYENS1zBVm + zYfDDuGOvjcMGT0etAkYN6x8kwD7MLQm9Q3G1ForCE8RH9jl5HIVI+TRqvSF3rJd5WcFiqLz + Cm/hv3odp1coqr0/E/F3oAMLhCQ7InQLlbGILes1htZGEk1F0/NtTo5w7Ri2tcx3oDga++wk + YqaT/P3aQfNNwFcbzp8B5Kr8HuDa9yr5Vv0FnRnDRx6lAe2e0s9VfrzFonoR5fMebS4K8bhL + wr15ODjpz2Bp3/BPfv++lrzWhVirrc/pmFigFIOM0SpqkAqSiDfTs/XnRfTAKtao2zhojx/9 + DlCnZWXSl0jF72Qo9YUcCEfFTpSP4Rt/KCSdBBTseTLp6HHW37r3ukrFARsZdRe/+92BWtJ5 + bofMj9lghKr17rwmu7iDLQywJ18daEHP6tH0p1k5SneFuoOQ5nFQKLS/dIe0DpYasVmQ66OO + 5JAMGMHgBLoWwRwMVsFObICo/qFn1bzdyRihGDOnP9ii4TU5Fcojeaxb4O9lsaxbcFSkUee4 + 3nb53z+GA0yPsGFxTPA/HW2mebVkWX3Veov+KaQ8/l3nBiLgzZLUVsTXFq/q/6pzEmkVLqzN + nD45AIniqto/mW7EuLPVj6A53ifkhw1cN5PRrhSBB629oLY5AOQB24hRzFHacA7uMJeeQHGx + mNljPu0X2E06uT9pWa1r+zI9WroUcQABTVaDRLoWzfp9PHPjenfZDrlQ8xnEajdYjbdQGmpm + mHiQMQWo7wVgYYh2r+//Favvt5Bjp3OUxJw/kCNBjvj6wp4YISid8qv81ezARd8wGSxEQXpU + JsswZL2AAUy4XelyHzlrAIlQejB2hp9GGeA6WOD5rF4n9hXx1atfJpL/BZ1L1pzP8APdFfBO + RGM4lMAv88JYiL6Ncebhr5d7ex0ncAM8vy6DpjpgiZmO/CdiSfcrH02DaJu9zmyziDAbp3Ty + b/AKJvyUSlDYUiW5Dq7RuEZ3L4tgzsj3XvUX4yzwBGt0dKjiI29Ft843K+1Rrlhtsus+VyNm + /4Gbpfi40gBDIXWP3eGmaZNdgpiBSZgWvjLRzl/K7TrzvxOQj9xUpc8ANoJJuRYokiivryZo + S/lAxEGmAGXaL+uAVziV02PoYjHBf5XxU/X9wR1Vbpx83R8M4up8okFcJ47Iesu+OB5lKcmT + fADeMKYGvkJRjmeo2YRapz0rYpDchW3hFvQYHP+PWViJsBtF17T59vpXgrz7y1SXCC5gs1v8 + bSv2zTSTYcHWwk/Xt3db+iizg3tsHVEwLByUkLEL8N9YkLp9IQ2eSX9guVuepMOIBPAwSOC2 + kCaDE5A9+XKpoY09vjPhLyF9tn2SrAjQxcDQWSCtOS4LyjX+Gan0LRsaufQcGCPTn7w9YWje + f5Rk6P2PsoBzQRDvIdLGrp2yb4zuon0rLhAwwU6QHjGYgj5Cr5kJXXaj8BDurcXnO1cvhaqH + 1rKoIEDf7CAOcfvF05XIxAqN7zR2fYRkzjUzPI0PESjunAup+faDBwMMknekjFZIZt0LJghn + bUrtvkQul62hRcdO9qbijxZqjaXJXsaXqR56pwXXN3xhgwwxg0QaJDQEHWsspSIdskJKgxwe + mbSgaPDg75b1gzFaXVqTSrB2u9UhJIvvhFWzQZceA3Sx4eY36E6jE9L7DA6bgVJ1REbgeh9D + W46ZUR6KJKH8ypsmMUeDXunHBtMBUPF90H8o7fTeLY1k6V1uqfxwKHR9ApDEI31M46RQ9SDw + Iyl9Q== +IronPort-HdrOrdr: A9a23:8y8NqqHssZLZ5JoIpLqE88eALOsnbusQ8zAXPidKKSC9E/b4qy + nKpp4mPHDP+VUssR0b9uxoW5PwI080l6QZ3WB5B97LNzUO01HFEGgN1+Xf/wE= +X-IronPort-Anti-Spam-Filtered: true +X-IronPort-AV: E=Sophos;i="5.97,222,1669071600"; + d="scan'208";a="60840111" +X-Amp-Result: SKIPPED(no attachment in message) +Received: from mout-p-201.mailbox.org ([80.241.56.171]) + by maila.burda.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 17 Jan 2023 10:25:16 +0100 +Received: from smtp1.mailbox.org (smtp1.mailbox.org [10.196.197.1]) + (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) + key-exchange ECDHE (P-384) server-signature RSA-PSS (4096 bits) server-digest SHA256) + (No client certificate requested) + by mout-p-201.mailbox.org (Postfix) with ESMTPS id 4Nx3QJ6GdJz9sQp + for <***>; Tue, 17 Jan 2023 10:25:12 +0100 (CET) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=****; s=MBO0001; + t=1673947512; + h=from:from:reply-to:subject:subject:date:date:message-id:message-id: + to:to:cc:mime-version:mime-version:content-type:content-type: + content-transfer-encoding:content-transfer-encoding; + bh=OMh+Pfp7SvIiDJjyXg53DevMPfaJRBhjQUkokwQIgDY=; + b=m3nM2KPbJdO7D+Vq/fMLLCXpkeDgvLs/JRzGTzWO4OoQhSaXLp76/vkBGPCU7BSGxir2Fu + g6e9Ggnrf0l8vL7rpvTgttta64wImaP9wOmJ0fOjzHcg/PX7sYjlUjyVyfThqZ7M5qg6/P + E9wItt4lQoUeQRVc6EoUlUaL7S+2R4P2WsQ6HCjulmpC3fmZdPOTXph/a1YfGvSfSj0pjp + LauH2n6EURKFmtMv8MbDcvTVKcq7o1bLGnK/RAjkLmRAORt+eC08IEAb5stVE6T6UPZ14Q + GUqrK2HEm5THS9vlH/11LwxwmnAdqUm8nl+Ymo3n1UNF9r8wkh8BUndGQFOQqQ== +Message-ID: <****> +Subject: Testing 123 +From: **** +To: *** +Content-Type: text/plain +Content-Transfer-Encoding: 7bit +Date: Tue, 17 Jan 2023 10:25:11 +0100 +Return-Path: *** +X-OrganizationHeadersPreserved: *** +X-MS-Exchange-Organization-ExpirationStartTime: 17 Jan 2023 09:25:18.2389 + (UTC) +X-MS-Exchange-Organization-ExpirationStartTimeReason: OriginalSubmit +X-MS-Exchange-Organization-ExpirationInterval: 1:00:00:00.0000000 +X-MS-Exchange-Organization-ExpirationIntervalReason: OriginalSubmit +X-MS-Exchange-Organization-Network-Message-Id: + ca3e116b-7c15-4efe-897e-08daf86cbfae +X-EOPAttributedMessage: 0 +X-MS-Exchange-Organization-MessageDirectionality: Originating +X-MS-Exchange-SkipListedInternetSender: + ip=[80.241.56.171];domain=mout-p-201.mailbox.org +X-MS-Exchange-ExternalOriginalInternetSender: + ip=[80.241.56.171];domain=mout-p-201.mailbox.org +X-CrossPremisesHeadersPromoted: + DB5EUR02FT011.eop-EUR02.prod.protection.outlook.com +X-CrossPremisesHeadersFiltered: + DB5EUR02FT011.eop-EUR02.prod.protection.outlook.com +X-MS-PublicTrafficType: Email +X-MS-TrafficTypeDiagnostic: DB5EUR02FT011:EE_|FR0P281MB2649:EE_ +X-MS-Exchange-Organization-AuthSource: **** +X-MS-Exchange-Organization-AuthAs: Anonymous +X-OriginatorOrg: *** +X-MS-Office365-Filtering-Correlation-Id: ca3e116b-7c15-4efe-897e-08daf86cbfae +X-MS-Exchange-Organization-SCL: 1 +X-Microsoft-Antispam: BCL:0; +X-Forefront-Antispam-Report: + CIP:193.26.100.144;CTRY:DE;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:mout-p-201.mailbox.org;PTR:mout-p-201.mailbox.org;CAT:NONE;SFS:(13230022)(4636009)(451199015)(6916009)(82310400005)(558084003)(2616005)(7116003)(6266002)(36005)(8676002)(86362001)(36756003)(5660300002)(1096003)(336012)(8936002)(156005)(7636003)(7596003);DIR:INB; +X-MS-Exchange-CrossTenant-OriginalArrivalTime: 17 Jan 2023 09:25:18.1452 + (UTC) +X-MS-Exchange-CrossTenant-Network-Message-Id: ca3e116b-7c15-4efe-897e-08daf86cbfae +X-MS-Exchange-CrossTenant-Id: 0a3106bb-aab1-42df-9639-39f349ecd2a0 +X-MS-Exchange-CrossTenant-OriginalAttributedTenantConnectingIp: TenantId=0a3106bb-aab1-42df-9639-39f349ecd2a0**** +X-MS-Exchange-CrossTenant-AuthSource: *** +X-MS-Exchange-CrossTenant-AuthAs: Anonymous +X-MS-Exchange-CrossTenant-FromEntityHeader: HybridOnPrem +X-MS-Exchange-Transport-CrossTenantHeadersStamped: FR0P281MB2649 +X-MS-Exchange-Transport-EndToEndLatency: 00:00:01.4839051 +X-MS-Exchange-Processed-By-BccFoldering: 15.20.6002.012 +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(930097); +X-Microsoft-Antispam-Message-Info: + =?iso-8859-1?Q?DN6oyY29jzFhRK1BGCOfg1yFnX4ACBaHBh9nbhnIoNZ4DGdi6zN+tIXnUR?= + =?iso-8859-1?Q?AY3F6SV1doOncexZBgqeE70YqvkB0xmo97NgnkQFb48xPvmZwoiAqkmIrS?= + =?iso-8859-1?Q?EbPesMMRL/pVX22Pde2C0KNThL0e02Li5ZCNvDxq+mEt0T9cww7HhoY6SS?= + =?iso-8859-1?Q?vMbDIBDA4c6Gyvb+34A6rYNFyHtZFVMas8S1KP4bV7QtI2WooRBzL1tOgM?= + =?iso-8859-1?Q?gyRVbzR69Mk++hTLhqTK7kBIkGMFhUC1McOMGMTOuUh9Ay7vMeY/oNGwOG?= + =?iso-8859-1?Q?WEFVsQjOz6lY4FIc3A+U5t3LMxkHtxRCql//ZhfKfjy78hPR7FCzYk70+T?= + =?iso-8859-1?Q?iCLlDZNF73KjvxH82FFKOersAl26L1kb2x5F/d1XJxAJe7BTG20/LXuMmx?= + =?iso-8859-1?Q?wGJs7s+C69LYa4cliwtGTMEuS6Rd7bOLihXnIy6hn8UhjVhNBv0+yGjint?= + =?iso-8859-1?Q?hbwBnQJ4Ny2jeS42bJYmKw+7Dnol+pphbvhjhtsDvKifo4Xx7xM45eV2s3?= + =?iso-8859-1?Q?wfKtSfeNGIR3Oi0VwreHTtsCOfvY8jbP+T2Z6KOFtQRvlUyutOFnOB2x1J?= + =?iso-8859-1?Q?BVgSblJzekltOB7gCAmZiCQnZuUMOtVVqRfURwUhOuQ47I/2LDJzi/NQnf?= + =?iso-8859-1?Q?XiqeHojof9SfngGas5TNx9tuQxmFqw4AWJE7iy4hxJo2ehSt4ReEvPzyt9?= + =?iso-8859-1?Q?1DTwLNYbyZ8Ub7Uq3EtUDE5vn3jY2thuVBo8eemdrjOvsx+hdH9RxcFwYz?= + =?iso-8859-1?Q?qU8NIflZ0TMRHTT2M2aBdzB9zsR3Kda+I8tNi7T6DCWve+MEEmLeRa4lU6?= + =?iso-8859-1?Q?XAFgPATiTR9BGrBBrwqKLye2Pnn6Bx8Hs+R7sFsv6HnQkS8B585+mjXV0A?= + =?iso-8859-1?Q?jXMm+5KDT/IUTiTAeJPwBTNEAG6Yo8gDg+6/9EMuZtQYAYZnM4tMefCwBm?= + =?iso-8859-1?Q?oV/M1vLbCGr7m1BDeRerH34C/2SWa0XnU9zOzqvm6/ery2Cv81aakjQr+S?= + =?iso-8859-1?Q?FdFwyYhZi96v0+HHAeT1Ncyrb84GZVp0kb1VYOBI0JH7TTZy88PMtFfbXQ?= + =?iso-8859-1?Q?nrSZ/VgrVa7hCvBOf2VQnj6iMnNqaJlmnI+E/yXvDNQJn2JDhor7QrClSA?= + =?iso-8859-1?Q?rrCXMDZyWWZhhDrf+VkN7ePKkju46/dqh0NRjqz6571xFdftnXx/b9dli1?= + =?iso-8859-1?Q?+XoZOiqbV8FyrJNCWDLuZRc1gIr0KVbbxN9nggMGa5c8tslnJvW/OTImm7?= + =?iso-8859-1?Q?SEKOX/bZx/aOe9bLlgzQhqdtwBkOJurGSWeDajB/ko2pFUcIYWyMaB6dFW?= + =?iso-8859-1?Q?RcQgdjWDgNNqiUnyvY585ZCFJCfiMaWj8hglJWADRQdLHNSqFAtuZdVCc1?= + =?iso-8859-1?Q?HPXeZDkLqPP+0VfD8EO3A1Fmp+E3kIrykjPJtNslbzwmtL3ATsC8e2mcob?= + =?iso-8859-1?Q?NGBKDmktRjDYG3mhya2XCCiFWaYiig6J/s1ePtuRp3TbBojEAUg2E5xItx?= + =?iso-8859-1?Q?gRGc6bNfg9Rihie4+BZSN+l+ynuYJQ/FGK3CvQYA9gXp00ZtfPwlHx7mXA?= + =?iso-8859-1?Q?gaDydNv1CyuYCOgUeTxo4Bi7N4Rrz72tn9pYxoZ7okujjzLWuiK7etRoRz?= + =?iso-8859-1?Q?JwOHGQklwT4VgMwwLQdXxNR+WMLbOyXjt6bfrKSUHq34oLx6ZVk2F6r9HI?= + =?iso-8859-1?Q?5uaCMLO8dLt3O9rb8FdV3EfLGg+zThKobHlrVJ2WFZLm0xtSsbzC04WLdR?= + =?iso-8859-1?Q?Oqf0F2GVBbFjulBFs9mvdhDXD33oUn5atsgLnkAitzWZH8qgeTOKgCbZD4?= + =?iso-8859-1?Q?WIXnz+DQx0g6sgmqGwa5fkPzRn+NfdGnwNwRyjXcAw7jW5DVkAnzd2OkEc?= + =?iso-8859-1?Q?WrVWN1eIbPedT77yCWDwLAjGBWSrpmI1bZkqI=3D?= +MIME-Version: 1.0 + +Asdf testing123 this is a body \ No newline at end of file From 5cfd7aeaf7366641f4299ee13056e5acdf3ef3d7 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 18 Jan 2023 19:21:03 +0100 Subject: [PATCH 398/600] Additional test for issue #275 added --- tests/issues/Issue275Test.php | 12 +++- tests/messages/issue-275-2.eml | 123 +++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 tests/messages/issue-275-2.eml diff --git a/tests/issues/Issue275Test.php b/tests/issues/Issue275Test.php index 5d004254..3cb5c5de 100644 --- a/tests/issues/Issue275Test.php +++ b/tests/issues/Issue275Test.php @@ -17,7 +17,7 @@ class Issue275Test extends TestCase { - public function testIssue() { + public function testIssueEmail1() { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-275.eml"]); $message = Message::fromFile($filename); @@ -25,4 +25,14 @@ public function testIssue() { self::assertSame("testing123 this is a body", $message->getTextBody()); } + public function testIssueEmail2() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-275-2.eml"]); + $message = Message::fromFile($filename); + + $body = "Test\r\n\r\nMed venlig hilsen\r\nMartin Larsen\r\nFeline Holidays A/S\r\nTlf 78 77 04 12"; + + self::assertSame("Test 1017", (string)$message->subject); + self::assertSame($body, $message->getTextBody()); + } + } \ No newline at end of file diff --git a/tests/messages/issue-275-2.eml b/tests/messages/issue-275-2.eml new file mode 100644 index 00000000..d80e66ed --- /dev/null +++ b/tests/messages/issue-275-2.eml @@ -0,0 +1,123 @@ +Received: from AS4PR02MB8234.eurprd02.prod.outlook.com (2603:10a6:20b:4f1::17) + by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Wed, 18 Jan 2023 + 09:17:10 +0000 +Received: from AS1PR02MB7917.eurprd02.prod.outlook.com (2603:10a6:20b:4a5::5) + by AS4PR02MB8234.eurprd02.prod.outlook.com (2603:10a6:20b:4f1::17) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5986.19; Wed, 18 Jan + 2023 09:17:09 +0000 +Received: from AS1PR02MB7917.eurprd02.prod.outlook.com + ([fe80::4871:bdde:a499:c0d9]) by AS1PR02MB7917.eurprd02.prod.outlook.com + ([fe80::4871:bdde:a499:c0d9%9]) with mapi id 15.20.5986.023; Wed, 18 Jan 2023 + 09:17:09 +0000 +From: =?iso-8859-1?Q?Martin_B=FClow_Larsen?= +To: Cofman Drift +Subject: Test 1017 +Thread-Topic: Test 1017 +Thread-Index: AdkrHaKsy+bUiL/eTIS5W3AP+zzLjQ== +Date: Wed, 18 Jan 2023 09:17:09 +0000 +Message-ID: + +Accept-Language: da-DK, en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Internal +X-MS-Exchange-Organization-AuthMechanism: 04 +X-MS-Exchange-Organization-AuthSource: AS1PR02MB7917.eurprd02.prod.outlook.com +X-MS-Has-Attach: +X-MS-Exchange-Organization-Network-Message-Id: + 98cea1c9-a497-454a-6606-08daf934c6c4 +X-MS-Exchange-Organization-SCL: -1 +X-MS-TNEF-Correlator: +X-MS-Exchange-Organization-RecordReviewCfmType: 0 +x-ms-publictraffictype: Email +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097); +X-Microsoft-Antispam-Message-Info: + BzqL6hvPyQW0lSkWGop6vQlsIZK48umY74vuKlNgF0pb/H659W+0fuTB+6guqGM0oof00mlzu3gn1pu1R5pUOE2Fb58OqnBEFkB30vVrG6TNsG/6KBtecXkP3FptqO/WRmsxCQx7bK7J2VyS2SbOibqX8mDZhkTrwP1+IA0R9eD0/NvoMqX9GssewUDxSAbaaKdADCuU1dG7qpF8M9tfLDJz+dUL5qZoO+oGINGLLdo2y6Z/F+h3UWv7BXiS4BJKc+jwAng26BUMKmg2VVRdMvc+LbZTovUr9hyEq1orS9fOg1iIV6sPcyIVl3NIEy5bHMYh1d6sUCqvTO1UPSdf7lIvKxSszyKptIPNgioOvFpF9tTHDyKU5p1IiLm5FxW/+kGdPq6ZoTIZVriJoyjx6gWKpPY3vHN6bHUK9mA+LspIYAeuDFFvwkZx2b2Rtw3S99S7zz3eBqv3xlGqJixx/apl4Af7ZaxKFpMj9XZXAQVIv9BA0tIA+1nLByt4dPW4Xzoj3KcBbX5K/HkuR/30Lrq7gRQQDyNYgf5S/MO2MLJqcvnVFPXgVubK6XFu5quDibsZjPjxOUfBTJkJ/n4gB8Z8/TOM0oKD76hszXXoaWd9leUeQ1x88oy+QuXPRxzuLzVec3GiPNMYA42QvvTiWmrrhdceRzhV0J7pJBbi10ik+hXqSeNkldgktd5cWPss5F74yxAaEaPJO9I7MOUpE0XzlRfljPptykrIQt8SARMllykzJNrDt8VAl37ArEZbWxFLm3RuypOI0zgCZMRLf5JeElpCv1ay4wilz4vsYGr4fs3KUQzI1YY43uaDxNMz8k7dH/UkC9Dfg1oyHlNs+w== +Content-Type: multipart/alternative; + boundary="_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_" +MIME-Version: 1.0 + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Test + +Med venlig hilsen +Martin Larsen +Feline Holidays A/S +Tlf 78 77 04 12 + + + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

Test

+

 

+

= +Med venlig hilsen

+

Martin Larsen

+

Feline Holidays A/S

+

Tlf 78 77 04 12

+

 

+

 

+
+ + + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_-- From bbc52543239dd681bb7e9ff5637c9b80e85b593e Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 18 Jan 2023 19:36:54 +0100 Subject: [PATCH 399/600] Release information added --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0c2a94..03d5f8dd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.0.0] - 2023-01-18 +### Fixed - The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 (thanks @szymekjanaczek) - Fix undefined attachment name when headers use "filename*=" format #301 (thanks @JulienChavee) - Fixed `ImapProtocol::logout` always throws 'not connected' Exception after upgraded to 4.1.2 #351 From 3facf95fe88134bc27db672ba6a7dbf1f8b35d05 Mon Sep 17 00:00:00 2001 From: Axel Guckelsberger Date: Fri, 20 Jan 2023 16:01:32 +0100 Subject: [PATCH 400/600] more unique ID generation to prevent multiple attachments with same ID (#363) --- src/Attachment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index ff280b1b..c6afe30f 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -202,7 +202,7 @@ protected function fetch(): void { if (($id = $this->part->id) !== null) { $this->id = str_replace(['<', '>'], '', $id); }else{ - $this->id = hash("sha256", (string)microtime(true)); + $this->id = hash("sha256", uniqid((string) rand(10000, 99999), true)); } $this->size = $this->part->bytes; @@ -339,4 +339,4 @@ public function mask(string $mask = null): mixed { throw new MaskNotFoundException("Unknown mask provided: ".$mask); } -} \ No newline at end of file +} From 3addefa1dda8d1a45b0b189a4ab124dff09cbd11 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 20 Jan 2023 16:02:47 +0100 Subject: [PATCH 401/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d5f8dd..ef170a34 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) ### Added - NaN From 46ef2a641ddd906b9eddaea8c796c4c9ab3abaa4 Mon Sep 17 00:00:00 2001 From: Adrian Kuriata Date: Mon, 13 Feb 2023 19:09:31 +0100 Subject: [PATCH 402/600] Fix for saving all attachments from them messages. (#372) There was a problem with fetch all attachments. Some attachments are not added to the collection. Checking attachment ID before add it to collection solved the problem in my case. --- src/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Message.php b/src/Message.php index c3eabb0a..568f2a1e 100755 --- a/src/Message.php +++ b/src/Message.php @@ -701,7 +701,7 @@ protected function fetchAttachment(Part $part): void { $oAttachment = new Attachment($this, $part); if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { - if ($oAttachment->getId() !== null) { + if ($oAttachment->getId() !== null && $this->attachments->offsetExists($oAttachment->getId())) { $this->attachments->put($oAttachment->getId(), $oAttachment); } else { $this->attachments->push($oAttachment); From 4a6228fbcbe236f7d4feb1015a742d046bfc5ac0 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 1 Mar 2023 02:08:47 +0100 Subject: [PATCH 403/600] Partial fix for #362 (allow search response to be empty) --- CHANGELOG.md | 2 ++ src/Connection/Protocols/ImapProtocol.php | 3 +-- src/Connection/Protocols/LegacyProtocol.php | 1 + src/Connection/Protocols/Response.php | 13 ++++++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef170a34..01f3af17 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) +- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) +- Partial fix for #362 (allow search response to be empty) ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index e8cd71d8..cc9cf46d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1196,12 +1196,11 @@ public function done(): bool { */ public function search(array $params, int|string $uid = IMAP::ST_UID): Response { $command = $this->buildUIDCommand("SEARCH", $uid); - $response = $this->requestAndResponse($command, $params); + $response = $this->requestAndResponse($command, $params)->setCanBeEmpty(true); foreach ($response->data() as $ids) { if ($ids[0] === 'SEARCH') { array_shift($ids); - return $response->setResult($ids); } } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index e8323372..de2ddb60 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -697,6 +697,7 @@ public function done() { */ public function search(array $params, int|string $uid = IMAP::ST_UID): Response { return $this->response("imap_search")->wrap(function($response)use($params, $uid){ + $response->setCanBeEmpty(true); $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); return $result ?: []; }); diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php index 6f9a2753..068f082f 100644 --- a/src/Connection/Protocols/Response.php +++ b/src/Connection/Protocols/Response.php @@ -64,6 +64,8 @@ class Response { */ protected bool $debug = false; + protected bool $can_be_empty = false; + /** * Create a new Response instance */ @@ -328,7 +330,7 @@ public function successful(): bool { return false; } } - return $this->boolean() && !$this->getErrors(); + return ($this->boolean() || $this->canBeEmpty()) && !$this->getErrors(); } @@ -369,4 +371,13 @@ public function failed(): bool { public function Noun(): int { return $this->noun; } + + public function setCanBeEmpty(bool $can_be_empty): Response { + $this->can_be_empty = $can_be_empty; + return $this; + } + + public function canBeEmpty(): bool { + return $this->can_be_empty; + } } \ No newline at end of file From 9f56ebf0dd4808cd5893f88cd45fd3d2bd0b207c Mon Sep 17 00:00:00 2001 From: Santiago H <52068520+shuergab@users.noreply.github.com> Date: Wed, 1 Mar 2023 02:13:45 +0100 Subject: [PATCH 404/600] Fixed issue #354 (#366) Fixed the issue #354 by casting the key to string in the switch. Tested on over 100000 emails and over 100 accounts without any problem. --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 77e3de11..d4a0117f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -265,7 +265,7 @@ public function rfc822_parse_headers($raw_headers): object { foreach ($headers as $key => $values) { if (isset($imap_headers[$key])) continue; $value = null; - switch ($key) { + switch ((string)$key) { case 'from': case 'to': case 'cc': From e108c27f3862356e687d63565f2e23618b170b36 Mon Sep 17 00:00:00 2001 From: Gioele Luchetti Date: Wed, 1 Mar 2023 02:18:19 +0100 Subject: [PATCH 405/600] Fix use of ST_MSGN as sequence method (#356) --- src/Connection/Protocols/ImapProtocol.php | 12 ++++++------ src/Connection/Protocols/LegacyProtocol.php | 2 +- src/Folder.php | 2 +- src/Message.php | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cc9cf46d..737f83ef 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -644,7 +644,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in $data = []; // find array key of UID value; try the last elements, or search for it - if ($uid) { + if ($uid === IMAP::ST_UID) { $count = count($tokens[2]); if ($tokens[2][$count - 2] == 'UID') { $uidKey = $count - 1; @@ -661,7 +661,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } // ignore other messages - if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { + if ($to === null && !is_array($from) && ($uid === IMAP::ST_UID ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { continue; } @@ -669,7 +669,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in if (count($items) == 1) { if ($tokens[2][0] == $items[0]) { $data = $tokens[2][1]; - } elseif ($uid && $tokens[2][2] == $items[0]) { + } elseif ($uid === IMAP::ST_UID && $tokens[2][2] == $items[0]) { $data = $tokens[2][3]; } else { $expectedResponse = 0; @@ -696,12 +696,12 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } // if we want only one message we can ignore everything else and just return - if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { + if ($to === null && !is_array($from) && ($uid === IMAP::ST_UID ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { // we still need to read all lines while (!$this->readLine($response, $tokens, $tag)) return $response->setResult($data); } - if ($uid) { + if ($uid === IMAP::ST_UID) { $result[$tokens[2][$uidKey]] = $data; } else { $result[$tokens[0]] = $data; @@ -1226,7 +1226,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp $response = $this->getUid(); $ids = []; foreach ($response->data() as $msgn => $v) { - $id = $uid ? $v : $msgn; + $id = $uid === IMAP::ST_UID ? $v : $msgn; if (($to >= $id && $from <= $id) || ($to === "*" && $from <= $id)) { $ids[] = $id; } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index de2ddb60..b515d255 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -253,7 +253,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { $response->addCommand("imap_fetchbody"); - $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::ST_UID : IMAP::NIL); + $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid === IMAP::ST_UID ? IMAP::ST_UID : IMAP::NIL); } return $result; diff --git a/src/Folder.php b/src/Folder.php index af15476d..99b1e7bf 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -295,7 +295,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN) == IMAP::ST_UID; + $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } diff --git a/src/Message.php b/src/Message.php index 568f2a1e..af2a23da 100755 --- a/src/Message.php +++ b/src/Message.php @@ -495,7 +495,7 @@ public function getHTMLBody(): string { */ private function parseHeader(): void { $sequence_id = $this->getSequenceId(); - $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID)->validatedData(); + $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->validatedData(); if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } @@ -548,7 +548,7 @@ private function parseFlags(): void { $sequence_id = $this->getSequenceId(); try { - $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID)->validatedData(); + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be fetched", 0, $e); } @@ -578,7 +578,7 @@ public function parseBody(): Message { $sequence_id = $this->getSequenceId(); try { - $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID)->validatedData(); + $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageContentFetchingException("failed to fetch content", 0); } @@ -1008,7 +1008,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID)->validatedData()) { + if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { return $this->fetchNewMail($folder, $next_uid, "copied", $expunge); } } @@ -1047,7 +1047,7 @@ public function move(string $folder_path, bool $expunge = false): ?Message { $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID)->validatedData()) { + if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { return $this->fetchNewMail($folder, $next_uid, "moved", $expunge); } } @@ -1172,7 +1172,7 @@ public function setFlag(array|string $flag): bool { $flag = "\\" . trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID)->validatedData(); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be set", 0, $e); } @@ -1204,7 +1204,7 @@ public function unsetFlag(array|string $flag): bool { $flag = "\\" . trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID)->validatedData(); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be removed", 0, $e); } From 56c2c5121e094489ac3b8e3fa555cc0d039a794b Mon Sep 17 00:00:00 2001 From: Daniel Castilla Date: Wed, 1 Mar 2023 02:21:48 +0100 Subject: [PATCH 406/600] Prevent infinite loop in ImapProtocol (#316) * Prevent infinite loop in ImapProtocol * Fix PHP TypeError --- src/Connection/Protocols/ImapProtocol.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 737f83ef..fc23c56f 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -112,7 +112,7 @@ protected function enableStartTls() { */ public function nextLine(Response $response): string { $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { + while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { $line .= $next_char; } if ($line === "" && $next_char === false) { @@ -147,7 +147,7 @@ protected function nextTaggedLine(Response $response, ?string &$tag): string { $line = $this->nextLine($response); list($tag, $line) = explode(' ', $line, 2); - return $line; + return $line ?? ''; } /** From 27446461b4340c459106e5b0950ba41d3d89b8b6 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 1 Mar 2023 02:24:19 +0100 Subject: [PATCH 407/600] Changelog updated --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f3af17..a5c2543f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) - Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) - Partial fix for #362 (allow search response to be empty) +- Unsafe usage of switch case. #354 #366 (thanks @shuergab) +- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) +- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) ### Added - NaN From 4fdf19bab8271940b85d31419ffcfd4359dd385d Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 1 Mar 2023 02:26:21 +0100 Subject: [PATCH 408/600] Release information added --- CHANGELOG.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c2543f..b5035414 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) -- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) -- Partial fix for #362 (allow search response to be empty) -- Unsafe usage of switch case. #354 #366 (thanks @shuergab) -- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) -- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) +- NaN ### Added - NaN @@ -20,6 +15,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [5.0.1] - 2023-03-01 +### Fixed +- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) +- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) +- Partial fix for #362 (allow search response to be empty) +- Unsafe usage of switch case. #354 #366 (thanks @shuergab) +- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) +- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) + + ## [5.0.0] - 2023-01-18 ### Fixed - The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 (thanks @szymekjanaczek) From 72f0868b4a1decafb168cf3ffd452bf5047eadab Mon Sep 17 00:00:00 2001 From: didi1357 Date: Wed, 1 Mar 2023 23:10:21 +0100 Subject: [PATCH 409/600] Add sizes() methods to protocol interface and implmentations (#379) * Add retrieval of message size using FETCH RFC822.SIZE * Replace misleading while with if * Only fetch size if explicitly requested --- CHANGELOG.md | 2 +- examples/size_test.php | 83 +++++++++++++++++++ src/Connection/Protocols/ImapProtocol.php | 15 +++- src/Connection/Protocols/LegacyProtocol.php | 28 +++++++ .../Protocols/ProtocolInterface.php | 11 +++ .../MessageSizeFetchingException.php | 24 ++++++ src/Message.php | 30 ++++++- 7 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 examples/size_test.php create mode 100644 src/Exceptions/MessageSizeFetchingException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b5035414..62ea0d92 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- sizes() methods to protocol interface and implmentations (to have FETCH NNN RFC822.SIZE attribute available) ### Breaking changes - NaN diff --git a/examples/size_test.php b/examples/size_test.php new file mode 100644 index 00000000..44f832c5 --- /dev/null +++ b/examples/size_test.php @@ -0,0 +1,83 @@ + 'toFill', + 'port' => 993, + 'encryption' => 'ssl', + 'validate_cert' => true, + 'username' => 'toFill', + 'password' => 'toFill', + 'protocol' => 'imap' +); +set_include_path('/home/didi1357/git/php-imap/src'); + + +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); + +require_once 'IMAP.php'; +require_once 'Traits/HasEvents.php'; +require_once 'Exceptions/MaskNotFoundException.php'; +require_once 'Client.php'; +require_once 'ClientManager.php'; +require_once 'Support/Masks/Mask.php'; +require_once 'Support/Masks/MessageMask.php'; +require_once 'Support/Masks/AttachmentMask.php'; +require_once 'Connection/Protocols/Response.php'; +require_once 'Connection/Protocols/ProtocolInterface.php'; +require_once 'Connection/Protocols/Protocol.php'; +require_once 'Connection/Protocols/ImapProtocol.php'; +require_once '../tests/vendor/autoload.php'; +require_once 'Support/PaginatedCollection.php'; +require_once 'Support/FolderCollection.php'; +require_once 'Folder.php'; +require_once 'Exceptions/ResponseException.php'; +require_once 'Query/Query.php'; +require_once 'Query/WhereQuery.php'; +require_once 'Support/MessageCollection.php'; +require_once 'Support/FlagCollection.php'; +require_once 'Support/AttachmentCollection.php'; +require_once 'Part.php'; +require_once 'Structure.php'; +require_once 'Attribute.php'; +require_once 'Address.php'; +require_once 'EncodingAliases.php'; +require_once 'Header.php'; +require_once 'Message.php'; + +use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Support\Masks\MessageMask; +use Webklex\PHPIMAP\Support\Masks\AttachmentMask; + +$CONFIG['masks'] = array( + 'message' => MessageMask::class, + 'attachment' => AttachmentMask::class +); +echo "
";
+$cm = new ClientManager($options = []);
+$client = $cm->make($CONFIG)->connect();
+//$client->getConnection()->enableDebug(); // uncomment this for debug output!
+$folder = $client->getFolderByPath('INBOX');
+//$message = $folder->messages()->getMessageByMsgn(1); // did not work as msgn implementation is currently broken!
+$message = $folder->messages()->getMessageByUid(2);
+var_dump($message->getSize());
+
+
+?>
+
diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index fc23c56f..07ea3f6d 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -698,7 +698,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in
             // if we want only one message we can ignore everything else and just return
             if ($to === null && !is_array($from) && ($uid === IMAP::ST_UID ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) {
                 // we still need to read all lines
-                while (!$this->readLine($response, $tokens, $tag))
+                if (!$this->readLine($response, $tokens, $tag))
                     return $response->setResult($data);
             }
             if ($uid === IMAP::ST_UID) {
@@ -756,6 +756,19 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response
         return $this->fetch(["FLAGS"], $uids, null, $uid);
     }
 
+    /**
+     * Fetch message sizes
+     * @param int|array $uids
+     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
+     * message numbers instead.
+     *
+     * @return Response
+     * @throws RuntimeException
+     */
+    public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response {
+        return $this->fetch(["RFC822.SIZE"], $uids, null, $uid);
+    }
+
     /**
      * Get uid for a given id
      * @param int|null $id message number
diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php
index b515d255..65c5342d 100644
--- a/src/Connection/Protocols/LegacyProtocol.php
+++ b/src/Connection/Protocols/LegacyProtocol.php
@@ -314,6 +314,34 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response
             return $result;
         });
     }
+    
+    /**
+     * Fetch message sizes
+     * @param int|array $uids
+     * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
+     *
+     * @return Response
+     */
+    public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response {
+        return $this->response()->wrap(function($response)use($uids, $uid){
+            /** @var Response $response */
+            $result = [];
+            $uids = is_array($uids) ? $uids : [$uids];
+            $uid_text = implode("','",$uids);
+            $response->addCommand("imap_fetch_overview");
+            $raw_overview = false;
+            if ($uid == IMAP::ST_UID)
+              $raw_overview = imap_fetch_overview($this->stream, $uid_text, FT_UID);
+            else
+              $raw_overview = imap_fetch_overview($this->stream, $uid_text);
+            if ($raw_overview !== false) {
+              foreach ($raw_overview as $overview_element) {
+                $result[$overview_element[$uid == IMAP::ST_UID ? 'uid': 'msgno']] = $overview_element['size'];
+              }
+            }
+            return $result;
+        });
+    }
 
     /**
      * Get uid for a given id
diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php
index 0093e513..c02d7800 100644
--- a/src/Connection/Protocols/ProtocolInterface.php
+++ b/src/Connection/Protocols/ProtocolInterface.php
@@ -151,6 +151,17 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid
      * @throws RuntimeException
      */
     public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response;
+    
+    /**
+     * Fetch message sizes
+     * @param int|array $uids
+     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
+     * message numbers instead.
+     *
+     * @return Response
+     * @throws RuntimeException
+     */
+    public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response;
 
     /**
      * Get uid for a given id
diff --git a/src/Exceptions/MessageSizeFetchingException.php b/src/Exceptions/MessageSizeFetchingException.php
new file mode 100644
index 00000000..f14fce51
--- /dev/null
+++ b/src/Exceptions/MessageSizeFetchingException.php
@@ -0,0 +1,24 @@
+attributes[$name] = $this->client->getConnection()->getMessageNumber($this->uid)->validate()->integer();
                 return $this->attributes[$name];
+            case "size":
+                if (!isset($this->attributes[$name])) {
+                    $this->fetchSize();
+                }
+                return $this->attributes[$name];
         }
 
         return $this->header->get($name);
@@ -592,6 +597,20 @@ public function parseBody(): Message {
 
         return $body;
     }
+    
+    /**
+     * Fetches the size for this message.
+     * 
+     * @throws MessageSizeFetchingException
+     */
+    private function fetchSize(): void {
+        $sequence_id = $this->getSequenceId();
+        $sizes = $this->client->getConnection()->sizes([$sequence_id], $this->sequence)->validatedData();
+         if (!isset($sizes[$sequence_id])) {
+            throw new MessageSizeFetchingException("sizes did not set an array entry for the supplied sequence_id", 0);
+        }
+        $this->attributes["size"] = $sizes[$sequence_id];
+    }
 
     /**
      * Handle auto "Seen" flag handling
@@ -1301,6 +1320,15 @@ public function getHeader(): ?Header {
         return $this->header;
     }
 
+    /**
+     * Get the message size in bytes
+     *
+     * @return int Size of the message in bytes
+     */
+    public function getSize(): int {
+        return $this->get("size");
+    }
+
     /**
      * Get the current client
      *
@@ -1565,7 +1593,7 @@ public function getSequence(): int {
     }
 
     /**
-     * Set the sequence type
+     * Get the current sequence id (either a UID or a message number!)
      *
      * @return int
      */

From 759c65b5d0ad02491d1bacc83507d5e18624d439 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:46:27 +0100
Subject: [PATCH 410/600] Changelog updated

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62ea0d92..3d81348a 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 - NaN
 
 ### Added
-- sizes() methods to protocol interface and implmentations (to have FETCH NNN RFC822.SIZE attribute available)
+- `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
 
 ### Breaking changes
 - NaN

From 377214ec064c358505335c02a623932d06085faa Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:46:54 +0100
Subject: [PATCH 411/600] Code formatted

---
 src/Connection/Protocols/LegacyProtocol.php |  4 ++--
 src/Message.php                             | 25 ++++++++++++---------
 2 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php
index 65c5342d..6eedddef 100644
--- a/src/Connection/Protocols/LegacyProtocol.php
+++ b/src/Connection/Protocols/LegacyProtocol.php
@@ -331,9 +331,9 @@ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response
             $response->addCommand("imap_fetch_overview");
             $raw_overview = false;
             if ($uid == IMAP::ST_UID)
-              $raw_overview = imap_fetch_overview($this->stream, $uid_text, FT_UID);
+              $raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID);
             else
-              $raw_overview = imap_fetch_overview($this->stream, $uid_text);
+              $raw_overview = \imap_fetch_overview($this->stream, $uid_text);
             if ($raw_overview !== false) {
               foreach ($raw_overview as $overview_element) {
                 $result[$overview_element[$uid == IMAP::ST_UID ? 'uid': 'msgno']] = $overview_element['size'];
diff --git a/src/Message.php b/src/Message.php
index 33c66161..97f34a5d 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -27,6 +27,7 @@
 use Webklex\PHPIMAP\Exceptions\MessageFlagException;
 use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException;
 use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
+use Webklex\PHPIMAP\Exceptions\MessageSizeFetchingException;
 use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
 use Webklex\PHPIMAP\Exceptions\ResponseException;
 use Webklex\PHPIMAP\Exceptions\RuntimeException;
@@ -45,6 +46,7 @@
  * @property integer msglist
  * @property integer uid
  * @property integer msgn
+ * @property integer size
  * @property Attribute subject
  * @property Attribute message_id
  * @property Attribute message_no
@@ -62,6 +64,7 @@
  * @method integer setMsglist($msglist)
  * @method integer getUid()
  * @method integer getMsgn()
+ * @method integer getSize()
  * @method Attribute getPriority()
  * @method Attribute getSubject()
  * @method Attribute getMessageId()
@@ -358,6 +361,7 @@ public function boot(): void {
      * @throws ImapServerErrorException
      * @throws MessageNotFoundException
      * @throws MethodNotFoundException
+     * @throws MessageSizeFetchingException
      * @throws RuntimeException
      * @throws ResponseException
      */
@@ -400,6 +404,7 @@ public function __set($name, $value) {
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
      * @throws MessageNotFoundException
+     * @throws MessageSizeFetchingException
      * @throws RuntimeException
      * @throws ResponseException
      */
@@ -419,6 +424,7 @@ public function __get($name) {
      * @throws MessageNotFoundException
      * @throws RuntimeException
      * @throws ResponseException
+     * @throws MessageSizeFetchingException
      */
     public function get($name): mixed {
         if (isset($this->attributes[$name]) && $this->attributes[$name] !== null) {
@@ -597,11 +603,17 @@ public function parseBody(): Message {
 
         return $body;
     }
-    
+
     /**
      * Fetches the size for this message.
-     * 
+     *
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
      * @throws MessageSizeFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
      */
     private function fetchSize(): void {
         $sequence_id = $this->getSequenceId();
@@ -1320,15 +1332,6 @@ public function getHeader(): ?Header {
         return $this->header;
     }
 
-    /**
-     * Get the message size in bytes
-     *
-     * @return int Size of the message in bytes
-     */
-    public function getSize(): int {
-        return $this->get("size");
-    }
-
     /**
      * Get the current client
      *

From cccd7c6d3beb737cd56dd2a0b3e0fdd64f180f1c Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:48:29 +0100
Subject: [PATCH 412/600] Live mailbox environment variables added

---
 phpunit.xml.dist | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6caa954a..6ec96954 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -20,6 +20,12 @@
     
   
   
-    
+    
+    
+    
+    
+    
+    
+    
   
 

From ec97dc5f244ab674e451c0601ccd27d680e052eb Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:50:15 +0100
Subject: [PATCH 413/600] Example unit test for #379 added

---
 examples/size_test.php        | 83 -----------------------------------
 tests/issues/Issue379Test.php | 55 +++++++++++++++++++++++
 2 files changed, 55 insertions(+), 83 deletions(-)
 delete mode 100644 examples/size_test.php
 create mode 100644 tests/issues/Issue379Test.php

diff --git a/examples/size_test.php b/examples/size_test.php
deleted file mode 100644
index 44f832c5..00000000
--- a/examples/size_test.php
+++ /dev/null
@@ -1,83 +0,0 @@
- 'toFill',
-  'port'          => 993,
-  'encryption'    => 'ssl',
-  'validate_cert' => true,
-  'username'      => 'toFill',
-  'password'      => 'toFill',
-  'protocol'      => 'imap'
-);
-set_include_path('/home/didi1357/git/php-imap/src');
-
-
-ini_set('display_errors', 1);
-ini_set('display_startup_errors', 1);
-error_reporting(E_ALL);
-
-require_once 'IMAP.php';
-require_once 'Traits/HasEvents.php';
-require_once 'Exceptions/MaskNotFoundException.php';
-require_once 'Client.php';
-require_once 'ClientManager.php';
-require_once 'Support/Masks/Mask.php';
-require_once 'Support/Masks/MessageMask.php';
-require_once 'Support/Masks/AttachmentMask.php';
-require_once 'Connection/Protocols/Response.php';
-require_once 'Connection/Protocols/ProtocolInterface.php';
-require_once 'Connection/Protocols/Protocol.php';
-require_once 'Connection/Protocols/ImapProtocol.php';
-require_once '../tests/vendor/autoload.php';
-require_once 'Support/PaginatedCollection.php';
-require_once 'Support/FolderCollection.php';
-require_once 'Folder.php';
-require_once 'Exceptions/ResponseException.php';
-require_once 'Query/Query.php';
-require_once 'Query/WhereQuery.php';
-require_once 'Support/MessageCollection.php';
-require_once 'Support/FlagCollection.php';
-require_once 'Support/AttachmentCollection.php';
-require_once 'Part.php';
-require_once 'Structure.php';
-require_once 'Attribute.php';
-require_once 'Address.php';
-require_once 'EncodingAliases.php';
-require_once 'Header.php';
-require_once 'Message.php';
-
-use Webklex\PHPIMAP\ClientManager;
-use Webklex\PHPIMAP\Support\Masks\MessageMask;
-use Webklex\PHPIMAP\Support\Masks\AttachmentMask;
-
-$CONFIG['masks'] = array(
-  'message' => MessageMask::class,
-  'attachment' => AttachmentMask::class
-);
-echo "
";
-$cm = new ClientManager($options = []);
-$client = $cm->make($CONFIG)->connect();
-//$client->getConnection()->enableDebug(); // uncomment this for debug output!
-$folder = $client->getFolderByPath('INBOX');
-//$message = $folder->messages()->getMessageByMsgn(1); // did not work as msgn implementation is currently broken!
-$message = $folder->messages()->getMessageByUid(2);
-var_dump($message->getSize());
-
-
-?>
-
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
new file mode 100644
index 00000000..c2394891
--- /dev/null
+++ b/tests/issues/Issue379Test.php
@@ -0,0 +1,55 @@
+markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
+        }
+        $cm = new ClientManager([
+            'accounts' => [
+                'default' => [
+                    'host'           => $_ENV["LIVE_MAILBOX_HOST"] ?? "localhost",
+                    'port'           => $_ENV["LIVE_MAILBOX_PORT"] ?? 143,
+                    'protocol'       => 'imap', //might also use imap, [pop3 or nntp (untested)]
+                    'encryption'     => $_ENV["LIVE_MAILBOX_ENCRYPTION"] ?? false, // Supported: false, 'ssl', 'tls'
+                    'validate_cert'  => $_ENV["LIVE_MAILBOX_VALIDATE_CERT"] ?? false,
+                    'username'       => $_ENV["LIVE_MAILBOX_USERNAME"] ?? "root@example.com",
+                    'password'       => $_ENV["LIVE_MAILBOX_PASSWORD"] ?? "foobar",
+                ],
+            ],
+        ]);
+
+        /** @var Client $client */
+        $client = $cm->account('default');
+        $this->assertNotNull($client);
+
+        //Connect to the IMAP Server
+        $client->connect();
+
+        $folder = $client->getFolderByPath('INBOX');
+        $this->assertNotNull($folder);
+
+        $message = $folder->messages()->getMessageByUid(2);
+        $this->assertNotNull($message);
+
+        $this->assertEquals(127, $message->getSize());
+    }
+
+}
\ No newline at end of file

From f1e3f83ead955c4900109ccfec817b5065fc1c5e Mon Sep 17 00:00:00 2001
From: webklex 
Date: Mon, 6 Mar 2023 19:54:49 +0100
Subject: [PATCH 414/600] Live mailbox testing support added

---
 .github/docker/Dockerfile       |  23 ++++++
 .github/docker/dovecot_setup.sh |  63 +++++++++++++++++
 .github/workflows/tests.yaml    |  12 ++++
 README.md                       |  55 +++++++++++++++
 phpunit.xml.dist                |   8 ++-
 tests/LiveMailboxTest.php       |  49 +++++++++++++
 tests/LiveMailboxTestCase.php   | 120 ++++++++++++++++++++++++++++++++
 tests/issues/Issue379Test.php   |  31 +++------
 tests/messages/plain.eml        |   9 +++
 9 files changed, 345 insertions(+), 25 deletions(-)
 create mode 100644 .github/docker/Dockerfile
 create mode 100644 .github/docker/dovecot_setup.sh
 create mode 100644 tests/LiveMailboxTest.php
 create mode 100644 tests/LiveMailboxTestCase.php
 create mode 100644 tests/messages/plain.eml

diff --git a/.github/docker/Dockerfile b/.github/docker/Dockerfile
new file mode 100644
index 00000000..ff0f11e6
--- /dev/null
+++ b/.github/docker/Dockerfile
@@ -0,0 +1,23 @@
+FROM ubuntu:latest
+LABEL maintainer="Webklex "
+
+RUN apt-get update
+RUN apt-get upgrade -y
+RUN apt-get install -y sudo dovecot-imapd
+
+ENV LIVE_MAILBOX=true
+ENV LIVE_MAILBOX_HOST=mail.example.local
+ENV LIVE_MAILBOX_PORT=993
+ENV LIVE_MAILBOX_ENCRYPTION=ssl
+ENV LIVE_MAILBOX_VALIDATE_CERT=true
+ENV LIVE_MAILBOX_USERNAME=root@example.local
+ENV LIVE_MAILBOX_PASSWORD=foobar
+ENV LIVE_MAILBOX_QUOTA_SUPPORT=true
+
+EXPOSE 993
+
+ADD dovecot_setup.sh /root/dovecot_setup.sh
+RUN chmod +x /root/dovecot_setup.sh
+
+CMD ["/bin/bash", "-c", "/root/dovecot_setup.sh && tail -f /dev/null"]
+
diff --git a/.github/docker/dovecot_setup.sh b/.github/docker/dovecot_setup.sh
new file mode 100644
index 00000000..25a51928
--- /dev/null
+++ b/.github/docker/dovecot_setup.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+set -ex
+
+sudo apt-get -q update
+sudo apt-get -q -y install dovecot-imapd
+
+{
+    echo "127.0.0.1 $LIVE_MAILBOX_HOST"
+} | sudo tee -a /etc/hosts
+
+SSL_CERT="/etc/ssl/certs/dovecot.crt"
+SSL_KEY="/etc/ssl/private/dovecot.key"
+
+sudo openssl req -new -x509 -days 3 -nodes \
+    -out "$SSL_CERT" \
+    -keyout "$SSL_KEY" \
+    -subj "/C=EU/ST=Europe/L=Home/O=Webklex/OU=Webklex DEV/CN=""$LIVE_MAILBOX_HOST"
+
+sudo chown root:dovecot "$SSL_CERT" "$SSL_KEY"
+sudo chmod 0440 "$SSL_CERT"
+sudo chmod 0400 "$SSL_KEY"
+
+DOVECOT_CONF="/etc/dovecot/local.conf"
+MAIL_CONF="/etc/dovecot/conf.d/10-mail.conf"
+IMAP_CONF="/etc/dovecot/conf.d/20-imap.conf"
+QUOTA_CONF="/etc/dovecot/conf.d/90-quota.conf"
+sudo touch "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF"
+sudo chown root:dovecot "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF"
+sudo chmod 0640 "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF"
+
+{
+    echo "ssl = required"
+    echo "disable_plaintext_auth = yes"
+    echo "ssl_cert = <""$SSL_CERT"
+    echo "ssl_key = <""$SSL_KEY"
+    echo "ssl_protocols = !SSLv2 !SSLv3"
+    echo "ssl_cipher_list = AES128+EECDH:AES128+EDH"
+} | sudo tee -a "$DOVECOT_CONF"
+
+{
+    echo "mail_plugins = \$mail_plugins quota"
+} | sudo tee -a "$MAIL_CONF"
+
+{
+    echo "protocol imap {"
+    echo "  mail_plugins = \$mail_plugins imap_quota"
+    echo "}"
+} | sudo tee -a "$IMAP_CONF"
+
+{
+    echo "plugin {"
+    echo "  quota = maildir:User quota"
+    echo "  quota_rule = *:storage=1G"
+    echo "}"
+} | sudo tee -a "$QUOTA_CONF"
+
+sudo useradd --create-home --shell /bin/false "$LIVE_MAILBOX_USERNAME"
+echo "$LIVE_MAILBOX_USERNAME"":""$LIVE_MAILBOX_PASSWORD" | sudo chpasswd
+
+sudo service dovecot restart
+
+sudo doveadm auth test -x service=imap "$LIVE_MAILBOX_USERNAME" "$LIVE_MAILBOX_PASSWORD"
\ No newline at end of file
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index d4146065..b441efd3 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -20,6 +20,16 @@ jobs:
 
     name: PHP ${{ matrix.php }}
 
+    env:
+      LIVE_MAILBOX: true
+      LIVE_MAILBOX_DEBUG: true
+      LIVE_MAILBOX_HOST: mail.example.local
+      LIVE_MAILBOX_PORT: 993
+      LIVE_MAILBOX_USERNAME: root@example.local
+      LIVE_MAILBOX_ENCRYPTION: ssl
+      LIVE_MAILBOX_PASSWORD: foobar
+      LIVE_MAILBOX_QUOTA_SUPPORT: true
+
     steps:
       - name: Checkout code
         uses: actions/checkout@v3
@@ -34,5 +44,7 @@ jobs:
       - name: Install Composer dependencies
         run: composer install --prefer-dist --no-interaction --no-progress
 
+      - run: "sh .github/docker/dovecot_setup.sh"
+
       - name: Execute tests
         run: vendor/bin/phpunit
\ No newline at end of file
diff --git a/README.md b/README.md
index 60bfa12a..912b46ce 100755
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord]
 - [Documentations](#documentations)
 - [Compatibility](#compatibility)
 - [Basic usage example](#basic-usage-example)
+- [Testing](#testing)
 - [Known issues](#known-issues)
 - [Support](#support)
 - [Features & pull requests](#features--pull-requests)
@@ -97,6 +98,60 @@ foreach($folders as $folder){
 ```
 
 
+## Testing
+To run the tests, please execute the following command:
+```bash
+composer test
+```
+
+### Quick-Test / Static Test
+To disable all test which require a live mailbox, please copy the `phpunit.xml.dist` to `phpunit.xml` and adjust the configuration:
+```xml
+
+    
+
+```
+
+### Full-Test / Live Mailbox Test
+To run all tests, you need to provide a valid imap configuration.
+
+To provide a valid imap configuration, please copy the `phpunit.xml.dist` to `phpunit.xml` and adjust the configuration:
+```xml
+
+    
+    
+    
+    
+    
+    
+    
+    
+    
+
+```
+
+The test account should **not** contain any important data, as it will be deleted during the test.
+Furthermore, the test account should be able to create new folders, move messages and should **not** be used by any other
+application during the test.
+
+It's recommended to use a dedicated test account for this purpose. You can use the provided `Dockerfile` to create an imap server used for testing purposes.
+
+Build the docker image:
+```bash
+cd .github/docker
+
+docker build -t php-imap-server .
+```
+Run the docker image:
+```bash
+docker run --name imap-server -p 993:993 -it --rm -d php-imap-server
+```
+Stop the docker image:
+```bash
+docker stop imap-server
+```
+
+
 ### Known issues
 | Error                                                                      | Solution                                                                                |
 |:---------------------------------------------------------------------------|:----------------------------------------------------------------------------------------|
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6ec96954..1f9bd48c 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -21,11 +21,13 @@
   
   
     
-    
-    
+    
+    
+    
     
+    
     
-    
+    
     
   
 
diff --git a/tests/LiveMailboxTest.php b/tests/LiveMailboxTest.php
new file mode 100644
index 00000000..ae484640
--- /dev/null
+++ b/tests/LiveMailboxTest.php
@@ -0,0 +1,49 @@
+getClient();
+        $client->connect();
+
+        self::assertTrue($client->isConnected());
+    }
+
+}
\ No newline at end of file
diff --git a/tests/LiveMailboxTestCase.php b/tests/LiveMailboxTestCase.php
new file mode 100644
index 00000000..e56da046
--- /dev/null
+++ b/tests/LiveMailboxTestCase.php
@@ -0,0 +1,120 @@
+-@#[]_ß_б_π_€_✔_你_يد_Z_';
+
+    /**
+     * Client manager
+     * @var ClientManager $manager
+     */
+    protected static ClientManager $manager;
+
+    /**
+     * Get the client manager
+     *
+     * @return ClientManager
+     */
+    final protected function getManager(): ClientManager {
+        if (!isset(self::$manager)) {
+            self::$manager = new ClientManager([
+                'options' => [
+                    "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false,
+                ],
+                'accounts' => [
+                    'default' => [
+                        'host'          => getenv("LIVE_MAILBOX_HOST"),
+                        'port'          => getenv("LIVE_MAILBOX_PORT"),
+                        'encryption'    => getenv("LIVE_MAILBOX_ENCRYPTION"),
+                        'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"),
+                        'username'      => getenv("LIVE_MAILBOX_USERNAME"),
+                        'password'      => getenv("LIVE_MAILBOX_PASSWORD"),
+                        'protocol'      => 'imap', //might also use imap, [pop3 or nntp (untested)]
+                    ],
+                ],
+            ]);
+        }
+        return self::$manager;
+    }
+
+    /**
+     * Get the client
+     *
+     * @return Client
+     * @throws MaskNotFoundException
+     */
+    final protected function getClient(): Client {
+        return $this->getManager()->account('default');
+    }
+
+    final protected function getSpecialChars(): string {
+        return self::SPECIAL_CHARS;
+    }
+
+    /**
+     * Append a message to a folder
+     * @param Folder $folder
+     * @param string $message
+     *
+     * @return Message
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    final protected function appendMessage(Folder $folder, string $message): Message {
+        $response = $folder->appendMessage($message);
+
+        if (!isset($response[0])) {
+            $this->fail("No message ID returned");
+        }
+        $test = explode(' ', $response[0]);
+        if (!isset($test[3])) {
+            $this->fail("No message ID returned");
+        }
+
+        $id = substr($test[3], 0, -1);
+        return $folder->messages()->getMessageByUid(intval($id));
+    }
+}
\ No newline at end of file
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index c2394891..9c942960 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -12,32 +12,16 @@
 
 namespace Tests\issues;
 
-use PHPUnit\Framework\TestCase;
-use Webklex\PHPIMAP\Client;
-use Webklex\PHPIMAP\ClientManager;
+use Tests\LiveMailboxTestCase;
 
-class Issue379Test extends TestCase {
+class Issue379Test extends LiveMailboxTestCase {
 
     public function testIssue() {
         if (!$_ENV["LIVE_MAILBOX"] ?? false) {
             $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
         }
-        $cm = new ClientManager([
-            'accounts' => [
-                'default' => [
-                    'host'           => $_ENV["LIVE_MAILBOX_HOST"] ?? "localhost",
-                    'port'           => $_ENV["LIVE_MAILBOX_PORT"] ?? 143,
-                    'protocol'       => 'imap', //might also use imap, [pop3 or nntp (untested)]
-                    'encryption'     => $_ENV["LIVE_MAILBOX_ENCRYPTION"] ?? false, // Supported: false, 'ssl', 'tls'
-                    'validate_cert'  => $_ENV["LIVE_MAILBOX_VALIDATE_CERT"] ?? false,
-                    'username'       => $_ENV["LIVE_MAILBOX_USERNAME"] ?? "root@example.com",
-                    'password'       => $_ENV["LIVE_MAILBOX_PASSWORD"] ?? "foobar",
-                ],
-            ],
-        ]);
-
-        /** @var Client $client */
-        $client = $cm->account('default');
+
+        $client = $this->getClient();
         $this->assertNotNull($client);
 
         //Connect to the IMAP Server
@@ -46,10 +30,13 @@ public function testIssue() {
         $folder = $client->getFolderByPath('INBOX');
         $this->assertNotNull($folder);
 
-        $message = $folder->messages()->getMessageByUid(2);
+        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "plain.eml"]));
+        $message = $this->appendMessage($folder, $content);
         $this->assertNotNull($message);
 
-        $this->assertEquals(127, $message->getSize());
+        $this->assertEquals(214, $message->getSize());
+
+        $this->assertEquals(true, $message->delete(true));
     }
 
 }
\ No newline at end of file
diff --git a/tests/messages/plain.eml b/tests/messages/plain.eml
new file mode 100644
index 00000000..4d3d22b1
--- /dev/null
+++ b/tests/messages/plain.eml
@@ -0,0 +1,9 @@
+From: from@someone.com
+To: to@someone-else.com
+Subject: Example
+Date: Mon, 21 Jan 2023 19:36:45 +0200
+Content-Type: text/plain;
+	charset=\"us-ascii\"
+Content-Transfer-Encoding: quoted-printable
+
+Hi there!
\ No newline at end of file

From 94e5f83a133ee7ee57176971763ab33b7e78a9f7 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Mon, 6 Mar 2023 22:15:18 +0100
Subject: [PATCH 415/600] interactive docker flag removed

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 912b46ce..2fb9c13a 100755
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ docker build -t php-imap-server .
 ```
 Run the docker image:
 ```bash
-docker run --name imap-server -p 993:993 -it --rm -d php-imap-server
+docker run --name imap-server -p 993:993 --rm -d php-imap-server
 ```
 Stop the docker image:
 ```bash

From 9491e4668a517a538114fe8784085053e8466034 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Mon, 6 Mar 2023 22:16:23 +0100
Subject: [PATCH 416/600] Extended UTF-7 support added (RFC2060) #383

---
 CHANGELOG.md                              |   2 +-
 src/Client.php                            |   1 +
 src/Connection/Protocols/ImapProtocol.php |  12 +++
 src/EncodingAliases.php                   | 101 ++++++++++++++++++-
 src/Folder.php                            |   9 +-
 src/Header.php                            |  34 +------
 tests/LiveMailboxTestCase.php             | 116 ++++++++++++++++++++--
 tests/issues/Issue379Test.php             |  52 +++++++---
 tests/issues/Issue383Test.php             |  67 +++++++++++++
 9 files changed, 334 insertions(+), 60 deletions(-)
 create mode 100644 tests/issues/Issue383Test.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d81348a..553b8781 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 
 ## [UNRELEASED]
 ### Fixed
-- NaN
+- Extended UTF-7 support added (RFC2060) #383
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
diff --git a/src/Client.php b/src/Client.php
index f5fa4909..893284f8 100755
--- a/src/Client.php
+++ b/src/Client.php
@@ -546,6 +546,7 @@ public function getFolderByName($folder_name): ?Folder {
      * @throws ResponseException
      */
     public function getFolderByPath($folder_path): ?Folder {
+        $folder_path = EncodingAliases::convert($folder_path, "", "UTF7-IMAP");
         return $this->getFolders(false)->where("path", $folder_path)->first();
     }
 
diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 07ea3f6d..db0bb216 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -13,6 +13,7 @@
 namespace Webklex\PHPIMAP\Connection\Protocols;
 
 use Exception;
+use Webklex\PHPIMAP\EncodingAliases;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
@@ -588,6 +589,7 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I
     public function selectFolder(string $folder = 'INBOX'): Response {
         $this->uid_cache = [];
 
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('SELECT', $folder);
     }
 
@@ -599,6 +601,7 @@ public function selectFolder(string $folder = 'INBOX'): Response {
      * @throws RuntimeException
      */
     public function examineFolder(string $folder = 'INBOX'): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('EXAMINE', $folder);
     }
 
@@ -911,6 +914,7 @@ public function store(
      * @throws RuntimeException
      */
     public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         $tokens = [];
         $tokens[] = $this->escapeString($folder);
         if ($flags !== null) {
@@ -990,6 +994,7 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
         $set = $this->buildSet($from, $to);
         $command = $this->buildUIDCommand("MOVE", $uid);
 
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
     }
 
@@ -1009,6 +1014,7 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
      */
     public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response {
         $command = $this->buildUIDCommand("MOVE", $uid);
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
 
         $set = implode(',', $messages);
         $tokens = [$set, $this->escapeString($folder)];
@@ -1051,6 +1057,7 @@ public function ID($ids = null): Response {
      * @throws RuntimeException
      */
     public function createFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
     }
 
@@ -1067,6 +1074,8 @@ public function createFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function renameFolder(string $old, string $new): Response {
+        $old = EncodingAliases::convert($old, "", "UTF7-IMAP");
+        $new = EncodingAliases::convert($new, "", "UTF7-IMAP");
         return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true);
     }
 
@@ -1081,6 +1090,7 @@ public function renameFolder(string $old, string $new): Response {
      * @throws RuntimeException
      */
     public function deleteFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
     }
 
@@ -1095,6 +1105,7 @@ public function deleteFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function subscribeFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
@@ -1109,6 +1120,7 @@ public function subscribeFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function unsubscribeFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php
index 274a8010..32d32e2f 100644
--- a/src/EncodingAliases.php
+++ b/src/EncodingAliases.php
@@ -107,6 +107,8 @@ class EncodingAliases {
         "ibm864"                   => "IBM864",
         "utf-8"                    => "UTF-8",
         "utf-7"                    => "UTF-7",
+        "utf-7-imap"               => "UTF7-IMAP",
+        "utf7-imap"                => "UTF7-IMAP",
         "shift_jis"                => "Shift_JIS",
         "big5"                     => "Big5",
         "euc-jp"                   => "EUC-JP",
@@ -478,5 +480,102 @@ public static function get(?string $encoding, string $fallback = null): string {
         }
         return $fallback ?: $encoding;
     }
-    
+
+
+    /**
+     * Convert the encoding of a string
+     * @param $str
+     * @param string $from
+     * @param string $to
+     *
+     * @return mixed
+     */
+    public static function convert($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed {
+        $from = self::get($from, self::detectEncoding($str));
+        $to = self::get($to, self::detectEncoding($str));
+
+        if ($from === $to) {
+            return $str;
+        }
+
+        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
+        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
+        //     https://stackoverflow.com/a/11303410
+        //
+        // us-ascii is the same as ASCII:
+        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
+        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
+        //     based on the typographical symbols predominantly in use there.
+        //     https://en.wikipedia.org/wiki/ASCII
+        //
+        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
+        if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
+            return $str;
+        }
+
+        try {
+            if (function_exists('iconv') && !self::isUtf7($from) && !self::isUtf7($to)) {
+                return iconv($from, $to, $str);
+            }
+            if (!$from) {
+                return mb_convert_encoding($str, $to);
+            }
+            return mb_convert_encoding($str, $to, $from);
+        } catch (\Exception $e) {
+            if (str_contains($from, '-')) {
+                $from = str_replace('-', '', $from);
+                return self::convert($str, $from, $to);
+            }
+            return $str;
+        }
+    }
+
+    /**
+     * Attempts to detect the encoding of a string
+     * @param string $string
+     *
+     * @return string
+     */
+    public static function detectEncoding(string $string): string {
+        $encoding = mb_detect_encoding($string, array_filter(self::getEncodings(), function($value){
+            return !in_array($value, [
+                'ISO-8859-6-I', 'ISO-8859-6-E', 'ISO-8859-8-I', 'ISO-8859-8-E',
+                'ISO-8859-11', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', 'ISO-IR-111',"ISO-2022-CN",
+                "windows-1250", "windows-1253", "windows-1255", "windows-1256", "windows-1257", "windows-1258",
+                "IBM852", "IBM855", "IBM857", "IBM866", "IBM864", "IBM862", "KOI8-R", "KOI8-U",
+                "TIS-620", "ISO-8859-1", "ISO-8859-2", "ISO-8859-3", "ISO-8859-4",
+                "VISCII", "T.61-8bit", "Big5-HKSCS", "windows-874", "macintosh", "ISO-8859-12", "ISO-8859-7",
+                "IMAP-UTF-7"
+            ]);
+        }), true);
+        if ($encoding === false) {
+            $encoding = 'UTF-8';
+        }
+        return $encoding;
+    }
+
+    /**
+     * Returns all available encodings
+     *
+     * @return array
+     */
+    public static function getEncodings(): array {
+        $encodings = [];
+        foreach (self::$aliases as $encoding) {
+            if (!in_array($encoding, $encodings)) {
+                $encodings[] = $encoding;
+            }
+        }
+        return $encodings;
+    }
+
+    /**
+     * Returns true if the encoding is UTF-7 like
+     * @param string $encoding
+     *
+     * @return bool
+     */
+    protected static function isUtf7(string $encoding): bool {
+        return str_contains(str_replace("-", "", strtolower($encoding)), "utf7");
+    }
 }
diff --git a/src/Folder.php b/src/Folder.php
index 99b1e7bf..047fe7e8 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -223,7 +223,12 @@ public function setChildren(FolderCollection $children): Folder {
      * @return string|array|bool|string[]|null
      */
     protected function decodeName($name): string|array|bool|null {
-        return mb_convert_encoding($name, "UTF-8", "UTF7-IMAP");
+        $parts = [];
+        foreach (explode($this->delimiter, $name) as $item) {
+            $parts[] = EncodingAliases::convert($item, "", "UTF-8");
+        }
+
+        return implode($this->delimiter, $parts);
     }
 
     /**
@@ -236,7 +241,7 @@ protected function decodeName($name): string|array|bool|null {
     protected function getSimpleName($delimiter, $full_name): string|bool {
         $arr = explode($delimiter, $full_name);
 
-        return end($arr);
+        return EncodingAliases::convert(end($arr), "", "UTF-8");
     }
 
     /**
diff --git a/src/Header.php b/src/Header.php
index d4a0117f..3e437aed 100644
--- a/src/Header.php
+++ b/src/Header.php
@@ -346,7 +346,6 @@ private function notDecoded($encoded, $decoded): bool {
      * @return mixed|string
      */
     public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed {
-
         $from = EncodingAliases::get($from, $this->fallback_encoding);
         $to = EncodingAliases::get($to, $this->fallback_encoding);
 
@@ -354,38 +353,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to =
             return $str;
         }
 
-        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
-        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
-        //     https://stackoverflow.com/a/11303410
-        //
-        // us-ascii is the same as ASCII:
-        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
-        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
-        //     based on the typographical symbols predominantly in use there.
-        //     https://en.wikipedia.org/wiki/ASCII
-        //
-        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
-        if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
-            return $str;
-        }
-
-        try {
-            if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
-                return iconv($from, $to, $str);
-            } else {
-                if (!$from) {
-                    return mb_convert_encoding($str, $to);
-                }
-                return mb_convert_encoding($str, $to, $from);
-            }
-        } catch (\Exception $e) {
-            if (str_contains($from, '-')) {
-                $from = str_replace('-', '', $from);
-                return $this->convertEncoding($str, $from, $to);
-            } else {
-                return $str;
-            }
-        }
+        return EncodingAliases::convert($str, $from, $to);
     }
 
     /**
diff --git a/tests/LiveMailboxTestCase.php b/tests/LiveMailboxTestCase.php
index e56da046..3bb9c9ae 100644
--- a/tests/LiveMailboxTestCase.php
+++ b/tests/LiveMailboxTestCase.php
@@ -18,6 +18,7 @@
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
+use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
 use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
 use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
 use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
@@ -36,6 +37,10 @@
  * @package Tests
  */
 abstract class LiveMailboxTestCase extends TestCase {
+
+    /**
+     * Special chars
+     */
     const SPECIAL_CHARS = 'A_\\|!"£$%&()=?àèìòùÀÈÌÒÙ<>-@#[]_ß_б_π_€_✔_你_يد_Z_';
 
     /**
@@ -81,10 +86,42 @@ final protected function getClient(): Client {
         return $this->getManager()->account('default');
     }
 
+    /**
+     * Get special chars
+     *
+     * @return string
+     */
     final protected function getSpecialChars(): string {
         return self::SPECIAL_CHARS;
     }
 
+    /**
+     * Get a folder
+     * @param string $folder_path
+     *
+     * @return Folder
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws FolderFetchingException
+     */
+    final protected function getFolder(string $folder_path = "INDEX"): Folder {
+        $client = $this->getClient();
+        $this->assertNotNull($client);
+
+        //Connect to the IMAP Server
+        $client->connect();
+
+        $folder = $client->getFolderByPath($folder_path);
+        $this->assertNotNull($folder);
+
+        return $folder;
+    }
+
     /**
      * Append a message to a folder
      * @param Folder $folder
@@ -104,17 +141,80 @@ final protected function getSpecialChars(): string {
      * @throws RuntimeException
      */
     final protected function appendMessage(Folder $folder, string $message): Message {
-        $response = $folder->appendMessage($message);
+        $status = $folder->examine();
+        if (!isset($status['uidnext'])) {
+            $this->fail("No UIDNEXT returned");
+        }
 
-        if (!isset($response[0])) {
-            $this->fail("No message ID returned");
+        $response = $folder->appendMessage($message);
+        $valid_response = false;
+        foreach ($response as $line) {
+            if (str_starts_with($line, 'OK')) {
+                $valid_response = true;
+                break;
+            }
         }
-        $test = explode(' ', $response[0]);
-        if (!isset($test[3])) {
-            $this->fail("No message ID returned");
+        if (!$valid_response) {
+            $this->fail("Failed to append message: ".implode("\n", $response));
         }
 
-        $id = substr($test[3], 0, -1);
-        return $folder->messages()->getMessageByUid(intval($id));
+        $message = $folder->messages()->getMessageByUid($status['uidnext']);
+        $this->assertNotNull($message);
+
+        return $message;
+    }
+
+    /**
+     * Append a message template to a folder
+     * @param Folder $folder
+     * @param string $template
+     *
+     * @return Message
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    final protected function appendMessageTemplate(Folder $folder, string $template): Message {
+        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", $template]));
+        return $this->appendMessage($folder, $content);
+    }
+
+    /**
+     * Delete a folder if it is given
+     * @param Folder|null $folder
+     *
+     * @return bool
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    final protected function deleteFolder(Folder $folder = null): bool {
+        $response = $folder?->delete(false);
+        if (is_array($response)) {
+            $valid_response = false;
+            foreach ($response as $line) {
+                if (str_starts_with($line, 'OK')) {
+                    $valid_response = true;
+                    break;
+                }
+            }
+            if (!$valid_response) {
+                $this->fail("Failed to delete mailbox: ".implode("\n", $response));
+            }
+            return $valid_response;
+        }
+        return false;
     }
 }
\ No newline at end of file
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index 9c942960..55efcdee 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -13,29 +13,51 @@
 namespace Tests\issues;
 
 use Tests\LiveMailboxTestCase;
+use Webklex\PHPIMAP\Exceptions\AuthFailedException;
+use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
+use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
+use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
+use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
+use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
+use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
+use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
+use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
+use Webklex\PHPIMAP\Exceptions\MessageFlagException;
+use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException;
+use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
+use Webklex\PHPIMAP\Exceptions\ResponseException;
+use Webklex\PHPIMAP\Exceptions\RuntimeException;
 
 class Issue379Test extends LiveMailboxTestCase {
 
-    public function testIssue() {
-        if (!$_ENV["LIVE_MAILBOX"] ?? false) {
+    /**
+     * Test issue #379 - Message::getSize() added
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws MaskNotFoundException
+     */
+    public function testIssue(): void {
+        if (!getenv("LIVE_MAILBOX") ?? false) {
             $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
         }
+        $folder = $this->getFolder('INBOX');
 
-        $client = $this->getClient();
-        $this->assertNotNull($client);
-
-        //Connect to the IMAP Server
-        $client->connect();
-
-        $folder = $client->getFolderByPath('INBOX');
-        $this->assertNotNull($folder);
-
-        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "plain.eml"]));
-        $message = $this->appendMessage($folder, $content);
-        $this->assertNotNull($message);
-
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
         $this->assertEquals(214, $message->getSize());
 
+        // Clean up
         $this->assertEquals(true, $message->delete(true));
     }
 
diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php
new file mode 100644
index 00000000..2f514b6e
--- /dev/null
+++ b/tests/issues/Issue383Test.php
@@ -0,0 +1,67 @@
+markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
+        }
+        $client = $this->getClient();
+        $client->connect();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'Entwürfe+']);
+
+        $folder = $client->getFolder($folder_path);
+        $this->deleteFolder($folder);
+
+        $folder = $client->createFolder($folder_path, false);
+        $this->assertNotNull($folder);
+        $folder = $this->getFolder($folder_path);
+        $this->assertNotNull($folder);
+
+        $this->assertEquals('Entwürfe+', $folder->name);
+        $this->assertEquals($folder_path, $folder->full_name);
+
+        // Clean up
+        if ($this->deleteFolder($folder) === false) {
+            $this->fail("Could not delete folder: " . $folder->path);
+        }
+    }
+}
\ No newline at end of file

From 2645b30098e7bc09aaf117a1b35fada2adbfed3b Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 02:47:38 +0100
Subject: [PATCH 417/600] Optional utf-7-imap support #383

---
 src/Client.php                            | 35 ++++++++++++-----------
 src/Connection/Protocols/ImapProtocol.php | 13 ---------
 src/Folder.php                            |  5 ++--
 tests/issues/Issue383Test.php             |  5 ++++
 4 files changed, 26 insertions(+), 32 deletions(-)

diff --git a/src/Client.php b/src/Client.php
index 893284f8..c10fa8a2 100755
--- a/src/Client.php
+++ b/src/Client.php
@@ -505,11 +505,11 @@ public function disconnect(): Client {
      * @throws RuntimeException
      * @throws ResponseException
      */
-    public function getFolder(string $folder_name, ?string $delimiter = null): ?Folder {
+    public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder {
         // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
         $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter;
         if (str_contains($folder_name, (string)$delimiter)) {
-            return $this->getFolderByPath($folder_name);
+            return $this->getFolderByPath($folder_name, $utf7);
         }
 
         return $this->getFolderByName($folder_name);
@@ -535,18 +535,18 @@ public function getFolderByName($folder_name): ?Folder {
     /**
      * Get a folder instance by a folder path
      * @param $folder_path
-     *
+     * @param bool $utf7
      * @return Folder|null
-     * @throws FolderFetchingException
-     * @throws ConnectionFailedException
      * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
-     * @throws RuntimeException
      * @throws ResponseException
+     * @throws RuntimeException
      */
-    public function getFolderByPath($folder_path): ?Folder {
-        $folder_path = EncodingAliases::convert($folder_path, "", "UTF7-IMAP");
+    public function getFolderByPath($folder_path, bool $utf7 = false): ?Folder {
+        if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "utf7-imap");
         return $this->getFolders(false)->where("path", $folder_path)->first();
     }
 
@@ -663,24 +663,27 @@ public function openFolder(string $folder_path, bool $force_select = false): arr
      * Create a new Folder
      * @param string $folder_path
      * @param boolean $expunge
-     *
+     * @param bool $utf7
      * @return Folder
-     * @throws ConnectionFailedException
      * @throws AuthFailedException
-     * @throws ImapBadRequestException
-     * @throws ImapServerErrorException
-     * @throws RuntimeException
+     * @throws ConnectionFailedException
      * @throws EventNotFoundException
      * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
      * @throws ResponseException
+     * @throws RuntimeException
      */
-    public function createFolder(string $folder_path, bool $expunge = true): Folder {
+    public function createFolder(string $folder_path, bool $expunge = true, bool $utf7 = false): Folder {
         $this->checkConnection();
+
+        if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "UTF7-IMAP");
+
         $status = $this->connection->createFolder($folder_path)->validatedData();
 
         if($expunge) $this->expunge();
 
-        $folder = $this->getFolderByPath($folder_path);
+        $folder = $this->getFolderByPath($folder_path, true);
         if($status && $folder) {
             $event = $this->getEvent("folder", "new");
             $event::dispatch($folder);
@@ -708,7 +711,7 @@ public function deleteFolder(string $folder_path, bool $expunge = true): array {
         $this->checkConnection();
 
         $folder = $this->getFolderByPath($folder_path);
-        $status = $this->getConnection()->deleteFolder($folder_path)->validatedData();
+        $status = $this->getConnection()->deleteFolder($folder->path)->validatedData();
         if ($expunge) $this->expunge();
 
         $event = $this->getEvent("folder", "deleted");
diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index db0bb216..aee0e16b 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -13,7 +13,6 @@
 namespace Webklex\PHPIMAP\Connection\Protocols;
 
 use Exception;
-use Webklex\PHPIMAP\EncodingAliases;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
@@ -589,7 +588,6 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I
     public function selectFolder(string $folder = 'INBOX'): Response {
         $this->uid_cache = [];
 
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('SELECT', $folder);
     }
 
@@ -601,7 +599,6 @@ public function selectFolder(string $folder = 'INBOX'): Response {
      * @throws RuntimeException
      */
     public function examineFolder(string $folder = 'INBOX'): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('EXAMINE', $folder);
     }
 
@@ -914,7 +911,6 @@ public function store(
      * @throws RuntimeException
      */
     public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         $tokens = [];
         $tokens[] = $this->escapeString($folder);
         if ($flags !== null) {
@@ -994,7 +990,6 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
         $set = $this->buildSet($from, $to);
         $command = $this->buildUIDCommand("MOVE", $uid);
 
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
     }
 
@@ -1014,8 +1009,6 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
      */
     public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response {
         $command = $this->buildUIDCommand("MOVE", $uid);
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
-
         $set = implode(',', $messages);
         $tokens = [$set, $this->escapeString($folder)];
 
@@ -1057,7 +1050,6 @@ public function ID($ids = null): Response {
      * @throws RuntimeException
      */
     public function createFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
     }
 
@@ -1074,8 +1066,6 @@ public function createFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function renameFolder(string $old, string $new): Response {
-        $old = EncodingAliases::convert($old, "", "UTF7-IMAP");
-        $new = EncodingAliases::convert($new, "", "UTF7-IMAP");
         return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true);
     }
 
@@ -1090,7 +1080,6 @@ public function renameFolder(string $old, string $new): Response {
      * @throws RuntimeException
      */
     public function deleteFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
     }
 
@@ -1105,7 +1094,6 @@ public function deleteFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function subscribeFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
@@ -1120,7 +1108,6 @@ public function subscribeFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function unsubscribeFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
diff --git a/src/Folder.php b/src/Folder.php
index 047fe7e8..9256492e 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -225,7 +225,7 @@ public function setChildren(FolderCollection $children): Folder {
     protected function decodeName($name): string|array|bool|null {
         $parts = [];
         foreach (explode($this->delimiter, $name) as $item) {
-            $parts[] = EncodingAliases::convert($item, "", "UTF-8");
+            $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8");
         }
 
         return implode($this->delimiter, $parts);
@@ -240,8 +240,7 @@ protected function decodeName($name): string|array|bool|null {
      */
     protected function getSimpleName($delimiter, $full_name): string|bool {
         $arr = explode($delimiter, $full_name);
-
-        return EncodingAliases::convert(end($arr), "", "UTF-8");
+        return end($arr);
     }
 
     /**
diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php
index 2f514b6e..db82c1dc 100644
--- a/tests/issues/Issue383Test.php
+++ b/tests/issues/Issue383Test.php
@@ -42,6 +42,7 @@ public function testIssue(): void {
         if (!getenv("LIVE_MAILBOX") ?? false) {
             $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
         }
+
         $client = $this->getClient();
         $client->connect();
 
@@ -53,12 +54,16 @@ public function testIssue(): void {
 
         $folder = $client->createFolder($folder_path, false);
         $this->assertNotNull($folder);
+
         $folder = $this->getFolder($folder_path);
         $this->assertNotNull($folder);
 
         $this->assertEquals('Entwürfe+', $folder->name);
         $this->assertEquals($folder_path, $folder->full_name);
 
+        $folder_path = implode($delimiter, ['INBOX', 'Entw&APw-rfe+']);
+        $this->assertEquals($folder_path, $folder->path);
+
         // Clean up
         if ($this->deleteFolder($folder) === false) {
             $this->fail("Could not delete folder: " . $folder->path);

From 38ac40ae23f5c6f5beeb70b16e65ae607e6c60ee Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 19:56:02 +0100
Subject: [PATCH 418/600] IMAP Quota root command fixed

---
 src/Connection/Protocols/ImapProtocol.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index aee0e16b..8715b6a8 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -1163,7 +1163,7 @@ public function getQuota($username): Response {
      * @throws RuntimeException
      */
     public function getQuotaRoot(string $quota_root = 'INBOX'): Response {
-        $command = "QUOTA";
+        $command = "GETQUOTAROOT";
         $params = [$quota_root];
 
         return $this->requestAndResponse($command, $params);

From 421ccf5ecc8c4ba857424881d3e22789f7b5fe5d Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 19:57:48 +0100
Subject: [PATCH 419/600] Prevent line-breaks in folder path caused by special
 chars

---
 src/Connection/Protocols/ImapProtocol.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 8715b6a8..f295293a 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -189,7 +189,7 @@ protected function decodeLine(Response $response, string $line): array {
                 $token = substr($token, 1);
             }
             if ($token[0] == '"') {
-                if (preg_match('%^\(*"((.|\\\\|\")*?)" *%', $line, $matches)) {
+                if (preg_match('%^\(*\"((.|\\\|\")*?)\"( |$)%', $line, $matches)) {
                     $tokens[] = $matches[1];
                     $line = substr($line, strlen($matches[0]));
                     continue;
@@ -844,6 +844,7 @@ public function folders(string $reference = '', string $folder = '*'): Response
                 if (count($item) != 4 || $item[0] != 'LIST') {
                     continue;
                 }
+                $item[3] = str_replace("\\\\", "\\", str_replace("\\\"", "\"", $item[3]));
                 $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]];
             }
         }

From 4219664d50e6397136da6d164acf58d65aabd4b7 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 19:59:05 +0100
Subject: [PATCH 420/600] Partial fix for #362 (allow overview response to be
 empty)

---
 src/Connection/Protocols/ImapProtocol.php | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index f295293a..f4f8eb62 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -1244,12 +1244,14 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp
                 $ids[] = $id;
             }
         }
-        $headers = $this->headers($ids, "RFC822", $uid);
-        $response->stack($headers);
-        foreach ($headers->data() as $id => $raw_header) {
-            $result[$id] = (new Header($raw_header, false))->getAttributes();
+        if (!empty($ids)) {
+            $headers = $this->headers($ids, "RFC822", $uid);
+            $response->stack($headers);
+            foreach ($headers->data() as $id => $raw_header) {
+                $result[$id] = (new Header($raw_header, false))->getAttributes();
+            }
         }
-        return $response->setResult($result);
+        return $response->setResult($result)->setCanBeEmpty(true);
     }
 
     /**

From 34f7757b1a70bb2a48b9b326d48fc35542f9db78 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:00:05 +0100
Subject: [PATCH 421/600] live test location moved

---
 tests/LiveMailboxTest.php                | 49 ------------------------
 tests/issues/Issue379Test.php            |  5 +--
 tests/issues/Issue383Test.php            | 11 ++----
 tests/{ => live}/LiveMailboxTestCase.php | 16 ++++----
 4 files changed, 13 insertions(+), 68 deletions(-)
 delete mode 100644 tests/LiveMailboxTest.php
 rename tests/{ => live}/LiveMailboxTestCase.php (93%)

diff --git a/tests/LiveMailboxTest.php b/tests/LiveMailboxTest.php
deleted file mode 100644
index ae484640..00000000
--- a/tests/LiveMailboxTest.php
+++ /dev/null
@@ -1,49 +0,0 @@
-getClient();
-        $client->connect();
-
-        self::assertTrue($client->isConnected());
-    }
-
-}
\ No newline at end of file
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index 55efcdee..fdb0436f 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -12,7 +12,7 @@
 
 namespace Tests\issues;
 
-use Tests\LiveMailboxTestCase;
+use Tests\live\LiveMailboxTestCase;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
@@ -49,9 +49,6 @@ class Issue379Test extends LiveMailboxTestCase {
      * @throws MaskNotFoundException
      */
     public function testIssue(): void {
-        if (!getenv("LIVE_MAILBOX") ?? false) {
-            $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
-        }
         $folder = $this->getFolder('INBOX');
 
         $message = $this->appendMessageTemplate($folder, "plain.eml");
diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php
index db82c1dc..0f20a396 100644
--- a/tests/issues/Issue383Test.php
+++ b/tests/issues/Issue383Test.php
@@ -12,7 +12,7 @@
 
 namespace Tests\issues;
 
-use Tests\LiveMailboxTestCase;
+use Tests\live\LiveMailboxTestCase;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
@@ -22,6 +22,7 @@
 use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
 use Webklex\PHPIMAP\Exceptions\ResponseException;
 use Webklex\PHPIMAP\Exceptions\RuntimeException;
+use Webklex\PHPIMAP\Folder;
 
 class Issue383Test extends LiveMailboxTestCase {
 
@@ -39,10 +40,6 @@ class Issue383Test extends LiveMailboxTestCase {
      * @throws MaskNotFoundException
      */
     public function testIssue(): void {
-        if (!getenv("LIVE_MAILBOX") ?? false) {
-            $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
-        }
-
         $client = $this->getClient();
         $client->connect();
 
@@ -53,10 +50,10 @@ public function testIssue(): void {
         $this->deleteFolder($folder);
 
         $folder = $client->createFolder($folder_path, false);
-        $this->assertNotNull($folder);
+        self::assertInstanceOf(Folder::class, $folder);
 
         $folder = $this->getFolder($folder_path);
-        $this->assertNotNull($folder);
+        self::assertInstanceOf(Folder::class, $folder);
 
         $this->assertEquals('Entwürfe+', $folder->name);
         $this->assertEquals($folder_path, $folder->full_name);
diff --git a/tests/LiveMailboxTestCase.php b/tests/live/LiveMailboxTestCase.php
similarity index 93%
rename from tests/LiveMailboxTestCase.php
rename to tests/live/LiveMailboxTestCase.php
index 3bb9c9ae..562c3809 100644
--- a/tests/LiveMailboxTestCase.php
+++ b/tests/live/LiveMailboxTestCase.php
@@ -10,7 +10,7 @@
 *  -
 */
 
-namespace Tests;
+namespace Tests\live;
 
 use PHPUnit\Framework\TestCase;
 use Webklex\PHPIMAP\Client;
@@ -83,6 +83,9 @@ final protected function getManager(): ClientManager {
      * @throws MaskNotFoundException
      */
     final protected function getClient(): Client {
+        if (!getenv("LIVE_MAILBOX") ?? false) {
+            $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
+        }
         return $this->getManager()->account('default');
     }
 
@@ -111,13 +114,10 @@ final protected function getSpecialChars(): string {
      */
     final protected function getFolder(string $folder_path = "INDEX"): Folder {
         $client = $this->getClient();
-        $this->assertNotNull($client);
-
-        //Connect to the IMAP Server
-        $client->connect();
+        self::assertInstanceOf(Client::class, $client->connect());
 
         $folder = $client->getFolderByPath($folder_path);
-        $this->assertNotNull($folder);
+        self::assertInstanceOf(Folder::class, $folder);
 
         return $folder;
     }
@@ -159,7 +159,7 @@ final protected function appendMessage(Folder $folder, string $message): Message
         }
 
         $message = $folder->messages()->getMessageByUid($status['uidnext']);
-        $this->assertNotNull($message);
+        self::assertInstanceOf(Message::class, $message);
 
         return $message;
     }
@@ -183,7 +183,7 @@ final protected function appendMessage(Folder $folder, string $message): Message
      * @throws RuntimeException
      */
     final protected function appendMessageTemplate(Folder $folder, string $template): Message {
-        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", $template]));
+        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]));
         return $this->appendMessage($folder, $content);
     }
 

From 49a78e63e4026bbdb081164da4f3172b8f23223e Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:00:37 +0100
Subject: [PATCH 422/600] live test location added

---
 phpunit.xml.dist | 1 +
 1 file changed, 1 insertion(+)

diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 1f9bd48c..76b56dfe 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -14,6 +14,7 @@
     
       tests
       tests/issues
+      tests/live
     
   
   

From f6df772664eba5d055652041f8b163ef7de3f33b Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:01:35 +0100
Subject: [PATCH 423/600] children accessor method added

---
 src/Folder.php | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/Folder.php b/src/Folder.php
index 9256492e..a1452a30 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -215,6 +215,15 @@ public function setChildren(FolderCollection $children): Folder {
         return $this;
     }
 
+    /**
+     * Get children.
+     *
+     * @return FolderCollection
+     */
+    public function getChildren(): FolderCollection {
+        return $this->children;
+    }
+
     /**
      * Decode name.
      * It converts UTF7-IMAP encoding to UTF-8.

From d3da1762665a8ac6b00acd34af05ca2dbfe493c3 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:01:58 +0100
Subject: [PATCH 424/600] Doc updated

---
 src/Client.php | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/Client.php b/src/Client.php
index c10fa8a2..b9a08053 100755
--- a/src/Client.php
+++ b/src/Client.php
@@ -495,19 +495,20 @@ public function disconnect(): Client {
      * Get a folder instance by a folder name
      * @param string $folder_name
      * @param string|null $delimiter
-     *
+     * @param bool $utf7
      * @return Folder|null
-     * @throws FolderFetchingException
-     * @throws ConnectionFailedException
      * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
-     * @throws RuntimeException
      * @throws ResponseException
+     * @throws RuntimeException
      */
     public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder {
         // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
         $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter;
+
         if (str_contains($folder_name, (string)$delimiter)) {
             return $this->getFolderByPath($folder_name, $utf7);
         }

From 110a78c48621021c7af0abd7a1b60beb5f15d405 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:02:39 +0100
Subject: [PATCH 425/600] Live Client::class test added

---
 tests/live/ClientTest.php | 318 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 318 insertions(+)
 create mode 100644 tests/live/ClientTest.php

diff --git a/tests/live/ClientTest.php b/tests/live/ClientTest.php
new file mode 100644
index 00000000..307894a7
--- /dev/null
+++ b/tests/live/ClientTest.php
@@ -0,0 +1,318 @@
+getClient()->connect());
+    }
+
+    /**
+     * Test if the connection is working
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testIsConnected(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertTrue($client->isConnected());
+    }
+
+    /**
+     * Test if the connection state can be determined
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testDisconnect(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertFalse($client->disconnect()->isConnected());
+    }
+
+    /**
+     * Test to get the default inbox folder
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws FolderFetchingException
+     */
+    public function testGetFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $folder = $client->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+    }
+
+    /**
+     * Test to get the default inbox folder by name
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFolderByName(): void {
+        $client = $this->getClient()->connect();
+
+        $folder = $client->getFolderByName('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+    }
+
+    /**
+     * Test to get the default inbox folder by path
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFolderByPath(): void {
+        $client = $this->getClient()->connect();
+
+        $folder = $client->getFolderByPath('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+    }
+
+    /**
+     * Test to get all folders
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFolders(): void {
+        $client = $this->getClient()->connect();
+
+        $folders = $client->getFolders(false);
+        self::assertTrue($folders->count() > 0);
+    }
+
+    public function testGetFoldersWithStatus(): void {
+        $client = $this->getClient()->connect();
+
+        $folders = $client->getFoldersWithStatus(false);
+        self::assertTrue($folders->count() > 0);
+    }
+
+    public function testOpenFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $status = $client->openFolder("INBOX");
+        self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0);
+        self::assertTrue(($status["uidnext"] ?? 0) > 0);
+        self::assertTrue(($status["uidvalidity"] ?? 0) > 0);
+        self::assertTrue(($status["recent"] ?? -1) >= 0);
+        self::assertTrue(($status["exists"] ?? -1) >= 0);
+    }
+
+    public function testCreateFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', $this->getSpecialChars()]);
+
+        $folder = $client->getFolder($folder_path);
+
+        $this->deleteFolder($folder);
+
+        $folder = $client->createFolder($folder_path, false);
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $folder = $this->getFolder($folder_path);
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $this->assertEquals($this->getSpecialChars(), $folder->name);
+        $this->assertEquals($folder_path, $folder->full_name);
+
+        $folder_path = implode($delimiter, ['INBOX', EncodingAliases::convert($this->getSpecialChars(), "utf-8", "utf7-imap")]);
+        $this->assertEquals($folder_path, $folder->path);
+
+        // Clean up
+        if ($this->deleteFolder($folder) === false) {
+            $this->fail("Could not delete folder: " . $folder->path);
+        }
+    }
+
+    public function testCheckFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $status = $client->checkFolder("INBOX");
+        self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0);
+        self::assertTrue(($status["uidnext"] ?? 0) > 0);
+        self::assertTrue(($status["uidvalidity"] ?? 0) > 0);
+        self::assertTrue(($status["recent"] ?? -1) >= 0);
+        self::assertTrue(($status["exists"] ?? -1) >= 0);
+    }
+
+    public function testGetFolderPath(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertIsArray($client->openFolder("INBOX"));
+        self::assertEquals("INBOX", $client->getFolderPath());
+    }
+
+    public function testId(): void {
+        $client = $this->getClient()->connect();
+
+        $info = $client->Id();
+        self::assertIsArray($info);
+        $valid = false;
+        foreach ($info as $value) {
+            if (str_starts_with($value, "OK")) {
+                $valid = true;
+                break;
+            }
+        }
+        self::assertTrue($valid);
+    }
+
+    public function testGetQuotaRoot(): void {
+        if (!getenv("LIVE_MAILBOX_QUOTA_SUPPORT")) {
+            $this->markTestSkipped("Quota support is not enabled");
+        }
+
+        $client = $this->getClient()->connect();
+
+        $quota = $client->getQuotaRoot("INBOX");
+        self::assertIsArray($quota);
+        self::assertTrue(count($quota) > 1);
+        self::assertIsArray($quota[0]);
+        self::assertEquals("INBOX", $quota[0][1]);
+        self::assertIsArray($quota[1]);
+        self::assertIsArray($quota[1][2]);
+        self::assertTrue($quota[1][2][2] > 0);
+    }
+
+    public function testSetTimeout(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertInstanceOf(ProtocolInterface::class, $client->setTimeout(57));
+        self::assertEquals(57, $client->getTimeout());
+    }
+
+    public function testExpunge(): void {
+        $client = $this->getClient()->connect();
+
+        $client->openFolder("INBOX");
+        $status = $client->expunge();
+
+        self::assertIsArray($status);
+        self::assertIsArray($status[0]);
+        self::assertEquals("OK", $status[0][0]);
+    }
+
+    public function testGetDefaultMessageMask(): void {
+        $client = $this->getClient();
+
+        self::assertEquals(MessageMask::class, $client->getDefaultMessageMask());
+    }
+
+    public function testGetDefaultEvents(): void {
+        $client = $this->getClient();
+
+        self::assertIsArray($client->getDefaultEvents("message"));
+    }
+
+    public function testSetDefaultMessageMask(): void {
+        $client = $this->getClient();
+
+        self::assertInstanceOf(Client::class, $client->setDefaultMessageMask(AttachmentMask::class));
+        self::assertEquals(AttachmentMask::class, $client->getDefaultMessageMask());
+
+        $client->setDefaultMessageMask(MessageMask::class);
+    }
+
+    public function testGetDefaultAttachmentMask(): void {
+        $client = $this->getClient();
+
+        self::assertEquals(AttachmentMask::class, $client->getDefaultAttachmentMask());
+    }
+
+    public function testSetDefaultAttachmentMask(): void {
+        $client = $this->getClient();
+
+        self::assertInstanceOf(Client::class, $client->setDefaultAttachmentMask(MessageMask::class));
+        self::assertEquals(MessageMask::class, $client->getDefaultAttachmentMask());
+
+        $client->setDefaultAttachmentMask(AttachmentMask::class);
+    }
+}
\ No newline at end of file

From 1eabd93f89edfd0636e4667ef116e018594597b2 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:02:53 +0100
Subject: [PATCH 426/600] Live Folder::class test added

---
 tests/live/FolderTest.php | 419 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 419 insertions(+)
 create mode 100644 tests/live/FolderTest.php

diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php
new file mode 100644
index 00000000..75d7774b
--- /dev/null
+++ b/tests/live/FolderTest.php
@@ -0,0 +1,419 @@
+getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        self::assertInstanceOf(WhereQuery::class, $folder->query());
+        self::assertInstanceOf(WhereQuery::class, $folder->search());
+        self::assertInstanceOf(WhereQuery::class, $folder->messages());
+    }
+
+    /**
+     * Test Folder::hasChildren()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws EventNotFoundException
+     */
+    public function testHasChildren(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $child_path = implode($delimiter, ['INBOX', 'test']);
+        if ($folder->getClient()->getFolder($child_path) === null) {
+            $folder->getClient()->createFolder($child_path, false);
+            $folder = $this->getFolder('INBOX');
+        }
+
+        self::assertTrue($folder->hasChildren());
+    }
+
+    /**
+     * Test Folder::setChildren()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetChildren(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $child_path = implode($delimiter, ['INBOX', 'test']);
+        if ($folder->getClient()->getFolder($child_path) === null) {
+            $folder->getClient()->createFolder($child_path, false);
+            $folder = $this->getFolder('INBOX');
+        }
+        self::assertTrue($folder->hasChildren());
+
+        $folder->setChildren(new FolderCollection());
+        self::assertTrue($folder->getChildren()->isEmpty());
+    }
+
+    /**
+     * Test Folder::getChildren()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetChildren(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $child_path = implode($delimiter, ['INBOX', 'test']);
+        if ($folder->getClient()->getFolder($child_path) === null) {
+            $folder->getClient()->createFolder($child_path, false);
+        }
+
+        $folder = $folder->getClient()->getFolders()->where('name', 'INBOX')->first();
+        self::assertInstanceOf(Folder::class, $folder);
+
+        self::assertTrue($folder->hasChildren());
+        self::assertFalse($folder->getChildren()->isEmpty());
+    }
+
+    /**
+     * Test Folder::move()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testMove(): void {
+        $client = $this->getClient();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'test']);
+
+        $folder = $client->getFolder($folder_path);
+        if ($folder === null) {
+            $folder = $client->createFolder($folder_path, false);
+        }
+        $new_folder_path = implode($delimiter, ['INBOX', 'other']);
+        $new_folder = $client->getFolder($new_folder_path);
+        $new_folder?->delete(false);
+
+        $status = $folder->move($new_folder_path, false);
+        self::assertIsArray($status);
+        self::assertTrue(str_starts_with($status[0], 'OK'));
+
+        $new_folder = $client->getFolder($new_folder_path);
+        self::assertEquals($new_folder_path, $new_folder->path);
+        self::assertEquals('other', $new_folder->name);
+
+        if ($this->deleteFolder($new_folder) === false) {
+            $this->fail("Could not delete folder: " . $new_folder->path);
+        }
+    }
+
+    /**
+     * Test Folder::delete()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testDelete(): void {
+        $client = $this->getClient();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'test']);
+
+        $folder = $client->getFolder($folder_path);
+        if ($folder === null) {
+            $folder = $client->createFolder($folder_path, false);
+        }
+        self::assertInstanceOf(Folder::class, $folder);
+
+        if ($this->deleteFolder($folder) === false) {
+            $this->fail("Could not delete folder: " . $folder->path);
+        }
+    }
+
+    /**
+     * Test Folder::overview()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     */
+    public function testOverview(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        // Test empty overview
+        $overview = $folder->overview();
+        self::assertIsArray($overview);
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        $overview = $folder->overview();
+        self::assertIsArray($overview);
+
+        self::assertEquals($message->from, end($overview)["from"]);
+
+        // Clean up
+        $this->assertTrue($message->delete(true));
+    }
+
+    /**
+     * Test Folder::appendMessage()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testAppendMessage(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("Example", $message->subject);
+        self::assertEquals("to@someone-else.com", $message->to);
+        self::assertEquals("from@someone.com", $message->from);
+
+        // Clean up
+        $this->assertTrue($message->delete(true));
+    }
+
+    /**
+     * Test Folder::subscribe()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSubscribe(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $status = $folder->subscribe();
+        self::assertIsArray($status);
+        self::assertTrue(str_starts_with($status[0], 'OK'));
+
+        // Clean up
+        $folder->unsubscribe();
+    }
+
+    /**
+     * Test Folder::unsubscribe()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testUnsubscribe(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $folder->subscribe();
+
+        $status = $folder->subscribe();
+        self::assertIsArray($status);
+        self::assertTrue(str_starts_with($status[0], 'OK'));
+    }
+
+    /**
+     * Test Folder::examine()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testExamine(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $status = $folder->examine();
+        self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0);
+        self::assertTrue(($status["uidnext"] ?? 0) > 0);
+        self::assertTrue(($status["uidvalidity"] ?? 0) > 0);
+        self::assertTrue(($status["recent"] ?? -1) >= 0);
+        self::assertTrue(($status["exists"] ?? -1) >= 0);
+    }
+
+    /**
+     * Test Folder::getClient()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetClient(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+        self::assertInstanceOf(Client::class, $folder->getClient());
+    }
+
+    /**
+     * Test Folder::setDelimiter()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetDelimiter(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $folder->setDelimiter("/");
+        self::assertEquals("/", $folder->delimiter);
+
+        $folder->setDelimiter(".");
+        self::assertEquals(".", $folder->delimiter);
+
+        $default_delimiter = $this->getManager()->get("options.delimiter", "/");
+        $folder->setDelimiter(null);
+        self::assertEquals($default_delimiter, $folder->delimiter);
+    }
+
+}
\ No newline at end of file

From 3fd790fc44dca31bc33350d2f6c12b004f2e8b83 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:03:06 +0100
Subject: [PATCH 427/600] Changelog updated

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 553b8781..9fe4c08f 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 ## [UNRELEASED]
 ### Fixed
 - Extended UTF-7 support added (RFC2060) #383
+- IMAP Quota root command fixed
+- Prevent line-breaks in folder path caused by special chars
+- Partial fix for #362 (allow overview response to be empty)
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)

From dd69b886f14c7eef2c66c2526cfb6114917918f9 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:01:37 +0100
Subject: [PATCH 428/600] message number added to not found exception

---
 src/Connection/Protocols/ImapProtocol.php | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index f4f8eb62..6cdc467a 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -812,14 +812,13 @@ public function getUid(?int $id = null): Response {
      * @throws MessageNotFoundException
      */
     public function getMessageNumber(string $id): Response {
-        $ids = $this->getUid();
-        foreach ($ids as $k => $v) {
+        foreach ($this->getUid()->data() as $k => $v) {
             if ($v == $id) {
                 return Response::empty($this->debug)->setResult((int)$k);
             }
         }
 
-        throw new MessageNotFoundException('message number not found');
+        throw new MessageNotFoundException('message number not found: ' . $id);
     }
 
     /**

From 605a28de404462840ecf3419bbc82b23bbeb6f3c Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:02:27 +0100
Subject: [PATCH 429/600] UTF-7 detection improved

---
 src/EncodingAliases.php | 2 +-
 src/Message.php         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php
index 32d32e2f..9ebdd958 100644
--- a/src/EncodingAliases.php
+++ b/src/EncodingAliases.php
@@ -575,7 +575,7 @@ public static function getEncodings(): array {
      *
      * @return bool
      */
-    protected static function isUtf7(string $encoding): bool {
+    public static function isUtf7(string $encoding): bool {
         return str_contains(str_replace("-", "", strtolower($encoding)), "utf7");
     }
 }
diff --git a/src/Message.php b/src/Message.php
index 97f34a5d..f0253dc9 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -866,7 +866,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to =
             return $str;
         }
 
-        if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
+        if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) {
             return @iconv($from, $to . '//IGNORE', $str);
         } else {
             if (!$from) {

From 4345bf72a727e99b4dcd9a3a6783297f57ff59e8 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:07:17 +0100
Subject: [PATCH 430/600] Docs added

---
 src/Connection/Protocols/Protocol.php |  4 ++++
 src/Connection/Protocols/Response.php | 32 +++++++++++++++++++++++++++
 2 files changed, 36 insertions(+)

diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php
index 3f4b83ba..6fe88ee9 100644
--- a/src/Connection/Protocols/Protocol.php
+++ b/src/Connection/Protocols/Protocol.php
@@ -216,6 +216,8 @@ public function createStream($transport, string $host, int $port, int $timeout)
     }
 
     /**
+     * Get the current connection timeout
+     *
      * @return int
      */
     public function getConnectionTimeout(): int {
@@ -223,7 +225,9 @@ public function getConnectionTimeout(): int {
     }
 
     /**
+     * Set the connection timeout
      * @param int $connection_timeout
+     *
      * @return Protocol
      */
     public function setConnectionTimeout(int $connection_timeout): Protocol {
diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php
index 068f082f..9771867a 100644
--- a/src/Connection/Protocols/Response.php
+++ b/src/Connection/Protocols/Response.php
@@ -64,6 +64,10 @@ class Response {
      */
     protected bool $debug = false;
 
+    /**
+     * Can the response be empty?
+     * @var bool $can_be_empty
+     */
     protected bool $can_be_empty = false;
 
     /**
@@ -334,6 +338,12 @@ public function successful(): bool {
     }
 
 
+    /**
+     * Check if the Response can be considered failed
+     * @param mixed $data
+     *
+     * @return bool
+     */
     public function verify_data(mixed $data): bool {
         if (is_array($data)) {
             foreach ($data as $line) {
@@ -355,6 +365,12 @@ public function verify_data(mixed $data): bool {
         return true;
     }
 
+    /**
+     * Verify a single line
+     * @param string $line
+     *
+     * @return bool
+     */
     public function verify_line(string $line): bool {
         return !str_starts_with($line, "TAG".$this->noun." BAD ") && !str_starts_with($line, "TAG".$this->noun." NO ");
     }
@@ -368,15 +384,31 @@ public function failed(): bool {
         return !$this->successful();
     }
 
+    /**
+     * Get the Response noun
+     *
+     * @return int
+     */
     public function Noun(): int {
         return $this->noun;
     }
 
+    /**
+     * Set the Response to be allowed to be empty
+     * @param bool $can_be_empty
+     *
+     * @return $this
+     */
     public function setCanBeEmpty(bool $can_be_empty): Response {
         $this->can_be_empty = $can_be_empty;
         return $this;
     }
 
+    /**
+     * Check if the Response can be empty
+     *
+     * @return bool
+     */
     public function canBeEmpty(): bool {
         return $this->can_be_empty;
     }

From e733eca156bd0fb679c9fe95edeef95492294953 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:21:31 +0100
Subject: [PATCH 431/600] class docs updated

---
 src/Connection/Protocols/Response.php |  2 +
 src/Query/Query.php                   | 69 ++++++++++++++++++++++++---
 2 files changed, 64 insertions(+), 7 deletions(-)

diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php
index 9771867a..9a30d56d 100644
--- a/src/Connection/Protocols/Response.php
+++ b/src/Connection/Protocols/Response.php
@@ -301,6 +301,7 @@ public function boolean(): bool {
 
     /**
      * Validate and retrieve the response data
+     *
      * @throws ResponseException
      */
     public function validatedData(): mixed {
@@ -309,6 +310,7 @@ public function validatedData(): mixed {
 
     /**
      * Validate the response date
+     *
      * @throws ResponseException
      */
     public function validate(): Response {
diff --git a/src/Query/Query.php b/src/Query/Query.php
index db356337..727e641f 100644
--- a/src/Query/Query.php
+++ b/src/Query/Query.php
@@ -707,6 +707,8 @@ public function limit(int $limit, int $page = 1): Query {
     }
 
     /**
+     * Get the current query collection
+     *
      * @return Collection
      */
     public function getQuery(): Collection {
@@ -714,7 +716,9 @@ public function getQuery(): Collection {
     }
 
     /**
+     * Set all query parameters
      * @param array $query
+     *
      * @return Query
      */
     public function setQuery(array $query): Query {
@@ -723,6 +727,8 @@ public function setQuery(array $query): Query {
     }
 
     /**
+     * Get the raw query
+     *
      * @return string
      */
     public function getRawQuery(): string {
@@ -730,7 +736,9 @@ public function getRawQuery(): string {
     }
 
     /**
+     * Set the raw query
      * @param string $raw_query
+     *
      * @return Query
      */
     public function setRawQuery(string $raw_query): Query {
@@ -739,6 +747,8 @@ public function setRawQuery(string $raw_query): Query {
     }
 
     /**
+     * Get all applied extensions
+     *
      * @return string[]
      */
     public function getExtensions(): array {
@@ -746,7 +756,9 @@ public function getExtensions(): array {
     }
 
     /**
+     * Set all extensions that should be used
      * @param string[] $extensions
+     *
      * @return Query
      */
     public function setExtensions(array $extensions): Query {
@@ -760,7 +772,9 @@ public function setExtensions(array $extensions): Query {
     }
 
     /**
+     * Set the client instance
      * @param Client $client
+     *
      * @return Query
      */
     public function setClient(Client $client): Query {
@@ -770,6 +784,7 @@ public function setClient(Client $client): Query {
 
     /**
      * Get the set fetch limit
+     *
      * @return ?int
      */
     public function getLimit(): ?int {
@@ -777,7 +792,9 @@ public function getLimit(): ?int {
     }
 
     /**
+     * Set the fetch limit
      * @param int $limit
+     *
      * @return Query
      */
     public function setLimit(int $limit): Query {
@@ -786,6 +803,8 @@ public function setLimit(int $limit): Query {
     }
 
     /**
+     * Get the set page
+     *
      * @return int
      */
     public function getPage(): int {
@@ -793,7 +812,9 @@ public function getPage(): int {
     }
 
     /**
+     * Set the page
      * @param int $page
+     *
      * @return Query
      */
     public function setPage(int $page): Query {
@@ -802,7 +823,9 @@ public function setPage(int $page): Query {
     }
 
     /**
+     * Set the fetch option flag
      * @param int $fetch_options
+     *
      * @return Query
      */
     public function setFetchOptions(int $fetch_options): Query {
@@ -811,7 +834,9 @@ public function setFetchOptions(int $fetch_options): Query {
     }
 
     /**
+     * Set the fetch option flag
      * @param int $fetch_options
+     *
      * @return Query
      */
     public function fetchOptions(int $fetch_options): Query {
@@ -819,6 +844,8 @@ public function fetchOptions(int $fetch_options): Query {
     }
 
     /**
+     * Get the fetch option flag
+     *
      * @return ?int
      */
     public function getFetchOptions(): ?int {
@@ -826,6 +853,8 @@ public function getFetchOptions(): ?int {
     }
 
     /**
+     * Get the fetch body flag
+     *
      * @return boolean
      */
     public function getFetchBody(): bool {
@@ -833,7 +862,9 @@ public function getFetchBody(): bool {
     }
 
     /**
+     * Set the fetch body flag
      * @param boolean $fetch_body
+     *
      * @return Query
      */
     public function setFetchBody(bool $fetch_body): Query {
@@ -842,7 +873,9 @@ public function setFetchBody(bool $fetch_body): Query {
     }
 
     /**
+     * Set the fetch body flag
      * @param boolean $fetch_body
+     *
      * @return Query
      */
     public function fetchBody(bool $fetch_body): Query {
@@ -850,6 +883,8 @@ public function fetchBody(bool $fetch_body): Query {
     }
 
     /**
+     * Get the fetch body flag
+     *
      * @return bool
      */
     public function getFetchFlags(): bool {
@@ -857,7 +892,9 @@ public function getFetchFlags(): bool {
     }
 
     /**
+     * Set the fetch flag
      * @param bool $fetch_flags
+     *
      * @return Query
      */
     public function setFetchFlags(bool $fetch_flags): Query {
@@ -866,7 +903,9 @@ public function setFetchFlags(bool $fetch_flags): Query {
     }
 
     /**
+     * Set the fetch order
      * @param string $fetch_order
+     *
      * @return Query
      */
     public function setFetchOrder(string $fetch_order): Query {
@@ -880,7 +919,9 @@ public function setFetchOrder(string $fetch_order): Query {
     }
 
     /**
+     * Set the fetch order
      * @param string $fetch_order
+     *
      * @return Query
      */
     public function fetchOrder(string $fetch_order): Query {
@@ -888,6 +929,8 @@ public function fetchOrder(string $fetch_order): Query {
     }
 
     /**
+     * Get the fetch order
+     *
      * @return string
      */
     public function getFetchOrder(): string {
@@ -895,6 +938,8 @@ public function getFetchOrder(): string {
     }
 
     /**
+     * Set the fetch order to ascending
+     *
      * @return Query
      */
     public function setFetchOrderAsc(): Query {
@@ -902,6 +947,8 @@ public function setFetchOrderAsc(): Query {
     }
 
     /**
+     * Set the fetch order to ascending
+     *
      * @return Query
      */
     public function fetchOrderAsc(): Query {
@@ -909,6 +956,8 @@ public function fetchOrderAsc(): Query {
     }
 
     /**
+     * Set the fetch order to descending
+     *
      * @return Query
      */
     public function setFetchOrderDesc(): Query {
@@ -916,6 +965,8 @@ public function setFetchOrderDesc(): Query {
     }
 
     /**
+     * Set the fetch order to descending
+     *
      * @return Query
      */
     public function fetchOrderDesc(): Query {
@@ -923,18 +974,20 @@ public function fetchOrderDesc(): Query {
     }
 
     /**
-     * @return Query
+     * Set soft fail mode
      * @var boolean $state
      *
+     * @return Query
      */
     public function softFail(bool $state = true): Query {
         return $this->setSoftFail($state);
     }
 
     /**
-     * @return Query
-     * @var boolean $state
+     * Set soft fail mode
      *
+     * @var boolean $state
+     * @return Query
      */
     public function setSoftFail(bool $state = true): Query {
         $this->soft_fail = $state;
@@ -943,6 +996,8 @@ public function setSoftFail(bool $state = true): Query {
     }
 
     /**
+     * Get soft fail mode
+     *
      * @return boolean
      */
     public function getSoftFail(): bool {
@@ -973,9 +1028,9 @@ protected function setError(int $uid, Exception $error): void {
 
     /**
      * Check if there are any errors / exceptions present
-     * @return boolean
      * @var ?integer $uid
      *
+     * @return boolean
      */
     public function hasErrors(?int $uid = null): bool {
         if ($uid !== null) {
@@ -986,9 +1041,9 @@ public function hasErrors(?int $uid = null): bool {
 
     /**
      * Check if there is an error / exception present
-     * @return boolean
      * @var integer $uid
      *
+     * @return boolean
      */
     public function hasError(int $uid): bool {
         return isset($this->errors[$uid]);
@@ -1014,9 +1069,9 @@ public function getErrors(): array {
 
     /**
      * Get a specific error / exception
-     * @return Exception|null
      * @var integer $uid
      *
+     * @return Exception|null
      */
     public function error(int $uid): ?Exception {
         return $this->getError($uid);
@@ -1024,9 +1079,9 @@ public function error(int $uid): ?Exception {
 
     /**
      * Get a specific error / exception
-     * @return ?Exception
      * @var integer $uid
      *
+     * @return ?Exception
      */
     public function getError(int $uid): ?Exception {
         if ($this->hasError($uid)) {

From f6cfe241c85eb2286284deb74d4e720e1cd45125 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:31:04 +0100
Subject: [PATCH 432/600] Method added to check if a message has a specific
 flag

---
 CHANGELOG.md    |  1 +
 src/Message.php | 11 +++++++++++
 2 files changed, 12 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fe4c08f..bb36bdac 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
+- `Message::hasFlag()` method added to check if a message has a specific flag
 
 ### Breaking changes
 - NaN
diff --git a/src/Message.php b/src/Message.php
index f0253dc9..b1e65129 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1395,6 +1395,17 @@ public function flags(): FlagCollection {
         return $this->getFlags();
     }
 
+    /**
+     * Check if a flag is set
+     *
+     * @param string $flag
+     * @return boolean
+     */
+    public function hasFlag(string $flag): bool {
+        $flag = str_replace("\\", "", strtolower($flag));
+        return $this->getFlags()->has($flag);
+    }
+
     /**
      * Get the fetched structure
      *

From f3c0c06d5ffa9d537a6e6dff1910be353942ba7b Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:34:21 +0100
Subject: [PATCH 433/600] config parameter type set to array

---
 CHANGELOG.md    | 1 +
 src/Message.php | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb36bdac..154ca9e9 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 - IMAP Quota root command fixed
 - Prevent line-breaks in folder path caused by special chars
 - Partial fix for #362 (allow overview response to be empty)
+- `Message::setConfig()` config parameter type set to array
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
diff --git a/src/Message.php b/src/Message.php
index b1e65129..cf1b1690 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1507,7 +1507,7 @@ public function setFolderPath($folder_path): Message {
      *
      * @return Message
      */
-    public function setConfig($config): Message {
+    public function setConfig(array $config): Message {
         $this->config = $config;
 
         return $this;

From 6ce11f950647363879392710eb4b53296dadd7a6 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:35:09 +0100
Subject: [PATCH 434/600] method added to get the current message configuration

---
 src/Message.php | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/Message.php b/src/Message.php
index cf1b1690..b878746d 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1513,6 +1513,15 @@ public function setConfig(array $config): Message {
         return $this;
     }
 
+    /**
+     * Get the config
+     *
+     * @return array
+     */
+    public function getConfig(): array {
+        return $this->config;
+    }
+
     /**
      * Set the available flags
      * @param $available_flags

From c240831fb097875de5d7fc1ec1d3a5ef6943ce55 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:35:49 +0100
Subject: [PATCH 435/600] use default encoding detection method

---
 src/Message.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Message.php b/src/Message.php
index b878746d..b51693e0 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -892,7 +892,7 @@ public function getEncoding(object|string $structure): string {
         } elseif (property_exists($structure, 'charset')) {
             return EncodingAliases::get($structure->charset, "ISO-8859-2");
         } elseif (is_string($structure) === true) {
-            return mb_detect_encoding($structure);
+            return EncodingAliases::detectEncoding($structure);
         }
 
         return 'UTF-8';
@@ -1503,7 +1503,7 @@ public function setFolderPath($folder_path): Message {
 
     /**
      * Set the config
-     * @param $config
+     * @param array $config
      *
      * @return Message
      */

From 58f685e193b6ae3d74c7d73df5a79bb4cb575829 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:36:28 +0100
Subject: [PATCH 436/600] rfc references added

---
 src/Connection/Protocols/ImapProtocol.php | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 6cdc467a..0e11d028 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -28,6 +28,8 @@
  * Class ImapProtocol
  *
  * @package Webklex\PHPIMAP\Connection\Protocols
+ *
+ * @reference https://www.rfc-editor.org/rfc/rfc2087.txt
  */
 class ImapProtocol extends Protocol {
 
@@ -1144,6 +1146,8 @@ public function noop(): Response {
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
      * @throws RuntimeException
+     *
+     * @Doc https://www.rfc-editor.org/rfc/rfc2087.txt
      */
     public function getQuota($username): Response {
         $command = "GETQUOTA";
@@ -1161,6 +1165,8 @@ public function getQuota($username): Response {
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
      * @throws RuntimeException
+     *
+     * @Doc https://www.rfc-editor.org/rfc/rfc2087.txt
      */
     public function getQuotaRoot(string $quota_root = 'INBOX'): Response {
         $command = "GETQUOTAROOT";

From 3b9bcbf185a019b16335c3aa5ce3d38c7ed95d91 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:36:51 +0100
Subject: [PATCH 437/600] assertion simplified

---
 tests/issues/Issue379Test.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index fdb0436f..99b67921 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -55,7 +55,7 @@ public function testIssue(): void {
         $this->assertEquals(214, $message->getSize());
 
         // Clean up
-        $this->assertEquals(true, $message->delete(true));
+        $this->assertTrue($message->delete(true));
     }
 
 }
\ No newline at end of file

From aa233eeee48ecd13c1dc1a02d3fe277850398b87 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:20:12 +0100
Subject: [PATCH 438/600] method added to select a folder (send SELECT
 folder_name)

---
 src/Folder.php | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/Folder.php b/src/Folder.php
index a1452a30..ddae8525 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -528,6 +528,21 @@ public function examine(): array {
         return $this->client->getConnection()->examineFolder($this->path)->validatedData();
     }
 
+    /**
+     * Select the current folder
+     *
+     * @return array
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function select(): array {
+        return $this->client->getConnection()->selectFolder($this->path)->validatedData();
+    }
+
     /**
      * Get the current Client instance
      *

From 036ee2f6c393b54b64dfba0662d3ebbaebd58469 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:21:07 +0100
Subject: [PATCH 439/600] Treat `in_reply_to` header as address

---
 src/Header.php | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/Header.php b/src/Header.php
index 3e437aed..61b37c79 100644
--- a/src/Header.php
+++ b/src/Header.php
@@ -263,7 +263,16 @@ public function rfc822_parse_headers($raw_headers): object {
         }
 
         foreach ($headers as $key => $values) {
-            if (isset($imap_headers[$key])) continue;
+            if (isset($imap_headers[$key])) {
+                switch ((string)$key) {
+                    case 'in_reply_to':
+                        $value = $this->decodeAddresses($values);
+                        $imap_headers[$key . "address"] = implode(", ", $values);
+                        $imap_headers[$key] = $value;
+                        break;
+                }
+                continue;
+            }
             $value = null;
             switch ((string)$key) {
                 case 'from':
@@ -271,6 +280,7 @@ public function rfc822_parse_headers($raw_headers): object {
                 case 'cc':
                 case 'bcc':
                 case 'reply_to':
+                case 'in_reply_to':
                 case 'sender':
                     $value = $this->decodeAddresses($values);
                     $headers[$key . "address"] = implode(", ", $values);
@@ -521,7 +531,7 @@ private function decodeAddresses($values): array {
      * @param object $header
      */
     private function extractAddresses(object $header): void {
-        foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) {
+        foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'in_reply_to', 'sender'] as $key) {
             if (property_exists($header, $key)) {
                 $this->set($key, $this->parseAddresses($header->$key));
             }

From 9a0a837d1212b6a0f227393e0d92a02b9eac15a8 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:21:52 +0100
Subject: [PATCH 440/600] Reset the protocol uid cache if the session gets
 expunged

---
 src/Connection/Protocols/ImapProtocol.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 0e11d028..b477b103 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -1122,6 +1122,7 @@ public function unsubscribeFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function expunge(): Response {
+        $this->uid_cache = [];
         return $this->requestAndResponse('EXPUNGE');
     }
 

From f5ca2083508cda0e694a89d2abcafad33d733dbb Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:23:15 +0100
Subject: [PATCH 441/600] Set the "seen" only if the flag isn't set and the
 fetch option isn't IMAP::FT_PEEK

---
 src/Message.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Message.php b/src/Message.php
index b51693e0..7b1885ce 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -641,7 +641,7 @@ public function peek(): void {
             if ($this->getFlags()->get("seen") == null) {
                 $this->unsetFlag("Seen");
             }
-        } elseif ($this->getFlags()->get("seen") != null) {
+        } elseif ($this->getFlags()->get("seen") == null) {
             $this->setFlag("Seen");
         }
     }

From 751d4687947f909efbe27fc94ad02eb50562c070 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:24:13 +0100
Subject: [PATCH 442/600] "Is" date comparison fixed

---
 src/Message.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Message.php b/src/Message.php
index 7b1885ce..042748b3 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1429,7 +1429,7 @@ public function is(Message $message = null): bool {
         return $this->uid == $message->uid
             && $this->message_id->first() == $message->message_id->first()
             && $this->subject->first() == $message->subject->first()
-            && $this->date->toDate()->eq($message->date);
+            && $this->date->toDate()->eq($message->date->toDate());
     }
 
     /**

From 29f37aa2dd1d1665d626d1343499e7f882b75c76 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:25:09 +0100
Subject: [PATCH 443/600] method added to get all available flags

---
 src/Message.php | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/Message.php b/src/Message.php
index 042748b3..84564ba9 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1534,6 +1534,15 @@ public function setAvailableFlags($available_flags): Message {
         return $this;
     }
 
+    /**
+     * Get the available flags
+     *
+     * @return array
+     */
+    public function getAvailableFlags(): array {
+        return $this->available_flags;
+    }
+
     /**
      * Set the attachment collection
      * @param $attachments

From 761bf2de690eb92862c0fafa54548bb368701f85 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:26:26 +0100
Subject: [PATCH 444/600] $client should be nullable

---
 src/Message.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Message.php b/src/Message.php
index 84564ba9..fd77b444 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1581,7 +1581,7 @@ public function setFlags($flags): Message {
      */
     public function setClient($client): Message {
         $this->client = $client;
-        $this->client->openFolder($this->folder_path);
+        $this->client?->openFolder($this->folder_path);
 
         return $this;
     }

From f97ae8693e0cabe8d3ebc2d8ea0328bc55809b4c Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:26:50 +0100
Subject: [PATCH 445/600] select a folder instead of examining it

---
 tests/live/LiveMailboxTestCase.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/live/LiveMailboxTestCase.php b/tests/live/LiveMailboxTestCase.php
index 562c3809..c59e6773 100644
--- a/tests/live/LiveMailboxTestCase.php
+++ b/tests/live/LiveMailboxTestCase.php
@@ -141,7 +141,7 @@ final protected function getFolder(string $folder_path = "INDEX"): Folder {
      * @throws RuntimeException
      */
     final protected function appendMessage(Folder $folder, string $message): Message {
-        $status = $folder->examine();
+        $status = $folder->select();
         if (!isset($status['uidnext'])) {
             $this->fail("No UIDNEXT returned");
         }

From 58c1697cfb1361dc07dcd126a48aa0e2f154d7fe Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:27:09 +0100
Subject: [PATCH 446/600] Number of attributes fixed

---
 tests/HeaderTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php
index 7afec065..daf4303b 100644
--- a/tests/HeaderTest.php
+++ b/tests/HeaderTest.php
@@ -71,7 +71,7 @@ public function testHeaderParsing(): void {
         self::assertInstanceOf(Carbon::class, $date);
         self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T"));
 
-        self::assertSame(48, count($header->getAttributes()));
+        self::assertSame(49, count($header->getAttributes()));
     }
 
     public function testRfc822ParseHeaders() {

From a4b3f4375bc14657bf6d2392eee469ae5f1f351f Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:27:44 +0100
Subject: [PATCH 447/600] Overview test improved

---
 tests/live/FolderTest.php | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php
index 75d7774b..78da7017 100644
--- a/tests/live/FolderTest.php
+++ b/tests/live/FolderTest.php
@@ -245,20 +245,23 @@ public function testOverview(): void {
         $folder = $this->getFolder('INBOX');
         self::assertInstanceOf(Folder::class, $folder);
 
+        $folder->select();
+
         // Test empty overview
         $overview = $folder->overview();
         self::assertIsArray($overview);
+        self::assertCount(0, $overview);
 
         $message = $this->appendMessageTemplate($folder, "plain.eml");
-        self::assertInstanceOf(Message::class, $message);
 
         $overview = $folder->overview();
+
         self::assertIsArray($overview);
+        self::assertCount(1, $overview);
 
-        self::assertEquals($message->from, end($overview)["from"]);
+        self::assertEquals($message->from->first()->full, end($overview)["from"]->toString());
 
-        // Clean up
-        $this->assertTrue($message->delete(true));
+        self::assertTrue($message->delete());
     }
 
     /**

From 2b654a875bb77775902f9653b032bad1d4db6b69 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:28:03 +0100
Subject: [PATCH 448/600] Live Message tests added

---
 tests/live/MessageTest.php | 2355 ++++++++++++++++++++++++++++++++++++
 1 file changed, 2355 insertions(+)
 create mode 100644 tests/live/MessageTest.php

diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php
new file mode 100644
index 00000000..cacb894f
--- /dev/null
+++ b/tests/live/MessageTest.php
@@ -0,0 +1,2355 @@
+getFolder('INBOX');
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        return $message;
+    }
+
+    /**
+     * Test Message::convertEncoding()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws MessageNotFoundException
+     */
+    public function testConvertEncoding(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals("Entwürfe+", $message->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::hasAttachments()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testHasAttachments(): void {
+        $message = $this->getDefaultMessage();
+        self::assertFalse($message->hasAttachments());
+
+        $folder = $message->getFolder();
+        self::assertInstanceOf(Folder::class, $folder);
+        self::assertTrue($message->delete());
+
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+        self::assertTrue($message->hasAttachments());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getFetchOptions()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFetchOptions(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals(IMAP::FT_PEEK, $message->getFetchOptions());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getMessageId()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetMessageId(): void {
+        $folder = $this->getFolder('INBOX');
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("d3a5e91963cb805cee975687d5acb1c6@swift.generated", $message->getMessageId());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getReplyTo()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetReplyTo(): void {
+        $folder = $this->getFolder('INBOX');
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("testreply_to ", $message->getReplyTo());
+        self::assertEquals("someone@domain.tld", $message->getReplyTo()->first()->mail);
+        self::assertEquals("testreply_to", $message->getReplyTo()->first()->personal);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setSequence()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetSequence(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $message->setSequence(IMAP::ST_MSGN);
+        self::assertEquals($message->msgn, $message->getSequenceId());
+
+        $message->setSequence(null);
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getEvent()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetEvent(): void {
+        $message = $this->getDefaultMessage();
+
+        $message->setEvent("message", "test", "test");
+        self::assertEquals("test", $message->getEvent("message", "test"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::__construct()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function test__construct(): void {
+        $message = $this->getDefaultMessage();
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setFlag()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetFlag(): void {
+        $message = $this->getDefaultMessage();
+
+        self::assertTrue($message->setFlag("seen"));
+        self::assertTrue($message->getFlags()->has("seen"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getMsgn()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetMsgn(): void {
+        $client = $this->getClient();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'test']);
+
+        $folder = $client->getFolder($folder_path);
+        if ($folder !== null) {
+            self::assertTrue($this->deleteFolder($folder));
+        }
+        $folder = $client->createFolder($folder_path, false);
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals(1, $message->getMsgn());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+        self::assertTrue($this->deleteFolder($folder));
+    }
+
+    /**
+     * Test Message::peek()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testPeek(): void {
+        $message = $this->getDefaultMessage();
+        self::assertFalse($message->getFlags()->has("seen"));
+        self::assertEquals(IMAP::FT_PEEK, $message->getFetchOptions());
+        $message->peek();
+        self::assertFalse($message->getFlags()->has("seen"));
+
+        $message->setFetchOption(IMAP::FT_UID);
+        self::assertEquals(IMAP::FT_UID, $message->getFetchOptions());
+        $message->peek();
+        self::assertTrue($message->getFlags()->has("seen"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+
+    }
+
+    /**
+     * Test Message::unsetFlag()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testUnsetFlag(): void {
+        $message = $this->getDefaultMessage();
+
+        self::assertFalse($message->getFlags()->has("seen"));
+
+        self::assertTrue($message->setFlag("seen"));
+        self::assertTrue($message->getFlags()->has("seen"));
+
+        self::assertTrue($message->unsetFlag("seen"));
+        self::assertFalse($message->getFlags()->has("seen"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setSequenceId()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetSequenceId(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->getSequenceId();
+
+        $message->setSequenceId(1, IMAP::ST_MSGN);
+        self::assertEquals(1, $message->getSequenceId());
+
+        $message->setSequenceId(1);
+        self::assertEquals(1, $message->getSequenceId());
+
+        $message->setSequenceId($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+
+    }
+
+    /**
+     * Test Message::getTo()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetTo(): void {
+        $message = $this->getDefaultMessage();
+        $folder = $message->getFolder();
+        self::assertInstanceOf(Folder::class, $folder);
+
+        self::assertEquals("to@someone-else.com", $message->getTo());
+        self::assertTrue($message->delete());
+
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("testnameto ", $message->getTo());
+        self::assertEquals("testnameto", $message->getTo()->first()->personal);
+        self::assertEquals("someone@domain.tld", $message->getTo()->first()->mail);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setUid()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetUid(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->getSequenceId();
+
+        $message->setUid(789);
+        self::assertEquals(789, $message->uid);
+
+        $message->setUid($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+
+    }
+
+    /**
+     * Test Message::getUid()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetUid(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->uid;
+
+        $message->setUid(789);
+        self::assertEquals(789, $message->uid);
+        self::assertEquals(789, $message->getUid());
+
+        $message->setUid($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::hasTextBody()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testHasTextBody(): void {
+        $message = $this->getDefaultMessage();
+        self::assertTrue($message->hasTextBody());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::__get()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function test__get(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+        self::assertEquals("Example", $message->subject);
+        self::assertEquals("to@someone-else.com", $message->to);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getDate()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetDate(): void {
+        $message = $this->getDefaultMessage();
+        self::assertInstanceOf(Carbon::class, $message->getDate()->toDate());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setMask()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetMask(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals(MessageMask::class, $message->getMask());
+
+        $message->setMask(AttachmentMask::class);
+        self::assertEquals(AttachmentMask::class, $message->getMask());
+
+        $message->setMask(MessageMask::class);
+        self::assertEquals(MessageMask::class, $message->getMask());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getSequenceId()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetSequenceId(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->getSequenceId();
+
+        $message->setSequenceId(789, IMAP::ST_MSGN);
+        self::assertEquals(789, $message->getSequenceId());
+
+        $message->setSequenceId(789);
+        self::assertEquals(789, $message->getSequenceId());
+
+        $message->setSequenceId($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setConfig()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetConfig(): void {
+        $message = $this->getDefaultMessage();
+
+        $config = $message->getConfig();
+        self::assertIsArray($config);
+
+        $message->setConfig(["foo" => "bar"]);
+        self::assertArrayHasKey("foo", $message->getConfig());
+
+        $message->setConfig($config);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getEvents()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetEvents(): void {
+        $message = $this->getDefaultMessage();
+
+        $events = $message->getEvents();
+        self::assertIsArray($events);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setFetchOption()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetFetchOption(): void {
+        $message = $this->getDefaultMessage();
+
+        $fetch_option = $message->fetch_options;
+
+        $message->setFetchOption(IMAP::FT_UID);
+        self::assertEquals(IMAP::FT_UID, $message->fetch_options);
+
+        $message->setFetchOption(IMAP::FT_PEEK);
+        self::assertEquals(IMAP::FT_PEEK, $message->fetch_options);
+
+        $message->setFetchOption(IMAP::FT_UID | IMAP::FT_PEEK);
+        self::assertEquals(IMAP::FT_UID | IMAP::FT_PEEK, $message->fetch_options);
+
+        $message->setFetchOption($fetch_option);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getMsglist()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetMsglist(): void {
+        $message = $this->getDefaultMessage();
+
+        self::assertEquals(0, (int)$message->getMsglist()->toString());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::decodeString()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testDecodeString(): void {
+        $message = $this->getDefaultMessage();
+
+        $string = '

Test

'; + self::assertEquals('

Test

', $message->decodeString($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::attachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAttachments(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertTrue($message->hasAttachments()); + self::assertSameSize([1], $message->attachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getMask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMask(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(MessageMask::class, $message->getMask()); + + $message->setMask(AttachmentMask::class); + self::assertEquals(AttachmentMask::class, $message->getMask()); + + $message->setMask(MessageMask::class); + self::assertEquals(MessageMask::class, $message->getMask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::hasHTMLBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testHasHTMLBody(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertTrue($message->hasHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setEvents() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetEvents(): void { + $message = $this->getDefaultMessage(); + + $events = $message->getEvents(); + self::assertIsArray($events); + + $message->setEvents(["foo" => "bar"]); + self::assertArrayHasKey("foo", $message->getEvents()); + + $message->setEvents($events); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__set() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__set(): void { + $message = $this->getDefaultMessage(); + + $message->foo = "bar"; + self::assertEquals("bar", $message->getFoo()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getHTMLBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetHTMLBody(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertTrue($message->hasHTMLBody()); + self::assertIsString($message->getHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasHTMLBody()); + self::assertEmpty($message->getHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSequence() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSequence(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(IMAP::ST_UID, $message->getSequence()); + + $original_sequence = $message->getSequence(); + + $message->setSequence(IMAP::ST_MSGN); + self::assertEquals(IMAP::ST_MSGN, $message->getSequence()); + + $message->setSequence($original_sequence); + self::assertEquals(IMAP::ST_UID, $message->getSequence()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::restore() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testRestore(): void { + $message = $this->getDefaultMessage(); + + $message->setFlag("deleted"); + self::assertTrue($message->hasFlag("deleted")); + + $message->restore(); + self::assertFalse($message->hasFlag("deleted")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getPriority() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetPriority(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertEquals(1, $message->getPriority()->first()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetAttachments(): void { + $message = $this->getDefaultMessage(); + + $message->setAttachments(new AttachmentCollection(["foo" => "bar"])); + self::assertIsArray($message->attachments()->toArray()); + self::assertTrue($message->attachments()->has("foo")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFrom() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFrom(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("from@someone.com", $message->getFrom()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setEvent() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetEvent(): void { + $message = $this->getDefaultMessage(); + + $message->setEvent("message", "bar", "foo"); + self::assertArrayHasKey("bar", $message->getEvents()["message"]); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getInReplyTo() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetInReplyTo(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("", $message->getInReplyTo()); + + // Cleanup + self::assertTrue($message->delete()); + + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertEquals("Webklex/php-imap/issues/349@github.com", $message->getInReplyTo()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::copy() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testCopy(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $new_message = $message->copy($folder->path, true); + self::assertInstanceOf(Message::class, $new_message); + self::assertEquals($folder->path, $new_message->getFolder()->path); + + // Cleanup + self::assertTrue($message->delete()); + self::assertTrue($new_message->delete()); + + } + + /** + * Test Message::getBodies() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetBodies(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getBodies()); + self::assertCount(1, $message->getBodies()); + + // Cleanup + self::assertTrue($message->delete()); + + } + + /** + * Test Message::getFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFlags(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getFlags()->all()); + + self::assertFalse($message->hasFlag("seen")); + + self::assertTrue($message->setFlag("seen")); + self::assertTrue($message->getFlags()->has("seen")); + self::assertTrue($message->hasFlag("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::addFlag() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAddFlag(): void { + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasFlag("seen")); + + self::assertTrue($message->addFlag("seen")); + self::assertTrue($message->hasFlag("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSubject() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSubject(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->getSubject()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetClient(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Client::class, $message->getClient()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchFlagsOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchFlagsOption(): void { + $message = $this->getDefaultMessage(); + + self::assertTrue($message->getFetchFlagsOption()); + $message->setFetchFlagsOption(false); + self::assertFalse($message->getFetchFlagsOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::mask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMask(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(MessageMask::class, $message->mask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMsglist() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMsglist(): void { + $message = $this->getDefaultMessage(); + $message->setMsglist("foo"); + self::assertEquals("foo", $message->getMsglist()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::flags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFlags(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(FlagCollection::class, $message->flags()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getAttributes() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetAttributes(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getAttributes()); + self::assertArrayHasKey("subject", $message->getAttributes()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetAttachments(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(AttachmentCollection::class, $message->getAttachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getRawBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetRawBody(): void { + $message = $this->getDefaultMessage(); + self::assertIsString($message->getRawBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::is() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testIs(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->is($message)); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFlags(): void { + $message = $this->getDefaultMessage(); + $message->setFlags(new FlagCollection()); + self::assertFalse($message->hasFlag("recent")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::make() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws ResponseException + * @throws RuntimeException + * @throws ReflectionException + */ + public function testMake(): void { + $folder = $this->getFolder('INBOX'); + $folder->getClient()->openFolder($folder->path); + + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "1366671050@github.com.eml"])); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $message = Message::make(0, null, $folder->getClient(), $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID); + self::assertInstanceOf(Message::class, $message); + } + + /** + * Test Message::setAvailableFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetAvailableFlags(): void { + $message = $this->getDefaultMessage(); + + $message->setAvailableFlags(["foo"]); + self::assertSameSize(["foo"], $message->getAvailableFlags()); + self::assertEquals("foo", $message->getAvailableFlags()[0]); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSender() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSender(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertEquals("testsender ", $message->getSender()); + self::assertEquals("testsender", $message->getSender()->first()->personal); + self::assertEquals("someone@domain.tld", $message->getSender()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::fromFile() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFromFile(): void { + $this->getManager(); + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "1366671050@github.com.eml"]); + $message = Message::fromFile($filename); + self::assertInstanceOf(Message::class, $message); + } + + /** + * Test Message::getStructure() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetStructure(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Structure::class, $message->getStructure()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::get() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageSizeFetchingException + */ + public function testGet(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->get("subject")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSize() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSize(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(214, $message->getSize()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getHeader() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetHeader(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Header::class, $message->getHeader()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getReferences() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetReferences(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertIsArray($message->getReferences()->all()); + self::assertEquals("", $message->getReferences()->first()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFolderPath() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFolderPath(): void { + $message = $this->getDefaultMessage(); + + $folder_path = $message->getFolderPath(); + + $message->setFolderPath("foo"); + self::assertEquals("foo", $message->getFolderPath()); + + $message->setFolderPath($folder_path); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getTextBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetTextBody(): void { + $message = $this->getDefaultMessage(); + self::assertIsString($message->getTextBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::move() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMove(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $message = $message->move($folder->path, true); + self::assertInstanceOf(Message::class, $message); + self::assertEquals($folder->path, $message->getFolder()->path); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFolderPath() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolderPath(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("INBOX", $message->getFolderPath()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFolder() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolder(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Folder::class, $message->getFolder()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchBodyOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchBodyOption(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->getFetchBodyOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchBodyOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchBodyOption(): void { + $message = $this->getDefaultMessage(); + + $message->setFetchBodyOption(false); + self::assertFalse($message->getFetchBodyOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchFlagsOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchFlagsOption(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->getFetchFlagsOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__call() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__call(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->getSubject()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetClient(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $message->setClient(null); + self::assertNull($message->getClient()); + + $message->setClient($client); + self::assertInstanceOf(Client::class, $message->getClient()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMsgn() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMsgn(): void { + $message = $this->getDefaultMessage(); + + $uid = $message->getUid(); + $message->setMsgn(789); + self::assertEquals(789, $message->getMsgn()); + $message->setUid($uid); + + // Cleanup + self::assertTrue($message->delete()); + } +} From f16984afcf5d86f3daeb58be3632e21b889d8d3a Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 8 Mar 2023 21:28:37 +0100 Subject: [PATCH 449/600] Changelog updated --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 154ca9e9..4720852e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) - `Message::setConfig()` config parameter type set to array +- Treat `in_reply_to` header as address +- Reset the protocol uid cache if the session gets expunged +- Set the "seen" flag only if the flag isn't set and the fetch option isn't IMAP::FT_PEEK +- `Message::is()` date comparison fixed +- `Message::$client` could not be set to null ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) - `Message::hasFlag()` method added to check if a message has a specific flag +- `Message::getConfig()` method added to get the current message configuration +- `Folder::select()` method added to select a folder +- `Message::getAvailableFlags()` method added to get all available flags +- Live mailbox tests added ### Breaking changes - NaN From 2fc3995a5c75d854b096e95188d00226d4e826e9 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:14:50 +0100 Subject: [PATCH 450/600] method added to map all attribute values --- CHANGELOG.md | 1 + src/Attribute.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4720852e..d8003383 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Folder::select()` method added to select a folder - `Message::getAvailableFlags()` method added to get all available flags - Live mailbox tests added +- `Attribute::map()` method added to map all attribute values ### Breaking changes - NaN diff --git a/src/Attribute.php b/src/Attribute.php index 7e77a469..c50cab75 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -314,4 +314,12 @@ public function offsetSet(mixed $offset, mixed $value): void { public function offsetUnset(mixed $offset): void { $this->remove($offset); } + + /** + * @param callable $callback + * @return array + */ + public function map(callable $callback): array { + return array_map($callback, $this->values); + } } \ No newline at end of file From b3ffb4a91447302d325bd7e14543986334b0366a Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:16:15 +0100 Subject: [PATCH 451/600] in_reply_to and references parsing fixed --- CHANGELOG.md | 2 +- src/Header.php | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8003383..260cea14 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) - `Message::setConfig()` config parameter type set to array -- Treat `in_reply_to` header as address - Reset the protocol uid cache if the session gets expunged - Set the "seen" flag only if the flag isn't set and the fetch option isn't IMAP::FT_PEEK - `Message::is()` date comparison fixed - `Message::$client` could not be set to null +- in_reply_to and references parsing fixed ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Header.php b/src/Header.php index 61b37c79..fcbf132d 100644 --- a/src/Header.php +++ b/src/Header.php @@ -185,11 +185,16 @@ protected function parse(): void { $this->set("subject", $this->decode($header->subject)); } if (property_exists($header, 'references')) { - $this->set("references", $this->decode($header->references)); + $this->set("references", array_map(function ($item) { + return str_replace(['<', '>'], '', $item); + }, explode(" ", $header->references))); } if (property_exists($header, 'message_id')) { $this->set("message_id", str_replace(['<', '>'], '', $header->message_id)); } + if (property_exists($header, 'in_reply_to')) { + $this->set("in_reply_to", str_replace(['<', '>'], '', $header->in_reply_to)); + } $this->parseDate($header); foreach ($header as $key => $value) { @@ -264,13 +269,6 @@ public function rfc822_parse_headers($raw_headers): object { foreach ($headers as $key => $values) { if (isset($imap_headers[$key])) { - switch ((string)$key) { - case 'in_reply_to': - $value = $this->decodeAddresses($values); - $imap_headers[$key . "address"] = implode(", ", $values); - $imap_headers[$key] = $value; - break; - } continue; } $value = null; @@ -280,7 +278,6 @@ public function rfc822_parse_headers($raw_headers): object { case 'cc': case 'bcc': case 'reply_to': - case 'in_reply_to': case 'sender': $value = $this->decodeAddresses($values); $headers[$key . "address"] = implode(", ", $values); @@ -531,7 +528,7 @@ private function decodeAddresses($values): array { * @param object $header */ private function extractAddresses(object $header): void { - foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'in_reply_to', 'sender'] as $key) { + foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) { if (property_exists($header, $key)) { $this->set($key, $this->parseAddresses($header->$key)); } From a590b3490769572cfab2157e38c65cd7fb2dfe1a Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:23:25 +0100 Subject: [PATCH 452/600] Prevent message body parsing from adding empty lines --- CHANGELOG.md | 1 + src/Message.php | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 260cea14..cb510017 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::is()` date comparison fixed - `Message::$client` could not be set to null - in_reply_to and references parsing fixed +- Prevent message body parsing from adding empty lines ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Message.php b/src/Message.php index fd77b444..38b56c97 100755 --- a/src/Message.php +++ b/src/Message.php @@ -326,7 +326,7 @@ public static function fromFile($filename): Message { $email = str_replace("\n", "\r\n", $email); } $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); - $raw_body = substr($email, strlen($raw_header)+8); + $raw_body = substr($email, strlen($raw_header)+4); $instance->parseRawHeader($raw_header); $instance->parseRawBody($raw_body); @@ -713,14 +713,27 @@ private function fetchPart(Part $part): void { $content = $this->convertEncoding($content, $encoding); } - $subtype = strtolower($part->subtype ?? ''); - $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; + $this->addBody($part->subtype ?? '', $content); + } + } - if (isset($this->bodies[$subtype])) { - $this->bodies[$subtype] .= "\n" . $content; - } else { - $this->bodies[$subtype] = $content; + /** + * Add a body to the message + * @param string $subtype + * @param string $content + * + * @return void + */ + protected function addBody(string $subtype, string $content): void { + $subtype = strtolower($subtype); + $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; + + if (isset($this->bodies[$subtype]) && $this->bodies[$subtype] !== null && $this->bodies[$subtype] !== "") { + if ($content !== "") { + $this->bodies[$subtype] .= "\n".$content; } + } else { + $this->bodies[$subtype] = $content; } } From c1a39e144625d8491fa2349a5b98c37da71a67f9 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:25:53 +0100 Subject: [PATCH 453/600] Don't parse regular inline message parts without name or filename as attachment --- CHANGELOG.md | 1 + src/Part.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb510017..2b9cdcdb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::$client` could not be set to null - in_reply_to and references parsing fixed - Prevent message body parsing from adding empty lines +- Don't parse regular inline message parts without name or filename as attachment ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Part.php b/src/Part.php index b5c25032..27533e30 100644 --- a/src/Part.php +++ b/src/Part.php @@ -289,6 +289,10 @@ public function isAttachment(): bool { return false; } } + + if ($this->disposition === "inline" && $this->filename == null && $this->name == null) { + return false; + } return true; } From 2c6475d81ba77fd2b73a2cdbd98e7fbf465922a3 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:26:46 +0100 Subject: [PATCH 454/600] Part number fixed --- tests/MessageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MessageTest.php b/tests/MessageTest.php index eaf6737c..88abd40c 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -180,7 +180,7 @@ public function testLoadMessageFromFile(): void { self::assertSame("znk551MP3TP3WPp9Kl1gnLErrWEgkJFAtvaKqkTgrk3dKI8dX38YT8BaVxRcOERN", $attachment->content); self::assertSame("application/octet-stream", $attachment->content_type); self::assertSame("6mfFxiU5Yhv9WYJx.txt", $attachment->name); - self::assertSame(1, $attachment->part_number); + self::assertSame(2, $attachment->part_number); self::assertSame("text", $attachment->type); self::assertNotEmpty($attachment->id); self::assertSame(90, $attachment->size); From 8ebf333aae0a688af7b2bbb55e68232a849b5024 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:27:09 +0100 Subject: [PATCH 455/600] reference value updated --- tests/live/MessageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index cacb894f..d7850917 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -2016,7 +2016,7 @@ public function testGetReferences(): void { $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); self::assertIsArray($message->getReferences()->all()); - self::assertEquals("", $message->getReferences()->first()); + self::assertEquals("Webklex/php-imap/issues/349@github.com", $message->getReferences()->first()); // Cleanup self::assertTrue($message->delete()); From 3f96ac1a67d3bb65508be102eae866e2055899b9 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:27:20 +0100 Subject: [PATCH 456/600] Missing text added --- tests/issues/Issue275Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/issues/Issue275Test.php b/tests/issues/Issue275Test.php index 3cb5c5de..4049998d 100644 --- a/tests/issues/Issue275Test.php +++ b/tests/issues/Issue275Test.php @@ -22,7 +22,7 @@ public function testIssueEmail1() { $message = Message::fromFile($filename); self::assertSame("Testing 123", (string)$message->subject); - self::assertSame("testing123 this is a body", $message->getTextBody()); + self::assertSame("Asdf testing123 this is a body", $message->getTextBody()); } public function testIssueEmail2() { From 90bf0fc78ea85de31e46862f594e190dfe080e9c Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:27:46 +0100 Subject: [PATCH 457/600] Attribute count updated --- tests/HeaderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index daf4303b..7afec065 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -71,7 +71,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Carbon::class, $date); self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); - self::assertSame(49, count($header->getAttributes())); + self::assertSame(48, count($header->getAttributes())); } public function testRfc822ParseHeaders() { From 89183802c8fff5d84931afc7ed574a56fabc3914 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:13:24 +0100 Subject: [PATCH 458/600] return false if the body is empty --- CHANGELOG.md | 1 + src/Message.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9cdcdb..a77f6cd6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - in_reply_to and references parsing fixed - Prevent message body parsing from adding empty lines - Don't parse regular inline message parts without name or filename as attachment +- `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Message.php b/src/Message.php index 38b56c97..157c1f7e 100755 --- a/src/Message.php +++ b/src/Message.php @@ -454,7 +454,7 @@ public function get($name): mixed { * @return bool */ public function hasTextBody(): bool { - return isset($this->bodies['text']); + return isset($this->bodies['text']) && $this->bodies['text'] !== ""; } /** @@ -476,7 +476,7 @@ public function getTextBody(): string { * @return bool */ public function hasHTMLBody(): bool { - return isset($this->bodies['html']); + return isset($this->bodies['html']) && $this->bodies['html'] !== ""; } /** From 8d007516bdb609b800e7cf8189fd81b05ab63043 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:13:47 +0100 Subject: [PATCH 459/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a77f6cd6..97d367da 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Extended UTF-7 support added (RFC2060) #383 - IMAP Quota root command fixed - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) @@ -21,6 +20,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty ### Added +- Extended UTF-7 support added (RFC2060) #383 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) - `Message::hasFlag()` method added to check if a message has a specific flag - `Message::getConfig()` method added to get the current message configuration From a56424474c33e18382bc969c70d975c14ab707f0 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:42:15 +0100 Subject: [PATCH 460/600] check if a header attribute / value exist --- CHANGELOG.md | 1 + src/Header.php | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d367da..3bcf6890 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::getAvailableFlags()` method added to get all available flags - Live mailbox tests added - `Attribute::map()` method added to map all attribute values +- `Header::has()` method added to check if a header attribute / value exist ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index fcbf132d..56b3587a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -110,6 +110,17 @@ public function get($name): Attribute { return new Attribute($name); } + /** + * Check if a specific attribute exists + * @param string $name + * + * @return bool + */ + public function has(string $name): bool { + $name = str_replace(["-", " "], "_", strtolower($name)); + return isset($this->attributes[$name]); + } + /** * Set a specific attribute * @param string $name From 8d6ef345c5e47d03295a57de6f2a432642fa9802 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:43:39 +0100 Subject: [PATCH 461/600] part attributes are now accessible via linked attribute --- CHANGELOG.md | 1 + src/Attachment.php | 2 ++ src/Part.php | 11 ++++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bcf6890..1af4be9b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Live mailbox tests added - `Attribute::map()` method added to map all attribute values - `Header::has()` method added to check if a header attribute / value exist +- All part attributes are now accessible via linked attribute ### Breaking changes - NaN diff --git a/src/Attachment.php b/src/Attachment.php index c6afe30f..a6e10430 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -223,6 +223,8 @@ protected function fetch(): void { $this->setName($this->part->subtype); } } + + $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); } /** diff --git a/src/Part.php b/src/Part.php index 27533e30..1759b8de 100644 --- a/src/Part.php +++ b/src/Part.php @@ -290,10 +290,19 @@ public function isAttachment(): bool { } } - if ($this->disposition === "inline" && $this->filename == null && $this->name == null) { + if ($this->disposition === "inline" && $this->filename == null && $this->name == null && !$this->header->has("content_id")) { return false; } return true; } + /** + * Get the part header + * + * @return Header|null + */ + public function getHeader(): ?Header { + return $this->header; + } + } From 67b9ee32a9551b0478fe73ebeb1e9f712ec0dd06 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 10 Mar 2023 00:40:31 +0100 Subject: [PATCH 462/600] Restore a message from string --- CHANGELOG.md | 1 + src/Message.php | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af4be9b..82a5ab54 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Attribute::map()` method added to map all attribute values - `Header::has()` method added to check if a header attribute / value exist - All part attributes are now accessible via linked attribute +- Restore a message from string `Message::fromString()` ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index 157c1f7e..ff7475c6 100755 --- a/src/Message.php +++ b/src/Message.php @@ -309,6 +309,30 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw * @throws MaskNotFoundException */ public static function fromFile($filename): Message { + $blob = file_get_contents($filename); + if ($blob === false) { + throw new RuntimeException("Unable to read file"); + } + return self::fromString($blob); + } + + /** + * Create a new message instance by reading and loading a string + * @param string $blob + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public static function fromString(string $blob): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -321,12 +345,11 @@ public static function fromFile($filename): Message { throw new MaskNotFoundException("Unknown message mask provided"); } - $email = file_get_contents($filename); - if(!str_contains($email, "\r\n")){ - $email = str_replace("\n", "\r\n", $email); + if(!str_contains($blob, "\r\n")){ + $blob = str_replace("\n", "\r\n", $blob); } - $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); - $raw_body = substr($email, strlen($raw_header)+4); + $raw_header = substr($blob, 0, strpos($blob, "\r\n\r\n")); + $raw_body = substr($blob, strlen($raw_header)+4); $instance->parseRawHeader($raw_header); $instance->parseRawBody($raw_body); From 1459a296b4be38cc5f73b1031544e5fba65910a2 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:25:57 +0100 Subject: [PATCH 463/600] use accessor instead of magic method --- src/Message.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Message.php b/src/Message.php index ff7475c6..5cd9ff7a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -982,11 +982,9 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = $this->fetchThreadByInReplyTo($thread, $this->message_id, $folder, $folder, $sent_folder); $this->fetchThreadByInReplyTo($thread, $this->message_id, $sent_folder, $folder, $sent_folder); - if (is_array($this->in_reply_to)) { - foreach ($this->in_reply_to as $in_reply_to) { - $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder); - $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder); - } + foreach ($this->in_reply_to->all() as $in_reply_to) { + $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder); + $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder); } return $thread; From a165e2c35caf677766eaaa50dbf3345e7c291e9a Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:37:07 +0100 Subject: [PATCH 464/600] Only explode the tag line if it contains a space --- src/Connection/Protocols/ImapProtocol.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b477b103..d9b49895 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -147,7 +147,9 @@ protected function assumedNextLine(Response $response, string $start): bool { */ protected function nextTaggedLine(Response $response, ?string &$tag): string { $line = $this->nextLine($response); - list($tag, $line) = explode(' ', $line, 2); + if (str_contains($line, ' ')) { + list($tag, $line) = explode(' ', $line, 2); + } return $line ?? ''; } @@ -300,7 +302,7 @@ public function readResponse(Response $response, string $tag, bool $dontParse = // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; - } elseif ($tokens[0] == 'NO') { + } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { throw new ImapServerErrorException(); } From 53e69bbe367100613d62952cb60b9d9aff1f6b1e Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:38:30 +0100 Subject: [PATCH 465/600] "empty response" detection extended to catch an empty response caused by a broken resource --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index d9b49895..55297af0 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -117,7 +117,7 @@ public function nextLine(Response $response): string { while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { $line .= $next_char; } - if ($line === "" && $next_char === false) { + if ($line === "" && ($next_char === false || $next_char === "")) { throw new RuntimeException('empty response'); } $line .= "\n"; From a09a45be5be789e30426d1fda58b80fa343e4d31 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:41:29 +0100 Subject: [PATCH 466/600] prevent iconv decoding from failing --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 56b3587a..b5ebf2ec 100644 --- a/src/Header.php +++ b/src/Header.php @@ -433,7 +433,7 @@ private function decode(mixed $value): mixed { $value = \imap_utf8($value); } } elseif ($decoder === 'iconv' && $this->is_uft8($value)) { - $value = iconv_mime_decode($value); + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); } if ($this->is_uft8($value)) { From d0cdb0803ee9e4a2fc9cde5ac5f8f106cf262589 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:42:16 +0100 Subject: [PATCH 467/600] Date decoding rules extended to support more date formats --- src/Header.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Header.php b/src/Header.php index b5ebf2ec..7d96b838 100644 --- a/src/Header.php +++ b/src/Header.php @@ -693,12 +693,20 @@ private function parseDate(object $header): void { if (str_contains($date, ' ')) { $date = str_replace(' ', ' ', $date); } + if (str_contains($date, ' UT ')) { + $date = str_replace(' UT ', ' UTC ', $date); + } $parsed_date = Carbon::parse($date); } catch (\Exception $e) { switch (true) { case preg_match('/([0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}\-[0-9]{1,2}\.[0-9]{1,2}.[0-9]{1,2})+$/i', $date) > 0: $date = Carbon::createFromFormat("Y.m.d-H.i.s", $date); break; + case preg_match('/([0-9]{2} [A-Z]{3} [0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+-][0-9]{1,4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+-][0-9]{1,4})+$/i', $date) > 0: + $parts = explode(' ', $date); + array_splice($parts, -2); + $date = implode(' ', $parts); + break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; From 647021478ce11fe65ec71fae222d38d3e2efe21e Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:42:56 +0100 Subject: [PATCH 468/600] changed the decode method to public --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 7d96b838..49aa4397 100644 --- a/src/Header.php +++ b/src/Header.php @@ -413,7 +413,7 @@ private function is_uft8($value): bool { * * @return mixed */ - private function decode(mixed $value): mixed { + public function decode(mixed $value): mixed { if (is_array($value)) { return $this->decodeArray($value); } From aaf186980e255a36ed0eb5edd98b3af0bae6d3b7 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:43:59 +0100 Subject: [PATCH 469/600] active_folder getter and setter added --- src/Client.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Client.php b/src/Client.php index b9a08053..33a05a42 100755 --- a/src/Client.php +++ b/src/Client.php @@ -660,6 +660,25 @@ public function openFolder(string $folder_path, bool $force_select = false): arr return $this->connection->selectFolder($folder_path)->validatedData(); } + /** + * Set active folder + * @param string|null $folder_path + * + * @return void + */ + public function setActiveFolder(?string $folder_path = null): void { + $this->active_folder = $folder_path; + } + + /** + * Get active folder + * + * @return string|null + */ + public function getActiveFolder(): ?string { + return $this->active_folder; + } + /** * Create a new Folder * @param string $folder_path From 1dad65fed741392ab9177935aaebc4d7df960706 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:44:46 +0100 Subject: [PATCH 470/600] Unset the currently active folder if it gets deleted --- src/Client.php | 3 +++ src/Folder.php | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/Client.php b/src/Client.php index 33a05a42..a4e004ba 100755 --- a/src/Client.php +++ b/src/Client.php @@ -731,6 +731,9 @@ public function deleteFolder(string $folder_path, bool $expunge = true): array { $this->checkConnection(); $folder = $this->getFolderByPath($folder_path); + if ($this->active_folder == $folder->path){ + $this->active_folder = null; + } $status = $this->getConnection()->deleteFolder($folder->path)->validatedData(); if ($expunge) $this->expunge(); diff --git a/src/Folder.php b/src/Folder.php index ddae8525..0089dc05 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -375,6 +375,10 @@ public function rename(string $new_name, bool $expunge = true): array { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); + if ($this->client->getActiveFolder() == $this->path){ + $this->client->setActiveFolder(null); + } + if ($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); From 8e0bec80ae856fa569ec3e8ac23313825044bbe5 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:46:26 +0100 Subject: [PATCH 471/600] Method to check if an encoding is supported added --- src/EncodingAliases.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 9ebdd958..888fc7fa 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -578,4 +578,14 @@ public static function getEncodings(): array { public static function isUtf7(string $encoding): bool { return str_contains(str_replace("-", "", strtolower($encoding)), "utf7"); } + + /** + * Check if an encoding is supported + * @param string $encoding + * + * @return bool + */ + public static function has(string $encoding): bool { + return isset(self::$aliases[strtolower($encoding)]); + } } From 0492379288374908bac9c74cebca2c19cb405f25 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:47:51 +0100 Subject: [PATCH 472/600] Attachment name and filename parsing fixed and improved to support more formats --- src/Attachment.php | 62 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index a6e10430..2c9c23cc 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -29,7 +29,9 @@ * @property string content_type * @property string id * @property string name - * @property string disposition + * @property string description + * @property string filename + * @property ?string disposition * @property string img_src * * @method integer getPartNumber() @@ -78,6 +80,8 @@ class Attachment { 'content_type' => null, 'id' => null, 'name' => null, + 'filename' => null, + 'description' => null, 'disposition' => null, 'img_src' => null, 'size' => null, @@ -209,21 +213,31 @@ protected function fetch(): void { $this->disposition = $this->part->disposition; if (($filename = $this->part->filename) !== null) { - $this->setName($filename); - } elseif (($name = $this->part->name) !== null) { - $this->setName($name); - }else { - $this->setName("undefined"); + $this->filename = $this->decodeName($filename); + } + + if (($name = $this->part->name) !== null) { + $this->name = $this->decodeName($name); + } + if (!$this->name && $this->filename != "") { + $this->name = $this->filename; } if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) { if ($this->part->ifdescription) { - $this->setName($this->part->description); - } else { - $this->setName($this->part->subtype); + if (!$this->name) { + $this->name = $this->part->description; + } + $this->description = $this->part->description; + } else if (!$this->name) { + $this->name = $this->part->subtype; } } + if (!$this->filename) { + $this->filename = $this->name; + } + $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); } @@ -241,18 +255,36 @@ public function save(string $path, string $filename = null): bool { } /** - * Set the attachment name and try to decode it + * Decode a given name * @param $name + * + * @return string */ - public function setName($name): void { - $decoder = $this->config['decoder']['attachment']; + public function decodeName($name): string { if ($name !== null) { + if (str_contains($name, "''")) { + $parts = explode("''", $name); + if (EncodingAliases::has($parts[0])) { + $name = implode("''", array_slice($parts, 1)); + } + } + + $decoder = $this->config['decoder']['message']; if($decoder === 'utf-8' && extension_loaded('imap')) { - $this->name = \imap_utf8($name); - }else{ - $this->name = mb_decode_mimeheader($name); + $name = \imap_utf8($name); + } + + if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { + $name = $this->part->getHeader()->decode($name); + } + + // check if $name is url encoded + if (preg_match('/%[0-9A-F]{2}/i', $name)) { + $name = urldecode($name); } + return $name; } + return ""; } /** From 5f800e1b1034392f512dba21d745204aa8f1a905 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:49:34 +0100 Subject: [PATCH 473/600] Fixture tests added --- phpunit.xml.dist | 1 + .../AttachmentEncodedFilenameTest.php | 51 +++++ tests/fixtures/AttachmentLongFilenameTest.php | 76 +++++++ .../fixtures/AttachmentNoDispositionTest.php | 51 +++++ tests/fixtures/BccTest.php | 43 ++++ tests/fixtures/BooleanDecodedContentTest.php | 54 +++++ tests/fixtures/DateTemplateTest.php | 112 ++++++++++ tests/fixtures/EmailAddressTest.php | 39 ++++ tests/fixtures/EmbeddedEmailTest.php | 63 ++++++ ...lWithoutContentDispositionEmbeddedTest.php | 73 +++++++ ...ddedEmailWithoutContentDispositionTest.php | 94 ++++++++ tests/fixtures/ExampleBounceTest.php | 97 +++++++++ tests/fixtures/FixtureTestCase.php | 93 ++++++++ tests/fixtures/FourNestedEmailsTest.php | 54 +++++ tests/fixtures/GbkCharsetTest.php | 37 ++++ tests/fixtures/HtmlOnlyTest.php | 37 ++++ .../ImapMimeHeaderDecodeReturnsFalseTest.php | 37 ++++ tests/fixtures/InlineAttachmentTest.php | 58 +++++ tests/fixtures/KsC56011987HeadersTest.php | 46 ++++ tests/fixtures/MailThatIsAttachmentTest.php | 62 ++++++ tests/fixtures/MissingDateTest.php | 37 ++++ tests/fixtures/MissingFromTest.php | 37 ++++ tests/fixtures/MixedFilenameTest.php | 60 ++++++ .../MultipleHtmlPartsAndAttachmentsTest.php | 74 +++++++ .../MultipleNestedAttachmentsTest.php | 67 ++++++ .../NestesEmbeddedWithAttachmentTest.php | 67 ++++++ tests/fixtures/NullContentCharsetTest.php | 39 ++++ tests/fixtures/PecTest.php | 79 +++++++ tests/fixtures/PlainOnlyTest.php | 37 ++++ tests/fixtures/PlainTextAttachmentTest.php | 53 +++++ tests/fixtures/ReferencesTest.php | 54 +++++ tests/fixtures/SimpleMultipartTest.php | 37 ++++ .../fixtures/StructuredWithAttachmentTest.php | 54 +++++ tests/fixtures/UndefinedCharsetHeaderTest.php | 59 +++++ .../UndisclosedRecipientsMinusTest.php | 42 ++++ .../UndisclosedRecipientsSpaceTest.php | 42 ++++ tests/fixtures/UnknownEncodingTest.php | 37 ++++ .../fixtures/WithoutCharsetPlainOnlyTest.php | 37 ++++ .../WithoutCharsetSimpleMultipartTest.php | 37 ++++ .../messages/attachment_encoded_filename.eml | 10 + tests/messages/attachment_long_filename.eml | 55 +++++ tests/messages/attachment_no_disposition.eml | 9 + tests/messages/bcc.eml | 12 ++ tests/messages/boolean_decoded_content.eml | 31 +++ tests/messages/date-template.eml | 8 + tests/messages/email_address.eml | 9 + tests/messages/embedded_email.eml | 63 ++++++ ...l_without_content_disposition-embedded.eml | 61 ++++++ ...dded_email_without_content_disposition.eml | 141 ++++++++++++ tests/messages/example_bounce.eml | 96 +++++++++ tests/messages/four_nested_emails.eml | 69 ++++++ tests/messages/gbk_charset.eml | 9 + tests/messages/html_only.eml | 9 + .../imap_mime_header_decode_returns_false.eml | 9 + tests/messages/inline_attachment.eml | 41 ++++ tests/messages/ks_c_5601-1987_headers.eml | 10 + tests/messages/mail_that_is_attachment.eml | 28 +++ tests/messages/missing_date.eml | 7 + tests/messages/missing_from.eml | 7 + tests/messages/mixed_filename.eml | 25 +++ .../multiple_html_parts_and_attachments.eml | 203 ++++++++++++++++++ .../messages/multiple_nested_attachments.eml | 84 ++++++++ .../nestes_embedded_with_attachment.eml | 145 +++++++++++++ tests/messages/null_content_charset.eml | 8 + tests/messages/pec.eml | 65 ++++++ tests/messages/plain_only.eml | 9 + tests/messages/plain_text_attachment.eml | 21 ++ tests/messages/references.eml | 11 + tests/messages/simple_multipart.eml | 23 ++ tests/messages/structured_with_attachment.eml | 24 +++ tests/messages/thread_my_topic.eml | 10 + tests/messages/thread_re_my_topic.eml | 11 + tests/messages/thread_unrelated.eml | 10 + tests/messages/undefined_charset_header.eml | 20 ++ .../messages/undisclosed_recipients_minus.eml | 8 + .../messages/undisclosed_recipients_space.eml | 8 + tests/messages/unknown_encoding.eml | 24 +++ tests/messages/without_charset_plain_only.eml | 8 + .../without_charset_simple_multipart.eml | 23 ++ 79 files changed, 3551 insertions(+) create mode 100644 tests/fixtures/AttachmentEncodedFilenameTest.php create mode 100644 tests/fixtures/AttachmentLongFilenameTest.php create mode 100644 tests/fixtures/AttachmentNoDispositionTest.php create mode 100644 tests/fixtures/BccTest.php create mode 100644 tests/fixtures/BooleanDecodedContentTest.php create mode 100644 tests/fixtures/DateTemplateTest.php create mode 100644 tests/fixtures/EmailAddressTest.php create mode 100644 tests/fixtures/EmbeddedEmailTest.php create mode 100644 tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php create mode 100644 tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php create mode 100644 tests/fixtures/ExampleBounceTest.php create mode 100644 tests/fixtures/FixtureTestCase.php create mode 100644 tests/fixtures/FourNestedEmailsTest.php create mode 100644 tests/fixtures/GbkCharsetTest.php create mode 100644 tests/fixtures/HtmlOnlyTest.php create mode 100644 tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php create mode 100644 tests/fixtures/InlineAttachmentTest.php create mode 100644 tests/fixtures/KsC56011987HeadersTest.php create mode 100644 tests/fixtures/MailThatIsAttachmentTest.php create mode 100644 tests/fixtures/MissingDateTest.php create mode 100644 tests/fixtures/MissingFromTest.php create mode 100644 tests/fixtures/MixedFilenameTest.php create mode 100644 tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php create mode 100644 tests/fixtures/MultipleNestedAttachmentsTest.php create mode 100644 tests/fixtures/NestesEmbeddedWithAttachmentTest.php create mode 100644 tests/fixtures/NullContentCharsetTest.php create mode 100644 tests/fixtures/PecTest.php create mode 100644 tests/fixtures/PlainOnlyTest.php create mode 100644 tests/fixtures/PlainTextAttachmentTest.php create mode 100644 tests/fixtures/ReferencesTest.php create mode 100644 tests/fixtures/SimpleMultipartTest.php create mode 100644 tests/fixtures/StructuredWithAttachmentTest.php create mode 100644 tests/fixtures/UndefinedCharsetHeaderTest.php create mode 100644 tests/fixtures/UndisclosedRecipientsMinusTest.php create mode 100644 tests/fixtures/UndisclosedRecipientsSpaceTest.php create mode 100644 tests/fixtures/UnknownEncodingTest.php create mode 100644 tests/fixtures/WithoutCharsetPlainOnlyTest.php create mode 100644 tests/fixtures/WithoutCharsetSimpleMultipartTest.php create mode 100644 tests/messages/attachment_encoded_filename.eml create mode 100644 tests/messages/attachment_long_filename.eml create mode 100644 tests/messages/attachment_no_disposition.eml create mode 100644 tests/messages/bcc.eml create mode 100644 tests/messages/boolean_decoded_content.eml create mode 100644 tests/messages/date-template.eml create mode 100644 tests/messages/email_address.eml create mode 100644 tests/messages/embedded_email.eml create mode 100644 tests/messages/embedded_email_without_content_disposition-embedded.eml create mode 100644 tests/messages/embedded_email_without_content_disposition.eml create mode 100644 tests/messages/example_bounce.eml create mode 100644 tests/messages/four_nested_emails.eml create mode 100644 tests/messages/gbk_charset.eml create mode 100644 tests/messages/html_only.eml create mode 100644 tests/messages/imap_mime_header_decode_returns_false.eml create mode 100644 tests/messages/inline_attachment.eml create mode 100644 tests/messages/ks_c_5601-1987_headers.eml create mode 100644 tests/messages/mail_that_is_attachment.eml create mode 100644 tests/messages/missing_date.eml create mode 100644 tests/messages/missing_from.eml create mode 100644 tests/messages/mixed_filename.eml create mode 100644 tests/messages/multiple_html_parts_and_attachments.eml create mode 100644 tests/messages/multiple_nested_attachments.eml create mode 100644 tests/messages/nestes_embedded_with_attachment.eml create mode 100644 tests/messages/null_content_charset.eml create mode 100644 tests/messages/pec.eml create mode 100644 tests/messages/plain_only.eml create mode 100644 tests/messages/plain_text_attachment.eml create mode 100644 tests/messages/references.eml create mode 100644 tests/messages/simple_multipart.eml create mode 100644 tests/messages/structured_with_attachment.eml create mode 100644 tests/messages/thread_my_topic.eml create mode 100644 tests/messages/thread_re_my_topic.eml create mode 100644 tests/messages/thread_unrelated.eml create mode 100644 tests/messages/undefined_charset_header.eml create mode 100644 tests/messages/undisclosed_recipients_minus.eml create mode 100644 tests/messages/undisclosed_recipients_space.eml create mode 100644 tests/messages/unknown_encoding.eml create mode 100644 tests/messages/without_charset_plain_only.eml create mode 100644 tests/messages/without_charset_simple_multipart.eml diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 76b56dfe..02d56a89 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,6 +13,7 @@ tests + tests/fixtures tests/issues tests/live diff --git a/tests/fixtures/AttachmentEncodedFilenameTest.php b/tests/fixtures/AttachmentEncodedFilenameTest.php new file mode 100644 index 00000000..33d97fde --- /dev/null +++ b/tests/fixtures/AttachmentEncodedFilenameTest.php @@ -0,0 +1,51 @@ +getFixture("attachment_encoded_filename.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.ms-excel", $attachment->content_type); + self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); + self::assertEquals(146, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php new file mode 100644 index 00000000..b3c3e903 --- /dev/null +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -0,0 +1,76 @@ +getFixture("attachment_long_filename.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + $attachments = $message->attachments(); + self::assertCount(3, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXX xxxxxxxxxxxxxxxxx XxxX, Lüdxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->name); + self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXXX xxxxxxxxxxxxxxxxx XxxX, Lüxxxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->filename); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php new file mode 100644 index 00000000..5a3fadd8 --- /dev/null +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -0,0 +1,51 @@ +getFixture("attachment_no_disposition.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.ms-excel", $attachment->content_type); + self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); + self::assertEquals(146, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/BccTest.php b/tests/fixtures/BccTest.php new file mode 100644 index 00000000..8de14f62 --- /dev/null +++ b/tests/fixtures/BccTest.php @@ -0,0 +1,43 @@ +getFixture("bcc.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("", $message->return_path); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("text/plain", $message->content_type); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals("to@here.com", $message->to); + self::assertEquals("A_€@{è_Z ", $message->bcc); + self::assertEquals("sender@here.com", $message->sender); + self::assertEquals("reply-to@here.com", $message->reply_to); + } +} \ No newline at end of file diff --git a/tests/fixtures/BooleanDecodedContentTest.php b/tests/fixtures/BooleanDecodedContentTest.php new file mode 100644 index 00000000..ae9b797f --- /dev/null +++ b/tests/fixtures/BooleanDecodedContentTest.php @@ -0,0 +1,54 @@ +getFixture("boolean_decoded_content.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Here is the problem mail\r\n \r\nBody text", $message->getTextBody()); + self::assertEquals("Here is the problem mail\r\n \r\nBody text", $message->getHTMLBody()); + + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals("to@here.com", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Example Domain.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); + self::assertEquals(53, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php new file mode 100644 index 00000000..4bf8f114 --- /dev/null +++ b/tests/fixtures/DateTemplateTest.php @@ -0,0 +1,112 @@ + "2019-04-05 10:10:49", + "04 Jan 2018 10:12:47 UT" => "2018-01-04 10:12:47", + "22 Jun 18 03:56:36 PM -05:00 (GMT -05:00)" => "2018-06-22 20:56:36", + "Sat, 31 Aug 2013 20:08:23 +0580" => "2013-08-31 14:38:23", + "Fri, 1 Feb 2019 01:30:04 +0600 (+06)" => "2019-01-31 19:30:04", + "Mon, 4 Feb 2019 04:03:49 -0300 (-03)" => "2019-02-04 07:03:49", + "Sun, 6 Apr 2008 21:24:33 UT" => "2008-04-06 21:24:33", + "Wed, 11 Sep 2019 15:23:06 +0600 (+06)" => "2019-09-11 09:23:06", + "14 Sep 2019 00:10:08 UT +0200" => "2019-09-14 00:10:08", + "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", + "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", + ]; + + /** + * Test the fixture date-template.eml + * + * @return void + * @throws InvalidMessageDateException + * @throws ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFixture() : void { + try { + $message = $this->getFixture("date-template.eml"); + $this->fail("Expected InvalidMessageDateException"); + } catch (InvalidMessageDateException $e) { + self::assertTrue(true); + } + + self::$manager->setConfig([ + "options" => [ + "fallback_date" => "2021-01-01 00:00:00", + ], + ]); + $message = $this->getFixture("date-template.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2021-01-01 00:00:00", $message->date->first()->timezone("UTC")->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", (string)$message->from); + self::assertEquals("to@here.com", $message->to); + + self::$manager->setConfig([ + "options" => [ + "fallback_date" => null, + ], + ]); + + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "date-template.eml"]); + $blob = file_get_contents($filename); + self::assertNotFalse($blob); + + foreach ($this->dates as $date => $expected) { + $message = Message::fromString(str_replace("%date_raw_header%", $date, $blob)); + self::assertEquals("test", $message->subject); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals($expected, $message->date->first()->timezone("UTC")->format("Y-m-d H:i:s"), "Date \"$date\" should be \"$expected\""); + self::assertEquals("from@there.com", (string)$message->from); + self::assertEquals("to@here.com", $message->to); + } + } +} \ No newline at end of file diff --git a/tests/fixtures/EmailAddressTest.php b/tests/fixtures/EmailAddressTest.php new file mode 100644 index 00000000..d4e403d7 --- /dev/null +++ b/tests/fixtures/EmailAddressTest.php @@ -0,0 +1,39 @@ +getFixture("email_address.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("123@example.com", $message->message_id); + self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("no_host@UNKNOWN", (string)$message->from); + self::assertEquals("", $message->to); + self::assertEquals("This one: is \"right\" , No-address@UNKNOWN", $message->cc); + } +} \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailTest.php b/tests/fixtures/EmbeddedEmailTest.php new file mode 100644 index 00000000..4d8baf77 --- /dev/null +++ b/tests/fixtures/EmbeddedEmailTest.php @@ -0,0 +1,63 @@ +getFixture("embedded_email.eml"); + + self::assertEquals("embedded message", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("7e5798da5747415e5b82fdce042ab2a6@cerstor.cz", $message->message_id); + self::assertEquals("demo@cerstor.cz", $message->return_path); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Roundcube Webmail/1.0.0", $message->user_agent); + self::assertEquals("email that contains embedded message", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2016-01-29 13:25:40", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->x_sender); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("demo.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("a1f965f10a9872e902a82dde039a237e863f522d238a1cb1968fe3396dbcac65", hash("sha256", $attachment->content)); + self::assertEquals(893, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php new file mode 100644 index 00000000..e7629bc3 --- /dev/null +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -0,0 +1,73 @@ +getFixture("embedded_email_without_content_disposition-embedded.eml"); + + self::assertEquals("embedded_message_subject", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D40E38@VM002.cerk.cc", $message->message_id); + self::assertEquals("pl-PL, nl-NL", $message->accept_language); + self::assertEquals("pl-PL", $message->content_language); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("some txt", $message->getTextBody()); + self::assertEquals("\r\n

some txt

\r\n", $message->getHTMLBody()); + + self::assertEquals("2019-04-05 10:10:49", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file1.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file2.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php new file mode 100644 index 00000000..c62e743c --- /dev/null +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -0,0 +1,94 @@ +getFixture("embedded_email_without_content_disposition.eml"); + + self::assertEquals("Subject", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D9F7E4@VM002.cerk.cc", $message->message_id); + self::assertEquals("pl-PL, nl-NL", $message->accept_language); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("TexT\r\n\r\n[cid:file.jpg]", $message->getTextBody()); + self::assertEquals("

TexT

", $message->getHTMLBody()); + + self::assertEquals("2019-04-05 11:48:50", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(4, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file.jpg", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/jpeg", $attachment->content_type); + self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); + self::assertEquals(96, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); + self::assertEquals(2073, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file3.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[3]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file4.zip", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/x-zip-compressed", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(7, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php new file mode 100644 index 00000000..d032f316 --- /dev/null +++ b/tests/fixtures/ExampleBounceTest.php @@ -0,0 +1,97 @@ +getFixture("example_bounce.eml"); + + self::assertEquals("<>", $message->return_path); + self::assertEquals([ + 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', + 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', + 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', + ], $message->received->all()); + self::assertEquals("demo@foo.de", $message->envelope_to); + self::assertEquals("Thu, 02 Mar 2023 05:27:29 +0100", $message->delivery_date); + self::assertEquals([ + 0 => 'somewhere.your-server.de; iprev=pass (somewhere06.your-server.de) smtp.remote-ip=1b21:2f8:e0a:50e4::2; spf=none smtp.mailfrom=<>; dmarc=skipped', + 1 => 'somewhere.your-server.de' + ], $message->authentication_results->all()); + self::assertEquals([ + 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', + 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', + 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', + ], $message->received->all()); + self::assertEquals("ding@ding.de", $message->x_failed_recipients); + self::assertEquals("auto-replied", $message->auto_submitted); + self::assertEquals("Mail Delivery System ", $message->from); + self::assertEquals("demo@foo.de", $message->to); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Mail delivery failed", $message->subject); + self::assertEquals("E1pXaXO-0008gb-6g@sslproxy01.your-server.de", $message->message_id); + self::assertEquals("2023-03-02 04:27:26", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("Clear (ClamAV 0.103.8/26827/Wed Mar 1 09:28:49 2023)", $message->x_virus_scanned); + self::assertEquals("0.0 (/)", $message->x_spam_score); + self::assertEquals("bar-demo@foo.de", $message->delivered_to); + self::assertEquals("multipart/report", $message->content_type->last()); + self::assertEquals("5d4847c21c8891e73d62c8246f260a46496958041a499f33ecd47444fdaa591b", hash("sha256", $message->getTextBody())); + self::assertFalse($message->hasHTMLBody()); + + $attachments = $message->attachments(); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->filename); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/delivery-status", $attachment->content_type); + self::assertEquals("85ac09d1d74b2d85853084dc22abcad205a6bfde62d6056e3a933ffe7e82e45c", hash("sha256", $attachment->content)); + self::assertEquals(267, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->filename); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("7525331f5fab23ea77f595b995336aca7b8dad12db00ada14abebe7fe5b96e10", hash("sha256", $attachment->content)); + self::assertEquals(776, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/FixtureTestCase.php b/tests/fixtures/FixtureTestCase.php new file mode 100644 index 00000000..8660bedc --- /dev/null +++ b/tests/fixtures/FixtureTestCase.php @@ -0,0 +1,93 @@ + [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'default' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)] + ], + ], + ]); + return self::$manager; + } + + /** + * Get a fixture message + * @param string $template + * + * @return Message + * @throws ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final public function getFixture(string $template) : Message { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); + $message = Message::fromFile($filename); + self::assertInstanceOf(Message::class, $message); + + return $message; + } +} \ No newline at end of file diff --git a/tests/fixtures/FourNestedEmailsTest.php b/tests/fixtures/FourNestedEmailsTest.php new file mode 100644 index 00000000..5511c05e --- /dev/null +++ b/tests/fixtures/FourNestedEmailsTest.php @@ -0,0 +1,54 @@ +getFixture("four_nested_emails.eml"); + + self::assertEquals("3-third-subject", $message->subject); + self::assertEquals("3-third-content", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("test@example.com", $message->from->first()->mail); + self::assertEquals("test@example.com", $message->to->first()->mail); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("2-second-email.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("85012e6a26d064a0288ee62618b3192687385adb4a4e27e48a28f738a325ca46", hash("sha256", $attachment->content)); + self::assertEquals(1376, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + } +} \ No newline at end of file diff --git a/tests/fixtures/GbkCharsetTest.php b/tests/fixtures/GbkCharsetTest.php new file mode 100644 index 00000000..7492d908 --- /dev/null +++ b/tests/fixtures/GbkCharsetTest.php @@ -0,0 +1,37 @@ +getFixture("gbk_charset.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/HtmlOnlyTest.php b/tests/fixtures/HtmlOnlyTest.php new file mode 100644 index 00000000..90ef44e3 --- /dev/null +++ b/tests/fixtures/HtmlOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("html_only.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Hi", $message->getHTMLBody()); + self::assertFalse($message->hasTextBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php new file mode 100644 index 00000000..6f2629ff --- /dev/null +++ b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php @@ -0,0 +1,37 @@ +getFixture("imap_mime_header_decode_returns_false.eml"); + + self::assertEquals("?p?#]ݰ?[??W̌ N? ?LL?̍L??NL˜", $message->subject->first()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php new file mode 100644 index 00000000..ae538b37 --- /dev/null +++ b/tests/fixtures/InlineAttachmentTest.php @@ -0,0 +1,58 @@ +getFixture("inline_attachment.eml"); + + self::assertEquals("", $message->subject); + self::assertFalse($message->hasTextBody()); + self::assertEquals('', $message->getHTMLBody()); + + self::assertFalse($message->date->first()); + self::assertFalse($message->from->first()); + self::assertFalse($message->to->first()); + + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("6568c9e9c35a7fa06f236e89f704d8c9b47183a24f2c978dba6c92e2747e3a13", hash("sha256", $attachment->content)); + self::assertEquals(1486, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertEquals("", $attachment->content_id); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/KsC56011987HeadersTest.php b/tests/fixtures/KsC56011987HeadersTest.php new file mode 100644 index 00000000..944e9bfb --- /dev/null +++ b/tests/fixtures/KsC56011987HeadersTest.php @@ -0,0 +1,46 @@ +getFixture("ks_c_5601-1987_headers.eml"); + + self::assertEquals("RE: 회원님께 Ersi님이 메시지를 보냈습니다.", $message->subject); + self::assertEquals("=?ks_c_5601-1987?B?yLi/+LTUsrIgRXJzabTUwMwguN69w8H2uKYgurizwr3AtM+02S4=?=", $message->thread_topic); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Content", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("to@here.com", $message->to->first()->mail); + + + $from = $message->from->first(); + self::assertEquals("김 현진", $from->personal); + self::assertEquals("from", $from->mailbox); + self::assertEquals("there.com", $from->host); + self::assertEquals("from@there.com", $from->mail); + self::assertEquals("김 현진 ", $from->full); + } +} \ No newline at end of file diff --git a/tests/fixtures/MailThatIsAttachmentTest.php b/tests/fixtures/MailThatIsAttachmentTest.php new file mode 100644 index 00000000..e28dc938 --- /dev/null +++ b/tests/fixtures/MailThatIsAttachmentTest.php @@ -0,0 +1,62 @@ +getFixture("mail_that_is_attachment.eml"); + + self::assertEquals("Report domain: yyy.cz Submitter: google.com Report-ID: 2244696771454641389", $message->subject); + self::assertEquals("2244696771454641389@google.com", $message->message_id); + self::assertEquals("1.0", $message->mime_version); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2015-02-15 10:21:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("xxx@yyy.cz", $message->to->first()->mail); + self::assertEquals("xxx@yyy.cz", $message->sender->first()->mail); + + $from = $message->from->first(); + self::assertEquals("noreply-dmarc-support via xxx", $from->personal); + self::assertEquals("xxx", $from->mailbox); + self::assertEquals("yyy.cz", $from->host); + self::assertEquals("xxx@yyy.cz", $from->mail); + self::assertEquals("noreply-dmarc-support via xxx ", $from->full); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("google.com!yyy.cz!1423872000!1423958399.zip", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/zip", $attachment->content_type); + self::assertEquals("c0d4f47b6fde124cea7460c3e509440d1a062705f550b0502b8ba0cbf621c97a", hash("sha256", $attachment->content)); + self::assertEquals(1062, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/MissingDateTest.php b/tests/fixtures/MissingDateTest.php new file mode 100644 index 00000000..93595adc --- /dev/null +++ b/tests/fixtures/MissingDateTest.php @@ -0,0 +1,37 @@ +getFixture("missing_date.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("from@here.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/MissingFromTest.php b/tests/fixtures/MissingFromTest.php new file mode 100644 index 00000000..5d57340c --- /dev/null +++ b/tests/fixtures/MissingFromTest.php @@ -0,0 +1,37 @@ +getFixture("missing_from.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertFalse($message->from->first()); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/MixedFilenameTest.php b/tests/fixtures/MixedFilenameTest.php new file mode 100644 index 00000000..fc1ffa88 --- /dev/null +++ b/tests/fixtures/MixedFilenameTest.php @@ -0,0 +1,60 @@ +getFixture("mixed_filename.eml"); + + self::assertEquals("Свежий прайс-лист", $message->subject); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2018-02-02 19:23:06", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertEquals("Прайсы || ПартКом", $from->personal); + self::assertEquals("support", $from->mailbox); + self::assertEquals("part-kom.ru", $from->host); + self::assertEquals("support@part-kom.ru", $from->mail); + self::assertEquals("Прайсы || ПартКом ", $from->full); + + self::assertEquals("foo@bar.com", $message->to->first()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Price4VladDaKar.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/octet-stream", $attachment->content_type); + self::assertEquals("b832983842b0ad65db69e4c7096444c540a2393e2d43f70c2c9b8b9fceeedbb1", hash('sha256', $attachment->content)); + self::assertEquals(94, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php new file mode 100644 index 00000000..c8f72bb8 --- /dev/null +++ b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -0,0 +1,74 @@ +getFixture("multiple_html_parts_and_attachments.eml"); + + self::assertEquals("multiple_html_parts_and_attachments", $message->subject); + self::assertEquals("This is the first html part\r\n\r\n\r\n\r\nThis is the second html part\r\n\r\n\r\n\r\nThis is the last html part\r\nhttps://www.there.com", $message->getTextBody()); + self::assertEquals("This is the first html part

\n

This is the second html part

\n

This is the last html part
https://www.there.com



\r\n
", $message->getHTMLBody()); + + self::assertEquals("2023-02-16 09:19:02", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertEquals("FromName", $from->personal); + self::assertEquals("from", $from->mailbox); + self::assertEquals("there.com", $from->host); + self::assertEquals("from@there.com", $from->mail); + self::assertEquals("FromName ", $from->full); + + self::assertEquals("to@there.com", $message->to->first()); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("attachment1.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); + self::assertEquals(4814, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("attachment2.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); + self::assertEquals(5090, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/MultipleNestedAttachmentsTest.php b/tests/fixtures/MultipleNestedAttachmentsTest.php new file mode 100644 index 00000000..e7b9cbdf --- /dev/null +++ b/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -0,0 +1,67 @@ +getFixture("multiple_nested_attachments.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("------------------------------------------------------------------------", $message->getTextBody()); + self::assertEquals("\r\n \r\n\r\n \r\n \r\n \r\n


\r\n

\r\n
\r\n \r\n \r\n  \"\"\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

\r\n

\r\n
\r\n
\r\n \r\n", $message->getHTMLBody()); + + self::assertEquals("2018-01-15 09:54:09", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertFalse($message->from->first()); + self::assertFalse($message->to->first()); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("mleokdgdlgkkecep.png", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); + self::assertEquals(114, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("FF4D00-1.png", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); + self::assertEquals(114, $attachment->size); + self::assertEquals(8, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php new file mode 100644 index 00000000..a1db7378 --- /dev/null +++ b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -0,0 +1,67 @@ +getFixture("nestes_embedded_with_attachment.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Dear Sarah", $message->getTextBody()); + self::assertEquals("\r\n\r\n
Dear Sarah,
\r\n", $message->getHTMLBody()); + + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("first.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); + self::assertEquals(2535, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("second.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); + self::assertEquals(631, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/NullContentCharsetTest.php b/tests/fixtures/NullContentCharsetTest.php new file mode 100644 index 00000000..f8e6c63f --- /dev/null +++ b/tests/fixtures/NullContentCharsetTest.php @@ -0,0 +1,39 @@ +getFixture("null_content_charset.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertEquals("1.0", $message->mime_version); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/PecTest.php b/tests/fixtures/PecTest.php new file mode 100644 index 00000000..01fe17c5 --- /dev/null +++ b/tests/fixtures/PecTest.php @@ -0,0 +1,79 @@ +getFixture("pec.eml"); + + self::assertEquals("Certified", $message->subject); + self::assertEquals("Signed", $message->getTextBody()); + self::assertEquals("Signed", $message->getHTMLBody()); + + self::assertEquals("2017-10-02 10:13:43", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("test@example.com", $message->from->first()->mail); + self::assertEquals("test@example.com", $message->to->first()->mail); + + $attachments = $message->attachments(); + + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(3, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("data.xml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/xml", $attachment->content_type); + self::assertEquals("", $attachment->content); + self::assertEquals(8, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("postacert.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); + self::assertEquals(216, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("smime.p7s", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); + self::assertEquals("1", $attachment->content); + self::assertEquals(4, $attachment->size); + self::assertEquals(7, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/PlainOnlyTest.php b/tests/fixtures/PlainOnlyTest.php new file mode 100644 index 00000000..b3a65bfe --- /dev/null +++ b/tests/fixtures/PlainOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("plain_only.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/PlainTextAttachmentTest.php b/tests/fixtures/PlainTextAttachmentTest.php new file mode 100644 index 00000000..6106b558 --- /dev/null +++ b/tests/fixtures/PlainTextAttachmentTest.php @@ -0,0 +1,53 @@ +getFixture("plain_text_attachment.eml"); + + self::assertEquals("Plain text attachment", $message->subject); + self::assertEquals("Test", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2018-08-21 07:05:14", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("a.txt", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertNull($attachment->content_type); + self::assertEquals("Hi!", $attachment->content); + self::assertEquals(4, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/ReferencesTest.php b/tests/fixtures/ReferencesTest.php new file mode 100644 index 00000000..18473d61 --- /dev/null +++ b/tests/fixtures/ReferencesTest.php @@ -0,0 +1,54 @@ +getFixture("references.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + + self::assertEquals("b9e87bd5e661a645ed6e3b832828fcc5@example.com", $message->in_reply_to); + self::assertEquals("", $message->from->first()->personal); + self::assertEquals("UNKNOWN", $message->from->first()->host); + self::assertEquals("no_host@UNKNOWN", $message->from->first()->mail); + self::assertFalse($message->to->first()); + + self::assertEquals([ + "231d9ac57aec7d8c1a0eacfeab8af6f3@example.com", + "08F04024-A5B3-4FDE-BF2C-6710DE97D8D9@example.com" + ], $message->getReferences()->all()); + + self::assertEquals([ + 'This one: is "right" ', + 'No-address@UNKNOWN' + ], $message->cc->map(function($address){ + /** @var \Webklex\PHPIMAP\Address $address */ + return $address->full; + })); + } +} \ No newline at end of file diff --git a/tests/fixtures/SimpleMultipartTest.php b/tests/fixtures/SimpleMultipartTest.php new file mode 100644 index 00000000..d2a2d885 --- /dev/null +++ b/tests/fixtures/SimpleMultipartTest.php @@ -0,0 +1,37 @@ +getFixture("simple_multipart.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/StructuredWithAttachmentTest.php b/tests/fixtures/StructuredWithAttachmentTest.php new file mode 100644 index 00000000..a49d3609 --- /dev/null +++ b/tests/fixtures/StructuredWithAttachmentTest.php @@ -0,0 +1,54 @@ +getFixture("structured_with_attachment.eml"); + + self::assertEquals("Test", $message->getSubject()); + self::assertEquals("Test", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2017-09-29 08:55:23", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("MyFile.txt", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("MyFileContent", $attachment->content); + self::assertEquals(20, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/UndefinedCharsetHeaderTest.php b/tests/fixtures/UndefinedCharsetHeaderTest.php new file mode 100644 index 00000000..acb3029c --- /dev/null +++ b/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -0,0 +1,59 @@ +getFixture("undefined_charset_header.eml"); + + self::assertEquals("", $message->get("x-real-to")); + self::assertEquals("1.0", $message->get("mime-version")); + self::assertEquals("Mon, 27 Feb 2017 13:21:44 +0930", $message->get("Resent-Date")); + self::assertEquals("", $message->get("Resent-From")); + self::assertEquals("BlaBla", $message->get("X-Stored-In")); + self::assertEquals("", $message->get("Return-Path")); + self::assertEquals([ + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804' + ], $message->get("Received")->all()); + self::assertEquals(")", $message->getHTMLBody()); + self::assertFalse($message->hasTextBody()); + self::assertEquals("2017-02-27 03:51:29", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertInstanceOf(Address::class, $from); + + self::assertEquals("myGov", $from->personal); + self::assertEquals("info", $from->mailbox); + self::assertEquals("bla.bla", $from->host); + self::assertEquals("info@bla.bla", $from->mail); + self::assertEquals("myGov ", $from->full); + + self::assertEquals("sales@bla.bla", $message->to->first()->mail); + self::assertEquals("Submit your tax refund | Australian Taxation Office.", $message->subject); + self::assertEquals("201702270351.BGF77614@bla.bla", $message->message_id); + } +} \ No newline at end of file diff --git a/tests/fixtures/UndisclosedRecipientsMinusTest.php b/tests/fixtures/UndisclosedRecipientsMinusTest.php new file mode 100644 index 00000000..f9ee7999 --- /dev/null +++ b/tests/fixtures/UndisclosedRecipientsMinusTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients_minus.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "undisclosed-recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/tests/fixtures/UndisclosedRecipientsSpaceTest.php b/tests/fixtures/UndisclosedRecipientsSpaceTest.php new file mode 100644 index 00000000..b86321eb --- /dev/null +++ b/tests/fixtures/UndisclosedRecipientsSpaceTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients_space.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "Undisclosed recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/tests/fixtures/UnknownEncodingTest.php b/tests/fixtures/UnknownEncodingTest.php new file mode 100644 index 00000000..3a570cbd --- /dev/null +++ b/tests/fixtures/UnknownEncodingTest.php @@ -0,0 +1,37 @@ +getFixture("unknown_encoding.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/WithoutCharsetPlainOnlyTest.php b/tests/fixtures/WithoutCharsetPlainOnlyTest.php new file mode 100644 index 00000000..497334d5 --- /dev/null +++ b/tests/fixtures/WithoutCharsetPlainOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("without_charset_plain_only.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/WithoutCharsetSimpleMultipartTest.php b/tests/fixtures/WithoutCharsetSimpleMultipartTest.php new file mode 100644 index 00000000..2a9ea2a0 --- /dev/null +++ b/tests/fixtures/WithoutCharsetSimpleMultipartTest.php @@ -0,0 +1,37 @@ +getFixture("without_charset_simple_multipart.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/messages/attachment_encoded_filename.eml b/tests/messages/attachment_encoded_filename.eml new file mode 100644 index 00000000..231d0452 --- /dev/null +++ b/tests/messages/attachment_encoded_filename.eml @@ -0,0 +1,10 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: application/vnd.ms-excel; name="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?="; charset="UTF-8" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?=" + +0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAACAAAAwgAAAAAA +AAAAEAAA/v///wAAAAD+////AAAAAMAAAADBAAAA//////////////////////////////// diff --git a/tests/messages/attachment_long_filename.eml b/tests/messages/attachment_long_filename.eml new file mode 100644 index 00000000..8630280f --- /dev/null +++ b/tests/messages/attachment_long_filename.eml @@ -0,0 +1,55 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain; + name*0*=utf-8''Buchungsbest%C3%A4tigung-%20Rechnung-Gesch%C3%A4ftsbedingung; + name*1*=en-Nr.B123-45%20-%20XXXX%20xxxxxxxxxxxxxxxxx%20XxxX%2C%20L%C3%BCd; + name*2*=xxxxxxxx%20-%20VM%20Klaus%20XXXXXX%20-%20xxxxxxxx.pdf +Content-Disposition: attachment; + filename*0*=utf-8''Buchungsbest%C3%A4tigung-%20Rechnung-Gesch%C3%A4ftsbedin; + filename*1*=gungen-Nr.B123-45%20-%20XXXXX%20xxxxxxxxxxxxxxxxx%20XxxX%2C; + filename*2*=%20L%C3%BCxxxxxxxxxx%20-%20VM%20Klaus%20XXXXXX%20-%20xxxxxxxx.p; + filename*3*=df +Content-Transfer-Encoding: base64 + +SGkh +--BOUNDARY +Content-Type: text/plain; charset=UTF-8; + name="=?UTF-8?B?MDFfQeKCrMOgw6TEhdCx2YrYr0BaLTAxMjM0NTY3ODktcXdlcnR5dWlv?= + =?UTF-8?Q?pasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzx?= + =?UTF-8?Q?cvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstu?= + =?UTF-8?Q?vz.txt?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=iso-8859-15''%30%31%5F%41%A4%E0%E4%3F%3F%3F%3F%40%5A%2D%30%31; + filename*1*=%32%33%34%35%36%37%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61; + filename*2*=%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73; + filename*3*=%74%75%76%7A%2D%30%31%32%33%34%35%36%37%38%39%2D%71%77%65%72; + filename*4*=%74%79%75%69%6F%70%61%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62; + filename*5*=%6E%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31%32%33%34%35%36%37; + filename*6*=%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61%73%64%66%67%68%6A; + filename*7*=%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73%74%75%76%7A%2E%74; + filename*8*=%78%74 + +SGkh +--BOUNDARY +Content-Type: text/plain; charset=UTF-8; + name="=?UTF-8?B?MDJfQeKCrMOgw6TEhdCx2YrYr0BaLTAxMjM0NTY3ODktcXdlcnR5dWlv?= + =?UTF-8?Q?pasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzx?= + =?UTF-8?Q?cvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstu?= + =?UTF-8?Q?vz.txt?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=UTF-8''%30%32%5F%41%E2%82%AC%C3%A0%C3%A4%C4%85%D0%B1%D9%8A%D8; + filename*1*=%AF%40%5A%2D%30%31%32%33%34%35%36%37%38%39%2D%71%77%65%72%74; + filename*2*=%79%75%69%6F%70%61%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E; + filename*3*=%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31%32%33%34%35%36%37%38; + filename*4*=%39%2D%71%77%65%72%74%79%75%69%6F%70%61%73%64%66%67%68%6A%6B; + filename*5*=%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31; + filename*6*=%32%33%34%35%36%37%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61; + filename*7*=%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73; + filename*8*=%74%75%76%7A%2E%74%78%74 + +SGkh +--BOUNDARY-- diff --git a/tests/messages/attachment_no_disposition.eml b/tests/messages/attachment_no_disposition.eml new file mode 100644 index 00000000..ce0ea3e9 --- /dev/null +++ b/tests/messages/attachment_no_disposition.eml @@ -0,0 +1,9 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: application/vnd.ms-excel; name="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?="; charset="UTF-8" +Content-Transfer-Encoding: base64 + +0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAACAAAAwgAAAAAA +AAAAEAAA/v///wAAAAD+////AAAAAMAAAADBAAAA//////////////////////////////// diff --git a/tests/messages/bcc.eml b/tests/messages/bcc.eml new file mode 100644 index 00000000..1a6f16a8 --- /dev/null +++ b/tests/messages/bcc.eml @@ -0,0 +1,12 @@ +Return-Path: +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: to@here.com +Bcc: =?UTF-8?B?QV/igqxAe8OoX1o=?= +Reply-To: reply-to@here.com +Sender: sender@here.com + +Hi! diff --git a/tests/messages/boolean_decoded_content.eml b/tests/messages/boolean_decoded_content.eml new file mode 100644 index 00000000..91818038 --- /dev/null +++ b/tests/messages/boolean_decoded_content.eml @@ -0,0 +1,31 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: multipart/mixed; boundary="=-vyqYb0SSRwuGFKv/Trdf" + +--=-vyqYb0SSRwuGFKv/Trdf +Content-Type: multipart/alternative; boundary="=-ewUvwipK68Y6itClYNpy" + +--=-ewUvwipK68Y6itClYNpy +Content-Type: text/plain; charset="us-ascii" + +Here is the problem mail + +Body text +--=-ewUvwipK68Y6itClYNpy +Content-Type: text/html; charset="us-ascii" + +Here is the problem mail + +Body text +--=-ewUvwipK68Y6itClYNpy-- + +--=-vyqYb0SSRwuGFKv/Trdf +Content-Type: application/pdf; name="Example Domain.pdf" +Content-Disposition: attachment; filename="Example Domain.pdf" +Content-Transfer-Encoding: base64 + +nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?= + +--=-vyqYb0SSRwuGFKv/Trdf-- diff --git a/tests/messages/date-template.eml b/tests/messages/date-template.eml new file mode 100644 index 00000000..9354fd10 --- /dev/null +++ b/tests/messages/date-template.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: %date_raw_header% +From: from@there.com +To: to@here.com + +Hi! diff --git a/tests/messages/email_address.eml b/tests/messages/email_address.eml new file mode 100644 index 00000000..62b76d70 --- /dev/null +++ b/tests/messages/email_address.eml @@ -0,0 +1,9 @@ +Message-ID: <123@example.com> +From: no_host +Cc: "This one: is \"right\"" , No-address +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi +How are you? diff --git a/tests/messages/embedded_email.eml b/tests/messages/embedded_email.eml new file mode 100644 index 00000000..293bc28e --- /dev/null +++ b/tests/messages/embedded_email.eml @@ -0,0 +1,63 @@ +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_5d4b2de51e6aad0435b4bfbd7a23594a" +Date: Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Subject: embedded message +Message-ID: <7e5798da5747415e5b82fdce042ab2a6@cerstor.cz> +X-Sender: demo@cerstor.cz +User-Agent: Roundcube Webmail/1.0.0 + +--=_5d4b2de51e6aad0435b4bfbd7a23594a +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=US-ASCII; + format=flowed + +email that contains embedded message +--=_5d4b2de51e6aad0435b4bfbd7a23594a +Content-Transfer-Encoding: 8bit +Content-Type: message/rfc822; + name=demo.eml +Content-Disposition: attachment; + filename=demo.eml; + size=889 + +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:22:13 +0100 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_995890bdbf8bd158f2cbae0e8d966000" +Date: Fri, 29 Jan 2016 14:22:13 +0100 +From: demo-from@cerstor.cz +To: demo-to@cerstor.cz +Subject: demo +Message-ID: <4cbaf57cb00891c53b32e1d63367740c@cerstor.cz> +X-Sender: demo@cerstor.cz +User-Agent: Roundcube Webmail/1.0.0 + +--=_995890bdbf8bd158f2cbae0e8d966000 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=US-ASCII; + format=flowed + +demo text +--=_995890bdbf8bd158f2cbae0e8d966000 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; + name=testfile.txt +Content-Disposition: attachment; + filename=testfile.txt; + size=29 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= +--=_995890bdbf8bd158f2cbae0e8d966000-- + + +--=_5d4b2de51e6aad0435b4bfbd7a23594a-- diff --git a/tests/messages/embedded_email_without_content_disposition-embedded.eml b/tests/messages/embedded_email_without_content_disposition-embedded.eml new file mode 100644 index 00000000..df5fa40b --- /dev/null +++ b/tests/messages/embedded_email_without_content_disposition-embedded.eml @@ -0,0 +1,61 @@ +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- diff --git a/tests/messages/embedded_email_without_content_disposition.eml b/tests/messages/embedded_email_without_content_disposition.eml new file mode 100644 index 00000000..c5c8cffe --- /dev/null +++ b/tests/messages/embedded_email_without_content_disposition.eml @@ -0,0 +1,141 @@ +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 13:48:50 +0200 +Subject: Subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Type: multipart/mixed; + boundary="_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_" +MIME-Version: 1.0 + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: multipart/related; + boundary="_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_"; + type="multipart/alternative" + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +TexT + +[cid:file.jpg] + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +

TexT

= + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: image/jpeg; name= + "file.jpg" +Content-Description: file.jpg +Content-Disposition: inline; filename= + "file.jpg"; + size=54558; creation-date="Fri, 05 Apr 2019 11:48:58 GMT"; + modification-date="Fri, 05 Apr 2019 11:48:58 GMT" +Content-ID: +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg== + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: message/rfc822 + +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file3.xlsx" +Content-Description: file3.xlsx +Content-Disposition: attachment; filename="file3.xlsx"; + size=90672; creation-date="Fri, 05 Apr 2019 11:46:30 GMT"; + modification-date="Thu, 28 Mar 2019 08:07:58 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: application/x-zip-compressed; name="file4.zip" +Content-Description: file4.zip +Content-Disposition: attachment; filename="file4.zip"; size=29; + creation-date="Fri, 05 Apr 2019 11:48:45 GMT"; + modification-date="Fri, 05 Apr 2019 11:35:24 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- diff --git a/tests/messages/example_bounce.eml b/tests/messages/example_bounce.eml new file mode 100644 index 00000000..a1f5683f --- /dev/null +++ b/tests/messages/example_bounce.eml @@ -0,0 +1,96 @@ +Return-Path: <> +Received: from somewhere.your-server.de + by somewhere.your-server.de with LMTP + id 3TP8LrElAGSOaAAAmBr1xw + (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100 +Envelope-to: demo@foo.de +Delivery-date: Thu, 02 Mar 2023 05:27:29 +0100 +Authentication-Results: somewhere.your-server.de; + iprev=pass (somewhere06.your-server.de) smtp.remote-ip=1b21:2f8:e0a:50e4::2; + spf=none smtp.mailfrom=<>; + dmarc=skipped +Received: from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) + by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 + (Exim 4.94.2) + id 1pXaXR-0006xQ-BN + for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100 +Received: from [192.168.0.10] (helo=sslproxy01.your-server.de) + by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + id 1pXaXO-000LYP-9R + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +Received: from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) + by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + id 1pXaXO-0008gy-7x + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +Received: from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) + id 1pXaXO-0008gb-6g + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +X-Failed-Recipients: ding@ding.de +Auto-Submitted: auto-replied +From: Mail Delivery System +To: demo@foo.de +Content-Type: multipart/report; report-type=delivery-status; boundary=1677731246-eximdsn-678287796 +MIME-Version: 1.0 +Subject: Mail delivery failed +Message-Id: +Date: Thu, 02 Mar 2023 05:27:26 +0100 +X-Virus-Scanned: Clear (ClamAV 0.103.8/26827/Wed Mar 1 09:28:49 2023) +X-Spam-Score: 0.0 (/) +Delivered-To: bar-demo@foo.de + +--1677731246-eximdsn-678287796 +Content-type: text/plain; charset=us-ascii + +This message was created automatically by mail delivery software. + +A message sent by + + + +could not be delivered to one or more of its recipients. The following +address(es) failed: + + ding@ding.de + host 36.143.65.153 [36.143.65.153] + SMTP error from remote mail server after pipelined end of data: + 550-Verification failed for + 550-Unrouteable address + 550 Sender verify failed + +--1677731246-eximdsn-678287796 +Content-type: message/delivery-status + +Reporting-MTA: dns; sslproxy01.your-server.de + +Action: failed +Final-Recipient: rfc822;ding@ding.de +Status: 5.0.0 +Remote-MTA: dns; 36.143.65.153 +Diagnostic-Code: smtp; 550-Verification failed for + 550-Unrouteable address + 550 Sender verify failed + +--1677731246-eximdsn-678287796 +Content-type: message/rfc822 + +Return-path: +Received: from [31.18.107.47] (helo=127.0.0.1) + by sslproxy01.your-server.de with esmtpsa (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + (envelope-from ) + id 1pXaXO-0008eK-11 + for ding@ding.de; Thu, 02 Mar 2023 05:27:26 +0100 +Date: Thu, 2 Mar 2023 05:27:25 +0100 +To: ding +From: =?iso-8859-1?Q?S=C3=BCderbar_Foo_=28SI=29_GmbH?= +Subject: Test +Message-ID: +X-Mailer: PHPMailer 6.7.1 (https://github.com/PHPMailer/PHPMailer) +Return-Path: bounce@foo.de +MIME-Version: 1.0 +Content-Type: text/html; charset=iso-8859-1 +X-Authenticated-Sender: demo@foo.de + +
Hallo, dies ist ein Beispiel-Text.
\ No newline at end of file diff --git a/tests/messages/four_nested_emails.eml b/tests/messages/four_nested_emails.eml new file mode 100644 index 00000000..0b34e986 --- /dev/null +++ b/tests/messages/four_nested_emails.eml @@ -0,0 +1,69 @@ +To: test@example.com +From: test@example.com +Subject: 3-third-subject +Content-Type: multipart/mixed; + boundary="------------2E5D78A17C812FEFF825F7D5" + +This is a multi-part message in MIME format. +--------------2E5D78A17C812FEFF825F7D5 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +3-third-content +--------------2E5D78A17C812FEFF825F7D5 +Content-Type: message/rfc822; + name="2-second-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="2-second-email.eml" + +To: test@example.com +From: test@example.com +Subject: 2-second-subject +Content-Type: multipart/mixed; + boundary="------------9919377E37A03209B057D47F" + +This is a multi-part message in MIME format. +--------------9919377E37A03209B057D47F +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +2-second-content +--------------9919377E37A03209B057D47F +Content-Type: message/rfc822; + name="1-first-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="1-first-email.eml" + +To: test@example.com +From: test@example.com +Subject: 1-first-subject +Content-Type: multipart/mixed; + boundary="------------0919377E37A03209B057D47A" + +This is a multi-part message in MIME format. +--------------0919377E37A03209B057D47A +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +1-first-content +--------------0919377E37A03209B057D47A +Content-Type: message/rfc822; + name="0-zero-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="0-zero-email.eml" + +To: test@example.com +From: test@example.com +Subject: 0-zero-subject +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +0-zero-content +--------------0919377E37A03209B057D47A-- + +--------------9919377E37A03209B057D47F-- + +--------------2E5D78A17C812FEFF825F7D5-- diff --git a/tests/messages/gbk_charset.eml b/tests/messages/gbk_charset.eml new file mode 100644 index 00000000..b40936bd --- /dev/null +++ b/tests/messages/gbk_charset.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="X-GBK" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/html_only.eml b/tests/messages/html_only.eml new file mode 100644 index 00000000..ce3ee17c --- /dev/null +++ b/tests/messages/html_only.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/tests/messages/imap_mime_header_decode_returns_false.eml b/tests/messages/imap_mime_header_decode_returns_false.eml new file mode 100644 index 00000000..af3a7f36 --- /dev/null +++ b/tests/messages/imap_mime_header_decode_returns_false.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: =?UTF-8?B?nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/inline_attachment.eml b/tests/messages/inline_attachment.eml new file mode 100644 index 00000000..be60f598 --- /dev/null +++ b/tests/messages/inline_attachment.eml @@ -0,0 +1,41 @@ +Content-Type: multipart/mixed; + boundary="----=_Part_1114403_1160068121.1505882828080" + +------=_Part_1114403_1160068121.1505882828080 +Content-Type: multipart/related; + boundary="----=_Part_1114404_576719783.1505882828080" + +------=_Part_1114404_576719783.1505882828080 +Content-Type: text/html;charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + + +------=_Part_1114404_576719783.1505882828080 +Content-Type: image/png +Content-Disposition: inline +Content-Transfer-Encoding: base64 +Content-ID: + +iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk +NTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8 +kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju +ZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH +pEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF +o0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv +vhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd +yKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf +wtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u +Si6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm +5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA +LabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh +eD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T +5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7 +s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3 +a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU +9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH +iTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN +J3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu +QmCC +------=_Part_1114404_576719783.1505882828080-- + +------=_Part_1114403_1160068121.1505882828080-- diff --git a/tests/messages/ks_c_5601-1987_headers.eml b/tests/messages/ks_c_5601-1987_headers.eml new file mode 100644 index 00000000..a8de3d08 --- /dev/null +++ b/tests/messages/ks_c_5601-1987_headers.eml @@ -0,0 +1,10 @@ +Subject: =?ks_c_5601-1987?B?UkU6IMi4v/i01LKyIEVyc2m01MDMILjevcPB9rimILq4s8K9wLTP?= + =?ks_c_5601-1987?B?tNku?= +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Thread-Topic: =?ks_c_5601-1987?B?yLi/+LTUsrIgRXJzabTUwMwguN69w8H2uKYgurizwr3AtM+02S4=?= +From: =?ks_c_5601-1987?B?seggx/bB+A==?= +To: to@here.com + +Content diff --git a/tests/messages/mail_that_is_attachment.eml b/tests/messages/mail_that_is_attachment.eml new file mode 100644 index 00000000..89215cf8 --- /dev/null +++ b/tests/messages/mail_that_is_attachment.eml @@ -0,0 +1,28 @@ +Sender: xxx@yyy.cz +MIME-Version: 1.0 +Message-ID: <2244696771454641389@google.com> +Date: Sun, 15 Feb 2015 10:21:51 +0000 +Subject: Report domain: yyy.cz Submitter: google.com Report-ID: 2244696771454641389 +From: noreply-dmarc-support via xxx +To: xxx@yyy.cz +Content-Type: application/zip; + name="google.com!yyy.cz!1423872000!1423958399.zip" +Content-Disposition: attachment; + filename="google.com!yyy.cz!1423872000!1423958399.zip" +Content-Transfer-Encoding: base64 +Reply-To: noreply-dmarc-support@google.com + +UEsDBAoAAAAIABRPT0bdJB+DSwIAALgKAAAuAAAAZ29vZ2xlLmNvbSFzdW5mb3guY3ohMTQyMzg3 +MjAwMCExNDIzOTU4Mzk5LnhtbO1WwY6bMBC971dEuQcDIQSQQ3rqF7RnZIwh7oJt2WY32a+viQ2h +u9moqnarqOop8Gbmjd/4OQbuj127eCJSUc52y8DzlwvCMK8oa3bL79++rpLlYp8/wJqQqkT4MX9Y +LKAkgktddESjCmk0YAblsikY6kjecN60xMO8g2ACbQ7pEG1zxg1De1pVHZJ4pXox0H2Zl9k8V3PU +EhWYM42wLiireX7QWmQAuErvUgkQKCkDiKlnIj1x2tunXRjF8SbxDfFbMtvFaaJVHoZRFKfxdhtE +myiOgnWSQnAJ23SjmxQSscYpM1BJGsryIArXyTb0fdPMImOcsOocTTfJOjWUw7slA7+yTd3mA4aC +txSfCtGXLVUHMi2Em1GxXPVGytHDL4bMIjaMqkfa5RIC++BAJeozNvxaSOSS/CBYQyAcoi6QGjGB +dR4MyoaH80qvrcrMEnM5LlDy52kEivcSk4KKPIz9bVYnpZ9Fvr/OsB9kWbgOTa8pZSzCvGemLQT2 +YYRdZ/KE2t6MrxoDw0yoElxRbTxtvMaImckMmeUNIxFIKZMwTceJr11gGtFM7aueZr9GjZBWhGla +U3OiprIDQRWRRS15N9+nOex43lRD1OtDIYnqW30hfLXY2xZw7h4YnCT3Mqma08GZ3g+gvhgMvFYy +JI82+R3HpL4XbDdesIm84SB/tE9Gr99wSm3+k646xQbu0Sl/uptW0Sfu5tXzH6b3dP7vd1f/+vl/ +KU83eRnpzbX6uY5JzMeJZ25PLwji920S/r8m/tVrAoLLR+hPUEsBAgoACgAAAAgAFE9PRt0kH4NL +AgAAuAoAAC4AAAAAAAAAAAAAAAAAAAAAAGdvb2dsZS5jb20hc3VuZm94LmN6ITE0MjM4NzIwMDAh +MTQyMzk1ODM5OS54bWxQSwUGAAAAAAEAAQBcAAAAlwIAAAAA \ No newline at end of file diff --git a/tests/messages/missing_date.eml b/tests/messages/missing_date.eml new file mode 100644 index 00000000..5f8a6032 --- /dev/null +++ b/tests/messages/missing_date.eml @@ -0,0 +1,7 @@ +From: from@here.com +To: to@here.com +Subject: Nuu +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/missing_from.eml b/tests/messages/missing_from.eml new file mode 100644 index 00000000..e09dd9d9 --- /dev/null +++ b/tests/messages/missing_from.eml @@ -0,0 +1,7 @@ +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/mixed_filename.eml b/tests/messages/mixed_filename.eml new file mode 100644 index 00000000..4a353da3 --- /dev/null +++ b/tests/messages/mixed_filename.eml @@ -0,0 +1,25 @@ +Date: Fri, 02 Feb 2018 22:23:06 +0300 +To: foo@bar.com +Subject: =?windows-1251?B?0eLl5ujpIO/w4OnxLevo8fI=?= +From: =?windows-1251?B?z/Dg6fH7IHx8IM/g8PLK7uw=?= +Content-Transfer-Encoding: quoted-printable +Content-Type: multipart/mixed; + boundary="=_743251f7a933f6b30c004fcb14eabb57" +Content-Disposition: inline + +This is a message in Mime Format. If you see this, your mail reader does not support this format. + +--=_743251f7a933f6b30c004fcb14eabb57 +Content-Type: text/html; charset=windows-1251 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +--=_743251f7a933f6b30c004fcb14eabb57 +Content-Type: application/octet-stream +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="Price4VladDaKar.xlsx" + +UEsDBBQAAgAIAHOyQkwNlxQhWwEAAAYFAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbK2UTU7D +CwALANECAAAT1E4AAAA= +--=_743251f7a933f6b30c004fcb14eabb57-- diff --git a/tests/messages/multiple_html_parts_and_attachments.eml b/tests/messages/multiple_html_parts_and_attachments.eml new file mode 100644 index 00000000..5ccbd2c3 --- /dev/null +++ b/tests/messages/multiple_html_parts_and_attachments.eml @@ -0,0 +1,203 @@ +Return-Path: +Delivered-To: to@there.com +Return-path: +Envelope-to: to@there.com +Delivery-date: Thu, 16 Feb 2023 09:19:23 +0000 +From: FromName +Content-Type: multipart/alternative; + boundary="Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43" +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.400.51.1.1\)) +Subject: multiple_html_parts_and_attachments +Date: Thu, 16 Feb 2023 10:19:02 +0100 +To: to@there.com + + + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + +This is the first html part + +=EF=BF=BC + +This is the second html part + +=EF=BF=BC + +This is the last html part +https://www.there.com + + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43 +Content-Type: multipart/mixed; + boundary="Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE" + + +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +This is the first html part

+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Disposition: inline; + filename=attachment1.pdf +Content-Type: application/pdf; + x-unix-mode=0666; + name="attachment1.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoKPDwgL0ZpbHRlciAvRmxhdGVEZWNvZGUgL0xl +bmd0aCA5MyA+PgpzdHJlYW0KeAEdjDsOgCAQBXtP8U7AzwWW3sZOKmtDKExEhej9JWbamamIqFAd +G6ww3jowacEcGC1jxQm55Jby/bzbgbZ3W2v6CzPCkBJu9AxSQRBRGFKBnIvGdPVz/ABGZhatCmVu +ZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9SZXNv +dXJjZXMgNCAwIFIgL0NvbnRlbnRzIDMgMCBSIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44 +ODk4XQo+PgplbmRvYmoKNCAwIG9iago8PCAvUHJvY1NldCBbIC9QREYgL0ltYWdlQiAvSW1hZ2VD +IC9JbWFnZUkgXSAvWE9iamVjdCA8PCAvSW0xIDUgMCBSID4+ID4+CmVuZG9iago1IDAgb2JqCjw8 +IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMTE0IC9IZWlnaHQgMjMgL0lu +dGVycG9sYXRlIHRydWUKL0NvbG9yU3BhY2UgNiAwIFIgL0ludGVudCAvUGVyY2VwdHVhbCAvQml0 +c1BlckNvbXBvbmVudCA4IC9MZW5ndGggMTUzNCAvRmlsdGVyCi9GbGF0ZURlY29kZSA+PgpzdHJl +YW0KeAHtmAlTllUUxz9CZTVN+75rmkJmZqMpVgoCoogikArhgmyhKYoKCqK4gQQoCgIBKiiEGCIg +u4CoKU1Mi/uu7fUB7CdnunPnPmLvy4Bj08s888w592z3/J9zz7kvN286/voWgb9u9Hc8PUagu2/T +Y4cOQxBwoNoXZXBvonrj0uCfLg3ui3zvjk9bUD16/MP7xh3j2Vo0z9jVhTNvd3476rerA2XdYA1l +29nHvZoGzyq3Xb+PNG1M58p556CETV9W+6pt2ILqgpR4QXVsRL4yFCIwIRnRkWMf3ZY1lG1n7xFU +jey6239i9iJASMharBT+FVXq8Hmf2icmNT47pe7+8Uc7O0cqWwgjrsHqmnbR/wlUvz7psmJLzISF +O4DFXlSLK/0xmZe0Tip25bYlCp+AuNQXptUgHRW6i3WDFbWiioDgxA39Aw4MDS6dv27tnko/ZQ6x +r2bqB5H5T01u4MNNWrJN1bygWn7IZ+qyDEKMmFesV8KUmK3sp6Zpol9s2qt+B31XpFc1Tjrz4/CZ +8SkSKDot9o/rA1QgehSiIbP20VWCEze2HR2nRLgir6Y2N++lma9Mrxoxtzguc8mfXba3TUcZQmSV +zAZPgdReVAmHSd1hD0JDDJq5X3n2XJz1tHc9i06B+xanxhksahm7Q5DyAPvI+bsfnNAGXbh/pnjI +3DOXLfVzO/J+6K7nfGoR8T79w7tIQfUB13b0H3ZvHfBxhThZnxclhkgf6hI96nlYNsDK6/6VqKGM +QwjAEeWGFvcnJzewwtcZPmdvP9f2xyY2872UK2wf8WhByg4Jh+aa7M+QWtMRE+t7c2EYVvp3v3MH +OHPqHTapkITAvL7FXXk2jrzBukQUAE7rP7UBjJhTNphfPe9MS8E5JQdLn6HhI92Q9yksmYrmxbND +qZyc0k9gOWsSV6ToXz731vWLQ1hHyhdpaR+PQmX9ZFiOADS2lB9sckGEjNSSqukA6xRUprui2q9d +cGKFs4Oy28IckRrpyKL1bS+q63KjiLJqW7S4Wr5lGSwHWXk24hpsfvksVZmYkBHmHDpoRNAccOWq +45vRiJLzI1gBN8oY0ET6+7U3qKKXfKuFFSmTV9jVWbeGBadeWJBEgZYC29g6AdHAGV+JSN5jwgpZ +PH5iLCyanAi+nYjoG9StCmSkozvRaXtRdQ4qYwPTlmdEJcfzSDfg0P16ZZC4NeIaLDrNR1zpgaPD +drJVaUGCKucFz3qX1vdJshxJfQVzJqasIH1Rk6YUhOMqbVeo0n/Gu45Nwu4oDUYEwtSzevDMYkWd +Nwq4op0qQ4jX/A6qQNZ0dE1F24WqNFI2YH2YQeLTiKuz1AwAYksTYNzMWLU5NnMprKAauWk1tPUC +LG5J1rivGqjqUkFVdwWqTEBc0U+IAoz0IuM51OyJgjUQqNKHb5udLFrfdqEakpTElpJyFjBe1SM9 +QZDBvw6jwdIwMWeHp7oGEFKGLytiu/GLSOg5a9erTX7/3XvhGxNpoaxYk+0ZqsUHbl1gmDsqCgSB +aPW/dB03a6A+rdWfL78JIPQcfl/oWzrL/HJtZwTLuqCq5pHO5pYFkZHeORelrlSoMvKgGdnqd1n4 +hjWsgHYvosrtmt3SKhlqkgW/hflAdGlpy7agqrLTcdBp22s1ryyQHLlD6uZCj4/KRZS6Mwx2afoK +aK/o7Uwig2Uc0EjJiIvK9r2zgVf6KvO39rAHyh6LsrFl4G4pCvGP/Zz7AMdWCtuabM9qlSjysbg1 +cQqyS4K5XBGUGYGIxxpIr1UjOzGxvm1HVVJmUludpO+ez8ZcwgsR8R2ZsLByrg2WwmOsIOWhuZVW ++TL4oOXqQrXI+BMFrm1c5iUc84JLux76Zd9qaZUsGlJJKrN4rtLnlkVrFZb/0oSuXysheHPFjUlf +rqat4QoTfkeoQEY6yr9BUGB45nerWr/zfVWp3ZngoKkjhqbOcikCqxMdLsoDNNdgxZ4/Pay60etk +xxg01WKvE0TkLrq/dsq508Psda6nY6Ntr6BqY6z/j5oD1b741g5U7yaq3aHtWLcXgb8Bi7W2lwpl +bmRzdHJlYW0KZW5kb2JqCjcgMCBvYmoKPDwgL04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0xl +bmd0aCAzNDMgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngBdZA7SwNBFIXPxsiCpkgh +Vim2sRBWiZsgdpKoiJBiiYqPbrKJiZDHsFkRG7HzD4iVYCdprWKhIP4CQVCxshC1FwI+wnomUTaF +3uHOfHPm3N3LBUIRIWU5DKBS9dzsfNpYXVs39BeEEIWOJEzh1GXKtjO04PdUHETrFpq63Yypb8VO +P7Kv+eP9+7Q5rYvGYuD7kwbyhbrDly+m5UjXA7Q42d72pOI98pDLpsgHiotdbijOdfm841nKztBz +TY46JZEnP5LNXI9e7OFKeUv9V4XqPlKoLqteh5kxzGIOGS4DNixOYQJT1PBPTbJTM4MaJHbgYhNF +lOCxOkVFoowCeQFVOBiHSbYQZybUrFnLCGYYaLUIMPnMx6tAE0/A2e7PSAxVCYzQEz0CLt6lcEVX +4661wvWNhNW5a4NNoP/Q999WAH0UaN/5/mfT99snQN8DcNn6BsE0ZMMKZW5kc3RyZWFtCmVuZG9i +ago2IDAgb2JqClsgL0lDQ0Jhc2VkIDcgMCBSIF0KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1Bh +Z2VzIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44ODk4XSAvQ291bnQgMSAvS2lkcyBbIDEg +MCBSIF0KPj4KZW5kb2JqCjggMCBvYmoKPDwgL1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+ +CmVuZG9iago5IDAgb2JqCjw8IC9UaXRsZSAo/v9cMDAwU1wwMDBjXDAwMGhcMDAwZVwwMDByXDAw +MG1cMDAwrVwwMDBhXDAwMGZcMDAwYlwwMDBlXDAwMGVcMDAwbFwwMDBkXDAwMGlcMDAwblwwMDBn +XDAwMCBcMDAwMlwwMDAwXDAwMDJcMDAwM1wwMDAtXDAwMDBcMDAwMlwwMDAtXDAwMDFcMDAwNlww +MDAgXDAwMG9cMDAwbVwwMDAgXDAwMDFcMDAwMFwwMDAuXDAwMDFcMDAwMVwwMDAuXDAwMDBcMDAw +M1wwMDAuXDAwMHBcMDAwblwwMDBnKQovUHJvZHVjZXIgKG1hY09TIFZlcnNpZSAxMy4yIFwoYnVp +bGQgMjJENDlcKSBRdWFydHogUERGQ29udGV4dCkgL0NyZWF0b3IgKFZvb3J2ZXJ0b25pbmcpCi9D +cmVhdGlvbkRhdGUgKEQ6MjAyMzAyMTYwOTExNDdaMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDIzMDIx +NjA5MTE0N1owMCcwMCcpCj4+CmVuZG9iagp4cmVmCjAgMTAKMDAwMDAwMDAwMCA2NTUzNSBmIAow +MDAwMDAwMTg2IDAwMDAwIG4gCjAwMDAwMDI2MDIgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBu +IAowMDAwMDAwMzAwIDAwMDAwIG4gCjAwMDAwMDAzODkgMDAwMDAgbiAKMDAwMDAwMjU2NyAwMDAw +MCBuIAowMDAwMDAyMTI1IDAwMDAwIG4gCjAwMDAwMDI2OTUgMDAwMDAgbiAKMDAwMDAwMjc0NCAw +MDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDEwIC9Sb290IDggMCBSIC9JbmZvIDkgMCBSIC9JRCBb +IDxlNzk0OWE2YzQ2MWM1MDYxNTk0ODU5YjkxZjczYzAzMz4KPGU3OTQ5YTZjNDYxYzUwNjE1OTQ4 +NTliOTFmNzNjMDMzPiBdID4+CnN0YXJ0eHJlZgozMTYxCiUlRU9GCg== +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +

This is the second html part

+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Disposition: inline; + filename=attachment2.pdf +Content-Type: application/pdf; + x-unix-mode=0666; + name="attachment2.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoKPDwgL0ZpbHRlciAvRmxhdGVEZWNvZGUgL0xl +bmd0aCA5MyA+PgpzdHJlYW0KeAEdjDsOgCAQBXtP8U4ALB9Zehs7qawNoTARFaL3l5hpZ6YiokJ1 +XHBCezeCLQnmwGgZK07IJbeU7+fdDrS926TpL4yCNl6Q8QyrnAjWhiEVyLkQpquf4wdGBBarCmVu +ZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9SZXNv +dXJjZXMgNCAwIFIgL0NvbnRlbnRzIDMgMCBSIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44 +ODk4XQo+PgplbmRvYmoKNCAwIG9iago8PCAvUHJvY1NldCBbIC9QREYgL0ltYWdlQiAvSW1hZ2VD +IC9JbWFnZUkgXSAvWE9iamVjdCA8PCAvSW0xIDUgMCBSID4+ID4+CmVuZG9iago1IDAgb2JqCjw8 +IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMTIxIC9IZWlnaHQgMzAgL0lu +dGVycG9sYXRlIHRydWUKL0NvbG9yU3BhY2UgNiAwIFIgL0ludGVudCAvUGVyY2VwdHVhbCAvQml0 +c1BlckNvbXBvbmVudCA4IC9MZW5ndGggMTczNSAvRmlsdGVyCi9GbGF0ZURlY29kZSA+PgpzdHJl +YW0KeAHtl4lT1kUYx/+Eymq677s0TTHzajSPPBFE8UA0T1RuxRMVUfEAEVECFQVREQ9UxiNFVFAU +FJAxGrDM+9bu+gPsI8+0s+27v3qFmnLm987OO8+9u9999nn2d++e+3MRcBFwEXARcBFwEXBE4Le7 +Td3RYAQcYbUpGjyL6wgCNkQdZS5ijUHAEVabojETNd737vWW311v2fg4/1UEG6KOMn2RVdU9Hul1 +mrFme6guh7568aO62s4/3WoucoM1jL1nnws43nL0Xu/t/yXLv93O+XMd0rZEjluUMjRu1ayMuZXV +PWQljrDaFPrip6xIEKi7R+fqcugxC1NRVZzuKXKDNYy9Z/8nUP/1dsjAt4IOsf0n+51kQDzue2rd +zvFs04aoo0zBQsa+Nrj4+QGlrwwqebR3VV1dJ6WCMBZjsLrlA9EPBdQ9J28C3qUbYoDo1lWflXmR +sE/7l106384RVptCIZNfOJwIoUlLJbfnr41VqhHz0l4fehht54ityA1WzLbvHxGyeFnTEQfahBSE +L03cURis3CH2HB7y6aTcFwce4zQHxK5Vt0Og3ntk8JA5q5iiY2j+wqwZynHQ7DWs5/Dx/sHx6e8E +Hwyam1FUOuDit+1HJayQiWamx/9yp5myp8ShajV6D0UpZHHKqapeSkUo9nX8VN/AWZlvDyvqODF/ +Xmbsr/W+1u0oR+AFVWIqCYTP2N2gsfNgsA1RR5mKwBpwLynzYz0QLUbtUyr/GVkvBR5F6DNmz4y0 +eQaL2aptYWgZnEWn8G3cL+i8faMkQuaOiVyTJn0rPonY+urgYlT8XzjXAS1QP9anEnsuZrPP9kuQ +5I0x4oj2iXrVM/5lsgAk7w0vxAxjAkKAmBgfK+/3wsBjSDiy9hN2NulT+Wz/ExyiCoXvU37laFmh +1IEl2dPQem5HXOS/trYzCRCbPlcJOSCQZyLyxxFWm0IiXDzfjpUreCEIdbS8n4pvVAyD7Ra9GcRO +/pFFYIs7CYb7rSutqUgEJzlhSZKxC5ejXbZxMizbF8trl9qwhZyCcbC+U9fLvKLF/sblD+9ca4Uc +LcdUXtkbg8KjA2G5LND4kqiwqZujmQLJrqJhoE366aG4F7ev+iABJYz7Ts0RrbEdETr9b9w9Bl/O +kWZqQ9RRJgEpRLgvWDtT2LjVc2CpA2o6YzEGm7t3tMphXNgm7txZaFTQ1AcVquarLqhSc6ORACYJ +D5Ki/fn2++Tbm0GHhBXtzSuthV2UNZ1QFA1hgRcDKhJs6UlfVM1HfiEq+e8amYew+kx3WCy5Oxyo +qCg7ZLiayNiOHkSneZdShVgwg3RC5QirTSGhWtcXH14yMakJDCkm3Nkfb7YQA2MxBovNiYo+1NUu +kVtYPythjwI1tRdar/wSUP5BgButS3CnNSvtG5p2xeYoQqVvjVD2LweWsEjY9QUhqICdzFeDyAj3 +lwRiwESUaOUI8W7wQTWR53Z0S6FpRlK7eI2QSyK0Ieoow0WKM6vyHMSXmMZidJbsAlV8qSGUtZEL +VsZnzoIVqCctXwTt+VCXsCBgvKsNqHWtQK2HAmpaLaEoR8wCtpQyYxw54Y+B50RATW237k6E6p+K +NCExmfjcOIo2jxClcoTVpsArLCmJOEk5U2juakhJEbiw0bE1WIow7iybd76sgdaPRHxTNk2CZqmi +4v+brz+OSllMWYb2RKBhUOcfuP98osGpWWQi2scP9RfTcyLvs3rayvkEbzt+l9QifQoboo6y7298 +AErUMYq8HoRHI22FB4DIBWrV+HR2w+6xrESvxtPT7q9NoKa3QvNgkG7FFFHLliDhCKA9EWgY1HwF +sFrKL91TdsEnP6HIQyn1nhN5Qq12p+PASRGEJ9DZs3/60BAbR1htCumnvHX1+EL3jtkAJnyQwvI1 +Ch0wc52UKZ3lrCnObJO3E99QYC61mu5fXOaHr9/0bHxp96u3hw2P/5zXCLderoAnAg2DmlnkBHnI +cV+yd4Xw3mNS+o7sxXMiHWp9O2Kv/uXO0hHYhTHoxTZEHWWCA+8EFVwRGdvCWW23qDwknDj9HVZy +1WBJUfoXWgYFs6AoiA4LLa8p8kr6rBjwkuRLRGahMRlfBzQdKb8YGFr5TMvMv9/6ZfDwo1wLzfMg +IjlRpuCfPJydEafauhEKFz6C1ETGdlR8iMScqSqmQTT4E0aP70RzT9UNxUZneacB4JmabsoXmue6 +Yq9caHuoNODLmq5YKuE/TjAjb+Z9xYMuX2j7oMH17Xjp65jBNoWXMV0zKwI2RB1l1giu0EsEHGG1 +KbyM6ZpZEbAh6spcBFwEXARcBB4aBH4HWDJl2AplbmRzdHJlYW0KZW5kb2JqCjcgMCBvYmoKPDwg +L04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0xlbmd0aCAzNDMgL0ZpbHRlciAvRmxhdGVEZWNv +ZGUgPj4Kc3RyZWFtCngBdZA7SwNBFIXPxsiCpkghVim2sRBWiZsgdpKoiJBiiYqPbrKJiZDHsFkR +G7HzD4iVYCdprWKhIP4CQVCxshC1FwI+wnomUTaF3uHOfHPm3N3LBUIRIWU5DKBS9dzsfNpYXVs3 +9BeEEIWOJEzh1GXKtjO04PdUHETrFpq63Yypb8VOP7Kv+eP9+7Q5rYvGYuD7kwbyhbrDly+m5UjX +A7Q42d72pOI98pDLpsgHiotdbijOdfm841nKztBzTY46JZEnP5LNXI9e7OFKeUv9V4XqPlKoLqte +h5kxzGIOGS4DNixOYQJT1PBPTbJTM4MaJHbgYhNFlOCxOkVFoowCeQFVOBiHSbYQZybUrFnLCGYY +aLUIMPnMx6tAE0/A2e7PSAxVCYzQEz0CLt6lcEVX4661wvWNhNW5a4NNoP/Q999WAH0UaN/5/mfT +99snQN8DcNn6BsE0ZMMKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqClsgL0lDQ0Jhc2VkIDcgMCBS +IF0KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2 +IDg0MS44ODk4XSAvQ291bnQgMSAvS2lkcyBbIDEgMCBSIF0KPj4KZW5kb2JqCjggMCBvYmoKPDwg +L1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+CmVuZG9iago5IDAgb2JqCjw8IC9UaXRsZSAo +/v9cMDAwU1wwMDBjXDAwMGhcMDAwZVwwMDByXDAwMG1cMDAwrVwwMDBhXDAwMGZcMDAwYlwwMDBl +XDAwMGVcMDAwbFwwMDBkXDAwMGlcMDAwblwwMDBnXDAwMCBcMDAwMlwwMDAwXDAwMDJcMDAwM1ww +MDAtXDAwMDBcMDAwMlwwMDAtXDAwMDFcMDAwNlwwMDAgXDAwMG9cMDAwbVwwMDAgXDAwMDFcMDAw +MFwwMDAuXDAwMDFcMDAwMVwwMDAuXDAwMDFcMDAwMFwwMDAuXDAwMHBcMDAwblwwMDBnKQovUHJv +ZHVjZXIgKG1hY09TIFZlcnNpZSAxMy4yIFwoYnVpbGQgMjJENDlcKSBRdWFydHogUERGQ29udGV4 +dCkgL0NyZWF0b3IgKFZvb3J2ZXJ0b25pbmcpCi9DcmVhdGlvbkRhdGUgKEQ6MjAyMzAyMTYwOTEx +MzhaMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDIzMDIxNjA5MTEzOFowMCcwMCcpCj4+CmVuZG9iagp4 +cmVmCjAgMTAKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMTg2IDAwMDAwIG4gCjAwMDAwMDI4 +MDMgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBuIAowMDAwMDAwMzAwIDAwMDAwIG4gCjAwMDAw +MDAzODkgMDAwMDAgbiAKMDAwMDAwMjc2OCAwMDAwMCBuIAowMDAwMDAyMzI2IDAwMDAwIG4gCjAw +MDAwMDI4OTYgMDAwMDAgbiAKMDAwMDAwMjk0NSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDEw +IC9Sb290IDggMCBSIC9JbmZvIDkgMCBSIC9JRCBbIDxjOWI4YzBlZGQ5Mjk1Y2U3ZmQ2YzFkOWJj +ZTJhZTRiOD4KPGM5YjhjMGVkZDkyOTVjZTdmZDZjMWQ5YmNlMmFlNGI4PiBdID4+CnN0YXJ0eHJl +ZgozMzYyCiUlRU9GCg== +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +

This is the last html part
https://www.there.com



+
+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE-- + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43-- diff --git a/tests/messages/multiple_nested_attachments.eml b/tests/messages/multiple_nested_attachments.eml new file mode 100644 index 00000000..ad48d47b --- /dev/null +++ b/tests/messages/multiple_nested_attachments.eml @@ -0,0 +1,84 @@ +Date: Mon, 15 Jan 2018 10:54:09 +0100 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 + Thunderbird/52.5.0 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------85793CE1578A77318D5A90A6" +Content-Language: en-US + +This is a multi-part message in MIME format. +--------------85793CE1578A77318D5A90A6 +Content-Type: multipart/alternative; + boundary="------------32D598A7FC3F8A8E228998B0" + + +--------------32D598A7FC3F8A8E228998B0 +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + + +------------------------------------------------------------------------ + + + + +--------------32D598A7FC3F8A8E228998B0 +Content-Type: multipart/related; + boundary="------------261DC39B47CFCDB73BCE3C18" + + +--------------261DC39B47CFCDB73BCE3C18 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 8bit + + + + + + + +


+

+
+ + + Â +
+ + + + + + + +

+

+
+
+ + + +--------------261DC39B47CFCDB73BCE3C18 +Content-Type: image/png; + name="mleokdgdlgkkecep.png" +Content-Transfer-Encoding: base64 +Content-ID: +Content-Disposition: inline; + filename="mleokdgdlgkkecep.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAACklE +QVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg== +--------------261DC39B47CFCDB73BCE3C18-- + +--------------32D598A7FC3F8A8E228998B0-- + +--------------85793CE1578A77318D5A90A6 +Content-Type: image/png; + name="FF4D00-1.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="FF4D00-1.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAACklE +QVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg== +--------------85793CE1578A77318D5A90A6-- \ No newline at end of file diff --git a/tests/messages/nestes_embedded_with_attachment.eml b/tests/messages/nestes_embedded_with_attachment.eml new file mode 100644 index 00000000..44498103 --- /dev/null +++ b/tests/messages/nestes_embedded_with_attachment.eml @@ -0,0 +1,145 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_000_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_000_000 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_111_000" + + +------=_NextPart_000_111_000 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Dear Sarah + +------=_NextPart_000_111_000 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +
Dear Sarah,
+ + +------=_NextPart_000_111_000-- + +------=_NextPart_000_000_000 +Content-Type: message/rfc822; + name="first.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="first.eml" + +From: from@there.com +To: to@here.com +Subject: FIRST +Date: Sat, 28 Apr 2018 14:37:16 -0400 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_222_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_222_000 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_222_111" + + +------=_NextPart_000_222_111 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +Please respond directly to this email to update your RMA + + +2018-04-17T11:04:03-04:00 +------=_NextPart_000_222_111 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + +
Please respond directly to this = +email to=20 +update your RMA
+ +------=_NextPart_000_222_111-- + +------=_NextPart_000_222_000 +Content-Type: image/png; + name="chrome.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="chrome.png" + +iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk +NTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8 +kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju +ZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH +pEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF +o0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv +vhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd +yKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf +wtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u +Si6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm +5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA +LabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh +eD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T +5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7 +s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3 +a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU +9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH +iTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN +J3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu +QmCC + +------=_NextPart_000_222_000-- + +------=_NextPart_000_000_000 +Content-Type: message/rfc822; + name="second.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="second.eml" + +From: from@there.com +To: to@here.com +Subject: SECOND +Date: Sat, 28 Apr 2018 13:37:30 -0400 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_333_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_333_000 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +T whom it may concern: +------=_NextPart_000_333_000 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + +
T whom it may concern:
+ + +------=_NextPart_000_333_000-- + +------=_NextPart_000_000_000-- + diff --git a/tests/messages/null_content_charset.eml b/tests/messages/null_content_charset.eml new file mode 100644 index 00000000..0f19eb44 --- /dev/null +++ b/tests/messages/null_content_charset.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: to@here.com + +Hi! \ No newline at end of file diff --git a/tests/messages/pec.eml b/tests/messages/pec.eml new file mode 100644 index 00000000..15dfacca --- /dev/null +++ b/tests/messages/pec.eml @@ -0,0 +1,65 @@ +To: test@example.com +From: test@example.com +Subject: Certified +Date: Mon, 2 Oct 2017 12:13:43 +0200 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----258A05BDE519DE69AAE8D59024C32F5E" + +This is an S/MIME signed message + +------258A05BDE519DE69AAE8D59024C32F5E +Content-Type: multipart/mixed; boundary="----------=_1506939223-24530-42" +Content-Transfer-Encoding: binary + +------------=_1506939223-24530-42 +Content-Type: multipart/alternative; + boundary="----------=_1506939223-24530-43" +Content-Transfer-Encoding: binary + +------------=_1506939223-24530-43 +Content-Type: text/plain; charset="iso-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Signed + +------------=_1506939223-24530-43 +Content-Type: text/html; charset="iso-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Signed + +------------=_1506939223-24530-43-- + +------------=_1506939223-24530-42 +Content-Type: application/xml; name="data.xml" +Content-Disposition: inline; filename="data.xml" +Content-Transfer-Encoding: base64 + +PHhtbC8+ + +------------=_1506939223-24530-42 +Content-Type: message/rfc822; name="postacert.eml" +Content-Disposition: inline; filename="postacert.eml" +Content-Transfer-Encoding: 7bit + +To: test@example.com +From: test@example.com +Subject: test-subject +Date: Mon, 2 Oct 2017 12:13:50 +0200 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +test-content + +------------=_1506939223-24530-42-- + +------258A05BDE519DE69AAE8D59024C32F5E +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MQ== + +------258A05BDE519DE69AAE8D59024C32F5E-- + diff --git a/tests/messages/plain_only.eml b/tests/messages/plain_only.eml new file mode 100644 index 00000000..bbf9f3b3 --- /dev/null +++ b/tests/messages/plain_only.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/tests/messages/plain_text_attachment.eml b/tests/messages/plain_text_attachment.eml new file mode 100644 index 00000000..6ec6a6c5 --- /dev/null +++ b/tests/messages/plain_text_attachment.eml @@ -0,0 +1,21 @@ +To: to@here.com +From: from@there.com +Subject: Plain text attachment +Date: Tue, 21 Aug 2018 09:05:14 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +This is a multi-part message in MIME format. +--------------B832AF745285AEEC6D5AEE42 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +Test +--------------B832AF745285AEEC6D5AEE42 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="a.txt" + +SGkh +--------------B832AF745285AEEC6D5AEE42-- diff --git a/tests/messages/references.eml b/tests/messages/references.eml new file mode 100644 index 00000000..478383cf --- /dev/null +++ b/tests/messages/references.eml @@ -0,0 +1,11 @@ +Message-ID: <123@example.com> +From: no_host +Cc: "This one: is \"right\"" , No-address +In-Reply-To: +References: <231d9ac57aec7d8c1a0eacfeab8af6f3@example.com> <08F04024-A5B3-4FDE-BF2C-6710DE97D8D9@example.com> +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi +How are you? diff --git a/tests/messages/simple_multipart.eml b/tests/messages/simple_multipart.eml new file mode 100644 index 00000000..60a6521c --- /dev/null +++ b/tests/messages/simple_multipart.eml @@ -0,0 +1,23 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +MyPlain +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/tests/messages/structured_with_attachment.eml b/tests/messages/structured_with_attachment.eml new file mode 100644 index 00000000..f795ec3a --- /dev/null +++ b/tests/messages/structured_with_attachment.eml @@ -0,0 +1,24 @@ +From: from@there.com +To: to@here.com +Subject: Test +Date: Fri, 29 Sep 2017 10:55:23 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------5B1F217006A67C28E756A62E" + +This is a multi-part message in MIME format. +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +Test + +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="MyFile.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="MyFile.txt" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E-- diff --git a/tests/messages/thread_my_topic.eml b/tests/messages/thread_my_topic.eml new file mode 100644 index 00000000..9a196351 --- /dev/null +++ b/tests/messages/thread_my_topic.eml @@ -0,0 +1,10 @@ +Message-ID: <123@example.com> +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/thread_re_my_topic.eml b/tests/messages/thread_re_my_topic.eml new file mode 100644 index 00000000..db4fdf41 --- /dev/null +++ b/tests/messages/thread_re_my_topic.eml @@ -0,0 +1,11 @@ +Message-ID: <456@example.com> +In-Reply-To: <123@example.com> +From: from@there.com +To: to@here.com +Subject: Re: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/thread_unrelated.eml b/tests/messages/thread_unrelated.eml new file mode 100644 index 00000000..e47faa81 --- /dev/null +++ b/tests/messages/thread_unrelated.eml @@ -0,0 +1,10 @@ +Message-ID: <999@example.com> +From: from@there.com +To: to@here.com +Subject: Wut +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/undefined_charset_header.eml b/tests/messages/undefined_charset_header.eml new file mode 100644 index 00000000..117a33e4 --- /dev/null +++ b/tests/messages/undefined_charset_header.eml @@ -0,0 +1,20 @@ +X-Real-To: +X-Stored-In: BlaBla +Return-Path: +Received: from + by bla.bla (CommuniGate Pro RULE 6.1.13) + with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930 +X-Autogenerated: Mirror +Resent-From: +Resent-Date: Mon, 27 Feb 2017 13:21:44 +0930 +Message-Id: <201702270351.BGF77614@bla.bla> +From: =?X-IAS-German?B?bXlHb3Y=?= +To: sales@bla.bla +Subject: =?X-IAS-German?B?U3VibWl0IHlvdXIgdGF4IHJlZnVuZCB8IEF1c3RyYWxpYW4gVGF4YXRpb24gT2ZmaWNlLg==?= +Date: 27 Feb 2017 04:51:29 +0100 +MIME-Version: 1.0 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +) \ No newline at end of file diff --git a/tests/messages/undisclosed_recipients_minus.eml b/tests/messages/undisclosed_recipients_minus.eml new file mode 100644 index 00000000..73f7b83f --- /dev/null +++ b/tests/messages/undisclosed_recipients_minus.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: undisclosed-recipients:; + +Hi! diff --git a/tests/messages/undisclosed_recipients_space.eml b/tests/messages/undisclosed_recipients_space.eml new file mode 100644 index 00000000..6c06cbd4 --- /dev/null +++ b/tests/messages/undisclosed_recipients_space.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: Undisclosed recipients:; + +Hi! diff --git a/tests/messages/unknown_encoding.eml b/tests/messages/unknown_encoding.eml new file mode 100644 index 00000000..271bc4e2 --- /dev/null +++ b/tests/messages/unknown_encoding.eml @@ -0,0 +1,24 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset= +Content-Transfer-Encoding: foobar + +MyPlain + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/tests/messages/without_charset_plain_only.eml b/tests/messages/without_charset_plain_only.eml new file mode 100644 index 00000000..bd4fe5de --- /dev/null +++ b/tests/messages/without_charset_plain_only.eml @@ -0,0 +1,8 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; charset= +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/tests/messages/without_charset_simple_multipart.eml b/tests/messages/without_charset_simple_multipart.eml new file mode 100644 index 00000000..d1a092d2 --- /dev/null +++ b/tests/messages/without_charset_simple_multipart.eml @@ -0,0 +1,23 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset= +Content-Transfer-Encoding: 7bit + +MyPlain +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + From a9e5975c34ebadca0ea17e3dbbb81db737b9019c Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:49:51 +0100 Subject: [PATCH 474/600] Thread test added --- tests/live/MessageTest.php | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index d7850917..31b15579 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -19,6 +19,7 @@ use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\FolderFetchingException; +use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -102,6 +103,60 @@ public function testConvertEncoding(): void { self::assertTrue($message->delete()); } + /** + * Test Message::thread() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + */ + public function testThread(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'thread']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $message1 = $this->appendMessageTemplate($folder, "thread_my_topic.eml"); + $message2 = $this->appendMessageTemplate($folder, "thread_re_my_topic.eml"); + $message3 = $this->appendMessageTemplate($folder, "thread_unrelated.eml"); + + $thread = $message1->thread($folder); + self::assertCount(2, $thread); + + $thread = $message2->thread($folder); + self::assertCount(2, $thread); + + $thread = $message3->thread($folder); + self::assertCount(1, $thread); + + // Cleanup + self::assertTrue($message1->delete()); + self::assertTrue($message2->delete()); + self::assertTrue($message3->delete()); + $client->expunge(); + + self::assertTrue($this->deleteFolder($folder)); + } + /** * Test Message::hasAttachments() * From 6420569249bf40a122df5eff1a11d777560c91a0 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:50:39 +0100 Subject: [PATCH 475/600] Changelog updated --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a5ab54..1af2da05 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prevent message body parsing from adding empty lines - Don't parse regular inline message parts without name or filename as attachment - `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty +- Imap-Protocol "empty response" detection extended to catch an empty response caused by a broken resource stream +- iconv_mime_decode() is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing +- Date decoding rules extended to support more date formats +- Unset the currently active folder if it gets deleted (prevent infinite loop) +- Attachment name and filename parsing fixed and improved to support more formats ### Added - Extended UTF-7 support added (RFC2060) #383 @@ -26,7 +31,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::getConfig()` method added to get the current message configuration - `Folder::select()` method added to select a folder - `Message::getAvailableFlags()` method added to get all available flags -- Live mailbox tests added +- Live mailbox and fixture tests added - `Attribute::map()` method added to map all attribute values - `Header::has()` method added to check if a header attribute / value exist - All part attributes are now accessible via linked attribute From 77026c23e0a17117b9fbde74d937383ab428b63a Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 04:32:12 +0100 Subject: [PATCH 476/600] Check if the next uid is available before fetching it #381 --- CHANGELOG.md | 1 + src/Message.php | 6 ++++ tests/MessageTest.php | 66 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af2da05..68a9e83e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Date decoding rules extended to support more date formats - Unset the currently active folder if it gets deleted (prevent infinite loop) - Attachment name and filename parsing fixed and improved to support more formats +- Check if the next uid is available (after copying or moving a message) before fetching it #381 ### Added - Extended UTF-7 support added (RFC2060) #383 diff --git a/src/Message.php b/src/Message.php index 5cd9ff7a..47562cb7 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1068,6 +1068,9 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; + if ((int)$next_uid <= 0) { + return null; + } /** @var Folder $folder */ $folder = $this->client->getFolderByPath($folder_path); @@ -1107,6 +1110,9 @@ public function move(string $folder_path, bool $expunge = false): ?Message { if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; + if ((int)$next_uid <= 0) { + return null; + } /** @var Folder $folder */ $folder = $this->client->getFolderByPath($folder_path); diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 88abd40c..3f854918 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -24,6 +24,7 @@ use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageSizeFetchingException; use Webklex\PHPIMAP\Exceptions\ResponseException; use Webklex\PHPIMAP\IMAP; use Webklex\PHPIMAP\Message; @@ -80,9 +81,10 @@ public function setUp(): void { * @throws MessageContentFetchingException * @throws MessageFlagException * @throws MessageNotFoundException + * @throws MessageSizeFetchingException * @throws ReflectionException - * @throws RuntimeException * @throws ResponseException + * @throws RuntimeException */ public function testMessage(): void { $this->createNewProtocolMockup(); @@ -123,16 +125,48 @@ public function testMessage(): void { } /** - * @throws RuntimeException - * @throws MessageContentFetchingException - * @throws ImapServerErrorException - * @throws ConnectionFailedException + * Test getMessageNumber + * + * @return void * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMessageNumber(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('getMessageNumber')->willReturn(Response::empty()->setResult("")); + + self::assertNotEmpty($this->client->openFolder("INBOX")); + + try { + $this->client->getConnection()->getMessageNumber(21)->validatedData(); + $this->fail("Message number should not exist"); + } catch (ResponseException $e) { + self::assertTrue(true); + } + + } + + /** + * Test loadMessageFromFile + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageNotFoundException * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageSizeFetchingException */ public function testLoadMessageFromFile(): void { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"]); @@ -189,6 +223,21 @@ public function testLoadMessageFromFile(): void { self::assertSame("text/plain", $attachment->getMimeType()); } + /** + * Test issue #348 + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ public function testIssue348() { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "issue-348.eml"]); $message = Message::fromFile($filename); @@ -210,7 +259,12 @@ public function testIssue348() { self::assertSame("application/pdf", $attachment->getMimeType()); } - protected function createNewProtocolMockup() { + /** + * Create a new protocol mockup + * + * @return void + */ + protected function createNewProtocolMockup(): void { $this->protocol = $this->createMock(ImapProtocol::class); $this->protocol->expects($this->any())->method('createStream')->willReturn(true); From 79e370d8f3bca3c9a16d381e7e6db806cdbba32e Mon Sep 17 00:00:00 2001 From: hhniao <12420958+hhniao@users.noreply.github.com> Date: Sat, 11 Mar 2023 11:37:14 +0800 Subject: [PATCH 477/600] Update PaginatedCollection.php (#385) fix bug for high version php Typed property Webklex\PHPIMAP\Support\PaginatedCollection::$total must not be accessed before initialization --- src/Support/PaginatedCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 27e7fd0d..3b3470e9 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -28,7 +28,7 @@ class PaginatedCollection extends Collection { * * @var int $total */ - protected int $total; + protected int $total = 0; /** * Paginate the current collection. From 70b9132c5c4717aca00d51afec6f7f1e2cd6e9bd Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 04:37:56 +0100 Subject: [PATCH 478/600] Changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a9e83e..c341729d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Unset the currently active folder if it gets deleted (prevent infinite loop) - Attachment name and filename parsing fixed and improved to support more formats - Check if the next uid is available (after copying or moving a message) before fetching it #381 +- Default pagination $total attribute value set to 0 #385 (thanks @hhniao) ### Added - Extended UTF-7 support added (RFC2060) #383 From 7e792494a17bfa616162c34d605a6a4b71a59765 Mon Sep 17 00:00:00 2001 From: Adrien Brault Date: Sat, 11 Mar 2023 04:39:17 +0100 Subject: [PATCH 479/600] add missing @implements to MessageCollection (#359) --- src/Support/MessageCollection.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Support/MessageCollection.php b/src/Support/MessageCollection.php index 6d1249fa..1ca97a70 100644 --- a/src/Support/MessageCollection.php +++ b/src/Support/MessageCollection.php @@ -12,11 +12,15 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; +use Webklex\PHPIMAP\Message; + /** * Class MessageCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class MessageCollection extends PaginatedCollection { -} \ No newline at end of file +} From 7595508dd0df657b3e37876fe56208b0be53d1b2 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 04:42:44 +0100 Subject: [PATCH 480/600] docs updated --- src/Support/AttachmentCollection.php | 4 ++++ src/Support/FlagCollection.php | 3 +++ src/Support/FolderCollection.php | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/src/Support/AttachmentCollection.php b/src/Support/AttachmentCollection.php index 8b3f9c32..9d2af20d 100644 --- a/src/Support/AttachmentCollection.php +++ b/src/Support/AttachmentCollection.php @@ -12,10 +12,14 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; +use Webklex\PHPIMAP\Attachment; + /** * Class AttachmentCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class AttachmentCollection extends PaginatedCollection { diff --git a/src/Support/FlagCollection.php b/src/Support/FlagCollection.php index 929594ee..b8bf352c 100644 --- a/src/Support/FlagCollection.php +++ b/src/Support/FlagCollection.php @@ -12,10 +12,13 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; + /** * Class FlagCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class FlagCollection extends PaginatedCollection { diff --git a/src/Support/FolderCollection.php b/src/Support/FolderCollection.php index f74ec4d4..212ddb0a 100644 --- a/src/Support/FolderCollection.php +++ b/src/Support/FolderCollection.php @@ -12,10 +12,14 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; +use Webklex\PHPIMAP\Folder; + /** * Class FolderCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class FolderCollection extends PaginatedCollection { From 3ca7a9d2a0b780810d958fed288cbbd503118894 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 05:00:21 +0100 Subject: [PATCH 481/600] Use attachment ID as fallback filename for saving an attachment --- CHANGELOG.md | 11 ++++++----- src/Attachment.php | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c341729d..6af555a1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,20 +11,21 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Partial fix for #362 (allow overview response to be empty) - `Message::setConfig()` config parameter type set to array - Reset the protocol uid cache if the session gets expunged -- Set the "seen" flag only if the flag isn't set and the fetch option isn't IMAP::FT_PEEK +- Set the "seen" flag only if the flag isn't set and the fetch option isn't `IMAP::FT_PEEK` - `Message::is()` date comparison fixed - `Message::$client` could not be set to null -- in_reply_to and references parsing fixed -- Prevent message body parsing from adding empty lines +- `in_reply_to` and `references` parsing fixed +- Prevent message body parser from injecting empty lines - Don't parse regular inline message parts without name or filename as attachment - `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty - Imap-Protocol "empty response" detection extended to catch an empty response caused by a broken resource stream -- iconv_mime_decode() is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing +- `iconv_mime_decode()` is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing - Date decoding rules extended to support more date formats - Unset the currently active folder if it gets deleted (prevent infinite loop) - Attachment name and filename parsing fixed and improved to support more formats - Check if the next uid is available (after copying or moving a message) before fetching it #381 -- Default pagination $total attribute value set to 0 #385 (thanks @hhniao) +- Default pagination `$total` attribute value set to 0 #385 (thanks @hhniao) +- Use attachment ID as fallback filename for saving an attachment ### Added - Extended UTF-7 support added (RFC2060) #383 diff --git a/src/Attachment.php b/src/Attachment.php index 2c9c23cc..963f30eb 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -249,9 +249,9 @@ protected function fetch(): void { * @return boolean */ public function save(string $path, string $filename = null): bool { - $filename = $filename ?: $this->getName(); + $filename = $filename ?? $this->filename ?? $this->name ?? $this->id; - return file_put_contents($path.$filename, $this->getContent()) !== false; + return file_put_contents($path.DIRECTORY_SEPARATOR.$filename, $this->getContent()) !== false; } /** From 69dc33380a75f38ebb48775adf47643c4863fb7f Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 13 Mar 2023 15:23:29 +0100 Subject: [PATCH 482/600] Additional multipart message test added --- tests/fixtures/MultipartWithoutBodyTest.php | 63 +++++++++++ tests/messages/multipart_without_body.eml | 110 ++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tests/fixtures/MultipartWithoutBodyTest.php create mode 100644 tests/messages/multipart_without_body.eml diff --git a/tests/fixtures/MultipartWithoutBodyTest.php b/tests/fixtures/MultipartWithoutBodyTest.php new file mode 100644 index 00000000..9989b3e7 --- /dev/null +++ b/tests/fixtures/MultipartWithoutBodyTest.php @@ -0,0 +1,63 @@ +getFixture("multipart_without_body.eml"); + + self::assertEquals("This mail will not contain a body", $message->subject); + self::assertEquals("This mail will not contain a body", $message->getTextBody()); + self::assertEquals("d76dfb1ff3231e3efe1675c971ce73f722b906cc049d328db0d255f8d3f65568", hash("sha256", $message->getHTMLBody())); + self::assertEquals("2023-03-11 08:24:31", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("Foo Bülow Bar ", $message->from); + self::assertEquals("some one ", $message->to); + self::assertEquals([ + 0 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 08:24:33 +0000', + 1 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar 2023 08:24:31 +0000', + 2 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 08:24:31 +0000', + 3 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS', + ], $message->received->all()); + self::assertEquals("This mail will not contain a body", $message->thread_topic); + self::assertEquals("AdlT8uVmpHPvImbCRM6E9LODIvAcQA==", $message->thread_index); + self::assertEquals("omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9@omef0ahNgeoJu.eurprd02.prod.outlook.com", $message->message_id); + self::assertEquals("da-DK, en-US", $message->accept_language); + self::assertEquals("en-US", $message->content_language); + self::assertEquals("Internal", $message->x_ms_exchange_organization_authAs); + self::assertEquals("04", $message->x_ms_exchange_organization_authMechanism); + self::assertEquals("omef0ahNgeoJu.eurprd02.prod.outlook.com", $message->x_ms_exchange_organization_authSource); + self::assertEquals("", $message->x_ms_Has_Attach); + self::assertEquals("aa546a02-2b7a-4fb1-7fd4-08db220a09f1", $message->x_ms_exchange_organization_Network_Message_Id); + self::assertEquals("-1", $message->x_ms_exchange_organization_SCL); + self::assertEquals("", $message->x_ms_TNEF_Correlator); + self::assertEquals("0", $message->x_ms_exchange_organization_RecordReviewCfmType); + self::assertEquals("Email", $message->x_ms_publictraffictype); + self::assertEquals("ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097);", $message->X_Microsoft_Antispam_Mailbox_Delivery->first()); + self::assertEquals("0712b5fe22cf6e75fa220501c1a6715a61098983df9e69bad4000c07531c1295", hash("sha256", $message->X_Microsoft_Antispam_Message_Info)); + self::assertEquals("multipart/alternative", $message->Content_Type->last()); + self::assertEquals("1.0", $message->mime_version); + + self::assertCount(0, $message->getAttachments()); + } +} \ No newline at end of file diff --git a/tests/messages/multipart_without_body.eml b/tests/messages/multipart_without_body.eml new file mode 100644 index 00000000..8f86799f --- /dev/null +++ b/tests/messages/multipart_without_body.eml @@ -0,0 +1,110 @@ +Received: from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) + by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 + 08:24:33 +0000 +Received: from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) + by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar + 2023 08:24:31 +0000 +Received: from omef0ahNgeoJu.eurprd02.prod.outlook.com + ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com + ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 + 08:24:31 +0000 +From: =?iso-8859-1?Q?Foo_B=FClow_Bar?= +To: some one +Subject: This mail will not contain a body +Thread-Topic: This mail will not contain a body +Thread-Index: AdlT8uVmpHPvImbCRM6E9LODIvAcQA== +Date: Sat, 11 Mar 2023 08:24:31 +0000 +Message-ID: + +Accept-Language: da-DK, en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Internal +X-MS-Exchange-Organization-AuthMechanism: 04 +X-MS-Exchange-Organization-AuthSource: omef0ahNgeoJu.eurprd02.prod.outlook.com +X-MS-Has-Attach: +X-MS-Exchange-Organization-Network-Message-Id: + aa546a02-2b7a-4fb1-7fd4-08db220a09f1 +X-MS-Exchange-Organization-SCL: -1 +X-MS-TNEF-Correlator: +X-MS-Exchange-Organization-RecordReviewCfmType: 0 +x-ms-publictraffictype: Email +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097); +X-Microsoft-Antispam-Message-Info: + PeifKDwBVlQGeDu/c7oB3MKTffBwvlRIg5GJo1AiA4LroGAjwgwlg+oPfLetX9CgtbtKZy4gZnbjLCn3jJnod5ehn3Sv9gQaoH9PWkH/PIj6izaJzwlcEk81/MdprZFrORMwR7TGNqP/7ELAHw8rOH2Dmz9vGCE4cv0EwyYS3ShUhXABj4eJ17GNu1B4o53T2m9CzTgm647FRR5jpvk5xNIjQOwrhonVXMkCf2XKwF21sd/k4XLS/jX08tFJXBNyBALhsB4cG9gx2rxZYDn11SBejGFDw4eaqIQw6fYsmsyZvEoCnBkO+vK9lGIrQhRzGj7nJ41+uHVBcUYThce7P/ORpxl3GgThHyQpXQDV00JbP1aCBzm+4w8TyFiL0aOHXDhU9UKRHg3A01F+oH8IKAIaYazLCoOzbVcijSw0icNjBQsNLWa0FvfRT8y/rIvGkOB3rZpKf7ZR0g7cSlDAB3Ml2AaTbIB4ZL6QMukP/waDIObMZFmlVaAvmJzdTEdhGSLtUBFk4CNJhd6szcwaaPZxROumOGtz0sDke2iap8wRZqpdMWHVYi/trA+IESlF2G8TPzZiXs1lRDvYjNlam5r+1Ay1zlSmKTMnGbfNvsJgHkTlcgswKTlip4jGFPh6INTSDZtx9dQuDi4vbyNQiN1qVxoOPScTXzxUKVZ2PJ+8ipL2dqudLb3R2GSDHL10uQTuoIftA/Wjf67QZ629qQ== +Content-Type: multipart/alternative; + boundary="_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_" +MIME-Version: 1.0 + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +This mail will not contain a body + + + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

This mail will not contain a bo= +dy

+

 

+
+ + + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_-- From fa73689dfd3e9e90dac31eba83db67d120f59c5d Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:19:22 +0100 Subject: [PATCH 483/600] Query tests added --- tests/live/QueryTest.php | 283 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 tests/live/QueryTest.php diff --git a/tests/live/QueryTest.php b/tests/live/QueryTest.php new file mode 100644 index 00000000..af651ef4 --- /dev/null +++ b/tests/live/QueryTest.php @@ -0,0 +1,283 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Try to create a new query instance with a where clause + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + * @throws InvalidWhereQueryCriteriaException + * @throws MessageSearchValidationException + */ + public function testQueryWhere(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'search']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $messages = [ + $this->appendMessageTemplate($folder, '1366671050@github.com.eml'), + $this->appendMessageTemplate($folder, 'attachment_encoded_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_long_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_no_disposition.eml'), + $this->appendMessageTemplate($folder, 'bcc.eml'), + $this->appendMessageTemplate($folder, 'boolean_decoded_content.eml'), + $this->appendMessageTemplate($folder, 'email_address.eml'), + $this->appendMessageTemplate($folder, 'embedded_email.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition-embedded.eml'), + $this->appendMessageTemplate($folder, 'example_attachment.eml'), + $this->appendMessageTemplate($folder, 'example_bounce.eml'), + $this->appendMessageTemplate($folder, 'four_nested_emails.eml'), + $this->appendMessageTemplate($folder, 'gbk_charset.eml'), + $this->appendMessageTemplate($folder, 'html_only.eml'), + $this->appendMessageTemplate($folder, 'imap_mime_header_decode_returns_false.eml'), + $this->appendMessageTemplate($folder, 'inline_attachment.eml'), + $this->appendMessageTemplate($folder, 'issue-275.eml'), + $this->appendMessageTemplate($folder, 'issue-275-2.eml'), + $this->appendMessageTemplate($folder, 'issue-348.eml'), + $this->appendMessageTemplate($folder, 'ks_c_5601-1987_headers.eml'), + $this->appendMessageTemplate($folder, 'mail_that_is_attachment.eml'), + $this->appendMessageTemplate($folder, 'missing_date.eml'), + $this->appendMessageTemplate($folder, 'missing_from.eml'), + $this->appendMessageTemplate($folder, 'mixed_filename.eml'), + $this->appendMessageTemplate($folder, 'multipart_without_body.eml'), + $this->appendMessageTemplate($folder, 'multiple_html_parts_and_attachments.eml'), + $this->appendMessageTemplate($folder, 'multiple_nested_attachments.eml'), + $this->appendMessageTemplate($folder, 'nestes_embedded_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'null_content_charset.eml'), + $this->appendMessageTemplate($folder, 'pec.eml'), + $this->appendMessageTemplate($folder, 'plain.eml'), + $this->appendMessageTemplate($folder, 'plain_only.eml'), + $this->appendMessageTemplate($folder, 'plain_text_attachment.eml'), + $this->appendMessageTemplate($folder, 'references.eml'), + $this->appendMessageTemplate($folder, 'simple_multipart.eml'), + $this->appendMessageTemplate($folder, 'structured_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'thread_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_re_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_unrelated.eml'), + $this->appendMessageTemplate($folder, 'undefined_charset_header.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_minus.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_space.eml'), + $this->appendMessageTemplate($folder, 'unknown_encoding.eml'), + $this->appendMessageTemplate($folder, 'without_charset_plain_only.eml'), + $this->appendMessageTemplate($folder, 'without_charset_simple_multipart.eml'), + ]; + + $folder->getClient()->expunge(); + + $query = $folder->query()->all(); + self::assertEquals(count($messages), $query->count()); + + $query = $folder->query()->whereSubject("test"); + self::assertEquals(11, $query->count()); + + $query = $folder->query()->whereOn(Carbon::now()); + self::assertEquals(count($messages), $query->count()); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test query where criteria + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQueryWhereCriteria(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertWhereSearchCriteria($folder, 'SUBJECT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'BODY', 'Test'); + $this->assertWhereSearchCriteria($folder, 'TEXT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'KEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'UNKEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'FLAGGED', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'UNFLAGGED', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'Message-ID', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'In-Reply-To', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'BCC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'CC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'FROM', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'TO', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'UID', '1'); + $this->assertWhereSearchCriteria($folder, 'UID', '1,2'); + $this->assertWhereSearchCriteria($folder, 'ALL'); + $this->assertWhereSearchCriteria($folder, 'NEW'); + $this->assertWhereSearchCriteria($folder, 'OLD'); + $this->assertWhereSearchCriteria($folder, 'SEEN'); + $this->assertWhereSearchCriteria($folder, 'UNSEEN'); + $this->assertWhereSearchCriteria($folder, 'RECENT'); + $this->assertWhereSearchCriteria($folder, 'ANSWERED'); + $this->assertWhereSearchCriteria($folder, 'UNANSWERED'); + $this->assertWhereSearchCriteria($folder, 'DELETED'); + $this->assertWhereSearchCriteria($folder, 'UNDELETED'); + $this->assertHeaderSearchCriteria($folder, 'Content-Language','en_US'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag NO'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag YES'); + $this->assertWhereSearchCriteria($folder, 'NOT'); + $this->assertWhereSearchCriteria($folder, 'OR'); + $this->assertWhereSearchCriteria($folder, 'AND'); + $this->assertWhereSearchCriteria($folder, 'BEFORE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'BEFORE', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'ON', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'ON', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'SINCE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'SINCE', Carbon::now()->subDays(1), true); + } + + /** + * Assert where search criteria + * @param Folder $folder + * @param string $criteria + * @param string|Carbon|null $value + * @param bool $date + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + $query = $folder->query()->where($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + $criteria = str_replace("CUSTOM ", "", $criteria); + $expected = $value === null ? [$criteria] : [$criteria, $value]; + if ($date === true && $value instanceof Carbon) { + $date_format = ClientManager::get('date_format', 'd M y'); + $expected[1] = $value->format($date_format); + } + + self::assertIsArray($item); + self::assertIsString($item[0]); + if($value !== null) { + self::assertCount(2, $item); + self::assertIsString($item[1]); + }else{ + self::assertCount(1, $item); + } + self::assertSame($expected, $item); + } + + /** + * Assert header search criteria + * @param Folder $folder + * @param string $criteria + * @param mixed|null $value + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertHeaderSearchCriteria(Folder $folder, string $criteria, mixed $value = null): void { + $query = $folder->query()->whereHeader($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + + self::assertIsArray($item); + self::assertIsString($item[0]); + self::assertCount(1, $item); + self::assertSame(['HEADER '.$criteria.' '.$value], $item); + } +} \ No newline at end of file From f5033a5bcd72753bcb6737645b53246393eaafc3 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:56:33 +0100 Subject: [PATCH 484/600] Additional value check added to prevent null values --- src/Address.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Address.php b/src/Address.php index ba186d1e..b45c72de 100644 --- a/src/Address.php +++ b/src/Address.php @@ -38,11 +38,11 @@ class Address { * @param object $object */ public function __construct(object $object) { - if (property_exists($object, "personal")){ $this->personal = $object->personal; } - if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox; } - if (property_exists($object, "host")){ $this->host = $object->host; } - if (property_exists($object, "mail")){ $this->mail = $object->mail; } - if (property_exists($object, "full")){ $this->full = $object->full; } + if (property_exists($object, "personal")){ $this->personal = $object->personal ?? ''; } + if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox ?? ''; } + if (property_exists($object, "host")){ $this->host = $object->host ?? ''; } + if (property_exists($object, "mail")){ $this->mail = $object->mail ?? ''; } + if (property_exists($object, "full")){ $this->full = $object->full ?? ''; } } From 63f26f6087e6f52bb9a5667ec617979c73e95f85 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:58:32 +0100 Subject: [PATCH 485/600] Available criteria accessor added --- src/Query/WhereQuery.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index b9903aca..b218e545 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -543,4 +543,13 @@ public function unless(mixed $value, callable $callback, ?callable $default = nu return $this; } + + /** + * Get all available search criteria + * + * @return array|string[] + */ + public function getAvailableCriteria(): array { + return $this->available_criteria; + } } \ No newline at end of file From 88e65a8c9a10e23d6193dd8bac901a88995ff509 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:59:35 +0100 Subject: [PATCH 486/600] Address decoding error detection added #388 --- CHANGELOG.md | 1 + src/Header.php | 7 ++++ tests/fixtures/UndisclosedRecipientsTest.php | 42 ++++++++++++++++++++ tests/messages/undisclosed_recipients.eml | 8 ++++ 4 files changed, 58 insertions(+) create mode 100644 tests/fixtures/UndisclosedRecipientsTest.php create mode 100644 tests/messages/undisclosed_recipients.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af555a1..7507004a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Check if the next uid is available (after copying or moving a message) before fetching it #381 - Default pagination `$total` attribute value set to 0 #385 (thanks @hhniao) - Use attachment ID as fallback filename for saving an attachment +- Address decoding error detection added #388 ### Added - Extended UTF-7 support added (RFC2060) #383 diff --git a/src/Header.php b/src/Header.php index 49aa4397..4bd4390d 100644 --- a/src/Header.php +++ b/src/Header.php @@ -583,6 +583,13 @@ private function parseAddresses($list): array { } } + if ($address->host == ".SYNTAX-ERROR.") { + $address->host = ""; + } + if ($address->mailbox == "UNEXPECTED_DATA_AFTER_ADDRESS") { + $address->mailbox = ""; + } + $address->mail = ($address->mailbox && $address->host) ? $address->mailbox . '@' . $address->host : false; $address->full = ($address->personal) ? $address->personal . ' <' . $address->mail . '>' : $address->mail; diff --git a/tests/fixtures/UndisclosedRecipientsTest.php b/tests/fixtures/UndisclosedRecipientsTest.php new file mode 100644 index 00000000..f5f44164 --- /dev/null +++ b/tests/fixtures/UndisclosedRecipientsTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "Undisclosed Recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/tests/messages/undisclosed_recipients.eml b/tests/messages/undisclosed_recipients.eml new file mode 100644 index 00000000..5d33ecff --- /dev/null +++ b/tests/messages/undisclosed_recipients.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: "Undisclosed Recipients" <> + +Hi! From abf080eb745e21df7a3070ca356248dda021d4ad Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 01:04:11 +0100 Subject: [PATCH 487/600] Release information added --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7507004a..c454a212 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.1.0] - 2023-03-16 +### Fixed - IMAP Quota root command fixed - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) @@ -41,9 +52,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - All part attributes are now accessible via linked attribute - Restore a message from string `Message::fromString()` -### Breaking changes -- NaN - ## [5.0.1] - 2023-03-01 ### Fixed From db3b997ce5f718ccf460f6ca8975e8067f94d4c7 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Mar 2023 03:49:05 +0100 Subject: [PATCH 488/600] Use all available methods to detect the attachment extension instead of just one --- CHANGELOG.md | 2 +- src/Attachment.php | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c454a212..0f00dbb6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Use all available methods to detect the attachment extension instead of just one ### Added - NaN diff --git a/src/Attachment.php b/src/Attachment.php index 963f30eb..efae50ca 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -302,21 +302,25 @@ public function getMimeType(): ?string { * @return string|null */ public function getExtension(): ?string { + $extension = null; $guesser = "\Symfony\Component\Mime\MimeTypes"; if (class_exists($guesser) !== false) { /** @var Symfony\Component\Mime\MimeTypes $guesser */ $extensions = $guesser::getDefault()->getExtensions($this->getMimeType()); - return $extensions[0] ?? null; + $extension = $extensions[0] ?? null; } - - $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; - if (class_exists($deprecated_guesser) !== false){ - /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ - return $deprecated_guesser::getInstance()->guess($this->getMimeType()); + if ($extension === null) { + $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; + if (class_exists($deprecated_guesser) !== false){ + /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ + $extension = $deprecated_guesser::getInstance()->guess($this->getMimeType()); + } } - - $extensions = explode(".", $this->part->filename ?: $this->part->name); - return end($extensions); + if ($extension === null) { + $extensions = explode(".", $this->filename); + $extension = end($extensions); + } + return $extension; } /** From 75c988c3c7cef118e97108166fb8ff75355d3b4c Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Mar 2023 03:49:23 +0100 Subject: [PATCH 489/600] Extension test added --- tests/fixtures/AttachmentEncodedFilenameTest.php | 1 + tests/fixtures/AttachmentLongFilenameTest.php | 3 +++ tests/fixtures/AttachmentNoDispositionTest.php | 1 + tests/fixtures/BooleanDecodedContentTest.php | 1 + tests/fixtures/EmbeddedEmailTest.php | 1 + .../EmbeddedEmailWithoutContentDispositionEmbeddedTest.php | 2 ++ tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php | 4 ++++ tests/fixtures/ExampleBounceTest.php | 2 ++ tests/fixtures/FourNestedEmailsTest.php | 1 + tests/fixtures/InlineAttachmentTest.php | 1 + tests/fixtures/MailThatIsAttachmentTest.php | 1 + tests/fixtures/MixedFilenameTest.php | 1 + tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php | 2 ++ tests/fixtures/MultipleNestedAttachmentsTest.php | 2 ++ tests/fixtures/NestesEmbeddedWithAttachmentTest.php | 2 ++ tests/fixtures/PecTest.php | 3 +++ tests/fixtures/PlainTextAttachmentTest.php | 1 + tests/fixtures/StructuredWithAttachmentTest.php | 1 + 18 files changed, 30 insertions(+) diff --git a/tests/fixtures/AttachmentEncodedFilenameTest.php b/tests/fixtures/AttachmentEncodedFilenameTest.php index 33d97fde..b5da3597 100644 --- a/tests/fixtures/AttachmentEncodedFilenameTest.php +++ b/tests/fixtures/AttachmentEncodedFilenameTest.php @@ -40,6 +40,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('xls', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/vnd.ms-excel", $attachment->content_type); self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php index b3c3e903..c9e67d15 100644 --- a/tests/fixtures/AttachmentLongFilenameTest.php +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -42,6 +42,7 @@ public function testFixture() : void { self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXX xxxxxxxxxxxxxxxxx XxxX, Lüdxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->name); self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXXX xxxxxxxxxxxxxxxxx XxxX, Lüxxxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->filename); self::assertEquals('text', $attachment->type); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals("text/plain", $attachment->content_type); self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); self::assertEquals(4, $attachment->size); @@ -54,6 +55,7 @@ public function testFixture() : void { self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); self::assertEquals('text', $attachment->type); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals("text/plain", $attachment->content_type); self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); self::assertEquals(4, $attachment->size); @@ -67,6 +69,7 @@ public function testFixture() : void { self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); self::assertEquals('text', $attachment->type); self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); self::assertEquals(4, $attachment->size); self::assertEquals(2, $attachment->part_number); diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index 5a3fadd8..cb037053 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -41,6 +41,7 @@ public function testFixture() : void { self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('xls', $attachment->getExtension()); self::assertEquals("application/vnd.ms-excel", $attachment->content_type); self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); self::assertEquals(146, $attachment->size); diff --git a/tests/fixtures/BooleanDecodedContentTest.php b/tests/fixtures/BooleanDecodedContentTest.php index ae9b797f..e49229a4 100644 --- a/tests/fixtures/BooleanDecodedContentTest.php +++ b/tests/fixtures/BooleanDecodedContentTest.php @@ -44,6 +44,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("Example Domain.pdf", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); self::assertEquals(53, $attachment->size); diff --git a/tests/fixtures/EmbeddedEmailTest.php b/tests/fixtures/EmbeddedEmailTest.php index 4d8baf77..fb6e2492 100644 --- a/tests/fixtures/EmbeddedEmailTest.php +++ b/tests/fixtures/EmbeddedEmailTest.php @@ -53,6 +53,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("demo.eml", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("a1f965f10a9872e902a82dde039a237e863f522d238a1cb1968fe3396dbcac65", hash("sha256", $attachment->content)); self::assertEquals(893, $attachment->size); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php index e7629bc3..7d3fb2d0 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -52,6 +52,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file1.xlsx", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); @@ -62,6 +63,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file2.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index c62e743c..0c7e53cb 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -50,6 +50,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file.jpg", $attachment->name); + self::assertEquals('jpg', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("image/jpeg", $attachment->content_type); self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); @@ -62,6 +63,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); self::assertEquals(2073, $attachment->size); @@ -72,6 +74,7 @@ public function testFixture() : void { $attachment = $attachments[2]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file3.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); @@ -83,6 +86,7 @@ public function testFixture() : void { $attachment = $attachments[3]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file4.zip", $attachment->name); + self::assertEquals('zip', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/x-zip-compressed", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index d032f316..89810fc0 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -74,6 +74,7 @@ public function testFixture(): void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->filename); self::assertEquals("", $attachment->name); + self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/delivery-status", $attachment->content_type); self::assertEquals("85ac09d1d74b2d85853084dc22abcad205a6bfde62d6056e3a933ffe7e82e45c", hash("sha256", $attachment->content)); @@ -86,6 +87,7 @@ public function testFixture(): void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->filename); self::assertEquals("", $attachment->name); + self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("7525331f5fab23ea77f595b995336aca7b8dad12db00ada14abebe7fe5b96e10", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/FourNestedEmailsTest.php b/tests/fixtures/FourNestedEmailsTest.php index 5511c05e..e6c37ccd 100644 --- a/tests/fixtures/FourNestedEmailsTest.php +++ b/tests/fixtures/FourNestedEmailsTest.php @@ -43,6 +43,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("2-second-email.eml", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("85012e6a26d064a0288ee62618b3192687385adb4a4e27e48a28f738a325ca46", hash("sha256", $attachment->content)); self::assertEquals(1376, $attachment->size); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index ae538b37..c20ac8d5 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -47,6 +47,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); self::assertEquals("6568c9e9c35a7fa06f236e89f704d8c9b47183a24f2c978dba6c92e2747e3a13", hash("sha256", $attachment->content)); self::assertEquals(1486, $attachment->size); diff --git a/tests/fixtures/MailThatIsAttachmentTest.php b/tests/fixtures/MailThatIsAttachmentTest.php index e28dc938..0f4164c9 100644 --- a/tests/fixtures/MailThatIsAttachmentTest.php +++ b/tests/fixtures/MailThatIsAttachmentTest.php @@ -51,6 +51,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("google.com!yyy.cz!1423872000!1423958399.zip", $attachment->name); + self::assertEquals('zip', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/zip", $attachment->content_type); self::assertEquals("c0d4f47b6fde124cea7460c3e509440d1a062705f550b0502b8ba0cbf621c97a", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/MixedFilenameTest.php b/tests/fixtures/MixedFilenameTest.php index fc1ffa88..b2be0e32 100644 --- a/tests/fixtures/MixedFilenameTest.php +++ b/tests/fixtures/MixedFilenameTest.php @@ -49,6 +49,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("Price4VladDaKar.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/octet-stream", $attachment->content_type); self::assertEquals("b832983842b0ad65db69e4c7096444c540a2393e2d43f70c2c9b8b9fceeedbb1", hash('sha256', $attachment->content)); diff --git a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php index c8f72bb8..c08c03e0 100644 --- a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php +++ b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -52,6 +52,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("attachment1.pdf", $attachment->name); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); @@ -63,6 +64,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("attachment2.pdf", $attachment->name); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/MultipleNestedAttachmentsTest.php b/tests/fixtures/MultipleNestedAttachmentsTest.php index e7b9cbdf..543c0e21 100644 --- a/tests/fixtures/MultipleNestedAttachmentsTest.php +++ b/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -45,6 +45,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("mleokdgdlgkkecep.png", $attachment->name); + self::assertEquals('png', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); @@ -56,6 +57,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("FF4D00-1.png", $attachment->name); + self::assertEquals('png', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php index a1db7378..c5c8c561 100644 --- a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php +++ b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -45,6 +45,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("first.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); @@ -56,6 +57,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("second.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); diff --git a/tests/fixtures/PecTest.php b/tests/fixtures/PecTest.php index 01fe17c5..28ecb27b 100644 --- a/tests/fixtures/PecTest.php +++ b/tests/fixtures/PecTest.php @@ -46,6 +46,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("data.xml", $attachment->name); + self::assertEquals('xml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/xml", $attachment->content_type); self::assertEquals("", $attachment->content); @@ -57,6 +58,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("postacert.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); @@ -68,6 +70,7 @@ public function testFixture() : void { $attachment = $attachments[2]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("smime.p7s", $attachment->name); + self::assertEquals('p7s', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); self::assertEquals("1", $attachment->content); diff --git a/tests/fixtures/PlainTextAttachmentTest.php b/tests/fixtures/PlainTextAttachmentTest.php index 6106b558..2e9993cc 100644 --- a/tests/fixtures/PlainTextAttachmentTest.php +++ b/tests/fixtures/PlainTextAttachmentTest.php @@ -42,6 +42,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("a.txt", $attachment->name); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertNull($attachment->content_type); self::assertEquals("Hi!", $attachment->content); diff --git a/tests/fixtures/StructuredWithAttachmentTest.php b/tests/fixtures/StructuredWithAttachmentTest.php index a49d3609..3fc591c3 100644 --- a/tests/fixtures/StructuredWithAttachmentTest.php +++ b/tests/fixtures/StructuredWithAttachmentTest.php @@ -43,6 +43,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("MyFile.txt", $attachment->name); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("text/plain", $attachment->content_type); self::assertEquals("MyFileContent", $attachment->content); From 7fa61f6467a1fbfe7627ea79b668e6a187f8b17b Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:20:19 +0100 Subject: [PATCH 490/600] Allow the `LIST` command response to be empty #393 --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 55297af0..914347b6 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -838,7 +838,7 @@ public function getMessageNumber(string $id): Response { * @throws RuntimeException */ public function folders(string $reference = '', string $folder = '*'): Response { - $response = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); + $response = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder))->setCanBeEmpty(true); $list = $response->data(); $result = []; From 4bad1705753c1f345e12e9ccf76cd393d6155357 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:21:19 +0100 Subject: [PATCH 491/600] Test for issue #393 added --- tests/issues/Issue393Test.php | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/issues/Issue393Test.php diff --git a/tests/issues/Issue393Test.php b/tests/issues/Issue393Test.php new file mode 100644 index 00000000..73099e07 --- /dev/null +++ b/tests/issues/Issue393Test.php @@ -0,0 +1,62 @@ +getClient(); + $client->connect(); + + $delimiter = $this->getManager()->get("options.delimiter"); + $pattern = implode($delimiter, ['doesnt_exist', '%']); + + $folder = $client->getFolder('doesnt_exist'); + $this->deleteFolder($folder); + + $folders = $client->getFolders(true, $pattern, true); + self::assertCount(0, $folders); + + try { + $client->getFolders(true, $pattern, false); + $this->fail('Expected FolderFetchingException::class exception not thrown'); + } catch (FolderFetchingException $e) { + self::assertInstanceOf(FolderFetchingException::class, $e); + } + } +} \ No newline at end of file From 4913ec6e8da53ff69fdf1244ea29db3478a78730 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:24:32 +0100 Subject: [PATCH 492/600] Soft fail option added to all folder fetching methods --- src/Client.php | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Client.php b/src/Client.php index a4e004ba..8027dc53 100755 --- a/src/Client.php +++ b/src/Client.php @@ -519,6 +519,7 @@ public function getFolder(string $folder_name, ?string $delimiter = null, bool $ /** * Get a folder instance by a folder name * @param $folder_name + * @param bool $soft_fail If true, it will return null instead of throwing an exception * * @return Folder|null * @throws FolderFetchingException @@ -529,14 +530,16 @@ public function getFolder(string $folder_name, ?string $delimiter = null, bool $ * @throws RuntimeException * @throws ResponseException */ - public function getFolderByName($folder_name): ?Folder { - return $this->getFolders(false)->where("name", $folder_name)->first(); + public function getFolderByName($folder_name, bool $soft_fail = false): ?Folder { + return $this->getFolders(false, null, $soft_fail)->where("name", $folder_name)->first(); } /** * Get a folder instance by a folder path * @param $folder_path * @param bool $utf7 + * @param bool $soft_fail If true, it will return null instead of throwing an exception + * * @return Folder|null * @throws AuthFailedException * @throws ConnectionFailedException @@ -546,9 +549,9 @@ public function getFolderByName($folder_name): ?Folder { * @throws ResponseException * @throws RuntimeException */ - public function getFolderByPath($folder_path, bool $utf7 = false): ?Folder { + public function getFolderByPath($folder_path, bool $utf7 = false, bool $soft_fail = false): ?Folder { if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "utf7-imap"); - return $this->getFolders(false)->where("path", $folder_path)->first(); + return $this->getFolders(false, null, $soft_fail)->where("path", $folder_path)->first(); } /** @@ -557,17 +560,18 @@ public function getFolderByPath($folder_path, bool $utf7 = false): ?Folder { * * @param boolean $hierarchical * @param string|null $parent_folder + * @param bool $soft_fail If true, it will return an empty collection instead of throwing an exception * * @return FolderCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws AuthFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws RuntimeException * @throws ResponseException + * @throws RuntimeException */ - public function getFolders(bool $hierarchical = true, string $parent_folder = null): FolderCollection { + public function getFolders(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -581,7 +585,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFolders(true, $pattern); + $children = $this->getFolders(true, $pattern, $soft_fail); $folder->setChildren($children); } @@ -589,9 +593,11 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu } return $folders; - }else{ + }else if (!$soft_fail){ throw new FolderFetchingException("failed to fetch any folders"); } + + return $folders; } /** @@ -600,6 +606,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * * @param boolean $hierarchical * @param string|null $parent_folder + * @param bool $soft_fail If true, it will return an empty collection instead of throwing an exception * * @return FolderCollection * @throws FolderFetchingException @@ -610,7 +617,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * @throws RuntimeException * @throws ResponseException */ - public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null): FolderCollection { + public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -624,7 +631,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFoldersWithStatus(true, $pattern); + $children = $this->getFoldersWithStatus(true, $pattern, $soft_fail); $folder->setChildren($children); } @@ -633,9 +640,11 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f } return $folders; - }else{ + }else if (!$soft_fail){ throw new FolderFetchingException("failed to fetch any folders"); } + + return $folders; } /** From d8174e6e976f573c3a0588b11747c551a4e7e730 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:25:36 +0100 Subject: [PATCH 493/600] Initialize folder children attributes on class initialization --- src/Folder.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Folder.php b/src/Folder.php index 0089dc05..c9ca395d 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -138,6 +138,8 @@ public function __construct(Client $client, string $folder_name, string $delimit $this->path = $folder_name; $this->full_name = $this->decodeName($folder_name); $this->name = $this->getSimpleName($this->delimiter, $this->full_name); + $this->children = new FolderCollection(); + $this->has_children = false; $this->parseAttributes($attributes); } From b2d127ea3996c36fe901732445c1948d47f03678 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:25:49 +0100 Subject: [PATCH 494/600] Changelog updated --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f00dbb6..431cbf1f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Use all available methods to detect the attachment extension instead of just one +- Allow the `LIST` command response to be empty #393 +- Initialize folder children attributes on class initialization ### Added -- NaN +- Soft fail option added to all folder fetching methods. If soft fail is enabled, the method will return an empty collection instead of throwing an exception if the folder doesn't exist ### Breaking changes - NaN From 3b3bd567839826b061233687cddcc0fb4f06d122 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 11 Apr 2023 00:18:21 +0200 Subject: [PATCH 495/600] Sponsor link added --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2fb9c13a..fa7febde 100755 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] - [Documentations](#documentations) - [Compatibility](#compatibility) - [Basic usage example](#basic-usage-example) +- [Sponsors](#sponsors) - [Testing](#testing) - [Known issues](#known-issues) - [Support](#support) @@ -97,6 +98,9 @@ foreach($folders as $folder){ } ``` +## Sponsors +[![Feline][ico-sponsor-feline]][link-sponsor-feline] + ## Testing To run the tests, please execute the following command: @@ -232,3 +236,7 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap [link-discord]: https://discord.gg/rd4cN9h6 + + +[ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png +[link-sponsor-feline]: https://www.feline.dk \ No newline at end of file From 62362cd527f9ba95fc4a35a0b5b0503b5d817d2e Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 11 Apr 2023 00:19:29 +0200 Subject: [PATCH 496/600] Release information added --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 431cbf1f..4934fae8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.2.0] - 2023-04-11 +### Fixed - Use all available methods to detect the attachment extension instead of just one - Allow the `LIST` command response to be empty #393 - Initialize folder children attributes on class initialization @@ -13,9 +24,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Soft fail option added to all folder fetching methods. If soft fail is enabled, the method will return an empty collection instead of throwing an exception if the folder doesn't exist -### Breaking changes -- NaN - ## [5.1.0] - 2023-03-16 ### Fixed From 6ea5a94f2f8936ac5c2c40a46fbbd88f2bdeb16b Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 11 Apr 2023 00:21:40 +0200 Subject: [PATCH 497/600] Old links removed --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index fa7febde..c110d48f 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Latest release on Packagist][ico-release]][link-packagist] [![Latest prerelease on Packagist][ico-prerelease]][link-packagist] [![Software License][ico-license]][link-license] -[![Build Status][ico-travis]][link-scrutinizer] [![Total Downloads][ico-downloads]][link-downloads] [![Hits][ico-hits]][link-hits] [![Discord][ico-discord]][link-discord] @@ -213,26 +212,17 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [ico-release]: https://img.shields.io/packagist/v/Webklex/php-imap.svg?style=flat-square&label=version [ico-prerelease]: https://img.shields.io/github/v/release/webklex/php-imap?include_prereleases&style=flat-square&label=pre-release [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square -[ico-travis]: https://img.shields.io/travis/Webklex/php-imap/master.svg?style=flat-square -[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/Webklex/php-imap.svg?style=flat-square -[ico-code-quality]: https://img.shields.io/scrutinizer/g/Webklex/php-imap.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/Webklex/php-imap.svg?style=flat-square -[ico-build]: https://img.shields.io/scrutinizer/build/g/Webklex/php-imap/master?style=flat-square -[ico-quality]: https://img.shields.io/scrutinizer/quality/g/Webklex/php-imap/master?style=flat-square [ico-hits]: https://hits.webklex.com/svg/webklex/php-imap [ico-snyk]: https://snyk-widget.herokuapp.com/badge/composer/webklex/php-imap/badge.svg [ico-discord]: https://img.shields.io/static/v1?label=discord&message=open&color=5865f2&style=flat-square [link-packagist]: https://packagist.org/packages/Webklex/php-imap -[link-travis]: https://travis-ci.org/Webklex/php-imap -[link-scrutinizer]: https://scrutinizer-ci.com/g/Webklex/php-imap/code-structure -[link-code-quality]: https://scrutinizer-ci.com/g/Webklex/php-imap [link-downloads]: https://packagist.org/packages/Webklex/php-imap [link-author]: https://github.com/webklex [link-contributors]: https://github.com/Webklex/php-imap/graphs/contributors [link-license]: https://github.com/Webklex/php-imap/blob/master/LICENSE [link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md -[link-jetbrains]: https://www.jetbrains.com [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap [link-discord]: https://discord.gg/rd4cN9h6 From 88aaafbd881f09c3ec174f7417d36a8dedbd1b37 Mon Sep 17 00:00:00 2001 From: Jeremy Angele <131715596+angelej@users.noreply.github.com> Date: Tue, 20 Jun 2023 13:07:45 +0200 Subject: [PATCH 498/600] Improve security (#414) * Improve security * Update phpunit tests --- src/Attachment.php | 26 +++++++++++-------- .../fixtures/AttachmentNoDispositionTest.php | 2 +- ...ddedEmailWithoutContentDispositionTest.php | 2 +- tests/fixtures/ExampleBounceTest.php | 2 +- tests/fixtures/InlineAttachmentTest.php | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index efae50ca..2fbde843 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -216,6 +216,10 @@ protected function fetch(): void { $this->filename = $this->decodeName($filename); } + if(!$this->filename){ + $this->filename = bin2hex(random_bytes(10)); + } + if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } @@ -233,11 +237,6 @@ protected function fetch(): void { $this->name = $this->part->subtype; } } - - if (!$this->filename) { - $this->filename = $this->name; - } - $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); } @@ -248,19 +247,19 @@ protected function fetch(): void { * * @return boolean */ - public function save(string $path, string $filename = null): bool { - $filename = $filename ?? $this->filename ?? $this->name ?? $this->id; + public function save(string $path, ?string $filename = null): bool { + $filename = $filename ? $this->decodeName($filename) : $this->filename; return file_put_contents($path.DIRECTORY_SEPARATOR.$filename, $this->getContent()) !== false; } /** * Decode a given name - * @param $name + * @param string|null $name * * @return string */ - public function decodeName($name): string { + public function decodeName(?string $name): string { if ($name !== null) { if (str_contains($name, "''")) { $parts = explode("''", $name); @@ -282,6 +281,11 @@ public function decodeName($name): string { if (preg_match('/%[0-9A-F]{2}/i', $name)) { $name = urldecode($name); } + + // sanitize $name + // order of '..' is important + $name = str_replace(['\\', '/', chr(0), ':', '..'], '', $name); + return $name; } return ""; @@ -317,8 +321,8 @@ public function getExtension(): ?string { } } if ($extension === null) { - $extensions = explode(".", $this->filename); - $extension = end($extensions); + $parts = explode(".", $this->filename); + $extension = count($parts) > 1 ? end($parts) : null; } return $extension; } diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index cb037053..1eee597b 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -38,7 +38,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('xls', $attachment->getExtension()); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 0c7e53cb..0f6a8a3f 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -61,7 +61,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->name); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index 89810fc0..685a72cf 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -72,7 +72,7 @@ public function testFixture(): void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->filename); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index c20ac8d5..b66976ef 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -45,7 +45,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->name); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); From a7770066936b2e8f6952f8598ec18f13bc1a58e7 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:33:12 +0200 Subject: [PATCH 499/600] Additional "extension" location added --- src/Attachment.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 2fbde843..fcbc54e6 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -216,10 +216,6 @@ protected function fetch(): void { $this->filename = $this->decodeName($filename); } - if(!$this->filename){ - $this->filename = bin2hex(random_bytes(10)); - } - if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } @@ -238,6 +234,10 @@ protected function fetch(): void { } } $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); + + if(!$this->filename){ + $this->filename = bin2hex(random_bytes(10)); + } } /** @@ -324,6 +324,10 @@ public function getExtension(): ?string { $parts = explode(".", $this->filename); $extension = count($parts) > 1 ? end($parts) : null; } + if ($extension === null) { + $parts = explode(".", $this->name); + $extension = count($parts) > 1 ? end($parts) : null; + } return $extension; } From 495a3fb44e03775d3f58c7cbd23ae80ab04322b8 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:33:55 +0200 Subject: [PATCH 500/600] tests updated --- tests/fixtures/AttachmentNoDispositionTest.php | 1 + tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php | 2 +- tests/fixtures/ExampleBounceTest.php | 2 +- tests/fixtures/InlineAttachmentTest.php | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index 1eee597b..76b3c12b 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -37,6 +37,7 @@ public function testFixture() : void { self::assertCount(1, $message->attachments()); $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 0f6a8a3f..67ad1cb0 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -61,7 +61,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); + self::assertEquals('', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index 685a72cf..20288206 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -85,7 +85,7 @@ public function testFixture(): void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->filename); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index b66976ef..a482f25b 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -45,7 +45,8 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); + self::assertEquals('', $attachment->name); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); From d1aeb4ed6a142f55f90a1bad2a5c797990a15294 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:34:27 +0200 Subject: [PATCH 501/600] Security release --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4934fae8..d967e5c1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,25 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [5.3.0] - Security patch - 2023-06-20 +### Fixed +- Potential RCE through path traversal fixed #414 (special thanks @angelej) + +### Security Impact and Mitigation +Impacted are all versions below v5.3.0. +If possible, update to >= v5.3.0 as soon as possible. Impacted was the `Attachment::save` +method which could be used to write files to the local filesystem. The path was not +properly sanitized and could be used to write files to arbitrary locations. + +However, the `Attachment::save` method is not used by default and has to be called +manually. If you are using this method without providing a sanitized path, you are +affected by this vulnerability. +If you are not using this method or are providing a sanitized path, you are not affected +by this vulnerability and no immediate action is required. + +If you have any questions, please feel to join this issue: https://github.com/Webklex/php-imap/issues/416 + + ## [5.2.0] - 2023-04-11 ### Fixed - Use all available methods to detect the attachment extension instead of just one From 4f99ebcc1edb371ca5698ad5e425701ef1f2cc5c Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:40:42 +0200 Subject: [PATCH 502/600] Timeline added --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d967e5c1..e9f1df28 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ by this vulnerability and no immediate action is required. If you have any questions, please feel to join this issue: https://github.com/Webklex/php-imap/issues/416 +#### Timeline +- 17.06.23 21:30: Vulnerability reported +- 18.06.23 19:14: Vulnerability confirmed +- 19.06.23 18:41: Vulnerability fixed via PR #414 +- 20.06.23 13:45: Security patch released + ## [5.2.0] - 2023-04-11 ### Fixed From 5c45dc2fad444dd3a21abcadbc686f3e815e03c6 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:43:16 +0200 Subject: [PATCH 503/600] spelling fixed --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f1df28..3a4bb3f2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,7 @@ affected by this vulnerability. If you are not using this method or are providing a sanitized path, you are not affected by this vulnerability and no immediate action is required. -If you have any questions, please feel to join this issue: https://github.com/Webklex/php-imap/issues/416 - +If you have any questions, please feel welcome to join this issue: https://github.com/Webklex/php-imap/issues/416 #### Timeline - 17.06.23 21:30: Vulnerability reported - 18.06.23 19:14: Vulnerability confirmed From d3b9eaf1498d8dc1fee7cdca1c4a10f497a40cad Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 19:01:34 +0200 Subject: [PATCH 504/600] Legacy protocol support fixed #411 --- CHANGELOG.md | 4 +- src/Connection/Protocols/LegacyProtocol.php | 129 +++--- tests/live/LegacyTest.php | 475 ++++++++++++++++++++ 3 files changed, 542 insertions(+), 66 deletions(-) create mode 100644 tests/live/LegacyTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4bb3f2..f451f40c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Legacy protocol support fixed (object to array conversion) #411 ### Added - NaN @@ -37,6 +37,8 @@ If you have any questions, please feel welcome to join this issue: https://githu - 18.06.23 19:14: Vulnerability confirmed - 19.06.23 18:41: Vulnerability fixed via PR #414 - 20.06.23 13:45: Security patch released +- 21.06.23 20:48: CVE-2023-35169 got assigned +- 21.06.23 20:58: Advisory released https://github.com/Webklex/php-imap/security/advisories/GHSA-47p7-xfcc-4pv9 ## [5.2.0] - 2023-04-11 diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 6eedddef..10bc9d9f 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -72,7 +72,7 @@ public function connect(string $host, int $port = null) { * @return Response */ public function login(string $user, string $password): Response { - return $this->response()->wrap(function($response) use($user, $password){ + return $this->response()->wrap(function($response) use ($user, $password) { /** @var Response $response */ try { $this->stream = \imap_open( @@ -108,7 +108,7 @@ public function login(string $user, string $password): Response { } if ($this->stream !== false) { - return ["TAG".$response->Noun()." OK [] Logged in\r\n"]; + return ["TAG" . $response->Noun() . " OK [] Logged in\r\n"]; } $response->addError("failed to login"); @@ -156,7 +156,7 @@ protected function getAddress(): string { * @return Response */ public function logout(): Response { - return $this->response()->wrap(function($response){ + return $this->response()->wrap(function($response) { /** @var Response $response */ if ($this->stream) { $this->uid_cache = []; @@ -165,7 +165,7 @@ public function logout(): Response { $this->stream = false; return [ 0 => "BYE Logging out\r\n", - 1 => "TAG".$response->Noun()." OK Logout completed (0.001 + 0.000 secs).\r\n", + 1 => "TAG" . $response->Noun() . " OK Logout completed (0.001 + 0.000 secs).\r\n", ]; } $this->stream = false; @@ -199,7 +199,7 @@ public function selectFolder(string $folder = 'INBOX'): Response { throw new RuntimeException("failed to reopen stream."); } - return $this->response("imap_reopen")->wrap(function($response)use($folder, $flags){ + return $this->response("imap_reopen")->wrap(function($response) use ($folder, $flags) { /** @var Response $response */ \imap_reopen($this->stream, $this->getAddress() . $folder, $flags, 3); $this->uid_cache = []; @@ -222,10 +222,9 @@ public function examineFolder(string $folder = 'INBOX'): Response { if (str_starts_with($folder, ".")) { throw new RuntimeException("Segmentation fault prevented. Folders starts with an illegal char '.'."); } - $folder = $this->getAddress() . $folder; - return $this->response("imap_status")->wrap(function($response)use($folder){ + return $this->response("imap_status")->wrap(function($response) use ($folder) { /** @var Response $response */ - $status = \imap_status($this->stream, $folder, IMAP::SA_ALL); + $status = \imap_status($this->stream, $this->getAddress() . $folder, IMAP::SA_ALL); return $status ? [ "flags" => [], @@ -246,7 +245,7 @@ public function examineFolder(string $folder = 'INBOX'): Response { * @return Response */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; @@ -269,7 +268,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid * @return Response */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; @@ -291,7 +290,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid * @return Response */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; @@ -314,7 +313,7 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response return $result; }); } - + /** * Fetch message sizes * @param int|array $uids @@ -323,21 +322,22 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @return Response */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; $uids = is_array($uids) ? $uids : [$uids]; - $uid_text = implode("','",$uids); + $uid_text = implode("','", $uids); $response->addCommand("imap_fetch_overview"); - $raw_overview = false; - if ($uid == IMAP::ST_UID) - $raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID); - else - $raw_overview = \imap_fetch_overview($this->stream, $uid_text); + if ($uid == IMAP::ST_UID) { + $raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID); + } else { + $raw_overview = \imap_fetch_overview($this->stream, $uid_text); + } if ($raw_overview !== false) { - foreach ($raw_overview as $overview_element) { - $result[$overview_element[$uid == IMAP::ST_UID ? 'uid': 'msgno']] = $overview_element['size']; - } + foreach ($raw_overview as $overview_element) { + $overview_element = (array)$overview_element; + $result[$overview_element[$uid == IMAP::ST_UID ? 'uid' : 'msgno']] = $overview_element['size']; + } } return $result; }); @@ -350,7 +350,7 @@ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @return Response message number for given message or all messages as array */ public function getUid(int $id = null): Response { - return $this->response()->wrap(function($response)use($id){ + return $this->response()->wrap(function($response) use ($id) { /** @var Response $response */ if ($id === null) { if ($this->enable_uid_cache && $this->uid_cache) { @@ -370,7 +370,7 @@ public function getUid(int $id = null): Response { $response->addCommand("imap_uid"); $uid = \imap_uid($this->stream, $id); - if($uid) { + if ($uid) { return $uid; } @@ -385,7 +385,7 @@ public function getUid(int $id = null): Response { * @return Response message number */ public function getMessageNumber(string $id): Response { - return $this->response("imap_msgno")->wrap(function($response)use($id){ + return $this->response("imap_msgno")->wrap(function($response) use ($id) { /** @var Response $response */ return \imap_msgno($this->stream, $id); }); @@ -399,7 +399,7 @@ public function getMessageNumber(string $id): Response { * @return Response */ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_fetch_overview")->wrap(function($response)use($sequence, $uid){ + return $this->response("imap_fetch_overview")->wrap(function($response) use ($sequence, $uid) { /** @var Response $response */ return \imap_fetch_overview($this->stream, $sequence, $uid ? IMAP::ST_UID : IMAP::NIL) ?: []; }); @@ -413,7 +413,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp * @return Response folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) */ public function folders(string $reference = '', string $folder = '*'): Response { - return $this->response("imap_getmailboxes")->wrap(function($response)use($reference, $folder){ + return $this->response("imap_getmailboxes")->wrap(function($response) use ($reference, $folder) { /** @var Response $response */ $result = []; @@ -447,7 +447,7 @@ public function folders(string $reference = '', string $folder = '*'): Response public function store(array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null): Response { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); - return $this->response()->wrap(function($response)use($mode, $from, $flag, $uid, $silent){ + return $this->response()->wrap(function($response) use ($mode, $from, $flag, $uid, $silent) { /** @var Response $response */ if ($mode == "+") { @@ -459,9 +459,9 @@ public function store(array|string $flags, int $from, int $to = null, string $mo } if ($silent === true) { - if ($status){ + if ($status) { return [ - "TAG".$response->Noun()." OK Store completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Store completed (0.001 + 0.000 secs).\r\n" ]; } return []; @@ -481,21 +481,20 @@ public function store(array|string $flags, int $from, int $to = null, string $mo * @return Response */ public function appendMessage(string $folder, string $message, array $flags = null, mixed $date = null): Response { - return $this->response("imap_append")->wrap(function($response)use($folder, $message, $flags, $date){ + return $this->response("imap_append")->wrap(function($response) use ($folder, $message, $flags, $date) { /** @var Response $response */ - if ($date != null) { if ($date instanceof \Carbon\Carbon) { $date = $date->format('d-M-Y H:i:s O'); } - if(\imap_append($this->stream, $folder, $message, $flags, $date)) { + if (\imap_append($this->stream, $this->getAddress() . $folder, $message, $flags, $date)) { return [ - "TAG".$response->Noun()." OK Append completed (0.001 + 0.000 secs).\r\n" + "OK Append completed (0.001 + 0.000 secs).\r\n" ]; } - } else if (\imap_append($this->stream, $folder, $message, $flags)){ + } else if (\imap_append($this->stream, $this->getAddress() . $folder, $message, $flags)) { return [ - "TAG".$response->Noun()." OK Append completed (0.001 + 0.000 secs).\r\n" + "OK Append completed (0.001 + 0.000 secs).\r\n" ]; } return []; @@ -513,12 +512,12 @@ public function appendMessage(string $folder, string $message, array $flags = nu * @return Response */ public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_mail_copy")->wrap(function($response)use($from, $folder, $uid){ + return $this->response("imap_mail_copy")->wrap(function($response) use ($from, $folder, $uid) { /** @var Response $response */ - if (\imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { + if (\imap_mail_copy($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { return [ - "TAG".$response->Noun()." OK Copy completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Copy completed (0.001 + 0.000 secs).\r\n" ]; } throw new ImapBadRequestException("Invalid ID $from"); @@ -534,20 +533,20 @@ public function copyMessage(string $folder, $from, int $to = null, int|string $u * @return Response Tokens if operation successful, false if an error occurred */ public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($messages, $folder, $uid){ + return $this->response()->wrap(function($response) use ($messages, $folder, $uid) { /** @var Response $response */ foreach ($messages as $msg) { $copy_response = $this->copyMessage($folder, $msg, null, $uid); $response->stack($copy_response); if (empty($copy_response->data())) { return [ - "TAG".$response->Noun()." BAD Copy failed (0.001 + 0.000 secs).\r\n", + "TAG" . $response->Noun() . " BAD Copy failed (0.001 + 0.000 secs).\r\n", "Invalid ID $msg\r\n" ]; } } return [ - "TAG".$response->Noun()." OK Copy completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Copy completed (0.001 + 0.000 secs).\r\n" ]; }); } @@ -563,10 +562,10 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * @return Response success */ public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_mail_move")->wrap(function($response)use($from, $folder, $uid){ - if (\imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { + return $this->response("imap_mail_move")->wrap(function($response) use ($from, $folder, $uid) { + if (\imap_mail_move($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { return [ - "TAG".$response->Noun()." OK Move completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Move completed (0.001 + 0.000 secs).\r\n" ]; } throw new ImapBadRequestException("Invalid ID $from"); @@ -583,19 +582,19 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u * @throws ImapBadRequestException */ public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($messages, $folder, $uid){ + return $this->response()->wrap(function($response) use ($messages, $folder, $uid) { foreach ($messages as $msg) { $move_response = $this->moveMessage($folder, $msg, null, $uid); $response = $response->include($response); if (empty($move_response->data())) { return [ - "TAG".$response->Noun()." BAD Move failed (0.001 + 0.000 secs).\r\n", - "Invalid ID $msg\r\n" - ]; + "TAG" . $response->Noun() . " BAD Move failed (0.001 + 0.000 secs).\r\n", + "Invalid ID $msg\r\n" + ]; } } return [ - "TAG".$response->Noun()." OK Move completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Move completed (0.001 + 0.000 secs).\r\n" ]; }); } @@ -620,9 +619,9 @@ public function ID($ids = null): Response { * @return Response */ public function createFolder(string $folder): Response { - return $this->response("imap_createmailbox")->wrap(function($response)use($folder){ - return \imap_createmailbox($this->stream, $folder) ? [ - 0 => "TAG".$response->Noun()." OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", + return $this->response("imap_createmailbox")->wrap(function($response) use ($folder) { + return \imap_createmailbox($this->stream, $this->getAddress() . $folder) ? [ + 0 => "TAG" . $response->Noun() . " OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", ] : []; }); } @@ -635,9 +634,9 @@ public function createFolder(string $folder): Response { * @return Response */ public function renameFolder(string $old, string $new): Response { - return $this->response("imap_renamemailbox")->wrap(function($response)use($old, $new){ - return \imap_renamemailbox($this->stream, $old, $new) ? [ - 0 => "TAG".$response->Noun()." OK Move completed (0.004 + 0.000 + 0.003 secs).\r\n", + return $this->response("imap_renamemailbox")->wrap(function($response) use ($old, $new) { + return \imap_renamemailbox($this->stream, $this->getAddress() . $old, $this->getAddress() . $new) ? [ + 0 => "TAG" . $response->Noun() . " OK Move completed (0.004 + 0.000 + 0.003 secs).\r\n", ] : []; }); } @@ -649,9 +648,9 @@ public function renameFolder(string $old, string $new): Response { * @return Response */ public function deleteFolder(string $folder): Response { - return $this->response("imap_deletemailbox")->wrap(function($response)use($folder){ - return \imap_deletemailbox($this->stream, $folder) ? [ - 0 => "TAG".$response->Noun()." OK Delete completed (0.004 + 0.000 + 0.003 secs).\r\n", + return $this->response("imap_deletemailbox")->wrap(function($response) use ($folder) { + return \imap_deletemailbox($this->stream, $this->getAddress() . $folder) ? [ + 0 => "OK Delete completed (0.004 + 0.000 + 0.003 secs).\r\n", ] : []; }); } @@ -682,9 +681,9 @@ public function unsubscribeFolder(string $folder): Response { * @return Response */ public function expunge(): Response { - return $this->response("imap_expunge")->wrap(function($response){ + return $this->response("imap_expunge")->wrap(function($response) { return \imap_expunge($this->stream) ? [ - 0 => "TAG".$response->Noun()." OK Expunge completed (0.001 + 0.000 secs).\r\n", + 0 => "TAG" . $response->Noun() . " OK Expunge completed (0.001 + 0.000 secs).\r\n", ] : []; }); } @@ -724,7 +723,7 @@ public function done() { * @return Response message ids */ public function search(array $params, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_search")->wrap(function($response)use($params, $uid){ + return $this->response("imap_search")->wrap(function($response) use ($params, $uid) { $response->setCanBeEmpty(true); $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); return $result ?: []; @@ -772,7 +771,7 @@ public function getProtocol(): string { * @return Response */ public function getQuota($username): Response { - return $this->response("imap_get_quota")->wrap(function($response)use($username){ + return $this->response("imap_get_quota")->wrap(function($response) use ($username) { $result = \imap_get_quota($this->stream, 'user.' . $username); return $result ?: []; }); @@ -785,8 +784,8 @@ public function getQuota($username): Response { * @return Response */ public function getQuotaRoot(string $quota_root = 'INBOX'): Response { - return $this->response("imap_get_quotaroot")->wrap(function($response)use($quota_root){ - $result = \imap_get_quotaroot($this->stream, $quota_root); + return $this->response("imap_get_quotaroot")->wrap(function($response) use ($quota_root) { + $result = \imap_get_quotaroot($this->stream, $this->getAddress() . $quota_root); return $result ?: []; }); } diff --git a/tests/live/LegacyTest.php b/tests/live/LegacyTest.php new file mode 100644 index 00000000..85fa4448 --- /dev/null +++ b/tests/live/LegacyTest.php @@ -0,0 +1,475 @@ +markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test."); + } + + parent::__construct($name, $data, $dataName); + $manager = new ClientManager([ + 'options' => [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'legacy' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'legacy-imap', + ], + ], + ]); + self::$client = $manager->account('legacy'); + self::$client->connect(); + self::assertInstanceOf(Client::class, self::$client->connect()); + } + + /** + * @throws RuntimeException + * @throws MessageFlagException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws AuthFailedException + * @throws MessageHeaderFetchingException + */ + public function testSizes(): void { + + $delimiter = ClientManager::get("options.delimiter"); + $child_path = implode($delimiter, ['INBOX', 'test']); + if (self::$client->getFolder($child_path) === null) { + self::$client->createFolder($child_path, false); + } + $folder = $this->getFolder($child_path); + + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals(214, $message->size); + self::assertEquals(214, self::$client->getConnection()->sizes($message->uid)->array()[$message->uid]); + } + + /** + * Try to create a new query instance + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQuery(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Get a folder + * @param string $folder_path + * + * @return Folder + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws FolderFetchingException + */ + final protected function getFolder(string $folder_path = "INDEX"): Folder { + $folder = self::$client->getFolderByPath($folder_path); + self::assertInstanceOf(Folder::class, $folder); + + return $folder; + } + + + /** + * Append a message to a folder + * @param Folder $folder + * @param string $message + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessage(Folder $folder, string $message): Message { + $status = $folder->select(); + if (!isset($status['uidnext'])) { + $this->fail("No UIDNEXT returned"); + } + + $response = $folder->appendMessage($message); + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to append message: ".implode("\n", $response)); + } + + $message = $folder->messages()->getMessageByUid($status['uidnext']); + self::assertInstanceOf(Message::class, $message); + + return $message; + } + + /** + * Append a message template to a folder + * @param Folder $folder + * @param string $template + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessageTemplate(Folder $folder, string $template): Message { + $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template])); + return $this->appendMessage($folder, $content); + } + + /** + * Delete a folder if it is given + * @param Folder|null $folder + * + * @return bool + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function deleteFolder(Folder $folder = null): bool { + $response = $folder?->delete(false); + if (is_array($response)) { + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to delete mailbox: ".implode("\n", $response)); + } + return $valid_response; + } + return false; + } + + /** + * Try to create a new query instance with a where clause + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + * @throws InvalidWhereQueryCriteriaException + * @throws MessageSearchValidationException + */ + public function testQueryWhere(): void { + $delimiter = ClientManager::get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'search']); + + $folder = self::$client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = self::$client->createFolder($folder_path, false); + + $messages = [ + $this->appendMessageTemplate($folder, '1366671050@github.com.eml'), + $this->appendMessageTemplate($folder, 'attachment_encoded_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_long_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_no_disposition.eml'), + $this->appendMessageTemplate($folder, 'bcc.eml'), + $this->appendMessageTemplate($folder, 'boolean_decoded_content.eml'), + $this->appendMessageTemplate($folder, 'email_address.eml'), + $this->appendMessageTemplate($folder, 'embedded_email.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition-embedded.eml'), + $this->appendMessageTemplate($folder, 'example_attachment.eml'), + $this->appendMessageTemplate($folder, 'example_bounce.eml'), + $this->appendMessageTemplate($folder, 'four_nested_emails.eml'), + $this->appendMessageTemplate($folder, 'gbk_charset.eml'), + $this->appendMessageTemplate($folder, 'html_only.eml'), + $this->appendMessageTemplate($folder, 'imap_mime_header_decode_returns_false.eml'), + $this->appendMessageTemplate($folder, 'inline_attachment.eml'), + $this->appendMessageTemplate($folder, 'issue-275.eml'), + $this->appendMessageTemplate($folder, 'issue-275-2.eml'), + $this->appendMessageTemplate($folder, 'issue-348.eml'), + $this->appendMessageTemplate($folder, 'ks_c_5601-1987_headers.eml'), + $this->appendMessageTemplate($folder, 'mail_that_is_attachment.eml'), + $this->appendMessageTemplate($folder, 'missing_date.eml'), + $this->appendMessageTemplate($folder, 'missing_from.eml'), + $this->appendMessageTemplate($folder, 'mixed_filename.eml'), + $this->appendMessageTemplate($folder, 'multipart_without_body.eml'), + $this->appendMessageTemplate($folder, 'multiple_html_parts_and_attachments.eml'), + $this->appendMessageTemplate($folder, 'multiple_nested_attachments.eml'), + $this->appendMessageTemplate($folder, 'nestes_embedded_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'null_content_charset.eml'), + $this->appendMessageTemplate($folder, 'pec.eml'), + $this->appendMessageTemplate($folder, 'plain.eml'), + $this->appendMessageTemplate($folder, 'plain_only.eml'), + $this->appendMessageTemplate($folder, 'plain_text_attachment.eml'), + $this->appendMessageTemplate($folder, 'references.eml'), + $this->appendMessageTemplate($folder, 'simple_multipart.eml'), + $this->appendMessageTemplate($folder, 'structured_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'thread_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_re_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_unrelated.eml'), + $this->appendMessageTemplate($folder, 'undefined_charset_header.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_minus.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_space.eml'), + $this->appendMessageTemplate($folder, 'unknown_encoding.eml'), + $this->appendMessageTemplate($folder, 'without_charset_plain_only.eml'), + $this->appendMessageTemplate($folder, 'without_charset_simple_multipart.eml'), + ]; + + $folder->getClient()->expunge(); + + $query = $folder->query()->all(); + self::assertEquals(count($messages), $query->count()); + + $query = $folder->query()->whereSubject("test"); + self::assertEquals(11, $query->count()); + + $query = $folder->query()->whereOn(Carbon::now()); + self::assertEquals(count($messages), $query->count()); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test query where criteria + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQueryWhereCriteria(): void { + self::$client->reconnect(); + + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertWhereSearchCriteria($folder, 'SUBJECT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'BODY', 'Test'); + $this->assertWhereSearchCriteria($folder, 'TEXT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'KEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'UNKEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'FLAGGED', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'UNFLAGGED', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'Message-ID', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'In-Reply-To', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'BCC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'CC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'FROM', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'TO', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'UID', '1'); + $this->assertWhereSearchCriteria($folder, 'UID', '1,2'); + $this->assertWhereSearchCriteria($folder, 'ALL'); + $this->assertWhereSearchCriteria($folder, 'NEW'); + $this->assertWhereSearchCriteria($folder, 'OLD'); + $this->assertWhereSearchCriteria($folder, 'SEEN'); + $this->assertWhereSearchCriteria($folder, 'UNSEEN'); + $this->assertWhereSearchCriteria($folder, 'RECENT'); + $this->assertWhereSearchCriteria($folder, 'ANSWERED'); + $this->assertWhereSearchCriteria($folder, 'UNANSWERED'); + $this->assertWhereSearchCriteria($folder, 'DELETED'); + $this->assertWhereSearchCriteria($folder, 'UNDELETED'); + $this->assertHeaderSearchCriteria($folder, 'Content-Language','en_US'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag NO'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag YES'); + $this->assertWhereSearchCriteria($folder, 'NOT'); + $this->assertWhereSearchCriteria($folder, 'OR'); + $this->assertWhereSearchCriteria($folder, 'AND'); + $this->assertWhereSearchCriteria($folder, 'BEFORE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'BEFORE', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'ON', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'ON', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'SINCE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'SINCE', Carbon::now()->subDays(1), true); + } + + /** + * Assert where search criteria + * @param Folder $folder + * @param string $criteria + * @param string|Carbon|null $value + * @param bool $date + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + $query = $folder->query()->where($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + $criteria = str_replace("CUSTOM ", "", $criteria); + $expected = $value === null ? [$criteria] : [$criteria, $value]; + if ($date === true && $value instanceof Carbon) { + $date_format = ClientManager::get('date_format', 'd M y'); + $expected[1] = $value->format($date_format); + } + + self::assertIsArray($item); + self::assertIsString($item[0]); + if($value !== null) { + self::assertCount(2, $item); + self::assertIsString($item[1]); + }else{ + self::assertCount(1, $item); + } + self::assertSame($expected, $item); + } + + /** + * Assert header search criteria + * @param Folder $folder + * @param string $criteria + * @param mixed|null $value + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertHeaderSearchCriteria(Folder $folder, string $criteria, mixed $value = null): void { + $query = $folder->query()->whereHeader($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + + self::assertIsArray($item); + self::assertIsString($item[0]); + self::assertCount(1, $item); + self::assertSame(['HEADER '.$criteria.' '.$value], $item); + } +} \ No newline at end of file From 9d6a2d5190555af1bff122cb86e64bfc92569319 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:05:27 +0200 Subject: [PATCH 505/600] Header value decoding improved #410 --- CHANGELOG.md | 1 + src/Header.php | 14 +++++----- .../ImapMimeHeaderDecodeReturnsFalseTest.php | 2 +- tests/issues/Issue410Test.php | 27 +++++++++++++++++++ tests/messages/issue-410.eml | 9 +++++++ 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 tests/issues/Issue410Test.php create mode 100644 tests/messages/issue-410.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index f451f40c..a1a0f0b1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Legacy protocol support fixed (object to array conversion) #411 +- Header value decoding improved #410 ### Added - NaN diff --git a/src/Header.php b/src/Header.php index 4bd4390d..e9ff162b 100644 --- a/src/Header.php +++ b/src/Header.php @@ -421,7 +421,7 @@ public function decode(mixed $value): mixed { $decoder = $this->config['decoder']['message']; if ($value !== null) { - if ($decoder === 'utf-8' && extension_loaded('imap')) { + if ($decoder === 'utf-8') { $decoded_values = $this->mime_header_decode($value); $tempValue = ""; foreach ($decoded_values as $decoded_value) { @@ -429,14 +429,16 @@ public function decode(mixed $value): mixed { } if ($tempValue) { $value = $tempValue; - } else { + } else if (extension_loaded('imap')) { $value = \imap_utf8($value); + }else if (function_exists('iconv_mime_decode')){ + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + }else{ + $value = mb_decode_mimeheader($value); } - } elseif ($decoder === 'iconv' && $this->is_uft8($value)) { + }elseif ($decoder === 'iconv') { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - } - - if ($this->is_uft8($value)) { + }else if ($this->is_uft8($value)) { $value = mb_decode_mimeheader($value); } diff --git a/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php index 6f2629ff..7f90a4ec 100644 --- a/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php +++ b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php @@ -27,7 +27,7 @@ class ImapMimeHeaderDecodeReturnsFalseTest extends FixtureTestCase { public function testFixture() : void { $message = $this->getFixture("imap_mime_header_decode_returns_false.eml"); - self::assertEquals("?p?#]ݰ?[??W̌ N? ?LL?̍L??NL˜", $message->subject->first()); + self::assertEquals("=?UTF-8?B?nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?=", $message->subject->first()); self::assertEquals("Hi", $message->getTextBody()); self::assertFalse($message->hasHTMLBody()); self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php new file mode 100644 index 00000000..7c3e6dc0 --- /dev/null +++ b/tests/issues/Issue410Test.php @@ -0,0 +1,27 @@ +subject); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-410.eml b/tests/messages/issue-410.eml new file mode 100644 index 00000000..05ddeff7 --- /dev/null +++ b/tests/messages/issue-410.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: =?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file From a4fe003f1135b61edef0141b5d2668439f361bff Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:13:39 +0200 Subject: [PATCH 506/600] issue #413 test case added --- tests/issues/Issue413Test.php | 28 ++++++++++++++++++++++++++++ tests/messages/issue-413.eml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/issues/Issue413Test.php create mode 100644 tests/messages/issue-413.eml diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php new file mode 100644 index 00000000..22cc881b --- /dev/null +++ b/tests/issues/Issue413Test.php @@ -0,0 +1,28 @@ +subject); + self::assertSame("This is just a test, so ignore it (if you can!)\r\n\r\nTony Marston", $message->getTextBody()); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-413.eml b/tests/messages/issue-413.eml new file mode 100644 index 00000000..a01412b3 --- /dev/null +++ b/tests/messages/issue-413.eml @@ -0,0 +1,32 @@ +Return-Path: +Delivered-To: gmx@tonymarston.co.uk +Received: from ion.dnsprotect.com + by ion.dnsprotect.com with LMTP + id oPy8IzIke2Rr4gIAzEkvSQ + (envelope-from ) + for ; Sat, 03 Jun 2023 07:29:54 -0400 +Return-path: +Envelope-to: gmx@tonymarston.net +Delivery-date: Sat, 03 Jun 2023 07:29:54 -0400 +Received: from [::1] (port=48740 helo=ion.dnsprotect.com) + by ion.dnsprotect.com with esmtpa (Exim 4.96) + (envelope-from ) + id 1q5PSF-000nPQ-1F + for gmx@tonymarston.net; + Sat, 03 Jun 2023 07:29:54 -0400 +MIME-Version: 1.0 +Date: Sat, 03 Jun 2023 07:29:54 -0400 +From: radicore +To: gmx@tonymarston.net +Subject: Test Message +User-Agent: Roundcube Webmail/1.6.0 +Message-ID: +X-Sender: radicore@radicore.org +Content-Type: text/plain; charset=US-ASCII; + format=flowed +Content-Transfer-Encoding: 7bit +X-From-Rewrite: unmodified, already matched + +This is just a test, so ignore it (if you can!) + +Tony Marston \ No newline at end of file From df4f3c3d60a5fd6a2c0f952a2b1609dd2e253405 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:25:52 +0200 Subject: [PATCH 507/600] issue #412 test case added --- tests/issues/Issue412Test.php | 30 +++++ tests/messages/issue-412.eml | 216 ++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 tests/issues/Issue412Test.php create mode 100644 tests/messages/issue-412.eml diff --git a/tests/issues/Issue412Test.php b/tests/issues/Issue412Test.php new file mode 100644 index 00000000..5d105541 --- /dev/null +++ b/tests/issues/Issue412Test.php @@ -0,0 +1,30 @@ +subject); + self::assertSame("64254d63e92a36ee02c760676351e60a", md5($message->getTextBody())); + self::assertSame("2e4de288f6a1ed658548ed11fcdb1d79", md5($message->getHTMLBody())); + self::assertSame(0, $message->attachments()->count()); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-412.eml b/tests/messages/issue-412.eml new file mode 100644 index 00000000..f28b81a7 --- /dev/null +++ b/tests/messages/issue-412.eml @@ -0,0 +1,216 @@ +Return-Path: +Delivered-To: gmx@tonymarston.co.uk +Received: from ion.dnsprotect.com + by ion.dnsprotect.com with LMTP + id YFDRGeZ6d2SlCRUAzEkvSQ + (envelope-from ) + for ; Wed, 31 May 2023 12:50:46 -0400 +Return-path: +Envelope-to: gmx@tonymarston.co.uk +Delivery-date: Wed, 31 May 2023 12:50:46 -0400 +Received: from mail-vi1eur04olkn2050.outbound.protection.outlook.com ([40.92.75.50]:23637 helo=EUR04-VI1-obe.outbound.protection.outlook.com) + by ion.dnsprotect.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + (Exim 4.96) + (envelope-from ) + id 1q4P28-005pTm-0E + for gmx@tonymarston.co.uk; + Wed, 31 May 2023 12:50:46 -0400 +ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; + b=SYBG6BEOiWsmVfcgXUQJ0moS1SuG/IdTK6DIT4H3g7CQ+hbWIbItTxOhFzJiHP+q0uz+XzR1FzX2Daso+4iKotX7x2ViHIA0Hs65xUZVFtvflMsUrB+5RLlf3Pr7DiNKguQQtC+R2IBLvedc+IqElnMrTHcxLVS2gWl89MZx5Q0bXGWW/9wBVq6yvc6C69ynppcEdD0QZsoUQlp2DgzDpg8iG3y6dYGxFiTvLzw08nTQiCuqi8qQ+nmHyeeItIiAmyKynvyiL+kh4frcSDS67r6PU/CBxvto/nP3RCAxHuzJEGOpS7LJPqoJAlRSrUp2zpeEMpmDFJUE/Jo0K+EgcQ== +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=ISg5C/1AgVASEPkGp/cgowfc/y9qywGvXMTv5yjJffs=; + b=eVYGErUWMxOeFGLg2nPuB0E/ngKet0hEVcN8Ay4ujGFY4k7i+1hrmnOMD6XiaIk3gVrqPalsmDjmEHpS0zV3+fPPTSktlSvkLrUr5ViVI1kMVBbBQsowLD5x3FpX7fnP2q17WPQ2P6R8Ibudkdnei8uq7gZhc3CSDLv4PfNma45H0FmdaB40mF2dCYzj5hEzr6jmMliANHJjznuDEFEUH3CfS1/iIA9rzhBKPKtahipTNeYiLqvZpKo1fO/XkZ57T44fqHkocfCyEK3Y1rehWudmkU8a9eEZlU5nBC6xoGO3P5Q1XIUNEHFmx2HH7eO8IgGzq/vbLMhcvbc3Ysdb2A== +ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none; + dkim=none; arc=none +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com; + s=selector1; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; + bh=ISg5C/1AgVASEPkGp/cgowfc/y9qywGvXMTv5yjJffs=; + b=Qv71Bx+LsM9Lo0uen0vxQzWI3ju4Q095ZMkKPXDaKsd5Y8dA3QteMpAPhy3/mAdP1+EV8NviDBhamtTG4qMO+zEqu/pyRpBGZOtjyiJGKh7aFh2bbodOkJkiGqH3jPwYBnE7T1XAqDVFmvRpuqIkqBe9FXeCZKRrF/Na5Y+zuklH7ebuGQVzIK+xol6q7BDgb/oLul7Wa3r3Lw40cPW5leUgwxngRFMucUVVO5aJ4MWlk76CmcN8XqgwVcFaACY80HLWRqRZfM8n24/KzV9nKSZIQFCgJi2CiqnEWVRSZZtZ9SudJJv4S3C/gU4OYoiFKr7GkEQibyqE2QkGHCBA1g== +Received: from PAXP193MB2364.EURP193.PROD.OUTLOOK.COM (2603:10a6:102:22b::9) + by DB8P193MB0773.EURP193.PROD.OUTLOOK.COM (2603:10a6:10:15a::10) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6455.22; Wed, 31 May + 2023 16:50:03 +0000 +Received: from PAXP193MB2364.EURP193.PROD.OUTLOOK.COM + ([fe80::b962:59b:2d33:85c2]) by PAXP193MB2364.EURP193.PROD.OUTLOOK.COM + ([fe80::b962:59b:2d33:85c2%5]) with mapi id 15.20.6455.020; Wed, 31 May 2023 + 16:50:03 +0000 +From: Tony Marston +To: "gmx@tonymarston.co.uk" +Subject: RE: TEST MESSAGE +Thread-Topic: TEST MESSAGE +Thread-Index: AQHZjysmbQn8p2cYOEawNV6ywnFmN690oIyAgAAA5rw= +Date: Wed, 31 May 2023 16:50:03 +0000 +Message-ID: + +References: + + <944a7d16fcb607628f358cdd0f5ba8c0@tonymarston.co.uk> +In-Reply-To: <944a7d16fcb607628f358cdd0f5ba8c0@tonymarston.co.uk> +Accept-Language: en-GB, en-US +Content-Language: en-GB +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-ms-exchange-messagesentrepresentingtype: 1 +x-tmn: [ABOzTq1ytyWYuvBD5A4I78Eqna1hBLeM] +x-ms-publictraffictype: Email +x-ms-traffictypediagnostic: PAXP193MB2364:EE_|DB8P193MB0773:EE_ +x-ms-office365-filtering-correlation-id: 7fbd6771-9a0d-495d-c4d3-08db61f714a6 +x-microsoft-antispam: BCL:0; +x-microsoft-antispam-message-info: + fwWKs8Qs/JyQ+pSnCNC804PQ86JYn/R63U7P6WCeXLC/fVSGqcAnslOc2vbxC2cUeTPPfow1WAz3f73/7Uk+byKxiE6L48WkgL6BFKVkYCwPKcQ3ps6KyEdGL6uUvPqYKwf5gFgUiTt+fKDiR/GJljj6qmN51jd5lUBvLf1g59q3LryUC+lEy6iLQ8cjCivzmBcR+0C2+uaa/xsjJxokbIEQoMicjjUiVWFkxRFBDr6FO8kEQyzzs70pNivK6mcXpVeJSOiLQfYa/2Q5QDYhE8kznG1EYUzHBQD/sLp/maUgmpKj1b8ObeI13QXed1qih+CtdLYAmPs5GPaoz8aY7pmaxroKLjBAqfOAC2IeQ3grxdQ8eRXlrnZ29cQnvD9tDryUvE9nyQLinaM2Dft4MueHvBTL5+WOTNnVQB98zVjnop8iVkwNrBKzPYiox9ufs9XFXNl0+2fFn66647ET7y/DkBKcszTKYF5RRp5o59QAh8rUTsaKeGJkPkyowZd+i6R12NIavL+eOOgnKvziTTbNl1lGgP+3zTKbgbb6K+DxfbKNl1zaTNvonOHwzI3jfdkDRtg5BvZVKstyl9AfZqo6OZ7ii3JKgVquRVWAtEQ6J6tx1Va/nTVrn1478+Dj +x-ms-exchange-antispam-messagedata-chunkcount: 1 +x-ms-exchange-antispam-messagedata-0: + =?us-ascii?Q?EMaSeqWhMBslXR7KptI34/opub+lBzTReVt3ACHDu/w7NvVUPpdp4YhwogpL?= + =?us-ascii?Q?8V9o/Zj+8vv6zZ7YL0n2NpODi8fzrPScgRiZGtJmfS58OqfgW2yygp1BmawZ?= + =?us-ascii?Q?y99Omv39THENDmCAba4d8zYLikOc+HJUfhpWeb/L9yqut7T2QkPFJYoeN5vU?= + =?us-ascii?Q?LeJ1hoPlguhNnDwMtE3HTgAl0Hbesms0Rb7wGaTDsgAc7XOO+8XpqLamnU0m?= + =?us-ascii?Q?zPpkQXV8+V5dFU2HxgIcYPYSaTX1CLPCdNcCAr1uHPnN81Ntm9Jb7fpYs1oA?= + =?us-ascii?Q?tgzPMwt37Ks9eQucJWZ5LQnNmZl/kYtVnvkylqYYMWiAg3y2XtjVkW/Ut/V2?= + =?us-ascii?Q?6vMfyCB5fEGJrn/uCp+KwL1s7s/4B332Bn4zvwpYd5TioMSGf9Rdp157eAfg?= + =?us-ascii?Q?LiNlLjDFdRc5SSEkUEl1TeX0FOQLsaCsQQqC/hzb10boa49GuxpoKwRmwhAO?= + =?us-ascii?Q?LZ2+veS5Jr1qWngBGo4MTkEq7nD6vBRIXQmKiLMpJc+Gk3/PCADL+H0IRH0+?= + =?us-ascii?Q?4uHnvcVr6CrDDZ2BEwcaWOa/ct8yUI5G9SC5gFP53TaS2llCnYwHAX1PkMAo?= + =?us-ascii?Q?w2LHFHE5I5dtQQJHaWNGMJGYdPmb9dDrksggLWXN+IxsxvFcFNSK2GPaqOMJ?= + =?us-ascii?Q?H4ht1rpqHTlU0uzgjb19gKCCcBdZfIzv118RTjFYG+EX/rsHlNRei/OWdTBF?= + =?us-ascii?Q?xf5cdnap836rQmre5ZoubNsSw2qKSRJhmZH1pCHoCFLtreM1fk7kkVJfkUz5?= + =?us-ascii?Q?ApMa03dEfFvzVv5wvPdWBLiCqzI6z6z8fUmwg2XfvK9Nyxb1AOZoT7JUXnp2?= + =?us-ascii?Q?Me6dTKqGKsBx87Dtny+fANHqgOm+Eo/pBZqyXwN93udbltxPmtNJ84MJJjyx?= + =?us-ascii?Q?vbEiDnMVb4knBO+sBqlKAVUv1F9ZJA2oUTrOD7t1xx6nBmQTgoYN6zsi+dgw?= + =?us-ascii?Q?nPebsl1/6fUy73FWLUKkeA64PeSa00Zi/q53ylXmUZV4Pc+11blKdL8o+p32?= + =?us-ascii?Q?dTD0ndul0WvvpQf8RdYNtGJ/BMurqNfvHq9wJo7Iu4fgTElR50ngwEsr28Bc?= + =?us-ascii?Q?G81pAb2fNhID6ewyOGfj87kqybxUhv1E+4pquh770UagjD1J3rKAUiw1sxWp?= + =?us-ascii?Q?u+FSmd7HKgd2cKJsmMErnQelF3DNozw5d0qdNELXZNO03xlMiADVvhqEJuqc?= + =?us-ascii?Q?uArdsSo2hyApUaiB+dM4Fp+oeiGienEQl64NJ7QFxRb/h96J0iTL3Vp8+8Y?= + =?us-ascii?Q?=3D?= +Content-Type: multipart/alternative; + boundary="_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_" +MIME-Version: 1.0 +X-OriginatorOrg: sct-15-20-4755-11-msonline-outlook-80ceb.templateTenant +X-MS-Exchange-CrossTenant-AuthAs: Internal +X-MS-Exchange-CrossTenant-AuthSource: PAXP193MB2364.EURP193.PROD.OUTLOOK.COM +X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg: 00000000-0000-0000-0000-000000000000 +X-MS-Exchange-CrossTenant-Network-Message-Id: 7fbd6771-9a0d-495d-c4d3-08db61f714a6 +X-MS-Exchange-CrossTenant-originalarrivaltime: 31 May 2023 16:50:03.3982 + (UTC) +X-MS-Exchange-CrossTenant-fromentityheader: Hosted +X-MS-Exchange-CrossTenant-id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa +X-MS-Exchange-CrossTenant-rms-persistedconsumerorg: 00000000-0000-0000-0000-000000000000 +X-MS-Exchange-Transport-CrossTenantHeadersStamped: DB8P193MB0773 +X-Spam-Status: No, score=-1.6 +X-Spam-Score: -15 +X-Spam-Bar: - +X-Ham-Report: Spam detection software, running on the system "ion.dnsprotect.com", + has NOT identified this incoming email as spam. The original + message has been attached to this so you can view it or label + similar future email. If you have any questions, see + root\@localhost for details. + Content preview: Here is my reply to your reply. Tony Marston From: gmx@tonymarston.co.uk + Sent: 31 May 2023 17:46 To: Tony Marston + Subject: Re: TEST MESSAGE + Content analysis details: (-1.6 points, 8.0 required) + pts rule name description + ---- ---------------------- -------------------------------------------------- + -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1% + [score: 0.0005] + 0.0 FREEMAIL_FROM Sender email is commonly abused enduser mail + provider + [tonymarston[at]hotmail.com] + -0.0 SPF_PASS SPF: sender matches SPF record + 0.5 SUBJ_ALL_CAPS Subject is all capitals + -0.0 SPF_HELO_PASS SPF: HELO matches SPF record + 0.0 HTML_MESSAGE BODY: HTML included in message + -0.1 DKIM_VALID_AU Message has a valid DKIM or DK signature from + author's domain + 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily + valid + -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from + envelope-from domain + -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature + -0.0 T_SCC_BODY_TEXT_LINE No description available. +X-Spam-Flag: NO +X-From-Rewrite: unmodified, no actual sender determined from check mail permissions + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_ +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Here is my reply to your reply. + +Tony Marston + +From: gmx@tonymarston.co.uk +Sent: 31 May 2023 17:46 +To: Tony Marston +Subject: Re: TEST MESSAGE + +On 2023-05-25 13:06, Tony Marston wrote: +Here is my reply to your message +> Tony Marston + + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_ +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

Here is my reply to your reply.

+

 

+

Tony Marston

+

 

+
+

From: gmx@tonymarston.co.uk
+Sent: 31 May 2023 17:46
+To: Tony Marston
+Subject: Re: TEST MESSAGE

+
+

 

+

On 2023-05-25 13:06, Tony Marston wrote:
+Here is my reply to your message
+> Tony Marston

+

 

+
+ + + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_-- \ No newline at end of file From 50b4ea1e090f3718ff615d0ec5a1a0296f36c3b2 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:34:54 +0200 Subject: [PATCH 508/600] Protocol exception handling improved (bad response message added) #408 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a0f0b1..e50b541e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Legacy protocol support fixed (object to array conversion) #411 - Header value decoding improved #410 +- Protocol exception handling improved (bad response message added) #408 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 914347b6..b5d84b52 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -303,10 +303,10 @@ public function readResponse(Response $response, string $tag, bool $dontParse = if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(); + throw new ImapServerErrorException(implode("\n", $tokens)); } - throw new ImapBadRequestException(); + throw new ImapBadRequestException(implode("\n", $tokens)); } /** From 9f02c8417c638b3200a407c70a1bf219f4647515 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 22:18:54 +0200 Subject: [PATCH 509/600] Prevent fetching singular rfc partials from running indefinitely #407 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 15 +++--- tests/issues/Issue407Test.php | 57 +++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/issues/Issue407Test.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e50b541e..6e4dfd5e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Legacy protocol support fixed (object to array conversion) #411 - Header value decoding improved #410 - Protocol exception handling improved (bad response message added) #408 +- Prevent fetching singular rfc partials from running indefinitely #407 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b5d84b52..3270fa97 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -294,19 +294,22 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $lines[] = $tokens; } while (!$readAll); + $original = $tokens; if ($dontParse) { // First two chars are still needed for the response code $tokens = [substr($tokens, 0, 2)]; } + $original = is_array($original)?$original : [$original]; + // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(implode("\n", $tokens)); + throw new ImapServerErrorException(implode("\n", $original)); } - throw new ImapBadRequestException(implode("\n", $tokens)); + throw new ImapBadRequestException(implode("\n", $original)); } /** @@ -730,7 +733,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); + return $this->fetch(["$rfc.TEXT"], is_array($uids)?$uids:[$uids], null, $uid); } /** @@ -744,7 +747,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); + return $this->fetch(["$rfc.HEADER"], is_array($uids)?$uids:[$uids], null, $uid); } /** @@ -757,7 +760,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["FLAGS"], $uids, null, $uid); + return $this->fetch(["FLAGS"], is_array($uids)?$uids:[$uids], null, $uid); } /** @@ -770,7 +773,7 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @throws RuntimeException */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["RFC822.SIZE"], $uids, null, $uid); + return $this->fetch(["RFC822.SIZE"], is_array($uids)?$uids:[$uids], null, $uid); } /** diff --git a/tests/issues/Issue407Test.php b/tests/issues/Issue407Test.php new file mode 100644 index 00000000..4adcbb39 --- /dev/null +++ b/tests/issues/Issue407Test.php @@ -0,0 +1,57 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + $message->setFlag("Seen"); + + $flags = $this->getClient()->getConnection()->flags($message->uid, IMAP::ST_UID)->validatedData(); + + self::assertIsArray($flags); + self::assertSame(1, count($flags)); + self::assertSame("\\Seen", $flags[$message->uid][0]); + + $message->delete(); + } + +} \ No newline at end of file From f1d82d9f69cfd3bcf326467a966e214e05f07a74 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 22:51:09 +0200 Subject: [PATCH 510/600] Subject with colon ";" is truncated #401 --- CHANGELOG.md | 1 + src/Header.php | 2 +- tests/issues/Issue401Test.php | 27 +++++++++++++++++++++++++++ tests/messages/issue-401.eml | 9 +++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/issues/Issue401Test.php create mode 100644 tests/messages/issue-401.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4dfd5e..84a52d29 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Header value decoding improved #410 - Protocol exception handling improved (bad response message added) #408 - Prevent fetching singular rfc partials from running indefinitely #407 +- Subject with colon ";" is truncated #401 ### Added - NaN diff --git a/src/Header.php b/src/Header.php index e9ff162b..6a80412a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -612,7 +612,7 @@ private function extractHeaderExtensions(): void { $value = (string)$value; } // Only parse strings and don't parse any attributes like the user-agent - if (($key == "user_agent") === false) { + if (!in_array($key, ["user-agent", "subject"])) { if (($pos = strpos($value, ";")) !== false) { $original = substr($value, 0, $pos); $this->set($key, trim(rtrim($original))); diff --git a/tests/issues/Issue401Test.php b/tests/issues/Issue401Test.php new file mode 100644 index 00000000..e250a550 --- /dev/null +++ b/tests/issues/Issue401Test.php @@ -0,0 +1,27 @@ +subject); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-401.eml b/tests/messages/issue-401.eml new file mode 100644 index 00000000..386fdad8 --- /dev/null +++ b/tests/messages/issue-401.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: 1;00pm Client running few minutes late +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file From 8ff2782565cc6f115fce568c01fe4664b3a8ad19 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 23:04:02 +0200 Subject: [PATCH 511/600] Catching and handling iconv decoding exception #397 --- CHANGELOG.md | 1 + src/Message.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84a52d29..da9697a5 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Protocol exception handling improved (bad response message added) #408 - Prevent fetching singular rfc partials from running indefinitely #407 - Subject with colon ";" is truncated #401 +- Catching and handling iconv decoding exception #397 ### Added - NaN diff --git a/src/Message.php b/src/Message.php index 47562cb7..2a60c641 100755 --- a/src/Message.php +++ b/src/Message.php @@ -903,7 +903,11 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = } if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { - return @iconv($from, $to . '//IGNORE', $str); + try { + return iconv($from, $to.'//IGNORE', $str); + } catch (\Exception $e) { + return @iconv($from, $to, $str); + } } else { if (!$from) { return mb_convert_encoding($str, $to); From 4bbcb4ba0f1c0b89e9660300175e5b94b20e760f Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 00:38:16 +0200 Subject: [PATCH 512/600] Additional timestamp format added #392 --- CHANGELOG.md | 2 +- src/Header.php | 5 +++++ tests/fixtures/DateTemplateTest.php | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da9697a5..50233901 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Catching and handling iconv decoding exception #397 ### Added -- NaN +- Additional timestamp format added #392 (thanks @esk-ap) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index 6a80412a..c87a8d09 100644 --- a/src/Header.php +++ b/src/Header.php @@ -716,6 +716,11 @@ private function parseDate(object $header): void { array_splice($parts, -2); $date = implode(' ', $parts); break; + case preg_match('/([A-Z]{2,4}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: + $array = explode(',', $date); + array_shift($array); + $date = Carbon::createFromFormat("d M Y H:i:s O", trim(implode(',', $array))); + break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index 4bf8f114..aaa338aa 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -48,6 +48,7 @@ class DateTemplateTest extends FixtureTestCase { "14 Sep 2019 00:10:08 UT +0200" => "2019-09-14 00:10:08", "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", + "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", ]; /** From dc237efb208d8892b9b119a70bf4d87a01e7e77a Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 00:46:08 +0200 Subject: [PATCH 513/600] Test case for #382 added --- tests/issues/Issue382Test.php | 33 +++++++++++++++++++++++++++++++++ tests/messages/issue-382.eml | 9 +++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/issues/Issue382Test.php create mode 100644 tests/messages/issue-382.eml diff --git a/tests/issues/Issue382Test.php b/tests/issues/Issue382Test.php new file mode 100644 index 00000000..4e638827 --- /dev/null +++ b/tests/issues/Issue382Test.php @@ -0,0 +1,33 @@ +from->first(); + + self::assertSame("Mail Delivery System", $from->personal); + self::assertSame("MAILER-DAEMON", $from->mailbox); + self::assertSame("mta-09.someserver.com", $from->host); + self::assertSame("MAILER-DAEMON@mta-09.someserver.com", $from->mail); + self::assertSame("Mail Delivery System ", $from->full); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-382.eml b/tests/messages/issue-382.eml new file mode 100644 index 00000000..f34b89fc --- /dev/null +++ b/tests/messages/issue-382.eml @@ -0,0 +1,9 @@ +From: MAILER-DAEMON@mta-09.someserver.com (Mail Delivery System) +To: to@here.com +Subject: Test +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file From d8880dff606ea7451ca88b6e0539ff6bc3d59320 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 01:36:12 +0200 Subject: [PATCH 514/600] Additional timestamp formats added #198 --- CHANGELOG.md | 2 +- src/Header.php | 21 +++++++++++++++++++++ tests/fixtures/DateTemplateTest.php | 2 ++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50233901..8ebab514 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Catching and handling iconv decoding exception #397 ### Added -- Additional timestamp format added #392 (thanks @esk-ap) +- Additional timestamp formats added #198 #392 (thanks @esk-ap) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index c87a8d09..9c8ae046 100644 --- a/src/Header.php +++ b/src/Header.php @@ -728,6 +728,27 @@ private function parseDate(object $header): void { case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: $date = str_replace(',', '', $date); break; + // match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ) + case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+$/i', $date) > 0: + $dates = explode('/', $date); + $date = array_shift($dates); + $array = explode(',', $date); + array_shift($array); + $date = trim(implode(',', $array)); + $array = explode(' ', $date); + array_pop($array); + $date = trim(implode(' ', $array)); + $date = Carbon::createFromFormat("d M. Y H:i:s O", $date); + break; + // match case for: fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100 + case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: + $dates = explode('/', $date); + $date = array_shift($dates); + $array = explode(',', $date); + array_shift($array); + $date = trim(implode(',', $array)); + $date = Carbon::createFromFormat("d M. Y H:i:s O", $date); + break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index aaa338aa..79f42dfe 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -49,6 +49,8 @@ class DateTemplateTest extends FixtureTestCase { "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", + "fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100" => "2022-11-25 05:27:14", + "Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)" => "2022-02-15 05:52:44", ]; /** From 829a6e6fe84b6c713858d227c24a5d01a3ccefca Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 03:46:07 +0200 Subject: [PATCH 515/600] Changelog updated --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ebab514..3edc4efb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.4.0] - 2023-06-24 +### Fixed - Legacy protocol support fixed (object to array conversion) #411 - Header value decoding improved #410 - Protocol exception handling improved (bad response message added) #408 @@ -16,9 +27,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Additional timestamp formats added #198 #392 (thanks @esk-ap) -### Breaking changes -- NaN - ## [5.3.0] - Security patch - 2023-06-20 ### Fixed From 29245c8c99b58af71b7c5cc3e92714cd10690378 Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 25 Jun 2023 22:07:09 +0200 Subject: [PATCH 516/600] #413 live test added & sample readded --- tests/issues/Issue413Test.php | 54 ++++++++++++++++++++++++++++++++++- tests/messages/issue-413.eml | 2 +- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php index 22cc881b..f3bd2212 100644 --- a/tests/issues/Issue413Test.php +++ b/tests/issues/Issue413Test.php @@ -13,10 +13,62 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Tests\live\LiveMailboxTestCase; +use Webklex\PHPIMAP\Folder; use Webklex\PHPIMAP\Message; -class Issue413Test extends TestCase { +class Issue413Test extends LiveMailboxTestCase { + /** + * Live server test + * + * @return void + * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\FolderFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\GetMessagesFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException + * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws \Webklex\PHPIMAP\Exceptions\MaskNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException + * @throws \Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\MessageNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\ResponseException + * @throws \Webklex\PHPIMAP\Exceptions\RuntimeException + */ + public function testLiveIssueEmail() { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $_message = $this->appendMessageTemplate($folder, 'issue-413.eml'); + + $message = $folder->messages()->all()->get()->last(); + self::assertEquals($message->uid, $_message->uid); + + self::assertSame("Test Message", (string)$message->subject); + self::assertSame("This is just a test, so ignore it (if you can!)\r\n\r\nTony Marston", $message->getTextBody()); + + $message->delete(); + } + + /** + * Static parsing test + * + * @return void + * @throws \ReflectionException + * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException + * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws \Webklex\PHPIMAP\Exceptions\MaskNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\ResponseException + * @throws \Webklex\PHPIMAP\Exceptions\RuntimeException + */ public function testIssueEmail() { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-413.eml"]); $message = Message::fromFile($filename); diff --git a/tests/messages/issue-413.eml b/tests/messages/issue-413.eml index a01412b3..9f0cbfa2 100644 --- a/tests/messages/issue-413.eml +++ b/tests/messages/issue-413.eml @@ -29,4 +29,4 @@ X-From-Rewrite: unmodified, already matched This is just a test, so ignore it (if you can!) -Tony Marston \ No newline at end of file +Tony Marston From f8f17f69693d57138888fb0ca8fdc430ed21d018 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 27 Jun 2023 01:50:27 +0200 Subject: [PATCH 517/600] #413 live test adjusted to fit the provided example code --- tests/issues/Issue413Test.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php index f3bd2212..cfdae548 100644 --- a/tests/issues/Issue413Test.php +++ b/tests/issues/Issue413Test.php @@ -43,9 +43,10 @@ public function testLiveIssueEmail() { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); + /** @var Message $message */ $_message = $this->appendMessageTemplate($folder, 'issue-413.eml'); - $message = $folder->messages()->all()->get()->last(); + $message = $folder->messages()->getMessageByMsgn($_message->msgn); self::assertEquals($message->uid, $_message->uid); self::assertSame("Test Message", (string)$message->subject); From 510c4eaf1074140e92931d77285be890de02d1f0 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 27 Jun 2023 01:51:59 +0200 Subject: [PATCH 518/600] Test case extended to include attachment filename decoding #410 --- tests/issues/Issue410Test.php | 8 ++++++++ tests/messages/issue-410.eml | 15 +++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index 7c3e6dc0..068cb7df 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -22,6 +22,14 @@ public function testIssueEmail() { $message = Message::fromFile($filename); self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->filename); + self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); } } \ No newline at end of file diff --git a/tests/messages/issue-410.eml b/tests/messages/issue-410.eml index 05ddeff7..e5fe0c62 100644 --- a/tests/messages/issue-410.eml +++ b/tests/messages/issue-410.eml @@ -2,8 +2,15 @@ From: from@there.com To: to@here.com Subject: =?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?= Date: Wed, 13 Sep 2017 13:05:45 +0200 -Content-Type: text/plain; - charset="us-ascii" -Content-Transfer-Encoding: quoted-printable +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" -Hi \ No newline at end of file +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="=?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=" + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file From 5ee4990435ea92dcfb780c57d7b2a12fe8b8433f Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 27 Jun 2023 02:32:07 +0200 Subject: [PATCH 519/600] Error token length mismatch in readResponse #400 --- CHANGELOG.md | 2 +- src/Connection/Protocols/ImapProtocol.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edc4efb..74d573ab 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Error token length mismatch in `ImapProtocol::readResponse` #400 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 3270fa97..4d54579f 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -297,7 +297,7 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $original = $tokens; if ($dontParse) { // First two chars are still needed for the response code - $tokens = [substr($tokens, 0, 2)]; + $tokens = [trim(substr($tokens, 0, 3))]; } $original = is_array($original)?$original : [$original]; From 57e7d287ab4239295b50138eea2c2edeabc59539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Wed, 28 Jun 2023 03:42:20 +0200 Subject: [PATCH 520/600] add failing test (#421) --- tests/issues/Issue410Test.php | 16 ++++++++++++++++ tests/messages/issue-410b.eml | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/messages/issue-410b.eml diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index 068cb7df..b300ec7a 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -13,6 +13,7 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Message; class Issue410Test extends TestCase { @@ -32,4 +33,19 @@ public function testIssueEmail() { self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); } + public function testIssueEmailB() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410b.eml"]); + $message = Message::fromFile($filename); + + self::assertSame("386 - 400021804 - 19., Heiligenstädter Straße 80 - 0819306 - Anfrage Vergabevorschlag", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->filename); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); + } + } \ No newline at end of file diff --git a/tests/messages/issue-410b.eml b/tests/messages/issue-410b.eml new file mode 100644 index 00000000..b260a1e0 --- /dev/null +++ b/tests/messages/issue-410b.eml @@ -0,0 +1,22 @@ +From: from@there.com +To: to@here.com +Subject: =?iso-8859-1?Q?386_-_400021804_-_19.,_Heiligenst=E4dter_Stra=DFe_80_-_081?= + =?iso-8859-1?Q?9306_-_Anfrage_Vergabevorschlag?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="=?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?=" +Content-Description: =?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?= +Content-Disposition: attachment; + filename="=?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?="; size=11641; + creation-date="Mon, 10 Jan 2022 09:01:00 GMT"; + modification-date="Mon, 10 Jan 2022 09:01:00 GMT" +Content-Transfer-Encoding: base64 + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file From 25fa1a067c670010553799873bab75ad0b866155 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:46:21 +0200 Subject: [PATCH 521/600] Attachment name parsing fixed #410 #421 --- src/Attachment.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index fcbc54e6..85ea62fb 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -269,12 +269,10 @@ public function decodeName(?string $name): string { } $decoder = $this->config['decoder']['message']; - if($decoder === 'utf-8' && extension_loaded('imap')) { - $name = \imap_utf8($name); - } - if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { $name = $this->part->getHeader()->decode($name); + } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { + $name = \imap_utf8($name); } // check if $name is url encoded @@ -284,9 +282,7 @@ public function decodeName(?string $name): string { // sanitize $name // order of '..' is important - $name = str_replace(['\\', '/', chr(0), ':', '..'], '', $name); - - return $name; + return str_replace(['\\', '/', chr(0), ':', '..'], '', $name); } return ""; } From 9d0377e4cdf584ad68a1027b3a237dc79231339e Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:47:53 +0200 Subject: [PATCH 522/600] Attachment content hash added --- src/Attachment.php | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 85ea62fb..5a918b35 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -28,6 +28,7 @@ * @property string type * @property string content_type * @property string id + * @property string hash * @property string name * @property string description * @property string filename @@ -44,6 +45,8 @@ * @method string setContentType(string $content_type) * @method string getId() * @method string setId(string $id) + * @method string getHash() + * @method string setHash(string $hash) * @method string getSize() * @method string setSize(integer $size) * @method string getName() @@ -74,17 +77,18 @@ class Attachment { * @var array $attributes */ protected array $attributes = [ - 'content' => null, - 'type' => null, - 'part_number' => 0, + 'content' => null, + 'hash' => null, + 'type' => null, + 'part_number' => 0, 'content_type' => null, - 'id' => null, - 'name' => null, - 'filename' => null, - 'description' => null, - 'disposition' => null, - 'img_src' => null, - 'size' => null, + 'id' => null, + 'name' => null, + 'filename' => null, + 'description' => null, + 'disposition' => null, + 'img_src' => null, + 'size' => null, ]; /** @@ -203,10 +207,26 @@ protected function fetch(): void { $this->content_type = $this->part->content_type; $this->content = $this->oMessage->decodeString($content, $this->part->encoding); + // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, + // it is not guaranteed to be unique and collisions are possible. + // Some additional online resources: + // - https://en.wikipedia.org/wiki/Hash_collision + // - https://www.php.net/manual/en/function.hash.php + // - https://php.watch/articles/php-hash-benchmark + // Benchmark speeds: + // -xxh3 ~15.19(GB/s) (requires php-xxhash extension or >= php8.1) + // -crc32c ~14.12(GB/s) + // -sha256 ~0.25(GB/s) + // xxh3 would be nice to use, because of its extra speed and 32 instead of 8 bytes, but it is not compatible with + // php < 8.1. crc32c is the next fastest and is compatible with php >= 5.1. sha256 is the slowest, but is compatible + // with php >= 5.1 and is the most likely to be unique. crc32c is the best compromise between speed and uniqueness. + // Unique enough for our purposes, but not so slow that it could be a bottleneck. + $this->hash = hash("crc32c", $this->part->getHeader()->raw."\r\n\r\n".$this->part->content); + if (($id = $this->part->id) !== null) { $this->id = str_replace(['<', '>'], '', $id); - }else{ - $this->id = hash("sha256", uniqid((string) rand(10000, 99999), true)); + }else { + $this->id = $this->hash; } $this->size = $this->part->bytes; From 141dc105dc1272e1ce0cb0aa8fa13a3be46d2faa Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:48:36 +0200 Subject: [PATCH 523/600] Always parse the attachment description if it is available --- src/Attachment.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Attachment.php b/src/Attachment.php index 5a918b35..8c217186 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -236,6 +236,10 @@ protected function fetch(): void { $this->filename = $this->decodeName($filename); } + if (($description = $this->part->description) !== null) { + $this->description = $this->part->getHeader()->decode($description); + } + if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } @@ -248,7 +252,6 @@ protected function fetch(): void { if (!$this->name) { $this->name = $this->part->description; } - $this->description = $this->part->description; } else if (!$this->name) { $this->name = $this->part->subtype; } From b8484ba199315ca92580ebd0807e173ad80c7a4d Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:49:56 +0200 Subject: [PATCH 524/600] Additional Attachment name fallback added to prevent missing attachments --- src/Attachment.php | 11 ++++++----- src/Message.php | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 8c217186..899b4e69 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -243,9 +243,6 @@ protected function fetch(): void { if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } - if (!$this->name && $this->filename != "") { - $this->name = $this->filename; - } if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) { if ($this->part->ifdescription) { @@ -258,8 +255,12 @@ protected function fetch(): void { } $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); - if(!$this->filename){ - $this->filename = bin2hex(random_bytes(10)); + if (!$this->filename) { + $this->filename = $this->hash; + } + + if (!$this->name && $this->filename != "") { + $this->name = $this->filename; } } diff --git a/src/Message.php b/src/Message.php index 2a60c641..09a534f2 100755 --- a/src/Message.php +++ b/src/Message.php @@ -767,7 +767,7 @@ protected function addBody(string $subtype, string $content): void { protected function fetchAttachment(Part $part): void { $oAttachment = new Attachment($this, $part); - if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { + if ($oAttachment->getSize() > 0) { if ($oAttachment->getId() !== null && $this->attachments->offsetExists($oAttachment->getId())) { $this->attachments->put($oAttachment->getId(), $oAttachment); } else { From 8e69ee69fe0652de3b6da3c6f63f68719f3d7ded Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:50:22 +0200 Subject: [PATCH 525/600] Test case for #414 added --- tests/issues/Issue414Test.php | 43 +++++++++++++++++++++++++++++++++++ tests/messages/issue-414.eml | 27 ++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/issues/Issue414Test.php create mode 100644 tests/messages/issue-414.eml diff --git a/tests/issues/Issue414Test.php b/tests/issues/Issue414Test.php new file mode 100644 index 00000000..36a1d300 --- /dev/null +++ b/tests/issues/Issue414Test.php @@ -0,0 +1,43 @@ +subject); + + $attachments = $message->getAttachments(); + + self::assertSame(2, $attachments->count()); + + $attachment = $attachments->first(); + self::assertEmpty($attachment->description); + self::assertSame("exampleMyFile.txt", $attachment->filename); + self::assertSame("exampleMyFile.txt", $attachment->name); + self::assertSame("be62f7e6", $attachment->id); + + $attachment = $attachments->last(); + self::assertEmpty($attachment->description); + self::assertSame("phpfoo", $attachment->filename); + self::assertSame("phpfoo", $attachment->name); + self::assertSame("12e1d38b", $attachment->hash); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-414.eml b/tests/messages/issue-414.eml new file mode 100644 index 00000000..302e4492 --- /dev/null +++ b/tests/messages/issue-414.eml @@ -0,0 +1,27 @@ +From: from@there.com +To: to@here.com +Subject: Test +Date: Fri, 29 Sep 2017 10:55:23 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------5B1F217006A67C28E756A62E" + +This is a multi-part message in MIME format. + +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="../../example/MyFile.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="../../example/MyFile.txt" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="php://foo" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="php://foo" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E-- From abc9cb3367a3a7b76d31a94c880538942d6193dd Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:51:00 +0200 Subject: [PATCH 526/600] test updated and extended --- tests/fixtures/AttachmentLongFilenameTest.php | 4 ++-- tests/fixtures/AttachmentNoDispositionTest.php | 4 +++- .../EmbeddedEmailWithoutContentDispositionTest.php | 2 +- tests/fixtures/ExampleBounceTest.php | 8 ++++---- tests/fixtures/InlineAttachmentTest.php | 6 ++++-- tests/issues/Issue410Test.php | 1 + 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php index c9e67d15..650a2dcb 100644 --- a/tests/fixtures/AttachmentLongFilenameTest.php +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -52,7 +52,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); self::assertEquals('text', $attachment->type); self::assertEquals('txt', $attachment->getExtension()); @@ -65,7 +65,7 @@ public function testFixture() : void { $attachment = $attachments[2]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); self::assertEquals('text', $attachment->type); self::assertEquals("text/plain", $attachment->content_type); diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index 76b3c12b..aa1ef946 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -39,7 +39,8 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); + self::assertEquals('26ed3dd2', $attachment->filename); + self::assertEquals('26ed3dd2', $attachment->id); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('xls', $attachment->getExtension()); @@ -49,5 +50,6 @@ public function testFixture() : void { self::assertEquals(0, $attachment->part_number); self::assertNull($attachment->disposition); self::assertNotEmpty($attachment->id); + self::assertEmpty($attachment->content_id); } } \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 67ad1cb0..603a956d 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -61,7 +61,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('', $attachment->name); + self::assertEquals('a1abc19a', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index 20288206..d2f418a9 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -72,8 +72,8 @@ public function testFixture(): void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); - self::assertEquals("", $attachment->name); + self::assertEquals('c541a506', $attachment->filename); + self::assertEquals("c541a506", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/delivery-status", $attachment->content_type); @@ -85,8 +85,8 @@ public function testFixture(): void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); - self::assertEquals("", $attachment->name); + self::assertEquals('da786518', $attachment->filename); + self::assertEquals("da786518", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index a482f25b..edb380cd 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -44,9 +44,11 @@ public function testFixture() : void { self::assertCount(1, $attachments); $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('', $attachment->name); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); + self::assertEquals('d2913999', $attachment->name); + self::assertEquals('d2913999', $attachment->filename); + self::assertEquals('ii_15f0aad691bb745f', $attachment->id); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index b300ec7a..d02724ca 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -44,6 +44,7 @@ public function testIssueEmailB() { self::assertSame(1, $attachments->count()); $attachment = $attachments->first(); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->description); self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->filename); self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); } From 7d7b897ea5219c34d7972cb99e878893b0a2c5f1 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:51:18 +0200 Subject: [PATCH 527/600] file formatted --- src/Attachment.php | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 899b4e69..15b83f63 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -100,8 +100,8 @@ class Attachment { /** * Attachment constructor. - * @param Message $oMessage - * @param Part $part + * @param Message $oMessage + * @param Part $part */ public function __construct(Message $oMessage, Part $part) { $this->config = ClientManager::get('options'); @@ -112,13 +112,13 @@ public function __construct(Message $oMessage, Part $part) { if ($this->oMessage->getClient()) { $default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask(); - if($default_mask != null) { + if ($default_mask != null) { $this->mask = $default_mask; } - }else{ - $default_mask = ClientManager::getMask("attachment"); - if($default_mask != ""){ - $this->mask =$default_mask; + } else { + $default_mask = ClientManager::getMask("attachment"); + if ($default_mask != "") { + $this->mask = $default_mask; } } @@ -135,15 +135,15 @@ public function __construct(Message $oMessage, Part $part) { * @throws MethodNotFoundException */ public function __call(string $method, array $arguments) { - if(strtolower(substr($method, 0, 3)) === 'get') { + if (strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); - if(isset($this->attributes[$name])) { + if (isset($this->attributes[$name])) { return $this->attributes[$name]; } return null; - }elseif (strtolower(substr($method, 0, 3)) === 'set') { + } elseif (strtolower(substr($method, 0, 3)) === 'set') { $name = Str::snake(substr($method, 3)); $this->attributes[$name] = array_pop($arguments); @@ -151,7 +151,7 @@ public function __call(string $method, array $arguments) { return $this->attributes[$name]; } - throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported'); + throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported'); } /** @@ -174,7 +174,7 @@ public function __set($name, $value) { * @return mixed|null */ public function __get($name) { - if(isset($this->attributes[$name])) { + if (isset($this->attributes[$name])) { return $this->attributes[$name]; } @@ -274,7 +274,7 @@ protected function fetch(): void { public function save(string $path, ?string $filename = null): bool { $filename = $filename ? $this->decodeName($filename) : $this->filename; - return file_put_contents($path.DIRECTORY_SEPARATOR.$filename, $this->getContent()) !== false; + return file_put_contents($path . DIRECTORY_SEPARATOR . $filename, $this->getContent()) !== false; } /** @@ -335,7 +335,7 @@ public function getExtension(): ?string { } if ($extension === null) { $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; - if (class_exists($deprecated_guesser) !== false){ + if (class_exists($deprecated_guesser) !== false) { /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ $extension = $deprecated_guesser::getInstance()->guess($this->getMimeType()); } @@ -374,7 +374,7 @@ public function getMessage(): Message { * @return $this */ public function setMask($mask): Attachment { - if(class_exists($mask)){ + if (class_exists($mask)) { $this->mask = $mask; } @@ -399,10 +399,10 @@ public function getMask(): string { */ public function mask(string $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; - if(class_exists($mask)){ + if (class_exists($mask)) { return new $mask($this); } - throw new MaskNotFoundException("Unknown mask provided: ".$mask); + throw new MaskNotFoundException("Unknown mask provided: " . $mask); } } From d286ead5b83052ed7417578cd492d0ba7f237b72 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:51:39 +0200 Subject: [PATCH 528/600] changelog updated --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d573ab..56e5e211 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Error token length mismatch in `ImapProtocol::readResponse` #400 +- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) +- Additional Attachment name fallback added to prevent missing attachments +- Attachment id is now static (based on the raw part content) and now longer random +- Always parse the attachment description if it is available ### Added -- NaN +- Attachment content hash added ### Breaking changes - NaN From 3c23c8f66b772ce8597772816e068326559e7e4b Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:57:03 +0200 Subject: [PATCH 529/600] v5.5.0 release information added --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56e5e211..6e775710 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,18 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [5.5.0] - 2023-06-28 +### Fixed +- Error token length mismatch in `ImapProtocol::readResponse` #400 +- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) +- Additional Attachment name fallback added to prevent missing attachments +- Attachment id is now static (based on the raw part content) and now longer random +- Always parse the attachment description if it is available + +### Added +- Attachment content hash added + + ## [5.4.0] - 2023-06-24 ### Fixed - Legacy protocol support fixed (object to array conversion) #411 From cf32a9dc0213869f4ce2b7c86c863afbd01d806b Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Fri, 30 Jun 2023 06:20:26 -0400 Subject: [PATCH 530/600] ImapProtocol.php: fix documentation typo (#423) Fixes a copy/paste typo in the documentation. --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4d54579f..15859372 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -723,7 +723,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } /** - * Fetch message headers + * Fetch message body (without headers) * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use From 27d911ca2ec0a825a846909dce73d8decf4f4b4d Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Fri, 30 Jun 2023 08:50:57 -0400 Subject: [PATCH 531/600] ImapProtocol: Add STATUS command support. (#424) This library currently contains some functions that purport to get the status of a folder, but these are misleading because they issue the EXAMINE command, not the STATUS command. This library actually currently lacks support for the STATUS command, and this command may be necessary to get some information that the EXAMINE command will not provide. This adds support for STATUS at the protocol level and the folder level, and clarifies the difference in the documentation. --- src/Connection/Protocols/ImapProtocol.php | 19 +++++++++++++++++ src/Folder.php | 26 +++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 15859372..cece4303 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -536,6 +536,25 @@ public function getCapabilities(): Response { return $response->setResult($response->validatedData()[0]); } + /** + * Get an array of available STATUS items + * + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException + */ + public function getStatus(): Response { + $s = implode(" ", $properties); + $response = $this->requestAndResponse('STATUS', array($this->escapeString($folder), "(" . $s . ")")); + + if (!$response->getResponse()) return $response; + + return $response->setResult($response->validatedData()[0]); + } + /** * Examine and select have the same response. * @param string $command can be 'EXAMINE' or 'SELECT' diff --git a/src/Folder.php b/src/Folder.php index c9ca395d..8b3da4db 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -491,8 +491,30 @@ public function idle(callable $callback, int $timeout = 300): void { } } - /** - * Get folder status information + /** + * Get the standard folder status items from STATUS command + * + * @return array + * @throws ConnectionFailedException + * @throws ImapbadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException + */ + public function getFolderStatus(string $folder, array $properties): array { + $status = $client->getConnection()->getStatus($folder->full_name, array('MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'))->validatedData(); + $c = count($status[2]); + $s = array(); + for ($i = 0; $i < $c; $i++) { + $a = $i++; + $s[$status[2][$a]] = $status[2][$i]; + } + return $s; + } + + /** + * Get folder status information from the EXAMINE command * * @return array * @throws ConnectionFailedException From f71f6daa25edbec1e7e119bd193cdcd00464f197 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:21:04 +0200 Subject: [PATCH 532/600] Folder status retrieval fixed and missing methods added --- src/Connection/Protocols/ImapProtocol.php | 55 ++++++++++++------- src/Connection/Protocols/LegacyProtocol.php | 10 ++++ .../Protocols/ProtocolInterface.php | 11 ++++ src/Folder.php | 24 +------- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cece4303..4b93d6b7 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -536,25 +536,6 @@ public function getCapabilities(): Response { return $response->setResult($response->validatedData()[0]); } - /** - * Get an array of available STATUS items - * - * @return Response list of STATUS items - * - * @throws ImapBadRequestException - * @throws ImapServerErrorException - * @throws RuntimeException - * @throws ResponseException - */ - public function getStatus(): Response { - $s = implode(" ", $properties); - $response = $this->requestAndResponse('STATUS', array($this->escapeString($folder), "(" . $s . ")")); - - if (!$response->getResponse()) return $response; - - return $response->setResult($response->validatedData()[0]); - } - /** * Examine and select have the same response. * @param string $command can be 'EXAMINE' or 'SELECT' @@ -628,6 +609,42 @@ public function examineFolder(string $folder = 'INBOX'): Response { return $this->examineOrSelect('EXAMINE', $folder); } + /** + * Get the status of a given folder + * + * @param string $folder + * @param string[] $arguments + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { + $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)], false); + $data = $response->validatedData(); + + if (!isset($data[0]) || !isset($data[0][2])) { + throw new RuntimeException("folder status could not be fetched"); + } + + $result = []; + $key = null; + foreach($data[0][2] as $value) { + if ($key === null) { + $key = $value; + } else { + $result[$key] = (int)$value; + $key = null; + } + } + + $response->setResult($result); + + return $response; + } + /** * Fetch one or more items of one or more messages * @param array|string $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 10bc9d9f..cb6393e5 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -236,6 +236,16 @@ public function examineFolder(string $folder = 'INBOX'): Response { }); } + /** + * Get the status of a given folder + * + * @return Response list of STATUS items + * @throws MethodNotSupportedException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { + throw new MethodNotSupportedException(); + } + /** * Fetch message content * @param int|array $uids diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index c02d7800..0a0dbfad 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -117,6 +117,17 @@ public function selectFolder(string $folder = 'INBOX'): Response; */ public function examineFolder(string $folder = 'INBOX'): Response; + /** + * Get the status of a given folder + * + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response; + /** * Fetch message headers * @param int|array $uids diff --git a/src/Folder.php b/src/Folder.php index 8b3da4db..b3655869 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -491,28 +491,6 @@ public function idle(callable $callback, int $timeout = 300): void { } } - /** - * Get the standard folder status items from STATUS command - * - * @return array - * @throws ConnectionFailedException - * @throws ImapbadRequestException - * @throws ImapServerErrorException - * @throws RuntimeException - * @throws AuthFailedException - * @throws ResponseException - */ - public function getFolderStatus(string $folder, array $properties): array { - $status = $client->getConnection()->getStatus($folder->full_name, array('MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'))->validatedData(); - $c = count($status[2]); - $s = array(); - for ($i = 0; $i < $c; $i++) { - $a = $i++; - $s[$status[2][$a]] = $status[2][$i]; - } - return $s; - } - /** * Get folder status information from the EXAMINE command * @@ -525,7 +503,7 @@ public function getFolderStatus(string $folder, array $properties): array { * @throws ResponseException */ public function getStatus(): array { - return $this->examine(); + return $this->client->getConnection()->folderStatus($this->path, ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'])->validatedData(); } /** From be9bded99388f83eade00f1fd4508429d00e5cdc Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:21:38 +0200 Subject: [PATCH 533/600] folder status test added --- tests/live/FolderTest.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php index 78da7017..7e826452 100644 --- a/tests/live/FolderTest.php +++ b/tests/live/FolderTest.php @@ -347,6 +347,31 @@ public function testUnsubscribe(): void { self::assertTrue(str_starts_with($status[0], 'OK')); } + /** + * Test Folder::status() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testStatus(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $status = $folder->getStatus(); + self::assertEquals(0, $status['MESSAGES']); + self::assertEquals(0, $status['RECENT']); + self::assertEquals(0, $status['UNSEEN']); + self::assertGreaterThan(0, $status['UIDNEXT']); + self::assertGreaterThan(0, $status['UIDVALIDITY']); + } + /** * Test Folder::examine() * From b8ce7993b010cc74b9026363860b3e8ebe1d9580 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:21:51 +0200 Subject: [PATCH 534/600] file formatted --- src/Connection/Protocols/ImapProtocol.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4b93d6b7..0d13d417 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -114,7 +114,7 @@ protected function enableStartTls() { */ public function nextLine(Response $response): string { $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { + while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["", "\n"])) { $line .= $next_char; } if ($line === "" && ($next_char === false || $next_char === "")) { @@ -300,7 +300,7 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $tokens = [trim(substr($tokens, 0, 3))]; } - $original = is_array($original)?$original : [$original]; + $original = is_array($original) ? $original : [$original]; // last line has response code if ($tokens[0] == 'OK') { @@ -492,7 +492,7 @@ public function logout(): Response { if (!$this->stream) { $this->reset(); return new Response(0, $this->debug); - }elseif ($this->meta()["timed_out"]) { + } elseif ($this->meta()["timed_out"]) { $this->reset(); return new Response(0, $this->debug); } @@ -501,7 +501,8 @@ public function logout(): Response { try { $result = $this->requestAndResponse('LOGOUT', [], true); fclose($this->stream); - } catch (\Throwable) {} + } catch (\Throwable) { + } $this->reset(); @@ -769,7 +770,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["$rfc.TEXT"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -783,7 +784,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.HEADER"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["$rfc.HEADER"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -796,7 +797,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["FLAGS"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["FLAGS"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -809,7 +810,7 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @throws RuntimeException */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["RFC822.SIZE"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["RFC822.SIZE"], is_array($uids) ? $uids : [$uids], null, $uid); } /** From 0bd96d1e8639746d84c7e48482ca9a81d84e17b5 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:35:20 +0200 Subject: [PATCH 535/600] getStatus() deprecated --- src/Folder.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index b3655869..5bfe2f55 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -502,20 +502,39 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws AuthFailedException * @throws ResponseException */ - public function getStatus(): array { + public function status(): array { return $this->client->getConnection()->folderStatus($this->path, ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'])->validatedData(); } /** + * Get folder status information from the EXAMINE command + * + * @return array + * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException + * @throws ResponseException * @throws RuntimeException + * + * @deprecated Use Folder::status() instead + */ + public function getStatus(): array { + return $this->status(); + } + + /** + * Load folder status information from the EXAMINE command + * @return Folder * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ResponseException + * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->getStatus(); + $this->status = $this->examine(); return $this; } From b08cbebc7a8f2e8003819e606ef74d0ec44df874 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:36:21 +0200 Subject: [PATCH 536/600] switched to lower case letters to be inline with legacy and other command responses --- src/Connection/Protocols/ImapProtocol.php | 2 +- tests/live/FolderTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 0d13d417..1115b0e0 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -636,7 +636,7 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', if ($key === null) { $key = $value; } else { - $result[$key] = (int)$value; + $result[strtolower($key)] = (int)$value; $key = null; } } diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php index 7e826452..d9997d75 100644 --- a/tests/live/FolderTest.php +++ b/tests/live/FolderTest.php @@ -364,12 +364,12 @@ public function testStatus(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $status = $folder->getStatus(); - self::assertEquals(0, $status['MESSAGES']); - self::assertEquals(0, $status['RECENT']); - self::assertEquals(0, $status['UNSEEN']); - self::assertGreaterThan(0, $status['UIDNEXT']); - self::assertGreaterThan(0, $status['UIDVALIDITY']); + $status = $folder->status(); + self::assertEquals(0, $status['messages']); + self::assertEquals(0, $status['recent']); + self::assertEquals(0, $status['unseen']); + self::assertGreaterThan(0, $status['uidnext']); + self::assertGreaterThan(0, $status['uidvalidity']); } /** From 0966e53399f7c14880f7a3453ef28e6f8efe09b7 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:36:40 +0200 Subject: [PATCH 537/600] Changelog updated --- CHANGELOG.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e775710..79cf049f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Error token length mismatch in `ImapProtocol::readResponse` #400 -- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) -- Additional Attachment name fallback added to prevent missing attachments -- Attachment id is now static (based on the raw part content) and now longer random -- Always parse the attachment description if it is available +- NaN ### Added -- Attachment content hash added +- IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) ### Breaking changes -- NaN +- `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. ## [5.5.0] - 2023-06-28 From bb818faa8566b38ed1429d921cc6b56e4a00099b Mon Sep 17 00:00:00 2001 From: Mikhail Sazanov Date: Thu, 11 Apr 2024 16:55:06 +0300 Subject: [PATCH 538/600] Add attributes and special flags (#428) Signed-off-by: Mikhail Sazanov Co-authored-by: Mikhail Sazanov --- src/Folder.php | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index 5bfe2f55..58211e8c 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -121,6 +121,22 @@ class Folder { /** @var array */ public array $status; + /** @var array */ + public array $attributes = []; + + + const SPECIAL_ATTRIBUTES = [ + 'haschildren' => ['\haschildren'], + 'hasnochildren' => ['\hasnochildren'], + 'template' => ['\template', '\templates'], + 'inbox' => ['\inbox'], + 'sent' => ['\sent'], + 'drafts' => ['\draft', '\drafts'], + 'archive' => ['\archive', '\archives'], + 'trash' => ['\trash'], + 'junk' => ['\junk', '\spam'], + ]; + /** * Folder constructor. * @param Client $client @@ -235,7 +251,7 @@ public function getChildren(): FolderCollection { */ protected function decodeName($name): string|array|bool|null { $parts = []; - foreach (explode($this->delimiter, $name) as $item) { + foreach(explode($this->delimiter, $name) as $item) { $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); } @@ -264,6 +280,14 @@ protected function parseAttributes($attributes): void { $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); + + array_map(function($el) { + foreach(self::SPECIAL_ATTRIBUTES as $key => $attribute) { + if(in_array(strtolower($el), $attribute)){ + $this->attributes[] = $key; + } + } + }, $attributes); } /** @@ -284,7 +308,7 @@ protected function parseAttributes($attributes): void { public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData(); - if ($expunge) $this->client->expunge(); + if($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); $event = $this->getEvent("folder", "moved"); @@ -336,7 +360,7 @@ public function appendMessage(string $message, array $options = null, Carbon|str * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if ($internal_date instanceof Carbon) { + if($internal_date instanceof Carbon){ $internal_date = $internal_date->format('d-M-Y H:i:s O'); } @@ -377,11 +401,11 @@ public function rename(string $new_name, bool $expunge = true): array { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); - if ($this->client->getActiveFolder() == $this->path){ + if($this->client->getActiveFolder() == $this->path){ $this->client->setActiveFolder(null); } - if ($expunge) $this->client->expunge(); + if($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); $event::dispatch($this); @@ -437,7 +461,7 @@ public function unsubscribe(): array { public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); - if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { + if(!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())){ throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } @@ -450,15 +474,15 @@ public function idle(callable $callback, int $timeout = 300): void { $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - while (true) { + while(true) { // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand $line = $idle_client->getConnection()->nextLine(Response::empty()); - if (($pos = strpos($line, "EXISTS")) !== false) { + if(($pos = strpos($line, "EXISTS")) !== false){ $msgn = (int)substr($line, 2, $pos - 2); // Check if the stream is still alive or should be considered stale - if (!$this->client->isConnected() || $last_action->isBefore(Carbon::now())) { + if(!$this->client->isConnected() || $last_action->isBefore(Carbon::now())){ // Reset the connection before interacting with it. Otherwise, the resource might be stale which // would result in a stuck interaction. If you know of a way of detecting a stale resource, please // feel free to improve this logic. I tried a lot but nothing seem to work reliably... @@ -582,7 +606,7 @@ public function getClient(): Client { * @param $delimiter */ public function setDelimiter($delimiter): void { - if (in_array($delimiter, [null, '', ' ', false]) === true) { + if(in_array($delimiter, [null, '', ' ', false]) === true){ $delimiter = ClientManager::get('options.delimiter', '/'); } From 70a134254f20a3e608edd3a960de7e4de96441a0 Mon Sep 17 00:00:00 2001 From: ferrisbuellers Date: Thu, 11 Apr 2024 14:56:32 +0100 Subject: [PATCH 539/600] Fixed date issue if timezone is UT and a 2 digit year (#429) --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 9c8ae046..6eda3fea 100644 --- a/src/Header.php +++ b/src/Header.php @@ -722,7 +722,7 @@ private function parseDate(object $header): void { $date = Carbon::createFromFormat("d M Y H:i:s O", trim(implode(',', $array))); break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ ([0-9]{2}|[0-9]{4})\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: From ed1eb0410806afef149f2da951eb30cd2afb8a9c Mon Sep 17 00:00:00 2001 From: Adam Morrison Date: Thu, 11 Apr 2024 09:58:28 -0400 Subject: [PATCH 540/600] Make the space optional after a comma separator (#437) --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 6eda3fea..ce386b40 100644 --- a/src/Header.php +++ b/src/Header.php @@ -510,7 +510,7 @@ private function decodeAddresses($values): array { } foreach ($values as $address) { - foreach (preg_split('/, (?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { + foreach (preg_split('/, ?(?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { $split_address = trim(rtrim($split_address)); if (strpos($split_address, ",") == strlen($split_address) - 1) { From b916e4e4cb910f12b9fef29166d6afd6f56da183 Mon Sep 17 00:00:00 2001 From: Daniel Castilla Date: Thu, 11 Apr 2024 16:00:28 +0200 Subject: [PATCH 541/600] Update ImapProtocol.php (#449) --- src/Connection/Protocols/ImapProtocol.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 1115b0e0..e29d0470 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -90,6 +90,24 @@ public function connect(string $host, int $port = null): bool { return true; } + /** + * Check if the current session is connected + * + * @return bool + */ + public function connected(): bool { + if ((bool)$this->stream) { + try { + $this->requestAndResponse('NOOP'); + return true; + } + catch (ImapServerErrorException|RuntimeException) { + return false; + } + } + return false; + } + /** * Enable tls on the current connection * From fad09ad197dfe3880c65929db63a945e8ed602a4 Mon Sep 17 00:00:00 2001 From: Michal Kortas Date: Thu, 11 Apr 2024 16:02:13 +0200 Subject: [PATCH 542/600] Return recursively detected parts only if there are more than 1 of them. (#455) Co-authored-by: Michal Kortas --- src/Structure.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Structure.php b/src/Structure.php index 745a6234..00a44db2 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -112,7 +112,11 @@ private function parsePart(string $context, int $part_number = 0): array { $headers = new Header($headers); if (($boundary = $headers->getBoundary()) !== null) { - return $this->detectParts($boundary, $body, $part_number); + $parts = $this->detectParts($boundary, $body, $part_number); + + if(count($parts) > 1) { + return $parts; + } } return [new Part($body, $headers, $part_number)]; From e5e8eb0fb1fa08b81bf6996c3c0c35ab65418f9d Mon Sep 17 00:00:00 2001 From: Oliver Scase Date: Thu, 11 Apr 2024 16:03:31 +0200 Subject: [PATCH 543/600] Fix: improve return type hints and return docblocks for query classes (#470) --- src/Query/Query.php | 84 ++++++++++++------------ src/Query/WhereQuery.php | 138 +++++++++++++++++++-------------------- 2 files changed, 111 insertions(+), 111 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 727e641f..8ecdf09e 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -640,7 +640,7 @@ public function getByUidLowerThan(int $uid): MessageCollection { * * @return $this */ - public function leaveUnread(): Query { + public function leaveUnread(): static { $this->setFetchOptions(IMAP::FT_PEEK); return $this; @@ -651,7 +651,7 @@ public function leaveUnread(): Query { * * @return $this */ - public function markAsRead(): Query { + public function markAsRead(): static { $this->setFetchOptions(IMAP::FT_UID); return $this; @@ -663,7 +663,7 @@ public function markAsRead(): Query { * * @return $this */ - public function setSequence(int $sequence): Query { + public function setSequence(int $sequence): static { $this->sequence = $sequence; return $this; @@ -699,7 +699,7 @@ public function getClient(): Client { * * @return $this */ - public function limit(int $limit, int $page = 1): Query { + public function limit(int $limit, int $page = 1): static { if ($page >= 1) $this->page = $page; $this->limit = $limit; @@ -719,9 +719,9 @@ public function getQuery(): Collection { * Set all query parameters * @param array $query * - * @return Query + * @return $this */ - public function setQuery(array $query): Query { + public function setQuery(array $query): static { $this->query = new Collection($query); return $this; } @@ -739,9 +739,9 @@ public function getRawQuery(): string { * Set the raw query * @param string $raw_query * - * @return Query + * @return $this */ - public function setRawQuery(string $raw_query): Query { + public function setRawQuery(string $raw_query): static { $this->raw_query = $raw_query; return $this; } @@ -759,9 +759,9 @@ public function getExtensions(): array { * Set all extensions that should be used * @param string[] $extensions * - * @return Query + * @return $this */ - public function setExtensions(array $extensions): Query { + public function setExtensions(array $extensions): static { $this->extensions = $extensions; if (count($this->extensions) > 0) { if (in_array("UID", $this->extensions) === false) { @@ -775,9 +775,9 @@ public function setExtensions(array $extensions): Query { * Set the client instance * @param Client $client * - * @return Query + * @return $this */ - public function setClient(Client $client): Query { + public function setClient(Client $client): static { $this->client = $client; return $this; } @@ -795,9 +795,9 @@ public function getLimit(): ?int { * Set the fetch limit * @param int $limit * - * @return Query + * @return $this */ - public function setLimit(int $limit): Query { + public function setLimit(int $limit): static { $this->limit = $limit <= 0 ? null : $limit; return $this; } @@ -815,9 +815,9 @@ public function getPage(): int { * Set the page * @param int $page * - * @return Query + * @return $this */ - public function setPage(int $page): Query { + public function setPage(int $page): static { $this->page = $page; return $this; } @@ -826,9 +826,9 @@ public function setPage(int $page): Query { * Set the fetch option flag * @param int $fetch_options * - * @return Query + * @return $this */ - public function setFetchOptions(int $fetch_options): Query { + public function setFetchOptions(int $fetch_options): static { $this->fetch_options = $fetch_options; return $this; } @@ -837,9 +837,9 @@ public function setFetchOptions(int $fetch_options): Query { * Set the fetch option flag * @param int $fetch_options * - * @return Query + * @return $this */ - public function fetchOptions(int $fetch_options): Query { + public function fetchOptions(int $fetch_options): static { return $this->setFetchOptions($fetch_options); } @@ -865,9 +865,9 @@ public function getFetchBody(): bool { * Set the fetch body flag * @param boolean $fetch_body * - * @return Query + * @return $this */ - public function setFetchBody(bool $fetch_body): Query { + public function setFetchBody(bool $fetch_body): static { $this->fetch_body = $fetch_body; return $this; } @@ -876,9 +876,9 @@ public function setFetchBody(bool $fetch_body): Query { * Set the fetch body flag * @param boolean $fetch_body * - * @return Query + * @return $this */ - public function fetchBody(bool $fetch_body): Query { + public function fetchBody(bool $fetch_body): static { return $this->setFetchBody($fetch_body); } @@ -895,9 +895,9 @@ public function getFetchFlags(): bool { * Set the fetch flag * @param bool $fetch_flags * - * @return Query + * @return $this */ - public function setFetchFlags(bool $fetch_flags): Query { + public function setFetchFlags(bool $fetch_flags): static { $this->fetch_flags = $fetch_flags; return $this; } @@ -906,9 +906,9 @@ public function setFetchFlags(bool $fetch_flags): Query { * Set the fetch order * @param string $fetch_order * - * @return Query + * @return $this */ - public function setFetchOrder(string $fetch_order): Query { + public function setFetchOrder(string $fetch_order): static { $fetch_order = strtolower($fetch_order); if (in_array($fetch_order, ['asc', 'desc'])) { @@ -922,9 +922,9 @@ public function setFetchOrder(string $fetch_order): Query { * Set the fetch order * @param string $fetch_order * - * @return Query + * @return $this */ - public function fetchOrder(string $fetch_order): Query { + public function fetchOrder(string $fetch_order): static { return $this->setFetchOrder($fetch_order); } @@ -940,36 +940,36 @@ public function getFetchOrder(): string { /** * Set the fetch order to ascending * - * @return Query + * @return $this */ - public function setFetchOrderAsc(): Query { + public function setFetchOrderAsc(): static { return $this->setFetchOrder('asc'); } /** * Set the fetch order to ascending * - * @return Query + * @return $this */ - public function fetchOrderAsc(): Query { + public function fetchOrderAsc(): static { return $this->setFetchOrderAsc(); } /** * Set the fetch order to descending * - * @return Query + * @return $this */ - public function setFetchOrderDesc(): Query { + public function setFetchOrderDesc(): static { return $this->setFetchOrder('desc'); } /** * Set the fetch order to descending * - * @return Query + * @return $this */ - public function fetchOrderDesc(): Query { + public function fetchOrderDesc(): static { return $this->setFetchOrderDesc(); } @@ -977,9 +977,9 @@ public function fetchOrderDesc(): Query { * Set soft fail mode * @var boolean $state * - * @return Query + * @return $this */ - public function softFail(bool $state = true): Query { + public function softFail(bool $state = true): static { return $this->setSoftFail($state); } @@ -987,9 +987,9 @@ public function softFail(bool $state = true): Query { * Set soft fail mode * * @var boolean $state - * @return Query + * @return $this */ - public function setSoftFail(bool $state = true): Query { + public function setSoftFail(bool $state = true): static { $this->soft_fail = $state; return $this; diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index b218e545..74024ddc 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -132,7 +132,7 @@ protected function validate_criteria($criteria): string { * $query->where(["FROM" => "someone@email.tld", "SEEN"]); * $query->where("FROM", "someone@email.tld")->where("SEEN"); */ - public function where(mixed $criteria, mixed $value = null): WhereQuery { + public function where(mixed $criteria, mixed $value = null): static { if (is_array($criteria)) { foreach ($criteria as $key => $value) { if (is_numeric($key)) { @@ -171,7 +171,7 @@ protected function push_search_criteria(string $criteria, mixed $value){ * * @return $this */ - public function orWhere(Closure $closure = null): WhereQuery { + public function orWhere(Closure $closure = null): static { $this->query->push(['OR']); if ($closure !== null) $closure($this); @@ -183,7 +183,7 @@ public function orWhere(Closure $closure = null): WhereQuery { * * @return $this */ - public function andWhere(Closure $closure = null): WhereQuery { + public function andWhere(Closure $closure = null): static { $this->query->push(['AND']); if ($closure !== null) $closure($this); @@ -191,38 +191,38 @@ public function andWhere(Closure $closure = null): WhereQuery { } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereAll(): WhereQuery { + public function whereAll(): static { return $this->where('ALL'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereAnswered(): WhereQuery { + public function whereAnswered(): static { return $this->where('ANSWERED'); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereBcc(string $value): WhereQuery { + public function whereBcc(string $value): static { return $this->where('BCC', $value); } /** * @param mixed $value - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException * @throws MessageSearchValidationException */ - public function whereBefore(mixed $value): WhereQuery { + public function whereBefore(mixed $value): static { $date = $this->parse_date($value); return $this->where('BEFORE', $date); } @@ -230,121 +230,121 @@ public function whereBefore(mixed $value): WhereQuery { /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereBody(string $value): WhereQuery { + public function whereBody(string $value): static { return $this->where('BODY', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereCc(string $value): WhereQuery { + public function whereCc(string $value): static { return $this->where('CC', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereDeleted(): WhereQuery { + public function whereDeleted(): static { return $this->where('DELETED'); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereFlagged(string $value): WhereQuery { + public function whereFlagged(string $value): static { return $this->where('FLAGGED', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereFrom(string $value): WhereQuery { + public function whereFrom(string $value): static { return $this->where('FROM', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereKeyword(string $value): WhereQuery { + public function whereKeyword(string $value): static { return $this->where('KEYWORD', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNew(): WhereQuery { + public function whereNew(): static { return $this->where('NEW'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNot(): WhereQuery { + public function whereNot(): static { return $this->where('NOT'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereOld(): WhereQuery { + public function whereOld(): static { return $this->where('OLD'); } /** * @param mixed $value * - * @return WhereQuery + * @return $this * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereOn(mixed $value): WhereQuery { + public function whereOn(mixed $value): static { $date = $this->parse_date($value); return $this->where('ON', $date); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereRecent(): WhereQuery { + public function whereRecent(): static { return $this->where('RECENT'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereSeen(): WhereQuery { + public function whereSeen(): static { return $this->where('SEEN'); } /** * @param mixed $value * - * @return WhereQuery + * @return $this * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereSince(mixed $value): WhereQuery { + public function whereSince(mixed $value): static { $date = $this->parse_date($value); return $this->where('SINCE', $date); } @@ -352,88 +352,88 @@ public function whereSince(mixed $value): WhereQuery { /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereSubject(string $value): WhereQuery { + public function whereSubject(string $value): static { return $this->where('SUBJECT', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereText(string $value): WhereQuery { + public function whereText(string $value): static { return $this->where('TEXT', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereTo(string $value): WhereQuery { + public function whereTo(string $value): static { return $this->where('TO', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnkeyword(string $value): WhereQuery { + public function whereUnkeyword(string $value): static { return $this->where('UNKEYWORD', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnanswered(): WhereQuery { + public function whereUnanswered(): static { return $this->where('UNANSWERED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUndeleted(): WhereQuery { + public function whereUndeleted(): static { return $this->where('UNDELETED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnflagged(): WhereQuery { + public function whereUnflagged(): static { return $this->where('UNFLAGGED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnseen(): WhereQuery { + public function whereUnseen(): static { return $this->where('UNSEEN'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNoXSpam(): WhereQuery { + public function whereNoXSpam(): static { return $this->where("CUSTOM X-Spam-Flag NO"); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereIsXSpam(): WhereQuery { + public function whereIsXSpam(): static { return $this->where("CUSTOM X-Spam-Flag YES"); } @@ -442,10 +442,10 @@ public function whereIsXSpam(): WhereQuery { * @param $header * @param $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereHeader($header, $value): WhereQuery { + public function whereHeader($header, $value): static { return $this->where("CUSTOM HEADER $header $value"); } @@ -453,10 +453,10 @@ public function whereHeader($header, $value): WhereQuery { * Search for a specific message id * @param $messageId * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereMessageId($messageId): WhereQuery { + public function whereMessageId($messageId): static { return $this->whereHeader("Message-ID", $messageId); } @@ -464,20 +464,20 @@ public function whereMessageId($messageId): WhereQuery { * Search for a specific message id * @param $messageId * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereInReplyTo($messageId): WhereQuery { + public function whereInReplyTo($messageId): static { return $this->whereHeader("In-Reply-To", $messageId); } /** * @param $country_code * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereLanguage($country_code): WhereQuery { + public function whereLanguage($country_code): static { return $this->where("Content-Language $country_code"); } @@ -486,10 +486,10 @@ public function whereLanguage($country_code): WhereQuery { * * @param int|string $uid * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUid(int|string $uid): WhereQuery { + public function whereUid(int|string $uid): static { return $this->where('UID', $uid); } @@ -498,10 +498,10 @@ public function whereUid(int|string $uid): WhereQuery { * * @param array $uids * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUidIn(array $uids): WhereQuery { + public function whereUidIn(array $uids): static { $uids = implode(',', $uids); return $this->where('UID', $uids); } From 61e0f1a3ed76c23658ab977b65e9df2782065bdb Mon Sep 17 00:00:00 2001 From: NeekTheNook <75854740+NeekTheNook@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:10:35 +0100 Subject: [PATCH 544/600] Query - Chunked - Resolved infinite loop when start chunk > 1 (#477) Resolved an issue when the start chunks value was > 1 an infinite loop would be created as handled messages would always be less than available messages when chunks are skipped. Added more safety around input arguments, forcing minimums of 1 for both $chunk_size and $start_chunk. --- src/Query/Query.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 8ecdf09e..b0c9eddd 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -395,8 +395,14 @@ public function get(): MessageCollection { * @throws ResponseException */ public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1): void { + $start_chunk = max($start_chunk,1); + $chunk_size = max($chunk_size,1); + $skipped_messages_count = $chunk_size * ($start_chunk-1); + $available_messages = $this->search(); - if (($available_messages_count = $available_messages->count()) > 0) { + $available_messages_count = max($available_messages->count() - $skipped_messages_count,0); + + if ($available_messages_count > 0) { $old_limit = $this->limit; $old_page = $this->page; From 0d61de530e196eb28dea82def899729c7dee84e0 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:12:35 +0200 Subject: [PATCH 545/600] Config handling moved into a new class to allow class serialization --- src/Attachment.php | 75 ++++-- src/Client.php | 93 +++---- src/ClientManager.php | 200 ++------------- src/Config.php | 266 ++++++++++++++++++++ src/Connection/Protocols/ImapProtocol.php | 8 +- src/Connection/Protocols/LegacyProtocol.php | 9 +- src/Connection/Protocols/Protocol.php | 17 +- src/Folder.php | 6 +- src/Header.php | 56 ++++- src/Message.php | 99 +++++--- src/Part.php | 24 +- src/Query/Query.php | 18 +- src/Structure.php | 15 +- 13 files changed, 576 insertions(+), 310 deletions(-) create mode 100644 src/Config.php diff --git a/src/Attachment.php b/src/Attachment.php index 15b83f63..451dfe26 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -57,16 +57,23 @@ class Attachment { /** - * @var Message $oMessage + * @var Message $message */ - protected Message $oMessage; + protected Message $message; /** * Used config * - * @var array $config + * @var Config $config */ - protected array $config = []; + protected Config $config; + + /** + * Attachment options + * + * @var array $options + */ + protected array $options = []; /** @var Part $part */ protected Part $part; @@ -100,23 +107,24 @@ class Attachment { /** * Attachment constructor. - * @param Message $oMessage + * @param Message $message * @param Part $part */ - public function __construct(Message $oMessage, Part $part) { - $this->config = ClientManager::get('options'); + public function __construct(Message $message, Part $part) { + $this->message = $message; + $this->config = $this->message->getConfig(); + $this->options = $this->config->get('options'); - $this->oMessage = $oMessage; $this->part = $part; $this->part_number = $part->part_number; - if ($this->oMessage->getClient()) { - $default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask(); + if ($this->message->getClient()) { + $default_mask = $this->message->getClient()?->getDefaultAttachmentMask(); if ($default_mask != null) { $this->mask = $default_mask; } } else { - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if ($default_mask != "") { $this->mask = $default_mask; } @@ -205,7 +213,7 @@ protected function fetch(): void { $content = $this->part->content; $this->content_type = $this->part->content_type; - $this->content = $this->oMessage->decodeString($content, $this->part->encoding); + $this->content = $this->message->decodeString($content, $this->part->encoding); // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, // it is not guaranteed to be unique and collisions are possible. @@ -292,7 +300,7 @@ public function decodeName(?string $name): string { } } - $decoder = $this->config['decoder']['message']; + $decoder = $this->options['decoder']['message']; if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { $name = $this->part->getHeader()->decode($name); } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { @@ -364,7 +372,7 @@ public function getAttributes(): array { * @return Message */ public function getMessage(): Message { - return $this->oMessage; + return $this->message; } /** @@ -390,6 +398,45 @@ public function getMask(): string { return $this->mask; } + /** + * Get the attachment options + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Set the attachment options + * @param array $options + * + * @return $this + */ + public function setOptions(array $options): Attachment { + $this->options = $options; + return $this; + } + + /** + * Get the used config + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + + /** + * Set the used config + * @param Config $config + * + * @return $this + */ + public function setConfig(Config $config): Attachment { + $this->config = $config; + return $this; + } + /** * Get a masked instance by providing a mask name * @param string|null $mask diff --git a/src/Client.php b/src/Client.php index 8027dc53..2ec9ebd9 100755 --- a/src/Client.php +++ b/src/Client.php @@ -46,6 +46,13 @@ class Client { */ public ?ProtocolInterface $connection = null; + /** + * Client configuration + * + * @var Config + */ + protected Config $config; + /** * Server hostname. * @@ -174,14 +181,14 @@ class Client { /** * Client constructor. - * @param array $config + * @param Config $config * * @throws MaskNotFoundException */ - public function __construct(array $config = []) { + public function __construct(Config $config) { $this->setConfig($config); - $this->setMaskFromConfig($config); - $this->setEventsFromConfig($config); + $this->setMaskFromConfig(); + $this->setEventsFromConfig(); } /** @@ -199,16 +206,17 @@ public function __destruct() { * Clone the current Client instance * * @return Client + * @throws MaskNotFoundException */ public function clone(): Client { - $client = new self(); + $client = new self($this->config); $client->events = $this->events; $client->timeout = $this->timeout; $client->active_folder = $this->active_folder; $client->default_account_config = $this->default_account_config; $config = $this->getAccountConfig(); foreach($config as $key => $value) { - $client->setAccountConfig($key, $config, $this->default_account_config); + $client->setAccountConfig($key, $this->default_account_config); } $client->default_message_mask = $this->default_message_mask; $client->default_attachment_mask = $this->default_message_mask; @@ -217,16 +225,17 @@ public function clone(): Client { /** * Set the Client configuration - * @param array $config + * @param Config $config * * @return self */ - public function setConfig(array $config): Client { - $default_account = ClientManager::get('default'); - $default_config = ClientManager::get("accounts.$default_account"); + public function setConfig(Config $config): Client { + $this->config = $config; + $default_account = $this->config->get('default'); + $default_config = $this->config->get("accounts.$default_account"); foreach ($this->default_account_config as $key => $value) { - $this->setAccountConfig($key, $config, $default_config); + $this->setAccountConfig($key, $default_config); } return $this; @@ -235,27 +244,20 @@ public function setConfig(array $config): Client { /** * Get the current config * - * @return array + * @return Config */ - public function getConfig(): array { - $config = []; - foreach($this->default_account_config as $key => $value) { - $config[$key] = $this->$key; - } - return $config; + public function getConfig(): Config { + return $this->config; } /** * Set a specific account config * @param string $key - * @param array $config * @param array $default_config */ - private function setAccountConfig(string $key, array $config, array $default_config): void { + private function setAccountConfig(string $key, array $default_config): void { $value = $this->default_account_config[$key]; - if(isset($config[$key])) { - $value = $config[$key]; - }elseif(isset($default_config[$key])) { + if(isset($default_config[$key])) { $value = $default_config[$key]; } $this->$key = $value; @@ -278,10 +280,9 @@ public function getAccountConfig(): array { /** * Look for a possible events in any available config - * @param $config */ - protected function setEventsFromConfig($config): void { - $this->events = ClientManager::get("events"); + protected function setEventsFromConfig(): void { + $this->events = $this->config->get("events"); if(isset($config['events'])){ foreach($config['events'] as $section => $events) { $this->events[$section] = array_merge($this->events[$section], $events); @@ -291,35 +292,35 @@ protected function setEventsFromConfig($config): void { /** * Look for a possible mask in any available config - * @param $config * * @throws MaskNotFoundException */ - protected function setMaskFromConfig($config): void { + protected function setMaskFromConfig(): void { + $masks = $this->config->get("masks"); - if(isset($config['masks'])){ - if(isset($config['masks']['message'])) { - if(class_exists($config['masks']['message'])) { - $this->default_message_mask = $config['masks']['message']; + if(isset($masks)){ + if(isset($masks['message'])) { + if(class_exists($masks['message'])) { + $this->default_message_mask = $masks['message']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']); + throw new MaskNotFoundException("Unknown mask provided: ".$masks['message']); } }else{ - $default_mask = ClientManager::getMask("message"); + $default_mask = $this->config->getMask("message"); if($default_mask != ""){ $this->default_message_mask = $default_mask; }else{ throw new MaskNotFoundException("Unknown message mask provided"); } } - if(isset($config['masks']['attachment'])) { - if(class_exists($config['masks']['attachment'])) { - $this->default_attachment_mask = $config['masks']['attachment']; + if(isset($masks['attachment'])) { + if(class_exists($masks['attachment'])) { + $this->default_attachment_mask = $masks['attachment']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ". $config['masks']['attachment']); + throw new MaskNotFoundException("Unknown mask provided: ". $masks['attachment']); } }else{ - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if($default_mask != ""){ $this->default_attachment_mask = $default_mask; }else{ @@ -327,14 +328,14 @@ protected function setMaskFromConfig($config): void { } } }else{ - $default_mask = ClientManager::getMask("message"); + $default_mask = $this->config->getMask("message"); if($default_mask != ""){ $this->default_message_mask = $default_mask; }else{ throw new MaskNotFoundException("Unknown message mask provided"); } - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if($default_mask != ""){ $this->default_attachment_mask = $default_mask; }else{ @@ -424,25 +425,25 @@ public function connect(): Client { $protocol = strtolower($this->protocol); if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) { - $this->connection = new ImapProtocol($this->validate_cert, $this->encryption); + $this->connection = new ImapProtocol($this->config, $this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); }else{ if (extension_loaded('imap') === false) { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); } - $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption); + $this->connection = new LegacyProtocol($this->config, $this->validate_cert, $this->encryption); if (str_starts_with($protocol, "legacy-")) { $protocol = substr($protocol, 7); } $this->connection->setProtocol($protocol); } - if (ClientManager::get('options.debug')) { + if ($this->config->get('options.debug')) { $this->connection->enableDebug(); } - if (!ClientManager::get('options.uid_cache')) { + if (!$this->config->get('options.uid_cache')) { $this->connection->disableUidCache(); } @@ -507,7 +508,7 @@ public function disconnect(): Client { */ public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) - $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; + $delimiter = is_null($delimiter) ? $this->config->get('options.delimiter', "/") : $delimiter; if (str_contains($folder_name, (string)$delimiter)) { return $this->getFolderByPath($folder_name, $utf7); diff --git a/src/ClientManager.php b/src/ClientManager.php index 7f724ffe..a63f3a4e 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -16,17 +16,15 @@ * Class ClientManager * * @package Webklex\IMAP - * - * @mixin Client */ class ClientManager { /** * All library config * - * @var array $config + * @var Config $config */ - public static array $config = []; + public Config $config; /** * @var array $accounts @@ -35,9 +33,9 @@ class ClientManager { /** * ClientManager constructor. - * @param array|string $config + * @param array|string|Config $config */ - public function __construct(array|string $config = []) { + public function __construct(array|string|Config $config = []) { $this->setConfig($config); } @@ -63,52 +61,10 @@ public function __call(string $method, array $parameters) { * @throws Exceptions\MaskNotFoundException */ public function make(array $config): Client { - return new Client($config); - } - - /** - * Get a dotted config parameter - * @param string $key - * @param null $default - * - * @return mixed|null - */ - public static function get(string $key, $default = null): mixed { - $parts = explode('.', $key); - $value = null; - foreach ($parts as $part) { - if ($value === null) { - if (isset(self::$config[$part])) { - $value = self::$config[$part]; - } else { - break; - } - } else { - if (isset($value[$part])) { - $value = $value[$part]; - } else { - break; - } - } - } - - return $value === null ? $default : $value; - } - - /** - * Get the mask for a given section - * @param string $section section name such as "message" or "attachment" - * - * @return string|null - */ - public static function getMask(string $section): ?string { - $default_masks = ClientManager::get("masks"); - if (isset($default_masks[$section])) { - if (class_exists($default_masks[$section])) { - return $default_masks[$section]; - } - } - return null; + $name = $this->config->getDefaultAccount(); + $clientConfig = $this->config->all(); + $clientConfig["accounts"] = [$name => $config]; + return new Client(Config::make($clientConfig)); } /** @@ -119,7 +75,7 @@ public static function getMask(string $section): ?string { * @throws Exceptions\MaskNotFoundException */ public function account(string $name = null): Client { - $name = $name ?: $this->getDefaultAccount(); + $name = $name ?: $this->config->getDefaultAccount(); // If the connection has not been resolved we will resolve it now as all // the connections are resolved when they are actually needed, so we do @@ -139,45 +95,11 @@ public function account(string $name = null): Client { * @throws Exceptions\MaskNotFoundException */ protected function resolve(string $name): Client { - $config = $this->getClientConfig($name); + $config = $this->config->getClientConfig($name); return new Client($config); } - /** - * Get the account configuration. - * @param string|null $name - * - * @return array - */ - protected function getClientConfig(?string $name): array { - if ($name === null || $name === 'null' || $name === "") { - return ['driver' => 'null']; - } - $account = self::$config["accounts"][$name] ?? []; - - return is_array($account) ? $account : []; - } - - /** - * Get the name of the default account. - * - * @return string - */ - public function getDefaultAccount(): string { - return self::$config['default']; - } - - /** - * Set the name of the default account. - * @param string $name - * - * @return void - */ - public function setDefaultAccount(string $name): void { - self::$config['default'] = $name; - } - /** * Merge the vendor settings with the local config @@ -186,108 +108,24 @@ public function setDefaultAccount(string $name): void { * If however the default account is missing a parameter the package default account parameter will be used. * This can be disabled by setting imap.default in your config file to 'false' * - * @param array|string $config + * @param array|string|Config $config * * @return $this */ - public function setConfig(array|string $config): ClientManager { - - if (is_array($config) === false) { - $config = require $config; - } - - $config_key = 'imap'; - $path = __DIR__ . '/config/' . $config_key . '.php'; - - $vendor_config = require $path; - $config = $this->array_merge_recursive_distinct($vendor_config, $config); - - if (is_array($config)) { - if (isset($config['default'])) { - if (isset($config['accounts']) && $config['default']) { - - $default_config = $vendor_config['accounts']['default']; - if (isset($config['accounts'][$config['default']])) { - $default_config = array_merge($default_config, $config['accounts'][$config['default']]); - } - - if (is_array($config['accounts'])) { - foreach ($config['accounts'] as $account_key => $account) { - $config['accounts'][$account_key] = array_merge($default_config, $account); - } - } - } - } + public function setConfig(array|string|Config $config): ClientManager { + if (!$config instanceof Config) { + $config = Config::make($config); } - - self::$config = $config; + $this->config = $config; return $this; } /** - * Marge arrays recursively and distinct - * - * Merges any number of arrays / parameters recursively, replacing - * entries with string keys with values from latter arrays. - * If the entry or the next value to be assigned is an array, then it - * automatically treats both arguments as an array. - * Numeric entries are appended, not replaced, but only if they are - * unique - * - * @return array|mixed - * - * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 - * @author Mark Roduner + * Get the config instance + * @return Config */ - private function array_merge_recursive_distinct(): mixed { - - $arrays = func_get_args(); - $base = array_shift($arrays); - - // From https://stackoverflow.com/a/173479 - $isAssoc = function(array $arr) { - if (array() === $arr) return false; - return array_keys($arr) !== range(0, count($arr) - 1); - }; - - if (!is_array($base)) $base = empty($base) ? array() : array($base); - - foreach ($arrays as $append) { - - if (!is_array($append)) $append = array($append); - - foreach ($append as $key => $value) { - - if (!array_key_exists($key, $base) and !is_numeric($key)) { - $base[$key] = $value; - continue; - } - - if ( - ( - is_array($value) - && $isAssoc($value) - ) - || ( - is_array($base[$key]) - && $isAssoc($base[$key]) - ) - ) { - // If the arrays are not associates we don't want to array_merge_recursive_distinct - // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] - // results in $resultConfig['dispositions'] = ['attachment', 'inline'] - $base[$key] = $this->array_merge_recursive_distinct($base[$key], $value); - } else if (is_numeric($key)) { - if (!in_array($value, $base)) $base[] = $value; - } else { - $base[$key] = $value; - } - - } - - } - - return $base; + public function getConfig(): Config { + return $this->config; } } \ No newline at end of file diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 00000000..b5cf2599 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,266 @@ +config = $config; + } + + /** + * Get a dotted config parameter + * @param string $key + * @param null $default + * + * @return mixed|null + */ + public function get(string $key, $default = null): mixed { + $parts = explode('.', $key); + $value = null; + foreach ($parts as $part) { + if ($value === null) { + if (isset($this->config[$part])) { + $value = $this->config[$part]; + } else { + break; + } + } else { + if (isset($value[$part])) { + $value = $value[$part]; + } else { + break; + } + } + } + + return $value === null ? $default : $value; + } + + /** + * Set a dotted config parameter + * @param string $key + * @param string|array|mixed$value + * + * @return void + */ + public function set(string $key, mixed $value): void { + $parts = explode('.', $key); + $config = &$this->config; + + foreach ($parts as $part) { + if (!isset($config[$part])) { + $config[$part] = []; + } + $config = &$config[$part]; + } + + if(is_array($config) && is_array($value)){ + $config = array_merge($config, $value); + }else{ + $config = $value; + } + } + + /** + * Get the mask for a given section + * @param string $section section name such as "message" or "attachment" + * + * @return string|null + */ + public function getMask(string $section): ?string { + $default_masks = $this->get('masks', []); + if (isset($default_masks[$section])) { + if (class_exists($default_masks[$section])) { + return $default_masks[$section]; + } + } + return null; + } + + /** + * Get the account configuration. + * @param string|null $name + * + * @return self + */ + public function getClientConfig(?string $name): self { + $config = $this->all(); + $defaultName = $this->getDefaultAccount(); + $defaultAccount = $this->get('accounts.'.$defaultName, []); + + if ($name === null || $name === 'null' || $name === "") { + $account = $defaultAccount; + $name = $defaultName; + }else{ + $account = $this->get('accounts.'.$name, $defaultAccount); + } + + $config["default"] = $name; + $config["accounts"] = [ + $name => $account + ]; + + return new self($config); + } + + /** + * Get the name of the default account. + * + * @return string + */ + public function getDefaultAccount(): string { + return $this->get('default', 'default'); + } + + /** + * Set the name of the default account. + * @param string $name + * + * @return void + */ + public function setDefaultAccount(string $name): void { + $this->set('default', $name); + } + + /** + * Create a new instance of the Config class + * @param array|string $config + * @return Config + */ + public static function make(array|string $config = []): Config { + if (is_array($config) === false) { + $config = require $config; + } + + $config_key = 'imap'; + $path = __DIR__ . '/config/' . $config_key . '.php'; + + $vendor_config = require $path; + $config = self::array_merge_recursive_distinct($vendor_config, $config); + + if (isset($config['default'])) { + if (isset($config['accounts']) && $config['default']) { + + $default_config = $vendor_config['accounts']['default']; + if (isset($config['accounts'][$config['default']])) { + $default_config = array_merge($default_config, $config['accounts'][$config['default']]); + } + + if (is_array($config['accounts'])) { + foreach ($config['accounts'] as $account_key => $account) { + $config['accounts'][$account_key] = array_merge($default_config, $account); + } + } + } + } + + return new self($config); + } + + /** + * Marge arrays recursively and distinct + * + * Merges any number of arrays / parameters recursively, replacing + * entries with string keys with values from latter arrays. + * If the entry or the next value to be assigned is an array, then it + * automatically treats both arguments as an array. + * Numeric entries are appended, not replaced, but only if they are + * unique + * + * @return array + * + * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 + * @author Mark Roduner + */ + private static function array_merge_recursive_distinct(): array { + $arrays = func_get_args(); + $base = array_shift($arrays); + + // From https://stackoverflow.com/a/173479 + $isAssoc = function(array $arr) { + if (array() === $arr) return false; + return array_keys($arr) !== range(0, count($arr) - 1); + }; + + if (!is_array($base)) $base = empty($base) ? array() : array($base); + + foreach ($arrays as $append) { + if (!is_array($append)) $append = array($append); + + foreach ($append as $key => $value) { + + if (!array_key_exists($key, $base) and !is_numeric($key)) { + $base[$key] = $value; + continue; + } + + if ((is_array($value) && $isAssoc($value)) || (is_array($base[$key]) && $isAssoc($base[$key]))) { + // If the arrays are not associates we don't want to array_merge_recursive_distinct + // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] + // results in $resultConfig['dispositions'] = ['attachment', 'inline'] + $base[$key] = self::array_merge_recursive_distinct($base[$key], $value); + } else if (is_numeric($key)) { + if (!in_array($value, $base)) $base[] = $value; + } else { + $base[$key] = $value; + } + + } + + } + + return $base; + } + + /** + * Get all configuration values + * @return array + */ + public function all(): array { + return $this->config; + } + + /** + * Check if a configuration value exists + * @param string $key + * @return bool + */ + public function has(string $key): bool { + return $this->get($key) !== null; + } + + /** + * Remove all configuration values + * @return $this + */ + public function clear(): static { + $this->config = []; + return $this; + } +} \ No newline at end of file diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index e29d0470..4de12c1e 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -13,6 +13,8 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Exception; +use Throwable; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; @@ -41,10 +43,12 @@ class ImapProtocol extends Protocol { /** * Imap constructor. + * @param Config $config * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, mixed $encryption = false) { + public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) { + $this->config = $config; $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -1314,7 +1318,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp $headers = $this->headers($ids, "RFC822", $uid); $response->stack($headers); foreach ($headers->data() as $id => $raw_header) { - $result[$id] = (new Header($raw_header, false))->getAttributes(); + $result[$id] = (new Header($raw_header, $this->config))->getAttributes(); } } return $response->setResult($result)->setCanBeEmpty(true); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index cb6393e5..6313dca9 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; use Webklex\PHPIMAP\Exceptions\MethodNotSupportedException; @@ -32,10 +33,12 @@ class LegacyProtocol extends Protocol { /** * Imap constructor. + * @param Config $config * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, mixed $encryption = false) { + public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) { + $this->config = $config; $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -52,7 +55,7 @@ public function __destruct() { * @param string $host * @param int|null $port */ - public function connect(string $host, int $port = null) { + public function connect(string $host, int $port = null): void { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { @@ -81,7 +84,7 @@ public function login(string $user, string $password): Response { $password, 0, $attempts = 3, - ClientManager::get('options.open') + $this->config->get('options.open') ); $response->addCommand("imap_open"); } catch (\ErrorException $e) { diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 6fe88ee9..dd909b40 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\IMAP; @@ -38,10 +39,15 @@ abstract class Protocol implements ProtocolInterface { protected bool $enable_uid_cache = true; /** - * @var resource + * @var resource|mixed|boolean|null $stream */ public $stream = false; + /** + * @var Config $config + */ + protected Config $config; + /** * Connection encryption method * @var string $encryption @@ -363,4 +369,13 @@ public function meta(): array { public function getStream(): mixed { return $this->stream; } + + /** + * Set the Config instance + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } } diff --git a/src/Folder.php b/src/Folder.php index 58211e8c..81634e30 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -334,7 +334,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -472,7 +472,7 @@ public function idle(callable $callback, int $timeout = 300): void { $last_action = Carbon::now()->addSeconds($timeout); - $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $sequence = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); while(true) { // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand @@ -607,7 +607,7 @@ public function getClient(): Client { */ public function setDelimiter($delimiter): void { if(in_array($delimiter, [null, '', ' ', false]) === true){ - $delimiter = ClientManager::get('options.delimiter', '/'); + $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); } $this->delimiter = $delimiter; diff --git a/src/Header.php b/src/Header.php index ce386b40..d4b86975 100644 --- a/src/Header.php +++ b/src/Header.php @@ -41,9 +41,16 @@ class Header { /** * Config holder * - * @var array $config + * @var Config $config */ - protected array $config = []; + protected Config $config; + + /** + * Config holder + * + * @var array $options + */ + protected array $options = []; /** * Fallback Encoding @@ -54,13 +61,15 @@ class Header { /** * Header constructor. + * @param Config $config * @param string $raw_header * * @throws InvalidMessageDateException */ - public function __construct(string $raw_header) { + public function __construct(string $raw_header, Config $config) { $this->raw = $raw_header; - $this->config = ClientManager::get('options'); + $this->config = $config; + $this->options = $this->config->get('options'); $this->parse(); } @@ -162,7 +171,7 @@ public function find($pattern): mixed { * @return string|null */ public function getBoundary(): ?string { - $regex = $this->config["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; + $regex = $this->options["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; $boundary = $this->find($regex); if ($boundary === null) { @@ -229,7 +238,7 @@ protected function parse(): void { public function rfc822_parse_headers($raw_headers): object { $headers = []; $imap_headers = []; - if (extension_loaded('imap') && $this->config["rfc822"]) { + if (extension_loaded('imap') && $this->options["rfc822"]) { $raw_imap_headers = (array)\imap_rfc822_parse_headers($raw_headers); foreach ($raw_imap_headers as $key => $values) { $key = strtolower(str_replace("-", "_", $key)); @@ -418,7 +427,7 @@ public function decode(mixed $value): mixed { return $this->decodeArray($value); } $original_value = $value; - $decoder = $this->config['decoder']['message']; + $decoder = $this->options['decoder']['message']; if ($value !== null) { if ($decoder === 'utf-8') { @@ -490,7 +499,7 @@ private function findPriority(): void { private function decodeAddresses($values): array { $addresses = []; - if (extension_loaded('mailparse') && $this->config["rfc822"]) { + if (extension_loaded('mailparse') && $this->options["rfc822"]) { foreach ($values as $address) { foreach (\mailparse_rfc822_parse_addresses($address) as $parsed_address) { if (isset($parsed_address['address'])) { @@ -800,9 +809,38 @@ public function setAttributes(array $attributes): Header { * * @return Header */ - public function setConfig(array $config): Header { + public function setOptions(array $config): Header { + $this->options = $config; + return $this; + } + + /** + * Get the configuration used for parsing a raw header + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Set the configuration used for parsing a raw header + * @param Config $config + * + * @return Header + */ + public function setConfig(Config $config): Header { $this->config = $config; return $this; } + /** + * Get the configuration used for parsing a raw header + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + } diff --git a/src/Message.php b/src/Message.php index 09a534f2..050ebc9d 100755 --- a/src/Message.php +++ b/src/Message.php @@ -97,11 +97,18 @@ class Message { protected string $mask = MessageMask::class; /** - * Used config + * Used options * - * @var array $config + * @var array $options */ - protected array $config = []; + protected array $options = []; + + /** + * All library configs + * + * @var Config $config + */ + protected Config $config; /** * Attribute holder @@ -205,7 +212,7 @@ class Message { * @throws ResponseException */ public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { - $this->boot(); + $this->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); if ($default_mask != null) { @@ -269,7 +276,7 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); - $instance->boot(); + $instance->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); if ($default_mask != null) { @@ -296,29 +303,33 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw /** * Create a new message instance by reading and loading a file or remote location + * @param string $filename + * @param ?Config $config * - * @throws RuntimeException - * @throws MessageContentFetchingException - * @throws ResponseException - * @throws ImapBadRequestException - * @throws InvalidMessageDateException + * @return Message + * @throws AuthFailedException * @throws ConnectionFailedException + * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws ReflectionException - * @throws AuthFailedException + * @throws InvalidMessageDateException * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException */ - public static function fromFile($filename): Message { + public static function fromFile(string $filename, Config $config = null): Message { $blob = file_get_contents($filename); if ($blob === false) { throw new RuntimeException("Unable to read file"); } - return self::fromString($blob); + return self::fromString($blob, $config); } /** * Create a new message instance by reading and loading a string * @param string $blob + * @param ?Config $config * * @return Message * @throws AuthFailedException @@ -332,13 +343,13 @@ public static function fromFile($filename): Message { * @throws ResponseException * @throws RuntimeException */ - public static function fromString(string $blob): Message { + public static function fromString(string $blob, Config $config = null): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); - $instance->boot(); + $instance->boot($config); - $default_mask = ClientManager::getMask("message"); + $default_mask = $instance->getConfig()->getMask("message"); if($default_mask != ""){ $instance->setMask($default_mask); }else{ @@ -361,15 +372,18 @@ public static function fromString(string $blob): Message { /** * Boot a new instance + * @param ?Config $config */ - public function boot(): void { + public function boot(Config $config = null): void { $this->attributes = []; + $this->client = null; + $this->config = $config ?? Config::make(); - $this->config = ClientManager::get('options'); - $this->available_flags = ClientManager::get('flags'); + $this->options = $this->config->get('options'); + $this->available_flags = $this->config->get('flags'); - $this->attachments = AttachmentCollection::make([]); - $this->flags = FlagCollection::make([]); + $this->attachments = AttachmentCollection::make(); + $this->flags = FlagCollection::make(); } /** @@ -543,7 +557,7 @@ private function parseHeader(): void { * @throws InvalidMessageDateException */ public function parseRawHeader(string $raw_header): void { - $this->header = new Header($raw_header); + $this->header = new Header($raw_header, $this->getConfig()); } /** @@ -786,7 +800,7 @@ public function setFetchOption($option): Message { if (is_long($option) === true) { $this->fetch_options = $option; } elseif (is_null($option) === true) { - $config = ClientManager::get('options.fetch', IMAP::FT_UID); + $config = $this->config->get('options.fetch', IMAP::FT_UID); $this->fetch_options = is_long($config) ? $config : 1; } @@ -803,7 +817,7 @@ public function setSequence(?int $sequence): Message { if (is_long($sequence)) { $this->sequence = $sequence; } elseif (is_null($sequence)) { - $config = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $config = $this->config->get('options.sequence', IMAP::ST_MSGN); $this->sequence = is_long($config) ? $config : IMAP::ST_MSGN; } @@ -820,7 +834,7 @@ public function setFetchBodyOption($option): Message { if (is_bool($option)) { $this->fetch_body = $option; } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_body', true); + $config = $this->config->get('options.fetch_body', true); $this->fetch_body = is_bool($config) ? $config : true; } @@ -837,7 +851,7 @@ public function setFetchFlagsOption($option): Message { if (is_bool($option)) { $this->fetch_flags = $option; } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_flags', true); + $config = $this->config->get('options.fetch_flags', true); $this->fetch_flags = is_bool($config) ? $config : true; } @@ -973,7 +987,7 @@ public function getFolder(): ?Folder { public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { $thread = $thread ?: MessageCollection::make([]); $folder = $folder ?: $this->getFolder(); - $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); + $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ foreach ($thread as $message) { @@ -1547,11 +1561,11 @@ public function setFolderPath($folder_path): Message { /** * Set the config - * @param array $config + * @param Config $config * * @return Message */ - public function setConfig(array $config): Message { + public function setConfig(Config $config): Message { $this->config = $config; return $this; @@ -1560,12 +1574,33 @@ public function setConfig(array $config): Message { /** * Get the config * - * @return array + * @return Config */ - public function getConfig(): array { + public function getConfig(): Config { return $this->config; } + /** + * Set the options + * @param array $options + * + * @return Message + */ + public function setOptions(array $options): Message { + $this->options = $options; + + return $this; + } + + /** + * Get the options + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + /** * Set the available flags * @param $available_flags diff --git a/src/Part.php b/src/Part.php index 1759b8de..1dbf9439 100644 --- a/src/Part.php +++ b/src/Part.php @@ -139,16 +139,23 @@ class Part { */ private ?Header $header; + /** + * @var Config $config + */ + protected Config $config; + /** * Part constructor. - * @param $raw_part + * @param string $raw_part + * @param Config $config * @param Header|null $header * @param integer $part_number * * @throws InvalidMessageDateException */ - public function __construct($raw_part, Header $header = null, int $part_number = 0) { + public function __construct(string $raw_part, Config $config, Header $header = null, int $part_number = 0) { $this->raw = $raw_part; + $this->config = $config; $this->header = $header; $this->part_number = $part_number; $this->parse(); @@ -211,7 +218,7 @@ private function findHeaders(): string { $headers = substr($this->raw, 0, strlen($body) * -1); $body = substr($body, 0, -2); - $this->header = new Header($headers); + $this->header = new Header($headers, $this->config); return $body; } @@ -282,7 +289,7 @@ private function parseEncoding(): void { * @return bool */ public function isAttachment(): bool { - $valid_disposition = in_array(strtolower($this->disposition ?? ''), ClientManager::get('options.dispositions')); + $valid_disposition = in_array(strtolower($this->disposition ?? ''), $this->config->get('options.dispositions')); if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) { @@ -305,4 +312,13 @@ public function getHeader(): ?Header { return $this->header; } + /** + * Get the Config instance + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + } diff --git a/src/Query/Query.php b/src/Query/Query.php index b0c9eddd..7ae035de 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -18,7 +18,6 @@ use Illuminate\Support\Collection; use ReflectionException; use Webklex\PHPIMAP\Client; -use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; @@ -93,18 +92,19 @@ class Query { */ public function __construct(Client $client, array $extensions = []) { $this->setClient($client); + $config = $this->client->getConfig(); - $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - if (ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); + $this->sequence = $config->get('options.sequence', IMAP::ST_MSGN); + if ($config->get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); - if (ClientManager::get('options.fetch_order') === 'desc') { + if ($config->get('options.fetch_order') === 'desc') { $this->fetch_order = 'desc'; } else { $this->fetch_order = 'asc'; } - $this->date_format = ClientManager::get('date_format', 'd M y'); - $this->soft_fail = ClientManager::get('options.soft_fail', false); + $this->date_format = $config->get('date_format', 'd M y'); + $this->soft_fail = $config->get('options.soft_fail', false); $this->setExtensions($extensions); $this->query = new Collection(); @@ -235,6 +235,7 @@ protected function fetch(Collection $available_messages): array { $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); $extensions = $this->getExtensions(); if (empty($extensions) === false && method_exists($this->client->getConnection(), "fetch")) { + // this polymorphic call is fine - the method exists at this point $extensions = $this->client->getConnection()->fetch($extensions, $uids, null, $this->sequence)->validatedData(); } $flags = $this->client->getConnection()->flags($uids, $this->sequence)->validatedData(); @@ -336,11 +337,12 @@ public function curate_messages(Collection $available_messages): MessageCollecti * @throws ResponseException */ protected function populate(Collection $available_messages): MessageCollection { - $messages = MessageCollection::make([]); + $messages = MessageCollection::make(); + $config = $this->client->getConfig(); $messages->total($available_messages->count()); - $message_key = ClientManager::get('options.message_key'); + $message_key = $config->get('options.message_key'); $raw_messages = $this->fetch($available_messages); diff --git a/src/Structure.php b/src/Structure.php index 00a44db2..4907c542 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -50,11 +50,11 @@ class Structure { public array $parts = []; /** - * Config holder + * Options holder * - * @var array $config + * @var array $options */ - protected array $config = []; + protected array $options = []; /** * Structure constructor. @@ -67,7 +67,7 @@ class Structure { public function __construct($raw_structure, Header $header) { $this->raw = $raw_structure; $this->header = $header; - $this->config = ClientManager::get('options'); + $this->options = $header->getConfig()->get('options'); $this->parse(); } @@ -110,7 +110,8 @@ private function parsePart(string $context, int $part_number = 0): array { $headers = substr($context, 0, strlen($body) * -1); $body = substr($body, 0, -2); - $headers = new Header($headers); + $config = $this->header->getConfig(); + $headers = new Header($headers, $config); if (($boundary = $headers->getBoundary()) !== null) { $parts = $this->detectParts($boundary, $body, $part_number); @@ -119,7 +120,7 @@ private function parsePart(string $context, int $part_number = 0): array { } } - return [new Part($body, $headers, $part_number)]; + return [new Part($body, $this->header->getConfig(), $headers, $part_number)]; } /** @@ -163,6 +164,6 @@ public function find_parts(): array { return $this->detectParts($boundary, $this->raw); } - return [new Part($this->raw, $this->header)]; + return [new Part($this->raw, $this->header->getConfig(), $this->header)]; } } From d23aecc035533464230788f1eff6c13c257478db Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:15:23 +0200 Subject: [PATCH 546/600] code quality improved & doc blocks added --- examples/custom_attachment_mask.php | 3 +- examples/custom_message_mask.php | 5 +-- src/Connection/Protocols/ImapProtocol.php | 39 +++++++++++++++------ src/Connection/Protocols/LegacyProtocol.php | 4 +-- src/Connection/Protocols/Protocol.php | 4 +-- src/Folder.php | 6 ++-- src/Header.php | 14 ++++---- src/Message.php | 13 +++---- src/Query/Query.php | 2 +- src/Query/WhereQuery.php | 2 +- src/Support/Masks/AttachmentMask.php | 1 + src/Support/Masks/MessageMask.php | 1 + 12 files changed, 58 insertions(+), 36 deletions(-) diff --git a/examples/custom_attachment_mask.php b/examples/custom_attachment_mask.php index 5a6323a3..eb4973e3 100644 --- a/examples/custom_attachment_mask.php +++ b/examples/custom_attachment_mask.php @@ -33,8 +33,9 @@ public function custom_save(): bool { } -/** @var \Webklex\PHPIMAP\Client $client */ $cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ $client = $cm->account('default'); $client->connect(); $client->setDefaultAttachmentMask(CustomAttachmentMask::class); diff --git a/examples/custom_message_mask.php b/examples/custom_message_mask.php index 187eeed4..0463c65b 100644 --- a/examples/custom_message_mask.php +++ b/examples/custom_message_mask.php @@ -30,8 +30,9 @@ public function getAttachmentCount(): int { } -/** @var \Webklex\PHPIMAP\Client $client */ $cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ $client = $cm->account('default'); $client->connect(); @@ -44,7 +45,7 @@ public function getAttachmentCount(): int { /** @var CustomMessageMask $masked_message */ $masked_message = $message->mask(CustomMessageMask::class); -echo 'Token for uid ['.$masked_message->uid.']: '.$masked_message->token().' @atms:'.$masked_message->getAttachmentCount(); +echo 'Token for uid [' . $masked_message->uid . ']: ' . $masked_message->token() . ' @atms:' . $masked_message->getAttachmentCount(); $masked_message->setFlag('seen'); diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4de12c1e..cb7cf4b4 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -98,14 +98,14 @@ public function connect(string $host, int $port = null): bool { * Check if the current session is connected * * @return bool + * @throws ImapBadRequestException */ public function connected(): bool { if ((bool)$this->stream) { try { $this->requestAndResponse('NOOP'); return true; - } - catch (ImapServerErrorException|RuntimeException) { + } catch (ImapServerErrorException|RuntimeException) { return false; } } @@ -120,7 +120,7 @@ public function connected(): bool { * @throws ImapServerErrorException * @throws RuntimeException */ - protected function enableStartTls() { + protected function enableStartTls(): void { $response = $this->requestAndResponse('STARTTLS'); $result = $response->successful() && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); if (!$result) { @@ -324,14 +324,33 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $original = is_array($original) ? $original : [$original]; + // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(implode("\n", $original)); + throw new ImapServerErrorException($this->stringifyArray($original)); } - throw new ImapBadRequestException(implode("\n", $original)); + throw new ImapBadRequestException($this->stringifyArray($original)); + } + + /** + * Convert an array to a string + * @param array $arr array to stringify + * + * @return string stringified array + */ + private function stringifyArray(array $arr): string { + $string = ""; + foreach ($arr as $value) { + if (is_array($value)) { + $string .= "(" . $this->stringifyArray($value) . ")"; + } else { + $string .= $value . " "; + } + } + return $string; } /** @@ -523,7 +542,7 @@ public function logout(): Response { try { $result = $this->requestAndResponse('LOGOUT', [], true); fclose($this->stream); - } catch (\Throwable) { + } catch (Throwable) { } $this->reset(); @@ -572,7 +591,7 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I $result = []; $tokens = []; // define $tokens variable before first use - while (!$this->readLine($response, $tokens, $tag, false)) { + while (!$this->readLine($response, $tokens, $tag)) { if ($tokens[0] == 'FLAGS') { array_shift($tokens); $result['flags'] = $tokens; @@ -645,7 +664,7 @@ public function examineFolder(string $folder = 'INBOX'): Response { * @throws RuntimeException */ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { - $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)], false); + $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)]); $data = $response->validatedData(); if (!isset($data[0]) || !isset($data[0][2])) { @@ -654,7 +673,7 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', $result = []; $key = null; - foreach($data[0][2] as $value) { + foreach ($data[0][2] as $value) { if ($key === null) { $key = $value; } else { @@ -1245,7 +1264,7 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): Response { * * @throws RuntimeException */ - public function idle() { + public function idle(): void { $response = $this->sendRequest("IDLE"); if (!$this->assumedNextLine($response, '+ ')) { throw new RuntimeException('idle failed'); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 6313dca9..c6471a02 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -125,8 +125,6 @@ public function login(string $user, string $password): Response { * @param string $token access token * * @return Response - * @throws AuthFailedException - * @throws RuntimeException */ public function authenticate(string $user, string $token): Response { return $this->login($user, $token); @@ -392,7 +390,7 @@ public function getUid(int $id = null): Response { } /** - * Get a message number for a uid + * Get the message number of a given uid * @param string $id uid * * @return Response message number diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index dd909b40..a3886b55 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -274,7 +274,7 @@ public function buildUIDCommand(string $command, int|string $uid): string { * * @param array|null $uids */ - public function setUidCache(?array $uids) { + public function setUidCache(?array $uids): void { if (is_null($uids)) { $this->uid_cache = []; return; @@ -336,7 +336,7 @@ public function connected(): bool { } /** - * Retrieves header/meta data from the resource stream + * Retrieves header/metadata from the resource stream * * @return array */ diff --git a/src/Folder.php b/src/Folder.php index 81634e30..8c9abd61 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -252,7 +252,7 @@ public function getChildren(): FolderCollection { protected function decodeName($name): string|array|bool|null { $parts = []; foreach(explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); } return implode($this->delimiter, $parts); @@ -402,7 +402,7 @@ public function rename(string $new_name, bool $expunge = true): array { public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); if($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(null); + $this->client->setActiveFolder(); } if($expunge) $this->client->expunge(); @@ -527,7 +527,7 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws ResponseException */ public function status(): array { - return $this->client->getConnection()->folderStatus($this->path, ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'])->validatedData(); + return $this->client->getConnection()->folderStatus($this->path)->validatedData(); } /** diff --git a/src/Header.php b/src/Header.php index d4b86975..539bd05e 100644 --- a/src/Header.php +++ b/src/Header.php @@ -205,7 +205,7 @@ protected function parse(): void { $this->set("subject", $this->decode($header->subject)); } if (property_exists($header, 'references')) { - $this->set("references", array_map(function ($item) { + $this->set("references", array_map(function($item) { return str_replace(['<', '>'], '', $item); }, explode(" ", $header->references))); } @@ -440,14 +440,14 @@ public function decode(mixed $value): mixed { $value = $tempValue; } else if (extension_loaded('imap')) { $value = \imap_utf8($value); - }else if (function_exists('iconv_mime_decode')){ + } else if (function_exists('iconv_mime_decode')) { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - }else{ + } else { $value = mb_decode_mimeheader($value); } - }elseif ($decoder === 'iconv') { + } elseif ($decoder === 'iconv') { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - }else if ($this->is_uft8($value)) { + } else if ($this->is_uft8($value)) { $value = mb_decode_mimeheader($value); } @@ -771,10 +771,10 @@ private function parseDate(object $header): void { try { $parsed_date = Carbon::parse($date); } catch (\Exception $_e) { - if (!isset($this->config["fallback_date"])) { + if (!isset($this->options["fallback_date"])) { throw new InvalidMessageDateException("Invalid message date. ID:" . $this->get("message_id") . " Date:" . $header->date . "/" . $date, 1100, $e); } else { - $parsed_date = Carbon::parse($this->config["fallback_date"]); + $parsed_date = Carbon::parse($this->options["fallback_date"]); } } } diff --git a/src/Message.php b/src/Message.php index 050ebc9d..10b96018 100755 --- a/src/Message.php +++ b/src/Message.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP; +use Exception; use ReflectionClass; use ReflectionException; use Webklex\PHPIMAP\Exceptions\AuthFailedException; @@ -87,7 +88,7 @@ class Message { * * @var ?Client */ - private ?Client $client = null; + private ?Client $client; /** * Default mask @@ -565,7 +566,7 @@ public function parseRawHeader(string $raw_header): void { * @param array $raw_flags */ public function parseRawFlags(array $raw_flags): void { - $this->flags = FlagCollection::make([]); + $this->flags = FlagCollection::make(); foreach ($raw_flags as $flag) { if (str_starts_with($flag, "\\")) { @@ -592,7 +593,7 @@ public function parseRawFlags(array $raw_flags): void { */ private function parseFlags(): void { $this->client->openFolder($this->folder_path); - $this->flags = FlagCollection::make([]); + $this->flags = FlagCollection::make(); $sequence_id = $this->getSequenceId(); try { @@ -628,7 +629,7 @@ public function parseBody(): Message { try { $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { - throw new MessageContentFetchingException("failed to fetch content", 0); + throw new MessageContentFetchingException("failed to fetch content", 0, $e); } if (!isset($contents[$sequence_id])) { throw new MessageContentFetchingException("no content found", 0); @@ -919,7 +920,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { try { return iconv($from, $to.'//IGNORE', $str); - } catch (\Exception $e) { + } catch (Exception) { return @iconv($from, $to, $str); } } else { @@ -985,7 +986,7 @@ public function getFolder(): ?Folder { * @throws ResponseException */ public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { - $thread = $thread ?: MessageCollection::make([]); + $thread = $thread ?: MessageCollection::make(); $folder = $folder ?: $this->getFolder(); $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); diff --git a/src/Query/Query.php b/src/Query/Query.php index 7ae035de..cade729d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -315,7 +315,7 @@ public function curate_messages(Collection $available_messages): MessageCollecti if ($available_messages->count() > 0) { return $this->populate($available_messages); } - return MessageCollection::make([]); + return MessageCollection::make(); } catch (Exception $e) { throw new GetMessagesFailedException($e->getMessage(), 0, $e); } diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 74024ddc..e76cbe85 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -155,7 +155,7 @@ public function where(mixed $criteria, mixed $value = null): static { * * @throws InvalidWhereQueryCriteriaException */ - protected function push_search_criteria(string $criteria, mixed $value){ + protected function push_search_criteria(string $criteria, mixed $value): void { $criteria = $this->validate_criteria($criteria); $value = $this->parse_value($value); diff --git a/src/Support/Masks/AttachmentMask.php b/src/Support/Masks/AttachmentMask.php index d79b948a..2559c5b9 100644 --- a/src/Support/Masks/AttachmentMask.php +++ b/src/Support/Masks/AttachmentMask.php @@ -18,6 +18,7 @@ * Class AttachmentMask * * @package Webklex\PHPIMAP\Support\Masks + * @mixin Attachment */ class AttachmentMask extends Mask { diff --git a/src/Support/Masks/MessageMask.php b/src/Support/Masks/MessageMask.php index 4cc3d5c0..aa3623f9 100644 --- a/src/Support/Masks/MessageMask.php +++ b/src/Support/Masks/MessageMask.php @@ -19,6 +19,7 @@ * Class MessageMask * * @package Webklex\PHPIMAP\Support\Masks + * @mixin Message */ class MessageMask extends Mask { From 5e0f4a81193512ec17fa9906146a3a6dfda9c2ea Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:17:10 +0200 Subject: [PATCH 547/600] Test for issue #420 added --- tests/issues/Issue420Test.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/issues/Issue420Test.php diff --git a/tests/issues/Issue420Test.php b/tests/issues/Issue420Test.php new file mode 100644 index 00000000..0cac9b6e --- /dev/null +++ b/tests/issues/Issue420Test.php @@ -0,0 +1,31 @@ +get("subject"); + + // Ticket No: [��17] Mailbox Inbox - (17) Incoming failed messages + $this->assertEquals('Ticket No: [??17] Mailbox Inbox - (17) Incoming failed messages', utf8_decode($subject->toString())); + } + +} \ No newline at end of file From 46a75ab83ab5735d1585e3e2dc5c3b0542bb756a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:17:46 +0200 Subject: [PATCH 548/600] tests updated to use the correct config --- tests/ClientManagerTest.php | 25 +++++++++-------- tests/ClientTest.php | 42 ++++++++++++++++------------- tests/HeaderTest.php | 19 ++++++++++--- tests/ImapProtocolTest.php | 15 ++++++++++- tests/MessageTest.php | 30 ++++++++++++--------- tests/PartTest.php | 25 ++++++++++++----- tests/StructureTest.php | 15 ++++++++++- tests/fixtures/DateTemplateTest.php | 2 +- tests/fixtures/FixtureTestCase.php | 5 ++-- tests/issues/Issue355Test.php | 3 ++- tests/issues/Issue383Test.php | 2 +- tests/issues/Issue393Test.php | 2 +- tests/issues/Issue412Test.php | 1 + tests/issues/Issue413Test.php | 1 + tests/live/ClientTest.php | 2 +- tests/live/FolderTest.php | 12 ++++----- tests/live/LegacyTest.php | 6 ++--- tests/live/MessageTest.php | 18 ++++++------- tests/live/QueryTest.php | 4 +-- 19 files changed, 149 insertions(+), 80 deletions(-) diff --git a/tests/ClientManagerTest.php b/tests/ClientManagerTest.php index 80ec4ceb..e7910cf1 100644 --- a/tests/ClientManagerTest.php +++ b/tests/ClientManagerTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\IMAP; @@ -38,10 +39,12 @@ public function setUp(): void { * @return void */ public function testConfigAccessorAccount(): void { - self::assertSame("default", ClientManager::get("default")); - self::assertSame("d-M-Y", ClientManager::get("date_format")); - self::assertSame(IMAP::FT_PEEK, ClientManager::get("options.fetch")); - self::assertSame([], ClientManager::get("options.open")); + $config = $this->cm->getConfig(); + self::assertInstanceOf(Config::class, $config); + self::assertSame("default", $config->get("default")); + self::assertSame("d-M-Y", $config->get("date_format")); + self::assertSame(IMAP::FT_PEEK, $config->get("options.fetch")); + self::assertSame([], $config->get("options.open")); } /** @@ -59,12 +62,12 @@ public function testMakeClient(): void { * @throws MaskNotFoundException */ public function testAccountAccessor(): void { - self::assertSame("default", $this->cm->getDefaultAccount()); + self::assertSame("default", $this->cm->getConfig()->getDefaultAccount()); self::assertNotEmpty($this->cm->account("default")); - $this->cm->setDefaultAccount("foo"); - self::assertSame("foo", $this->cm->getDefaultAccount()); - $this->cm->setDefaultAccount("default"); + $this->cm->getConfig()->setDefaultAccount("foo"); + self::assertSame("foo", $this->cm->getConfig()->getDefaultAccount()); + $this->cm->getConfig()->setDefaultAccount("default"); } /** @@ -82,10 +85,10 @@ public function testSetConfig(): void { ]; $cm = new ClientManager($config); - self::assertSame("foo", $cm->getDefaultAccount()); + self::assertSame("foo", $cm->getConfig()->getDefaultAccount()); self::assertInstanceOf(Client::class, $cm->account("foo")); - self::assertSame(IMAP::ST_MSGN, $cm->get("options.fetch")); - self::assertSame(false, is_array($cm->get("options.open"))); + self::assertSame(IMAP::ST_MSGN, $cm->getConfig()->get("options.fetch")); + self::assertSame(false, is_array($cm->getConfig()->get("options.open"))); } } \ No newline at end of file diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 8d9f8e66..315f3c42 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Client; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Connection\Protocols\Response; use Webklex\PHPIMAP\Exceptions\AuthFailedException; @@ -42,18 +43,22 @@ class ClientTest extends TestCase { * @throws MaskNotFoundException */ public function setUp(): void { - $this->client = new Client([ - 'protocol' => 'imap', - 'encryption' => 'ssl', - 'username' => 'foo@domain.tld', - 'password' => 'bar', - 'proxy' => [ - 'socket' => null, - 'request_fulluri' => false, - 'username' => null, - 'password' => null, - ], - ]); + $config = Config::make([ + "accounts" => [ + "default" => [ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]] + ]); + $this->client = new Client($config); } /** @@ -274,18 +279,19 @@ public function testClientId(): void { } public function testClientConfig(): void { - $config = $this->client->getConfig(); + $config = $this->client->getConfig()->get("accounts.".$this->client->getConfig()->getDefaultAccount()); self::assertSame("foo@domain.tld", $config["username"]); self::assertSame("bar", $config["password"]); self::assertSame("localhost", $config["host"]); self::assertSame(true, $config["validate_cert"]); self::assertSame(993, $config["port"]); - $this->client->setConfig([ - "host" => "domain.tld", - 'password' => 'bar', - ]); - $config = $this->client->getConfig(); + $this->client->getConfig()->set("accounts.".$this->client->getConfig()->getDefaultAccount(), [ + "host" => "domain.tld", + 'password' => 'bar', + ]); + $config = $this->client->getConfig()->get("accounts.".$this->client->getConfig()->getDefaultAccount()); + self::assertSame("bar", $config["password"]); self::assertSame("domain.tld", $config["host"]); self::assertSame(true, $config["validate_cert"]); diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 7afec065..65e31d7f 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -16,12 +16,25 @@ use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Address; use Webklex\PHPIMAP\Attribute; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Header; use Webklex\PHPIMAP\IMAP; class HeaderTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * Test parsing email headers * @@ -35,7 +48,7 @@ public function testHeaderParsing(): void { $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); - $header = new Header($raw_header); + $header = new Header($raw_header, $this->config); $subject = $header->get("subject"); $returnPath = $header->get("Return-Path"); /** @var Carbon $date */ @@ -80,9 +93,9 @@ public function testRfc822ParseHeaders() { ->onlyMethods([]) ->getMock(); - $config = new \ReflectionProperty($mock, 'config'); + $config = new \ReflectionProperty($mock, 'options'); $config->setAccessible(true); - $config->setValue($mock, ['rfc822' => true]); + $config->setValue($mock, $this->config->get("options")); $mockHeader = "Content-Type: text/csv; charset=WINDOWS-1252; name*0=\"TH_Is_a_F ile name example 20221013.c\"; name*1=sv\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Disposition: attachment; filename*0=\"TH_Is_a_F ile name example 20221013.c\"; filename*1=\"sv\"\r\n"; diff --git a/tests/ImapProtocolTest.php b/tests/ImapProtocolTest.php index 4d744332..78716548 100644 --- a/tests/ImapProtocolTest.php +++ b/tests/ImapProtocolTest.php @@ -13,11 +13,24 @@ namespace Tests; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; class ImapProtocolTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * ImapProtocol test @@ -26,7 +39,7 @@ class ImapProtocolTest extends TestCase { */ public function testImapProtocol(): void { - $protocol = new ImapProtocol(false); + $protocol = new ImapProtocol($this->config, false); self::assertSame(false, $protocol->getCertValidation()); self::assertSame("", $protocol->getEncryption()); diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 3f854918..1bc9ce6f 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -18,6 +18,7 @@ use Webklex\PHPIMAP\Attachment; use Webklex\PHPIMAP\Attribute; use Webklex\PHPIMAP\Client; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Connection\Protocols\Response; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -51,21 +52,24 @@ class MessageTest extends TestCase { * Setup the test environment. * * @return void - * @throws MaskNotFoundException */ public function setUp(): void { - $this->client = new Client([ - 'protocol' => 'imap', - 'encryption' => 'ssl', - 'username' => 'foo@domain.tld', - 'password' => 'bar', - 'proxy' => [ - 'socket' => null, - 'request_fulluri' => false, - 'username' => null, - 'password' => null, - ], - ]); + $config = Config::make([ + "accounts" => [ + "default" => [ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]] + ]); + $this->client = new Client($config); } /** diff --git a/tests/PartTest.php b/tests/PartTest.php index f4653519..8543c46b 100644 --- a/tests/PartTest.php +++ b/tests/PartTest.php @@ -14,6 +14,7 @@ use Carbon\Carbon; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Header; @@ -23,6 +24,18 @@ class PartTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * Test parsing a text Part * @throws InvalidMessageDateException @@ -31,8 +44,8 @@ public function testTextPart(): void { $raw_headers = "Content-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; $raw_body = "\r\nAny updates?"; - $headers = new Header($raw_headers); - $part = new Part($raw_body, $headers, 0); + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); self::assertSame("UTF-8", $part->charset); self::assertSame("text/plain", $part->content_type); @@ -53,8 +66,8 @@ public function testHTMLPart(): void { $raw_headers = "Content-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; $raw_body = "\r\n

\r\n

Any updates?

"; - $headers = new Header($raw_headers); - $part = new Part($raw_body, $headers, 0); + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); self::assertSame("UTF-8", $part->charset); self::assertSame("text/html", $part->content_type); @@ -75,8 +88,8 @@ public function testBase64Part(): void { $raw_headers = "Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt\r\n"; $raw_body = "em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW\r\neFJjT0VSTg=="; - $headers = new Header($raw_headers); - $part = new Part($raw_body, $headers, 0); + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); self::assertSame("", $part->charset); self::assertSame("application/octet-stream", $part->content_type); diff --git a/tests/StructureTest.php b/tests/StructureTest.php index a1df098b..22d71bbf 100644 --- a/tests/StructureTest.php +++ b/tests/StructureTest.php @@ -13,6 +13,7 @@ namespace Tests; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Header; @@ -20,6 +21,18 @@ class StructureTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * Test parsing email headers * @@ -35,7 +48,7 @@ public function testStructureParsing(): void { $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); $raw_body = substr($email, strlen($raw_header)+8); - $header = new Header($raw_header); + $header = new Header($raw_header, $this->config); $structure = new Structure($raw_body, $header); self::assertSame(2, count($structure->parts)); diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index 79f42dfe..9c43ae8b 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -81,7 +81,7 @@ public function testFixture() : void { "fallback_date" => "2021-01-01 00:00:00", ], ]); - $message = $this->getFixture("date-template.eml"); + $message = $this->getFixture("date-template.eml", self::$manager->getConfig()); self::assertEquals("test", $message->subject); self::assertEquals("1.0", $message->mime_version); diff --git a/tests/fixtures/FixtureTestCase.php b/tests/fixtures/FixtureTestCase.php index 8660bedc..00f31157 100644 --- a/tests/fixtures/FixtureTestCase.php +++ b/tests/fixtures/FixtureTestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; @@ -83,9 +84,9 @@ final public function __construct(?string $name = null, array $data = [], $dataN * @throws ResponseException * @throws RuntimeException */ - final public function getFixture(string $template) : Message { + final public function getFixture(string $template, Config $config = null) : Message { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); - $message = Message::fromFile($filename); + $message = Message::fromFile($filename, $config); self::assertInstanceOf(Message::class, $message); return $message; diff --git a/tests/issues/Issue355Test.php b/tests/issues/Issue355Test.php index a61cd07a..0fa6d07e 100644 --- a/tests/issues/Issue355Test.php +++ b/tests/issues/Issue355Test.php @@ -13,6 +13,7 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Header; class Issue355Test extends TestCase { @@ -20,7 +21,7 @@ class Issue355Test extends TestCase { public function testIssue() { $raw_header = "Subject: =?UTF-8?Q?Re=3A_Uppdaterat_=C3=A4rende_=28447899=29=2C_kostnader_f=C3=B6r_hj=C3=A4?= =?UTF-8?Q?lp_med_stadge=C3=A4ndring_enligt_ny_lagstiftning?=\r\n"; - $header = new Header($raw_header); + $header = new Header($raw_header, Config::make()); $subject = $header->get("subject"); $this->assertEquals("Re: Uppdaterat ärende (447899), kostnader för hjälp med stadgeändring enligt ny lagstiftning", $subject->toString()); diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php index 0f20a396..30cb3a9b 100644 --- a/tests/issues/Issue383Test.php +++ b/tests/issues/Issue383Test.php @@ -43,7 +43,7 @@ public function testIssue(): void { $client = $this->getClient(); $client->connect(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'Entwürfe+']); $folder = $client->getFolder($folder_path); diff --git a/tests/issues/Issue393Test.php b/tests/issues/Issue393Test.php index 73099e07..017ff535 100644 --- a/tests/issues/Issue393Test.php +++ b/tests/issues/Issue393Test.php @@ -43,7 +43,7 @@ public function testIssue(): void { $client = $this->getClient(); $client->connect(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $pattern = implode($delimiter, ['doesnt_exist', '%']); $folder = $client->getFolder('doesnt_exist'); diff --git a/tests/issues/Issue412Test.php b/tests/issues/Issue412Test.php index 5d105541..bfaa883d 100644 --- a/tests/issues/Issue412Test.php +++ b/tests/issues/Issue412Test.php @@ -13,6 +13,7 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Message; class Issue412Test extends TestCase { diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php index cfdae548..2162d6b5 100644 --- a/tests/issues/Issue413Test.php +++ b/tests/issues/Issue413Test.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Tests\live\LiveMailboxTestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Folder; use Webklex\PHPIMAP\Message; diff --git a/tests/live/ClientTest.php b/tests/live/ClientTest.php index 307894a7..1d224d57 100644 --- a/tests/live/ClientTest.php +++ b/tests/live/ClientTest.php @@ -187,7 +187,7 @@ public function testOpenFolder(): void { public function testCreateFolder(): void { $client = $this->getClient()->connect(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', $this->getSpecialChars()]); $folder = $client->getFolder($folder_path); diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php index d9997d75..178329cf 100644 --- a/tests/live/FolderTest.php +++ b/tests/live/FolderTest.php @@ -79,7 +79,7 @@ public function testHasChildren(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if ($folder->getClient()->getFolder($child_path) === null) { $folder->getClient()->createFolder($child_path, false); @@ -107,7 +107,7 @@ public function testSetChildren(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if ($folder->getClient()->getFolder($child_path) === null) { $folder->getClient()->createFolder($child_path, false); @@ -137,7 +137,7 @@ public function testGetChildren(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if ($folder->getClient()->getFolder($child_path) === null) { $folder->getClient()->createFolder($child_path, false); @@ -167,7 +167,7 @@ public function testGetChildren(): void { public function testMove(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -208,7 +208,7 @@ public function testMove(): void { public function testDelete(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -439,7 +439,7 @@ public function testSetDelimiter(): void { $folder->setDelimiter("."); self::assertEquals(".", $folder->delimiter); - $default_delimiter = $this->getManager()->get("options.delimiter", "/"); + $default_delimiter = $this->getManager()->getConfig()->get("options.delimiter", "/"); $folder->setDelimiter(null); self::assertEquals($default_delimiter, $folder->delimiter); } diff --git a/tests/live/LegacyTest.php b/tests/live/LegacyTest.php index 85fa4448..61e2ee09 100644 --- a/tests/live/LegacyTest.php +++ b/tests/live/LegacyTest.php @@ -98,7 +98,7 @@ public function __construct(?string $name = null, array $data = [], int|string $ */ public function testSizes(): void { - $delimiter = ClientManager::get("options.delimiter"); + $delimiter = self::$client->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if (self::$client->getFolder($child_path) === null) { self::$client->createFolder($child_path, false); @@ -276,7 +276,7 @@ final protected function deleteFolder(Folder $folder = null): bool { * @throws MessageSearchValidationException */ public function testQueryWhere(): void { - $delimiter = ClientManager::get("options.delimiter"); + $delimiter = self::$client->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'search']); $folder = self::$client->getFolder($folder_path); @@ -431,7 +431,7 @@ protected function assertWhereSearchCriteria(Folder $folder, string $criteria, C $criteria = str_replace("CUSTOM ", "", $criteria); $expected = $value === null ? [$criteria] : [$criteria, $value]; if ($date === true && $value instanceof Carbon) { - $date_format = ClientManager::get('date_format', 'd M y'); + $date_format = $folder->getClient()->getConfig()->get('date_format', 'd M y'); $expected[1] = $value->format($date_format); } diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index 31b15579..30e953ec 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -126,7 +126,7 @@ public function testConvertEncoding(): void { public function testThread(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'thread']); $folder = $client->getFolder($folder_path); @@ -420,7 +420,7 @@ public function testSetFlag(): void { public function testGetMsgn(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -830,13 +830,13 @@ public function testGetSequenceId(): void { public function testSetConfig(): void { $message = $this->getDefaultMessage(); - $config = $message->getConfig(); - self::assertIsArray($config); + $options = $message->getOptions(); + self::assertIsArray($options); - $message->setConfig(["foo" => "bar"]); - self::assertArrayHasKey("foo", $message->getConfig()); + $message->setOptions(["foo" => "bar"]); + self::assertArrayHasKey("foo", $message->getOptions()); - $message->setConfig($config); + $message->setOptions($options); // Cleanup self::assertTrue($message->delete()); @@ -1406,7 +1406,7 @@ public function testCopy(): void { $client = $message->getClient(); self::assertInstanceOf(Client::class, $client); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -2161,7 +2161,7 @@ public function testMove(): void { $client = $message->getClient(); self::assertInstanceOf(Client::class, $client); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); diff --git a/tests/live/QueryTest.php b/tests/live/QueryTest.php index af651ef4..34cd77a4 100644 --- a/tests/live/QueryTest.php +++ b/tests/live/QueryTest.php @@ -86,7 +86,7 @@ public function testQuery(): void { public function testQueryWhere(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'search']); $folder = $client->getFolder($folder_path); @@ -239,7 +239,7 @@ protected function assertWhereSearchCriteria(Folder $folder, string $criteria, C $criteria = str_replace("CUSTOM ", "", $criteria); $expected = $value === null ? [$criteria] : [$criteria, $value]; if ($date === true && $value instanceof Carbon) { - $date_format = ClientManager::get('date_format', 'd M y'); + $date_format = $folder->getClient()->getConfig()->get('date_format', 'd M y'); $expected[1] = $value->format($date_format); } From 777c6d87a57f4302ef53d2c05305712c758506ce Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:24:15 +0200 Subject: [PATCH 549/600] Sponsor list extended --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c110d48f..2eb4ef40 100755 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ foreach($folders as $folder){ ``` ## Sponsors +[![elb-BIT][ico-sponsor-elb-bit]][link-sponsor-elb-bit] [![Feline][ico-sponsor-feline]][link-sponsor-feline] @@ -229,4 +230,6 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png -[link-sponsor-feline]: https://www.feline.dk \ No newline at end of file +[link-sponsor-feline]: https://www.feline.dk +[ico-sponsor-elb-bit]: https://www.elb-bit.de/user/themes/deliver/images/logo_small.png +[link-sponsor-elb-bit]: https://www.elb-bit.de?ref=webklex/php-imap \ No newline at end of file From 8696b28ba0899185f1250b54d10254f54afcb5bb Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:26:12 +0200 Subject: [PATCH 550/600] Support for Carbon 3 added #483 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 37efc1c5..4956e549 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-libxml": "*", "ext-zip": "*", "ext-fileinfo": "*", - "nesbot/carbon": "^2.62.1", + "nesbot/carbon": "^2.62.1|^3.2.4", "symfony/http-foundation": ">=2.8.0", "illuminate/pagination": ">=5.0.0" }, From 6d999438d29ed0bb920cd897b200a3a5fd6b6380 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:26:41 +0200 Subject: [PATCH 551/600] Changelog updated --- CHANGELOG.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cf049f..edeea50d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,21 +6,43 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) +- Make the space optional after a comma separator #437 (thanks @marc0adam) +- Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) +- Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) +- Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) +- Add attributes and special flags #428 (thanks @sazanof) +- Better connection check for IMAP #449 (thanks @thin-k-design) +- Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) +- Support for Carbon 3 added #483 ### Breaking changes - `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. - +- `ClientManager::class` has now longer access to all configs. Config handling has been moved to its own class `Config::class`. If you want to access the config you can use the retriever method `::getConfig()` instead. Example: `$client->getConfig()` or `$message->getConfig()`, etc. +- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()` +- `M̀essage::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$message->getOptions()` instead. +- `Attachment::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$attachment->getOptions()` instead. +- `Header::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$header->getOptions()` instead. +- `M̀essage::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$message->setOptions` instead. +- `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. +- `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. +- All protocol constructors now require a `Config::class` instance +- The `Client::class` constructors now require a `Config::class` instance +- The `Part::class` constructors now require a `Config::class` instance +- The `Header::class` constructors now require a `Config::class` instance +- The `Message::fromFile` method now requires a `Config::class` instance +- The `Message::fromString` method now requires a `Config::class` instance +- The `Message::boot` method now requires a `Config::class` instance ## [5.5.0] - 2023-06-28 ### Fixed - Error token length mismatch in `ImapProtocol::readResponse` #400 - Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) - Additional Attachment name fallback added to prevent missing attachments -- Attachment id is now static (based on the raw part content) and now longer random +- Attachment id is now static (based on the raw part content) instead of random - Always parse the attachment description if it is available ### Added From a1b29eb354343867fb515c5821b4fa917592c726 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 01:19:05 +0100 Subject: [PATCH 552/600] Custom decoder support added --- CHANGELOG.md | 16 +- src/Attachment.php | 39 ++++- src/Config.php | 29 ++++ src/Decoder/AttachmentDecoder.php | 25 +++ src/Decoder/Decoder.php | 161 ++++++++++++++++++ src/Decoder/DecoderInterface.php | 52 ++++++ src/Decoder/HeaderDecoder.php | 98 +++++++++++ src/Decoder/MessageDecoder.php | 119 +++++++++++++ src/Exceptions/DecoderNotFoundException.php | 24 +++ src/Header.php | 177 ++++---------------- src/Message.php | 134 ++++----------- src/config/imap.php | 32 +++- tests/live/MessageTest.php | 4 +- 13 files changed, 648 insertions(+), 262 deletions(-) create mode 100644 src/Decoder/AttachmentDecoder.php create mode 100644 src/Decoder/Decoder.php create mode 100644 src/Decoder/DecoderInterface.php create mode 100644 src/Decoder/HeaderDecoder.php create mode 100644 src/Decoder/MessageDecoder.php create mode 100644 src/Exceptions/DecoderNotFoundException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index edeea50d..f97aa391 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,24 +18,30 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Better connection check for IMAP #449 (thanks @thin-k-design) - Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) - Support for Carbon 3 added #483 +- Custom decoder support added ### Breaking changes +- The decoder config has been moved from `options.decoder` to `decoding` and contains now the `decoder` class to used as well as their decoding fallbacks - `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. - `ClientManager::class` has now longer access to all configs. Config handling has been moved to its own class `Config::class`. If you want to access the config you can use the retriever method `::getConfig()` instead. Example: `$client->getConfig()` or `$message->getConfig()`, etc. -- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()` +- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()->get($key)` - `M̀essage::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$message->getOptions()` instead. - `Attachment::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$attachment->getOptions()` instead. - `Header::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$header->getOptions()` instead. - `M̀essage::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$message->setOptions` instead. - `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. - `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. -- All protocol constructors now require a `Config::class` instance -- The `Client::class` constructors now require a `Config::class` instance -- The `Part::class` constructors now require a `Config::class` instance -- The `Header::class` constructors now require a `Config::class` instance +- All protocol constructors now require a `Config::class` instance +- The `Client::class` constructor now require a `Config::class` instance +- The `Part::class` constructor now require a `Config::class` instance +- The `Header::class` constructor now require a `Config::class` instance - The `Message::fromFile` method now requires a `Config::class` instance - The `Message::fromString` method now requires a `Config::class` instance - The `Message::boot` method now requires a `Config::class` instance +- The `Message::decode` method has been removed. Use `Message::getDecoder()->decode($str)` instead. +- The `Message::getEncoding` method has been removed. Use `Message::getDecoder()->getEncoding($str)` instead. +- The `Message::convertEncoding` method has been removed. Use `Message::getDecoder()->convertEncoding()` instead. +- The `Header::decode` method has been removed. Use `Header::getDecoder()->decode($str)` instead. ## [5.5.0] - 2023-06-28 ### Fixed diff --git a/src/Attachment.php b/src/Attachment.php index 451dfe26..bcbe91c5 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -13,6 +13,8 @@ namespace Webklex\PHPIMAP; use Illuminate\Support\Str; +use Webklex\PHPIMAP\Decoder\DecoderInterface; +use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; @@ -78,6 +80,13 @@ class Attachment { /** @var Part $part */ protected Part $part; + /** + * Decoder instance + * + * @var DecoderInterface $decoder + */ + protected DecoderInterface $decoder; + /** * Attribute holder * @@ -109,11 +118,13 @@ class Attachment { * Attachment constructor. * @param Message $message * @param Part $part + * @throws DecoderNotFoundException */ public function __construct(Message $message, Part $part) { $this->message = $message; $this->config = $this->message->getConfig(); $this->options = $this->config->get('options'); + $this->decoder = $this->config->getDecoder("attachment"); $this->part = $part; $this->part_number = $part->part_number; @@ -213,7 +224,7 @@ protected function fetch(): void { $content = $this->part->content; $this->content_type = $this->part->content_type; - $this->content = $this->message->decodeString($content, $this->part->encoding); + $this->content = $this->decoder->decode($content, $this->part->encoding); // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, // it is not guaranteed to be unique and collisions are possible. @@ -245,7 +256,7 @@ protected function fetch(): void { } if (($description = $this->part->description) !== null) { - $this->description = $this->part->getHeader()->decode($description); + $this->description = $this->part->getHeader()->getDecoder()->decode($description); } if (($name = $this->part->name) !== null) { @@ -300,9 +311,9 @@ public function decodeName(?string $name): string { } } - $decoder = $this->options['decoder']['message']; + $decoder = $this->decoder->getOptions()['message']; if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { - $name = $this->part->getHeader()->decode($name); + $name = $this->part->getHeader()->getDecoder()->decode($name); } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { $name = \imap_utf8($name); } @@ -452,4 +463,24 @@ public function mask(string $mask = null): mixed { throw new MaskNotFoundException("Unknown mask provided: " . $mask); } + + /** + * Get the decoder instance + * + * @return DecoderInterface + */ + public function getDecoder(): DecoderInterface { + return $this->decoder; + } + + /** + * Set the decoder instance + * @param DecoderInterface $decoder + * + * @return $this + */ + public function setDecoder(DecoderInterface $decoder): static { + $this->decoder = $decoder; + return $this; + } } diff --git a/src/Config.php b/src/Config.php index b5cf2599..f2f1a174 100644 --- a/src/Config.php +++ b/src/Config.php @@ -12,6 +12,9 @@ namespace Webklex\PHPIMAP; +use Webklex\PHPIMAP\Decoder\DecoderInterface; +use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException; + /** * Class Config * @@ -87,6 +90,32 @@ public function set(string $key, mixed $value): void { } } + /** + * Get the decoder for a given name + * @param $name string Decoder name + * + * @return DecoderInterface + * @throws DecoderNotFoundException + */ + public function getDecoder(string $name): DecoderInterface { + $default_decoders = $this->get('decoding.decoder', [ + 'header' => \Webklex\PHPIMAP\Decoder\HeaderDecoder::class, + 'message' => \Webklex\PHPIMAP\Decoder\MessageDecoder::class, + 'attachment' => \Webklex\PHPIMAP\Decoder\AttachmentDecoder::class + ]); + $options = $this->get('decoding.options', [ + 'header' => 'utf-8', + 'message' => 'utf-8', + 'attachment' => 'utf-8', + ]); + if (isset($default_decoders[$name])) { + if (class_exists($default_decoders[$name])) { + return new $default_decoders[$name]($options); + } + } + throw new DecoderNotFoundException(); + } + /** * Get the mask for a given section * @param string $section section name such as "message" or "attachment" diff --git a/src/Decoder/AttachmentDecoder.php b/src/Decoder/AttachmentDecoder.php new file mode 100644 index 00000000..22780353 --- /dev/null +++ b/src/Decoder/AttachmentDecoder.php @@ -0,0 +1,25 @@ +options = array_merge([ + 'header' => 'utf-8', + 'message' => 'utf-8', + 'attachment' => 'utf-8', + ], $this->options); + } + + /** + * Decode a given value + * @param array|string|null $value + * @param string|null $encoding + * @return mixed + */ + public function decode(array|string|null $value, string $encoding = null): mixed { + return $value; + } + + /** + * Convert the encoding + * @param string $str The string to convert + * @param string $from The source encoding + * @param string $to The target encoding + * + * @return mixed|string + */ + public function convertEncoding(string $str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { + $from = EncodingAliases::get($from, $this->fallback_encoding); + $to = EncodingAliases::get($to, $this->fallback_encoding); + + if ($from === $to) { + return $str; + } + + return EncodingAliases::convert($str, $from, $to); + } + + /** + * Decode MIME header elements + * @link https://php.net/manual/en/function.imap-mime-header-decode.php + * @param string $text The MIME text + * + * @return array Returns an array of objects. Each *object has two properties, charset and text. + */ + public function mimeHeaderDecode(string $text): array { + if (extension_loaded('imap')) { + $result = \imap_mime_header_decode($text); + return is_array($result) ? $result : []; + } + $charset = $this->getEncoding($text); + return [(object)[ + "charset" => $charset, + "text" => $this->convertEncoding($text, $charset) + ]]; + } + + /** + * Test if a given value is utf-8 encoded + * @param $value + * + * @return bool + */ + public static function isUTF8($value): bool { + return str_starts_with(strtolower($value), '=?utf-8?'); + } + + /** + * Check if a given pair of strings has been decoded + * @param $encoded + * @param $decoded + * + * @return bool + */ + public static function notDecoded($encoded, $decoded): bool { + return str_starts_with($decoded, '=?') + && strlen($decoded) - 2 === strpos($decoded, '?=') + && str_contains($encoded, $decoded); + } + + /** + * Set the configuration used for decoding + * @param array $config + * + * @return Decoder + */ + public function setOptions(array $config): static { + $this->options = $config; + return $this; + } + + /** + * Get the configuration used for decoding + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Get the fallback encoding + * + * @return string + */ + public function getFallbackEncoding(): string { + return $this->fallback_encoding; + } + + /** + * Set the fallback encoding + * + * @param string $fallback_encoding + * @return Decoder + */ + public function setFallbackEncoding(string $fallback_encoding): static { + $this->fallback_encoding = $fallback_encoding; + return $this; + } +} \ No newline at end of file diff --git a/src/Decoder/DecoderInterface.php b/src/Decoder/DecoderInterface.php new file mode 100644 index 00000000..c44f2118 --- /dev/null +++ b/src/Decoder/DecoderInterface.php @@ -0,0 +1,52 @@ +decodeHeaderArray($value); + } + $original_value = $value; + $decoder = $this->options['header']; + + if ($value !== null) { + if ($decoder === 'utf-8') { + $decoded_values = $this->mimeHeaderDecode($value); + $tempValue = ""; + foreach ($decoded_values as $decoded_value) { + $tempValue .= $this->convertEncoding($decoded_value->text, $decoded_value->charset); + } + if ($tempValue) { + $value = $tempValue; + } else if (extension_loaded('imap')) { + $value = \imap_utf8($value); + } else if (function_exists('iconv_mime_decode')) { + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + } else { + $value = mb_decode_mimeheader($value); + } + } elseif ($decoder === 'iconv') { + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + } else if (self::isUTF8($value)) { + $value = mb_decode_mimeheader($value); + } + + if (self::notDecoded($original_value, $value)) { + $value = $this->convertEncoding($original_value, $this->getEncoding($original_value)); + } + } + + return $value; + } + + /** + * Get the encoding of a given abject + * @param object|string $structure + * + * @return string + */ + public function getEncoding(object|string $structure): string { + if (property_exists($structure, 'parameters')) { + foreach ($structure->parameters as $parameter) { + if (strtolower($parameter->attribute) == "charset") { + return EncodingAliases::get($parameter->value, $this->fallback_encoding); + } + } + } elseif (property_exists($structure, 'charset')) { + return EncodingAliases::get($structure->charset, $this->fallback_encoding); + } elseif (is_string($structure) === true) { + $result = mb_detect_encoding($structure); + return $result === false ? $this->fallback_encoding : $result; + } + + return $this->fallback_encoding; + } + + + /** + * Decode a given array + * @param array $values + * + * @return array + */ + private function decodeHeaderArray(array $values): array { + foreach ($values as $key => $value) { + $values[$key] = $this->decode($value); + } + return $values; + } + +} \ No newline at end of file diff --git a/src/Decoder/MessageDecoder.php b/src/Decoder/MessageDecoder.php new file mode 100644 index 00000000..5d4178bc --- /dev/null +++ b/src/Decoder/MessageDecoder.php @@ -0,0 +1,119 @@ +decode($item); + }, $value); + } + + switch ($encoding) { + case IMAP::MESSAGE_ENC_BINARY: + if (extension_loaded('imap')) { + return base64_decode(\imap_binary($value)); + } + return base64_decode($value); + case IMAP::MESSAGE_ENC_BASE64: + return base64_decode($value); + case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE: + return quoted_printable_decode($value); + case IMAP::MESSAGE_ENC_8BIT: + case IMAP::MESSAGE_ENC_7BIT: + case IMAP::MESSAGE_ENC_OTHER: + default: + return $value; + } + } + + /** + * Get the encoding of a given abject + * @param object|string $structure + * + * @return string + */ + public function getEncoding(object|string $structure): string { + if (property_exists($structure, 'parameters')) { + foreach ($structure->parameters as $parameter) { + if (strtolower($parameter->attribute) == "charset") { + return EncodingAliases::get($parameter->value, "ISO-8859-2"); + } + } + } elseif (property_exists($structure, 'charset')) { + return EncodingAliases::get($structure->charset, "ISO-8859-2"); + } elseif (is_string($structure) === true) { + return EncodingAliases::detectEncoding($structure); + } + + return $this->fallback_encoding; + } + + + /** + * Convert the encoding + * @param $str + * @param string $from + * @param string $to + * + * @return mixed|string + */ + public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { + $from = EncodingAliases::get($from); + $to = EncodingAliases::get($to); + + if ($from === $to) { + return $str; + } + + // We don't need to do convertEncoding() if charset is ASCII (us-ascii): + // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded + // https://stackoverflow.com/a/11303410 + // + // us-ascii is the same as ASCII: + // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) + // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and + // based on the typographical symbols predominantly in use there. + // https://en.wikipedia.org/wiki/ASCII + // + // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. + if (strtolower($from ?? '') == 'us-ascii' && $to == 'UTF-8') { + return $str; + } + + if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { + try { + return iconv($from, $to.'//IGNORE', $str); + } catch (Exception) { + return @iconv($from, $to, $str); + } + } else { + if (!$from) { + return mb_convert_encoding($str, $to); + } + return mb_convert_encoding($str, $to, $from); + } + } + +} \ No newline at end of file diff --git a/src/Exceptions/DecoderNotFoundException.php b/src/Exceptions/DecoderNotFoundException.php new file mode 100644 index 00000000..0acdb9c8 --- /dev/null +++ b/src/Exceptions/DecoderNotFoundException.php @@ -0,0 +1,24 @@ +decoder = $config->getDecoder("header"); $this->raw = $raw_header; $this->config = $config; $this->options = $this->config->get('options'); @@ -202,7 +206,7 @@ protected function parse(): void { $this->extractAddresses($header); if (property_exists($header, 'subject')) { - $this->set("subject", $this->decode($header->subject)); + $this->set("subject", $this->decoder->decode($header->subject)); } if (property_exists($header, 'references')) { $this->set("references", array_map(function($item) { @@ -331,147 +335,6 @@ public function rfc822_parse_headers($raw_headers): object { return (object)array_merge($headers, $imap_headers); } - /** - * Decode MIME header elements - * @link https://php.net/manual/en/function.imap-mime-header-decode.php - * @param string $text The MIME text - * - * @return array The decoded elements are returned in an array of objects, where each - * object has two properties, charset and text. - */ - public function mime_header_decode(string $text): array { - if (extension_loaded('imap')) { - $result = \imap_mime_header_decode($text); - return is_array($result) ? $result : []; - } - $charset = $this->getEncoding($text); - return [(object)[ - "charset" => $charset, - "text" => $this->convertEncoding($text, $charset) - ]]; - } - - /** - * Check if a given pair of strings has been decoded - * @param $encoded - * @param $decoded - * - * @return bool - */ - private function notDecoded($encoded, $decoded): bool { - return str_starts_with($decoded, '=?') - && strlen($decoded) - 2 === strpos($decoded, '?=') - && str_contains($encoded, $decoded); - } - - /** - * Convert the encoding - * @param $str - * @param string $from - * @param string $to - * - * @return mixed|string - */ - public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { - $from = EncodingAliases::get($from, $this->fallback_encoding); - $to = EncodingAliases::get($to, $this->fallback_encoding); - - if ($from === $to) { - return $str; - } - - return EncodingAliases::convert($str, $from, $to); - } - - /** - * Get the encoding of a given abject - * @param object|string $structure - * - * @return string - */ - public function getEncoding(object|string $structure): string { - if (property_exists($structure, 'parameters')) { - foreach ($structure->parameters as $parameter) { - if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value, $this->fallback_encoding); - } - } - } elseif (property_exists($structure, 'charset')) { - return EncodingAliases::get($structure->charset, $this->fallback_encoding); - } elseif (is_string($structure) === true) { - $result = mb_detect_encoding($structure); - return $result === false ? $this->fallback_encoding : $result; - } - - return $this->fallback_encoding; - } - - /** - * Test if a given value is utf-8 encoded - * @param $value - * - * @return bool - */ - private function is_uft8($value): bool { - return str_starts_with(strtolower($value), '=?utf-8?'); - } - - /** - * Try to decode a specific header - * @param mixed $value - * - * @return mixed - */ - public function decode(mixed $value): mixed { - if (is_array($value)) { - return $this->decodeArray($value); - } - $original_value = $value; - $decoder = $this->options['decoder']['message']; - - if ($value !== null) { - if ($decoder === 'utf-8') { - $decoded_values = $this->mime_header_decode($value); - $tempValue = ""; - foreach ($decoded_values as $decoded_value) { - $tempValue .= $this->convertEncoding($decoded_value->text, $decoded_value->charset); - } - if ($tempValue) { - $value = $tempValue; - } else if (extension_loaded('imap')) { - $value = \imap_utf8($value); - } else if (function_exists('iconv_mime_decode')) { - $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - } else { - $value = mb_decode_mimeheader($value); - } - } elseif ($decoder === 'iconv') { - $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - } else if ($this->is_uft8($value)) { - $value = mb_decode_mimeheader($value); - } - - if ($this->notDecoded($original_value, $value)) { - $value = $this->convertEncoding($original_value, $this->getEncoding($original_value)); - } - } - - return $value; - } - - /** - * Decode a given array - * @param array $values - * - * @return array - */ - private function decodeArray(array $values): array { - foreach ($values as $key => $value) { - $values[$key] = $this->decode($value); - } - return $values; - } - /** * Try to extract the priority from a given raw header string */ @@ -582,11 +445,11 @@ private function parseAddresses($list): array { if (!property_exists($address, 'personal')) { $address->personal = false; } else { - $personalParts = $this->mime_header_decode($address->personal); + $personalParts = $this->decoder->mimeHeaderDecode($address->personal); $address->personal = ''; foreach ($personalParts as $p) { - $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); + $address->personal .= $this->decoder->convertEncoding($p->text, $this->decoder->getEncoding($p)); } if (str_starts_with($address->personal, "'")) { @@ -843,4 +706,24 @@ public function getConfig(): Config { return $this->config; } + /** + * Get the decoder instance + * + * @return DecoderInterface + */ + public function getDecoder(): DecoderInterface { + return $this->decoder; + } + + /** + * Set the decoder instance + * @param DecoderInterface $decoder + * + * @return $this + */ + public function setDecoder(DecoderInterface $decoder): static { + $this->decoder = $decoder; + return $this; + } + } diff --git a/src/Message.php b/src/Message.php index 10b96018..bae2acfe 100755 --- a/src/Message.php +++ b/src/Message.php @@ -13,10 +13,14 @@ namespace Webklex\PHPIMAP; use Exception; +use Illuminate\Support\Str; use ReflectionClass; use ReflectionException; +use Webklex\PHPIMAP\Decoder\Decoder; +use Webklex\PHPIMAP\Decoder\DecoderInterface; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\FolderFetchingException; use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; @@ -35,7 +39,6 @@ use Webklex\PHPIMAP\Support\AttachmentCollection; use Webklex\PHPIMAP\Support\FlagCollection; use Webklex\PHPIMAP\Support\Masks\MessageMask; -use Illuminate\Support\Str; use Webklex\PHPIMAP\Support\MessageCollection; use Webklex\PHPIMAP\Traits\HasEvents; @@ -111,6 +114,13 @@ class Message { */ protected Config $config; + /** + * Decoder instance + * + * @var DecoderInterface $decoder + */ + protected DecoderInterface $decoder; + /** * Attribute holder * @@ -374,11 +384,13 @@ public static function fromString(string $blob, Config $config = null): Message /** * Boot a new instance * @param ?Config $config + * @throws DecoderNotFoundException */ public function boot(Config $config = null): void { $this->attributes = []; $this->client = null; $this->config = $config ?? Config::make(); + $this->decoder = $this->config->getDecoder("message"); $this->options = $this->config->get('options'); $this->available_flags = $this->config->get('flags'); @@ -732,9 +744,9 @@ private function fetchPart(Part $part): void { if ($part->isAttachment()) { $this->fetchAttachment($part); } else { - $encoding = $this->getEncoding($part); + $encoding = $this->decoder->getEncoding($part); - $content = $this->decodeString($part->content, $part->encoding); + $content = $this->decoder->decode($part->content, $part->encoding); // We don't need to do convertEncoding() if charset is ASCII (us-ascii): // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded @@ -748,7 +760,7 @@ private function fetchPart(Part $part): void { // // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. if ($encoding != 'us-ascii') { - $content = $this->convertEncoding($content, $encoding); + $content = $this->decoder->convertEncoding($content, $encoding); } $this->addBody($part->subtype ?? '', $content); @@ -859,100 +871,6 @@ public function setFetchFlagsOption($option): Message { return $this; } - /** - * Decode a given string - * @param $string - * @param $encoding - * - * @return string - */ - public function decodeString($string, $encoding): string { - switch ($encoding) { - case IMAP::MESSAGE_ENC_BINARY: - if (extension_loaded('imap')) { - return base64_decode(\imap_binary($string)); - } - return base64_decode($string); - case IMAP::MESSAGE_ENC_BASE64: - return base64_decode($string); - case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE: - return quoted_printable_decode($string); - case IMAP::MESSAGE_ENC_8BIT: - case IMAP::MESSAGE_ENC_7BIT: - case IMAP::MESSAGE_ENC_OTHER: - default: - return $string; - } - } - - /** - * Convert the encoding - * @param $str - * @param string $from - * @param string $to - * - * @return mixed|string - */ - public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { - - $from = EncodingAliases::get($from); - $to = EncodingAliases::get($to); - - if ($from === $to) { - return $str; - } - - // We don't need to do convertEncoding() if charset is ASCII (us-ascii): - // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded - // https://stackoverflow.com/a/11303410 - // - // us-ascii is the same as ASCII: - // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) - // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and - // based on the typographical symbols predominantly in use there. - // https://en.wikipedia.org/wiki/ASCII - // - // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. - if (strtolower($from ?? '') == 'us-ascii' && $to == 'UTF-8') { - return $str; - } - - if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { - try { - return iconv($from, $to.'//IGNORE', $str); - } catch (Exception) { - return @iconv($from, $to, $str); - } - } else { - if (!$from) { - return mb_convert_encoding($str, $to); - } - return mb_convert_encoding($str, $to, $from); - } - } - - /** - * Get the encoding of a given abject - * @param object|string $structure - * - * @return string - */ - public function getEncoding(object|string $structure): string { - if (property_exists($structure, 'parameters')) { - foreach ($structure->parameters as $parameter) { - if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value, "ISO-8859-2"); - } - } - } elseif (property_exists($structure, 'charset')) { - return EncodingAliases::get($structure->charset, "ISO-8859-2"); - } elseif (is_string($structure) === true) { - return EncodingAliases::detectEncoding($structure); - } - - return 'UTF-8'; - } - /** * Get the messages folder * @@ -1727,6 +1645,26 @@ public function setSequenceId($uid, int $msglist = null): void { } } + /** + * Get the decoder instance + * + * @return DecoderInterface + */ + public function getDecoder(): DecoderInterface { + return $this->decoder; + } + + /** + * Set the decoder instance + * @param DecoderInterface $decoder + * + * @return $this + */ + public function setDecoder(DecoderInterface $decoder): static { + $this->decoder = $decoder; + return $this; + } + /** * Safe the entire message in a file * @param $filename diff --git a/src/config/imap.php b/src/config/imap.php index 590d27cb..7c184435 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -136,9 +136,6 @@ | error: "Kerberos error: No credentials cache | file found (try running kinit) (...)" | or ['GSSAPI','PLAIN'] if you are using outlook mail - | -Decoder options (currently only the message subject and attachment name decoder can be set) - | 'utf-8' - Uses imap_utf8($string) to decode a string - | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string | */ 'options' => [ @@ -163,12 +160,35 @@ "sent" => "INBOX/Sent", "trash" => "INBOX/Trash", ], - 'decoder' => [ + 'open' => [ + // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' + ] + ], + + /** + * |-------------------------------------------------------------------------- + * | Available IMAP options + * |-------------------------------------------------------------------------- + * | + * | Available php imap config parameters are listed below + * | -options: Decoder options (currently only the message subject and attachment name decoder can be set) + * | 'utf-8' - Uses imap_utf8($string) to decode a string + * | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string + * | -decoder: Decoder to be used. Can be replaced by custom decoders if needed. + * | 'header' - HeaderDecoder + * | 'message' - MessageDecoder + * | 'attachment' - AttachmentDecoder + */ + 'decoding' => [ + 'options' => [ + 'header' => 'utf-8', // mimeheader 'message' => 'utf-8', // mimeheader 'attachment' => 'utf-8' // mimeheader ], - 'open' => [ - // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' + 'decoder' => [ + 'header' => \Webklex\PHPIMAP\Decoder\HeaderDecoder::class, + 'message' => \Webklex\PHPIMAP\Decoder\MessageDecoder::class, + 'attachment' => \Webklex\PHPIMAP\Decoder\AttachmentDecoder::class ] ], diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index 30e953ec..841b125b 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -97,7 +97,7 @@ protected function getDefaultMessage() : Message { */ public function testConvertEncoding(): void { $message = $this->getDefaultMessage(); - self::assertEquals("Entwürfe+", $message->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8")); + self::assertEquals("Entwürfe+", $message->getDecoder()->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8")); // Cleanup self::assertTrue($message->delete()); @@ -961,7 +961,7 @@ public function testDecodeString(): void { $message = $this->getDefaultMessage(); $string = '

Test

'; - self::assertEquals('

Test

', $message->decodeString($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); + self::assertEquals('

Test

', $message->getDecoder()->decode($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); // Cleanup self::assertTrue($message->delete()); From 72d5812839cff2050159ec2144c9d3e3946a8243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 17 Jan 2025 01:31:31 +0100 Subject: [PATCH 553/600] Failing test: Attachment with symbols in filename (#436) * add failing test with symbols in filename * use proper test case class --- tests/issues/Issue410Test.php | 22 +++++++++++++++++----- tests/messages/issue-410symbols.eml | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/messages/issue-410symbols.eml diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index d02724ca..458c8a13 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -13,14 +13,14 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Tests\fixtures\FixtureTestCase; use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Message; -class Issue410Test extends TestCase { +class Issue410Test extends FixtureTestCase { public function testIssueEmail() { - $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410.eml"]); - $message = Message::fromFile($filename); + $message = $this->getFixture("issue-410.eml"); self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", (string)$message->subject); @@ -34,8 +34,7 @@ public function testIssueEmail() { } public function testIssueEmailB() { - $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410b.eml"]); - $message = Message::fromFile($filename); + $message = $this->getFixture("issue-410b.eml"); self::assertSame("386 - 400021804 - 19., Heiligenstädter Straße 80 - 0819306 - Anfrage Vergabevorschlag", (string)$message->subject); @@ -49,4 +48,17 @@ public function testIssueEmailB() { self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); } + public function testIssueEmailSymbols() { + $message = $this->getFixture("issue-410symbols.eml"); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->description); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->filename); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->name); + } + } \ No newline at end of file diff --git a/tests/messages/issue-410symbols.eml b/tests/messages/issue-410symbols.eml new file mode 100644 index 00000000..b83a2f26 --- /dev/null +++ b/tests/messages/issue-410symbols.eml @@ -0,0 +1,22 @@ +From: from@there.com +To: to@here.com +Subject: =?iso-8859-1?Q?386_-_400021804_-_19.,_Heiligenst=E4dter_Stra=DFe_80_-_081?= + =?iso-8859-1?Q?9306_-_Anfrage_Vergabevorschlag?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Type: application/pdf; name="Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf" +Content-Description: Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf +Content-Disposition: attachment; + filename="Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf"; size=3439313; + creation-date="Tue, 12 Sep 2023 06:53:03 GMT"; + modification-date="Tue, 12 Sep 2023 08:18:16 GMT" +Content-ID: <34A0EDD24A954140A472605B7526F190@there.com> +Content-Transfer-Encoding: base64 + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file From e5ad66267382f319f385131cefe5336692a54486 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 03:16:54 +0100 Subject: [PATCH 554/600] fixed Attachment with symbols in filename #436 --- CHANGELOG.md | 1 + src/Header.php | 132 +++++++++++------- tests/HeaderTest.php | 11 +- tests/MessageTest.php | 6 +- tests/fixtures/EmbeddedEmailTest.php | 1 - ...lWithoutContentDispositionEmbeddedTest.php | 1 - ...ddedEmailWithoutContentDispositionTest.php | 1 - tests/fixtures/ExampleBounceTest.php | 2 - tests/fixtures/MultipartWithoutBodyTest.php | 1 - tests/fixtures/UndefinedCharsetHeaderTest.php | 1 - tests/issues/Issue410Test.php | 49 ++++++- 11 files changed, 144 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97aa391..b006fb36 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) - Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) - Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) +- Attachment with symbols in filename #436 ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) diff --git a/src/Header.php b/src/Header.php index ef1a348c..d6392906 100644 --- a/src/Header.php +++ b/src/Header.php @@ -484,61 +484,95 @@ private function extractHeaderExtensions(): void { $value = (string)$value; } // Only parse strings and don't parse any attributes like the user-agent - if (!in_array($key, ["user-agent", "subject"])) { - if (($pos = strpos($value, ";")) !== false) { - $original = substr($value, 0, $pos); - $this->set($key, trim(rtrim($original))); - - // Get all potential extensions - $extensions = explode(";", substr($value, $pos + 1)); - $previousKey = null; - $previousValue = ''; - - foreach ($extensions as $extension) { - if (($pos = strpos($extension, "=")) !== false) { - $key = substr($extension, 0, $pos); - $key = trim(rtrim(strtolower($key))); - - $matches = []; - - if (preg_match('/^(?P\w+)\*/', $key, $matches) !== 0) { - $key = $matches['key_name']; - $previousKey = $key; - - $value = substr($extension, $pos + 1); - $value = str_replace('"', "", $value); - $previousValue .= trim(rtrim($value)); - - continue; - } - - if ( - $previousKey !== null - && $previousKey !== $key - && isset($this->attributes[$previousKey]) === false - ) { - $this->set($previousKey, $previousValue); - - $previousValue = ''; - } - - if (isset($this->attributes[$key]) === false) { - $value = substr($extension, $pos + 1); - $value = str_replace('"', "", $value); - $value = trim(rtrim($value)); - - $this->set($key, $value); - } - - $previousKey = $key; + if (!in_array($key, ["user-agent", "subject", "received"])) { + if (str_contains($value, ";") && str_contains($value, "=")) { + $_attributes = $this->read_attribute($value); + foreach($_attributes as $_key => $_value) { + if($_value === "") { + $this->set($key, $_key); + } + if (!isset($this->attributes[$_key])) { + $this->set($_key, $_value); } } - if ($previousValue !== '') { - $this->set($previousKey, $previousValue); + } + } + } + } + + /** + * Read a given attribute string + * - this isn't pretty, but it works - feel free to improve :) + * @param string $raw_attribute + * @return array + */ + private function read_attribute(string $raw_attribute): array { + $attributes = []; + $key = ''; + $value = ''; + $inside_word = false; + $inside_key = true; + $escaped = false; + foreach (str_split($raw_attribute) as $char) { + if($escaped) { + $escaped = false; + continue; + } + if($inside_word) { + if($char === '\\') { + $escaped = true; + }elseif($char === "\"" && $value !== "") { + $inside_word = false; + }else{ + $value .= $char; + } + }else{ + if($inside_key) { + if($char === '"') { + $inside_word = true; + }elseif($char === ';'){ + $attributes[$key] = $value; + $key = ''; + $value = ''; + $inside_key = true; + }elseif($char === '=') { + $inside_key = false; + }else{ + $key .= $char; + } + }else{ + if($char === '"' && $value === "") { + $inside_word = true; + }elseif($char === ';'){ + $attributes[$key] = $value; + $key = ''; + $value = ''; + $inside_key = true; + }else{ + $value .= $char; } } } } + $attributes[$key] = $value; + $result = []; + + foreach($attributes as $key => $value) { + if (($pos = strpos($key, "*")) !== false) { + $key = substr($key, 0, $pos); + } + $key = trim(rtrim(strtolower($key))); + + if(!isset($result[$key])) { + $result[$key] = ""; + } + $value = trim(rtrim(str_replace(["\r", "\n"], "", $value))); + if(str_starts_with($value, "\"") && str_ends_with($value, "\"")) { + $value = substr($value, 1, -1); + } + $result[$key] .= $value; + } + return $result; } /** diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 65e31d7f..9de25004 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -59,6 +59,13 @@ public function testHeaderParsing(): void { $to = $header->get("to")->first(); self::assertSame($raw_header, $header->raw); + self::assertSame([ + 0 => 'from mx.domain.tld by localhost with LMTP id SABVMNfGqWP+PAAA0J78UA (envelope-from ) for ; Mon, 26 Dec 2022 17:07:51 +0100', + 1 => 'from localhost (localhost [127.0.0.1]) by mx.domain.tld (Postfix) with ESMTP id C3828140227 for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET)', + 2 => 'from mx.domain.tld ([127.0.0.1]) by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id JcIS9RuNBTNx for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET)', + 3 => 'from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET)', + 4 => 'from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) by smtp.github.com (Postfix) with ESMTP id 176985E0200 for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST)', + ], $header->get("received")->toArray()); self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$header->subject); @@ -66,7 +73,7 @@ public function testHeaderParsing(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$header->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$header->get("Message-ID")); - self::assertSame(6, $header->get("received")->count()); + self::assertSame(5, $header->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$header->get("priority")()); self::assertSame("Username", $from->personal); @@ -84,7 +91,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Carbon::class, $date); self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); - self::assertSame(48, count($header->getAttributes())); + self::assertSame(50, count($header->getAttributes())); } public function testRfc822ParseHeaders() { diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 1bc9ce6f..72e32f5e 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -124,7 +124,7 @@ public function testMessage(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); - self::assertSame(6, $message->get("received")->count()); + self::assertSame(5, $message->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); } @@ -186,7 +186,7 @@ public function testLoadMessageFromFile(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); - self::assertSame(6, $message->get("received")->count()); + self::assertSame(5, $message->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); self::assertNull($message->getClient()); @@ -205,7 +205,7 @@ public function testLoadMessageFromFile(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("1.103", (string)$message->get("X-Spam-Score")); self::assertSame("d3a5e91963cb805cee975687d5acb1c6@swift.generated", (string)$message->get("Message-ID")); - self::assertSame(5, $message->get("received")->count()); + self::assertSame(4, $message->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_HIGHEST, (int)$message->get("priority")()); self::assertNull($message->getClient()); diff --git a/tests/fixtures/EmbeddedEmailTest.php b/tests/fixtures/EmbeddedEmailTest.php index fb6e2492..024a8885 100644 --- a/tests/fixtures/EmbeddedEmailTest.php +++ b/tests/fixtures/EmbeddedEmailTest.php @@ -32,7 +32,6 @@ public function testFixture() : void { self::assertEquals("embedded message", $message->subject); self::assertEquals([ 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', - 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' ], $message->received->toArray()); self::assertEquals("7e5798da5747415e5b82fdce042ab2a6@cerstor.cz", $message->message_id); self::assertEquals("demo@cerstor.cz", $message->return_path); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php index 7d3fb2d0..a620936b 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -32,7 +32,6 @@ public function testFixture() : void { self::assertEquals("embedded_message_subject", $message->subject); self::assertEquals([ 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', - 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' ], $message->received->toArray()); self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D40E38@VM002.cerk.cc", $message->message_id); self::assertEquals("pl-PL, nl-NL", $message->accept_language); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 603a956d..36287a6b 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -32,7 +32,6 @@ public function testFixture() : void { self::assertEquals("Subject", $message->subject); self::assertEquals([ 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', - 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' ], $message->received->toArray()); self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D9F7E4@VM002.cerk.cc", $message->message_id); self::assertEquals("pl-PL, nl-NL", $message->accept_language); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index d2f418a9..c92d35c6 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -36,7 +36,6 @@ public function testFixture(): void { 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', - 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', ], $message->received->all()); self::assertEquals("demo@foo.de", $message->envelope_to); self::assertEquals("Thu, 02 Mar 2023 05:27:29 +0100", $message->delivery_date); @@ -50,7 +49,6 @@ public function testFixture(): void { 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', - 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', ], $message->received->all()); self::assertEquals("ding@ding.de", $message->x_failed_recipients); self::assertEquals("auto-replied", $message->auto_submitted); diff --git a/tests/fixtures/MultipartWithoutBodyTest.php b/tests/fixtures/MultipartWithoutBodyTest.php index 9989b3e7..01112bb7 100644 --- a/tests/fixtures/MultipartWithoutBodyTest.php +++ b/tests/fixtures/MultipartWithoutBodyTest.php @@ -37,7 +37,6 @@ public function testFixture() : void { 0 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 08:24:33 +0000', 1 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar 2023 08:24:31 +0000', 2 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 08:24:31 +0000', - 3 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS', ], $message->received->all()); self::assertEquals("This mail will not contain a body", $message->thread_topic); self::assertEquals("AdlT8uVmpHPvImbCRM6E9LODIvAcQA==", $message->thread_index); diff --git a/tests/fixtures/UndefinedCharsetHeaderTest.php b/tests/fixtures/UndefinedCharsetHeaderTest.php index acb3029c..b2f9d8fb 100644 --- a/tests/fixtures/UndefinedCharsetHeaderTest.php +++ b/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -37,7 +37,6 @@ public function testFixture() : void { self::assertEquals("", $message->get("Return-Path")); self::assertEquals([ 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', - 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804' ], $message->get("Received")->all()); self::assertEquals(")", $message->getHTMLBody()); self::assertFalse($message->hasTextBody()); diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index 458c8a13..4f8b831d 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -14,11 +14,33 @@ use PHPUnit\Framework\TestCase; use Tests\fixtures\FixtureTestCase; +use Webklex\PHPIMAP\Attachment; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; +use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\ResponseException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Message; class Issue410Test extends FixtureTestCase { + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws InvalidMessageDateException + * @throws ConnectionFailedException + * @throws \ReflectionException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws MaskNotFoundException + */ public function testIssueEmail() { $message = $this->getFixture("issue-410.eml"); @@ -33,6 +55,18 @@ public function testIssueEmail() { self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); } + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws InvalidMessageDateException + * @throws ConnectionFailedException + * @throws \ReflectionException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws MaskNotFoundException + */ public function testIssueEmailB() { $message = $this->getFixture("issue-410b.eml"); @@ -48,6 +82,18 @@ public function testIssueEmailB() { self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); } + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws \ReflectionException + * @throws MaskNotFoundException + */ public function testIssueEmailSymbols() { $message = $this->getFixture("issue-410symbols.eml"); @@ -55,10 +101,11 @@ public function testIssueEmailSymbols() { self::assertSame(1, $attachments->count()); + /** @var Attachment $attachment */ $attachment = $attachments->first(); self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->description); - self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->filename); self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->name); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->filename); } } \ No newline at end of file From be974001c3d230df0c697e27f4133837b9488380 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 03:19:29 +0100 Subject: [PATCH 555/600] php8.3 and php8.4 checks added --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b441efd3..682cbd51 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: true matrix: - php: ['8.0', 8.1, 8.2] + php: ['8.0', 8.1, 8.2, 8.3, 8.4] name: PHP ${{ matrix.php }} From 279ad47be78cd07215ecf127c978405feb76d99a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 03:40:38 +0100 Subject: [PATCH 556/600] Ignore possible untagged lines after IDLE and DONE commands #445 (thanks @gazben) --- CHANGELOG.md | 3 +- src/Connection/Protocols/ImapProtocol.php | 74 +++++++++++++++++- src/Folder.php | 92 +++++++---------------- src/Message.php | 4 +- 4 files changed, 103 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b006fb36..c85451bd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) - Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) - Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) -- Attachment with symbols in filename #436 +- Attachment with symbols in filename #436 (thanks @nuernbergerA) +- Ignore possible untagged lines after IDLE and DONE commands #445 (thanks @gazben) ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cb7cf4b4..b054d462 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -160,6 +160,25 @@ protected function assumedNextLine(Response $response, string $start): bool { return str_starts_with($this->nextLine($response), $start); } + /** + * Get the next line and check if it starts with a given string + * The server can send untagged status updates starting with '*' if we are not looking for a status update, + * the untagged lines will be ignored. + * + * @param Response $response + * @param string $start + * + * @return bool + * @throws RuntimeException + */ + protected function assumedNextLineIgnoreUntagged(Response $response, string $start): bool { + do { + $line = $this->nextLine($response); + } while (!(str_starts_with($start, '*')) && $this->isUntaggedLine($line)); + + return str_starts_with($line, $start); + } + /** * Get the next line and split the tag * @param string|null $tag reference tag @@ -176,6 +195,25 @@ protected function nextTaggedLine(Response $response, ?string &$tag): string { return $line ?? ''; } + /** + * Get the next line and split the tag + * The server can send untagged status updates starting with '*', the untagged lines will be ignored. + * + * @param string|null $tag reference tag + * + * @return string next line + * @throws RuntimeException + */ + protected function nextTaggedLineIgnoreUntagged(Response $response, &$tag): string { + do { + $line = $this->nextLine($response); + } while ($this->isUntaggedLine($line)); + + list($tag, $line) = explode(' ', $line, 2); + + return $line; + } + /** * Get the next line and check if it contains a given string and split the tag * @param Response $response @@ -189,6 +227,32 @@ protected function assumedNextTaggedLine(Response $response, string $start, &$ta return str_contains($this->nextTaggedLine($response, $tag), $start); } + /** + * Get the next line and check if it contains a given string and split the tag + * @param string $start + * @param $tag + * + * @return bool + * @throws RuntimeException + */ + protected function assumedNextTaggedLineIgnoreUntagged(Response $response, string $start, &$tag): bool { + $line = $this->nextTaggedLineIgnoreUntagged($response, $tag); + return strpos($line, $start) !== false; + } + + /** + * RFC3501 - 2.2.2 + * Data transmitted by the server to the client and status responses + * that do not indicate command completion are prefixed with the token + * "*", and are called untagged responses. + * + * @param string $line + * @return bool + */ + protected function isUntaggedLine(string $line) : bool { + return str_starts_with($line, '* '); + } + /** * Split a given line in values. A value is literal of any form or a list * @param Response $response @@ -703,10 +767,12 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', * @throws RuntimeException */ public function fetch(array|string $items, array|int $from, mixed $to = null, int|string $uid = IMAP::ST_UID): Response { - if (is_array($from)) { + if (is_array($from) && count($from) > 1) { $set = implode(',', $from); + } elseif (is_array($from) && count($from) === 1) { + $set = $from[0] . ':' . $from[0]; } elseif ($to === null) { - $set = $from; + $set = $from . ':' . $from; } elseif ($to == INF) { $set = $from . ':*'; } else { @@ -1266,7 +1332,7 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): Response { */ public function idle(): void { $response = $this->sendRequest("IDLE"); - if (!$this->assumedNextLine($response, '+ ')) { + if (!$this->assumedNextLineIgnoreUntagged($response, '+ ')) { throw new RuntimeException('idle failed'); } } @@ -1278,7 +1344,7 @@ public function idle(): void { public function done(): bool { $response = new Response($this->noun, $this->debug); $this->write($response, "DONE"); - if (!$this->assumedNextTaggedLine($response, 'OK', $tags)) { + if (!$this->assumedNextTaggedLineIgnoreUntagged($response, 'OK', $tags)) { throw new RuntimeException('done failed'); } return true; diff --git a/src/Folder.php b/src/Folder.php index 8c9abd61..11affde2 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -121,22 +121,6 @@ class Folder { /** @var array */ public array $status; - /** @var array */ - public array $attributes = []; - - - const SPECIAL_ATTRIBUTES = [ - 'haschildren' => ['\haschildren'], - 'hasnochildren' => ['\hasnochildren'], - 'template' => ['\template', '\templates'], - 'inbox' => ['\inbox'], - 'sent' => ['\sent'], - 'drafts' => ['\draft', '\drafts'], - 'archive' => ['\archive', '\archives'], - 'trash' => ['\trash'], - 'junk' => ['\junk', '\spam'], - ]; - /** * Folder constructor. * @param Client $client @@ -251,8 +235,8 @@ public function getChildren(): FolderCollection { */ protected function decodeName($name): string|array|bool|null { $parts = []; - foreach(explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); + foreach (explode($this->delimiter, $name) as $item) { + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); } return implode($this->delimiter, $parts); @@ -280,14 +264,6 @@ protected function parseAttributes($attributes): void { $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); - - array_map(function($el) { - foreach(self::SPECIAL_ATTRIBUTES as $key => $attribute) { - if(in_array(strtolower($el), $attribute)){ - $this->attributes[] = $key; - } - } - }, $attributes); } /** @@ -308,7 +284,7 @@ protected function parseAttributes($attributes): void { public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData(); - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); $event = $this->getEvent("folder", "moved"); @@ -334,7 +310,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); + $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -360,7 +336,7 @@ public function appendMessage(string $message, array $options = null, Carbon|str * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if($internal_date instanceof Carbon){ + if ($internal_date instanceof Carbon) { $internal_date = $internal_date->format('d-M-Y H:i:s O'); } @@ -401,11 +377,11 @@ public function rename(string $new_name, bool $expunge = true): array { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); - if($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(); + if ($this->client->getActiveFolder() == $this->path){ + $this->client->setActiveFolder(null); } - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); $event::dispatch($this); @@ -461,7 +437,7 @@ public function unsubscribe(): array { public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); - if(!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())){ + if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } @@ -474,15 +450,24 @@ public function idle(callable $callback, int $timeout = 300): void { $sequence = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); - while(true) { - // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand - $line = $idle_client->getConnection()->nextLine(Response::empty()); + while (true) { + try { + // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand + $line = $idle_client->getConnection()->nextLine(Response::empty()); + } catch (Exceptions\RuntimeException $e) { + if(strpos($e->getMessage(), "empty response") >= 0 && $idle_client->getConnection()->connected()) { + continue; + } + if(!str_contains($e->getMessage(), "connection closed")) { + throw $e; + } + } - if(($pos = strpos($line, "EXISTS")) !== false){ + if (($pos = strpos($line, "EXISTS")) !== false) { $msgn = (int)substr($line, 2, $pos - 2); // Check if the stream is still alive or should be considered stale - if(!$this->client->isConnected() || $last_action->isBefore(Carbon::now())){ + if (!$this->client->isConnected() || $last_action->isBefore(Carbon::now())) { // Reset the connection before interacting with it. Otherwise, the resource might be stale which // would result in a stuck interaction. If you know of a way of detecting a stale resource, please // feel free to improve this logic. I tried a lot but nothing seem to work reliably... @@ -516,7 +501,7 @@ public function idle(callable $callback, int $timeout = 300): void { } /** - * Get folder status information from the EXAMINE command + * Get folder status information * * @return array * @throws ConnectionFailedException @@ -526,39 +511,20 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws AuthFailedException * @throws ResponseException */ - public function status(): array { - return $this->client->getConnection()->folderStatus($this->path)->validatedData(); + public function getStatus(): array { + return $this->examine(); } /** - * Get folder status information from the EXAMINE command - * - * @return array - * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws ResponseException * @throws RuntimeException - * - * @deprecated Use Folder::status() instead - */ - public function getStatus(): array { - return $this->status(); - } - - /** - * Load folder status information from the EXAMINE command - * @return Folder * @throws AuthFailedException - * @throws ConnectionFailedException - * @throws ImapBadRequestException - * @throws ImapServerErrorException * @throws ResponseException - * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->examine(); + $this->status = $this->getStatus(); return $this; } @@ -606,8 +572,8 @@ public function getClient(): Client { * @param $delimiter */ public function setDelimiter($delimiter): void { - if(in_array($delimiter, [null, '', ' ', false]) === true){ - $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); + if (in_array($delimiter, [null, '', ' ', false]) === true) { + $delimiter = ClientManager::get('options.delimiter', '/'); } $this->delimiter = $delimiter; diff --git a/src/Message.php b/src/Message.php index bae2acfe..855373b8 100755 --- a/src/Message.php +++ b/src/Message.php @@ -556,7 +556,7 @@ public function getHTMLBody(): string { */ private function parseHeader(): void { $sequence_id = $this->getSequenceId(); - $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->validatedData(); + $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->setCanBeEmpty(true)->validatedData(); if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } @@ -609,7 +609,7 @@ private function parseFlags(): void { $sequence_id = $this->getSequenceId(); try { - $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->validatedData(); + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->setCanBeEmpty(true)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be fetched", 0, $e); } From bd1614dcf75d10e959aa9dcb610852b6f58c9996 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:05:17 +0100 Subject: [PATCH 557/600] Merge conflict resolved #445 --- src/Folder.php | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index 11affde2..8eff19bc 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -236,7 +236,7 @@ public function getChildren(): FolderCollection { protected function decodeName($name): string|array|bool|null { $parts = []; foreach (explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); } return implode($this->delimiter, $parts); @@ -310,7 +310,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -378,7 +378,7 @@ public function rename(string $new_name, bool $expunge = true): array { public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); if ($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(null); + $this->client->setActiveFolder(); } if ($expunge) $this->client->expunge(); @@ -501,7 +501,7 @@ public function idle(callable $callback, int $timeout = 300): void { } /** - * Get folder status information + * Get folder status information from the EXAMINE command * * @return array * @throws ConnectionFailedException @@ -511,20 +511,39 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws AuthFailedException * @throws ResponseException */ - public function getStatus(): array { - return $this->examine(); + public function status(): array { + return $this->client->getConnection()->folderStatus($this->path)->validatedData(); } /** + * Get folder status information from the EXAMINE command + * + * @return array + * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException + * @throws ResponseException * @throws RuntimeException + * + * @deprecated Use Folder::status() instead + */ + public function getStatus(): array { + return $this->status(); + } + + /** + * Load folder status information from the EXAMINE command + * @return Folder * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ResponseException + * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->getStatus(); + $this->status = $this->examine(); return $this; } @@ -573,7 +592,7 @@ public function getClient(): Client { */ public function setDelimiter($delimiter): void { if (in_array($delimiter, [null, '', ' ', false]) === true) { - $delimiter = ClientManager::get('options.delimiter', '/'); + $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); } $this->delimiter = $delimiter; From 7f0e219a2a496ef6ca3bfee4ad98f5255106d8b1 Mon Sep 17 00:00:00 2001 From: Jeff Bierschbach Date: Thu, 16 Jan 2025 21:11:28 -0600 Subject: [PATCH 558/600] Fix Empty Child Folder Error (#474) --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 2ec9ebd9..90016a10 100755 --- a/src/Client.php +++ b/src/Client.php @@ -586,7 +586,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFolders(true, $pattern, $soft_fail); + $children = $this->getFolders(true, $pattern, true); $folder->setChildren($children); } @@ -632,7 +632,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFoldersWithStatus(true, $pattern, $soft_fail); + $children = $this->getFoldersWithStatus(true, $pattern, true); $folder->setChildren($children); } From 6bd8ba43cf25282ab9a99ec31c4e2b16c7374022 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:14:50 +0100 Subject: [PATCH 559/600] #487 TestCase added --- tests/HeaderTest.php | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 9de25004..767f4388 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -159,4 +159,46 @@ public function testExtractHeaderExtensions() { $this->assertArrayHasKey('attribute_test', $mock->getAttributes()); $this->assertEquals('attribute_test_value', $mock->get('attribute_test')); } + + public function testExtractHeaderExtensions2() { + $mock = $this->getMockBuilder(Header::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + + $method = new \ReflectionMethod($mock, 'extractHeaderExtensions'); + $method->setAccessible(true); + + $mockAttributes = [ + 'content_type' => new Attribute('content_type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name="=?utf-8?Q?=D0=A2=D0=B8=D0=BF=D0=BE=D0=B2=D0=BE=D0=B9_?= =?utf-8?Q?=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82_=D0=BF?= =?utf-8?Q?=D0=BE=D1=82=D1=80=D0=B5=D0=B1=D0=BB=D0=B5=D0=BD?= =?utf-8?Q?=D0=B8=D1=8F_=D1=8D=D0=BB=D0=B5=D0=BA=D1=82?= =?utf-8?Q?=D1=80=D0=BE=D1=8D=D0=BD=D0=B5=D1=80=D0=B3=D0=B8=D0=B8_=D0=B2_?= =?utf-8?Q?=D0=9A=D0=9F_=D0=97=D0=B2=D0=B5=D0=B7=D0=B4?= =?utf-8?Q?=D0=BD=D1=8B=D0=B9=2Exlsx?="'), + 'content_transfer_encoding' => new Attribute('content_transfer_encoding', 'base64'), + 'content_disposition' => new Attribute('content_disposition', 'attachment; name*0*=utf-8\'\'%D0%A2%D0%B8%D0%BF%D0%BE%D0%B2%D0%BE%D0%B9%20; name*1*=%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%20%D0%BF; name*2*=%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%BB%D0%B5%D0%BD; name*3*=%D0%B8%D1%8F%20%D1%8D%D0%BB%D0%B5%D0%BA%D1%82; name*4*=%D1%80%D0%BE%D1%8D%D0%BD%D0%B5%D1%80%D0%B3%D0%B8; name*5*=%D0%B8%20%D0%B2%20%D0%9A%D0%9F%20%D0%97%D0%B2%D0%B5%D0%B7%D0%B4; name*6*=%D0%BD%D1%8B%D0%B9.xlsx; filename*0*=utf-8\'\'%D0%A2%D0%B8%D0%BF%D0%BE%D0%B2%D0%BE%D0%B9%20; filename*1*=%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%20%D0%BF; filename*2*=%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%BB%D0%B5%D0%BD; filename*3*=%D0%B8%D1%8F%20%D1%8D%D0%BB%D0%B5%D0%BA%D1%82; filename*4*=%D1%80%D0%BE%D1%8D%D0%BD%D0%B5%D1%80%D0%B3%D0%B8; filename*5*=%D0%B8%20%D0%B2%20%D0%9A%D0%9F%20%D0%97; filename*6*=%D0%B2%D0%B5%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9.xlsx; attribute_test=attribute_test_value'), + ]; + + $attributes = new \ReflectionProperty($mock, 'attributes'); + $attributes->setAccessible(true); + $attributes->setValue($mock, $mockAttributes); + + $method->invoke($mock); + + $this->assertArrayHasKey('filename', $mock->getAttributes()); + $this->assertArrayNotHasKey('filename*0', $mock->getAttributes()); + $this->assertEquals('utf-8\'\'%D0%A2%D0%B8%D0%BF%D0%BE%D0%B2%D0%BE%D0%B9%20%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%20%D0%BF%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%8D%D0%BB%D0%B5%D0%BA%D1%82%D1%80%D0%BE%D1%8D%D0%BD%D0%B5%D1%80%D0%B3%D0%B8%D0%B8%20%D0%B2%20%D0%9A%D0%9F%20%D0%97%D0%B2%D0%B5%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9.xlsx', $mock->get('filename')); + + $this->assertArrayHasKey('name', $mock->getAttributes()); + $this->assertArrayNotHasKey('name*0', $mock->getAttributes()); + $this->assertEquals('=?utf-8?Q?=D0=A2=D0=B8=D0=BF=D0=BE=D0=B2=D0=BE=D0=B9_?= =?utf-8?Q?=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82_=D0=BF?= =?utf-8?Q?=D0=BE=D1=82=D1=80=D0=B5=D0=B1=D0=BB=D0=B5=D0=BD?= =?utf-8?Q?=D0=B8=D1=8F_=D1=8D=D0=BB=D0=B5=D0=BA=D1=82?= =?utf-8?Q?=D1=80=D0=BE=D1=8D=D0=BD=D0=B5=D1=80=D0=B3=D0=B8=D0=B8_=D0=B2_?= =?utf-8?Q?=D0=9A=D0=9F_=D0=97=D0=B2=D0=B5=D0=B7=D0=B4?= =?utf-8?Q?=D0=BD=D1=8B=D0=B9=2Exlsx?=', $mock->get('name')); + + $this->assertArrayHasKey('content_type', $mock->getAttributes()); + $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $mock->get('content_type')->last()); + + $this->assertArrayHasKey('content_transfer_encoding', $mock->getAttributes()); + $this->assertEquals('base64', $mock->get('content_transfer_encoding')); + + $this->assertArrayHasKey('content_disposition', $mock->getAttributes()); + $this->assertEquals('attachment', $mock->get('content_disposition')->last()); + + $this->assertArrayHasKey('attribute_test', $mock->getAttributes()); + $this->assertEquals('attribute_test_value', $mock->get('attribute_test')); + } } \ No newline at end of file From 1cabca657b8139fb7ecbac16c59f2e94b47ffd65 Mon Sep 17 00:00:00 2001 From: neolip Date: Fri, 17 Jan 2025 06:20:46 +0300 Subject: [PATCH 560/600] Fix: Attachment::decodeName remove .. from file name (#501) If attached file has name like test..xml, then dots remove and broke file extension. --- src/Attachment.php | 7 ++++++- tests/AttachmentTest.php | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/AttachmentTest.php diff --git a/src/Attachment.php b/src/Attachment.php index bcbe91c5..19b639f6 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -325,7 +325,12 @@ public function decodeName(?string $name): string { // sanitize $name // order of '..' is important - return str_replace(['\\', '/', chr(0), ':', '..'], '', $name); + $replaces = [ + '/\\\\/' => '', + '/[\/\0:]+/' => '', + '/\.+/' => '.', + ]; + return preg_replace(array_keys($replaces), array_values($replaces), $name); } return ""; } diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php new file mode 100644 index 00000000..566ea658 --- /dev/null +++ b/tests/AttachmentTest.php @@ -0,0 +1,37 @@ +getFixture("attachment_encoded_filename.eml"); + $this->attachment = $message->getAttachments()->first(); + } + /** + * @dataProvider decodeNameDataProvider + */ + public function testDecodeName(string $input, string $output): void + { + $name = $this->attachment->decodeName($input); + $this->assertEquals($output, $name); + } + + public function decodeNameDataProvider(): array + { + return [ + ['../../../../../../../../../../../var/www/shell.php', '.varwwwshell.php'], + ['test..xml', 'test.xml'], + [chr(0), ''], + ['C:\\file.txt', 'Cfile.txt'], + ]; + } +} From af752f3eb4fdb9f6e1d0e90e250203dcfaccced7 Mon Sep 17 00:00:00 2001 From: Arnaud Lemercier Date: Fri, 17 Jan 2025 04:24:44 +0100 Subject: [PATCH 561/600] docs: fix a comment (#504) --- src/Connection/Protocols/ProtocolInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 0a0dbfad..eb6d7c9d 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -129,7 +129,7 @@ public function examineFolder(string $folder = 'INBOX'): Response; public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response; /** - * Fetch message headers + * Fetch message contents * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use From 4601d2f9b3389171d57d536f8dabe386bc5082ba Mon Sep 17 00:00:00 2001 From: Arnaud Lemercier Date: Fri, 17 Jan 2025 04:26:21 +0100 Subject: [PATCH 562/600] feat: client->getFolderPath return null if folder is not set. (#506) * feat: client->getFolderPath return null if folder is not set. * docs: update comment --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 90016a10..ceaae8f6 100755 --- a/src/Client.php +++ b/src/Client.php @@ -773,9 +773,9 @@ public function checkFolder(string $folder_path): array { /** * Get the current active folder * - * @return string + * @return null|string */ - public function getFolderPath(): string { + public function getFolderPath(): ?string { return $this->active_folder; } From e88fd4c62767d2323bc9ea496880ed5c162cccac Mon Sep 17 00:00:00 2001 From: campbell-m <87438215+campbell-m@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:27:56 +0000 Subject: [PATCH 563/600] Fix implicit marking of parameters as nullable, deprecated in PHP 8.4 (#518) * Fix implicit marking of parameters as nullable, which is deprecated in PHP 8.4. * Change line separators to LF --- src/Client.php | 6 +- src/ClientManager.php | 2 +- src/Connection/Protocols/ImapProtocol.php | 12 +- src/Connection/Protocols/LegacyProtocol.php | 12 +- src/EncodingAliases.php | 2 +- src/Folder.php | 4 +- src/Message.php | 18 +- src/Part.php | 2 +- src/Query/WhereQuery.php | 4 +- tests/messages/1366671050@github.com.eml | 216 ++++++++++---------- tests/messages/example_attachment.eml | 112 +++++----- 11 files changed, 195 insertions(+), 195 deletions(-) diff --git a/src/Client.php b/src/Client.php index ceaae8f6..a79511c0 100755 --- a/src/Client.php +++ b/src/Client.php @@ -572,7 +572,7 @@ public function getFolderByPath($folder_path, bool $utf7 = false, bool $soft_fai * @throws ResponseException * @throws RuntimeException */ - public function getFolders(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { + public function getFolders(bool $hierarchical = true, ?string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -618,7 +618,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * @throws RuntimeException * @throws ResponseException */ - public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { + public function getFoldersWithStatus(bool $hierarchical = true, ?string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -793,7 +793,7 @@ public function getFolderPath(): ?string { * @throws RuntimeException * @throws ResponseException */ - public function Id(array $ids = null): array { + public function Id(?array $ids = null): array { $this->checkConnection(); return $this->connection->ID($ids)->validatedData(); } diff --git a/src/ClientManager.php b/src/ClientManager.php index a63f3a4e..6f66886b 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -74,7 +74,7 @@ public function make(array $config): Client { * @return Client * @throws Exceptions\MaskNotFoundException */ - public function account(string $name = null): Client { + public function account(?string $name = null): Client { $name = $name ?: $this->config->getDefaultAccount(); // If the connection has not been resolved we will resolve it now as all diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b054d462..9272be91 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -67,7 +67,7 @@ public function __destruct() { * * @throws ConnectionFailedException */ - public function connect(string $host, int $port = null): bool { + public function connect(string $host, ?int $port = null): bool { $transport = 'tcp'; $encryption = ''; @@ -426,7 +426,7 @@ private function stringifyArray(array $arr): string { * @return Response * @throws RuntimeException */ - public function sendRequest(string $command, array $tokens = [], string &$tag = null): Response { + public function sendRequest(string $command, array $tokens = [], ?string &$tag = null): Response { if (!$tag) { $this->noun++; $tag = 'TAG' . $this->noun; @@ -1021,7 +1021,7 @@ public function folders(string $reference = '', string $folder = '*'): Response * @throws RuntimeException */ public function store( - array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null + array|string $flags, int $from, ?int $to = null, ?string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, ?string $item = null ): Response { $flags = $this->escapeList(is_array($flags) ? $flags : [$flags]); $set = $this->buildSet($from, $to); @@ -1061,7 +1061,7 @@ public function store( * @throws ImapServerErrorException * @throws RuntimeException */ - public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response { + public function appendMessage(string $folder, string $message, ?array $flags = null, ?string $date = null): Response { $tokens = []; $tokens[] = $this->escapeString($folder); if ($flags !== null) { @@ -1091,7 +1091,7 @@ public function appendMessage(string $folder, string $message, array $flags = nu * @throws ImapServerErrorException * @throws RuntimeException */ - public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function copyMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("COPY", $uid); @@ -1137,7 +1137,7 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * @throws ImapServerErrorException * @throws RuntimeException */ - public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function moveMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index c6471a02..81b52f77 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -55,7 +55,7 @@ public function __destruct() { * @param string $host * @param int|null $port */ - public function connect(string $host, int $port = null): void { + public function connect(string $host, ?int $port = null): void { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { @@ -360,7 +360,7 @@ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response * * @return Response message number for given message or all messages as array */ - public function getUid(int $id = null): Response { + public function getUid(?int $id = null): Response { return $this->response()->wrap(function($response) use ($id) { /** @var Response $response */ if ($id === null) { @@ -455,7 +455,7 @@ public function folders(string $reference = '', string $folder = '*'): Response * * @return Response new flags if $silent is false, else true or false depending on success */ - public function store(array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null): Response { + public function store(array|string $flags, int $from, ?int $to = null, ?string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, ?string $item = null): Response { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); return $this->response()->wrap(function($response) use ($mode, $from, $flag, $uid, $silent) { @@ -491,7 +491,7 @@ public function store(array|string $flags, int $from, int $to = null, string $mo * * @return Response */ - public function appendMessage(string $folder, string $message, array $flags = null, mixed $date = null): Response { + public function appendMessage(string $folder, string $message, ?array $flags = null, mixed $date = null): Response { return $this->response("imap_append")->wrap(function($response) use ($folder, $message, $flags, $date) { /** @var Response $response */ if ($date != null) { @@ -522,7 +522,7 @@ public function appendMessage(string $folder, string $message, array $flags = nu * * @return Response */ - public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function copyMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { return $this->response("imap_mail_copy")->wrap(function($response) use ($from, $folder, $uid) { /** @var Response $response */ @@ -572,7 +572,7 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * * @return Response success */ - public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function moveMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { return $this->response("imap_mail_move")->wrap(function($response) use ($from, $folder, $uid) { if (\imap_mail_move($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { return [ diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 888fc7fa..8a3dc0fb 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -474,7 +474,7 @@ class EncodingAliases { * * @return string */ - public static function get(?string $encoding, string $fallback = null): string { + public static function get(?string $encoding, ?string $fallback = null): string { if (isset(self::$aliases[strtolower($encoding ?? '')])) { return self::$aliases[strtolower($encoding ?? '')]; } diff --git a/src/Folder.php b/src/Folder.php index 8eff19bc..dced7d6b 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -307,7 +307,7 @@ public function move(string $new_name, bool $expunge = true): array { * @throws MessageNotFoundException * @throws ResponseException */ - public function overview(string $sequence = null): array { + public function overview(?string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); @@ -329,7 +329,7 @@ public function overview(string $sequence = null): array { * @throws AuthFailedException * @throws ResponseException */ - public function appendMessage(string $message, array $options = null, Carbon|string $internal_date = null): array { + public function appendMessage(string $message, ?array $options = null, Carbon|string|null $internal_date = null): array { /** * Check if $internal_date is parsed. If it is null it should not be set. Otherwise, the message can't be stored. * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a diff --git a/src/Message.php b/src/Message.php index 855373b8..b0dabb2d 100755 --- a/src/Message.php +++ b/src/Message.php @@ -222,7 +222,7 @@ class Message { * @throws RuntimeException * @throws ResponseException */ - public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { + public function __construct(int $uid, ?int $msglist, Client $client, ?int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, ?int $sequence = null) { $this->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); @@ -329,7 +329,7 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw * @throws ResponseException * @throws RuntimeException */ - public static function fromFile(string $filename, Config $config = null): Message { + public static function fromFile(string $filename, ?Config $config = null): Message { $blob = file_get_contents($filename); if ($blob === false) { throw new RuntimeException("Unable to read file"); @@ -354,7 +354,7 @@ public static function fromFile(string $filename, Config $config = null): Messag * @throws ResponseException * @throws RuntimeException */ - public static function fromString(string $blob, Config $config = null): Message { + public static function fromString(string $blob, ?Config $config = null): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -386,7 +386,7 @@ public static function fromString(string $blob, Config $config = null): Message * @param ?Config $config * @throws DecoderNotFoundException */ - public function boot(Config $config = null): void { + public function boot(?Config $config = null): void { $this->attributes = []; $this->client = null; $this->config = $config ?? Config::make(); @@ -903,7 +903,7 @@ public function getFolder(): ?Folder { * @throws RuntimeException * @throws ResponseException */ - public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { + public function thread(?Folder $sent_folder = null, ?MessageCollection &$thread = null, ?Folder $folder = null): MessageCollection { $thread = $thread ?: MessageCollection::make(); $folder = $folder ?: $this->getFolder(); $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); @@ -1123,7 +1123,7 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo * @throws RuntimeException * @throws ResponseException */ - public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false): bool { + public function delete(bool $expunge = true, ?string $trash_path = null, bool $force_move = false): bool { $status = $this->setFlag("Deleted"); if ($force_move) { $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"] : $trash_path; @@ -1398,7 +1398,7 @@ public function getStructure(): ?Structure { * @param null|Message $message * @return boolean */ - public function is(Message $message = null): bool { + public function is(?Message $message = null): bool { if (is_null($message)) { return false; } @@ -1605,7 +1605,7 @@ public function setUid(int $uid): Message { * * @return Message */ - public function setMsgn(int $msgn, int $msglist = null): Message { + public function setMsgn(int $msgn, ?int $msglist = null): Message { $this->msgn = $msgn; $this->msglist = $msglist; $this->uid = null; @@ -1636,7 +1636,7 @@ public function getSequenceId(): int { * @param $uid * @param int|null $msglist */ - public function setSequenceId($uid, int $msglist = null): void { + public function setSequenceId($uid, ?int $msglist = null): void { if ($this->getSequence() === IMAP::ST_UID) { $this->setUid($uid); $this->setMsglist($msglist); diff --git a/src/Part.php b/src/Part.php index 1dbf9439..3c55bdfb 100644 --- a/src/Part.php +++ b/src/Part.php @@ -153,7 +153,7 @@ class Part { * * @throws InvalidMessageDateException */ - public function __construct(string $raw_part, Config $config, Header $header = null, int $part_number = 0) { + public function __construct(string $raw_part, Config $config, ?Header $header = null, int $part_number = 0) { $this->raw = $raw_part; $this->config = $config; $this->header = $header; diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index e76cbe85..5636cf35 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -171,7 +171,7 @@ protected function push_search_criteria(string $criteria, mixed $value): void { * * @return $this */ - public function orWhere(Closure $closure = null): static { + public function orWhere(?Closure $closure = null): static { $this->query->push(['OR']); if ($closure !== null) $closure($this); @@ -183,7 +183,7 @@ public function orWhere(Closure $closure = null): static { * * @return $this */ - public function andWhere(Closure $closure = null): static { + public function andWhere(?Closure $closure = null): static { $this->query->push(['AND']); if ($closure !== null) $closure($this); diff --git a/tests/messages/1366671050@github.com.eml b/tests/messages/1366671050@github.com.eml index 91f51cf8..97cb7ad1 100644 --- a/tests/messages/1366671050@github.com.eml +++ b/tests/messages/1366671050@github.com.eml @@ -1,108 +1,108 @@ -Return-Path: -Delivered-To: someone@domain.tld -Received: from mx.domain.tld - by localhost with LMTP - id SABVMNfGqWP+PAAA0J78UA - (envelope-from ) - for ; Mon, 26 Dec 2022 17:07:51 +0100 -Received: from localhost (localhost [127.0.0.1]) - by mx.domain.tld (Postfix) with ESMTP id C3828140227 - for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET) -X-Virus-Scanned: Debian amavisd-new at mx.domain.tld -X-Spam-Flag: NO -X-Spam-Score: -4.299 -X-Spam-Level: -X-Spam-Status: No, score=-4.299 required=6.31 tests=[BAYES_00=-1.9, - DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, - DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_IMAGE_ONLY_16=1.092, - HTML_MESSAGE=0.001, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_MED=-2.3, - RCVD_IN_MSPIKE_H2=-0.001, T_KAM_HTML_FONT_INVALID=0.01] - autolearn=ham autolearn_force=no -Received: from mx.domain.tld ([127.0.0.1]) - by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) - with ESMTP id JcIS9RuNBTNx for ; - Mon, 26 Dec 2022 17:07:21 +0100 (CET) -Received: from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) - (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) - (No client certificate requested) - by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 - for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET) -Received: from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) - by smtp.github.com (Postfix) with ESMTP id 176985E0200 - for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST) -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=github.com; - s=pf2014; t=1672070834; - bh=v91TPiLpM/cpUb4lgt2NMIUfM4HOIxCEWMR1+JTco+Q=; - h=Date:From:Reply-To:To:Cc:In-Reply-To:References:Subject:List-ID: - List-Archive:List-Post:List-Unsubscribe:From; - b=jW4Tac9IjWAPbEImyiYf1bzGLzY3ceohVbBg1V8BlpMTQ+o+yY3YB0eOe20hAsqZR - jrDjArx7rKQcslqBFL/b2B1C51rHuCBrz2cccLEERu9l/u0mTGCxTNtCRXHbCKbnR1 - VLWBeFLjATHth83kK6Kt7lkVuty+G3V1B6ZKPhCI= -Date: Mon, 26 Dec 2022 08:07:14 -0800 -From: Username -Reply-To: Webklex/php-imap -To: Webklex/php-imap -Cc: Subscribed -Message-ID: -In-Reply-To: -References: -Subject: Re: [Webklex/php-imap] Read all folders? (Issue #349) -Mime-Version: 1.0 -Content-Type: multipart/alternative; - boundary="--==_mimepart_63a9c6b293fe_65b5c71014155a"; - charset=UTF-8 -Content-Transfer-Encoding: 7bit -Precedence: list -X-GitHub-Sender: consigliere23 -X-GitHub-Recipient: Webklex -X-GitHub-Reason: subscribed -List-ID: Webklex/php-imap -List-Archive: https://github.com/Webklex/php-imap -List-Post: -List-Unsubscribe: , - -X-Auto-Response-Suppress: All -X-GitHub-Recipient-Address: someone@domain.tld - - -----==_mimepart_63a9c6b293fe_65b5c71014155a -Content-Type: text/plain; - charset=UTF-8 -Content-Transfer-Encoding: 7bit - -Any updates? - --- -Reply to this email directly or view it on GitHub: -https://github.com/Webklex/php-imap/issues/349#issuecomment-1365266070 -You are receiving this because you are subscribed to this thread. - -Message ID: -----==_mimepart_63a9c6b293fe_65b5c71014155a -Content-Type: text/html; - charset=UTF-8 -Content-Transfer-Encoding: 7bit - -

-

Any updates?

- -


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <Webklex/php-imap/issues/349/1365266070@github.com>

- -----==_mimepart_63a9c6b293fe_65b5c71014155a-- +Return-Path: +Delivered-To: someone@domain.tld +Received: from mx.domain.tld + by localhost with LMTP + id SABVMNfGqWP+PAAA0J78UA + (envelope-from ) + for ; Mon, 26 Dec 2022 17:07:51 +0100 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id C3828140227 + for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: -4.299 +X-Spam-Level: +X-Spam-Status: No, score=-4.299 required=6.31 tests=[BAYES_00=-1.9, + DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, + DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_IMAGE_ONLY_16=1.092, + HTML_MESSAGE=0.001, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_MED=-2.3, + RCVD_IN_MSPIKE_H2=-0.001, T_KAM_HTML_FONT_INVALID=0.01] + autolearn=ham autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id JcIS9RuNBTNx for ; + Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 + for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) + by smtp.github.com (Postfix) with ESMTP id 176985E0200 + for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=github.com; + s=pf2014; t=1672070834; + bh=v91TPiLpM/cpUb4lgt2NMIUfM4HOIxCEWMR1+JTco+Q=; + h=Date:From:Reply-To:To:Cc:In-Reply-To:References:Subject:List-ID: + List-Archive:List-Post:List-Unsubscribe:From; + b=jW4Tac9IjWAPbEImyiYf1bzGLzY3ceohVbBg1V8BlpMTQ+o+yY3YB0eOe20hAsqZR + jrDjArx7rKQcslqBFL/b2B1C51rHuCBrz2cccLEERu9l/u0mTGCxTNtCRXHbCKbnR1 + VLWBeFLjATHth83kK6Kt7lkVuty+G3V1B6ZKPhCI= +Date: Mon, 26 Dec 2022 08:07:14 -0800 +From: Username +Reply-To: Webklex/php-imap +To: Webklex/php-imap +Cc: Subscribed +Message-ID: +In-Reply-To: +References: +Subject: Re: [Webklex/php-imap] Read all folders? (Issue #349) +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="--==_mimepart_63a9c6b293fe_65b5c71014155a"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +Precedence: list +X-GitHub-Sender: consigliere23 +X-GitHub-Recipient: Webklex +X-GitHub-Reason: subscribed +List-ID: Webklex/php-imap +List-Archive: https://github.com/Webklex/php-imap +List-Post: +List-Unsubscribe: , + +X-Auto-Response-Suppress: All +X-GitHub-Recipient-Address: someone@domain.tld + + +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Any updates? + +-- +Reply to this email directly or view it on GitHub: +https://github.com/Webklex/php-imap/issues/349#issuecomment-1365266070 +You are receiving this because you are subscribed to this thread. + +Message ID: +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/html; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +

+

Any updates?

+ +


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <Webklex/php-imap/issues/349/1365266070@github.com>

+ +----==_mimepart_63a9c6b293fe_65b5c71014155a-- diff --git a/tests/messages/example_attachment.eml b/tests/messages/example_attachment.eml index a452ae79..3e929a34 100644 --- a/tests/messages/example_attachment.eml +++ b/tests/messages/example_attachment.eml @@ -1,56 +1,56 @@ -Return-Path: -Delivered-To: -Received: from mx.domain.tld - by localhost (Dovecot) with LMTP id T7mwLn3ddlvKWwAA0J78UA - for ; Fri, 17 Aug 2018 16:36:45 +0200 -Received: from localhost (localhost [127.0.0.1]) - by mx.domain.tld (Postfix) with ESMTP id B642913BA0BE2 - for ; Fri, 17 Aug 2018 16:36:45 +0200 (CEST) -X-Virus-Scanned: Debian amavisd-new at mx.domain.tld -X-Spam-Flag: NO -X-Spam-Score: 1.103 -X-Spam-Level: * -X-Spam-Status: No, score=1.103 required=6.31 tests=[ALL_TRUSTED=-1, - OBFU_TEXT_ATTACH=1, TRACKER_ID=1.102, TVD_SPACE_RATIO=0.001] - autolearn=no autolearn_force=no -Received: from mx.domain.tld ([127.0.0.1]) - by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) - with ESMTP id L8E9vyX80d44 for ; - Fri, 17 Aug 2018 16:36:39 +0200 (CEST) -Received: from [127.0.0.1] (ip.dynamic.domain.tld [192.168.0.24]) - (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) - (No client certificate requested) - by mx.domain.tld (Postfix) with ESMTPSA id EF01E13BA0BD7 - for ; Fri, 17 Aug 2018 16:36:38 +0200 (CEST) -Sender: testsender -Message-ID: -Date: Fri, 17 Aug 2018 14:36:24 +0000 -Subject: ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk -From: testfrom -Reply-To: testreply_to -To: testnameto -Cc: testnamecc -MIME-Version: 1.0 -Content-Type: multipart/mixed; - boundary="_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_" -X-Priority: 1 (Highest) - - - - ---_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: quoted-printable - -n1IaXFkbeqKyg4lYToaJ3u1Ond2EDrN3UWuiLFNjOLJEAabSYagYQaOHtV5QDlZE - ---_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ -Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt - -em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW -eFJjT0VSTg== - ---_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_-- - +Return-Path: +Delivered-To: +Received: from mx.domain.tld + by localhost (Dovecot) with LMTP id T7mwLn3ddlvKWwAA0J78UA + for ; Fri, 17 Aug 2018 16:36:45 +0200 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id B642913BA0BE2 + for ; Fri, 17 Aug 2018 16:36:45 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: 1.103 +X-Spam-Level: * +X-Spam-Status: No, score=1.103 required=6.31 tests=[ALL_TRUSTED=-1, + OBFU_TEXT_ATTACH=1, TRACKER_ID=1.102, TVD_SPACE_RATIO=0.001] + autolearn=no autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id L8E9vyX80d44 for ; + Fri, 17 Aug 2018 16:36:39 +0200 (CEST) +Received: from [127.0.0.1] (ip.dynamic.domain.tld [192.168.0.24]) + (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPSA id EF01E13BA0BD7 + for ; Fri, 17 Aug 2018 16:36:38 +0200 (CEST) +Sender: testsender +Message-ID: +Date: Fri, 17 Aug 2018 14:36:24 +0000 +Subject: ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk +From: testfrom +Reply-To: testreply_to +To: testnameto +Cc: testnamecc +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_" +X-Priority: 1 (Highest) + + + + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +n1IaXFkbeqKyg4lYToaJ3u1Ond2EDrN3UWuiLFNjOLJEAabSYagYQaOHtV5QDlZE + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt + +em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW +eFJjT0VSTg== + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_-- + From 861557cc7519922a5cc3b644ac1faa3cfdc9af7f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:38:11 +0100 Subject: [PATCH 564/600] Filename sanitization optimized to remove any leftover dot prefix --- src/Attachment.php | 8 ++++++-- tests/AttachmentTest.php | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 19b639f6..17d25efa 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -324,13 +324,17 @@ public function decodeName(?string $name): string { } // sanitize $name - // order of '..' is important $replaces = [ '/\\\\/' => '', '/[\/\0:]+/' => '', '/\.+/' => '.', ]; - return preg_replace(array_keys($replaces), array_values($replaces), $name); + $name_starts_with_dots = str_starts_with($name, '..'); + $name = preg_replace(array_keys($replaces), array_values($replaces), $name); + if($name_starts_with_dots) { + return substr($name, 1); + } + return $name; } return ""; } diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php index 566ea658..c9ba2f17 100644 --- a/tests/AttachmentTest.php +++ b/tests/AttachmentTest.php @@ -28,7 +28,7 @@ public function testDecodeName(string $input, string $output): void public function decodeNameDataProvider(): array { return [ - ['../../../../../../../../../../../var/www/shell.php', '.varwwwshell.php'], + ['../../../../../../../../../../../var/www/shell.php', 'varwwwshell.php'], ['test..xml', 'test.xml'], [chr(0), ''], ['C:\\file.txt', 'Cfile.txt'], From 7484a0ed372e8741a70dc3b295c4b2a90debf06b Mon Sep 17 00:00:00 2001 From: grnsv <70471000+grnsv@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:40:14 +0300 Subject: [PATCH 565/600] Fix decode filename (#535) --- src/Attachment.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Attachment.php b/src/Attachment.php index 17d25efa..e5a035bc 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -307,6 +307,7 @@ public function decodeName(?string $name): string { if (str_contains($name, "''")) { $parts = explode("''", $name); if (EncodingAliases::has($parts[0])) { + $encoding = $parts[0]; $name = implode("''", array_slice($parts, 1)); } } @@ -323,6 +324,10 @@ public function decodeName(?string $name): string { $name = urldecode($name); } + if (isset($encoding)) { + $name = EncodingAliases::convert($name, $encoding); + } + // sanitize $name $replaces = [ '/\\\\/' => '', From 490ff39941f531d09c3b61d061b8d362f1b6b23d Mon Sep 17 00:00:00 2001 From: grnsv <70471000+grnsv@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:41:21 +0300 Subject: [PATCH 566/600] Fixed annotations (#536) --- src/Attachment.php | 24 ++++++++++++------------ src/Message.php | 32 ++++++++++++++++---------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index e5a035bc..2e0e5821 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -24,18 +24,18 @@ * * @package Webklex\PHPIMAP * - * @property integer part_number - * @property integer size - * @property string content - * @property string type - * @property string content_type - * @property string id - * @property string hash - * @property string name - * @property string description - * @property string filename - * @property ?string disposition - * @property string img_src + * @property integer $part_number + * @property integer $size + * @property string $content + * @property string $type + * @property string $content_type + * @property string $id + * @property string $hash + * @property string $name + * @property string $description + * @property string $filename + * @property ?string $disposition + * @property string $img_src * * @method integer getPartNumber() * @method integer setPartNumber(integer $part_number) diff --git a/src/Message.php b/src/Message.php index b0dabb2d..66193436 100755 --- a/src/Message.php +++ b/src/Message.php @@ -47,22 +47,22 @@ * * @package Webklex\PHPIMAP * - * @property integer msglist - * @property integer uid - * @property integer msgn - * @property integer size - * @property Attribute subject - * @property Attribute message_id - * @property Attribute message_no - * @property Attribute references - * @property Attribute date - * @property Attribute from - * @property Attribute to - * @property Attribute cc - * @property Attribute bcc - * @property Attribute reply_to - * @property Attribute in_reply_to - * @property Attribute sender + * @property integer $msglist + * @property integer $uid + * @property integer $msgn + * @property integer $size + * @property Attribute $subject + * @property Attribute $message_id + * @property Attribute $message_no + * @property Attribute $references + * @property Attribute $date + * @property Attribute $from + * @property Attribute $to + * @property Attribute $cc + * @property Attribute $bcc + * @property Attribute $reply_to + * @property Attribute $in_reply_to + * @property Attribute $sender * * @method integer getMsglist() * @method integer setMsglist($msglist) From 2d91d33cc100c9256f9623a779d803fa627b13d3 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:54:17 +0100 Subject: [PATCH 567/600] Fixture adjusted --- tests/fixtures/AttachmentLongFilenameTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php index 650a2dcb..5a3e00ed 100644 --- a/tests/fixtures/AttachmentLongFilenameTest.php +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -53,7 +53,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); - self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); + self::assertEquals("cebd34e48eaa06311da3d3130d5a9b465b096dc1094a6548f8c94c24ca52f34e", hash("sha256", $attachment->filename)); self::assertEquals('text', $attachment->type); self::assertEquals('txt', $attachment->getExtension()); self::assertEquals("text/plain", $attachment->content_type); From d760c9f6bbf60f0d7d6687672647919dc684de06 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:55:45 +0100 Subject: [PATCH 568/600] Changelog updated --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c85451bd..b83c9016 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) - Attachment with symbols in filename #436 (thanks @nuernbergerA) - Ignore possible untagged lines after IDLE and DONE commands #445 (thanks @gazben) +- Fix Empty Child Folder Error #474 (thanks @bierpub) +- Filename sanitization improved #501 (thanks @neolip) +- `Client::getFolderPath()` return null if folder is not set #506 (thanks @arnolem) +- Fix implicit marking of parameters as nullable, deprecated in PHP 8.4 #518 (thanks @campbell-m) ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) @@ -21,6 +25,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) - Support for Carbon 3 added #483 - Custom decoder support added +- Decoding filename with non-standard encoding #535 (thanks @grnsv) ### Breaking changes - The decoder config has been moved from `options.decoder` to `decoding` and contains now the `decoder` class to used as well as their decoding fallbacks From 352901f470a98506b6f277af78e0fff04b35d4ba Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:56:54 +0100 Subject: [PATCH 569/600] Alternatives added & discord link updated --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2eb4ef40..ec49921e 100755 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] - [Known issues](#known-issues) - [Support](#support) - [Features & pull requests](#features--pull-requests) +- [Alternatives & Different Flavors](#alternatives--different-flavors) - [Security](#security) - [Credits](#credits) - [License](#license) @@ -193,6 +194,14 @@ first, if you're planning to do bigger changes. Of course, you can also create a if you're just wishing a feature ;) +## Alternatives & Different Flavors +This library and especially the code flavor It's written in, is certainly not for everyone. If you are looking for a +different approach, you might want to check out the following libraries: +- [ddeboer/imap](https://github.com/ddeboer/imap) +- [barbushin/php-imap](https://github.com/barbushin/php-imap) +- [stevebauman/php-imap](https://github.com/stevebauman/php-imap) + + ## Change log Please see [CHANGELOG][link-changelog] for more information what has changed recently. @@ -226,7 +235,7 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap -[link-discord]: https://discord.gg/rd4cN9h6 +[link-discord]: https://discord.gg/vUHrbfbDr9 [ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png From 0aa3b670d3518d53170dd74026b1667a341182bc Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:58:13 +0100 Subject: [PATCH 570/600] Release information added --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83c9016..0e5a032e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + +## [6.0.0] - 2025-01-17 +### Fixed - Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) - Make the space optional after a comma separator #437 (thanks @marc0adam) - Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) From 45f03cfe4774b99989794e3e7eeea65414f28f81 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 16:10:24 +0100 Subject: [PATCH 571/600] Typo fixed --- src/config/imap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/imap.php b/src/config/imap.php index 7c184435..b628a0fd 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -167,7 +167,7 @@ /** * |-------------------------------------------------------------------------- - * | Available IMAP options + * | Available decoding options * |-------------------------------------------------------------------------- * | * | Available php imap config parameters are listed below From f5dd36838cb10973c7e7d6695967ec6a84ef4e17 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 16:27:49 +0100 Subject: [PATCH 572/600] Filename sanitization made optional --- CHANGELOG.md | 2 +- src/Attachment.php | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5a032e..2f972ef9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Filename sanitization is now optional (enabled via default) ### Added - NaN diff --git a/src/Attachment.php b/src/Attachment.php index 2e0e5821..223de45e 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -328,17 +328,10 @@ public function decodeName(?string $name): string { $name = EncodingAliases::convert($name, $encoding); } - // sanitize $name - $replaces = [ - '/\\\\/' => '', - '/[\/\0:]+/' => '', - '/\.+/' => '.', - ]; - $name_starts_with_dots = str_starts_with($name, '..'); - $name = preg_replace(array_keys($replaces), array_values($replaces), $name); - if($name_starts_with_dots) { - return substr($name, 1); + if($this->config->get('security.sanitize_filenames', true)) { + $name = $this->sanitizeName($name); } + return $name; } return ""; @@ -497,4 +490,26 @@ public function setDecoder(DecoderInterface $decoder): static { $this->decoder = $decoder; return $this; } + + /** + * Sanitize a given name to prevent common attacks + * !!IMPORTANT!! Do not rely on this method alone - this is just the bare minimum. Additional measures should be taken + * to ensure that the file is safe to use. + * @param string $name + * + * @return string + */ + private function sanitizeName(string $name): string { + $replaces = [ + '/\\\\/' => '', + '/[\/\0:]+/' => '', + '/\.+/' => '.', + ]; + $name_starts_with_dots = str_starts_with($name, '..'); + $name = preg_replace(array_keys($replaces), array_values($replaces), $name); + if($name_starts_with_dots) { + return substr($name, 1); + } + return $name; + } } From e07b9660366cfcc0e87f7fb56369cac22bae259a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 17:30:36 +0100 Subject: [PATCH 573/600] Spoofing detection added #40 --- CHANGELOG.md | 2 +- .../SpoofingAttemptDetectedException.php | 24 ++++++ src/Header.php | 30 +++++++- tests/issues/Issue40Test.php | 74 +++++++++++++++++++ tests/messages/issue-40.eml | 39 ++++++++++ 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/Exceptions/SpoofingAttemptDetectedException.php create mode 100644 tests/issues/Issue40Test.php create mode 100644 tests/messages/issue-40.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f972ef9..68b0d1e1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Filename sanitization is now optional (enabled via default) ### Added -- NaN +- Spoofing detection added #40 ### Breaking changes - NaN diff --git a/src/Exceptions/SpoofingAttemptDetectedException.php b/src/Exceptions/SpoofingAttemptDetectedException.php new file mode 100644 index 00000000..f3771817 --- /dev/null +++ b/src/Exceptions/SpoofingAttemptDetectedException.php @@ -0,0 +1,24 @@ +rfc822_parse_headers($this->raw); @@ -230,6 +232,11 @@ protected function parse(): void { $this->extractHeaderExtensions(); $this->findPriority(); + + if($this->config->get('security.detect_spoofing', true)) { + // Detect spoofing + $this->detectSpoofing(); + } } /** @@ -413,7 +420,7 @@ private function decodeAddresses($values): array { * @param object $header */ private function extractAddresses(object $header): void { - foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) { + foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender', 'return_path', 'envelope_from', 'envelope_to', 'delivered_to'] as $key) { if (property_exists($header, $key)) { $this->set($key, $this->parseAddresses($header->$key)); } @@ -760,4 +767,25 @@ public function setDecoder(DecoderInterface $decoder): static { return $this; } + /** + * Detect spoofing by checking the from, reply_to, return_path, sender and envelope_from headers + * @throws SpoofingAttemptDetectedException + */ + private function detectSpoofing(): void { + $header_keys = ["from", "reply_to", "return_path", "sender", "envelope_from"]; + $potential_senders = []; + foreach($header_keys as $key) { + $header = $this->get($key); + foreach ($header->toArray() as $address) { + $potential_senders[] = $address->mailbox . "@" . $address->host; + } + } + if(count($potential_senders) > 1) { + $this->set("spoofed", true); + if($this->config->get('security.detect_spoofing_exception', false)) { + throw new SpoofingAttemptDetectedException("Potential spoofing detected. Message ID: " . $this->get("message_id") . " Senders: " . implode(", ", $potential_senders)); + } + } + } + } diff --git a/tests/issues/Issue40Test.php b/tests/issues/Issue40Test.php new file mode 100644 index 00000000..7288484c --- /dev/null +++ b/tests/issues/Issue40Test.php @@ -0,0 +1,74 @@ +getFixture("issue-40.eml"); + + self::assertSame("Zly from", (string)$message->subject); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'faked_sender', + 'host' => 'sender_domain.pl', + 'mail' => 'faked_sender@sender_domain.pl', + 'full' => 'faked_sender@sender_domain.pl', + ], $message->from->first()->toArray()); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'real_sender', + 'host' => 'sender_domain.pl', + 'mail' => 'real_sender@sender_domain.pl', + 'full' => ' ', + ], (array)$message->return_path->first()); + self::assertSame(true, $message->spoofed->first()); + + $config = $message->getConfig(); + self::assertSame(false, $config->get("security.detect_spoofing_exception")); + $config->set("security.detect_spoofing_exception", true); + self::assertSame(true, $config->get("security.detect_spoofing_exception")); + + $this->expectException(SpoofingAttemptDetectedException::class); + $this->getFixture("issue-40.eml", $config); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-40.eml b/tests/messages/issue-40.eml new file mode 100644 index 00000000..52e7ff9a --- /dev/null +++ b/tests/messages/issue-40.eml @@ -0,0 +1,39 @@ +Return-Path: +Delivered-To: receipent@receipent_domain.pl +Received: from h2.server.pl +\tby h2.server.pl with LMTP +\tid 4IDTIEUkm18ZSSkA87l24w +\t(envelope-from ) +\tfor ; Thu, 29 Oct 2020 21:21:25 +0100 +Return-path: +Envelope-to: receipent@receipent_domain.pl +Delivery-date: Thu, 29 Oct 2020 21:21:25 +0100 +Received: from sender_domain.pl ([server ip]) +\tby h2.server.pl with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +\t(Exim 4.94) +\t(envelope-from ) +\tid 1kYEQG-00BPgD-S0 +\tfor receipent@receipent_domain.pl; Thu, 29 Oct 2020 21:21:25 +0100 +Received: by sender_domain.pl (Postfix, from userid 1000) +\tid 57DADAB; Thu, 29 Oct 2020 21:21:23 +0100 (CET) +DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=sender_domain.pl; s=default; +\tt=1604002883; bh=CsZufJouWdjY/W12No6MSSMwbp0VaS8EOMGg9WptEaI=; +\th=From:To:Subject:Date; +\tb=v0NAncnNT/w+gInANxAkMt20ktM4LZquuwlokUmLpPyO3++8dy112olu63Dkn9L2E +\t GwfHGqW+8f7g494UK6asUKqTx8fHxlEJbHqAiEV5QrlynSeZDFXsKvGDW8XNMFBKop +\t sAjvp8NTUiNcA4MTbFaZ7RX15A/9d9QVEynU8MaNP2ZYKnq9J/JXgUjjMnx+FiULqf +\t xJN/5rjwHRx7f6JQoXXUxuck6Zh4tSDiLLnDFasrSxed6sTNfnZMAggCyb1++estNk +\t q6HNBwp85Az3ELo10RbBF/WM2FhxxFz1khncRtCyLXLUZ2lzhjan765KXpeYg7FUa9 +\t zItPWVTaTzTEg== +From: faked_sender@sender_domain.pl +To: receipent@receipent_domain.pl +Subject: Zly from +Message-Id: <20201029202123.57DADAB@sender_domain.pl> +Date: Thu, 29 Oct 2020 21:21:01 +0100 (CET) +Forward-Confirmed-ReverseDNS: Reverse and forward lookup success on server ip, -10 Spam score +SPFCheck: Server passes SPF test, -30 Spam score +X-DKIM: signer='sender_domain.pl' status='pass' reason='' +DKIMCheck: Server passes DKIM test, -20 Spam score +X-Spam-Score: -0.2 (/) + +Test message From 916e273d102c6e4b8f10363a500d8caa6ab94111 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 17:32:22 +0100 Subject: [PATCH 574/600] Address parsing improved and extended to include more cases --- CHANGELOG.md | 1 + src/Header.php | 30 ++++++++++++++++++- tests/HeaderTest.php | 7 +++-- tests/MessageTest.php | 6 ++-- tests/fixtures/BccTest.php | 10 +++++-- tests/fixtures/ExampleBounceTest.php | 8 ++++- tests/fixtures/UndefinedCharsetHeaderTest.php | 14 ++++++--- 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b0d1e1..bba62dc1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Filename sanitization is now optional (enabled via default) +- Address parsing improved and extended to include more cases ### Added - Spoofing detection added #40 diff --git a/src/Header.php b/src/Header.php index c294d7f6..604eb896 100644 --- a/src/Header.php +++ b/src/Header.php @@ -437,7 +437,35 @@ private function parseAddresses($list): array { $addresses = []; if (is_array($list) === false) { - return $addresses; + if(is_string($list)) { + // $list = "" + if (preg_match( + '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', + $list, + $matches + )) { + $name = trim(rtrim($matches["name"])); + $email = trim(rtrim($matches["email"])); + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + if($mailbox === ">") { // Fix trailing ">" in malformed mailboxes + $mailbox = ""; + } + if($name === "" && $mailbox === "" && $host === "") { + return $addresses; + } + $list = [ + (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ] + ]; + }else{ + return $addresses; + } + }else{ + return $addresses; + } } foreach ($list as $item) { diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 767f4388..7c27944f 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -50,7 +50,8 @@ public function testHeaderParsing(): void { $header = new Header($raw_header, $this->config); $subject = $header->get("subject"); - $returnPath = $header->get("Return-Path"); + $returnPath = $header->get("return_path"); + var_dump($returnPath); /** @var Carbon $date */ $date = $header->get("date")->first(); /** @var Address $from */ @@ -69,7 +70,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$header->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("noreply@github.com", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$header->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$header->get("Message-ID")); @@ -91,7 +92,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Carbon::class, $date); self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); - self::assertSame(50, count($header->getAttributes())); + self::assertSame(51, count($header->getAttributes())); } public function testRfc822ParseHeaders() { diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 72e32f5e..0ce55cd0 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -120,7 +120,7 @@ public function testMessage(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("noreply@github.com", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); @@ -182,7 +182,7 @@ public function testLoadMessageFromFile(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("noreply@github.com", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); @@ -201,7 +201,7 @@ public function testLoadMessageFromFile(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", $subject->toString()); self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", (string)$message->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("someone@domain.tld", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("1.103", (string)$message->get("X-Spam-Score")); self::assertSame("d3a5e91963cb805cee975687d5acb1c6@swift.generated", (string)$message->get("Message-ID")); diff --git a/tests/fixtures/BccTest.php b/tests/fixtures/BccTest.php index 8de14f62..be8ef72e 100644 --- a/tests/fixtures/BccTest.php +++ b/tests/fixtures/BccTest.php @@ -24,11 +24,17 @@ class BccTest extends FixtureTestCase { * * @return void */ - public function testFixture() : void { + public function testFixture(): void { $message = $this->getFixture("bcc.eml"); self::assertEquals("test", $message->subject); - self::assertEquals("", $message->return_path); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'return-path', + 'host' => 'here.com', + 'mail' => 'return-path@here.com', + 'full' => 'return-path@here.com', + ], $message->return_path->first()->toArray()); self::assertEquals("1.0", $message->mime_version); self::assertEquals("text/plain", $message->content_type); self::assertEquals("Hi!", $message->getTextBody()); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index c92d35c6..8153518d 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -29,7 +29,13 @@ class ExampleBounceTest extends FixtureTestCase { public function testFixture(): void { $message = $this->getFixture("example_bounce.eml"); - self::assertEquals("<>", $message->return_path); + self::assertEquals([ + 'personal' => '', + 'mailbox' => '', + 'host' => '', + 'mail' => '', + 'full' => '', + ], (array)$message->return_path->first()); self::assertEquals([ 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', diff --git a/tests/fixtures/UndefinedCharsetHeaderTest.php b/tests/fixtures/UndefinedCharsetHeaderTest.php index b2f9d8fb..f7a51cb0 100644 --- a/tests/fixtures/UndefinedCharsetHeaderTest.php +++ b/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -26,7 +26,7 @@ class UndefinedCharsetHeaderTest extends FixtureTestCase { * * @return void */ - public function testFixture() : void { + public function testFixture(): void { $message = $this->getFixture("undefined_charset_header.eml"); self::assertEquals("", $message->get("x-real-to")); @@ -34,10 +34,16 @@ public function testFixture() : void { self::assertEquals("Mon, 27 Feb 2017 13:21:44 +0930", $message->get("Resent-Date")); self::assertEquals("", $message->get("Resent-From")); self::assertEquals("BlaBla", $message->get("X-Stored-In")); - self::assertEquals("", $message->get("Return-Path")); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'info', + 'host' => 'bla.bla', + 'mail' => 'info@bla.bla', + 'full' => 'info@bla.bla', + ], $message->get("Return-Path")->first()->toArray()); self::assertEquals([ - 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', - ], $message->get("Received")->all()); + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', + ], $message->get("Received")->all()); self::assertEquals(")", $message->getHTMLBody()); self::assertFalse($message->hasTextBody()); self::assertEquals("2017-02-27 03:51:29", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); From cafda4fc8514e552c4aebc49cc636c8debdde11c Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 17:32:51 +0100 Subject: [PATCH 575/600] Security configuration options added --- CHANGELOG.md | 1 + src/config/imap.php | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba62dc1..beb11276 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Address parsing improved and extended to include more cases ### Added +- Security configuration options added - Spoofing detection added #40 ### Breaking changes diff --git a/src/config/imap.php b/src/config/imap.php index b628a0fd..8e39fe8e 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -35,6 +35,32 @@ */ 'default' => 'default', + /* + |-------------------------------------------------------------------------- + | Security options + |-------------------------------------------------------------------------- + | + | You can enable or disable certain security features here by setting them to true or false to enable or disable + | them. + | -detect_spoofing: + | Detect spoofing attempts by checking the message sender against the message headers. + | Default TRUE + | -detect_spoofing_exception: + | Throw an exception if a spoofing attempt is detected. + | Default FALSE + | -sanitize_filenames: + | Sanitize attachment filenames by removing any unwanted and potentially dangerous characters. This is not a + | 100% secure solution, but it should help to prevent some common attacks. Please sanitize the filenames + | again if you need a more secure solution. + | Default TRUE + | + */ + 'security' => [ + "detect_spoofing" => true, + "detect_spoofing_exception" => false, + "sanitize_filenames" => true, + ], + /* |-------------------------------------------------------------------------- | Available accounts From 512f9f5e2fb273c4dd7a07986fe1818d36b7430f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 18:01:28 +0100 Subject: [PATCH 576/600] RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 42 +++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb11276..7f197ffc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Security configuration options added - Spoofing detection added #40 +- RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) ### Breaking changes - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 9272be91..da7483b8 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1141,7 +1141,25 @@ public function moveMessage(string $folder, $from, ?int $to = null, int|string $ $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); - return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + $result = $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + // RFC4315 fallback to COPY, STORE and EXPUNGE. + // Required for cases where MOVE isn't supported by the server. So we copy the message to the target folder, + // mark the original message as deleted and expunge the mailbox. + // See the following links for more information: + // - https://github.com/freescout-help-desk/freescout/issues/4313 + // - https://github.com/Webklex/php-imap/issues/123 + if (!$result->boolean()) { + $result = $this->copyMessage($folder, $from, $to, $uid); + if (!$result->boolean()) { + return $result; + } + $result = $this->store(['\Deleted'], $from, $to, null, true, $uid); + if (!$result->boolean()) { + return $result; + } + return $this->expunge(); + } + return $result; } /** @@ -1163,7 +1181,27 @@ public function moveManyMessages(array $messages, string $folder, int|string $ui $set = implode(',', $messages); $tokens = [$set, $this->escapeString($folder)]; - return $this->requestAndResponse($command, $tokens, true); + $result = $this->requestAndResponse($command, $tokens, true); + // RFC4315 fallback to COPY, STORE and EXPUNGE. + // Required for cases where MOVE isn't supported by the server. So we copy the message to the target folder, + // mark the original message as deleted and expunge the mailbox. + // See the following links for more information: + // - https://github.com/freescout-help-desk/freescout/issues/4313 + // - https://github.com/Webklex/php-imap/issues/123 + if (!$result->boolean()) { + $result = $this->copyManyMessages($messages, $folder, $uid); + if (!$result->boolean()) { + return $result; + } + foreach ($messages as $message) { + $result = $this->store(['\Deleted'], $message, $message, null, true, $uid); + if (!$result->boolean()) { + return $result; + } + } + return $this->expunge(); + } + return $result; } /** From 0a9b263eb4e29c2822cf7d68bec27a9af33ced2f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 19:36:00 +0100 Subject: [PATCH 577/600] Boundary parsing fixed and improved to support more formats #544 --- CHANGELOG.md | 1 + src/Structure.php | 5 +- tests/HeaderTest.php | 1 - tests/fixtures/BooleanDecodedContentTest.php | 2 +- ...lWithoutContentDispositionEmbeddedTest.php | 4 +- ...ddedEmailWithoutContentDispositionTest.php | 8 +- .../MultipleHtmlPartsAndAttachmentsTest.php | 4 +- .../MultipleNestedAttachmentsTest.php | 4 +- .../NestesEmbeddedWithAttachmentTest.php | 4 +- tests/fixtures/PecTest.php | 6 +- tests/issues/Issue544Test.php | 61 +++++ tests/messages/issue-544.eml | 220 ++++++++++++++++++ 12 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 tests/issues/Issue544Test.php create mode 100644 tests/messages/issue-544.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f197ffc..c532bb97 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Filename sanitization is now optional (enabled via default) - Address parsing improved and extended to include more cases +- Boundary parsing fixed and improved to support more formats #544 ### Added - Security configuration options added diff --git a/src/Structure.php b/src/Structure.php index 4907c542..11f4cd66 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -132,7 +132,10 @@ private function parsePart(string $context, int $part_number = 0): array { * @throws InvalidMessageDateException */ private function detectParts(string $boundary, string $context, int $part_number = 0): array { - $base_parts = explode( $boundary, $context); + $base_parts = explode( "--".$boundary, $context); + if(count($base_parts) == 0) { + $base_parts = explode($boundary, $context); + } $final_parts = []; foreach($base_parts as $ctx) { $ctx = substr($ctx, 2); diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 7c27944f..0ec60c6c 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -51,7 +51,6 @@ public function testHeaderParsing(): void { $header = new Header($raw_header, $this->config); $subject = $header->get("subject"); $returnPath = $header->get("return_path"); - var_dump($returnPath); /** @var Carbon $date */ $date = $header->get("date")->first(); /** @var Address $from */ diff --git a/tests/fixtures/BooleanDecodedContentTest.php b/tests/fixtures/BooleanDecodedContentTest.php index e49229a4..5d15fe57 100644 --- a/tests/fixtures/BooleanDecodedContentTest.php +++ b/tests/fixtures/BooleanDecodedContentTest.php @@ -48,7 +48,7 @@ public function testFixture() : void { self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); self::assertEquals(53, $attachment->size); - self::assertEquals(3, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php index a620936b..387b0de0 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -55,7 +55,7 @@ public function testFixture() : void { self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(3, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -67,7 +67,7 @@ public function testFixture() : void { self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(4, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 36287a6b..2ec5a4fa 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -54,7 +54,7 @@ public function testFixture() : void { self::assertEquals("image/jpeg", $attachment->content_type); self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); self::assertEquals(96, $attachment->size); - self::assertEquals(3, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -66,7 +66,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); self::assertEquals(2073, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertNull($attachment->disposition); self::assertNotEmpty($attachment->id); @@ -78,7 +78,7 @@ public function testFixture() : void { self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(6, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -90,7 +90,7 @@ public function testFixture() : void { self::assertEquals("application/x-zip-compressed", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(7, $attachment->part_number); + self::assertEquals(5, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php index c08c03e0..48bc0172 100644 --- a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php +++ b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -57,7 +57,7 @@ public function testFixture() : void { self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); self::assertEquals(4814, $attachment->size); - self::assertEquals(4, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -69,7 +69,7 @@ public function testFixture() : void { self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); self::assertEquals(5090, $attachment->size); - self::assertEquals(6, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/MultipleNestedAttachmentsTest.php b/tests/fixtures/MultipleNestedAttachmentsTest.php index 543c0e21..a3cba097 100644 --- a/tests/fixtures/MultipleNestedAttachmentsTest.php +++ b/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -50,7 +50,7 @@ public function testFixture() : void { self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); self::assertEquals(114, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -62,7 +62,7 @@ public function testFixture() : void { self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); self::assertEquals(114, $attachment->size); - self::assertEquals(8, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php index c5c8c561..e1ff57ea 100644 --- a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php +++ b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -50,7 +50,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); self::assertEquals(2535, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -62,7 +62,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); self::assertEquals(631, $attachment->size); - self::assertEquals(6, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/PecTest.php b/tests/fixtures/PecTest.php index 28ecb27b..341ad185 100644 --- a/tests/fixtures/PecTest.php +++ b/tests/fixtures/PecTest.php @@ -51,7 +51,7 @@ public function testFixture() : void { self::assertEquals("application/xml", $attachment->content_type); self::assertEquals("", $attachment->content); self::assertEquals(8, $attachment->size); - self::assertEquals(4, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -63,7 +63,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); self::assertEquals(216, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -75,7 +75,7 @@ public function testFixture() : void { self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); self::assertEquals("1", $attachment->content); self::assertEquals(4, $attachment->size); - self::assertEquals(7, $attachment->part_number); + self::assertEquals(5, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/issues/Issue544Test.php b/tests/issues/Issue544Test.php new file mode 100644 index 00000000..b8ee0e7e --- /dev/null +++ b/tests/issues/Issue544Test.php @@ -0,0 +1,61 @@ +getFixture("issue-544.eml"); + + self::assertSame("Test bad boundary", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + /** @var Attachment $attachment */ + $attachment = $attachments->first(); + self::assertSame("file.pdf", $attachment->name); + self::assertSame("file.pdf", $attachment->filename); + self::assertStringStartsWith("%PDF-1.4", $attachment->content); + self::assertStringEndsWith("%%EOF\n", $attachment->content); + self::assertSame(14938, $attachment->size); + } +} \ No newline at end of file diff --git a/tests/messages/issue-544.eml b/tests/messages/issue-544.eml new file mode 100644 index 00000000..96e2c48c --- /dev/null +++ b/tests/messages/issue-544.eml @@ -0,0 +1,220 @@ +Sender: "test@mail.com" +From: "test@mail.com" +Subject: Test bad boundary +To: "test_1@mail.com" +Cc: +Bcc: +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="-" + +This is a multi-part message in MIME format. + +--- +Content-Type: text/plain + +This message may contain an attachment in a PDF format. + +Special Comments +- +See attached document + +--- +Content-Type: application/pdf; name="file.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="file.pdf" +Content-MD5: MLGn6wT7mIo/SUBWQ/mmng== + +JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoZmlsZS0xLnBkZikKL1Byb2R1Y2VyIChT +a2lhL1BERiBtMTE5IEdvb2dsZSBEb2NzIFJlbmRlcmVyKT4+CmVuZG9iagozIDAgb2JqCjw8L2Nh +IDEKL0JNIC9Ob3JtYWw+PgplbmRvYmoKNSAwIG9iago8PC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9M +ZW5ndGggMTkzPj4gc3RyZWFtCnicdZBNDgIhDIX3PUUvILZDoZAYFxrHtYYb+JeYuHC8fyIwo2NM +oASa99FHgZFyLDgvGjs8PeAJRl1VP3sWGUsc9zgmww2We4u3FxQe2COT8zhc4AqHPwftysweVKXs +MSazxybBshdkMb4MxXQFnvsyojGw2oipmCzYmk7Vig2YzrgikrjGdAcxpBzVaa6ZwLaCfN55lmhn +0LdAqCAYdsyk4QuYpjtCIC/uB0irghtdOduoEG2B8YG7lD/3DS1VWPgKZW5kc3RyZWFtCmVuZG9i +agoyIDAgb2JqCjw8L1R5cGUgL1BhZ2UKL1Jlc291cmNlcyA8PC9Qcm9jU2V0IFsvUERGIC9UZXh0 +IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQovRXh0R1N0YXRlIDw8L0czIDMgMCBSPj4KL0ZvbnQg +PDwvRjQgNCAwIFI+Pj4+Ci9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgov +U3RydWN0UGFyZW50cyAwCi9QYXJlbnQgNiAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlIC9Q +YWdlcwovQ291bnQgMQovS2lkcyBbMiAwIFJdPj4KZW5kb2JqCjcgMCBvYmoKPDwvVHlwZSAvQ2F0 +YWxvZwovUGFnZXMgNiAwIFI+PgplbmRvYmoKOCAwIG9iago8PC9MZW5ndGgxIDE3ODUyCi9GaWx0 +ZXIgL0ZsYXRlRGVjb2RlCi9MZW5ndGggODc4OD4+IHN0cmVhbQp4nO16C3wURdbvqerueSWTzOT9 +mGQ6DAmSAQJ5kBAimZAHaowECJggSAJEAogEAoiPlbgughGF9VNUdAXf+GQSIgbUJauuu6IIq6gr +PoiIiroI+ikqSPr7V80EQtR75d7vPvz9tpvzr1NVp05Vn3PqdE0HYkQUCVBp6JiS0jI2n80g4nFo +zR9TOXbCssvWX0SkiHrLmAkTR9v+bF5FxPyoDx07ISNzqefpNMhXoV47qaSiuvKWOd8SZZ9F5Lx1 +xry6RraXf4X+dvTPnLFkkX6v6+0viEwHQFWXNM6a9+oVNeuI7HmoXzarrqmR4sgK/QWQd8y69IpL +Si/57HmiAZgvdEnDzHlL93yTeTsWvJHIck5Dfd3MrqiXoI91QX54AxoisqxJqGON1L9h3qKl/ZZo +DiLtVrRVXjp/Rl32j8Ma8DwPoP/ReXVLG7VN9mb0nY+6flndvPrY2mHvwxiVaCtpnN+0yEinteAv +Ff2NC+sbU9+q2E6UeIQo5M9oU8hCnJzEDAO8sGUVfU0F9Ccyo91BGTQJ2h6HrIa6QvIyBgidP3Nh +vHlU9wVU7KBjm45d6ZAtp11VssVGQ3BrdQvrppM+44qFl5I+a2H9XNIb6qcvJP3SukWXkX5KJ2nx +6+4qOfz2tPCCby2JFtl830cD0kX5SuXILcc2nZjlIEsoqtaTM4oyNMhzPE2gzSGelaJwMxpD5wLP +x81oLG5GE3CLcYqykq3BE1u0dVoWhiUGSuUfdAmPsGg8xKRycanSgr2uirEXjCUfDaYntDe6x7Es +8yjW5hPGNWDFNO0Z4QVS5YoCI6OCVo9Ca6WIGdyifQgNo0toNl1KjfSEGA2dw2jmqRbjo173jJ/Y +mYJ2Hk1TSJEWHhSwsLRtlFwDQS/DSk7xrBeP9c2tXwjZ3hjUK+Q4IodJCgs+kYtipL4oaddzjXeF +pY0GuZro/8mdhzgT9zv/N252kB9SHhS3ukrcWmHgNo2UazXTZOEVFfuY5lBzkGew5pIgz/HMDUFe +gb/PCvJqLxmNEiAV4E3giIpoIfxXBw9WYB9MonrUm9Ayn0S050ifD0V/hWyZT4voCvi6Hn3n0jy0 +z4LsZUAdsaD30qbTeEjNosXg69B6eu2U3COQzMQMw3DrWEGD1P3T2YpRWwheYB3aAyscIue8NDjf +bMzQgL6m4OxN8mmWAGfSENPPhOOZXPxRKuIjaFtPXW2iff9bCv8/uvAsZaByPF9kTxv7G63sxV/X +R36Fto3iQQnawxSvpuENQ8anoIOi7J5tHBT9ouSfY0BHkIg20hNsNj1B2+l5dgSjNtFWaqe/UyyV +0N10Nd1KKxCZk9FyA3w4HhFbQreyeKMdWf9eRO69tBOyF9I18EQMizM+o2W0XHkDo5aTnfohGioR +GTex843FyDL71OsoF7n0MmpkzUa1cbNxi/EAPUhblb8bJygEu2EG7p3Gl9o/jfcQwVPoNrqT9rFb +rE9hB12IfbZV+RNiaJ0yVWXGLOMYVpBCl2MNKmJ0J+vkXmivp09ZHLtaKYaW+w2/8SKkXDQVsbiO +trEcNoanaFOMCmMnctFgWgqtd1IbbcHdQc/RXhaqHTEeMI5QPA3CrloGe7zGOpXuE9d2F8JiGqw0 +kEagZz79mf5Gu5mH/YXP10K1TM2nXWnsQW4bRhOx2ocx8hP2Hb8G9zLlJbXMGI09vpz+KKxNf6UP +WQLLYGPZJD6Qz+f3KAuRKQfJnSdy+A10B7R/wLxsCw/lu5T71cfU46ak7i4jDB5Jo7vw/v0Ls+NJ +ddbEfs/eYh/xYj6N38X3K7eqj6ivm+vw1BcjK9xEj9F3LILlsXHsItbArmYr2B/ZnWwn240sV8Sr ++Fx+WGlQFijPqaNxT1Cb1Ou067UbTQe7q7tf7P5H93dGpnE9jUM8XIvV30b34Mm20i6ZKffRfqax +EBaGW2cpbCK7Cvc17CZ2H9vIHmHtmGU3288+Y1+zb9lxjsTITTyRp/B+uD18Ib+c38rv5rtw7+b/ +4j8osUo/xavkKAVKjTIfq1qhrMH9lPKhmqDuUg3YOVNbq63XNmqPac9rR0yh5t9byPLqj/efSD/x +QTd1r+xe293W3W58iHdGPGLKRW6cVsYhT9UhVy/FueRBxPkbLBS2S2DpbBQ7H5aZxuawBWwpLPkH +to49KNf+JHsWVnqbHcaa7dwl1zyE5/DRfCzui3k9X8DX8Ft4O3+LH1PMSogSrkQr6coYZapSryxS +rlDWKn7lVeV9Zb9yVPkRt6HaVLfaT01TveoYdZq6WL1H/VT9VJuivaJ9bLKZ5pmuN3WYvjIPN48y +V5rHmaeaV5u3mPdYahGdL9BT9HTvfc+6lGuVUuUpuplnqfH8Nf4a4nkazVQqOCKVb2Qr+e9YO++v +LTWN5CPZBXRETYOtX+Lr+VE+Uqlg5WwCzeHDAtpMUeqjKArUF+iQ+iye7TVoXmoKZdfww6ZQasNr +ewTm/KsyVPUqr9BeZR8zq/fSu6qNxbJD/GGlElHwnDpKq6YU5W56UlnAfkdP8VIcMY5bViGOL2CP +Ii9UsUz2vYKTJL8AUZSrfIRcNpf/kw5hH6+k29lMdRbdTFnsavqUHsKuGKhdZko3RbOX+Wy1hUey +duLqI3i6Eaw/U7Qo+gObqqwzHebv4G22S7XRB8rjWP0u/qRSoR7RxrMG7IDf0fW0wLiWrtCq1dfZ +LFLYJEpVu5DdrlYy1RSUy5BVpiCnbcHu3oY8UKRUoCUOkXM+4mIiMsQ63HcgT6iIoNnY4xcii71G +7aYq3kGztDCGrINM/Er3eJpsPER3GrPoMuMWGox8sMK4Gho30se0mjay5d1X4b2ZjJ3zATtfK+O7 +tDJjMG/h7/AJfO3p/oW1U1kcfY77SVRG4WzYor6N82ehscp4E9F9FjLsnTSdzqMDeMovMcM5Sidl +dV/AW40ypRHPu4/GGQ8bbmajBuNSnF+fpQfNGtWZvfCxn72O572K6vl4Y5FS3z0bdlgNK/hgrcXI +Pzf4iidWFfkKR51dMDJ/RF5uTnZW5rChGUMGD/KmDzxrQFpqf0+/FN2dnORKTIiPi42JjoqMcDrC +w+yhITarxWzSVIUzGlTqKavV/Wm1fjXNc845g0XdU4eGul4NtX4dTWWny/j1Wimmny7pg+QlfSR9 +AUnfSUnm0AuoYPAgvdSj+3eWePQONnlcNfibSjw1uv+Q5Cskv0bydvApKRigl8Y1lOh+VquX+suW +NLSU1pZAXWuIrdhTXG8bPIhabSFgQ8D5Yz2NrSx2FJMMjy3Nb8WJ145F+RM8JaX+eE+JWIFfSS2t +m+mvHFddWpKYklIzeJCfFc/wTPeTZ7Q/3CtFqFhO4zcV+81yGn22eBq6UW8d1NmyqsNB02u9oTM9 +M+umVPuVuhoxh9OLeUv8sVceiDtVhfKI4uoVvXsTlZbSuNm6qLa0rND9G8ZV9+5NEVhTAx0Yy1PL +alvKMPUqGLF8go7Z+PKaaj9bjil18STiqQLPV+8pFS21c3S/1TPa09AypxauSWjx0/grUtoSEnxb +jS5KKNVbqqo9Kf7CRE9NXYmrNYpaxl+xOd6nx5/eM3hQq8MZMGxrWHiQCbX3ZupP9klOiguufPxJ +yzKxIs+5CAi/PkPHSqo9eKY8AfV51DIjD2K4ahhG+WfCI7P91uLaFke+aBfj/Vqqw6O3fEuIAM+h +f53eUhdsMaU6viXBijg5GWro7+H9Xq8/PV2EiLkYPsUaR8l6zuBBSzq4x9Po0FHAfFQJ29bV5GfA +/CkpwsE3dvhoOir+5nHVgbpO0xPbyJfhrfHzWtHT2dMTPVH0NPf0nBxe60Ekt8sTdrTfknbyX7gj +JrK0Id/PYv4H3fWB/vIJnvJxk6v10pbaoG3Lq06rBfrzTvYFOX9kcbWSyIMcT1RkL4JyyklhUakO +9aup+GeSQT2zw2xBVMoWppf5HbXnBLDGlpLyKwd1GEfEKFmcGhZcpj/fe3p95Gn105YX2qJgwXhV +lldNbmmxndaHUAtMeG6wQMRTVXWKXuynidiZqfjXYXTmCapJ9PtgsmIhgPgLNAWrpwkmBvkaXCI6 +Bw8qQ6JraSnz6GUttS11HUbzdI/u8LRs5c/z51saS2t7AqfD2HZjor9sVQ1s1cDysSk4jW71sJXj +Wn1s5YTJ1VsdRPrKquo2znhx7eia1v7oq96qE/lkKxetolFUdFGhcoaHbOMWKZ+41UfULHtV2SDr +MzoYyTZLTxujGR080OboaeNoUwNtPtkmLpFjiquqe0eP3JI1g+ULT3yDqw6xWPr+IlJwPjmDX1DB +y2RS5WcU8SUFw0Ot1p/Raz5zvWazIj/NiK8wGG632frqVf879IaHhPQR0KC3r21+xWWxqCf1Yrgj +NLSPgNDb1za/4rJaVfFRVerFcKfd3kfApP0v6tV6640MC/sZvX1t8yuukBCT1Cu+RWF4tMPRR8Bs +OvVB8Ez0hprED8MevXEREX0ELCJKzlxvmN0s9YrQhwkSo6L6CFjhzfAz1xsebpZKhV4MT4qJ6SNg +E1Fy5nodDosMXBH6IvHExfURCIE3+9rmV1xOp+2kXidRSnz8f4/eiF56MTzV5eojEGoTnyXP+IqO +OqUXw9N1vY9AGIIs9sz1xsbaxQdb8U8MH+Lx9BEIR3/CmetNSAg7qRfDM9PS+gg40d/XNr/icrnC +5H4SWwrDhw8c2EcgAtGXfOZ6k5PD5X4SWwrD8wcN6iMQKaLvzPXquuOkXgwvzszsIxATiSg5c72p +qZFyo4mtiuHleXl9BOJFlJy53vT0aLkhnCSHTxg1qo+AS0TJmesdMiRObgixpTB8SmlpHwE3oiTr +zPVmZSWKP2PIvxFg+Mzy8j4CKYiSvrb5FVdeXhJ8I/+J4VupSjlrc1qce/ezykDqAnFlYJs3yb1V +GaAktY10+zoUz+aI6MzwosGKjrNLhkQdOB+0CbRdEX+DmaYki78HAZeBmkGbQNtBu0HInkDRq4Pm +g9aDukSPkqS42nS3o2iAEo+x8TiBhCuxdBhkgBRyAzNAY0HTQKtB60EmKSda5oOWgbaDjsgenxLb +dksW1h7bdqMsNs+5NFNW6wLVKVNldfOFNYGyYlygLDk3IJYfEBuWHWgeMjpQDhgUKCNSM5tFabNn +dhbFKDF4yBgsvBHI+IsUzhi5aYMSTX4QV0zBFp8Ssbl/Wub67YpKTOEKo5nkNjoV1mZ3ZhbZuMEP +I5Dc/Et+KNDDD20Oc2auLzqP76dNoO0ghe/H/SH/kJbxLmFzYCFoPWg7aBfoMMjEu3Dvw/0B/4DC ++fuUASoETQOtB20HHQaZ+ftAB39P/M6RKPhCEOfvAR38XTzWu8BwvhfcXr4XS3ujLXdE5lbJeDOC +jDs1yMQmBpmImMwO/nrbDwMRUWnwNCLqGaUfjaIspV9b6jB3hxLXVjDb3cE/2qx73RuKhvI95AeJ +o+gezLyHdFAlqBbUCDKBewvcW9QMWgPaAPKDEGVAB0jnO0Cvgt6ioSAfqBJk4bvbME0H39WWNtpd +FMNf43/D68HNd/K/y/JV/pIsX+F/leXLKJNR7uAvtSW7qSgE/YQxDpQOlBno1/hfNvePcBtFTr4d +tnMDM0CFoLGgaaDVIBPfzvu1zXRHQMkztAOvfzdvo89k+RDdZyHfHLcvrRgBqAtIyz8bHGC9vj6N ++9LW3omqgLSbbwEnIO0Pq8AJSLvyWnAC0i5dAk5A2sw54ASkTZ4GTkDa2CpwgA5+z9P9B7hzx85l +elE4vxxWuhxWuhxWupxUfrm46QdVrO2utvR0WGydzzsw3d28jTU/y5rHs+b7WHM9a76GNV/LmgtY +88Ws2cuaXaw5mTX7WPMzLA+maGa+9tOqI3xxrHkHa36CNTex5jTWnMqa+7NmneX6OnhK27lZsiiV +xeYiselQnj0K2Secp8CiKYj5FOSE7cBdIEPWfBDS+wWE45NF2W9zemGgPiQ/c37ROfwFDHwBbniB +9oFUOOgFhNELUPICFIQDC0HTQJ2gwyADZIJ0Pyx8tcRwYAaoEDQNtAx0GGSSyzkM4jQ/uMRNcmEZ +wUWPFTX+Am7xIT+Fp/iSHC6H13GOstrFwpPZ2GQjmeeSPFtGOC3ODmbf8p39++/sZC2y8pv5akqC +I9YEy9VtPyS5O9gdbWnPuIui2e2UrCLq2AhKY6ko86hJ1nPIZRFlNrn4Yygz21yTMCy8LW2QexsL +E6O2uH9wHXB/5urgYA+6nnG/rXeorM39Jloe2+Le47rB/XJGhwUtz6Z1MBTbdCm61ZXnfmKHFL0W +Heva3NeIYov7d64x7rku2VEf6Li4CTVfuHt82mT3OdBX4pru9jVB5xZ3oetid0FAKkeM2eIeiiV4 +A2w6FjvQJSf1JEuFE3M7WINvkHmtudo81jzcnGkeZE4xu81J5kRzlCXC4rCEWUItNovFYrKoFm4h +S1SH0eXzir+vR5nkf54Qv3EZqZJ3cJJ/Xpd/gufMwuk88kcq5bx8wmhW7u+cQeXTdf/RCZ4OZhs3 +2a95RjN/RDmVV43253nLO8zGeH+ut9xvrryoupWxm2vQ6ucr8Wu/qrqDGaJpeaL4vriVGHMuvylR +lGctv6mmhuJilhTGFUaMco4oK/kZqA2i99QVdxqf5F9bPqHa/2hSjT9TMEZSTbn/P8QHyK3sa3ak +tGQr+0oUNdVblVHs69Lxol0ZVVJTU97BJkk50tlXkEPEfCXlLHgxCznSLckBuXUBuVSMh1x/UUDO +aqVUKZdqtUo5lQm51qb+pSWt/ftLmVidmqRMU6zeW2ZHKmRSU6VMTDPtkDI7YpqFjH+UFHG5IJLs +kiIsgVxSxMUSpMikUyIZQZEbTorcIGdS2CkZV0DG3tUjY++CjPfXXvWjvV62eWTNjCni422tp7Qe +VOu/cUlDnL95uq63zqgJftVNq50+o0GUdfX+Gk99iX+Gp0RvHTnlZ7qniO6RnpJWnBOrqlun+OpL +2kb6RpZ66kpqNo+pzM49ba4bTs6VXfkzyiqFsmwx15jcn+nOFd1jxFy5Yq5cMdcY3xg5F8kYr6xu +tdDomuIpgXIzD7EhXmsTU2pGxzgaR8ngHZkSd03iNpxWNlKIt8Yf6hntt4NE1+CiwUWiC3tKdIWJ +L/TBrrhrRqYkbmMbg10ONDs9o8m7aHHTYoornV0S+NeEC02LFguDB9Db9EsX+kr9vrqSpkX4VeBP +n1DuLxw3ubrVbEZrrXgkf35PW0hIaYfRGWgcgsZ80agoJwVFW4Fos1qDgj/1/+JgWSx2QTN/ZjPz +JbNF1FSj+JPLqzhSQVXwU+g2nKXE66GpBg/YxLysqUdHcNleLwXqJJ65hxYtDnJBWywKloGRGNLU +Y5KTlzCW96TFFkGhyFwkvnto4gsQfkOnOFOcqQDxX4p+1JXOH30aHSdd7RRproh18Dl8HiQH+eIb +eaPCK1gF58xDPEFrhEC82nhTnPcCx4Gpjk8oo+LQsKG0gE2NzEmJLuIDWcdTTwkt2wAraCe0pPri +eAHZeME0mk/LaBOpG9C/Qb33jjiv4+jUqYeoECqycrKit+3cuVOM3YcFH9c68et5r6/EFhoaOnoi +SbSFhYSAl2iz2O3gJSo+uzN7rrqMr+Z3WtTHVWYlk8YVq8ZCOdthI7jPZ0vxZA8lJn6XIOG3Oxx8 +IpjPfc7wcHCu0FBgmN0uW4/44sPDTRPFdz2BdjswIVTz2cOzNaErTOjSmK75NK7Fh2xjBWw5BUyy +wIuHCnoClYKKEwVUWBg7gjlHDBvKptJULwt0pnicJpM5Z/jw3Cx+vL3ojarb92csUq8adbX7yTE7 +puGVU2YcVPZp2/BbNIlt911t46o91Z5tL7FrOVE5rgt5lW181ATXLD5Tq7fOiKp1dbr3aG9Gvh// +ceTHUYdjv4j/OKnLbbhj3G5vQkFMQUJ5QqN7jds8hPe3D4nJ5zn2cl5qL4s613WhbZJ9lv1j06cx +x9g3YQ4WrYSFOMIp0RVidpIt2qWExHUY37cLOwvmaeGIuCxhxa+flu5IdYb3CID5pl0IgPnaN0B0 +h6c6HLudzOH0OWudzU7V7QsJ4RPdPmFqZ4RwgxODAm5wmsLCgHGyT2gICQkxTXSGORwmUf+yXfjD +GZgswPhqxWzORREyDiJklETI+Ijob3aImtkherabd5n3mQ2z6jYX4lygmJPFKsxxwu3mZDGfOVTM +ZQ4Vms0JYiJzfHJ2pfDqNyfd6a04BOZEr902dUGBQ7Q5TngLDmCvFR4qLBDkHOGMgMdpKlswlRak +5Jg8/dLScrIjhmdlxsQ6s5wsKiYrc3hOdpqnn0nJq39x2ZuL5+y5rnZtxuYT+uOLlzy48aql915/ +z6rj969nSsu4Ih52rIxHvLrjLy/tffVF8f293DioJqujKBrRcY8v1k2uaD5RmapNtU4MqVfmavOt +9SGW6A7jQI+pDvjGCy7JJXBAxDvasaijCeqwiPz4Ya6iiIqEIte4iCnx4111EfMS6lxLTUujj/Kj +cQ6KYeH22NjKmNqYRvyEdYWvcWxwcIdDTXTZzLSNP0rM6JS7icmdIVzlYIzdFulSQ2KxlWQ4gPm6 +XXgFzPdbhENiffYO4712YXm78KxYFZjPpYvtQpV1QHq2387sCW7UNqemZYvy6WTsPDdzx3QYP/qm +CEUxWQ6LmMIhve6QceDob/b1T8/u8bWMCuFZoN7L7y7p9zDpd5f0eIz0Pvye28vvcLK3Qvj8ANoQ +A0cXiDYZCXD3ianoKDwUMSJjasGJBQUMbh8R0bPXcTpYsJDFmuB9cjooK5OcUeaUGOF6lpI2QDr/ +4m2Dvtz6WfdhFvXemyyM/XjQ1rZ8xqoTe/m40LxJN1z9CJsUe387czOFhbKzuj/o/sGhb9rWwG67 +vrjhIZEtIxEOzdobFMsG+pKjrCw8PiN+aDxSd/xdoXfbH7FbEuxn2f3xnfFqvDCrL8GdnWSxK6Hh +LhuL5t6oSFUxkW19FIsyIqUNI31qrGRipTFjpfliU1X8OLmFiX3fuXlYXrZMql6XO3sNsXif2L3x +Pjt2L0XJ1HqWTKv9xH6mQcG0iv0s02yUsLhIvu0iWsB80i7TbYdx7GmZce+Pi3+WbaMUOspsyKve +o703nNdb4PimwFEgd90h76GpyK8FBQUnsO1GiCRbfIUvyuE0Wc0mi4mbHNaIRHKawhPxyvWmX3st +82I/LsxyenKycrJzh2M7xpqFG6Kjs6I9zrb16yMTrlty/pTEvMzxJbt2KetWLZibXXZhxJ9sZbXT +V/14CXbeSvE/zZGX8SZlD/viuU2YSpFokmiWiO3wowx56mG0HkYVwZskOB4ibKtINEk0S8TgE3Lj +UA+j9TAYfMKXJDi8EMS7T6JJolminFm+HHsYrYeRM+cLzjpcbISx1jXWDVa/tdO6z3rEaiar29po +bbauDzZ1WQ2rzW1lxMwqXqcmRfh8sJz1GvxO0kyqzWRO1Uhdr25Q/Wqn2qWaOtUjKidVV3ejpqoi +WYuAAHPMFyvCQVVFEKg2Mb8qQ0HtCQUw3TITyHXaRCyoF1jGVMadeqci4S4skO9U+Nsrd5sgsd8W +LviFU7rXG4nThYKsu7K9vV39Yteu49Fq2vG9YvdcB8gV3mQVPntvX57mP5+zr7dO85DP2dcfp/nA +FyKdIC0uzg+bc/PkOWJzdk6gHDosUPZLDZwvUqNjs8M1t7Ze26epYwFHNMWtNWrNmqHhVzsOU0pq +YCPmBjdiNKJ5PbFOOiJ+rOq0m7pwtusxvthZviS5F6XxSRo/uA8twU0YsDwYQ+ZgOukCukA93QXC +B9iF0gvC8KLW98qCta9r17YdKxNvqxXds9UUvK0iKJmt9S0KdQx2nO0od6iFul/nbn1gqCcpMzoz +aXRSo75Gt+TH5ieeF3teYo3lotApsVMS51jmhs52zIudm9ipvxH1ftz7CW8kH4g6kNylG3qMR/U6 +vNE5ar6jTD3PMdnxccgXSd2OEGcYXlYuk5mZYlxhIRQW3/MGiu85osSL44NbeCa+/24bc9h8tlpb +s03V5QFFlwcUG3KTL0SkMltcsH6sXVjQJt5Zwnw2oU5kLzAHfTnCXrZFLDKLZwWPJIHDSOBgkkrU +ydgatoH52RGmulkhG4u8LtOB8A5ziEmYQ8zAHGIZTJ5KIHFUbhApGiOmY6FiKhYhfMbi3WNy41jv +N5bcJhUO8db65oDjxKnWwFsLJ5TCQ84RwRMKZGlBpDMrWqTDmJjoKC5OKwOcSq8zyooH8m9pWLl7 +zuJ9V01ePcT50JKljz28qKm1e7b2XMu4cauMO+7vPn7j+fknjisP7HzxlTdf2fF24C8JCnaWuDRF +YRxvjzjtXyGd9L3FIAtZjG6yktU4gZO+Tf5/5RBgKIUC7WQHhkkMpzCgg8KBTuCPiCMnMJIigFEU +CYwGHqcYigLGUjQwDniM4ikWfALFg0+kBKBLYhIlApPJZfxAbok6JQFTyA3sRzrQA/ye+lMKMJX6 +AdOA39EA8gDPov7AgZQGTJfopQHGURpEZwEHSxxC6cAM8gKH0mDgMOC3lElDgFmUAcymocY3lCNx +OA0D5lIWMI+yjf+kERLzKQc4UmIBDQeeTbnAUZQHLKQRxtfko3xgEY0EjqYCYDHwKyqhs4GlNApY +RoXGERpDPuA5VAQ8l0YDz5NYTsXA86kEWIHfH4fpAoljaQywks4BjqNzjS9pvMQJdB6wCmfRQzSR +KoCTJF5IFwCraazxL6qhSuBk4CG6iMaBn0ITgFOpCnixxGk00fiCamkSsI4uBE4Hfk4zqAY4kyYD +6+ki4CU0xfiMZklsoKnA2XSxcZDmUC34uRIvpTrgPJqO9stoBnC+xEaaaXxKC6geuJBmAZskLqIG +4xNaTLOBS2gO8HLgx7SU5gKvoHnAK+ky4FUSr6b5wN9RI/AaWmAcwI9agc3UBLyWFgF/T4sN8f9w +lwD/IHE5XW7sp+tpKXAFXQFcSVcCb6CrjA+pha4G3ki/Q8sq4Id0E10DvJmWAVfTtcA1wC76I/0e +eAtdB/wP+oOxj26VeBstB66lFcDbaSV67wDuozvpBuA6ajE+oLvoRuDdtAr4J4n30M3A9bQauIHW +AO8Fvk/30R+B99MtwAfoP4AP0q3Ge/QQ3Wa8Sw/TWuBGuh34iMRH6Q7gY3Qn8HG6C/iExCfpbuAm ++hPQT/cAW4F7qY3WAzfTBmA73We8Q0/R/cY/aYvEp+kBYAc9CNxKDwG3SXyGNgKfpUeMt+k5ehT4 +Z4nb6TFgJz0O/As9AXyengS+QJuMt+hF8gP/Sq3Gm/SSxL9RG/DvtNnYQy9TO3AHPQV8hbYAX6Wn +gTvx/ttDr9FW4C6Ju2kb8B/0LPB1es54g94Avk576M/AN2k78C3qNP5Bb0v8Jz0PfIdeAO6lF4Hv +SnyP/gp8n14CfkB/M3bTPold9LKxiz6kHcD99ArwI4kH6FXgx7QT+Am9BvyUdhuv0UGJn9E/gJ/T +68ZO+oLeAP5L4iHaA/yS3jJepcP0NvCIxK/on8Cv6R3gf9Je4DcSv6X3jFfoKL0P/I4+AH4P3EE/ +0D7gMeoCHqcPgT9KPEEfGS9TNx0AGvQx8N85/f98Tv/qN57Tv/jVOf2zX8jpn/0kpx/8hZz+6U9y ++ie/IqcfOJnTF56W0z/6hZz+kczpH/0kp++XOX1/r5y+X+b0/TKn7++V0z/8SU7vkjm9S+b0rt9g +Tn/n/1FO3/PvnP7vnP6by+m/9XP6bzen/9I5/d85/d85/edz+t9/+zn9vwCXmHoeCmVuZHN0cmVh +bQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFB +QStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYK +L1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9u +dEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIg +OCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAw +IFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURU +b0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3Jk +ZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzE2IFszMzMuMDA3ODEgMjc3Ljgz +MjAzXSAyMCA3MiA1NTYuMTUyMzQgNzMgWzI3Ny44MzIwM10gNzYgNzkgMjIyLjE2Nzk3IDgzIFs1 +NTYuMTUyMzRdXQovRFcgNzUwPj4KZW5kb2JqCjExIDAgb2JqCjw8L0ZpbHRlciAvRmxhdGVEZWNv +ZGUKL0xlbmd0aCAyNzE+PiBzdHJlYW0KeJxdkdtqxCAQhu99irncXizRbNplIQSWLAu56IGmfQCj +k1RojBhzkbevhzSFCiofM//vzJjVza3RykH2ZifRooNeaWlxnhYrEDoclCYsB6mE2yieYuSGZF7c +rrPDsdH9RMoSIHv30dnZFQ5XOXX4QLJXK9EqPcDhs249t4sx3ziidkBJVYHE3js9c/PCR4Qsyo6N +9HHl1qPX/GV8rAYhj8xSNWKSOBsu0HI9ICmpXxWUd78qglr+ixdJ1fXii9uQzQqfTemJVYGKOtLT +JdE9UR3p8RTpTKPv5pD/+u3PMxrTGItXfktO50jFJRkWm0UShSrDNPcRiMVa330ceWw7NKw07r9i +JhNUYf8AtiGI+QplbmRzdHJlYW0KZW5kb2JqCjQgMCBvYmoKPDwvVHlwZSAvRm9udAovU3VidHlw +ZSAvVHlwZTAKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovRW5jb2RpbmcgL0lkZW50aXR5LUgK +L0Rlc2NlbmRhbnRGb250cyBbMTAgMCBSXQovVG9Vbmljb2RlIDExIDAgUj4+CmVuZG9iagp4cmVm +CjAgMTIKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDA0MDEg +MDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDEwNDU5IDAwMDAwIG4gCjAwMDAwMDAx +MzggMDAwMDAgbiAKMDAwMDAwMDYwOSAwMDAwMCBuIAowMDAwMDAwNjY0IDAwMDAwIG4gCjAwMDAw +MDA3MTEgMDAwMDAgbiAKMDAwMDAwOTU4NSAwMDAwMCBuIAowMDAwMDA5ODE5IDAwMDAwIG4gCjAw +MDAwMTAxMTcgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDEyCi9Sb290IDcgMCBSCi9JbmZvIDEg +MCBSPj4Kc3RhcnR4cmVmCjEwNTk4CiUlRU9GCg== + +----- From d4df579fbbe22bb5eca10b7bb3c0192b1f9a5bf7 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 20:03:00 +0100 Subject: [PATCH 578/600] Content fetching RFC standard support added #510 --- CHANGELOG.md | 1 + src/Client.php | 8 ++++++++ src/Connection/Protocols/ImapProtocol.php | 4 +++- src/Message.php | 2 +- src/Query/Query.php | 2 +- src/config/imap.php | 1 + 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c532bb97..94f94701 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Security configuration options added - Spoofing detection added #40 - RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) +- Content fetching RFC standard support added #510 (thanks @ybizeul) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index a79511c0..0dd15a26 100755 --- a/src/Client.php +++ b/src/Client.php @@ -127,6 +127,13 @@ class Client { */ public array $extensions; + /** + * Account rfc. + * + * @var string + */ + public string $rfc; + /** * Account authentication method. * @@ -168,6 +175,7 @@ class Client { 'validate_cert' => true, 'username' => '', 'password' => '', + 'rfc' => 'RFC822', 'authentication' => null, "extensions" => [], 'proxy' => [ diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index da7483b8..17bc0e32 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -877,7 +877,9 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], is_array($uids) ? $uids : [$uids], null, $uid); + $rfc = $rfc ?? "RFC822"; + $item = $rfc === "BODY" ? "BODY[TEXT]" : "$rfc.TEXT"; + return $this->fetch([$item], is_array($uids) ? $uids : [$uids], null, $uid); } /** diff --git a/src/Message.php b/src/Message.php index 66193436..7ca0097b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -639,7 +639,7 @@ public function parseBody(): Message { $sequence_id = $this->getSequenceId(); try { - $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); + $contents = $this->client->getConnection()->content([$sequence_id], $this->client->rfc, $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageContentFetchingException("failed to fetch content", 0, $e); } diff --git a/src/Query/Query.php b/src/Query/Query.php index cade729d..f1aea3ac 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -243,7 +243,7 @@ protected function fetch(Collection $available_messages): array { $contents = []; if ($this->getFetchBody()) { - $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence)->validatedData(); + $contents = $this->client->getConnection()->content($uids, $this->client->rfc, $this->sequence)->validatedData(); } return [ diff --git a/src/config/imap.php b/src/config/imap.php index 8e39fe8e..407c53bd 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -81,6 +81,7 @@ 'username' => 'root@example.com', 'password' => '', 'authentication' => null, + 'rfc' => 'RFC822', // If you are using iCloud, you might want to set this to 'BODY' 'proxy' => [ 'socket' => null, 'request_fulluri' => false, From beb82e25a5f6cd0ab00afe9f56171f52b5b60abf Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 20:16:08 +0100 Subject: [PATCH 579/600] Support unescaped dates inside the search conditions #542 --- CHANGELOG.md | 1 + src/Query/Query.php | 5 ++++- src/config/imap.php | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f94701..d664fe6f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Spoofing detection added #40 - RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) - Content fetching RFC standard support added #510 (thanks @ybizeul) +- Support unescaped dates inside the search conditions #542 ### Breaking changes - NaN diff --git a/src/Query/Query.php b/src/Query/Query.php index f1aea3ac..e65a5a9d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -164,7 +164,10 @@ public function generate_query(): string { if ($statement[1] === null) { $query .= $statement[0]; } else { - if (is_numeric($statement[1])) { + if (is_numeric($statement[1]) || ( + ($statement[0] === 'SINCE' || $statement[0] === 'BEFORE') && + $this->client->getConfig()->get('options.unescaped_search_dates', false) + )) { $query .= $statement[0] . ' ' . $statement[1]; } else { $query .= $statement[0] . ' "' . $statement[1] . '"'; diff --git a/src/config/imap.php b/src/config/imap.php index 407c53bd..abf8e6e6 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -174,6 +174,7 @@ 'soft_fail' => false, 'rfc822' => true, 'debug' => false, + 'unescaped_search_dates' => false, 'uid_cache' => true, // 'fallback_date' => "01.01.1970 00:00:00", 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', From 4289d41f80822b0c800cfa2a2c42a9d5a4fa67b0 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 21:31:08 +0100 Subject: [PATCH 580/600] clone method looses account configuration #521 --- CHANGELOG.md | 1 + src/Client.php | 2 +- tests/ClientTest.php | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d664fe6f..b598ce50 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) - Content fetching RFC standard support added #510 (thanks @ybizeul) - Support unescaped dates inside the search conditions #542 +- `Client::clone()` looses account configuration #521 (thanks @netpok) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index 0dd15a26..441d0ab9 100755 --- a/src/Client.php +++ b/src/Client.php @@ -224,7 +224,7 @@ public function clone(): Client { $client->default_account_config = $this->default_account_config; $config = $this->getAccountConfig(); foreach($config as $key => $value) { - $client->setAccountConfig($key, $this->default_account_config); + $client->setAccountConfig($key, $config); } $client->default_message_mask = $this->default_message_mask; $client->default_attachment_mask = $this->default_message_mask; diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 315f3c42..73a01be7 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -83,6 +83,40 @@ public function testClient(): void { self::assertArrayHasKey("new", $this->client->getDefaultEvents("message")); } + /** + * @throws MaskNotFoundException + */ + public function testClientClone(): void { + $config = Config::make([ + "accounts" => [ + "default" => [ + 'host' => 'example.com', + 'port' => 993, + 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)] + 'encryption' => 'ssl', // Supported: false, 'ssl', 'tls' + 'validate_cert' => true, + 'username' => 'root@example.com', + 'password' => 'foo', + 'authentication' => null, + 'rfc' => 'RFC822', // If you are using iCloud, you might want to set this to 'BODY' + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + "timeout" => 30, + "extensions" => [] + ]] + ]); + $client = new Client($config); + $clone = $client->clone(); + self::assertInstanceOf(Client::class, $clone); + self::assertSame($client->getConfig(), $clone->getConfig()); + self::assertSame($client->getAccountConfig(), $clone->getAccountConfig()); + self::assertSame($client->host, $clone->host); + } + public function testClientLogout(): void { $this->createNewProtocolMockup(); From e527bf3c8b7e8e73c21797b2ee8cea66bba03edf Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 22:20:42 +0100 Subject: [PATCH 581/600] Decode partially encoded address names #511 --- CHANGELOG.md | 1 + src/Decoder/HeaderDecoder.php | 4 +-- src/Header.php | 23 ++++++++++------ tests/issues/Issue511Test.php | 50 +++++++++++++++++++++++++++++++++++ tests/messages/issue-511.eml | 5 ++++ 5 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 tests/issues/Issue511Test.php create mode 100644 tests/messages/issue-511.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index b598ce50..7e02ee70 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Filename sanitization is now optional (enabled via default) - Address parsing improved and extended to include more cases - Boundary parsing fixed and improved to support more formats #544 +- Decode partially encoded address names #511 ### Added - Security configuration options added diff --git a/src/Decoder/HeaderDecoder.php b/src/Decoder/HeaderDecoder.php index c9aa33d0..3a98aa6c 100644 --- a/src/Decoder/HeaderDecoder.php +++ b/src/Decoder/HeaderDecoder.php @@ -68,11 +68,11 @@ public function getEncoding(object|string $structure): string { if (property_exists($structure, 'parameters')) { foreach ($structure->parameters as $parameter) { if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value, $this->fallback_encoding); + return EncodingAliases::get($parameter->value == "default" ? EncodingAliases::detectEncoding($parameter->value) : $parameter->value, $this->fallback_encoding); } } } elseif (property_exists($structure, 'charset')) { - return EncodingAliases::get($structure->charset, $this->fallback_encoding); + return EncodingAliases::get($structure->charset == "default" ? EncodingAliases::detectEncoding($structure->charset) : $structure->charset, $this->fallback_encoding); } elseif (is_string($structure) === true) { $result = mb_detect_encoding($structure); return $result === false ? $this->fallback_encoding : $result; diff --git a/src/Header.php b/src/Header.php index 604eb896..3d909302 100644 --- a/src/Header.php +++ b/src/Header.php @@ -480,16 +480,23 @@ private function parseAddresses($list): array { if (!property_exists($address, 'personal')) { $address->personal = false; } else { - $personalParts = $this->decoder->mimeHeaderDecode($address->personal); - - $address->personal = ''; - foreach ($personalParts as $p) { - $address->personal .= $this->decoder->convertEncoding($p->text, $this->decoder->getEncoding($p)); - } + $personal_slices = explode(" ", $address->personal); + $address->personal = ""; + foreach ($personal_slices as $slice) { + $personalParts = $this->decoder->mimeHeaderDecode($slice); + + $personal = ''; + foreach ($personalParts as $p) { + $personal .= $this->decoder->convertEncoding($p->text, $this->decoder->getEncoding($p)); + } - if (str_starts_with($address->personal, "'")) { - $address->personal = str_replace("'", "", $address->personal); + if (str_starts_with($personal, "'")) { + $personal = str_replace("'", "", $personal); + } + $personal = $this->decoder->decode($personal); + $address->personal .= $personal . " "; } + $address->personal = trim(rtrim($address->personal)); } if ($address->host == ".SYNTAX-ERROR.") { diff --git a/tests/issues/Issue511Test.php b/tests/issues/Issue511Test.php new file mode 100644 index 00000000..4782bace --- /dev/null +++ b/tests/issues/Issue511Test.php @@ -0,0 +1,50 @@ +getFixture("issue-511.eml"); + self::assertSame("RE: [EXTERNAL] Re: Lorem Ipsum /40 one", (string)$message->subject); + self::assertSame("COMPANYNAME | usługi ", (string)$message->from->first()); + } +} \ No newline at end of file diff --git a/tests/messages/issue-511.eml b/tests/messages/issue-511.eml new file mode 100644 index 00000000..7cd61faa --- /dev/null +++ b/tests/messages/issue-511.eml @@ -0,0 +1,5 @@ +From: COMPANYNAME | =?iso-8859-2?q?us=B3ugi?= +To: receipent@receipent_domain.tld +Subject: =?utf-8?B?UkU6IFtFWFRFUk5BTF0gUmU6IExvcmVtIElwc3VtIC8=?= =?utf-8?Q?40_one?= + +Test message From 0276983f8104793a5676e1bf1aec151007cfc710 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 22:50:36 +0100 Subject: [PATCH 582/600] dev-master version bumped #493 #496 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4956e549..334275fa 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "6.0-dev" } }, "minimum-stability": "dev", From ec2dcb863a9713189ef1caf80ecf6b49af2256fe Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 23:57:00 +0100 Subject: [PATCH 583/600] Compatibility table updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ec49921e..da47d048 100755 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] ## Compatibility | Version | PHP 5.6 | PHP 7 | PHP 8 | |:--------|:-------:|:-----:|:-----:| +| v6.x | / | / | X | | v5.x | / | / | X | | v4.x | / | X | X | | v3.x | / | X | / | From d21e00cf4158c23e087f6b7da2425bd8fd46755a Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:17:05 +0100 Subject: [PATCH 584/600] Event dispatching simplified (thanks @stevebauman) --- src/Client.php | 6 ++---- src/Folder.php | 9 +++------ src/Message.php | 15 +++++---------- src/Traits/HasEvents.php | 9 +++++++++ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Client.php b/src/Client.php index 441d0ab9..e8d971bc 100755 --- a/src/Client.php +++ b/src/Client.php @@ -723,8 +723,7 @@ public function createFolder(string $folder_path, bool $expunge = true, bool $ut $folder = $this->getFolderByPath($folder_path, true); if($status && $folder) { - $event = $this->getEvent("folder", "new"); - $event::dispatch($folder); + $this->dispatch("folder", "new", $folder); } return $folder; @@ -755,8 +754,7 @@ public function deleteFolder(string $folder_path, bool $expunge = true): array { $status = $this->getConnection()->deleteFolder($folder->path)->validatedData(); if ($expunge) $this->expunge(); - $event = $this->getEvent("folder", "deleted"); - $event::dispatch($folder); + $this->dispatch("folder", "deleted", $folder); return $status; } diff --git a/src/Folder.php b/src/Folder.php index dced7d6b..a6d8ee58 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -287,8 +287,7 @@ public function move(string $new_name, bool $expunge = true): array { if ($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); - $event = $this->getEvent("folder", "moved"); - $event::dispatch($this, $folder); + $this->dispatch("folder", "moved", $this, $folder); return $status; } @@ -383,8 +382,7 @@ public function delete(bool $expunge = true): array { if ($expunge) $this->client->expunge(); - $event = $this->getEvent("folder", "deleted"); - $event::dispatch($this); + $this->dispatch("folder", "deleted", $this); return $status; } @@ -494,8 +492,7 @@ public function idle(callable $callback, int $timeout = 300): void { $message->setSequence($sequence); $callback($message); - $event = $this->getEvent("message", "new"); - $event::dispatch($message); + $this->dispatch("message", "new", $message); } } } diff --git a/src/Message.php b/src/Message.php index 7ca0097b..3f4fa90b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1096,8 +1096,7 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo } $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); - $event = $this->getEvent("message", $event); - $event::dispatch($this, $message); + $this->dispatch("message", $event, $this, $message); return $message; } @@ -1131,8 +1130,7 @@ public function delete(bool $expunge = true, ?string $trash_path = null, bool $f } if ($expunge) $this->client->expunge(); - $event = $this->getEvent("message", "deleted"); - $event::dispatch($this); + $this->dispatch("message", "deleted", $this); return $status; } @@ -1155,8 +1153,7 @@ public function restore(bool $expunge = true): bool { $status = $this->unsetFlag("Deleted"); if ($expunge) $this->client->expunge(); - $event = $this->getEvent("message", "restored"); - $event::dispatch($this); + $this->dispatch("message", "restored", $this); return $status; } @@ -1186,8 +1183,7 @@ public function setFlag(array|string $flag): bool { } $this->parseFlags(); - $event = $this->getEvent("flag", "new"); - $event::dispatch($this, $flag); + $this->dispatch("flag", "new", $this, $flag); return (bool)$status; } @@ -1218,8 +1214,7 @@ public function unsetFlag(array|string $flag): bool { } $this->parseFlags(); - $event = $this->getEvent("flag", "deleted"); - $event::dispatch($this, $flag); + $this->dispatch("flag", "deleted", $this, $flag); return (bool)$status; } diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index 3b6902ed..6dc382b7 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -74,4 +74,13 @@ public function getEvents(): array { return $this->events; } + /** + * Dispatch a specific event. + * @throws EventNotFoundException + */ + public function dispatch(string $section, string $event, mixed ...$args): void { + $event = $this->getEvent($section, $event); + $event::dispatch(...$args); + } + } \ No newline at end of file From dae454840765236c88fdf71ef9eb0321ee62e894 Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:21:59 +0100 Subject: [PATCH 585/600] Enforce RFC822 parsing if enabled #462 --- CHANGELOG.md | 1 + src/Address.php | 18 ++++++++++ src/Header.php | 52 ++++++++++++++++++++++++++--- tests/fixtures/EmailAddressTest.php | 24 +++++++++++-- tests/fixtures/ReferencesTest.php | 6 ++-- tests/issues/Issue462Test.php | 48 ++++++++++++++++++++++++++ tests/messages/issue-462.eml | 5 +++ 7 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 tests/issues/Issue462Test.php create mode 100644 tests/messages/issue-462.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e02ee70..51337ca8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Address parsing improved and extended to include more cases - Boundary parsing fixed and improved to support more formats #544 - Decode partially encoded address names #511 +- Enforce RFC822 parsing if enabled #462 ### Added - Security configuration options added diff --git a/src/Address.php b/src/Address.php index b45c72de..87c6479e 100644 --- a/src/Address.php +++ b/src/Address.php @@ -43,6 +43,24 @@ public function __construct(object $object) { if (property_exists($object, "host")){ $this->host = $object->host ?? ''; } if (property_exists($object, "mail")){ $this->mail = $object->mail ?? ''; } if (property_exists($object, "full")){ $this->full = $object->full ?? ''; } + $this->boot(); + } + + /** + * Boot the address + */ + private function boot(): void { + if($this->mail === "" && $this->mailbox !== "" && $this->host !== ""){ + $this->mail = $this->mailbox . "@" . $this->host; + }elseif($this->mail === "" && $this->mailbox !== ""){ + $this->mail = $this->mailbox; + } + + if($this->full === "" && $this->mail !== "" && $this->personal !== ""){ + $this->full = $this->personal . " <" . $this->mail . ">"; + }elseif($this->full === "" && $this->mail !== ""){ + $this->full = $this->mail; + } } diff --git a/src/Header.php b/src/Header.php index 3d909302..47c1668a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -408,6 +408,24 @@ private function decodeAddresses($values): array { "mailbox" => $mailbox, "host" => $host, ]; + }elseif (preg_match( + '/^((?P.+)<)(?P[^<]+?)>$/', + $split_address, + $matches + )) { + $name = trim(rtrim($matches["name"])); + if(str_starts_with($name, "\"") && str_ends_with($name, "\"")) { + $name = substr($name, 1, -1); + }elseif(str_starts_with($name, "'") && str_ends_with($name, "'")) { + $name = substr($name, 1, -1); + } + $email = trim(rtrim($matches["email"])); + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + $addresses[] = (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ]; } } } @@ -438,7 +456,6 @@ private function parseAddresses($list): array { if (is_array($list) === false) { if(is_string($list)) { - // $list = "" if (preg_match( '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', $list, @@ -460,6 +477,32 @@ private function parseAddresses($list): array { "host" => $host, ] ]; + }elseif (preg_match( + '/^((?P.+)<)(?P[^<]+?)>$/', + $list, + $matches + )) { + $name = trim(rtrim($matches["name"])); + $email = trim(rtrim($matches["email"])); + if(str_starts_with($name, "\"") && str_ends_with($name, "\"")) { + $name = substr($name, 1, -1); + }elseif(str_starts_with($name, "'") && str_ends_with($name, "'")) { + $name = substr($name, 1, -1); + } + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + if($mailbox === ">") { // Fix trailing ">" in malformed mailboxes + $mailbox = ""; + } + if($name === "" && $mailbox === "" && $host === "") { + return $addresses; + } + $list = [ + (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ] + ]; }else{ return $addresses; } @@ -501,14 +544,15 @@ private function parseAddresses($list): array { if ($address->host == ".SYNTAX-ERROR.") { $address->host = ""; + }elseif ($address->host == "UNKNOWN") { + $address->host = ""; } if ($address->mailbox == "UNEXPECTED_DATA_AFTER_ADDRESS") { $address->mailbox = ""; + }elseif ($address->mailbox == "MISSING_MAILBOX_TERMINATOR") { + $address->mailbox = ""; } - $address->mail = ($address->mailbox && $address->host) ? $address->mailbox . '@' . $address->host : false; - $address->full = ($address->personal) ? $address->personal . ' <' . $address->mail . '>' : $address->mail; - $addresses[] = new Address($address); } diff --git a/tests/fixtures/EmailAddressTest.php b/tests/fixtures/EmailAddressTest.php index d4e403d7..0f87cf6a 100644 --- a/tests/fixtures/EmailAddressTest.php +++ b/tests/fixtures/EmailAddressTest.php @@ -12,6 +12,16 @@ namespace Tests\fixtures; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; +use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\ResponseException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; + /** * Class EmailAddressTest * @@ -23,6 +33,16 @@ class EmailAddressTest extends FixtureTestCase { * Test the fixture email_address.eml * * @return void + * @throws \ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException */ public function testFixture() : void { $message = $this->getFixture("email_address.eml"); @@ -32,8 +52,8 @@ public function testFixture() : void { self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); self::assertFalse($message->hasHTMLBody()); self::assertFalse($message->date->first()); - self::assertEquals("no_host@UNKNOWN", (string)$message->from); + self::assertEquals("no_host", (string)$message->from); self::assertEquals("", $message->to); - self::assertEquals("This one: is \"right\" , No-address@UNKNOWN", $message->cc); + self::assertEquals("This one: is \"right\" , No-address", (string)$message->cc); } } \ No newline at end of file diff --git a/tests/fixtures/ReferencesTest.php b/tests/fixtures/ReferencesTest.php index 18473d61..c86aa96f 100644 --- a/tests/fixtures/ReferencesTest.php +++ b/tests/fixtures/ReferencesTest.php @@ -34,8 +34,8 @@ public function testFixture() : void { self::assertEquals("b9e87bd5e661a645ed6e3b832828fcc5@example.com", $message->in_reply_to); self::assertEquals("", $message->from->first()->personal); - self::assertEquals("UNKNOWN", $message->from->first()->host); - self::assertEquals("no_host@UNKNOWN", $message->from->first()->mail); + self::assertEquals("", $message->from->first()->host); + self::assertEquals("no_host", $message->from->first()->mail); self::assertFalse($message->to->first()); self::assertEquals([ @@ -45,7 +45,7 @@ public function testFixture() : void { self::assertEquals([ 'This one: is "right" ', - 'No-address@UNKNOWN' + 'No-address' ], $message->cc->map(function($address){ /** @var \Webklex\PHPIMAP\Address $address */ return $address->full; diff --git a/tests/issues/Issue462Test.php b/tests/issues/Issue462Test.php new file mode 100644 index 00000000..e7fc2126 --- /dev/null +++ b/tests/issues/Issue462Test.php @@ -0,0 +1,48 @@ +set('options.rfc822', false); + $message = $this->getFixture("issue-462.eml", $config); + self::assertSame("Undeliverable: Some subject", (string)$message->subject); + self::assertSame("postmaster@ ", (string)$message->from->first()); + } +} \ No newline at end of file diff --git a/tests/messages/issue-462.eml b/tests/messages/issue-462.eml new file mode 100644 index 00000000..d13b3cf1 --- /dev/null +++ b/tests/messages/issue-462.eml @@ -0,0 +1,5 @@ +From: "postmaster@" +To: receipent@receipent_domain.tld +Subject: Undeliverable: Some subject + +Test message From dedff6f679829d4aadfa0b17f150923b2f8dd1fe Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:23:12 +0100 Subject: [PATCH 586/600] Release information added --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51337ca8..205133e3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + +## [6.1.0] - 2025-01-19 +### Fixed - Filename sanitization is now optional (enabled via default) - Address parsing improved and extended to include more cases - Boundary parsing fixed and improved to support more formats #544 @@ -20,9 +30,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Support unescaped dates inside the search conditions #542 - `Client::clone()` looses account configuration #521 (thanks @netpok) -### Breaking changes -- NaN - ## [6.0.0] - 2025-01-17 ### Fixed - Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) From b9d8b1ba8751fb5fcb6fcb0f115feb5eda858cd6 Mon Sep 17 00:00:00 2001 From: Laurent Le Moine Date: Tue, 28 Jan 2025 13:03:00 +0100 Subject: [PATCH 587/600] SSL stream context options added #238 #546 Co-authored-by: LE MOINE Laurent --- src/Client.php | 14 ++++++++++- src/Connection/Protocols/Protocol.php | 36 +++++++++++++++++++++++++++ tests/ImapProtocolTest.php | 12 +++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index e8d971bc..19749a32 100755 --- a/src/Client.php +++ b/src/Client.php @@ -100,6 +100,16 @@ class Client { 'password' => null, ]; + + /** + * SSL stream context options + * + * @see https://www.php.net/manual/en/context.ssl.php for possible options + * + * @var array + */ + protected array $ssl_options = []; + /** * Connection timeout * @var int $timeout @@ -184,7 +194,8 @@ class Client { 'username' => null, 'password' => null, ], - "timeout" => 30 + 'ssl_options' => [], + "timeout" => 30, ]; /** @@ -436,6 +447,7 @@ public function connect(): Client { $this->connection = new ImapProtocol($this->config, $this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); + $this->connection->setSslOptions($this->ssl_options); }else{ if (extension_loaded('imap') === false) { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index a3886b55..a1404b4b 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -71,6 +71,15 @@ abstract class Protocol implements ProtocolInterface { 'password' => null, ]; + /** + * SSL stream context options + * + * @see https://www.php.net/manual/en/context.ssl.php for possible options + * + * @var array + */ + protected array $ssl_options = []; + /** * Cache for uid of active folder. * @@ -162,6 +171,28 @@ public function getProxy(): array { return $this->proxy; } + /** + * Set SSL context options settings + * @var array $options + * + * @return Protocol + */ + public function setSslOptions(array $options): Protocol + { + $this->ssl_options = $options; + + return $this; + } + + /** + * Get the current SSL context options settings + * + * @return array + */ + public function getSslOptions(): array { + return $this->ssl_options; + } + /** * Prepare socket options * @return array @@ -175,6 +206,11 @@ private function defaultSocketOptions(string $transport): array { 'verify_peer_name' => $this->getCertValidation(), 'verify_peer' => $this->getCertValidation(), ]; + + if (count($this->ssl_options)) { + /* Get the ssl context options from the config, but prioritize the 'validate_cert' config over the ssl context options */ + $options["ssl"] = array_replace($this->ssl_options, $options["ssl"]); + } } if ($this->proxy["socket"] != null) { diff --git a/tests/ImapProtocolTest.php b/tests/ImapProtocolTest.php index 78716548..30f3a200 100644 --- a/tests/ImapProtocolTest.php +++ b/tests/ImapProtocolTest.php @@ -48,5 +48,17 @@ public function testImapProtocol(): void { self::assertSame(true, $protocol->getCertValidation()); self::assertSame("ssl", $protocol->getEncryption()); + + $protocol->setSslOptions([ + 'verify_peer' => true, + 'cafile' => '/dummy/path/for/testing', + 'peer_fingerprint' => ['md5' => 40], + ]); + + self::assertSame([ + 'verify_peer' => true, + 'cafile' => '/dummy/path/for/testing', + 'peer_fingerprint' => ['md5' => 40], + ], $protocol->getSslOptions()); } } \ No newline at end of file From 35bad8556032e07aa046b0036e398b43eae044d7 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 28 Jan 2025 13:03:30 +0100 Subject: [PATCH 588/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 205133e3..bcf9a0f8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- SSL stream context options added #238 #546 (thanks @llemoine) ### Breaking changes - NaN From f8d5f34c3f854a9c10bcaba0bdeec29fdccc510c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= <39156286+zeddmaster@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:49:46 +0300 Subject: [PATCH 589/600] When using the chunk function, some messages do not have an element with index 0 #552 #553 Co-authored-by: Dmitry Chizh --- src/Connection/Protocols/ImapProtocol.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 17bc0e32..455a5bae 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -770,6 +770,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in if (is_array($from) && count($from) > 1) { $set = implode(',', $from); } elseif (is_array($from) && count($from) === 1) { + $from = array_values($from); $set = $from[0] . ':' . $from[0]; } elseif ($to === null) { $set = $from . ':' . $from; From 88f28e4d766a52db8da42366a5364f13fcd8a9c2 Mon Sep 17 00:00:00 2001 From: Rene Date: Fri, 25 Apr 2025 07:52:13 +0200 Subject: [PATCH 590/600] Copy/Move Message with utf7 folder path (#559) Messages can be moved/copied to a folder with umlauts. Co-authored-by: Rene Rudnick --- src/Message.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Message.php b/src/Message.php index 3f4fa90b..9d15a7b8 100755 --- a/src/Message.php +++ b/src/Message.php @@ -983,6 +983,7 @@ protected function fetchThreadByMessageId(MessageCollection &$thread, string $me * Copy the current Messages to a mailbox * @param string $folder_path * @param boolean $expunge + * @param bool $utf7 * * @return null|Message * @throws AuthFailedException @@ -999,7 +1000,7 @@ protected function fetchThreadByMessageId(MessageCollection &$thread, string $me * @throws RuntimeException * @throws ResponseException */ - public function copy(string $folder_path, bool $expunge = false): ?Message { + public function copy(string $folder_path, bool $expunge = false, bool $utf7 = false): ?Message { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path)->validatedData(); @@ -1010,7 +1011,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { } /** @var Folder $folder */ - $folder = $this->client->getFolderByPath($folder_path); + $folder = $this->client->getFolderByPath($folder_path, $utf7); $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { @@ -1025,6 +1026,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { * Move the current Messages to a mailbox * @param string $folder_path * @param boolean $expunge + * @param bool $utf7 * * @return Message|null * @throws AuthFailedException @@ -1041,7 +1043,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { * @throws RuntimeException * @throws ResponseException */ - public function move(string $folder_path, bool $expunge = false): ?Message { + public function move(string $folder_path, bool $expunge = false, bool $utf7 = false): ?Message { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path)->validatedData(); @@ -1052,7 +1054,7 @@ public function move(string $folder_path, bool $expunge = false): ?Message { } /** @var Folder $folder */ - $folder = $this->client->getFolderByPath($folder_path); + $folder = $this->client->getFolderByPath($folder_path, $utf7); $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { From d1e916b397233a7ea11f612ca1a0e9040c755f87 Mon Sep 17 00:00:00 2001 From: rskrzypczak Date: Fri, 25 Apr 2025 07:53:50 +0200 Subject: [PATCH 591/600] fix #560 Get folders list in hierarchical order (#561) * fix #552 Get folders list in hierarchical order * fix #560 Get folders list in hierarchical order --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 19749a32..5eb26ff2 100755 --- a/src/Client.php +++ b/src/Client.php @@ -604,7 +604,7 @@ public function getFolders(bool $hierarchical = true, ?string $parent_folder = n $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); if ($hierarchical && $folder->hasChildren()) { - $pattern = $folder->full_name.$folder->delimiter.'%'; + $pattern = $folder->path.$folder->delimiter.'%'; $children = $this->getFolders(true, $pattern, true); $folder->setChildren($children); @@ -650,7 +650,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, ?string $parent_ $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); if ($hierarchical && $folder->hasChildren()) { - $pattern = $folder->full_name.$folder->delimiter.'%'; + $pattern = $folder->path.$folder->delimiter.'%'; $children = $this->getFoldersWithStatus(true, $pattern, true); $folder->setChildren($children); From 0a0a968c208b786a461be7a75fae62d1a39b7852 Mon Sep 17 00:00:00 2001 From: Roberto Guido Date: Fri, 25 Apr 2025 07:55:08 +0200 Subject: [PATCH 592/600] Public search() method for Query (#565) --- src/Query/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index e65a5a9d..bed64a95 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -193,7 +193,7 @@ public function generate_query(): string { * @throws ImapServerErrorException * @throws ResponseException */ - protected function search(): Collection { + public function search(): Collection { $this->generate_query(); try { From 700485f710d1ff4f0630ecb15d010010ba9a023f Mon Sep 17 00:00:00 2001 From: Steffen Weber Date: Fri, 25 Apr 2025 07:56:39 +0200 Subject: [PATCH 593/600] Fix remaining implicit marking of parameters as nullable (PHP 8.4) (#566) --- src/Attachment.php | 2 +- src/Decoder/Decoder.php | 2 +- src/Decoder/DecoderInterface.php | 2 +- src/Decoder/HeaderDecoder.php | 2 +- src/Decoder/MessageDecoder.php | 2 +- tests/fixtures/FixtureTestCase.php | 2 +- tests/live/LegacyTest.php | 4 ++-- tests/live/LiveMailboxTestCase.php | 2 +- tests/live/QueryTest.php | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 223de45e..e2ce8128 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -462,7 +462,7 @@ public function setConfig(Config $config): Attachment { * @return mixed * @throws MaskNotFoundException */ - public function mask(string $mask = null): mixed { + public function mask(?string $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; if (class_exists($mask)) { return new $mask($this); diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index 1df68c5e..23b4dc42 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -54,7 +54,7 @@ public function __construct( * @param string|null $encoding * @return mixed */ - public function decode(array|string|null $value, string $encoding = null): mixed { + public function decode(array|string|null $value, ?string $encoding = null): mixed { return $value; } diff --git a/src/Decoder/DecoderInterface.php b/src/Decoder/DecoderInterface.php index c44f2118..e4874602 100644 --- a/src/Decoder/DecoderInterface.php +++ b/src/Decoder/DecoderInterface.php @@ -33,7 +33,7 @@ public function __construct(array $options = [], string $fallback_encoding = 'UT * @param string|null $encoding * @return string|array|null */ - public function decode(array|string|null $value, string $encoding = null): mixed; + public function decode(array|string|null $value, ?string $encoding = null): mixed; public function mimeHeaderDecode(string $text): array; diff --git a/src/Decoder/HeaderDecoder.php b/src/Decoder/HeaderDecoder.php index 3a98aa6c..9a8a71b2 100644 --- a/src/Decoder/HeaderDecoder.php +++ b/src/Decoder/HeaderDecoder.php @@ -21,7 +21,7 @@ */ class HeaderDecoder extends Decoder { - public function decode(array|string|null $value, string $encoding = null): mixed { + public function decode(array|string|null $value, ?string $encoding = null): mixed { if (is_array($value)) { return $this->decodeHeaderArray($value); } diff --git a/src/Decoder/MessageDecoder.php b/src/Decoder/MessageDecoder.php index 5d4178bc..4a2cff2f 100644 --- a/src/Decoder/MessageDecoder.php +++ b/src/Decoder/MessageDecoder.php @@ -23,7 +23,7 @@ */ class MessageDecoder extends Decoder { - public function decode(array|string|null $value, string $encoding = null): mixed { + public function decode(array|string|null $value, ?string $encoding = null): mixed { if(is_array($value)) { return array_map(function($item){ return $this->decode($item); diff --git a/tests/fixtures/FixtureTestCase.php b/tests/fixtures/FixtureTestCase.php index 00f31157..cb64a9a6 100644 --- a/tests/fixtures/FixtureTestCase.php +++ b/tests/fixtures/FixtureTestCase.php @@ -84,7 +84,7 @@ final public function __construct(?string $name = null, array $data = [], $dataN * @throws ResponseException * @throws RuntimeException */ - final public function getFixture(string $template, Config $config = null) : Message { + final public function getFixture(string $template, ?Config $config = null) : Message { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); $message = Message::fromFile($filename, $config); self::assertInstanceOf(Message::class, $message); diff --git a/tests/live/LegacyTest.php b/tests/live/LegacyTest.php index 61e2ee09..3e385eb0 100644 --- a/tests/live/LegacyTest.php +++ b/tests/live/LegacyTest.php @@ -236,7 +236,7 @@ final protected function appendMessageTemplate(Folder $folder, string $template) * @throws ResponseException * @throws RuntimeException */ - final protected function deleteFolder(Folder $folder = null): bool { + final protected function deleteFolder(?Folder $folder = null): bool { $response = $folder?->delete(false); if (is_array($response)) { $valid_response = false; @@ -423,7 +423,7 @@ public function testQueryWhereCriteria(): void { * @throws ResponseException * @throws RuntimeException */ - protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string|null $value = null, bool $date = false): void { $query = $folder->query()->where($criteria, $value); self::assertInstanceOf(WhereQuery::class, $query); diff --git a/tests/live/LiveMailboxTestCase.php b/tests/live/LiveMailboxTestCase.php index c59e6773..d0f1b680 100644 --- a/tests/live/LiveMailboxTestCase.php +++ b/tests/live/LiveMailboxTestCase.php @@ -200,7 +200,7 @@ final protected function appendMessageTemplate(Folder $folder, string $template) * @throws ResponseException * @throws RuntimeException */ - final protected function deleteFolder(Folder $folder = null): bool { + final protected function deleteFolder(?Folder $folder = null): bool { $response = $folder?->delete(false); if (is_array($response)) { $valid_response = false; diff --git a/tests/live/QueryTest.php b/tests/live/QueryTest.php index 34cd77a4..194d804e 100644 --- a/tests/live/QueryTest.php +++ b/tests/live/QueryTest.php @@ -231,7 +231,7 @@ public function testQueryWhereCriteria(): void { * @throws ResponseException * @throws RuntimeException */ - protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string|null $value = null, bool $date = false): void { $query = $folder->query()->where($criteria, $value); self::assertInstanceOf(WhereQuery::class, $query); From fb10c94e4127ae9b02b8c96d64f82d41858d6170 Mon Sep 17 00:00:00 2001 From: smajti1 Date: Fri, 25 Apr 2025 07:57:39 +0200 Subject: [PATCH 594/600] Fix case sensitivity of folder attribute parsing (\NoSelect, \NoInferiors) #469 (#571) * Fix case sensitivity of folder attribute parsing (\NoSelect, \NoInferiors) #469 * changelog update --- CHANGELOG.md | 2 +- src/Folder.php | 4 ++-- tests/issues/Issue469Test.php | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/issues/Issue469Test.php diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf9a0f8..7159d0af 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix case sensitivity of Folder attribute parsing (\NoSelect, \NoInferiors) #469 (thanks @smajti1) ### Added - SSL stream context options added #238 #546 (thanks @llemoine) diff --git a/src/Folder.php b/src/Folder.php index a6d8ee58..45d2931d 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -259,8 +259,8 @@ protected function getSimpleName($delimiter, $full_name): string|bool { * @param $attributes */ protected function parseAttributes($attributes): void { - $this->no_inferiors = in_array('\NoInferiors', $attributes); - $this->no_select = in_array('\NoSelect', $attributes); + $this->no_inferiors = in_array('\NoInferiors', $attributes, true) || \in_array('\Noinferiors', $attributes, true); + $this->no_select = in_array('\NoSelect', $attributes, true) || \in_array('\Noselect', $attributes, true); $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); diff --git a/tests/issues/Issue469Test.php b/tests/issues/Issue469Test.php new file mode 100644 index 00000000..13bd841b --- /dev/null +++ b/tests/issues/Issue469Test.php @@ -0,0 +1,42 @@ +createStub(Client::class); + $folder_name = '[Gmail]'; + $delimiter = '/'; + + $attributes = [ + '\NoInferiors', + '\NoSelect', + ]; + $folder = new Folder($client, $folder_name, $delimiter, $attributes); + + $attributes_lowercase = [ + '\Noinferiors', + '\Noselect', + ]; + $folder_lowercase = new Folder($client, $folder_name, $delimiter, $attributes_lowercase); + + self::assertSame( + $folder->no_inferiors, + $folder_lowercase->no_inferiors, + 'The parsed "\NoInferiors" attribute does not match the parsed "\Noinferiors" attribute' + ); + self::assertSame( + $folder->no_select, + $folder_lowercase->no_select, + 'The parsed "\NoSelect" attribute does not match the parsed "\Noselect" attribute' + ); + } +} \ No newline at end of file From 0e75d7e77407edd325e93918e47baa5fae534b5e Mon Sep 17 00:00:00 2001 From: pierement Date: Fri, 25 Apr 2025 07:58:33 +0200 Subject: [PATCH 595/600] Fix error on getUid(null) with 0 results (#499) (#573) Co-authored-by: Wiebe Haanstra --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 455a5bae..ed1dac6e 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -940,7 +940,7 @@ public function getUid(?int $id = null): Response { $uids = $this->uid_cache; if ($id == null) { - return Response::empty($this->debug)->setResult($uids); + return Response::empty($this->debug)->setResult($uids)->setCanBeEmpty(true); } foreach ($uids as $k => $v) { From c00d9db35fe0c6232d455625e823d9a744531bae Mon Sep 17 00:00:00 2001 From: lm-cmxkonzepte <45796141+lm-cmxkonzepte@users.noreply.github.com> Date: Fri, 25 Apr 2025 07:59:51 +0200 Subject: [PATCH 596/600] Fix Date parsing on non-standard format from Aqua Mail (#575) fixes issue #574 --- src/Header.php | 5 +++-- tests/fixtures/DateTemplateTest.php | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Header.php b/src/Header.php index 47c1668a..d18e98a1 100644 --- a/src/Header.php +++ b/src/Header.php @@ -673,6 +673,7 @@ private function read_attribute(string $raw_attribute): array { * | Thu, 31 May 2018 18:15:00 +0800 (added by) | Non-standard details added by the | Unknown * | | mail server | * | Sat, 31 Aug 2013 20:08:23 +0580 | Invalid timezone | PHPMailer bug https://sourceforge.net/p/phpmailer/mailman/message/6132703/ + * | Mi., 23 Apr. 2025 09:48:37 +0200 (MESZ) | Non-standard localized format | Aqua Mail S/MIME implementation * * Please report any new invalid timestamps to [#45](https://github.com/Webklex/php-imap/issues) * @@ -720,8 +721,8 @@ private function parseDate(object $header): void { case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: $date = str_replace(',', '', $date); break; - // match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ) - case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+$/i', $date) > 0: + // match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ) and Mi., 23 Apr. 2025 09:48:37 +0200 (MESZ) + case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))(\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+)?$/i', $date) > 0: $dates = explode('/', $date); $date = array_shift($dates); $array = explode(',', $date); diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index 9c43ae8b..77dfd7bb 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -51,6 +51,7 @@ class DateTemplateTest extends FixtureTestCase { "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", "fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100" => "2022-11-25 05:27:14", "Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)" => "2022-02-15 05:52:44", + "Mi., 23 Apr. 2025 09:48:37 +0200 (MESZ)" => "2025-04-23 07:48:37", ]; /** From 6b8ef85d621bbbaf52741b00cca8e9237e2b2e05 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 25 Apr 2025 08:02:37 +0200 Subject: [PATCH 597/600] Release information added --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7159d0af..97935023 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,28 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Fix case sensitivity of Folder attribute parsing (\NoSelect, \NoInferiors) #469 (thanks @smajti1) +- NaN ### Added -- SSL stream context options added #238 #546 (thanks @llemoine) +- NaN ### Breaking changes - NaN +## [6.2.0] - 2025-04-25 +### Fixed +- When using the chunk function, some messages do not have an element with index 0 #552 #553 (thanks @zeddmaster) +- Get folders list in hierarchical order #560 #561 (thanks @rskrzypczak) +- Fix remaining implicit marking of parameters as nullable (PHP 8.4) #566 (thanks @steffenweber) +- Fix case sensitivity of folder attribute parsing (\NoSelect, \NoInferiors) #469 #571 (thanks @smajti1) +- Fix error on getUid(null) with 0 results (#499) #573 (thanks @pierement) +- Fix Date parsing on non-standard format from Aqua Mail #574 #575 (thanks @lm-cmxkonzepte) + +### Added +- SSL stream context options added #238 #546 (thanks @llemoine) +- Support copy/move Message with utf7 folder path #559 (thanks @loc4l) +- Public `Query::search()` method #565 (Thanks @madbob) + ## [6.1.0] - 2025-01-19 ### Fixed - Filename sanitization is now optional (enabled via default) @@ -65,12 +79,12 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. - `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. - All protocol constructors now require a `Config::class` instance -- The `Client::class` constructor now require a `Config::class` instance -- The `Part::class` constructor now require a `Config::class` instance -- The `Header::class` constructor now require a `Config::class` instance -- The `Message::fromFile` method now requires a `Config::class` instance -- The `Message::fromString` method now requires a `Config::class` instance -- The `Message::boot` method now requires a `Config::class` instance +- The `Client::class` constructor now require a `Config::class` instance +- The `Part::class` constructor now require a `Config::class` instance +- The `Header::class` constructor now require a `Config::class` instance +- The `Message::fromFile` method now requires a `Config::class` instance +- The `Message::fromString` method now requires a `Config::class` instance +- The `Message::boot` method now requires a `Config::class` instance - The `Message::decode` method has been removed. Use `Message::getDecoder()->decode($str)` instead. - The `Message::getEncoding` method has been removed. Use `Message::getDecoder()->getEncoding($str)` instead. - The `Message::convertEncoding` method has been removed. Use `Message::getDecoder()->convertEncoding()` instead. @@ -366,7 +380,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - Extend date parsing error message #173 - Fixed 'Where' method replaces the content with uppercase #148 - Don't surround numeric search values with quotes -- Context added to `InvalidWhereQueryCriteriaException` +- Context added to `InvalidWhereQueryCriteriaException` - Redundant `stream_set_timeout()` removed ### Added @@ -537,7 +551,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - Alias `Message::removeFlag()` for `Message::unsetFlag()` added - Alias `Message::flags()` for `Message::getFlags()` added - New Exception `MessageFlagException::class` added -- New method `Message::setSequenceId($id)` added +- New method `Message::setSequenceId($id)` added - Optional Header attributizion option added ### Affected Classes @@ -549,7 +563,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - [Attribute::class](src/Attribute.php) ### Breaking changes -- Stringified message headers are now separated by ", " instead of " ". +- Stringified message headers are now separated by ", " instead of " ". - All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - The formal address object "from", "to", etc now consists of an `Address::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - When fetching or manipulating message flags a `MessageFlagException::class` exception can be thrown if a runtime error occurs @@ -559,12 +573,12 @@ If you have any questions, please feel welcome to join this issue: https://githu ## [2.3.1] - 2020-12-30 ### Fixed -- Missing RFC attributes added +- Missing RFC attributes added - Set the message sequence when idling - Missing UID commands added #64 ### Added -- Get a message by its message number +- Get a message by its message number - Get a message by its uid #72 #66 #63 ### Affected Classes @@ -585,7 +599,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - `Message::getTextBody()` fallback value fixed ### Added -- Proxy support added +- Proxy support added - Flexible disposition support added #58 - New `options.message_key` option `uid` added - Protocol UID support added @@ -919,7 +933,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - Imap client timeout can be modified and read #186 - Decoder config options added #175 - Message search criteria "NOT" added #181 -- Invalid message date exception added +- Invalid message date exception added - Blade examples ### Breaking changes From 15d01425cf04e22b6523f40983c7fdbb682a85a2 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 25 Apr 2025 08:28:02 +0200 Subject: [PATCH 598/600] List of alternatives updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da47d048..fa6cd7a1 100755 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ This library and especially the code flavor It's written in, is certainly not fo different approach, you might want to check out the following libraries: - [ddeboer/imap](https://github.com/ddeboer/imap) - [barbushin/php-imap](https://github.com/barbushin/php-imap) -- [stevebauman/php-imap](https://github.com/stevebauman/php-imap) +- [DirectoryTree/ImapEngine](https://github.com/DirectoryTree/ImapEngine) ## Change log From 56f3b6b3c6c2c0dfcc07f0a2a2b0f322299fcbf3 Mon Sep 17 00:00:00 2001 From: wurst-hans <56444979+wurst-hans@users.noreply.github.com> Date: Wed, 7 May 2025 18:23:47 +0200 Subject: [PATCH 599/600] Reading whole lines from stream (#576) Replaced char-by-char retrieval from stream via fread() with fgets() to get full line from stream until EOF or newline. This speeds up data transfer by factor 10. --- src/Connection/Protocols/ImapProtocol.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ed1dac6e..6871d57f 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -135,14 +135,10 @@ protected function enableStartTls(): void { * @throws RuntimeException */ public function nextLine(Response $response): string { - $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["", "\n"])) { - $line .= $next_char; - } - if ($line === "" && ($next_char === false || $next_char === "")) { + $line = fgets($this->stream); + if ($line === false || $line === '') { throw new RuntimeException('empty response'); } - $line .= "\n"; $response->addResponse($line); if ($this->debug) echo "<< " . $line; return $line; From 5656386a3597dacf93f8887d2016ed3cea2adbb7 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 7 May 2025 18:27:06 +0200 Subject: [PATCH 600/600] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97935023..438ee606 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Reading whole lines from stream #576 (thanks @wurst-hans) ### Added - NaN