From 66c871550258ad7d7f5be392174387b89120bef2 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 9 Oct 2024 16:17:39 +0300 Subject: [PATCH 01/35] add mailbox bridge to imap module, subclass for imap/jmap and ews support, add ews package --- composer.json | 3 +- composer.lock | 356 ++++++++++++++++++- modules/imap/functions.php | 55 ++- modules/imap/handler_modules.php | 565 +++++++++++-------------------- modules/imap/hm-ews.php | 19 ++ modules/imap/hm-imap.php | 17 +- modules/imap/hm-mailbox.php | 339 +++++++++++++++++++ 7 files changed, 942 insertions(+), 412 deletions(-) create mode 100644 modules/imap/hm-ews.php create mode 100644 modules/imap/hm-mailbox.php diff --git a/composer.json b/composer.json index 6e4993990d..4cb7b48dcf 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,8 @@ "twbs/bootstrap": "^5.3", "twbs/bootstrap-icons": "^1.11", "webklex/composer-info": "^0.0.1", - "zbateson/mail-mime-parser": "^2.4" + "zbateson/mail-mime-parser": "^2.4", + "garethp/php-ews": "^0.10.1" }, "require-dev": { "phpunit/phpunit": "^10.5" diff --git a/composer.lock b/composer.lock index b7fd518d53..909e66ac8d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8de6d181041346fe7a32bd3994050781", + "content-hash": "ad61b5e9e80d518e0ffbd8c67609fa7a", "packages": [ { "name": "bacon/bacon-qr-code", @@ -305,6 +305,306 @@ }, "time": "2023-11-17T15:01:25+00:00" }, + { + "name": "garethp/http-playback", + "version": "v2.0", + "source": { + "type": "git", + "url": "https://github.com/Garethp/HttpPlayback.git", + "reference": "50fd7b75c3d08ac0548df6d834a60ac65fc365df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Garethp/HttpPlayback/zipball/50fd7b75c3d08ac0548df6d834a60ac65fc365df", + "reference": "50fd7b75c3d08ac0548df6d834a60ac65fc365df", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~7.0" + }, + "require-dev": { + "phpunit/phpunit": "~9.5", + "squizlabs/php_codesniffer": "~3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "garethp\\HttpPlayback\\": "src/", + "garethp\\HttpPlayback\\Test\\": "tests/src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/Garethp/HttpPlayback/issues", + "source": "https://github.com/Garethp/HttpPlayback/tree/v2.0" + }, + "time": "2021-05-10T01:13:55+00:00" + }, + { + "name": "garethp/php-ews", + "version": "v0.10.1", + "source": { + "type": "git", + "url": "https://github.com/Garethp/php-ews.git", + "reference": "6fbc1b17d0c04fe313c40c5d712090638f6a1c84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Garethp/php-ews/zipball/6fbc1b17d0c04fe313c40c5d712090638f6a1c84", + "reference": "6fbc1b17d0c04fe313c40c5d712090638f6a1c84", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-dom": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "ext-soap": "*", + "garethp/http-playback": "^2.0" + }, + "require-dev": { + "goetas/xsd-reader": "^2.0-dev", + "goetas/xsd2php": "^2.1", + "mockery/mockery": "^1.4", + "phpunit/phpunit": "~9.5", + "squizlabs/php_codesniffer": "~3.6.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Utilities/ensureIsArray.php", + "src/Utilities/ensureIsDateTime.php", + "src/Utilities/cloneValue.php", + "src/Utilities/getFolderIds.php" + ], + "psr-4": { + "garethp\\ews\\": "src/", + "garethp\\ews\\Test\\": "tests/src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A PHP Library to interact with the Exchange SOAP service", + "support": { + "issues": "https://github.com/Garethp/php-ews/issues", + "source": "https://github.com/Garethp/php-ews/tree/v0.10.1" + }, + "time": "2021-09-17T15:21:51+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-07-18T10:29:17+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.6.2", @@ -1036,6 +1336,58 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, { "name": "psr/http-factory", "version": "1.0.2", @@ -3605,5 +3957,5 @@ "platform-overrides": { "php": "8.1" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/modules/imap/functions.php b/modules/imap/functions.php index 107f95daec..ed2b80d00a 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -1282,14 +1282,11 @@ function get_request_params($request) { }} if (!hm_exists('snooze_message')) { -function snooze_message($imap, $msg_id, $folder, $snooze_tag) { - if (!$imap->select_mailbox($folder)) { - return false; - } +function snooze_message($mailbox, $msg_id, $folder, $snooze_tag) { if (!$snooze_tag) { - $imap->message_action('UNREAD', array($msg_id)); + $mailbox->message_action($folder, 'UNREAD', array($msg_id)); } - $msg = $imap->get_message_content($msg_id, 0); + $msg = $mailbox->get_message_content($folder, $msg_id); preg_match("/^X-Snoozed:.*(\r?\n[ \t]+.*)*\r?\n?/im", $msg, $matches); if (count($matches)) { $msg = str_replace($matches[0], '', $msg); @@ -1306,39 +1303,30 @@ function snooze_message($imap, $msg_id, $folder, $snooze_tag) { $res = false; $snooze_folder = 'Snoozed'; if ($snooze_tag) { - if (!count($imap->get_mailbox_status($snooze_folder))) { - $imap->create_mailbox($snooze_folder); - } - if ($imap->select_mailbox($snooze_folder) && $imap->append_start($snooze_folder, mb_strlen($msg))) { - $imap->append_feed($msg."\r\n"); - if ($imap->append_end()) { - if ($imap->select_mailbox($folder) && $imap->message_action('DELETE', array($msg_id))) { - $imap->message_action('EXPUNGE', array($msg_id)); - $res = true; - } + if (!count($mailbox->get_folder_status($snooze_folder))) { + $mailbox->create_folder($snooze_folder); + } + if ($mailbox->store_message($snooze_folder, $msg)) { + if ($mailbox->message_action($folder, 'DELETE', array($msg_id))) { + $mailbox->message_action($folder, 'EXPUNGE', array($msg_id)); + $res = true; } } } else { $snooze_headers = parse_snooze_header($matches[0]); $original_folder = $snooze_headers['from']; - if ($imap->select_mailbox($original_folder) && $imap->append_start($original_folder, mb_strlen($msg))) { - $imap->append_feed($msg."\r\n"); - if ($imap->append_end()) { - if ($imap->select_mailbox($snooze_folder) && $imap->message_action('DELETE', array($msg_id))) { - $imap->message_action('EXPUNGE', array($msg_id)); - $res = true; - } + if ($mailbox->store_message($original_folder, $msg)) { + if ($mailbox->message_action($snooze_folder, 'DELETE', array($msg_id))) { + $mailbox->message_action($snooze_folder, 'EXPUNGE', array($msg_id)); + $res = true; } } } return $res; }} if (!hm_exists('add_tag_to_message')) { -function add_tag_to_message($imap, $msg_id, $folder, $tag) { - if (!$imap->select_mailbox($folder)) { - return false; - } - $msg = $imap->get_message_content($msg_id, 0); +function add_tag_to_message($mailbox, $msg_id, $folder, $tag) { + $msg = $mailbox->get_message_content($folder, $msg_id); preg_match("/^X-Cypht-Tags:(.+)\r?\n/i", $msg, $matches); if (count($matches)) { @@ -1359,13 +1347,10 @@ function add_tag_to_message($imap, $msg_id, $folder, $tag) { $msg = rtrim($msg)."\r\n"; $res = false; - if ($imap->append_start($folder, strlen($msg))) { - $imap->append_feed($msg."\r\n"); - if ($imap->append_end()) { - if ($imap->message_action('DELETE', array($msg_id))) { - $imap->message_action('EXPUNGE', array($msg_id)); - $res = true; - } + if ($mailbox->store_message($folder, $msg)) { + if ($mailbox->message_action($folder, 'DELETE', array($msg_id))) { + $mailbox->message_action($folder, 'EXPUNGE', array($msg_id)); + $res = true; } } diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index 9008f4e99a..5eb4ca8cc1 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -37,15 +37,11 @@ public function process() { if (!$filepath) { return; } - $cache = Hm_IMAP_List::get_cache($this->cache, $path[1]); - $imap = Hm_IMAP_List::connect($path[1], $cache); - if (!imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1], $this->cache); + if (! $mailbox) { return; } - if (!$imap->select_mailbox(hex2bin($path[2]))) { - return; - } - $content = $imap->get_message_content($uid, 0); + $content = $mailbox->get_message_content(hex2bin($path[2]), $uid); if (!$content) { return; } @@ -71,10 +67,9 @@ class Hm_Handler_imap_folder_status extends Hm_Handler_Module { public function process() { list($success, $form) = $this->process_form(array('imap_server_id', 'folder')); if ($success) { - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (imap_authed($imap)) { - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $imap->get_mailbox_status(hex2bin($form['folder'])))); + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status(hex2bin($form['folder'])))); } } } @@ -255,16 +250,17 @@ public function process() { $screen = false; $parts = explode("_", $this->request->get['list_path']); $imap_server_id = $parts[1] ?? ''; - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); - $imap = Hm_IMAP_List::connect($imap_server_id, $cache); if ($form['imap_move_action'] == "screen_mail") { - $form['imap_move_action'] = "move"; - $screen = true; - $screen_folder = 'Screen emails'; - if (!count($imap->get_mailbox_status($screen_folder))) { - $imap->create_mailbox($screen_folder); + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { + $form['imap_move_action'] = "move"; + $screen = true; + $screen_folder = 'Screen emails'; + if (! count($mailbox->get_folder_status($screen_folder))) { + $mailbox->create_folder($screen_folder); + } + $form['imap_move_to'] = $parts[0] ."_". $parts[1] ."_".bin2hex($screen_folder); } - $form['imap_move_to'] = $parts[0] ."_". $parts[1] ."_".bin2hex($screen_folder); } list($msg_ids, $dest_path, $same_server_ids, $other_server_ids) = process_move_to_arguments($form); @@ -330,18 +326,17 @@ public function process() { $msg = str_replace("\r\n", "\n", $msg); $msg = str_replace("\n", "\r\n", $msg); $msg = rtrim($msg)."\r\n"; - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_id); - $imap = Hm_IMAP_List::connect($imap_id, $cache); $imap_details = Hm_IMAP_List::dump($imap_id); $sent_folder = false; - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_id, $this->cache); + if ($mailbox && $mailbox->authed()) { $specials = get_special_folders($this, $imap_id); if (array_key_exists('sent', $specials) && $specials['sent']) { $sent_folder = $specials['sent']; } if (!$sent_folder) { - $auto_sent = $imap->get_special_use_mailboxes('sent'); + $auto_sent = $mailbox->get_special_use_mailboxes('sent'); if (!array_key_exists('sent', $auto_sent)) { return; } @@ -352,16 +347,13 @@ public function process() { } if ($sent_folder) { Hm_Debug::add(sprintf("Attempting to save sent message for IMAP server %s in folder %s", $imap_details['server'], $sent_folder)); - if ($imap->append_start($sent_folder, mb_strlen($msg), true)) { - $imap->append_feed($msg."\r\n"); - if (!$imap->append_end()) { - Hm_Msgs::add('ERRAn error occurred saving the sent message'); - } + if (! $mailbox->store_message($sent_folder, $msg)) { + Hm_Msgs::add('ERRAn error occurred saving the sent message'); } $uid = null; - $mailbox_page = $imap->get_mailbox_page($sent_folder, 'ARRIVAL', true, 'ALL', 0, 10); + $mailbox_page = $mailbox->get_messages($sent_folder, 'ARRIVAL', true, 'ALL', 0, 10); foreach ($mailbox_page[1] as $mail) { - $msg_header = $imap->get_message_headers($mail['uid']); + $msg_header = $mailbox->get_message_headers($mail['uid']); if ($msg_header['Message-Id'] === $mime->get_headers()['Message-Id']) { $uid = $mail['uid']; break; @@ -386,10 +378,9 @@ public function process() { if ($success) { $path = explode('_', $form['compose_msg_path']); if (count($path) == 3 && $path[0] == 'imap') { - $cache = Hm_IMAP_List::get_cache($this->cache, $path[1]); - $imap = Hm_IMAP_List::connect($path[1], $cache); - if (imap_authed($imap) && $imap->select_mailbox(hex2bin($path[2]))) { - $imap->message_action('UNFLAG', array($form['compose_msg_uid'])); + $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1], $this->cache); + if ($mailbox && $mailbox->authed()) { + $mailbox->message_action(hex2bin($path[2]), 'UNFLAG', array($form['compose_msg_uid'])); } } } @@ -408,11 +399,10 @@ public function process() { if ($success) { $path = explode('_', $form['compose_msg_path']); if (count($path) == 3 && $path[0] == 'imap') { - $cache = Hm_IMAP_List::get_cache($this->cache, $path[1]); - $imap = Hm_IMAP_List::connect($path[1], $cache); - if (imap_authed($imap) && $imap->select_mailbox(hex2bin($path[2]))) { - $this->out('folder_status', array('imap_'.$path[1].'_'.$path[2] => $imap->folder_state)); - $imap->message_action('ANSWERED', array($form['compose_msg_uid'])); + $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1], $this->cache); + if ($mailbox && $mailbox->authed()) { + $this->out('folder_status', array('imap_'.$path[1].'_'.$path[2] => $mailbox->getFolderState())); + $mailbox->message_action(hex2bin($path[2]), 'ANSWERED', array($form['compose_msg_uid'])); } } } @@ -431,11 +421,10 @@ class Hm_Handler_imap_mark_as_read extends Hm_Handler_Module { public function process() { list($success, $form) = $this->process_form(array('imap_server_id', 'imap_msg_uid', 'folder')); if ($success) { - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (imap_authed($imap) && $imap->select_mailbox(hex2bin($form['folder']))) { - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $imap->folder_state)); - $imap->message_action('READ', array($form['imap_msg_uid'])); + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->getFolderState())); + $mailbox->message_action(hex2bin($form['folder']), 'READ', array($form['imap_msg_uid'])); } } } @@ -497,49 +486,14 @@ public function process() { $msg_id = preg_replace("/^0.{1}/", '', $this->request->get['imap_msg_part']); } if ($server_id !== NULL && $uid !== NULL && $folder !== NULL && $msg_id !== NULL) { - $cache = Hm_IMAP_List::get_cache($this->cache, $server_id); - $imap = Hm_IMAP_List::connect($server_id, $cache); - if (imap_authed($imap)) { - if ($imap->select_mailbox($folder)) { - $msg_struct = $imap->get_message_structure($uid); - $struct = $imap->search_bodystructure($msg_struct, array('imap_part_number' => $msg_id)); - if (!empty($struct)) { - $part_struct = array_shift($struct); - $encoding = false; - if (array_key_exists('encoding', $part_struct)) { - $encoding = trim(mb_strtolower($part_struct['encoding'])); - } - $stream_size = $imap->start_message_stream($uid, $msg_id); - if ($stream_size > 0) { - $charset = ''; - if (array_key_exists('attributes', $part_struct)) { - if (is_array($part_struct['attributes']) && array_key_exists('charset', $part_struct['attributes'])) { - $charset = '; charset='.$part_struct['attributes']['charset']; - } - } - header('Content-Type: '.$part_struct['type'].'/'.$part_struct['subtype'].$charset); - header('Content-Transfer-Encoding: binary'); - ob_end_clean(); - $output_line = ''; - while($line = $imap->read_stream_line()) { - if ($encoding == 'quoted-printable') { - $line = quoted_printable_decode($line); - } - elseif ($encoding == 'base64') { - $line = base64_decode($line); - } - echo $output_line; - $output_line = $line; - - } - if ($part_struct['type'] == 'text') { - $output_line = preg_replace("/\)(\r\n)$/m", '$1', $output_line); - } - echo $output_line; - Hm_Functions::cease(); - } - } - } + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { + $mailbox->stream_message_part($folder, $uid, $msg_id, function ($content_type) { + header('Content-Type: ' . $content_type); + header('Content-Transfer-Encoding: binary'); + ob_end_clean(); + }); + Hm_Functions::cease(); } } Hm_Msgs::add('ERRAn Error occurred trying to download the message'); @@ -560,50 +514,15 @@ public function process() { list($server_id, $uid, $folder, $msg_id) = get_request_params($this->request->get); if ($server_id !== NULL && $uid !== NULL && $folder !== NULL && $msg_id !== NULL) { - $cache = Hm_IMAP_List::get_cache($this->cache, $server_id); - $imap = Hm_IMAP_List::connect($server_id, $cache); - if (imap_authed($imap)) { - if ($imap->select_mailbox($folder)) { - $msg_struct = $imap->get_message_structure($uid); - $struct = $imap->search_bodystructure($msg_struct, array('imap_part_number' => $msg_id)); - if (!empty($struct)) { - $part_struct = array_shift($struct); - $encoding = false; - if (array_key_exists('encoding', $part_struct)) { - $encoding = trim(mb_strtolower($part_struct['encoding'])); - } - $stream_size = $imap->start_message_stream($uid, $msg_id); - if ($stream_size > 0) { - $name = get_imap_part_name($part_struct, $uid, $msg_id); - header('Content-Disposition: attachment; filename="'.$name.'"'); - $charset = ''; - if (array_key_exists('attributes', $part_struct)) { - if (is_array($part_struct['attributes']) && array_key_exists('charset', $part_struct['attributes'])) { - $charset = '; charset='.$part_struct['attributes']['charset']; - } - } - header('Content-Type: '.$part_struct['type'].'/'.$part_struct['subtype'].$charset); - header('Content-Transfer-Encoding: binary'); - ob_end_clean(); - $output_line = ''; - while($line = $imap->read_stream_line()) { - if ($encoding == 'quoted-printable') { - $line = quoted_printable_decode($line); - } - elseif ($encoding == 'base64') { - $line = base64_decode($line); - } - echo $output_line; - $output_line = $line; - } - if ($part_struct['type'] == 'text') { - $output_line = preg_replace("/\)(\r\n)$/m", '$1', $output_line); - } - echo $output_line; - Hm_Functions::cease(); - } - } - } + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { + $mailbox->stream_message_part($folder, $uid, $msg_id, function ($content_type, $part_name) { + header('Content-Disposition: attachment; filename="' . $part_name . '"'); + header('Content-Type: ' . $content_type); + header('Content-Transfer-Encoding: binary'); + ob_end_clean(); + }); + Hm_Functions::cease(); } } Hm_Msgs::add('ERRAn Error occurred trying to download the message'); @@ -698,29 +617,12 @@ public function process() { if (array_key_exists('imap_remove_attachment', $this->request->get) && $this->request->get['imap_remove_attachment']) { list($server_id, $uid, $folder, $msg_id) = get_request_params($this->request->get); if ($server_id !== NULL && $uid !== NULL && $folder !== NULL && $msg_id !== NULL) { - $cache = Hm_IMAP_List::get_cache($this->cache, $server_id); - $imap = Hm_IMAP_List::connect($server_id, $cache); - if (imap_authed($imap)) { - if ($imap->select_mailbox($folder)) { - $msg = $imap->get_message_content($uid, 0, false, false); - if ($msg) { - $attachment_id = get_attachment_id_for_mail_parser($imap, $uid, $this->request->get['imap_msg_part']); - if ($attachment_id !== false) { - $msg = remove_attachment($attachment_id, $msg); - if ($imap->append_start($folder, mb_strlen($msg))) { - $imap->append_feed($msg."\r\n"); - if ($imap->append_end()) { - if ($imap->message_action('DELETE', array($uid))) { - $imap->message_action('EXPUNGE', array($uid)); - Hm_Msgs::add('Attachment deleted'); - $this->out('redirect_url', '?page=message_list&list_path='.$this->request->get['list_path']); - return; - } - } - } - } - - } + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { + if ($mailbox->remove_attachment($folder, $uid, $this->request->get['imap_msg_part'])) { + Hm_Msgs::add('Attachment deleted'); + $this->out('redirect_url', '?page=message_list&list_path=' . $this->request->get['list_path']); + return; } } } @@ -753,12 +655,11 @@ public function process() { $this->session->set('imap_prefetched_ids', array_unique($prefetched, SORT_STRING)); } $with_subscription = isset($this->request->post['subscription_state']) && $this->request->post['subscription_state']; - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (imap_authed($imap)) { - $quota_root = $imap->get_quota_root($folder ? $folder : 'INBOX'); + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $quota_root = $mailbox->get_quota($folder ? $folder : 'INBOX', true); if ($quota_root && isset($quota_root[0]['name'])) { - $quota = $imap->get_quota($quota_root[0]['name']); + $quota = $mailbox->get_quota($quota_root[0]['name'], false); if ($quota) { $current = floatval($quota[0]['current']); $max = floatval($quota[0]['max']); @@ -833,9 +734,8 @@ public function process() { } $path = sprintf("imap_%s_%s", $form['imap_server_id'], $form['folder']); $details = Hm_IMAP_List::dump($form['imap_server_id']); - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { $this->out('imap_mailbox_page_path', $path); if (isset($this->request->get['screen_emails']) && hex2bin($form['folder']) == 'INBOX' && $this->module_is_supported("contacts")) { $contacts = $this->get('contact_store'); @@ -844,9 +744,9 @@ public function process() { $existingEmails = array_map(function($c){ return $c->value('email_address'); },$contact_list); - list($total, $results) = $imap->get_mailbox_page(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword, $existingEmails); + list($total, $results) = $mailbox->get_messages(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword, $existingEmails); } else { - list($total, $results) = $imap->get_mailbox_page(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword); + list($total, $results) = $mailbox->get_messages(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword); } foreach ($results as $msg) { $msg['server_id'] = $form['imap_server_id']; @@ -854,11 +754,11 @@ public function process() { $msg['folder'] = $form['folder']; $msgs[] = $msg; } - if ($imap->selected_mailbox) { - $imap->selected_mailbox['detail']['exists'] = $total; - $this->out('imap_folder_detail', array_merge($imap->selected_mailbox, array('offset' => $offset, 'limit' => $limit))); + if ($folder = $mailbox->get_selected_folder()) { + $folder['detail']['exists'] = $total; + $this->out('imap_folder_detail', array_merge($folder, array('offset' => $offset, 'limit' => $limit))); } - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $imap->folder_state)); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); } $this->out('imap_mailbox_page', $msgs); $this->out('list_page', $list_page); @@ -899,28 +799,17 @@ public function process() { list($success, $form) = $this->process_form(array('imap_msg_uid', 'imap_server_id', 'folder')); if ($success) { $del_result = false; - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); $trash_folder = false; $specials = get_special_folders($this, $form['imap_server_id']); if (array_key_exists('trash', $specials) && $specials['trash']) { $trash_folder = $specials['trash']; } - if (imap_authed($imap)) { - if ($imap->select_mailbox(hex2bin($form['folder']))) { - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $imap->folder_state)); - if ($trash_folder && $trash_folder != hex2bin($form['folder'])) { - if ($imap->message_action('MOVE', array($form['imap_msg_uid']), $trash_folder)) { - $del_result = true; - } - } - else { - if ($imap->message_action('DELETE', array($form['imap_msg_uid']))) { - $del_result = true; - $imap->message_action('EXPUNGE', array($form['imap_msg_uid'])); - } - } + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + if ($mailbox->delete_message(hex2bin($form['folder']), $form['imap_msg_uid'], $trash_folder)) { + $del_result = true; } + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); } if (!$del_result) { Hm_Msgs::add('ERRAn error occurred trying to delete this message'); @@ -950,8 +839,7 @@ public function process() { if (!$success) { return; } - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); + $archive_folder = false; $errors = 0; @@ -964,8 +852,9 @@ public function process() { $errors++; } - if (!$errors && imap_authed($imap)) { - $archive_exists = count($imap->get_mailbox_status($archive_folder)); + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if (! $errors && $mailbox && $mailbox->authed()) { + $archive_exists = count($mailbox->get_folder_status($archive_folder)); if (!$archive_exists) { Hm_Msgs::add('Configured archive folder for this IMAP server does not exist'); $errors++; @@ -973,18 +862,12 @@ public function process() { $form_folder = hex2bin($form['folder']); - /* select source folder */ - if ($errors || !$imap->select_mailbox($form_folder)) { - Hm_Msgs::add('ERRAn error occurred archiving the message'); - $errors++; - } - /* path according to original option setting */ if ($this->user_config->get('original_folder_setting', false)) { - $archive_folder .= '/'.$form_folder; - if (!count($imap->get_mailbox_status($archive_folder))) { - if (! $imap->create_mailbox($archive_folder)) { - $debug = $imap->show_debug(true, true, true); + $archive_folder .= '/' . $form_folder; + if (!count($mailbox->get_folder_status($archive_folder))) { + if (! $mailbox->create_folder($archive_folder)) { + $debug = $mailbox->get_debug(); if (! empty($debug['debug'])) { Hm_Msgs::add('ERR' . array_pop($debug['debug'])); } else { @@ -996,7 +879,7 @@ public function process() { } /* try to move the message */ - if (!$errors && $imap->message_action('MOVE', array($form['imap_msg_uid']), $archive_folder)) { + if (! $errors && $mailbox->message_action($form_folder, 'MOVE', array($form['imap_msg_uid']), $archive_folder)) { Hm_Msgs::add("Message archived"); } else { @@ -1019,21 +902,18 @@ public function process() { list($success, $form) = $this->process_form(array('imap_flag_state', 'imap_msg_uid', 'imap_server_id', 'folder')); if ($success) { $flag_result = false; - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (imap_authed($imap)) { - if ($imap->select_mailbox(hex2bin($form['folder']))) { - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $imap->folder_state)); - if ($form['imap_flag_state'] == 'flagged') { - $cmd = 'UNFLAG'; - } - else { - $cmd = 'FLAG'; - } - if ($imap->message_action($cmd, array($form['imap_msg_uid']))) { - $flag_result = true; - } + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + if ($form['imap_flag_state'] == 'flagged') { + $cmd = 'UNFLAG'; + } + else { + $cmd = 'FLAG'; + } + if ($mailbox->message_action(hex2bin($form['folder']), $cmd, array($form['imap_msg_uid']))) { + $flag_result = true; } + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); } if (!$flag_result) { Hm_Msgs::add('ERRAn error occurred trying to flag this message'); @@ -1065,11 +945,10 @@ public function process() { $ids = explode(',', $form['imap_snooze_ids']); foreach ($ids as $msg_part) { list($imap_server_id, $msg_id, $folder) = explode('_', $msg_part); - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); - $imap = Hm_IMAP_List::connect($imap_server_id, $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { $folder = hex2bin($folder); - if (snooze_message($imap, $msg_id, $folder, $snooze_tag)) { + if (snooze_message($mailbox, $msg_id, $folder, $snooze_tag)) { $snoozed_messages++; } } @@ -1104,11 +983,10 @@ public function process() { $ids = explode(',', $form['imap_server_ids']); foreach ($ids as $msg_part) { list($imap_server_id, $msg_id, $folder) = explode('_', $msg_part); - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); - $imap = Hm_IMAP_List::connect($imap_server_id, $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { $folder = hex2bin($folder); - if (add_tag_to_message($imap, $msg_id, $folder, $form['tag_id'])) { + if (add_tag_to_message($mailbox, $msg_id, $folder, $form['tag_id'])) { $taged_messages++; } } @@ -1137,21 +1015,20 @@ class Hm_Handler_imap_unsnooze_message extends Hm_Handler_Module { public function process() { $servers = Hm_IMAP_List::dump(); foreach (array_keys($servers) as $server_id) { - $cache = Hm_IMAP_List::get_cache($this->cache, $server_id); - $imap = Hm_IMAP_List::connect($server_id, $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { $folder = 'Snoozed'; - if (!count($imap->get_mailbox_status($folder))) { + if (! count($mailbox->get_folder_status($folder))) { continue; } - $ret = $imap->get_mailbox_page($folder, 'DATE', false, 'ALL'); + $ret = $mailbox->get_messages($folder, 'DATE', false, 'ALL'); foreach ($ret[1] as $msg) { - $msg_headers = $imap->get_message_headers($msg['uid']); + $msg_headers = $mailbox->get_message_headers($msg['uid']); if (isset($msg_headers['X-Snoozed'])) { try { $snooze_headers = parse_snooze_header($msg_headers['X-Snoozed']); if (new DateTime($snooze_headers['until']) <= new DateTime()) { - snooze_message($imap, $msg['uid'], $folder, null); + snooze_message($mailbox, $msg['uid'], $folder, null); } } catch (Exception $e) { Hm_Debug::add(sprintf('ERR Cannot unsnooze message: %s', $msg_headers['subject'])); @@ -1184,9 +1061,8 @@ public function process() { $specials = get_special_folders($this, $server); $trash_folder = false; $archive_folder = false; - $cache = Hm_IMAP_List::get_cache($this->cache, $server); - $imap = Hm_IMAP_List::connect($server, $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($server, $this->cache); + if ($mailbox && $mailbox->authed()) { $server_details = $this->user_config->get('imap_servers')[$server]; if ($form['action_type'] == 'delete') { if (array_key_exists('trash', $specials)) { @@ -1208,46 +1084,44 @@ public function process() { } foreach ($folders as $folder => $uids) { - if ($imap->select_mailbox(hex2bin($folder))) { - $status['imap_'.$server.'_'.$folder] = $imap->folder_state; + $status['imap_'.$server.'_'.$folder] = $imap->folder_state; - if ($form['action_type'] == 'delete' && $trash_folder && $trash_folder != hex2bin($folder)) { - if (!$imap->message_action('MOVE', $uids, $trash_folder)) { - $errs++; - } - else { - foreach ($uids as $uid) { - $moved[] = sprintf("imap_%s_%s_%s", $server, $uid, $folder); - } - } + if ($form['action_type'] == 'delete' && $trash_folder && $trash_folder != hex2bin($folder)) { + if (! $mailbox->message_action(hex2bin($folder), 'MOVE', $uids, $trash_folder)) { + $errs++; } - elseif ($form['action_type'] == 'archive' && $archive_folder && $archive_folder != hex2bin($folder)) { - /* path according to original option setting */ - if ($this->user_config->get('original_folder_setting', false)) { - $archive_folder .= '/'.hex2bin($folder); - $dest_path_exists = count($imap->get_mailbox_status($archive_folder)); - if (!$dest_path_exists) { - $imap->create_mailbox($archive_folder); - } - } - if (!$imap->message_action('MOVE', $uids, $archive_folder)) { - $errs++; + else { + foreach ($uids as $uid) { + $moved[] = sprintf("imap_%s_%s_%s", $server, $uid, $folder); } - else { - foreach ($uids as $uid) { - $moved[] = sprintf("imap_%s_%s_%s", $server, $uid, $folder); - } + } + } + elseif ($form['action_type'] == 'archive' && $archive_folder && $archive_folder != hex2bin($folder)) { + /* path according to original option setting */ + if ($this->user_config->get('original_folder_setting', false)) { + $archive_folder .= '/' . hex2bin($folder); + $dest_path_exists = count($mailbox->get_folder_status($archive_folder)); + if (!$dest_path_exists) { + $mailbox->create_folder($archive_folder); } } + if (! $mailbox->message_action(hex2bin($folder), 'MOVE', $uids, $archive_folder)) { + $errs++; + } else { - if (!$imap->message_action(mb_strtoupper($form['action_type']), $uids)) { - $errs++; + foreach ($uids as $uid) { + $moved[] = sprintf("imap_%s_%s_%s", $server, $uid, $folder); } - else { - $msgs += count($uids); - if ($form['action_type'] == 'delete') { - $imap->message_action('EXPUNGE', $uids); - } + } + } + else { + if (! $mailbox->message_action(hex2bin($folder), mb_strtoupper($form['action_type']), $uids)) { + $errs++; + } + else { + $msgs += count($uids); + if ($form['action_type'] == 'delete') { + $mailbox->message_action(hex2bin($folder), 'EXPUNGE', $uids); } } } @@ -1365,13 +1239,12 @@ public function process() { if ($success) { $ids = explode(',', $form['imap_server_ids']); foreach ($ids as $id) { - $cache = Hm_IMAP_List::get_cache($this->cache, $id); $start_time = microtime(true); - $imap = Hm_IMAP_List::connect($id, $cache); + $mailbox = Hm_IMAP_List::get_connected_mailbox($id, $this->cache); $this->out('imap_connect_time', microtime(true) - $start_time); - if (imap_authed($imap)) { - $this->out('imap_capabilities_list', $imap->get_capability()); - $this->out('imap_connect_status', $imap->get_state()); + if ($mailbox && $mailbox->authed()) { + $this->out('imap_capabilities_list', $mailbox->get_capability()); + $this->out('imap_connect_status', $mailbox->get_state()); $this->out('imap_status_server_id', $id); } else { @@ -1817,20 +1690,20 @@ public function process() { } } - $imap = false; + $mailbox = false; $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); if ($success) { - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache, $form['imap_user'], $form['imap_pass']); + $mailbox = Hm_IMAP_List::connect($form['imap_server_id'], $cache, $form['imap_user'], $form['imap_pass']); } elseif (isset($form['imap_server_id'])) { - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); + $mailbox = Hm_IMAP_List::connect($form['imap_server_id'], $cache); } - if ($imap) { - if ($imap->get_state() == 'authenticated') { - Hm_Msgs::add(sprintf("Successfully authenticated to the %s server : %s", $imap->server_type, $form['imap_user'])); + if ($mailbox) { + if ($mailbox->authed()) { + Hm_Msgs::add(sprintf("Successfully authenticated to the %s server : %s", $mailbox->server_type(), $form['imap_user'])); } else { - Hm_Msgs::add(sprintf("ERRFailed to authenticate to the %s server : %s", $imap->server_type, $form['imap_user'])); + Hm_Msgs::add(sprintf("ERRFailed to authenticate to the %s server : %s", $mailbox->server_type(), $form['imap_user'])); } } else { @@ -1900,11 +1773,11 @@ public function process() { return; } $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache, $form['imap_user'], $form['imap_pass'], true); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::connect($form['imap_server_id'], $cache, $form['imap_user'], $form['imap_pass'], true); + if ($mailbox && $mailbox->authed()) { $just_saved_credentials = true; Hm_Msgs::add("Server saved"); - $this->session->record_unsaved(sprintf('%s server saved', $imap->server_type)); + $this->session->record_unsaved(sprintf('%s server saved', $mailbox->server_type())); } else { Hm_Msgs::add("ERRUnable to save this server, are the username and password correct? " . $form['imap_user']); @@ -1948,97 +1821,54 @@ public function process() { $this->out('header_allow_images', $this->config->get('allow_external_image_sources')); - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { if ($this->user_config->get('unread_on_open_setting', false)) { - $imap->read_only = true; + $mailbox->set_read_only(true); } else { - $imap->read_only = $prefetch; - } - if ($imap->select_mailbox(hex2bin($form['folder']))) { - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $imap->folder_state)); - $msg_struct = $imap->get_message_structure($form['imap_msg_uid']); - $this->out('msg_struct', $msg_struct); - if ($part !== false) { - if ($part == 0) { - $max = 500000; - } - else { - $max = false; - } - $struct = $imap->search_bodystructure($msg_struct, array('imap_part_number' => $part)); - $msg_struct_current = array_shift($struct); - $msg_text = $imap->get_message_content($form['imap_msg_uid'], $part, $max, $msg_struct_current); - } - else { - if (!$this->user_config->get('text_only_setting', false)) { - list($part, $msg_text) = $imap->get_first_message_part($form['imap_msg_uid'], 'text', 'html', $msg_struct); - if (!$part) { - list($part, $msg_text) = $imap->get_first_message_part($form['imap_msg_uid'], 'text', false, $msg_struct); - } - } - else { - list($part, $msg_text) = $imap->get_first_message_part($form['imap_msg_uid'], 'text', false, $msg_struct); - } - $struct = $imap->search_bodystructure( $msg_struct, array('imap_part_number' => $part)); - $msg_struct_current = array_shift($struct); - if (!trim($msg_text)) { - if (is_array($msg_struct_current) && array_key_exists('subtype', $msg_struct_current)) { - if ($msg_struct_current['subtype'] == 'plain') { - $subtype = 'html'; - } - else { - $subtype = 'plain'; - } - list($part, $msg_text) = $imap->get_first_message_part($form['imap_msg_uid'], 'text', $subtype, $msg_struct); - $struct = $imap->search_bodystructure($msg_struct, array('imap_part_number' => $part)); - $msg_struct_current = array_shift($struct); - } - } - } - if (isset($msg_struct_current['subtype']) && mb_strtolower($msg_struct_current['subtype'] == 'html')) { - $msg_text = add_attached_images($msg_text, $form['imap_msg_uid'], $msg_struct, $imap); - } - $save_reply_text = false; - if ($part == 0 || (isset($msg_struct_current['type']) && mb_strtolower($msg_struct_current['type'] == 'text'))) { - $save_reply_text = true; - } - $msg_headers = $imap->get_message_headers($form['imap_msg_uid']); - $this->out('list_headers', get_list_headers($msg_headers)); - $this->out('msg_headers', $msg_headers); - $this->out('imap_prefecth', $prefetch); - $this->out('imap_msg_part', "$part"); - $this->out('use_message_part_icons', $this->user_config->get('msg_part_icons_setting', false)); - $this->out('simple_msg_part_view', $this->user_config->get('simple_msg_parts_setting', DEFAULT_SIMPLE_MSG_PARTS)); - $this->out('allow_delete_attachment', $this->user_config->get('allow_delete_attachment_setting', false)); - if ($msg_struct_current) { - $this->out('msg_struct_current', $msg_struct_current); - } - $this->out('msg_text', $msg_text); - $download_args = sprintf("page=message&uid=%s&list_path=imap_%s_%s", $form['imap_msg_uid'], $form['imap_server_id'], $form['folder']); - $this->out('msg_download_args', $download_args.'&imap_download_message=1'); - $this->out('msg_attachment_remove_args', $download_args.'&imap_remove_attachment=1'); - $this->out('msg_show_args', sprintf("page=message&uid=%s&list_path=imap_%s_%s&imap_show_message=1", $form['imap_msg_uid'], $form['imap_server_id'], $form['folder'])); - - if ($this->get('imap_allow_images', false)) { - if ($this->module_is_supported('contacts') && $this->user_config->get('contact_auto_collect_setting', false)) { - $this->out('collect_contacts', true); - $this->out('collected_contact_email', $msg_headers["Return-Path"]); - $this->out('collected_contact_name', $msg_headers["From"]); - } + $mailbox->set_read_only($prefetch); + } + list($msg_struct, $msg_struct_current, $msg_text, $part) = $mailbox->get_structured_message(hex2bin($form['folder']), $form['imap_msg_uid'], $part, $this->user_config->get('text_only_setting', false)); + $save_reply_text = false; + if ($part == 0 || (isset($msg_struct_current['type']) && mb_strtolower($msg_struct_current['type'] == 'text'))) { + $save_reply_text = true; + } + $msg_headers = $mailbox->get_message_headers($form['imap_msg_uid']); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); + $this->out('msg_struct', $msg_struct); + $this->out('list_headers', get_list_headers($msg_headers)); + $this->out('msg_headers', $msg_headers); + $this->out('imap_prefecth', $prefetch); + $this->out('imap_msg_part', "$part"); + $this->out('use_message_part_icons', $this->user_config->get('msg_part_icons_setting', false)); + $this->out('simple_msg_part_view', $this->user_config->get('simple_msg_parts_setting', DEFAULT_SIMPLE_MSG_PARTS)); + $this->out('allow_delete_attachment', $this->user_config->get('allow_delete_attachment_setting', false)); + if ($msg_struct_current) { + $this->out('msg_struct_current', $msg_struct_current); + } + $this->out('msg_text', $msg_text); + $download_args = sprintf("page=message&uid=%s&list_path=imap_%s_%s", $form['imap_msg_uid'], $form['imap_server_id'], $form['folder']); + $this->out('msg_download_args', $download_args.'&imap_download_message=1'); + $this->out('msg_attachment_remove_args', $download_args.'&imap_remove_attachment=1'); + $this->out('msg_show_args', sprintf("page=message&uid=%s&list_path=imap_%s_%s&imap_show_message=1", $form['imap_msg_uid'], $form['imap_server_id'], $form['folder'])); + + if ($this->get('imap_allow_images', false)) { + if ($this->module_is_supported('contacts') && $this->user_config->get('contact_auto_collect_setting', false)) { + $this->out('collect_contacts', true); + $this->out('collected_contact_email', $msg_headers["Return-Path"]); + $this->out('collected_contact_name', $msg_headers["From"]); } + } - if (!$prefetch) { - clear_existing_reply_details($this->session); - if ($part == 0) { - $msg_struct_current['type'] = 'text'; - $msg_struct_current['subtype'] = 'plain'; - } - $this->session->set(sprintf('reply_details_imap_%s_%s_%s', $form['imap_server_id'], $form['folder'], $form['imap_msg_uid']), - array('ts' => time(), 'msg_struct' => $msg_struct_current, 'msg_text' => ($save_reply_text ? $msg_text : ''), 'msg_headers' => $msg_headers)); + if (!$prefetch) { + clear_existing_reply_details($this->session); + if ($part == 0) { + $msg_struct_current['type'] = 'text'; + $msg_struct_current['subtype'] = 'plain'; } + $this->session->set(sprintf('reply_details_imap_%s_%s_%s', $form['imap_server_id'], $form['folder'], $form['imap_msg_uid']), + array('ts' => time(), 'msg_struct' => $msg_struct_current, 'msg_text' => ($save_reply_text ? $msg_text : ''), 'msg_headers' => $msg_headers)); } } } @@ -2054,10 +1884,9 @@ public function process() { $imap_msg_uid = $this->request->get['imap_msg_uid']; $folder = $this->request->get['imap_folder']; if ($imap_server_id && $imap_msg_uid && $folder) { - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); - $imap = Hm_IMAP_List::connect($imap_server_id, $cache); - if ($imap->select_mailbox(hex2bin($folder))) { - $msg_source = $imap->get_message_content($imap_msg_uid, 0, false); + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { + $msg_source = $mailbox->get_message_content(hex2bin($folder), $imap_msg_uid); $this->out('msg_source', $msg_source); } } diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php new file mode 100644 index 0000000000..71e74f41f3 --- /dev/null +++ b/modules/imap/hm-ews.php @@ -0,0 +1,19 @@ +load_cache($cache, 'array'); } @@ -58,6 +54,15 @@ public static function get_cache($hm_cache, $id) { $res = $hm_cache->get('imap'.$id); return $res; } + + public static function get_connected_mailbox($id, $hm_cache = null) { + if ($hm_cache) { + $cache = self::get_cache($hm_cache, $id); + } else { + $cache = false; + } + return self::connect($id, $cache); + } } /* for testing */ diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php new file mode 100644 index 0000000000..a4302bb783 --- /dev/null +++ b/modules/imap/hm-mailbox.php @@ -0,0 +1,339 @@ +connection = new Hm_JMAP(); + $this->type = TYPE_JMAP; + } + elseif (array_key_exists('type', $server) && $server['type'] == 'ews') { + $this->connection = new Hm_EWS(); + $this->type = TYPE_EWS; + } + else { + $this->connection = new Hm_IMAP(); + $this->type = TYPE_IMAP; + } + return $this->connection->connect($config); + } + + public function is_imap() { + return $this->type !== TYPE_EWS; + } + + public function server_type() { + switch ($this->type) { + case TYPE_IMAP: + return 'IMAP'; + case TYPE_JMAP: + return 'JMAP'; + case TYPE_EWS: + return 'EWS'; + } + } + + public function authed() { + if ($this->is_imap()) { + return $this->connection->get_state() == 'authenticated' || $this->connection->get_state() == 'selected'; + } else { + // TODO: EWS + } + } + + public function get_folder_status($folder) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->get_mailbox_status($folder); + } else { + // TODO: EWS + } + } + + public function create_folder($folder) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->create_mailbox($folder); + } else { + // TODO: EWS + } + } + + public function get_folder_state() { + if ($this->is_imap()) { + return $this->connection->folder_state; + } else { + // TODO: check EWS + return true; + } + } + + public function get_selected_folder() { + if ($this->is_imap()) { + return $this->connection->selected_mailbox; + } else { + // TODO: EWS + } + } + + public function get_special_use_mailboxes($folder) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->get_special_use_mailboxes($folder); + } else { + // TODO: EWS + } + } + + /** + * Get messages in a folder applying filters, sorting and pagination + * @return array - [total results found, results for a single page] + */ + public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, $limit=0, $keyword=false, $trusted_senders=[]) { + if ($this->is_imap()) { + return $this->connection->get_mailbox_page($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); + } else { + // TODO: EWS + } + } + + public function get_message_headers($msg_id) { + return $this->connection->get_message_headers($msg_id); + } + + public function get_message_content($folder, $msg_id) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + if (! $this->connection->select_mailbox($folder)) { + return; + } + return $this->connection->get_message_content($msg_id, 0); + } else { + // TODO: EWS + } + } + + public function get_structured_message($folder, $msg_id, $part, $text_only) { + if ($this->is_imap()) { + if (! $this->connection->select_mailbox($folder)) { + return; + } + $msg_struct = $this->connection->get_message_structure($msg_id); + if ($part !== false) { + if ($part == 0) { + $max = 500000; + } + else { + $max = false; + } + $struct = $this->connection->search_bodystructure($msg_struct, array('imap_part_number' => $part)); + $msg_struct_current = array_shift($struct); + $msg_text = $this->connection->get_message_content($msg_id, $part, $max, $msg_struct_current); + } + else { + if (! $text_only) { + list($part, $msg_text) = $this->connection->get_first_message_part($msg_id, 'text', 'html', $msg_struct); + if (!$part) { + list($part, $msg_text) = $this->connection->get_first_message_part($msg_id, 'text', false, $msg_struct); + } + } + else { + list($part, $msg_text) = $this->connection->get_first_message_part($msg_id, 'text', false, $msg_struct); + } + $struct = $this->connection->search_bodystructure($msg_struct, array('imap_part_number' => $part)); + $msg_struct_current = array_shift($struct); + if (! trim($msg_text)) { + if (is_array($msg_struct_current) && array_key_exists('subtype', $msg_struct_current)) { + if ($msg_struct_current['subtype'] == 'plain') { + $subtype = 'html'; + } + else { + $subtype = 'plain'; + } + list($part, $msg_text) = $this->connection->get_first_message_part($msg_id, 'text', $subtype, $msg_struct); + $struct = $this->connection->search_bodystructure($msg_struct, array('imap_part_number' => $part)); + $msg_struct_current = array_shift($struct); + } + } + } + if (isset($msg_struct_current['subtype']) && mb_strtolower($msg_struct_current['subtype'] == 'html')) { + $msg_text = add_attached_images($msg_text, $msg_id, $msg_struct, $this->connection); + } + return [$msg_struct, $msg_struct_current, $msg_text, $part]; + } else { + // TODO: EWS + } + } + + public function store_message($folder, $msg) { + if (! $this->authed()) { + return false; + } + if ($this->is_imap()) { + if ($this->connection->append_start($folder, mb_strlen($msg), true)) { + $this->connection->append_feed($msg."\r\n"); + if (! $this->connection->append_end()) { + return true; + } + } + } else { + // TODO: EWS + } + return false; + } + + public function delete_message($folder, $msg_id, $trash_folder) { + if ($this->is_imap()) { + if (! $this->connection->select_mailbox($folder)) { + return false; + } + if ($trash_folder && $trash_folder != $folder) { + if ($this->connection->message_action('MOVE', [$msg_id], $trash_folder)) { + return true; + } + } + else { + if ($this->connection->message_action('DELETE', array($msg_id))) { + $this->connection->message_action('EXPUNGE', array($msg_id)); + return true; + } + } + } else { + // TODO: EWS + } + return false; + } + + public function message_action($folder, $action, $uids, $mailbox=false, $keyword=false) { + if ($this->is_imap()) { + $this->connection->select_mailbox($folder); + } + return $this->connection->message_action($action, $uids, $mailbox, $keyword); + } + + public function stream_message_part($msg_id, $part_id, $start_cb) { + if ($this->is_imap()) { + if (! $this->connection->select_mailbox($folder)) { + return; + } + $msg_struct = $this->connection->get_message_structure($msg_id); + $struct = $this->connection->search_bodystructure($msg_struct, array('imap_part_number' => $part_id)); + if (! empty($struct)) { + $part_struct = array_shift($struct); + $encoding = false; + if (array_key_exists('encoding', $part_struct)) { + $encoding = trim(mb_strtolower($part_struct['encoding'])); + } + $stream_size = $this->connection->start_message_stream($msg_id, $part_id); + if ($stream_size > 0) { + $part_name = get_imap_part_name($part_struct, $msg_id, $part_id); + $charset = ''; + if (array_key_exists('attributes', $part_struct)) { + if (is_array($part_struct['attributes']) && array_key_exists('charset', $part_struct['attributes'])) { + $charset = '; charset='.$part_struct['attributes']['charset']; + } + } + $start_cb($part_struct['type'] . '/' . $part_struct['subtype'] . $charset, $part_name); + $output_line = ''; + while($line = $this->connection->read_stream_line()) { + if ($encoding == 'quoted-printable') { + $line = quoted_printable_decode($line); + } + elseif ($encoding == 'base64') { + $line = base64_decode($line); + } + echo $output_line; + $output_line = $line; + } + if ($part_struct['type'] == 'text') { + $output_line = preg_replace("/\)(\r\n)$/m", '$1', $output_line); + } + echo $output_line; + } + } + } else { + // TODO: EWS + } + } + + public function remove_attachment($folder, $msg_id, $part_id) { + if ($this->is_imap()) { + if (! $this->connection->select_mailbox($folder)) { + return false; + } + $msg = $this->connection->get_message_content($msg_id, 0, false, false); + if ($msg) { + $attachment_id = get_attachment_id_for_mail_parser($this->connection, $msg_id, $part_id); + if ($attachment_id !== false) { + $msg = remove_attachment($attachment_id, $msg); + if ($this->connection->append_start($folder, mb_strlen($msg))) { + $this->connection->append_feed($msg."\r\n"); + if ($this->connection->append_end()) { + if ($this->connection->message_action('DELETE', array($uid))) { + $this->connection->message_action('EXPUNGE', array($uid)); + return true; + } + } + } + } + } + } else { + // TODO: EWS + } + } + + public function get_quota($folder, $root = false) { + if ($this->is_imap()) { + if ($root) { + return $this->connection->get_quota_root($folder); + } else { + return $this->connection->get_quota($folder); + } + } else { + // TODO: EWS + } + } + + public function get_debug() { + if ($this->is_imap()) { + return $this->connection->show_debug(true, true, true); + } else { + // TODO: EWS + } + } + + public function get_state() { + return $this->connection->get_state(); + } + + public function get_capability() { + return $this->connection->get_capability(); + } + + public function set_read_only($read_only) { + $this->connection->read_only = $read_only; + } +} From 26a1d9de1821e44e8b0d133af4e5bf459660761a Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Thu, 10 Oct 2024 11:14:13 +0300 Subject: [PATCH 02/35] imap bridge update throughout the modules and bug fixes --- modules/advanced_search/modules.php | 20 ++- modules/core/handler_modules.php | 4 +- modules/imap/functions.php | 123 ++++++++---------- modules/imap/handler_modules.php | 22 ++-- modules/imap/hm-imap.php | 1 + modules/imap/hm-mailbox.php | 195 +++++++++++++++++++++++----- modules/imap_folders/modules.php | 54 ++++---- modules/nux/modules.php | 4 +- modules/sievefilters/modules.php | 18 +-- modules/smtp/modules.php | 76 +++++------ 10 files changed, 305 insertions(+), 212 deletions(-) diff --git a/modules/advanced_search/modules.php b/modules/advanced_search/modules.php index dedfcd322b..eb976e7944 100644 --- a/modules/advanced_search/modules.php +++ b/modules/advanced_search/modules.php @@ -73,16 +73,12 @@ public function process() { $charset = $this->request->post['charset']; } - $cache = Hm_IMAP_List::get_cache($this->cache, $this->imap_id); - $imap = Hm_IMAP_List::connect($this->imap_id, $cache); - if (!imap_authed($imap)) { - return; - } - if (!$imap->select_mailbox($this->folder)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($this->imap_id, $this->cache); + if (! $mailbox || ! $mailbox->authed()) { return; } if ($charset) { - $imap->search_charset = $charset; + $mailbox->set_search_charset($charset); } $params = array( array('SENTBEFORE', date('j-M-Y', strtotime($form['adv_end']))), @@ -93,23 +89,23 @@ public function process() { $params[] = array($target, $term); } } - $this->out('imap_search_results', $this->imap_search($flags, $imap, $params, $limit)); - $this->out('folder_status', $imap->folder_state); + $this->out('imap_search_results', $this->imap_search($flags, $mailbox, $params, $limit)); + $this->out('folder_status', $mailbox->get_folder_state()); $this->out('imap_server_ids', array($this->imap_id)); } - private function imap_search($flags, $imap, $params, $limit) { + private function imap_search($flags, $mailbox, $params, $limit) { $msg_list = array(); $exclude_deleted = true; if (in_array('deleted', $flags, true)) { $exclude_deleted = false; } - $msgs = $imap->search($flags, false, $params, array(), $exclude_deleted); + $msgs = $mailbox->search($this->folder, $flags, false, $params, array(), $exclude_deleted); if (!$msgs) { return $msg_list; } $server_details = Hm_IMAP_List::dump($this->imap_id); - foreach ($imap->get_message_list($msgs) as $msg) { + foreach ($mailbox->get_message_list($this->folder, $msgs) as $msg) { if (array_key_exists('content-type', $msg) && mb_stristr($msg['content-type'], 'multipart/mixed')) { $msg['flags'] .= ' \Attachment'; } diff --git a/modules/core/handler_modules.php b/modules/core/handler_modules.php index c0e4aa2031..bb46a4132e 100644 --- a/modules/core/handler_modules.php +++ b/modules/core/handler_modules.php @@ -60,8 +60,8 @@ public function process() { $current['pass'] = $form['password']; unset($current['nopass']); Hm_IMAP_List::edit($server['id'], $current); - $imap = Hm_IMAP_List::connect($server['id'], false); - if ($imap->get_state() == 'authenticated') { + $mailbox = Hm_IMAP_List::connect($server['id'], false); + if ($mailbox && $mailbox->authed()) { Hm_Msgs::add('Password Updated'); $this->out('connect_status', true); } diff --git a/modules/imap/functions.php b/modules/imap/functions.php index ed2b80d00a..c6812cf7f6 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -727,13 +727,12 @@ function merge_imap_search_results($ids, $search_type, $session, $hm_cache, $fol $sent_results = array(); $status = array(); foreach($ids as $index => $id) { - $cache = Hm_IMAP_List::get_cache($hm_cache, $id); - $imap = Hm_IMAP_List::connect($id, $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($id, $hm_cache); + if ($mailbox && $mailbox->authed()) { $server_details = Hm_IMAP_List::dump($id); $folder = $folders[$index]; if ($sent) { - $sent_folder = $imap->get_special_use_mailboxes('sent'); + $sent_folder = $mailbox->get_special_use_mailboxes('sent'); if (array_key_exists('sent', $sent_folder)) { list($sent_status, $sent_results) = merge_imap_search_results($ids, $search_type, $session, $hm_cache, array($sent_folder['sent']), $limit, $terms, false); $status = array_merge($status, $sent_status); @@ -742,44 +741,42 @@ function merge_imap_search_results($ids, $search_type, $session, $hm_cache, $fol continue; } } - if ($imap->select_mailbox($folder)) { - $status['imap_'.$id.'_'.bin2hex($folder)] = $imap->folder_state; - if (!empty($terms)) { - foreach ($terms as $term) { - if (preg_match('/(?:[^\x00-\x7F])/', $term[1]) === 1) { - $imap->search_charset = 'UTF-8'; - break; - } - } - if ($sent) { - $msgs = $imap->search($search_type, false, $terms, array(), true, false, true); - } - else { - $msgs = $imap->search($search_type, false, $terms); + if (!empty($terms)) { + foreach ($terms as $term) { + if (preg_match('/(?:[^\x00-\x7F])/', $term[1]) === 1) { + $mailbox->set_search_charset('UTF-8'); + break; } } + if ($sent) { + $msgs = $mailbox->search($folder, $search_type, false, $terms, array(), true, false, true); + } else { - $msgs = $imap->search($search_type); + $msgs = $mailbox->search($folder, $search_type, false, $terms); + } + } + else { + $msgs = $mailbox->search($folder, $search_type); + } + if ($msgs) { + if ($limit) { + rsort($msgs); + $msgs = array_slice($msgs, 0, $limit); } - if ($msgs) { - if ($limit) { - rsort($msgs); - $msgs = array_slice($msgs, 0, $limit); + foreach ($mailbox->get_message_list($folder, $msgs) as $msg) { + if (array_key_exists('content-type', $msg) && mb_stristr($msg['content-type'], 'multipart/mixed')) { + $msg['flags'] .= ' \Attachment'; } - foreach ($imap->get_message_list($msgs) as $msg) { - if (array_key_exists('content-type', $msg) && mb_stristr($msg['content-type'], 'multipart/mixed')) { - $msg['flags'] .= ' \Attachment'; - } - if (mb_stristr($msg['flags'], 'deleted')) { - continue; - } - $msg['server_id'] = $id; - $msg['folder'] = bin2hex($folder); - $msg['server_name'] = $server_details['name']; - $msg_list[] = $msg; + if (mb_stristr($msg['flags'], 'deleted')) { + continue; } + $msg['server_id'] = $id; + $msg['folder'] = bin2hex($folder); + $msg['server_name'] = $server_details['name']; + $msg_list[] = $msg; } } + $status['imap_'.$id.'_'.bin2hex($folder)] = $mailbox->get_folder_state(); } else { $connection_failed = true; @@ -861,23 +858,22 @@ function imap_move_same_server($ids, $action, $hm_cache, $dest_path, $screen_ema $moved = array(); $keys = array_keys($ids); $server_id = array_pop($keys); - $cache = Hm_IMAP_List::get_cache($hm_cache, $server_id); - $imap = Hm_IMAP_List::connect($server_id, $cache); - foreach ($ids[$server_id] as $folder => $msgs) { - if (imap_authed($imap) && $imap->select_mailbox(hex2bin($folder))) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $hm_cache); + if ($mailbox && $mailbox->authed()) { + foreach ($ids[$server_id] as $folder => $msgs) { if ($screen_emails) { foreach ($msgs as $msg) { $moved[] = sprintf('imap_%s_%s_%s', $server_id, $msg, $folder); - $email = current(array_column(process_address_fld($imap->get_message_headers($msg)['From']), "email")); - $uids = $imap->search('ALL', false, array(array('FROM', $email))); + $email = current(array_column(process_address_fld($mailbox->get_message_headers(hex2bin($folder), $msg)['From']), "email")); + $uids = $mailbox->search(hex2bin($folder), 'ALL', false, array(array('FROM', $email))); foreach ($uids as $uid) { - if ($imap->message_action(mb_strtoupper($action), $uid, hex2bin($dest_path[2]))) { + if ($mailbox->message_action(hex2bin($folder), mb_strtoupper($action), $uid, hex2bin($dest_path[2]))) { $moved[] = sprintf('imap_%s_%s_%s', $server_id, $uid, $folder); } } } } else { - if ($imap->message_action(mb_strtoupper($action), $msgs, hex2bin($dest_path[2]))) { + if ($mailbox->message_action(hex2bin($folder), mb_strtoupper($action), $msgs, hex2bin($dest_path[2]))) { foreach ($msgs as $msg) { $moved[] = sprintf('imap_%s_%s_%s', $server_id, $msg, $folder); } @@ -901,16 +897,14 @@ function imap_move_same_server($ids, $action, $hm_cache, $dest_path, $screen_ema if (!hm_exists('imap_move_different_server')) { function imap_move_different_server($ids, $action, $dest_path, $hm_cache) { $moved = array(); - $cache = Hm_IMAP_List::get_cache($hm_cache, $dest_path[1]); - $dest_imap = Hm_IMAP_List::connect($dest_path[1], $cache); - if ($dest_imap) { + $dest_mailbox = Hm_IMAP_List::get_connected_mailbox($dest_path[1], $hm_cache); + if ($dest_mailbox && $dest_mailbox->authed()) { foreach ($ids as $server_id => $folders) { - $cache = Hm_IMAP_List::get_cache($hm_cache, $server_id); - $imap = Hm_IMAP_List::connect($server_id, $cache); - foreach ($folders as $folder => $msg_ids) { - if (imap_authed($imap) && $imap->select_mailbox(hex2bin($folder))) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $hm_cache); + if ($mailbox && $mailbox->authed()) { + foreach ($folders as $folder => $msg_ids) { foreach ($msg_ids as $msg_id) { - $detail = $imap->get_message_list(array($msg_id)); + $detail = $mailbox->get_message_list(hex2bin($folder), array($msg_id)); if (array_key_exists($msg_id, $detail)) { if (mb_stristr($detail[$msg_id]['flags'], 'seen')) { $seen = true; @@ -919,23 +913,20 @@ function imap_move_different_server($ids, $action, $dest_path, $hm_cache) { $seen = false; } } - $msg = $imap->get_message_content($msg_id, 0); + $msg = $mailbox->get_message_content(hex2bin($folder), $msg_id); $msg = str_replace("\r\n", "\n", $msg); $msg = str_replace("\n", "\r\n", $msg); $msg = rtrim($msg)."\r\n"; if (!$seen) { - $imap->message_action('UNREAD', array($msg_id)); + $mailbox->message_action(hex2bin($folder), 'UNREAD', array($msg_id)); } - if ($dest_imap->append_start(hex2bin($dest_path[2]), mb_strlen($msg), $seen)) { - $dest_imap->append_feed($msg."\r\n"); - if ($dest_imap->append_end()) { - if ($action == 'move') { - if ($imap->message_action('DELETE', array($msg_id))) { - $imap->message_action('EXPUNGE', array($msg_id)); - } + if ($dest_mailbox->store_message(hex2bin($dest_path[2]), $msg, $seen)) { + if ($action == 'move') { + if ($mailbox->message_action(hex2bin($folder), 'DELETE', array($msg_id))) { + $mailbox->message_action(hex2bin($folder), 'EXPUNGE', array($msg_id)); } - $moved[] = sprintf('imap_%s_%s_%s', $server_id, $msg_id, $folder); } + $moved[] = sprintf('imap_%s_%s_%s', $server_id, $msg_id, $folder); } } } @@ -1217,14 +1208,14 @@ function decode_folder_str($folder) { * @subpackage imap/functions */ if (!hm_exists('prep_folder_name')) { -function prep_folder_name($imap, $folder, $decode_folder=false, $parent=false) { +function prep_folder_name($mailbox, $folder, $decode_folder=false, $parent=false) { if ($parent && $decode_folder) { $parent = decode_folder_str($parent); } if ($decode_folder) { $folder = decode_folder_str($folder); } - $ns = get_personal_ns($imap); + $ns = get_personal_ns($mailbox); if (!$folder) { return false; } @@ -1244,8 +1235,8 @@ function prep_folder_name($imap, $folder, $decode_folder=false, $parent=false) { * @subpackage imap/functions */ if (!hm_exists('get_personal_ns')) { -function get_personal_ns($imap) { - $namespaces = $imap->get_namespaces(); +function get_personal_ns($mailbox) { + $namespaces = $mailbox->get_namespaces(); foreach ($namespaces as $ns) { if ($ns['class'] == 'personal') { return $ns; @@ -1561,9 +1552,9 @@ function connect_to_imap_server($address, $name, $port, $user, $pass, $tls, $ima } } - $imap = Hm_IMAP_List::connect($imap_server_id, false); + $mailbox = Hm_IMAP_List::connect($imap_server_id, false); - if (imap_authed($imap)) { + if ($mailbox->authed()) { return $imap_server_id; }else { Hm_IMAP_List::del($imap_server_id); diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index 5eb4ca8cc1..d69e80cdcf 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -353,7 +353,7 @@ public function process() { $uid = null; $mailbox_page = $mailbox->get_messages($sent_folder, 'ARRIVAL', true, 'ALL', 0, 10); foreach ($mailbox_page[1] as $mail) { - $msg_header = $mailbox->get_message_headers($mail['uid']); + $msg_header = $mailbox->get_message_headers($sent_folder, $mail['uid']); if ($msg_header['Message-Id'] === $mime->get_headers()['Message-Id']) { $uid = $mail['uid']; break; @@ -677,12 +677,12 @@ public function process() { $this->out('with_input', $with_subscription); return; } - if (imap_authed($imap)) { + if ($mailbox && $mailbox->authed()) { $only_subscribed = $this->user_config->get('only_subscribed_folders_setting', false); if ($with_subscription) { $only_subscribed = false; } - $msgs = $imap->get_folder_list_by_level(hex2bin($folder), $only_subscribed, $with_subscription); + $msgs = $mailbox->get_subfolders(hex2bin($folder), $only_subscribed, $with_subscription); if (isset($msgs[$folder])) { unset($msgs[$folder]); } @@ -693,7 +693,7 @@ public function process() { $this->out('with_input', $with_subscription); } else { - Hm_Msgs::add(sprintf('ERRCould not authenticate to the selected %s server (%s)', $imap->server_type, $this->user_config->get('imap_servers')[$form['imap_server_id']]['user'])); + Hm_Msgs::add(sprintf('ERRCould not authenticate to the selected %s server (%s)', $mailbox->server_type(), $this->user_config->get('imap_servers')[$form['imap_server_id']]['user'])); } } } @@ -758,7 +758,7 @@ public function process() { $folder['detail']['exists'] = $total; $this->out('imap_folder_detail', array_merge($folder, array('offset' => $offset, 'limit' => $limit))); } - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_state())); } $this->out('imap_mailbox_page', $msgs); $this->out('list_page', $list_page); @@ -809,7 +809,7 @@ public function process() { if ($mailbox->delete_message(hex2bin($form['folder']), $form['imap_msg_uid'], $trash_folder)) { $del_result = true; } - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_state())); } if (!$del_result) { Hm_Msgs::add('ERRAn error occurred trying to delete this message'); @@ -913,7 +913,7 @@ public function process() { if ($mailbox->message_action(hex2bin($form['folder']), $cmd, array($form['imap_msg_uid']))) { $flag_result = true; } - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_state())); } if (!$flag_result) { Hm_Msgs::add('ERRAn error occurred trying to flag this message'); @@ -1023,7 +1023,7 @@ public function process() { } $ret = $mailbox->get_messages($folder, 'DATE', false, 'ALL'); foreach ($ret[1] as $msg) { - $msg_headers = $mailbox->get_message_headers($msg['uid']); + $msg_headers = $mailbox->get_message_headers($folder, $msg['uid']); if (isset($msg_headers['X-Snoozed'])) { try { $snooze_headers = parse_snooze_header($msg_headers['X-Snoozed']); @@ -1389,7 +1389,7 @@ public function process() { $cache = array(); foreach ($servers as $index => $server) { if (isset($server['object']) && is_object($server['object'])) { - if ($server['object']->use_cache) { + if ($server['object']->use_cache()) { $cache[$index] = $server['object']->dump_cache('array'); } } @@ -1834,8 +1834,8 @@ public function process() { if ($part == 0 || (isset($msg_struct_current['type']) && mb_strtolower($msg_struct_current['type'] == 'text'))) { $save_reply_text = true; } - $msg_headers = $mailbox->get_message_headers($form['imap_msg_uid']); - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_status())); + $msg_headers = $mailbox->get_message_headers(hex2bin($form['folder']), $form['imap_msg_uid']); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_state())); $this->out('msg_struct', $msg_struct); $this->out('list_headers', get_list_headers($msg_headers)); $this->out('msg_headers', $msg_headers); diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index a2947c6fb7..d4c9cf742a 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -12,6 +12,7 @@ require_once('hm-imap-bodystructure.php'); require_once('hm-jmap.php'); require_once('hm-ews.php'); +require_once('hm-mailbox.php'); /** * IMAP connection manager diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index a4302bb783..4bdadeb07e 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -20,30 +20,30 @@ class Hm_Mailbox { public function connect(array $config) { if (array_key_exists('type', $config) && $config['type'] == 'jmap') { $this->connection = new Hm_JMAP(); - $this->type = TYPE_JMAP; + $this->type = self::TYPE_JMAP; } - elseif (array_key_exists('type', $server) && $server['type'] == 'ews') { + elseif (array_key_exists('type', $config) && $config['type'] == 'ews') { $this->connection = new Hm_EWS(); - $this->type = TYPE_EWS; + $this->type = self::TYPE_EWS; } else { $this->connection = new Hm_IMAP(); - $this->type = TYPE_IMAP; + $this->type = self::TYPE_IMAP; } return $this->connection->connect($config); } public function is_imap() { - return $this->type !== TYPE_EWS; + return $this->type !== self::TYPE_EWS; } public function server_type() { switch ($this->type) { - case TYPE_IMAP: + case self::TYPE_IMAP: return 'IMAP'; - case TYPE_JMAP: + case self::TYPE_JMAP: return 'JMAP'; - case TYPE_EWS: + case self::TYPE_EWS: return 'EWS'; } } @@ -78,6 +78,61 @@ public function create_folder($folder) { } } + public function rename_folder($folder, $new_name) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->rename_mailbox($folder, $new_name); + } else { + // TODO: EWS + } + } + + public function delete_folder($folder) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->delete_mailbox($folder); + } else { + // TODO: EWS + } + } + + public function folder_subscription($folder, $action) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->mailbox_subscription($folder, $action); + } else { + // TODO: EWS + } + } + + public function get_folders($only_subscribed = false) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->get_mailbox_list($only_subscribed); + } else { + // TODO: EWS + } + } + + public function get_subfolders($folder, $only_subscribed = false, $with_input = false) { + if (! $this->authed()) { + return; + } + if ($this->is_imap()) { + return $this->connection->get_folder_list_by_level($folder, $only_subscribed, $with_input); + } else { + // TODO: EWS + } + } + public function get_folder_state() { if ($this->is_imap()) { return $this->connection->folder_state; @@ -95,7 +150,7 @@ public function get_selected_folder() { } } - public function get_special_use_mailboxes($folder) { + public function get_special_use_mailboxes($folder = false) { if (! $this->authed()) { return; } @@ -118,29 +173,37 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, } } - public function get_message_headers($msg_id) { - return $this->connection->get_message_headers($msg_id); + public function get_message_headers($folder, $msg_id) { + if (! $this->select_folder($folder)) { + return; + } + if ($this->is_imap()) { + return $this->connection->get_message_headers($msg_id); + } else { + // TODO: EWS + } + } - public function get_message_content($folder, $msg_id) { + public function get_message_content($folder, $msg_id, $part = 0) { if (! $this->authed()) { return; } + if (! $this->select_folder($folder)) { + return; + } if ($this->is_imap()) { - if (! $this->connection->select_mailbox($folder)) { - return; - } - return $this->connection->get_message_content($msg_id, 0); + return $this->connection->get_message_content($msg_id, $part); } else { // TODO: EWS } } public function get_structured_message($folder, $msg_id, $part, $text_only) { + if (! $this->select_folder($folder)) { + return; + } if ($this->is_imap()) { - if (! $this->connection->select_mailbox($folder)) { - return; - } $msg_struct = $this->connection->get_message_structure($msg_id); if ($part !== false) { if ($part == 0) { @@ -188,12 +251,12 @@ public function get_structured_message($folder, $msg_id, $part, $text_only) { } } - public function store_message($folder, $msg) { + public function store_message($folder, $msg, $seen = true, $draft = false) { if (! $this->authed()) { return false; } if ($this->is_imap()) { - if ($this->connection->append_start($folder, mb_strlen($msg), true)) { + if ($this->connection->append_start($folder, mb_strlen($msg), $seen, $draft)) { $this->connection->append_feed($msg."\r\n"); if (! $this->connection->append_end()) { return true; @@ -206,10 +269,10 @@ public function store_message($folder, $msg) { } public function delete_message($folder, $msg_id, $trash_folder) { + if (! $this->select_folder($folder)) { + return; + } if ($this->is_imap()) { - if (! $this->connection->select_mailbox($folder)) { - return false; - } if ($trash_folder && $trash_folder != $folder) { if ($this->connection->message_action('MOVE', [$msg_id], $trash_folder)) { return true; @@ -228,17 +291,17 @@ public function delete_message($folder, $msg_id, $trash_folder) { } public function message_action($folder, $action, $uids, $mailbox=false, $keyword=false) { - if ($this->is_imap()) { - $this->connection->select_mailbox($folder); + if (! $this->select_folder($folder)) { + return; } return $this->connection->message_action($action, $uids, $mailbox, $keyword); } - public function stream_message_part($msg_id, $part_id, $start_cb) { + public function stream_message_part($folder, $msg_id, $part_id, $start_cb) { + if (! $this->select_folder($folder)) { + return; + } if ($this->is_imap()) { - if (! $this->connection->select_mailbox($folder)) { - return; - } $msg_struct = $this->connection->get_message_structure($msg_id); $struct = $this->connection->search_bodystructure($msg_struct, array('imap_part_number' => $part_id)); if (! empty($struct)) { @@ -280,10 +343,10 @@ public function stream_message_part($msg_id, $part_id, $start_cb) { } public function remove_attachment($folder, $msg_id, $part_id) { + if (! $this->select_folder($folder)) { + return; + } if ($this->is_imap()) { - if (! $this->connection->select_mailbox($folder)) { - return false; - } $msg = $this->connection->get_message_content($msg_id, 0, false, false); if ($msg) { $attachment_id = get_attachment_id_for_mail_parser($this->connection, $msg_id, $part_id); @@ -325,6 +388,24 @@ public function get_debug() { } } + public function use_cache() { + if ($this->is_imap()) { + return $this->connection->use_cache; + } else { + // TODO: check EWS caching + return false; + } + } + + public function dump_cache($type = 'string') { + if ($this->is_imap()) { + return $this->connection->dump_cache($type); + } else { + // TODO: check EWS caching + return; + } + } + public function get_state() { return $this->connection->get_state(); } @@ -333,7 +414,53 @@ public function get_capability() { return $this->connection->get_capability(); } + public function get_namespaces() { + return $this->connection->get_namespaces(); + } + public function set_read_only($read_only) { - $this->connection->read_only = $read_only; + if ($this->is_imap()) { + $this->connection->read_only = $read_only; + } + } + + public function set_search_charset($charset) { + if ($this->is_imap()) { + $this->connection->search_charset = $charset; + } + } + + public function search($folder, $target='ALL', $uids=false, $terms=array(), $esearch=array(), $exclude_deleted=true, $exclude_auto_bcc=true, $only_auto_bcc=false) { + if (! $this->select_folder($folder)) { + return; + } + if ($this->is_imap()) { + return $this->connection->search($target, $uids, $terms, $esearch, $exclude_deleted, $exclude_auto_bcc, $only_auto_bcc); + } else { + // TODO: EWS + } + } + + public function get_message_list($folder, $msg_ids) { + if (! $this->select_folder($folder)) { + return; + } + if ($this->is_imap()) { + return $this->connection->get_message_list($msg_ids); + } else { + // TODO: EWS + } + } + + protected function select_folder($folder) { + if ($this->is_imap()) { + if (isset($this->connection->selected_mailbox['name']) && $this->connection->selected_mailbox['name'] == $folder) { + return true; + } + if (! $this->connection->select_mailbox($folder)) { + return false; + } + } + return true; } } diff --git a/modules/imap_folders/modules.php b/modules/imap_folders/modules.php index df6985abac..275ecc1779 100644 --- a/modules/imap_folders/modules.php +++ b/modules/imap_folders/modules.php @@ -98,15 +98,13 @@ public function process() { if (!$success || !in_array($form['special_folder_type'], array('sent', 'draft', 'trash', 'archive', 'junk'), true)) { return; } - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - - if (!is_object($imap) || $imap->get_state() != 'authenticated') { + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if (!is_object($mailbox) || ! $mailbox->authed()) { Hm_Msgs::add('ERRUnable to connect to the selected IMAP server'); return; } - $new_folder = prep_folder_name($imap, $form['folder'], true); - if (!$new_folder || !$imap->select_mailbox($new_folder)) { + $new_folder = prep_folder_name($mailbox, $form['folder'], true); + if (! $new_folder || ! $mailbox->get_folder_status($new_folder)) { Hm_Msgs::add('ERRSelected folder not found'); return; } @@ -132,15 +130,13 @@ public function process() { list($success, $form) = $this->process_form(array('imap_server_id', 'imap_service_name')); if ($success) { - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - - if (!is_object($imap) || $imap->get_state() != 'authenticated') { + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if (!is_object($mailbox) || ! $mailbox->authed()) { Hm_Msgs::add('ERRUnable to connect to the selected IMAP server'); return; } $specials = $this->user_config->get('special_imap_folders', array()); - $exposed = $imap->get_special_use_mailboxes(); + $exposed = $mailbox->get_special_use_mailboxes(); if ($form['imap_service_name'] == 'gandi') { $specials[$form['imap_server_id']] = array( 'sent' => 'Sent', @@ -182,11 +178,10 @@ public function process() { if (array_key_exists('parent', $this->request->post) && trim($this->request->post['parent'])) { $parent_str = decode_folder_str($this->request->post['parent']); } - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (is_object($imap) && $imap->get_state() == 'authenticated') { - $new_folder = prep_folder_name($imap, $form['folder'], false, $parent_str); - if ($new_folder && $imap->create_mailbox($new_folder)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $new_folder = prep_folder_name($mailbox, $form['folder'], false, $parent_str); + if ($new_folder && $mailbox->create_folder($new_folder)) { Hm_Msgs::add('Folder created'); $this->cache->del('imap_folders_imap_'.$form['imap_server_id'].'_'); $this->out('imap_folders_success', true); @@ -206,16 +201,15 @@ class Hm_Handler_process_folder_rename extends Hm_Handler_Module { public function process() { list($success, $form) = $this->process_form(array('imap_server_id', 'folder', 'new_folder')); if ($success) { - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); $parent_str = false; if (array_key_exists('parent', $this->request->post)) { $parent_str = $this->request->post['parent']; } - if (is_object($imap) && $imap->get_state() == 'authenticated') { - $old_folder = prep_folder_name($imap, $form['folder'], true); - $new_folder = prep_folder_name($imap, $form['new_folder'], false, $parent_str); - if ($new_folder && $old_folder && $imap->rename_mailbox($old_folder, $new_folder)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $old_folder = prep_folder_name($mailbox, $form['folder'], true); + $new_folder = prep_folder_name($mailbox, $form['new_folder'], false, $parent_str); + if ($new_folder && $old_folder && $mailbox->rename_mailbox($old_folder, $new_folder)) { if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER)) { $imap_servers = $this->user_config->get('imap_servers'); $imap_account = $imap_servers[$form['imap_server_id']]; @@ -271,17 +265,16 @@ class Hm_Handler_process_folder_delete extends Hm_Handler_Module { public function process() { list($success, $form) = $this->process_form(array('imap_server_id', 'folder')); if ($success) { - $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); - $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache); - if (is_object($imap) && $imap->get_state() == 'authenticated') { - $del_folder = prep_folder_name($imap, $form['folder'], true); + $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $del_folder = prep_folder_name($mailbox, $form['folder'], true); if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER)) { if (is_mailbox_linked_with_filters($del_folder, $form['imap_server_id'], $this)) { Hm_Msgs::add('ERRThis folder can\'t be deleted because it is used in a filter.'); return; } } - if ($del_folder && $imap->delete_mailbox($del_folder)) { + if ($del_folder && $mailbox->delete_folder($del_folder)) { Hm_Msgs::add('Folder deleted'); $this->cache->del('imap_folders_imap_'.$form['imap_server_id'].'_'); $this->out('imap_folders_success', true); @@ -344,11 +337,10 @@ public function process() { list($success, $form) = $this->process_form(array('folder', 'subscription_state')); if ($success) { $imap_server_id = $this->request->get['imap_server_id']; - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); - $imap = Hm_IMAP_List::connect($imap_server_id, $cache); - if (imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); + if ($mailbox && $mailbox->authed()) { $folder = hex2bin($form['folder']); - $success = $imap->mailbox_subscription($folder, $form['subscription_state']); + $success = $mailbox->folder_subscription($folder, $form['subscription_state']); if ($success) { Hm_Msgs::add(sprintf('%s to %s', $form['subscription_state']? 'Subscribed': 'Unsubscribed', $folder)); $this->cache->del('imap_folders_imap_'.$imap_server_id.'_'); diff --git a/modules/nux/modules.php b/modules/nux/modules.php index 936e7a73c0..36bd97cdc7 100644 --- a/modules/nux/modules.php +++ b/modules/nux/modules.php @@ -179,8 +179,8 @@ public function process() { if (! can_save_last_added_server('Hm_IMAP_List', $form['nux_email'])) { return; } - $imap = Hm_IMAP_List::connect($new_id, false); - if ($imap && $imap->get_state() == 'authenticated') { + $mailbox = Hm_IMAP_List::connect($new_id, false); + if ($mailbox && $mailbox->authed()) { if (isset($details['smtp'])) { Hm_SMTP_List::add(array( 'name' => $details['name'], diff --git a/modules/sievefilters/modules.php b/modules/sievefilters/modules.php index dd34d04c06..a42628d152 100644 --- a/modules/sievefilters/modules.php +++ b/modules/sievefilters/modules.php @@ -291,14 +291,13 @@ public function process() { $imap_server_id = $idx; } } - $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); - $imap = Hm_IMAP_List::connect($imap_server_id, $cache); - if (!imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); + if (! $mailbox || ! $mailbox->authed()) { Hm_Msgs::add('ERRIMAP Authentication Failed'); return; } $mailboxes = []; - foreach ($imap->get_mailbox_list() as $idx => $mailbox) { + foreach ($mailbox->get_folders() as $idx => $mailbox) { $mailboxes[] = $mailbox['name']; } $this->out('mailboxes', json_encode($mailboxes)); @@ -452,17 +451,12 @@ public function process() { } if (isset($this->request->post['imap_msg_uid'])) { - $imap = Hm_IMAP_List::connect($this->request->post['imap_server_id']); - - if (!imap_authed($imap)) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($this->request->post['imap_server_id'], $this->cache); + if (! $mailbox || ! $mailbox->authed()) { Hm_Msgs::add('ERRIMAP Authentication Failed'); return; } - if (!$imap->select_mailbox(hex2bin($this->request->post['folder']))) { - Hm_Msgs::add('ERRIMAP Mailbox select error'); - return; - } - $msg_header = $imap->get_message_headers($form['imap_msg_uid']); + $msg_header = $mailbox->get_message_headers(hex2bin($this->request->post['folder']), $form['imap_msg_uid']); $test_pattern = "/(?:[a-z0-9!#$%&'*+=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+=?^_`{|}~-]+)*|\"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/"; preg_match_all($test_pattern, $msg_header['From'], $email_sender); $email_sender = $email_sender[0][0]; diff --git a/modules/smtp/modules.php b/modules/smtp/modules.php index e82b7587db..7dbdef5cd4 100644 --- a/modules/smtp/modules.php +++ b/modules/smtp/modules.php @@ -56,11 +56,10 @@ public function process() { && array_key_exists('list_path', $this->request->get) && array_key_exists('uid', $this->request->get)) { $path = explode('_', $this->request->get['list_path']); - $imap = Hm_IMAP_List::connect($path[1]); - if ($imap->select_mailbox(hex2bin($path[2]))) { - $msg_struct = $imap->get_message_structure($this->request->get['uid']); - list($part, $msg_text) = $imap->get_first_message_part($this->request->get['uid'], 'text', 'plain', $msg_struct); - $msg_header = $imap->get_message_headers($this->request->get['uid']); + $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1]); + if ($mailbox && $mailbox->authed()) { + list ($msg_struct, $msg_struct_first, $msg_text, $part) = $mailbox->get_structured_message(hex2bin($path[2]), $this->request->get['uid'], false, true); + $msg_header = $mailbox->get_message_headers(hex2bin($path[2]), $this->request->get['uid']); if (!array_key_exists('From', $msg_header) || count($msg_header) == 0) { return; } @@ -78,7 +77,7 @@ public function process() { $new_attachment['size'] = $sub['size']; $new_attachment['type'] = $sub['type']; $file_path = $this->config->get('attachment_dir').DIRECTORY_SEPARATOR.$new_attachment['name']; - $content = Hm_Crypt::ciphertext($imap->get_message_content($this->request->get['uid'], $ind), Hm_Request_Key::generate()); + $content = Hm_Crypt::ciphertext($mailbox->get_message_content(hex2bin($path[2]), $this->request->get['uid'], $ind), Hm_Request_Key::generate()); file_put_contents($file_path, $content); $new_attachment['tmp_name'] = $file_path; $new_attachment['filename'] = $file_path; @@ -126,14 +125,13 @@ public function process() { if (array_key_exists('forward_as_attachment', $this->request->get)) { $path = explode('_', $this->request->get['list_path']); - $imap = Hm_IMAP_List::connect($path[1]); - if ($imap && $imap->select_mailbox(hex2bin($path[2]))) { - $msg_struct = $imap->get_message_structure($this->request->get['uid']); - $msg_header = $imap->get_message_headers($this->request->get['uid']); + $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1]); + if ($mailbox && $mailbox->authed()) { + $msg_header = $mailbox->get_message_headers(hex2bin($path[2]), $this->request->get['uid']); if (!array_key_exists('From', $msg_header) || count($msg_header) == 0) { return; } - $content = $imap->get_message_content($this->request->get['uid'], 0, false, false); + $content = $mailbox->get_message_content(hex2bin($path[2]), $this->request->get['uid']); # Attachment Download $attached_files = []; @@ -172,11 +170,10 @@ public function process() { } if (array_key_exists('forward', $this->request->get)) { $path = explode('_', $this->request->get['list_path']); - $imap = Hm_IMAP_List::connect($path[1]); - if ($imap && $imap->select_mailbox(hex2bin($path[2]))) { - $msg_struct = $imap->get_message_structure($this->request->get['uid']); - list($part, $msg_text) = $imap->get_first_message_part($this->request->get['uid'], 'text', 'plain', $msg_struct); - $msg_header = $imap->get_message_headers($this->request->get['uid']); + $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1]); + if ($mailbox && $mailbox->authed()) { + list ($msg_struct, $msg_struct_first, $msg_text, $part) = $mailbox->get_structured_message(hex2bin($path[2]), $this->request->get['uid'], false, true); + $msg_header = $mailbox->get_message_headers(hex2bin($path[2]), $this->request->get['uid']); if (!array_key_exists('From', $msg_header) || count($msg_header) == 0) { return; } @@ -196,7 +193,7 @@ public function process() { $new_attachment['size'] = $sub['size']; $new_attachment['type'] = $sub['type']; $file_path = $file_dir . $new_attachment['name']; - $content = Hm_Crypt::ciphertext($imap->get_message_content($this->request->get['uid'], $ind), Hm_Request_Key::generate()); + $content = Hm_Crypt::ciphertext($mailbox->get_message_content(hex2bin($path[2]), $this->request->get['uid'], $ind), Hm_Request_Key::generate()); file_put_contents($file_path, $content); $new_attachment['tmp_name'] = $file_path; $new_attachment['filename'] = $file_path; @@ -804,13 +801,13 @@ public function process() { $msg_path = explode('_', $this->request->post['compose_msg_path']); $msg_uid = $this->request->post['compose_msg_uid']; - $imap = Hm_IMAP_List::connect($msg_path[1]); - if ($imap->select_mailbox(hex2bin($msg_path[2]))) { + $mailbox = Hm_IMAP_List::get_connected_mailbox($msg_path[1]); + if ($mailbox && $mailbox->authed()) { $specials = get_special_folders($this, $msg_path[1]); if (array_key_exists('archive', $specials) && $specials['archive']) { $archive_folder = $specials['archive']; - $imap->message_action('ARCHIVE', array($msg_uid)); - $imap->message_action('MOVE', array($msg_uid), $archive_folder); + $mailbox->message_action(hex2bin($msg_path[2]), 'ARCHIVE', array($msg_uid)); + $mailbox->message_action(hex2bin($msg_path[2]), 'MOVE', array($msg_uid), $archive_folder); } } } @@ -1757,13 +1754,10 @@ function get_primary_recipient($profiles, $headers, $smtp_servers, $is_draft=Fal */ if (!hm_exists('delete_draft')) { function delete_draft($id, $cache, $imap_server_id, $folder) { - $imap = Hm_IMAP_List::connect($imap_server_id); - if (! imap_authed($imap)) { - return false; - } - if ($imap->select_mailbox($folder)) { - if ($imap->message_action('DELETE', array($id))) { - $imap->message_action('EXPUNGE', array($id)); + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id); + if ($mailbox && $mailbox->authed()) { + if ($mailbox->message_action($folder, 'DELETE', array($id))) { + $mailbox->message_action($folder, 'EXPUNGE', array($id)); return true; } } @@ -1919,9 +1913,10 @@ function save_imap_draft($atts, $id, $session, $mod, $mod_cache, $uploaded_files Hm_Msgs::add('ERRThere is no draft directory configured for this account.'); return -1; } - $cache = Hm_IMAP_List::get_cache($mod_cache, $imap_profile['id']); - $imap = Hm_IMAP_List::connect($imap_profile['id'], $cache); - $draft_folder = $imap->select_mailbox($specials['draft']); + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_profile['id'], $mod_cache); + if (! $mailbox || ! $mailbox->authed()) { + return -1; + } $mime = new Hm_MIME_Msg( $atts['draft_to'], @@ -1943,24 +1938,21 @@ function save_imap_draft($atts, $id, $session, $mod, $mod_cache, $uploaded_files $msg = str_replace("\n", "\r\n", $msg); $msg = rtrim($msg)."\r\n"; - if ($imap->append_start($specials['draft'], mb_strlen($msg), false, true)) { - $imap->append_feed($msg."\r\n"); - if (!$imap->append_end()) { - Hm_Msgs::add('ERRAn error occurred saving the draft message'); - return -1; - } + if (! $mailbox->store_message($specials['draft'], $msg, false, true)) { + Hm_Msgs::add('ERRAn error occurred saving the draft message'); + return -1; } - $mailbox_page = $imap->get_mailbox_page($specials['draft'], 'ARRIVAL', true, 'DRAFT', 0, 10); + $messages = $mailbox->get_messages($specials['draft'], 'ARRIVAL', true, 'DRAFT', 0, 10); // Remove old version from the mailbox if ($id) { - $imap->message_action('DELETE', array($id)); - $imap->message_action('EXPUNGE', array($id)); + $mailbox->message_action($specials['draft'], 'DELETE', array($id)); + $mailbox->message_action($specials['draft'], 'EXPUNGE', array($id)); } - foreach ($mailbox_page[1] as $mail) { - $msg_header = $imap->get_message_headers($mail['uid']); + foreach ($messages[1] as $mail) { + $msg_header = $mailbox->get_message_headers($specials['draft'], $mail['uid']); // Convert all header keys to lowercase $msg_header_lower = array_change_key_case($msg_header, CASE_LOWER); $mime_headers_lower = array_change_key_case($mime->get_headers(), CASE_LOWER); From 263665e90dd2aed700be27db758696277d007f89 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 11 Oct 2024 16:45:20 +0300 Subject: [PATCH 03/35] UI updates - manage EWS servers like IMAP/SMTP ones, auth and start using the EWS API --- modules/core/output_modules.php | 2 +- modules/core/setup.php | 2 +- modules/core/site.js | 4 +- modules/imap/functions.php | 7 +- modules/imap/handler_modules.php | 48 +++++++++ modules/imap/hm-ews.php | 27 ++++- modules/imap/hm-mailbox.php | 2 +- modules/imap/output_modules.php | 165 +++++++++++++++++++++++++++++++ modules/imap/setup.php | 12 +++ modules/imap/site.js | 21 ++++ 10 files changed, 280 insertions(+), 10 deletions(-) diff --git a/modules/core/output_modules.php b/modules/core/output_modules.php index e763fe52b2..9df95923f5 100644 --- a/modules/core/output_modules.php +++ b/modules/core/output_modules.php @@ -2109,7 +2109,7 @@ protected function output() { $hasJmapActivated = in_array('jmap', $this->get('router_module_list'), true); if($hasImapActivated){ - $imap_servers_count = count(array_filter($this->get('imap_servers', array()), function($v) { return !array_key_exists('type', $v) || $v['type'] != 'jmap'; })); + $imap_servers_count = count(array_filter($this->get('imap_servers', array()), function($v) { return !array_key_exists('type', $v) || $v['type'] == 'imap'; })); $accordionTitle .= 'IMAP'; $configuredText .= ' ' . $imap_servers_count .' IMAP'; $hasEssentialModuleActivated = true; diff --git a/modules/core/setup.php b/modules/core/setup.php index 6e5003b6cc..879003f54c 100644 --- a/modules/core/setup.php +++ b/modules/core/setup.php @@ -336,6 +336,6 @@ 'srv_setup_stepper_only_jmap' => FILTER_VALIDATE_BOOLEAN, 'srv_setup_stepper_jmap_hide_from_c_page' => FILTER_VALIDATE_BOOLEAN, 'srv_setup_stepper_jmap_address' => FILTER_DEFAULT, - 'srv_setup_stepper_imap_hide_from_c_page' => FILTER_VALIDATE_BOOLEAN + 'srv_setup_stepper_imap_hide_from_c_page' => FILTER_VALIDATE_BOOLEAN, ) ); diff --git a/modules/core/site.js b/modules/core/site.js index 4f81a9bfc9..d720785ad5 100644 --- a/modules/core/site.js +++ b/modules/core/site.js @@ -2311,9 +2311,9 @@ function resetQuickSetupForm() { function handleCreateProfileCheckboxChange(checkbox) { if(checkbox.checked) { - $('#srv_setup_stepper_profile_bloc').show(); + $(checkbox).closest('.form-check').next().show(); }else{ - $('#srv_setup_stepper_profile_bloc').hide(); + $(checkbox).closest('.form-check').next().hide(); } } diff --git a/modules/imap/functions.php b/modules/imap/functions.php index c6812cf7f6..2f6121e5e1 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -1498,6 +1498,7 @@ function connect_to_imap_server($address, $name, $port, $user, $pass, $tls, $ima $imap_list = array( 'name' => $name, 'server' => $address, + 'type' => $type, 'hide' => $hidden, 'port' => $port, 'user' => $user, @@ -1508,8 +1509,6 @@ function connect_to_imap_server($address, $name, $port, $user, $pass, $tls, $ima } if ($type === 'jmap') { - $imap_list['type'] = 'jmap'; - $imap_list['hide'] = $hidden; $imap_list['port'] = false; $imap_list['tls'] = false; } @@ -1526,7 +1525,7 @@ function connect_to_imap_server($address, $name, $port, $user, $pass, $tls, $ima } } else { $imap_server_id = Hm_IMAP_List::add($imap_list); - if (! can_save_last_added_server('Hm_IMAP_List', $user)) { + if ($type != 'ews' && ! can_save_last_added_server('Hm_IMAP_List', $user)) { return; } } @@ -1556,7 +1555,7 @@ function connect_to_imap_server($address, $name, $port, $user, $pass, $tls, $ima if ($mailbox->authed()) { return $imap_server_id; - }else { + } else { Hm_IMAP_List::del($imap_server_id); Hm_Msgs::add('ERRAuthentication failed'); return null; diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index d69e80cdcf..dd9c153474 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -1402,6 +1402,54 @@ public function process() { } } +/** + * Save EWS server details + * @subpackage imap/handler + */ +class Hm_Handler_save_ews_server extends Hm_Handler_Module { + public function process() { + list($success, $form) = $this->process_form(array( + 'ews_profile_name', + 'ews_email', + 'ews_password', + 'ews_server', + 'ews_server_id', + 'ews_hide_from_c_page', + 'ews_create_profile', + 'ews_profile_signature', + 'ews_profile_reply_to', + 'ews_profile_is_default', + )); + // var_dump($success); + // var_dump($form); + // exit; + if ($success) { + $server_id = connect_to_imap_server( + $form['ews_server'], + $form['ews_profile_name'], + null, + $form['ews_email'], + $form['ews_password'], + null, + null, + null, + 'ews', + $this, + $form['ews_hide_from_c_page'], + $form['ews_server_id'], + ); + if(empty($server_id)) { + Hm_Msgs::add("ERRCould not save server"); + return; + }; + // TODO: EWS check if we shouldn't add the same server to smtp server list for the profile + if ($form['ews_create_profile']) { + add_profile($form['ews_profile_name'], $form['ews_profile_signature'], $form['ews_profile_reply_to'], $form['ews_profile_is_default'], $form['ews_email'], $form['ews_server'], $server_id, $server_id, $this); + } + } + } +} + /** * Save IMAP servers * @subpackage imap/handler diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 71e74f41f3..b73ea49887 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -5,15 +5,40 @@ * @package modules * @subpackage imap * - * This is a drop-in replacment of IMAP and JMAP classes that allows usage of Exchange Web Services (EWS) + * This is a drop-in replacment of IMAP, JMAP and SMTP classes that allows usage of Exchange Web Services (EWS) * in all functions provided in imap and smtp modules - accessing mailbox, folders, reading messages, * attachments, moving, copying, read/unread, flags, sending messages. * Connection to EWS is handled by garethp/php-ews package handling NLTM auth and SOAP calls. */ +use garethp\ews\API\Enumeration; +use garethp\ews\API\Exception; +use garethp\ews\API\ExchangeWebServices; +use garethp\ews\API\Type; +use garethp\ews\MailAPI; + /** * public interface to EWS mailboxes * @subpackage imap/lib */ class Hm_EWS { + protected $ews; + protected $api; + protected $authed = false; + + public function connect(array $config) { + try { + $this->ews = ExchangeWebServices::fromUsernameAndPassword($config['server'], $config['username'], $config['password'], ['version' => ExchangeWebServices::VERSION_2016]); + $this->api = new MailAPI($this->ews); + $this->api->getFolderByDistinguishedId(Enumeration\DistinguishedFolderIdNameType::INBOX); + $this->authed = true; + return true; + } catch (Exception\UnauthorizedException $e) { + return false; + } + } + + public function authed() { + return $this->authed; + } } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 4bdadeb07e..240a935949 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -52,7 +52,7 @@ public function authed() { if ($this->is_imap()) { return $this->connection->get_state() == 'authenticated' || $this->connection->get_state() == 'selected'; } else { - // TODO: EWS + return $this->connection->authed(); } } diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index c1190f2481..8a45b323ae 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -455,6 +455,10 @@ protected function output() { $type = 'JMAP'; } + if (array_key_exists('type', $vals) && $vals['type'] == 'ews') { + continue; + } + if (array_key_exists('user', $vals) && !array_key_exists('nopass', $vals)) { $disabled = 'disabled="disabled"'; $user_pc = $vals['user']; @@ -1428,6 +1432,167 @@ protected function output() { } } +class Hm_Output_server_config_ews extends Hm_Output_Module { + protected function output() { + $hasEWSActivated = in_array('imap', $this->get('router_module_list'), true); + + if(! $hasEWSActivated){ + return ''; + } + + $ews_servers_count = count(array_filter($this->get('imap_servers', array()), function($v) { return array_key_exists('type', $v) && $v['type'] == 'ews'; })); + + $res = '
+
+ + + ' . $this->trans('Exchange Servers') . ' + +
' . $ews_servers_count .' ' . $this->trans('EWS') . '
+
+
+
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+
+
+ '; + + $list = $this->get('imap_servers', array()); + foreach ($list as $index => $vals) { + if (! array_key_exists('type', $vals) || $vals['type'] != 'ews') { + continue; + } + + $server_id = $vals['id']; + + if (array_key_exists('user', $vals) && !array_key_exists('nopass', $vals)) { + $disabled = 'disabled="disabled"'; + $user_pc = $vals['user']; + $pass_pc = $this->trans('[saved]'); + $pass_value = '*************'; + } + elseif (array_key_exists('nopass', $vals)) { + if (array_key_exists('user', $vals)) { + $user_pc = $vals['user']; + } + else { + $user_pc = ''; + } + $pass_pc = $this->trans('Password'); + $disabled = ''; + $pass_value = ''; + } + else { + $user_pc = ''; + $pass_pc = $this->trans('Password'); + $disabled = ''; + $pass_value = ''; + } + $res .= '
'; + $res .= '
'; + $res .= ''; + $res .= ''; + $res .= '
'; + $res .= sprintf(' +
%s
+
%s
', + $this->html_safe($vals['name']), $this->html_safe($vals['server'])); + + $res .= '
'; + + $res .= '
'; + $res .= ''; + $res .= '
'; + $res .= '
'; + $res .= '
'; + $res .= ''; + $res .= '
'; + $res .= '
'; + + $res .= '
'; + + // Buttons + $disabled = isset($vals['default']) ? ' disabled': ''; + if (!isset($vals['user']) || !$vals['user']) { + $res .= ''; + $res .= ''; + } else { + $keysToRemove = array('object', 'connected', 'default', 'nopass'); + $serverDetails = array_diff_key($vals, array_flip($keysToRemove)); + + $res .= 'html_safe(json_encode($serverDetails)).'\' data-id="'.$this->html_safe($serverDetails['name']).'" data-type="ews" />'; + $res .= ''; + $res .= ''; + $res .= ''; + } + + // Hide/Unhide Buttons + $hidden = array_key_exists('hide', $vals) && $vals['hide']; + $res .= ''; + $res .= ''; + + $res .= ''; + $res .= '
'; + } + + $res .= ' +
+
'; + + return $res; + } +} + /** * Option to set the per page count for IMAP folder views * @subpackage imap/output diff --git a/modules/imap/setup.php b/modules/imap/setup.php index 5056817aba..ba368b15ed 100644 --- a/modules/imap/setup.php +++ b/modules/imap/setup.php @@ -22,12 +22,14 @@ add_handler('servers', 'process_add_imap_server', true, 'imap', 'message_list_type', 'after'); add_handler('servers', 'process_add_jmap_server', true, 'imap', 'process_add_imap_server', 'after'); add_handler('servers', 'save_imap_servers', true, 'imap', 'process_add_jmap_server', 'after'); +add_handler('servers', 'save_ews_server', true, 'imap', 'save_imap_servers', 'after'); add_output('servers', 'display_configured_imap_servers', true, 'imap', 'server_config_stepper_accordion_end_part', 'before'); add_output('servers', 'imap_server_ids', true, 'imap', 'page_js', 'before'); add_output('servers', 'stepper_setup_server_jmap', true, 'imap', 'server_config_stepper_end_part', 'before'); add_output('servers', 'stepper_setup_server_imap', true, 'imap', 'server_config_stepper_end_part', 'before'); add_output('servers', 'stepper_setup_server_jmap_imap_common', true, 'imap', 'server_config_stepper_end_part', 'before'); +add_output('servers', 'server_config_ews', true, 'imap', 'server_config_stepper_accordion_end_part', 'after'); /* settings page data */ add_handler('settings', 'process_sent_since_setting', true, 'imap', 'date', 'after'); @@ -437,5 +439,15 @@ 'tag_id' => FILTER_DEFAULT, 'first_time_screen_emails' => FILTER_VALIDATE_INT, 'move_messages_in_screen_email' => FILTER_VALIDATE_BOOLEAN, + 'ews_server_id' => FILTER_DEFAULT, + 'ews_profile_name' => FILTER_DEFAULT, + 'ews_email' => FILTER_DEFAULT, + 'ews_password' => FILTER_UNSAFE_RAW, + 'ews_server' => FILTER_DEFAULT, + 'ews_hide_from_c_page' => FILTER_VALIDATE_INT, + 'ews_create_profile' => FILTER_VALIDATE_INT, + 'ews_profile_is_default' => FILTER_VALIDATE_INT, + 'ews_profile_signature' => FILTER_DEFAULT, + 'ews_profile_reply_to' => FILTER_DEFAULT, ) ); diff --git a/modules/imap/site.js b/modules/imap/site.js index 5017f139dc..42fc8c4d66 100644 --- a/modules/imap/site.js +++ b/modules/imap/site.js @@ -121,6 +121,7 @@ var imap_setup_server_page = function() { $('.unhide_imap_connection').on('click', imap_unhide); $('.forget_imap_connection').on('click', imap_forget_action); $('.test_imap_connect').on('click', imap_test_action); + $('.edit_ews_server_connection').on('click', ews_edit_action); var dsp = Hm_Utils.get_from_local_storage('.imap_section'); if (dsp === 'block' || dsp === 'none') { @@ -130,6 +131,26 @@ var imap_setup_server_page = function() { if (jdsp === 'block' || jdsp === 'none') { $('.jmap_section').css('display', jdsp); } + + $('.ews-btn').on('click', function() { + $(this).hide().prev().removeClass('d-none'); + }); +}; + +var ews_edit_action = function(event) { + event.preventDefault(); + Hm_Notices.hide(true); + var details = $(this).data('server-details'); + + $('.ews-btn').trigger('click'); + $('#ews_profile_name').val(details.name).trigger('focus'); + $('#ews_email').val(details.user); + $('#ews_password').val(''); + $('#ews_profile_reply_to').val(''); + $('#ews_create_profile').trigger("click", true); + $('#ews_server').val(details.server); + $('#ews_server_id').val(details.id); + $('#ews_hide_from_c_page').prop("checked", details.hide); }; var set_message_content = function(path, msg_uid) { From 8ed434cd0496aea339a06cc0f6031042d55bf150 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Mon, 14 Oct 2024 19:44:47 +0300 Subject: [PATCH 04/35] EWS: folder management and message display --- modules/imap/functions.php | 8 +- modules/imap/hm-ews.php | 151 +++++++++++++++++++++++++++++++ modules/imap/hm-mailbox.php | 48 ++++++---- modules/imap_folders/modules.php | 20 ++-- 4 files changed, 195 insertions(+), 32 deletions(-) diff --git a/modules/imap/functions.php b/modules/imap/functions.php index 2f6121e5e1..e518afc6a5 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -1208,14 +1208,14 @@ function decode_folder_str($folder) { * @subpackage imap/functions */ if (!hm_exists('prep_folder_name')) { -function prep_folder_name($mailbox, $folder, $decode_folder=false, $parent=false) { +function prep_folder_name($imap, $folder, $decode_folder=false, $parent=false) { if ($parent && $decode_folder) { $parent = decode_folder_str($parent); } if ($decode_folder) { $folder = decode_folder_str($folder); } - $ns = get_personal_ns($mailbox); + $ns = get_personal_ns($imap); if (!$folder) { return false; } @@ -1235,8 +1235,8 @@ function prep_folder_name($mailbox, $folder, $decode_folder=false, $parent=false * @subpackage imap/functions */ if (!hm_exists('get_personal_ns')) { -function get_personal_ns($mailbox) { - $namespaces = $mailbox->get_namespaces(); +function get_personal_ns($imap) { + $namespaces = $imap->get_namespaces(); foreach ($namespaces as $ns) { if ($ns['class'] == 'personal') { return $ns; diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index b73ea49887..540c8ccf7b 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -41,4 +41,155 @@ public function connect(array $config) { public function authed() { return $this->authed; } + + public function get_folders($folder = null) { + $result = []; + if (empty($folder)) { + $folder = new Type\DistinguishedFolderIdType(Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT); + } else { + $folder = new Type\FolderIdType($folder); + } + $request = array( + 'Traversal' => 'Deep', + 'FolderShape' => array( + 'BaseShape' => 'AllProperties', + ), + 'ParentFolderIds' => $folder->toArray(true) + ); + $resp = $this->ews->FindFolder($request); + $folders = $resp->get('folders')->get('folder'); + if ($folders) { + foreach($folders as $folder) { + $id = $folder->get('folderId')->get('id'); + $name = $folder->get('displayName'); + $result[$id] = array( + 'id' => $id, + 'parent' => null, // TODO + 'delim' => false, // TODO - check, might be IMAP-specific + 'name' => $name, + 'name_parts' => [], // TODO - check, might be IMAP-specific + 'basename' => $name, + 'realname' => $name, + 'namespace' => '', // TODO - check, might be IMAP-specific + // TODO - flags + 'marked' => false, + 'noselect' => false, + 'can_have_kids' => true, + 'has_kids' => $folder->get('childFolderCount') > 0, + 'special' => true, + 'clickable' => true + ); + } + } + return $result; + } + + public function get_folder_status($folder) { + try { + $result = $this->api->getFolder((new Type\FolderIdType($folder))->toArray(true)); + return [ + 'messages' => $result->get('totalCount'), + 'uidvalidity' => false, + 'uidnext' => false, + 'recent' => false, + 'unseen' => $result->get('unreadCount'), + ]; + } catch (Exception $e) { + return []; + } + } + + public function create_folder($folder, $parent = null) { + if (empty($parent)) { + $parent = Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT; + } + try { + return $this->api->createFolders([$folder], new Type\DistinguishedFolderIdType($parent)); + } catch(Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + return false; + } + } + + public function rename_folder($folder, $new_name, $parent = null) { + $result = []; + $new_folder = new Type\FolderType(); + $new_folder->displayName = $new_name; + if ($parent) { + $new_folder->parentFolderId = new Type\FolderIdType($parent); + } + $setFolderField = new Type\SetFolderFieldType(); + $setFolderField->folder = $new_folder; + $fieldURI = new Type\FieldURI(); + $fieldURI->fieldURI = 'folder:displayName'; + $setFolderField->fieldURI = $fieldURI; + $updates = new Type\NonEmptyArrayOfFolderChangeDescriptionsType(); + $updates->set('setFolderField', [$setFolderField]); + $change = new Type\FolderChangeType(); + $change->folderId = new Type\FolderIdType($folder); + $change->updates = $updates; + $request = [ + 'FolderChanges' => [ + $change + ], + ]; + try { + $resp = $this->ews->UpdateFolder($request); + // TODO: EWS: resolve internal server error issue and return status + return true; + } catch (Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + return false; + } + } + + public function delete_folder($folder) { + try { + return $this->api->deleteFolder(new Type\FolderIdType($folder)); + } catch(Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + return false; + } + } + + public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders) { + $folder = new Type\FolderIdType($folder); + $result = $this->api->getMailItems($folder, [ + // TODO: sort, pagination, search + ]); + $messages = []; + foreach ($result->get('items')->get('message') as $message) { + $flags = []; + if ($message->get('isRead')) { + $flags[] = '\\Seen'; + } + if ($message->get('isDraft')) { + $flags[] = '\\Draft'; + } + // TODO: EWS - check \Answered, \Flagged, \Deleted flags + $messages[] = [ + 'uid' => $message->get('itemId')->get('id'), + 'flags' => implode(' ', $flags), + 'internal_date' => $message->get('dateTimeCreated'), + 'size' => $message->get('size'), + 'date' => $message->get('dateTimeReceived'), + 'from' => $message->get('from')->get('mailbox')->get('emailAddress'), + 'to' => $message->get('receivedBy')->get('mailbox')->get('emailAddress'), + 'subject' => $message->get('subject'), + 'content-type' => $message->get('mimeContent'), + 'timestamp' => time(), + 'charset' => null, + 'x-priority' => null, + 'google_msg_id' => null, + 'google_thread_id' => null, + 'google_labels' => null, + 'list_archive' => null, + 'references' => $message->get('references'), + 'message_id' => $message->get('internetMessageId'), + 'x_auto_bcc' => null, + 'x_snoozed' => null, + ]; + } + return [$result->get('totalItemsInView'), $messages]; + } } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 240a935949..0048930499 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -33,6 +33,10 @@ public function connect(array $config) { return $this->connection->connect($config); } + public function get_connection() { + return $this->connection; + } + public function is_imap() { return $this->type !== self::TYPE_EWS; } @@ -63,29 +67,33 @@ public function get_folder_status($folder) { if ($this->is_imap()) { return $this->connection->get_mailbox_status($folder); } else { - // TODO: EWS + return $this->connection->get_folder_status($folder); } } - public function create_folder($folder) { + public function create_folder($folder, $parent = null) { if (! $this->authed()) { return; } if ($this->is_imap()) { - return $this->connection->create_mailbox($folder); + $new_folder = prep_folder_name($this->connection, $folder, false, $parent); + return $this->connection->create_mailbox($new_folder); } else { - // TODO: EWS + return $this->connection->create_folder($folder, $parent); } } - public function rename_folder($folder, $new_name) { + public function rename_folder($folder, $new_name, $parent = null) { if (! $this->authed()) { return; } if ($this->is_imap()) { - return $this->connection->rename_mailbox($folder, $new_name); + $old_folder = prep_folder_name($this->connection, $folder, true); + $new_folder = prep_folder_name($this->connection, $new_name, false, $parent); + return $this->connection->rename_mailbox($old_folder, $new_folder); } else { - // TODO: EWS + $folder = decode_folder_str($folder); + return $this->connection->rename_folder($folder, $new_name, $parent); } } @@ -94,9 +102,19 @@ public function delete_folder($folder) { return; } if ($this->is_imap()) { - return $this->connection->delete_mailbox($folder); + $del_folder = prep_folder_name($this->connection, $folder, true); + return $this->connection->delete_mailbox($del_folder); } else { - // TODO: EWS + $del_folder = decode_folder_str($folder); + return $this->connection->delete_folder($del_folder); + } + } + + public function prep_folder_name($folder) { + if ($this->is_imap()) { + return prep_folder_name($this->connection, $folder, true); + } else { + return $folder; } } @@ -118,7 +136,8 @@ public function get_folders($only_subscribed = false) { if ($this->is_imap()) { return $this->connection->get_mailbox_list($only_subscribed); } else { - // TODO: EWS + // TODO: EWS only_subscribed + return $this->connection->get_folders(); } } @@ -129,7 +148,8 @@ public function get_subfolders($folder, $only_subscribed = false, $with_input = if ($this->is_imap()) { return $this->connection->get_folder_list_by_level($folder, $only_subscribed, $with_input); } else { - // TODO: EWS + // TODO: EWS only_subscribed and with_input + return $this->connection->get_folders($folder); } } @@ -169,7 +189,7 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, if ($this->is_imap()) { return $this->connection->get_mailbox_page($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); } else { - // TODO: EWS + return $this->connection->get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); } } @@ -414,10 +434,6 @@ public function get_capability() { return $this->connection->get_capability(); } - public function get_namespaces() { - return $this->connection->get_namespaces(); - } - public function set_read_only($read_only) { if ($this->is_imap()) { $this->connection->read_only = $read_only; diff --git a/modules/imap_folders/modules.php b/modules/imap_folders/modules.php index 275ecc1779..abb23699f6 100644 --- a/modules/imap_folders/modules.php +++ b/modules/imap_folders/modules.php @@ -103,7 +103,7 @@ public function process() { Hm_Msgs::add('ERRUnable to connect to the selected IMAP server'); return; } - $new_folder = prep_folder_name($mailbox, $form['folder'], true); + $new_folder = $mailbox->prep_folder_name($form['folder']); if (! $new_folder || ! $mailbox->get_folder_status($new_folder)) { Hm_Msgs::add('ERRSelected folder not found'); return; @@ -174,14 +174,12 @@ public function process() { list($success, $form) = $this->process_form(array('folder', 'imap_server_id')); if ($success) { $parent = false; - $parent_str = false; if (array_key_exists('parent', $this->request->post) && trim($this->request->post['parent'])) { - $parent_str = decode_folder_str($this->request->post['parent']); + $parent = decode_folder_str($this->request->post['parent']); } $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); if ($mailbox && $mailbox->authed()) { - $new_folder = prep_folder_name($mailbox, $form['folder'], false, $parent_str); - if ($new_folder && $mailbox->create_folder($new_folder)) { + if ($form['folder'] && $mailbox->create_folder($form['folder'], $parent)) { Hm_Msgs::add('Folder created'); $this->cache->del('imap_folders_imap_'.$form['imap_server_id'].'_'); $this->out('imap_folders_success', true); @@ -201,15 +199,13 @@ class Hm_Handler_process_folder_rename extends Hm_Handler_Module { public function process() { list($success, $form) = $this->process_form(array('imap_server_id', 'folder', 'new_folder')); if ($success) { - $parent_str = false; + $parent = false; if (array_key_exists('parent', $this->request->post)) { - $parent_str = $this->request->post['parent']; + $parent = $this->request->post['parent']; } $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); if ($mailbox && $mailbox->authed()) { - $old_folder = prep_folder_name($mailbox, $form['folder'], true); - $new_folder = prep_folder_name($mailbox, $form['new_folder'], false, $parent_str); - if ($new_folder && $old_folder && $mailbox->rename_mailbox($old_folder, $new_folder)) { + if ($form['folder'] && $form['new_folder'] && $mailbox->rename_folder($form['folder'], $form['new_folder'], $parent)) { if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER)) { $imap_servers = $this->user_config->get('imap_servers'); $imap_account = $imap_servers[$form['imap_server_id']]; @@ -267,14 +263,14 @@ public function process() { if ($success) { $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); if ($mailbox && $mailbox->authed()) { - $del_folder = prep_folder_name($mailbox, $form['folder'], true); if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER)) { + $del_folder = prep_folder_name($mailbox->get_connection(), $form['folder'], true); if (is_mailbox_linked_with_filters($del_folder, $form['imap_server_id'], $this)) { Hm_Msgs::add('ERRThis folder can\'t be deleted because it is used in a filter.'); return; } } - if ($del_folder && $mailbox->delete_folder($del_folder)) { + if ($form['folder'] && $mailbox->delete_folder($form['folder'])) { Hm_Msgs::add('Folder deleted'); $this->cache->del('imap_folders_imap_'.$form['imap_server_id'].'_'); $this->out('imap_folders_success', true); From a42e9c527143a8fbe3d864043006e86b61a8de14 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 16 Oct 2024 16:11:56 +0300 Subject: [PATCH 05/35] EWS: use a combination of FindItem and GetItem to get message headers appropriately for message list page --- modules/imap/hm-ews.php | 64 ++++++++++++++++++++++++++++++------- modules/imap/hm-mailbox.php | 7 +++- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 540c8ccf7b..ca9870888e 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -154,11 +154,38 @@ public function delete_folder($folder) { public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders) { $folder = new Type\FolderIdType($folder); - $result = $this->api->getMailItems($folder, [ - // TODO: sort, pagination, search - ]); + $request = array( + 'Traversal' => 'Shallow', + 'ItemShape' => array( + 'BaseShape' => 'IdOnly' + ), + 'ParentFolderIds' => $folder->toArray(true) + ); + // TODO: sort, pagination, search + $request = Type::buildFromArray($request); + $result = $this->ews->FindItem($request); + $itemIds = array_map(function($msg) { + return $msg->get('itemId')->get('id'); + }, $result->get('items')->get('message')); + return [$result->get('totalItemsInView'), $this->get_message_list($itemIds)]; + } + + public function get_message_list($itemIds) { + $request = array( + 'ItemShape' => array( + 'BaseShape' => 'AllProperties' + ), + 'ItemIds' => [ + 'ItemId' => array_map(function($id) { + return ['Id' => $id]; + }, $itemIds), + ], + ); + $request = Type::buildFromArray($request); + $result = $this->ews->GetItem($request); $messages = []; - foreach ($result->get('items')->get('message') as $message) { + foreach ($result as $message) { + // TODO: EWS - check \Answered, \Flagged, \Deleted flags $flags = []; if ($message->get('isRead')) { $flags[] = '\\Seen'; @@ -166,17 +193,17 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $l if ($message->get('isDraft')) { $flags[] = '\\Draft'; } - // TODO: EWS - check \Answered, \Flagged, \Deleted flags - $messages[] = [ - 'uid' => $message->get('itemId')->get('id'), + $uid = bin2hex($message->get('itemId')->get('id')); + $msg = [ + 'uid' => $uid, 'flags' => implode(' ', $flags), 'internal_date' => $message->get('dateTimeCreated'), 'size' => $message->get('size'), 'date' => $message->get('dateTimeReceived'), - 'from' => $message->get('from')->get('mailbox')->get('emailAddress'), - 'to' => $message->get('receivedBy')->get('mailbox')->get('emailAddress'), + 'from' => $message->get('sender')->get('mailbox')->get('name') . ' <' . $message->get('from')->get('mailbox')->get('emailAddress') . '>', + 'to' => $message->get('toRecipients')->Mailbox->get('name') . ' <' . $message->get('toRecipients')->Mailbox->get('emailAddress') . '>', 'subject' => $message->get('subject'), - 'content-type' => $message->get('mimeContent'), + 'content-type' => null, 'timestamp' => time(), 'charset' => null, 'x-priority' => null, @@ -189,7 +216,22 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $l 'x_auto_bcc' => null, 'x_snoozed' => null, ]; + foreach ($message->get('internetMessageHeaders')->InternetMessageHeader as $header) { + foreach (['x-gm-msgid' => 'google_msg_id', 'x-gm-thrid' => 'google_thread_id', 'x-gm-labels' => 'google_labels', 'x-auto-bcc' => 'x_auto_bcc', 'message-id' => 'message_id', 'references' => 'references', 'x-snoozed' => 'x_snoozed', 'list-archive' => 'list_archive', 'content-type' => 'content-type', 'x-priority' => 'x-priority'] as $hname => $key) { + if (strtolower($header->get('headerName')) == $hname) { + $msg[$key] = (string) $header; + } + } + } + $cset = ''; + if (mb_stristr($msg['content-type'], 'charset=')) { + if (preg_match("/charset\=([^\s;]+)/", $msg['content-type'], $matches)) { + $cset = trim(mb_strtolower(str_replace(array('"', "'"), '', $matches[1]))); + } + } + $msg['charset'] = $cset; + $messages[$uid] = $msg; } - return [$result->get('totalItemsInView'), $messages]; + return $messages; } } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 0048930499..8dbf118e11 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -16,6 +16,7 @@ class Hm_Mailbox { protected $type; protected $connection; + protected $selected_folder; public function connect(array $config) { if (array_key_exists('type', $config) && $config['type'] == 'jmap') { @@ -166,7 +167,7 @@ public function get_selected_folder() { if ($this->is_imap()) { return $this->connection->selected_mailbox; } else { - // TODO: EWS + return $this->selected_folder; } } @@ -186,6 +187,9 @@ public function get_special_use_mailboxes($folder = false) { * @return array - [total results found, results for a single page] */ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, $limit=0, $keyword=false, $trusted_senders=[]) { + if (! $this->select_folder($folder)) { + return; + } if ($this->is_imap()) { return $this->connection->get_mailbox_page($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); } else { @@ -469,6 +473,7 @@ public function get_message_list($folder, $msg_ids) { } protected function select_folder($folder) { + $this->selected_folder = ['name' => $folder, 'detail' => []]; if ($this->is_imap()) { if (isset($this->connection->selected_mailbox['name']) && $this->connection->selected_mailbox['name'] == $folder) { return true; From ac7e7b98871808f5d8fff2b7d121f29ed3f5d067 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 16 Oct 2024 17:36:20 +0300 Subject: [PATCH 06/35] EWS: handle display of folder names --- modules/imap/handler_modules.php | 9 +++++++-- modules/imap/hm-ews.php | 1 + modules/imap/hm-mailbox.php | 22 +++++++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index dd9c153474..eb2345d302 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -573,7 +573,13 @@ public function process() { else { $folder = hex2bin($parts[2]); } - $title = array('IMAP', $details['name'], $folder); + $mailbox = Hm_IMAP_List::get_connected_mailbox($details['id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $label = $mailbox->get_folder_name($folder); + } else { + $label = $folder; + } + $title = array(strtoupper($details['type'] ?? 'IMAP'), $details['name'], $label); if ($this->get('list_page', 0)) { $title[] = sprintf('Page %d', $this->get('list_page', 0)); } @@ -751,7 +757,6 @@ public function process() { foreach ($results as $msg) { $msg['server_id'] = $form['imap_server_id']; $msg['server_name'] = $details['name']; - $msg['folder'] = $form['folder']; $msgs[] = $msg; } if ($folder = $mailbox->get_selected_folder()) { diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index ca9870888e..ad36cf309e 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -88,6 +88,7 @@ public function get_folder_status($folder) { try { $result = $this->api->getFolder((new Type\FolderIdType($folder))->toArray(true)); return [ + 'name' => $result->get('displayName'), 'messages' => $result->get('totalCount'), 'uidvalidity' => false, 'uidnext' => false, diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 8dbf118e11..35f6beded6 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -72,6 +72,15 @@ public function get_folder_status($folder) { } } + public function get_folder_name($folder) { + if ($this->is_imap()) { + return $folder; + } else { + $result = $this->connection->get_folder_status($folder); + return $result['name']; + } + } + public function create_folder($folder, $parent = null) { if (! $this->authed()) { return; @@ -191,10 +200,15 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, return; } if ($this->is_imap()) { - return $this->connection->get_mailbox_page($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); + $messages = $this->connection->get_mailbox_page($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); } else { - return $this->connection->get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); + $messages = $this->connection->get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); + $folder = $this->selected_folder['name']; } + foreach ($messages[1] as &$msg) { + $msg['folder'] = bin2hex($folder); + } + return $messages; } public function get_message_headers($folder, $msg_id) { @@ -473,7 +487,6 @@ public function get_message_list($folder, $msg_ids) { } protected function select_folder($folder) { - $this->selected_folder = ['name' => $folder, 'detail' => []]; if ($this->is_imap()) { if (isset($this->connection->selected_mailbox['name']) && $this->connection->selected_mailbox['name'] == $folder) { return true; @@ -481,6 +494,9 @@ protected function select_folder($folder) { if (! $this->connection->select_mailbox($folder)) { return false; } + } else { + $result = $this->get_folder_status($folder); + $this->selected_folder = ['id' => $folder, 'name' => $result['name'], 'detail' => []]; } return true; } From 0f7094ca05c9f97f0156a3175b6539567968083e Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Thu, 17 Oct 2024 20:25:01 +0300 Subject: [PATCH 07/35] EWS: get message structure similar to imap using mime content and parts, get message or part contents, display message --- modules/imap/hm-ews.php | 197 +++++++++++++++++++++++++++++++++++- modules/imap/hm-mailbox.php | 8 +- 2 files changed, 200 insertions(+), 5 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index ad36cf309e..01e8c97d12 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -17,6 +17,8 @@ use garethp\ews\API\Type; use garethp\ews\MailAPI; +use ZBateson\MailMimeParser\MailMimeParser; + /** * public interface to EWS mailboxes * @subpackage imap/lib @@ -202,7 +204,7 @@ public function get_message_list($itemIds) { 'size' => $message->get('size'), 'date' => $message->get('dateTimeReceived'), 'from' => $message->get('sender')->get('mailbox')->get('name') . ' <' . $message->get('from')->get('mailbox')->get('emailAddress') . '>', - 'to' => $message->get('toRecipients')->Mailbox->get('name') . ' <' . $message->get('toRecipients')->Mailbox->get('emailAddress') . '>', + 'to' => $this->extract_mailbox($message->get('toRecipients')), 'subject' => $message->get('subject'), 'content-type' => null, 'timestamp' => time(), @@ -235,4 +237,197 @@ public function get_message_list($itemIds) { } return $messages; } + + public function get_message_headers($itemId) { + $request = array( + 'ItemShape' => array( + 'BaseShape' => 'AllProperties', + ), + 'ItemIds' => [ + 'ItemId' => ['Id' => hex2bin($itemId)], + ], + ); + $request = Type::buildFromArray($request); + $message = $this->ews->GetItem($request); + $headers = []; + $headers['Arrival Date'] = $message->get('dateTimeCreated'); + $headers['From'] = $message->get('sender')->get('mailbox')->get('name') . ' <' . $message->get('from')->get('mailbox')->get('emailAddress') . '>'; + $headers['To'] = $this->extract_mailbox($message->get('toRecipients')); + if ($message->get('ccRecipients')) { + $headers['Cc'] = $this->extract_mailbox($message->get('ccRecipients')); + } + if ($message->get('bccRecipients')) { + $headers['Bcc'] = $this->extract_mailbox($message->get('bccRecipients')); + } + foreach ($message->get('internetMessageHeaders')->InternetMessageHeader as $header) { + $name = $header->get('headerName'); + if (isset($headers[$name])) { + if (! is_array($headers[$name])) { + $headers[$name] = [$headers[$name]]; + } + $headers[$name][] = (string) $header; + } else { + $headers[$name] = (string) $header; + } + } + return $headers; + } + + public function get_message_content($itemId, $part) { + if ($part) { + list($msg_struct, $msg_struct_current, $msg_text, $part) = $this->get_structured_message($itemId, $part, false); + return $msg_text; + } else { + $message = $this->get_mime_message_by_id($itemId); + $content = $message->getHtmlContent(); + if (empty($content)) { + $content = $message->getTextContent(); + } + return $content; + } + } + + public function get_structured_message($itemId, $part, $text_only) { + $message = $this->get_mime_message_by_id($itemId); + $msg_struct = []; + $this->parse_mime_part($message, $msg_struct, 0); + if ($part !== false) { + $struct = $this->search_mime_part_in_struct($msg_struct, ['part_id' => $part], true); + } else { + $struct = null; + if (! $text_only) { + $struct = $this->search_mime_part_in_struct($msg_struct, ['type' => 'text', 'subtype' => 'html']); + } + if (! $struct) { + $struct = $this->search_mime_part_in_struct($msg_struct, ['type' => 'text']); + } + } + if ($struct) { + $part = array_key_first($struct); + $msg_struct_current = $struct[$part]; + $msg_text = $msg_struct_current['mime_object']->getContent(); + } else { + $part = false; + $msg_struct_current = null; + $msg_text = ''; + } + if (isset($msg_struct_current['subtype']) && mb_strtolower($msg_struct_current['subtype'] == 'html')) { + // add inline images + if (preg_match_all("/src=('|\"|)cid:([^\s'\"]+)/", $msg_text, $matches)) { + $cids = array_pop($matches); + foreach ($cids as $id) { + $struct = $this->search_mime_part_in_struct($msg_struct, ['id' => $id, 'type' => 'image']); + if ($struct) { + $struct = array_shift($struct); + $msg_text = str_replace('cid:'.$id, 'data:image/'.$struct['subtype'].';base64,'.base64_encode($struct['mime_object']->getContent()), $msg_text); + } + } + } + } + return [$msg_struct, $msg_struct_current, $msg_text, $part]; + } + + protected function parse_mime_part($part, &$struct, $part_num) { + $struct[$part_num] = []; + list($struct[$part_num]['type'], $struct[$part_num]['subtype']) = explode('/', $part->getContentType()); + if ($part->isMultiPart()) { + $boundary = $part->getHeaderParameter('Content-Type', 'boundary'); + if ($boundary) { + $struct[$part_num]['attributes'] = ['boundary' => $boundary]; + } + $struct[$part_num]['disposition'] = $part->getContentDisposition(); + $struct[$part_num]['language'] = ''; + $struct[$part_num]['location'] = ''; + } else { + $content = $part->getContent(); + $charset = $part->getCharset(); + if ($charset) { + $struct[$part_num]['attributes'] = ['charset' => $charset]; + } + $struct[$part_num]['id'] = $part->getContentId(); + $struct[$part_num]['description'] = $part->getHeaderValue('Content-Description'); + $struct[$part_num]['encoding'] = $part->getContentTransferEncoding(); + $struct[$part_num]['size'] = strlen($content); + $struct[$part_num]['lines'] = substr_count($content, "\n"); + $struct[$part_num]['md5'] = ''; + $struct[$part_num]['disposition'] = $part->getContentDisposition(); + $struct[$part_num]['file_attributes'] = ''; + $struct[$part_num]['language'] = ''; + $struct[$part_num]['location'] = ''; + } + $struct[$part_num]['mime_object'] = $part; + if ($part->getChildCount() > 0) { + $struct[$part_num]['subs'] = []; + foreach ($part->getChildParts() as $i => $child) { + $this->parse_mime_part($child, $struct[$part_num]['subs'], $part_num . '.' . ($i+1)); + } + } + } + + protected function search_mime_part_in_struct($struct, $conditions, $all = false) { + $found = []; + foreach ($struct as $part_id => $sub) { + $matches = 0; + if (isset($conditions['part_id']) && $part_id == $conditions['part_id']) { + $matches++; + } + foreach ($conditions as $name => $value) { + if (isset($sub[$name]) && mb_stristr($sub[$name], $value)) { + $matches++; + } + } + if ($matches === count($conditions)) { + $part = $sub; + if (isset($part['subs'])) { + $part['subs'] = count($part['subs']); + } + $found[$part_id] = $part; + if (! $all) { + break; + } + } + if (isset($sub['subs'])) { + $found = array_merge($found, $this->search_mime_part_in_struct($sub['subs'], $conditions, $all)); + } + if (! $all && $found) { + break; + } + } + return $found; + } + + protected function get_mime_message_by_id($itemId) { + $request = array( + 'ItemShape' => array( + 'BaseShape' => 'IdOnly', + 'IncludeMimeContent' => true, + ), + 'ItemIds' => [ + 'ItemId' => ['Id' => hex2bin($itemId)], + ], + ); + $request = Type::buildFromArray($request); + $message = $this->ews->GetItem($request); + $mime = $message->get('mimeContent'); + $content = base64_decode($mime); + if (strtoupper($mime->get('characterSet')) != 'UTF-8') { + $content = mb_convert_encoding($content, 'UTF-8', $mime->get('characterSet')); + } + $parser = new MailMimeParser(); + return $parser->parse($content, false); + } + + protected function extract_mailbox($data) { + if (is_array($data)) { + $result = []; + foreach ($data as $mailbox) { + $result[] = $this->extract_mailbox($mailbox); + } + return $result; + } elseif (is_object($data)) { + return $data->Mailbox->get('name') . ' <' . $data->Mailbox->get('emailAddress') . '>'; + } else { + return (string) $data; + } + } } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 35f6beded6..0a3880d9d6 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -218,7 +218,7 @@ public function get_message_headers($folder, $msg_id) { if ($this->is_imap()) { return $this->connection->get_message_headers($msg_id); } else { - // TODO: EWS + return $this->connection->get_message_headers($msg_id); } } @@ -233,7 +233,7 @@ public function get_message_content($folder, $msg_id, $part = 0) { if ($this->is_imap()) { return $this->connection->get_message_content($msg_id, $part); } else { - // TODO: EWS + return $this->connection->get_message_content($msg_id, $part); } } @@ -285,7 +285,7 @@ public function get_structured_message($folder, $msg_id, $part, $text_only) { } return [$msg_struct, $msg_struct_current, $msg_text, $part]; } else { - // TODO: EWS + return $this->connection->get_structured_message($msg_id, $part, $text_only); } } @@ -482,7 +482,7 @@ public function get_message_list($folder, $msg_ids) { if ($this->is_imap()) { return $this->connection->get_message_list($msg_ids); } else { - // TODO: EWS + return $this->connection->get_message_list($msg_ids); } } From 23f723f4e10655dc4b6bccc0b33f962d2d5111ca Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Mon, 21 Oct 2024 14:05:52 +0300 Subject: [PATCH 08/35] EWS: stream/download message part --- modules/imap/hm-ews.php | 25 +++++++++++++++++++++++++ modules/imap/hm-mailbox.php | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 01e8c97d12..30f064140a 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -287,6 +287,31 @@ public function get_message_content($itemId, $part) { } } + public function stream_message_part($itemId, $part, $start_cb) { + if ($part !== '0' && $part) { + // imap handler modules strip this prefix + $part = '0.' . $part; + } + list($msg_struct, $part_struct, $msg_text, $part) = $this->get_structured_message($itemId, $part, false); + $charset = ''; + if (! empty($part_struct['attributes']['charset'])) { + $charset = '; charset=' . $part_struct['attributes']['charset']; + } + $part_name = get_imap_part_name($part_struct, $itemId, $part); + $start_cb($part_struct['type'] . '/' . $part_struct['subtype'] . $charset, $part_name); + if (! $charset) { + $charset = 'UTF-8'; + } else { + $charset = $part_struct['attributes']['charset']; + } + $stream = $part_struct['mime_object']->getContentStream($charset); + if ($stream) { + while (! $stream->eof()) { + echo $stream->read(1024); + } + } + } + public function get_structured_message($itemId, $part, $text_only) { $message = $this->get_mime_message_by_id($itemId); $msg_struct = []; diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 0a3880d9d6..f1a177861a 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -376,7 +376,7 @@ public function stream_message_part($folder, $msg_id, $part_id, $start_cb) { } } } else { - // TODO: EWS + return $this->connection->stream_message_part($msg_id, $part_id, $start_cb); } } From 9603d7f8d43f06a2a761979836df18ea9e4e50ae Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Mon, 21 Oct 2024 15:59:17 +0300 Subject: [PATCH 09/35] EWS: special folders, auto-assign, review and display --- modules/imap/handler_modules.php | 14 ++++++++++ modules/imap/hm-ews.php | 30 +++++++++++++++++++- modules/imap/hm-mailbox.php | 8 ++++-- modules/imap_folders/modules.php | 48 ++++++++++++++++++++++---------- 4 files changed, 82 insertions(+), 18 deletions(-) diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index eb2345d302..672ebe983b 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -1451,6 +1451,20 @@ public function process() { if ($form['ews_create_profile']) { add_profile($form['ews_profile_name'], $form['ews_profile_signature'], $form['ews_profile_reply_to'], $form['ews_profile_is_default'], $form['ews_email'], $form['ews_server'], $server_id, $server_id, $this); } + // auto-assign special folders + $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); + if (is_object($mailbox) && $mailbox->authed()) { + $specials = $this->user_config->get('special_imap_folders', array()); + $exposed = $mailbox->get_special_use_mailboxes(); + $specials[$server_id] = [ + 'sent' => $exposed['sent'] ?? '', + 'draft' => $exposed['drafts'] ?? '', + 'trash' => $exposed['trash'] ?? '', + 'archive' => $exposed['archive'] ?? '', + 'junk' => $exposed['junk'] ?? '' + ]; + $this->user_config->set('special_imap_folders', $specials); + } } } } diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 30f064140a..1775f90b58 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -86,9 +86,31 @@ public function get_folders($folder = null) { return $result; } + public function get_special_use_folders($folder = false) { + $special = array_filter([ + 'trash' => Enumeration\DistinguishedFolderIdNameType::DELETED, + 'sent' => Enumeration\DistinguishedFolderIdNameType::SENT, + 'flagged' => false, + 'all' => false, + 'junk' => Enumeration\DistinguishedFolderIdNameType::JUNK, + 'archive' => false, // TODO: check if Enumeration\DistinguishedFolderIdNameType::ARCHIVEMSGFOLDERROOT should be used - it is outside of MESSAGE_ROOT, however. + 'drafts' => Enumeration\DistinguishedFolderIdNameType::DRAFTS, + ]); + if (isset($special[$folder])) { + return [$folder => $special[$folder]]; + } else { + return $special; + } + } + public function get_folder_status($folder) { try { - $result = $this->api->getFolder((new Type\FolderIdType($folder))->toArray(true)); + if ($this->is_distinguished_folder($folder)) { + $folder = new Type\DistinguishedFolderIdType($folder); + } else { + $folder = new Type\FolderIdType($folder); + } + $result = $this->api->getFolder($folder->toArray(true)); return [ 'name' => $result->get('displayName'), 'messages' => $result->get('totalCount'), @@ -455,4 +477,10 @@ protected function extract_mailbox($data) { return (string) $data; } } + + protected function is_distinguished_folder($folder) { + $oClass = new ReflectionClass(new Enumeration\DistinguishedFolderIdNameType()); + $constants = $oClass->getConstants(); + return in_array($folder, $constants); + } } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index f1a177861a..e9d390ede9 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -124,7 +124,11 @@ public function prep_folder_name($folder) { if ($this->is_imap()) { return prep_folder_name($this->connection, $folder, true); } else { - return $folder; + if (substr_count($folder, '_') >= 2) { + return decode_folder_str($folder); + } else { + return $folder; + } } } @@ -187,7 +191,7 @@ public function get_special_use_mailboxes($folder = false) { if ($this->is_imap()) { return $this->connection->get_special_use_mailboxes($folder); } else { - // TODO: EWS + return $this->connection->get_special_use_folders($folder); } } diff --git a/modules/imap_folders/modules.php b/modules/imap_folders/modules.php index abb23699f6..f766e5d815 100644 --- a/modules/imap_folders/modules.php +++ b/modules/imap_folders/modules.php @@ -296,6 +296,14 @@ public function process() { $this->out('archive_folder', $specials[$this->request->get['imap_server_id']]['archive']); $this->out('draft_folder', $specials[$this->request->get['imap_server_id']]['draft']); $this->out('junk_folder', $specials[$this->request->get['imap_server_id']]['junk']); + $mailbox = Hm_IMAP_List::get_connected_mailbox($this->request->get['imap_server_id'], $this->cache); + if ($mailbox && $mailbox->authed()) { + $folder_names = []; + foreach ($specials[$this->request->get['imap_server_id']] as $name => $folder) { + $folder_names[$name] = $mailbox->get_folder_name($folder); + } + $this->out('special_folder_names', $folder_names); + } } } else { @@ -477,13 +485,15 @@ protected function output() { } $sent_folder = $this->get('sent_folder', $this->trans('Not set')); - if (!$sent_folder) { - $sent_folder = $this->trans('Not set'); + if (! $sent_folder) { + $folder_name = $this->trans('Not set'); + } else { + $folder_name = $this->get('special_folder_names')['sent'] ?? $sent_folder; } $res = '
'; $res .= ''; $res .= '
@@ -521,13 +531,15 @@ protected function output() { } $archive_folder = $this->get('archive_folder', $this->trans('Not set')); - if (!$archive_folder) { - $archive_folder = $this->trans('Not set'); + if (! $archive_folder) { + $folder_name = $this->trans('Not set'); + } else { + $folder_name = $this->get('special_folder_names')['archive'] ?? $archive_folder; } $res = '
'; $res .= ''; $res .= '
@@ -565,13 +577,15 @@ protected function output() { } $draft_folder = $this->get('draft_folder', $this->trans('Not set')); - if (!$draft_folder) { - $draft_folder = $this->trans('Not set'); + if (! $draft_folder) { + $folder_name = $this->trans('Not set'); + } else { + $folder_name = $this->get('special_folder_names')['draft'] ?? $draft_folder; } $res = '
'; $res .= ''; $res .= '
@@ -609,13 +623,15 @@ protected function output() { } $trash_folder = $this->get('trash_folder', $this->trans('Not set')); - if (!$trash_folder) { - $trash_folder = $this->trans('Not set'); + if (! $trash_folder) { + $folder_name = $this->trans('Not set'); + } else { + $folder_name = $this->get('special_folder_names')['trash'] ?? $trash_folder; } $res = '
'; $res .= ''; $res .= ''; $res .= '
@@ -650,13 +666,15 @@ protected function output() { } $junk_folder = $this->get('junk_folder', $this->trans('Not set')); - if (!$junk_folder) { - $junk_folder = $this->trans('Not set'); + if (! $junk_folder) { + $folder_name = $this->trans('Not set'); + } else { + $folder_name = $this->get('special_folder_names')['junk'] ?? $junk_folder; } $res = '
'; $res .= ''; $res .= ''; From 32279d14bb06ab012d02d4468392e5f0355126c5 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 22 Oct 2024 07:38:04 +0300 Subject: [PATCH 10/35] EWS: special folders update, emulate folder subscription and show only subscribed based on setting --- modules/imap/hm-ews.php | 24 +++++++++++++++++----- modules/imap/hm-imap.php | 6 +++++- modules/imap/hm-mailbox.php | 34 +++++++++++++++++++++++++++++--- modules/imap_folders/modules.php | 2 +- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 1775f90b58..d22ccded88 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -44,7 +44,7 @@ public function authed() { return $this->authed; } - public function get_folders($folder = null) { + public function get_folders($folder = null, $only_subscribed = false, $unsubscribed_folders = []) { $result = []; if (empty($folder)) { $folder = new Type\DistinguishedFolderIdType(Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT); @@ -61,9 +61,13 @@ public function get_folders($folder = null) { $resp = $this->ews->FindFolder($request); $folders = $resp->get('folders')->get('folder'); if ($folders) { + $special = $this->get_special_use_folders(); foreach($folders as $folder) { $id = $folder->get('folderId')->get('id'); $name = $folder->get('displayName'); + if ($only_subscribed && in_array($id, $unsubscribed_folders)) { + continue; + } $result[$id] = array( 'id' => $id, 'parent' => null, // TODO @@ -78,8 +82,9 @@ public function get_folders($folder = null) { 'noselect' => false, 'can_have_kids' => true, 'has_kids' => $folder->get('childFolderCount') > 0, - 'special' => true, - 'clickable' => true + 'special' => in_array($id, $special), + 'clickable' => true, + 'subscribed' => ! in_array($id, $unsubscribed_folders), ); } } @@ -87,7 +92,7 @@ public function get_folders($folder = null) { } public function get_special_use_folders($folder = false) { - $special = array_filter([ + $special = [ 'trash' => Enumeration\DistinguishedFolderIdNameType::DELETED, 'sent' => Enumeration\DistinguishedFolderIdNameType::SENT, 'flagged' => false, @@ -95,7 +100,16 @@ public function get_special_use_folders($folder = false) { 'junk' => Enumeration\DistinguishedFolderIdNameType::JUNK, 'archive' => false, // TODO: check if Enumeration\DistinguishedFolderIdNameType::ARCHIVEMSGFOLDERROOT should be used - it is outside of MESSAGE_ROOT, however. 'drafts' => Enumeration\DistinguishedFolderIdNameType::DRAFTS, - ]); + ]; + foreach ($special as $type => $folderId) { + if ($folderId) { + $distinguishedFolder = $this->api->getFolderByDistinguishedId($folderId); + if ($distinguishedFolder) { + $special[$type] = $distinguishedFolder->get('folderId')->get('id'); + } + } + } + $special = array_filter($special); if (isset($special[$folder])) { return [$folder => $special[$folder]]; } else { diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index d4c9cf742a..e649fe3313 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -23,13 +23,17 @@ class Hm_IMAP_List { use Hm_Server_List; public static $use_cache = true; + protected static $user_config; + protected static $session; public static function init($user_config, $session) { self::initRepo('imap_servers', $user_config, $session, self::$server_list); + self::$user_config = $user_config; + self::$session = $session; } public static function service_connect($id, $server, $user, $pass, $cache=false) { - self::$server_list[$id]['object'] = new Hm_Mailbox(); + self::$server_list[$id]['object'] = new Hm_Mailbox($id, self::$user_config, self::$session); if (self::$use_cache && $cache && is_array($cache)) { self::$server_list[$id]['object']->load_cache($cache, 'array'); } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index e9d390ede9..5c08794caf 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -18,6 +18,16 @@ class Hm_Mailbox { protected $connection; protected $selected_folder; + protected $server_id; + protected $user_config; + protected $session; + + public function __construct($server_id, $user_config, $session) { + $this->server_id = $server_id; + $this->user_config = $user_config; + $this->session = $session; + } + public function connect(array $config) { if (array_key_exists('type', $config) && $config['type'] == 'jmap') { $this->connection = new Hm_JMAP(); @@ -139,7 +149,25 @@ public function folder_subscription($folder, $action) { if ($this->is_imap()) { return $this->connection->mailbox_subscription($folder, $action); } else { - // TODO: EWS + // emulate folder subscription via settings + $config = $this->user_config->get('unsubscribed_folders'); + if (! isset($config[$this->server_id])) { + $config[$this->server_id] = []; + } + if ($action) { + $index = array_search($folder, $config[$this->server_id]); + if ($index !== false) { + unset($config[$this->server_id][$index]); + } + } else { + if (! in_array($folder, $config[$this->server_id])) { + $config[$this->server_id][] = $folder; + } + } + $this->user_config->set('unsubscribed_folders', $config); + $this->session->set('user_data', $this->user_config->dump()); + $this->session->record_unsaved('Folder subscription updated'); + return true; } } @@ -151,7 +179,7 @@ public function get_folders($only_subscribed = false) { return $this->connection->get_mailbox_list($only_subscribed); } else { // TODO: EWS only_subscribed - return $this->connection->get_folders(); + return $this->connection->get_folders(null, $only_subscribed, $this->user_config->get('unsubscribed_folders')[$this->server_id] ?? []); } } @@ -163,7 +191,7 @@ public function get_subfolders($folder, $only_subscribed = false, $with_input = return $this->connection->get_folder_list_by_level($folder, $only_subscribed, $with_input); } else { // TODO: EWS only_subscribed and with_input - return $this->connection->get_folders($folder); + return $this->connection->get_folders($folder, $only_subscribed, $this->user_config->get('unsubscribed_folders')[$this->server_id] ?? []); } } diff --git a/modules/imap_folders/modules.php b/modules/imap_folders/modules.php index f766e5d815..00e2cec115 100644 --- a/modules/imap_folders/modules.php +++ b/modules/imap_folders/modules.php @@ -346,7 +346,7 @@ public function process() { $folder = hex2bin($form['folder']); $success = $mailbox->folder_subscription($folder, $form['subscription_state']); if ($success) { - Hm_Msgs::add(sprintf('%s to %s', $form['subscription_state']? 'Subscribed': 'Unsubscribed', $folder)); + Hm_Msgs::add(sprintf('%s to %s', $form['subscription_state']? 'Subscribed': 'Unsubscribed', $mailbox->get_folder_name($folder))); $this->cache->del('imap_folders_imap_'.$imap_server_id.'_'); } else { Hm_Msgs::add(sprintf('ERRAn error occurred %s to %s', $form['subscription_state']? 'subscribing': 'unsubscribing', $folder)); From 0167a2a65ec399dfaf837da99a78ec5313dbe0cc Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 22 Oct 2024 10:02:45 +0300 Subject: [PATCH 11/35] EWS: child folders handling and subscription input handling --- modules/imap/hm-ews.php | 30 ++++++++++++++++++++---------- modules/imap/hm-mailbox.php | 3 +-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index d22ccded88..d1f4460e62 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -44,7 +44,7 @@ public function authed() { return $this->authed; } - public function get_folders($folder = null, $only_subscribed = false, $unsubscribed_folders = []) { + public function get_folders($folder = null, $only_subscribed = false, $unsubscribed_folders = [], $with_input = false) { $result = []; if (empty($folder)) { $folder = new Type\DistinguishedFolderIdType(Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT); @@ -52,7 +52,7 @@ public function get_folders($folder = null, $only_subscribed = false, $unsubscri $folder = new Type\FolderIdType($folder); } $request = array( - 'Traversal' => 'Deep', + 'Traversal' => 'Shallow', 'FolderShape' => array( 'BaseShape' => 'AllProperties', ), @@ -62,6 +62,9 @@ public function get_folders($folder = null, $only_subscribed = false, $unsubscri $folders = $resp->get('folders')->get('folder'); if ($folders) { $special = $this->get_special_use_folders(); + if ($folders instanceof Type\FolderType) { + $folders = [$folders]; + } foreach($folders as $folder) { $id = $folder->get('folderId')->get('id'); $name = $folder->get('displayName'); @@ -70,20 +73,21 @@ public function get_folders($folder = null, $only_subscribed = false, $unsubscri } $result[$id] = array( 'id' => $id, - 'parent' => null, // TODO - 'delim' => false, // TODO - check, might be IMAP-specific + 'parent' => $folder->get('parentFolderId')->get('id'), + 'delim' => false, 'name' => $name, - 'name_parts' => [], // TODO - check, might be IMAP-specific + 'name_parts' => [], 'basename' => $name, 'realname' => $name, - 'namespace' => '', // TODO - check, might be IMAP-specific + 'namespace' => '', // TODO - flags 'marked' => false, 'noselect' => false, 'can_have_kids' => true, 'has_kids' => $folder->get('childFolderCount') > 0, + 'children' => $folder->get('childFolderCount'), 'special' => in_array($id, $special), - 'clickable' => true, + 'clickable' => ! $with_input && ! in_array($id, $unsubscribed_folders), 'subscribed' => ! in_array($id, $unsubscribed_folders), ); } @@ -140,10 +144,12 @@ public function get_folder_status($folder) { public function create_folder($folder, $parent = null) { if (empty($parent)) { - $parent = Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT; + $parent = new Type\DistinguishedFolderIdType(Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT); + } else { + $parent = new Type\FolderIdType($parent); } try { - return $this->api->createFolders([$folder], new Type\DistinguishedFolderIdType($parent)); + return $this->api->createFolders([$folder], $parent); } catch(Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; @@ -203,13 +209,17 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $l // TODO: sort, pagination, search $request = Type::buildFromArray($request); $result = $this->ews->FindItem($request); + $messages = $result->get('items')->get('message') ?? []; $itemIds = array_map(function($msg) { return $msg->get('itemId')->get('id'); - }, $result->get('items')->get('message')); + }, $messages); return [$result->get('totalItemsInView'), $this->get_message_list($itemIds)]; } public function get_message_list($itemIds) { + if (empty($itemIds)) { + return []; + } $request = array( 'ItemShape' => array( 'BaseShape' => 'AllProperties' diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 5c08794caf..775bb57237 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -190,8 +190,7 @@ public function get_subfolders($folder, $only_subscribed = false, $with_input = if ($this->is_imap()) { return $this->connection->get_folder_list_by_level($folder, $only_subscribed, $with_input); } else { - // TODO: EWS only_subscribed and with_input - return $this->connection->get_folders($folder, $only_subscribed, $this->user_config->get('unsubscribed_folders')[$this->server_id] ?? []); + return $this->connection->get_folders($folder, $only_subscribed, $this->user_config->get('unsubscribed_folders')[$this->server_id] ?? [], $with_input); } } From 0eb2669f3c6619655b204e799d01d3c94228444c Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 22 Oct 2024 11:33:11 +0300 Subject: [PATCH 12/35] EWS: currently selected folder state --- modules/imap/hm-mailbox.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 775bb57237..00a5d6bada 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -17,6 +17,7 @@ class Hm_Mailbox { protected $type; protected $connection; protected $selected_folder; + protected $folder_state; protected $server_id; protected $user_config; @@ -198,8 +199,7 @@ public function get_folder_state() { if ($this->is_imap()) { return $this->connection->folder_state; } else { - // TODO: check EWS - return true; + return $this->folder_state; } } @@ -526,8 +526,8 @@ protected function select_folder($folder) { return false; } } else { - $result = $this->get_folder_status($folder); - $this->selected_folder = ['id' => $folder, 'name' => $result['name'], 'detail' => []]; + $this->folder_state = $this->get_folder_status($folder); + $this->selected_folder = ['id' => $folder, 'name' => $this->folder_state['name'], 'detail' => []]; } return true; } From 003ab9c2c1293c2926dc830a71deb624eb386bd6 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 22 Oct 2024 15:06:23 +0300 Subject: [PATCH 13/35] EWS: flagged/answered flags, search, sort and paginate for message list --- modules/imap/hm-ews.php | 154 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index d1f4460e62..9cfcccc75f 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -28,6 +28,11 @@ class Hm_EWS { protected $api; protected $authed = false; + // Extended property tags and their values defined in MS-OXOFLAG, MS-OXPROPS, MS-OXOMSG, MS-OXCMSG specs + const PID_TAG_FLAG_STATUS = 0x1090; + const PID_TAG_ICON_INDEX = 0x1080; + const PID_TAG_ICON_REPLIED = 0x00000105; + public function connect(array $config) { try { $this->ews = ExchangeWebServices::fromUsernameAndPassword($config['server'], $config['username'], $config['password'], ['version' => ExchangeWebServices::VERSION_2016]); @@ -204,12 +209,127 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $l 'ItemShape' => array( 'BaseShape' => 'IdOnly' ), + 'IndexedPageItemView' => [ + 'MaxEntriesReturned' => $limit, + 'Offset' => $offset, + 'BasePoint' => 'Beginning', + ], 'ParentFolderIds' => $folder->toArray(true) ); - // TODO: sort, pagination, search + if (! empty($sort)) { + switch ($sort) { + case 'ARRIVAL': + $fieldURI = 'item:DateTimeCreated'; + break; + case 'DATE': + $fieldURI = 'item:DateTimeReceived'; + break; + case 'CC': + // TODO: figure out a way to sort by something not availalbe in FindItem operation + $fieldURI = null; + break; + case 'TO': + // TODO: figure out a way to sort by something not availalbe in FindItem operation + $fieldURI = null; + break; + case 'SUBJECT': + $fieldURI = 'item:Subject'; + break; + case 'FROM': + $fieldURI = 'message:From'; + break; + case 'SIZE': + $fieldURI = 'item:Size'; + break; + default: + $fieldURI = null; + } + if ($fieldURI) { + $request['SortOrder'] = [ + 'FieldOrder' => [ + 'Order' => $reverse ? 'Descending' : 'Ascending', + 'FieldURI' => [ + 'FieldURI' => $fieldURI, + ], + ] + ]; + } + } + $qs = []; + if (! empty($keyword)) { + $qs[] = $keyword; + } + switch ($flag_filter) { + case 'UNSEEN': + $qs[] = 'isRead:false'; + break; + case 'UNSEEN': + $qs[] = 'isRead:true'; + break; + // TODO: + case 'FLAGGED': + $qs[] = 'isFlagged:true'; + break; + case 'UNFLAGGED': + $qs[] = 'isFlagged:false'; + break; + case 'ANSWERED': + $request['Restriction'] = [ + 'IsEqualTo' => [ + 'ExtendedFieldURI' => [ + 'PropertyTag' => self::PID_TAG_ICON_INDEX, + 'PropertyType' => 'Integer', + ], + 'FieldURIOrConstant' => [ + 'Constant' => ['Value' => self::PID_TAG_ICON_REPLIED], + ], + ], + ]; + break; + case 'UNANSWERED': + $request['Restriction'] = [ + 'IsNotEqualTo' => [ + 'ExtendedFieldURI' => [ + 'PropertyTag' => self::PID_TAG_ICON_INDEX, + 'PropertyType' => 'Integer', + ], + 'FieldURIOrConstant' => [ + 'Constant' => ['Value' => self::PID_TAG_ICON_REPLIED], + ], + ], + ]; + break; + break; + case 'ALL': + default: + // noop + } + if ($qs && empty($request['Restriction'])) { + $request['QueryString'] = implode(' ', $qs); + } elseif ($keyword && ! empty($request['Restriction'])) { + $restriction = ['And' => $request['Restriction']]; + $restriction['And']['Or'] = [ + [ + 'Contains' => [ + 'FieldURI' => ['FieldURI' => 'item:Subject'], + 'Constant' => ['Value' => $keyword], + ], + ], + [ + 'Contains' => [ + 'FieldURI' => ['FieldURI' => 'item:Body'], + 'Constant' => ['Value' => $keyword], + ], + ], + ]; + $request['Restriction'] = $restriction; + } $request = Type::buildFromArray($request); $result = $this->ews->FindItem($request); $messages = $result->get('items')->get('message') ?? []; + if ($messages instanceof Type\MessageType) { + $messages = [$messages]; + } $itemIds = array_map(function($msg) { return $msg->get('itemId')->get('id'); }, $messages); @@ -222,7 +342,19 @@ public function get_message_list($itemIds) { } $request = array( 'ItemShape' => array( - 'BaseShape' => 'AllProperties' + 'BaseShape' => 'AllProperties', + 'AdditionalProperties' => [ + 'ExtendedFieldURI' => [ + [ + 'PropertyTag' => self::PID_TAG_FLAG_STATUS, //check flagged msg + 'PropertyType' => 'Integer', + ], + [ + 'PropertyTag' => self::PID_TAG_ICON_INDEX, // check if replied/answered + 'PropertyType' => 'Integer', + ], + ], + ], ), 'ItemIds' => [ 'ItemId' => array_map(function($id) { @@ -232,9 +364,12 @@ public function get_message_list($itemIds) { ); $request = Type::buildFromArray($request); $result = $this->ews->GetItem($request); + if ($result instanceof Type\MessageType) { + $result = [$result]; + } $messages = []; foreach ($result as $message) { - // TODO: EWS - check \Answered, \Flagged, \Deleted flags + // note about flags: EWS - doesn't support the \Deleted flag $flags = []; if ($message->get('isRead')) { $flags[] = '\\Seen'; @@ -242,6 +377,19 @@ public function get_message_list($itemIds) { if ($message->get('isDraft')) { $flags[] = '\\Draft'; } + if ($extended_properties = $message->get('extendedProperty')) { + if ($extended_properties instanceof Type\ExtendedPropertyType) { + $extended_properties = [$extended_properties]; + } + foreach ($extended_properties as $prop) { + if (hexdec($prop->get('extendedFieldURI')->get('propertyTag')) == self::PID_TAG_FLAG_STATUS && $prop->get('value') > 0) { + $flags[] = '\\Flagged'; + } + if (hexdec($prop->get('extendedFieldURI')->get('propertyTag')) == self::PID_TAG_ICON_INDEX && $prop->get('value') == self::PID_TAG_ICON_REPLIED) { + $flags[] = '\\Answered'; + } + } + } $uid = bin2hex($message->get('itemId')->get('id')); $msg = [ 'uid' => $uid, From f7d11b80959dbeb137205b07531589e9bc3782b1 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Thu, 24 Oct 2024 22:30:26 +0300 Subject: [PATCH 14/35] EWS: message actions - read/unread, flag/unflag --- modules/imap/hm-ews.php | 76 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 9cfcccc75f..fb9fbab2f5 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -14,6 +14,7 @@ use garethp\ews\API\Enumeration; use garethp\ews\API\Exception; use garethp\ews\API\ExchangeWebServices; +use garethp\ews\API\ItemUpdateBuilder; use garethp\ews\API\Type; use garethp\ews\MailAPI; @@ -30,6 +31,7 @@ class Hm_EWS { // Extended property tags and their values defined in MS-OXOFLAG, MS-OXPROPS, MS-OXOMSG, MS-OXCMSG specs const PID_TAG_FLAG_STATUS = 0x1090; + const PID_TAG_FLAG_FLAGGED = 0x00000002; const PID_TAG_ICON_INDEX = 0x1080; const PID_TAG_ICON_REPLIED = 0x00000105; @@ -346,8 +348,8 @@ public function get_message_list($itemIds) { 'AdditionalProperties' => [ 'ExtendedFieldURI' => [ [ - 'PropertyTag' => self::PID_TAG_FLAG_STATUS, //check flagged msg - 'PropertyType' => 'Integer', + 'PropertyTag' => self::PID_TAG_FLAG_STATUS, //check flagged msg + 'PropertyType' => 'Integer', ], [ 'PropertyTag' => self::PID_TAG_ICON_INDEX, // check if replied/answered @@ -432,6 +434,76 @@ public function get_message_list($itemIds) { return $messages; } + public function message_action($action, $itemIds, $folder=false, $keyword=false) { + if (empty($itemIds)) { + return true; + } + if (! is_array($itemIds)) { + $itemIds = [$itemIds]; + } + switch ($action) { + case 'READ': + $change = ItemUpdateBuilder::buildUpdateItemChanges('Message', 'message', ['IsRead' => true]); + break; + case 'UNREAD': + $change = ItemUpdateBuilder::buildUpdateItemChanges('Message', 'message', ['IsRead' => false]); + break; + case 'FLAG': + $change = [ + 'SetItemField' => [ + 'ExtendedFieldURI' => [ + 'PropertyTag' => self::PID_TAG_FLAG_STATUS, + 'PropertyType' => 'Integer', + ], + 'Message' => [ + 'ExtendedProperty' => [ + 'ExtendedFieldURI' => [ + 'PropertyTag' => self::PID_TAG_FLAG_STATUS, + 'PropertyType' => 'Integer', + ], + 'Value' => self::PID_TAG_FLAG_FLAGGED, + ], + ], + ], + ]; + break; + case 'UNFLAG': + $change = [ + 'DeleteItemField' => [ + 'ExtendedFieldURI' => [ + 'PropertyTag' => self::PID_TAG_FLAG_STATUS, + 'PropertyType' => 'Integer', + ], + ], + ]; + break; + case 'ARCHIVE': + case 'ANSWERED': + case 'DELETE': + case 'UNDELETE': + case 'CUSTOM': + // TODO: unsupported out of the box, we can emulate via custom extended properties + break; + default: + $change = null; + } + + $changes = ['ItemChange' => []]; + foreach ($itemIds as $itemId) { + $changes['ItemChange'][] = [ + 'ItemId' => (new Type\ItemIdType(hex2bin($itemId)))->toArray(), + 'Updates' => $change, + ]; + } + $items = $this->api->updateItems($changes); + + if ($items) { + return true; + } else { + return false; + } + } + public function get_message_headers($itemId) { $request = array( 'ItemShape' => array( From 0183f4bb623eace1ad2a95123b9183da13ca07d5 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 25 Oct 2024 09:56:00 +0300 Subject: [PATCH 15/35] EWS: message actions: archive, delete, move, copy --- modules/imap/handler_modules.php | 68 ++++++++++-------- modules/imap/hm-ews.php | 114 ++++++++++++++++++++++++++----- modules/imap/hm-mailbox.php | 18 +++-- 3 files changed, 145 insertions(+), 55 deletions(-) diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index 672ebe983b..bae3098e42 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -846,51 +846,61 @@ public function process() { } $archive_folder = false; + $form_folder = hex2bin($form['folder']); $errors = 0; + $status = null; $specials = get_special_folders($this, $form['imap_server_id']); if (array_key_exists('archive', $specials) && $specials['archive']) { $archive_folder = $specials['archive']; } - if (!$archive_folder) { - Hm_Msgs::add('No archive folder configured for this IMAP server'); - $errors++; - } $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); - if (! $errors && $mailbox && $mailbox->authed()) { - $archive_exists = count($mailbox->get_folder_status($archive_folder)); - if (!$archive_exists) { - Hm_Msgs::add('Configured archive folder for this IMAP server does not exist'); + if ($mailbox && ! $mailbox->is_imap() && empty($archive_folder)) { + // EWS supports archiving to user archive folders + $status = $mailbox->message_action($form_folder, 'ARCHIVE', array($form['imap_msg_uid'])); + } else { + if (!$archive_folder) { + Hm_Msgs::add('No archive folder configured for this IMAP server'); $errors++; } - $form_folder = hex2bin($form['folder']); + if (! $errors && $mailbox && $mailbox->authed()) { + $archive_exists = count($mailbox->get_folder_status($archive_folder)); + if (!$archive_exists) { + Hm_Msgs::add('Configured archive folder for this IMAP server does not exist'); + $errors++; + } - /* path according to original option setting */ - if ($this->user_config->get('original_folder_setting', false)) { - $archive_folder .= '/' . $form_folder; - if (!count($mailbox->get_folder_status($archive_folder))) { - if (! $mailbox->create_folder($archive_folder)) { - $debug = $mailbox->get_debug(); - if (! empty($debug['debug'])) { - Hm_Msgs::add('ERR' . array_pop($debug['debug'])); - } else { - Hm_Msgs::add('ERRCould not create configured archive folder for the original folder of the message'); + /* path according to original option setting */ + if ($this->user_config->get('original_folder_setting', false)) { + $archive_folder .= '/' . $form_folder; + if (!count($mailbox->get_folder_status($archive_folder))) { + if (! $mailbox->create_folder($archive_folder)) { + $debug = $mailbox->get_debug(); + if (! empty($debug['debug'])) { + Hm_Msgs::add('ERR' . array_pop($debug['debug'])); + } else { + Hm_Msgs::add('ERRCould not create configured archive folder for the original folder of the message'); + } + $errors++; } - $errors++; } } - } - /* try to move the message */ - if (! $errors && $mailbox->message_action($form_folder, 'MOVE', array($form['imap_msg_uid']), $archive_folder)) { - Hm_Msgs::add("Message archived"); - } - else { - Hm_Msgs::add('ERRAn error occurred archiving the message'); + /* try to move the message */ + if (! $errors) { + $status = $mailbox->message_action($form_folder, 'MOVE', array($form['imap_msg_uid']), $archive_folder); + } } } + + if ($status) { + Hm_Msgs::add("Message archived"); + } else { + Hm_Msgs::add('ERRAn error occurred archiving the message'); + } + $this->save_hm_msgs(); } } @@ -1073,7 +1083,7 @@ public function process() { if (array_key_exists('trash', $specials)) { if ($specials['trash']) { $trash_folder = $specials['trash']; - } else { + } elseif ($mailbox->is_imap()) { Hm_Msgs::add(sprintf('ERRNo trash folder configured for %s', $server_details['name'])); } } @@ -1082,7 +1092,7 @@ public function process() { if(array_key_exists('archive', $specials)) { if($specials['archive']) { $archive_folder = $specials['archive']; - } else { + } elseif ($mailbox->is_imap()) { Hm_Msgs::add(sprintf('ERRNo archive folder configured for %s', $server_details['name'])); } } diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index fb9fbab2f5..1b5e847bf2 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -399,7 +399,7 @@ public function get_message_list($itemIds) { 'internal_date' => $message->get('dateTimeCreated'), 'size' => $message->get('size'), 'date' => $message->get('dateTimeReceived'), - 'from' => $message->get('sender')->get('mailbox')->get('name') . ' <' . $message->get('from')->get('mailbox')->get('emailAddress') . '>', + 'from' => $this->extract_mailbox($message->get('from')), 'to' => $this->extract_mailbox($message->get('toRecipients')), 'subject' => $message->get('subject'), 'content-type' => null, @@ -442,6 +442,14 @@ public function message_action($action, $itemIds, $folder=false, $keyword=false) $itemIds = [$itemIds]; } switch ($action) { + case 'ARCHIVE': + return $this->archive_items($itemIds); + case 'DELETE': + return $this->delete_items($itemIds); + case 'COPY': + return $this->copy_items($itemIds, $folder); + case 'MOVE': + return $this->move_items($itemIds, $folder); case 'READ': $change = ItemUpdateBuilder::buildUpdateItemChanges('Message', 'message', ['IsRead' => true]); break; @@ -477,31 +485,31 @@ public function message_action($action, $itemIds, $folder=false, $keyword=false) ], ]; break; - case 'ARCHIVE': case 'ANSWERED': - case 'DELETE': case 'UNDELETE': case 'CUSTOM': // TODO: unsupported out of the box, we can emulate via custom extended properties + $change = null; break; + case 'EXPUNGE': + // IMAP-only, not supported or needed by EWS + return true; default: $change = null; } - $changes = ['ItemChange' => []]; - foreach ($itemIds as $itemId) { - $changes['ItemChange'][] = [ - 'ItemId' => (new Type\ItemIdType(hex2bin($itemId)))->toArray(), - 'Updates' => $change, - ]; + if ($change) { + $changes = ['ItemChange' => []]; + foreach ($itemIds as $itemId) { + $changes['ItemChange'][] = [ + 'ItemId' => (new Type\ItemIdType(hex2bin($itemId)))->toArray(), + 'Updates' => $change, + ]; + } + return $this->api->updateItems($changes); } - $items = $this->api->updateItems($changes); - if ($items) { - return true; - } else { - return false; - } + return false; } public function get_message_headers($itemId) { @@ -715,8 +723,10 @@ protected function extract_mailbox($data) { $result[] = $this->extract_mailbox($mailbox); } return $result; - } elseif (is_object($data)) { + } elseif (is_object($data) && $data->Mailbox) { return $data->Mailbox->get('name') . ' <' . $data->Mailbox->get('emailAddress') . '>'; + } elseif (is_object($data) && $data->get('mailbox')) { + return $data->get('mailbox')->get('name') . ' <' . $data->get('mailbox')->get('emailAddress') . '>'; } else { return (string) $data; } @@ -727,4 +737,76 @@ protected function is_distinguished_folder($folder) { $constants = $oClass->getConstants(); return in_array($folder, $constants); } + + protected function archive_items($itemIds) { + // TODO: update underlying lib to support ArchiveItem and newer services.wsdl file (better get the wsdl from the actual server) + $request = [ + 'ArchiveSourceFolderId' => (new Type\DistinguishedFolderIdType(Enumeration\DistinguishedFolderIdNameType::ARCHIVE_INBOX))->toArray(true), + 'ItemIds' => [ + 'ItemId' => array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds), + ] + ]; + $request = Type::buildFromArray($request); + try { + $result = $this->ews->ArchiveItem($request); + } catch (Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + $result = false; + } + return $result; + } + + protected function delete_items($itemIds) { + try { + // TODO: use HardDelete type for items stored in deleted items or trash folder + $result = $this->api->deleteItems(array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds)); + } catch (Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + $result = false; + } + return $result; + } + + protected function copy_items($itemIds, $folder) { + $request = [ + 'ToFolderId' => (new Type\FolderIdType($folder))->toArray(true), + 'ItemIds' => [ + 'ItemId' => array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds), + ] + ]; + $request = Type::buildFromArray($request); + try { + $result = $this->ews->CopyItem($request); + } catch (Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + $result = false; + } + return $result; + } + + protected function move_items($itemIds, $folder) { + $request = [ + 'ToFolderId' => (new Type\FolderIdType($folder))->toArray(true), + 'ItemIds' => [ + 'ItemId' => array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds), + ], + 'ReturnNewItemIds' => false, + ]; + $request = Type::buildFromArray($request); + try { + $result = $this->ews->MoveItem($request); + } catch (Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + $result = false; + } + return $result; + } } diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 00a5d6bada..6d8eb7927d 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -341,20 +341,18 @@ public function delete_message($folder, $msg_id, $trash_folder) { if (! $this->select_folder($folder)) { return; } - if ($this->is_imap()) { - if ($trash_folder && $trash_folder != $folder) { - if ($this->connection->message_action('MOVE', [$msg_id], $trash_folder)) { - return true; - } + if ($trash_folder && $trash_folder != $folder) { + if ($this->connection->message_action('MOVE', [$msg_id], $trash_folder)) { + return true; } - else { - if ($this->connection->message_action('DELETE', array($msg_id))) { + } + else { + if ($this->connection->message_action('DELETE', array($msg_id))) { + if ($this->is_imap()) { $this->connection->message_action('EXPUNGE', array($msg_id)); - return true; } + return true; } - } else { - // TODO: EWS } return false; } From 1712504012fc34cf2235a4a1f90ff3f8b2a44eb0 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 30 Oct 2024 18:10:52 +0200 Subject: [PATCH 16/35] exception handling fixes and archive item fixes --- modules/imap/hm-ews.php | 60 ++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 1b5e847bf2..4ac41b65e2 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -144,7 +144,11 @@ public function get_folder_status($folder) { 'recent' => false, 'unseen' => $result->get('unreadCount'), ]; - } catch (Exception $e) { + } catch (Exception\ExchangeException $e) { + // since this is used for missing folders check, we skip error reporting + return []; + } catch (\Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); return []; } } @@ -157,7 +161,7 @@ public function create_folder($folder, $parent = null) { } try { return $this->api->createFolders([$folder], $parent); - } catch(Exception $e) { + } catch(\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; } @@ -189,7 +193,7 @@ public function rename_folder($folder, $new_name, $parent = null) { $resp = $this->ews->UpdateFolder($request); // TODO: EWS: resolve internal server error issue and return status return true; - } catch (Exception $e) { + } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; } @@ -198,7 +202,7 @@ public function rename_folder($folder, $new_name, $parent = null) { public function delete_folder($folder) { try { return $this->api->deleteFolder(new Type\FolderIdType($folder)); - } catch(Exception $e) { + } catch(\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; } @@ -739,19 +743,20 @@ protected function is_distinguished_folder($folder) { } protected function archive_items($itemIds) { - // TODO: update underlying lib to support ArchiveItem and newer services.wsdl file (better get the wsdl from the actual server) + $itemIds = array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds); + $parent = $this->get_parent_folder_of_items($itemIds); $request = [ - 'ArchiveSourceFolderId' => (new Type\DistinguishedFolderIdType(Enumeration\DistinguishedFolderIdNameType::ARCHIVE_INBOX))->toArray(true), + 'ArchiveSourceFolderId' => (new Type\FolderIdType($parent))->toArray(true), 'ItemIds' => [ - 'ItemId' => array_map(function($itemId) { - return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); - }, $itemIds), + 'ItemId' => $itemIds, ] ]; $request = Type::buildFromArray($request); try { $result = $this->ews->ArchiveItem($request); - } catch (Exception $e) { + } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); $result = false; } @@ -764,7 +769,7 @@ protected function delete_items($itemIds) { $result = $this->api->deleteItems(array_map(function($itemId) { return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); }, $itemIds)); - } catch (Exception $e) { + } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); $result = false; } @@ -783,7 +788,7 @@ protected function copy_items($itemIds, $folder) { $request = Type::buildFromArray($request); try { $result = $this->ews->CopyItem($request); - } catch (Exception $e) { + } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); $result = false; } @@ -803,10 +808,39 @@ protected function move_items($itemIds, $folder) { $request = Type::buildFromArray($request); try { $result = $this->ews->MoveItem($request); - } catch (Exception $e) { + } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); $result = false; } return $result; } + + protected function get_parent_folder_of_items($itemIdTypes) { + $folder = null; + $request = [ + 'ItemShape' => [ + 'BaseShape' => 'IdOnly', + 'AdditionalProperties' => [ + 'FieldURI' => ['FieldURI' => 'item:ParentFolderId'], + ], + ], + 'ItemIds' => [ + 'ItemId' => $itemIdTypes, + ], + ]; + $request = Type::buildFromArray($request); + $result = $this->ews->GetItem($request); + if ($result instanceof Type\MessageType) { + $result = [$result]; + } + foreach ($result as $message) { + if (! $folder) { + $folder = $message->get('ParentFolderId')->get('id'); + } + if ($folder != $message->get('ParentFolderId')->get('id')) { + throw new Exception('Parent folder of items stored in different folders cannot be determinted.'); + } + } + return $folder; + } } From 0690388dcc0b7f373c7872f7e877ead367969b5d Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 30 Oct 2024 18:23:38 +0200 Subject: [PATCH 17/35] EWS: fix folder pagination and links --- modules/imap/hm-mailbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 6d8eb7927d..e088df461a 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -234,7 +234,7 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, $messages = $this->connection->get_mailbox_page($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); } else { $messages = $this->connection->get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); - $folder = $this->selected_folder['name']; + $folder = $this->selected_folder['id']; } foreach ($messages[1] as &$msg) { $msg['folder'] = bin2hex($folder); From 3b1adbddfb9f7b15cf6683a5de564ed8ac0dacca Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Thu, 31 Oct 2024 08:49:49 +0200 Subject: [PATCH 18/35] EWS: search and combined folders, custom sources --- modules/core/message_list_functions.php | 10 ++--- modules/imap/functions.php | 17 +++++--- modules/imap/hm-ews.php | 53 ++++++++++++++++++++++--- modules/imap/hm-mailbox.php | 5 ++- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/modules/core/message_list_functions.php b/modules/core/message_list_functions.php index eac7e95a2f..af6df61eed 100644 --- a/modules/core/message_list_functions.php +++ b/modules/core/message_list_functions.php @@ -452,17 +452,17 @@ function list_sources($sources, $output_mod) { if (array_key_exists('nodisplay', $src) && $src['nodisplay']) { continue; } - if ($src['type'] == 'imap' && !array_key_exists('folder', $src)) { - $folder = '_INBOX'; + if ($src['type'] == 'imap' && !array_key_exists('folder_name', $src)) { + $folder = 'INBOX'; } - elseif (!array_key_exists('folder', $src)) { + elseif (!array_key_exists('folder_name', $src)) { $folder = ''; } else { - $folder = '_'.hex2bin($src['folder']); + $folder = $src['folder_name']; } $res .= '
'.$output_mod->html_safe($src['type']).' '.$output_mod->html_safe($src['name']); - $res .= ' '.$output_mod->html_safe(str_replace('_', '', $folder)); + $res .= ' '.$output_mod->html_safe($folder); $res .= '
'; } $res .= '
'; diff --git a/modules/imap/functions.php b/modules/imap/functions.php index e518afc6a5..cb57622825 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -29,13 +29,13 @@ function imap_sources($callback, $mod, $folder = 'sent') { } $folders = get_special_folders($mod, $index); if (array_key_exists($folder, $folders) && $folders[$folder]) { - $sources[] = array('callback' => $callback, 'folder' => bin2hex($folders[$folder]), 'type' => 'imap', 'name' => $vals['name'], 'id' => $index); + $sources[] = array('callback' => $callback, 'folder' => bin2hex($folders[$folder]), 'folder_name' => $folders[$folder], 'type' => $vals['type'] ?? 'imap', 'name' => $vals['name'], 'id' => $index); } elseif ($inbox) { - $sources[] = array('callback' => $callback, 'folder' => bin2hex('INBOX'), 'type' => 'imap', 'name' => $vals['name'], 'id' => $index); + $sources[] = array('callback' => $callback, 'folder' => bin2hex('INBOX'), 'folder_name' => 'INBOX', 'type' => $vals['type'] ?? 'imap', 'name' => $vals['name'], 'id' => $index); } else { - $sources[] = array('callback' => $callback, 'folder' => bin2hex('SPECIAL_USE_CHECK'), 'nodisplay' => true, 'type' => 'imap', 'name' => $vals['name'], 'id' => $index); + $sources[] = array('callback' => $callback, 'folder' => bin2hex('SPECIAL_USE_CHECK'), 'folder_name' => 'SPECIAL_USE_CHECK', 'nodisplay' => true, 'type' => $vals['type'] ?? 'imap', 'name' => $vals['name'], 'id' => $index); } } return $sources; @@ -58,7 +58,7 @@ function imap_data_sources($callback, $custom=array()) { if (!array_key_exists('user', $vals)) { continue; } - $sources[] = array('callback' => $callback, 'folder' => bin2hex('INBOX'), 'type' => 'imap', 'name' => $vals['name'], 'id' => $index); + $sources[] = array('callback' => $callback, 'folder' => bin2hex('INBOX'), 'folder_name' => 'INBOX', 'type' => $vals['type'] ?? 'imap', 'name' => $vals['name'], 'id' => $index); } foreach ($custom as $path => $type) { $parts = explode('_', $path, 3); @@ -67,7 +67,14 @@ function imap_data_sources($callback, $custom=array()) { if ($type == 'add') { $details = Hm_IMAP_List::dump($parts[1]); if ($details) { - $sources[] = array('callback' => $callback, 'folder' => $parts[2], 'type' => 'imap', 'name' => $details['name'], 'id' => $parts[1]); + $folder_name = $parts[2]; + if (! empty($details['type']) && $details['type'] == 'ews') { + $mailbox = Hm_IMAP_List::get_connected_mailbox($details['id']); + if ($mailbox && $mailbox->authed()) { + $folder_name = $mailbox->get_folder_name(hex2bin($folder_name)); + } + } + $sources[] = array('callback' => $callback, 'folder' => $parts[2], 'folder_name' => $folder_name, 'type' => $details['type'] ?? 'imap', 'name' => $details['name'], 'id' => $parts[1]); } } elseif ($type == 'remove') { diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 4ac41b65e2..acff2a277e 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -208,8 +208,18 @@ public function delete_folder($folder) { } } - public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders) { - $folder = new Type\FolderIdType($folder); + /** + * Performs an EWS search using FindItem operation and supplies sorting + pagination arguments. + * Search can be perfomed using Advanced Query Syntax when keyword is an array containing terms + * searching in specific fields (e.g. advanced search) or Restrictions list when requesting + * filtering by extended properties as answered or unanswered emails. + */ + public function search($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders) { + if ($this->is_distinguished_folder(strtolower($folder))) { + $folder = new Type\DistinguishedFolderIdType(strtolower($folder)); + } else { + $folder = new Type\FolderIdType($folder); + } $request = array( 'Traversal' => 'Shallow', 'ItemShape' => array( @@ -262,14 +272,42 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $l } } $qs = []; - if (! empty($keyword)) { + if (is_array($keyword)) { + foreach ($keyword as $term) { + switch ($term[0]) { + case 'SINCE': + $qs[] = "Received:>$term[1]"; + break; + case 'FROM': + $qs[] = "From:($term[1])"; + break; + case 'TO': + $qs[] = "To:($term[1])"; + break; + case 'CC': + $qs[] = "Cc:($term[1])"; + break; + case 'TEXT': + $qs[] = "(Subject:($term[1]) OR Body:($term[1]))"; + break; + case 'BODY': + $qs[] = "Body:($term[1])"; + break; + case 'SUBJECT': + $qs[] = "Subject:($term[1])"; + break; + default: + // TODO: check for other types of terms + } + } + } elseif (! empty($keyword)) { $qs[] = $keyword; } switch ($flag_filter) { case 'UNSEEN': $qs[] = 'isRead:false'; break; - case 'UNSEEN': + case 'SEEN': $qs[] = 'isRead:true'; break; // TODO: @@ -339,7 +377,12 @@ public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $l $itemIds = array_map(function($msg) { return $msg->get('itemId')->get('id'); }, $messages); - return [$result->get('totalItemsInView'), $this->get_message_list($itemIds)]; + return [$result->get('totalItemsInView'), $itemIds]; + } + + public function get_messages($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders) { + list ($total, $itemIds) = $this->search($folder, $sort, $reverse, $flag_filter, $offset, $limit, $keyword, $trusted_senders); + return [$total, $this->get_message_list($itemIds)]; } public function get_message_list($itemIds) { diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index e088df461a..781962b4f2 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -179,7 +179,6 @@ public function get_folders($only_subscribed = false) { if ($this->is_imap()) { return $this->connection->get_mailbox_list($only_subscribed); } else { - // TODO: EWS only_subscribed return $this->connection->get_folders(null, $only_subscribed, $this->user_config->get('unsubscribed_folders')[$this->server_id] ?? []); } } @@ -500,7 +499,9 @@ public function search($folder, $target='ALL', $uids=false, $terms=array(), $ese if ($this->is_imap()) { return $this->connection->search($target, $uids, $terms, $esearch, $exclude_deleted, $exclude_auto_bcc, $only_auto_bcc); } else { - // TODO: EWS + // deleted flag, auto-bcc feature - not supported by EWS + list($total, $itemIds) = $this->connection->search($folder, false, false, $target, 0, 9999, $terms, []); + return $itemIds; } } From 0662e94831c62d3bb992b339572fd4d0b8f181ba Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Thu, 31 Oct 2024 14:15:17 +0200 Subject: [PATCH 19/35] ews: skip quotas and caching as unsupported --- modules/imap/hm-mailbox.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/imap/hm-mailbox.php b/modules/imap/hm-mailbox.php index 781962b4f2..f6e8ca59e4 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/imap/hm-mailbox.php @@ -442,7 +442,8 @@ public function get_quota($folder, $root = false) { return $this->connection->get_quota($folder); } } else { - // TODO: EWS + // not supported by EWS + return []; } } @@ -450,7 +451,7 @@ public function get_debug() { if ($this->is_imap()) { return $this->connection->show_debug(true, true, true); } else { - // TODO: EWS + return []; } } @@ -458,7 +459,6 @@ public function use_cache() { if ($this->is_imap()) { return $this->connection->use_cache; } else { - // TODO: check EWS caching return false; } } @@ -467,8 +467,7 @@ public function dump_cache($type = 'string') { if ($this->is_imap()) { return $this->connection->dump_cache($type); } else { - // TODO: check EWS caching - return; + return null; } } From 83f8387edf3b690a927c4ce2aae1108e84156f79 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 1 Nov 2024 15:53:32 +0200 Subject: [PATCH 20/35] EWS: imap/smtp group with profile on save, smtp module full implementation - sending emails via API, fixes for delete/hard-delete items, expunge, removing attachments, folder selection, flags --- lib/servers.php | 4 +- modules/core/handler_modules.php | 2 +- modules/{imap => core}/hm-mailbox.php | 81 +++++-- modules/core/modules.php | 1 + modules/imap/functions.php | 20 +- modules/imap/handler_modules.php | 49 +++-- modules/imap/hm-ews.php | 304 ++++++++++++++++++-------- modules/imap/hm-imap.php | 1 - modules/imap/output_modules.php | 6 +- modules/imap/setup.php | 6 + modules/imap/site.js | 19 +- modules/profiles/functions.php | 6 +- modules/smtp/functions.php | 9 +- modules/smtp/hm-smtp.php | 15 +- modules/smtp/modules.php | 40 ++-- modules/smtp/setup.php | 8 +- modules/smtp/site.js | 2 +- 17 files changed, 382 insertions(+), 191 deletions(-) rename modules/{imap => core}/hm-mailbox.php (87%) diff --git a/lib/servers.php b/lib/servers.php index 1bd5fbf159..9e37fdb967 100644 --- a/lib/servers.php +++ b/lib/servers.php @@ -179,8 +179,8 @@ trait Hm_Server_List { Hm_Repository::get as repo_get; } - public static function init($name, $user_config) { - self::initRepo($name, $user_config, self::$server_list); + public static function init($name, $user_config, $session) { + self::initRepo($name, $user_config, $session, self::$server_list); } /** diff --git a/modules/core/handler_modules.php b/modules/core/handler_modules.php index bb46a4132e..5da9c03597 100644 --- a/modules/core/handler_modules.php +++ b/modules/core/handler_modules.php @@ -1095,7 +1095,7 @@ public function process() { Hm_Msgs::add("ERRSMTP module is not enabled"); return; } - $this->smtp_server_id = connect_to_smtp_server($smtpAddress, $profileName, $smtpPort, $email, $password, $smtpTls, $smtpServerId); + $this->smtp_server_id = connect_to_smtp_server($smtpAddress, $profileName, $smtpPort, $email, $password, $smtpTls, 'smtp', $smtpServerId); if(!isset($this->smtp_server_id)){ Hm_Msgs::add("ERRCould not save server"); return; diff --git a/modules/imap/hm-mailbox.php b/modules/core/hm-mailbox.php similarity index 87% rename from modules/imap/hm-mailbox.php rename to modules/core/hm-mailbox.php index f6e8ca59e4..e00202b39b 100644 --- a/modules/imap/hm-mailbox.php +++ b/modules/core/hm-mailbox.php @@ -5,7 +5,7 @@ * @package modules * @subpackage imap * - * This class hides the implementation details of IMAP, JMAP and EWS connections and provides + * This class hides the implementation details of IMAP, JMAP, SMTP and EWS connections and provides * a common interface to work with a mail account/mailbox. It acts as a bridge to more than one * underlying connections/protocols. */ @@ -13,6 +13,7 @@ class Hm_Mailbox { const TYPE_IMAP = 1; const TYPE_JMAP = 2; const TYPE_EWS = 3; + const TYPE_SMTP = 4; protected $type; protected $connection; @@ -30,19 +31,23 @@ public function __construct($server_id, $user_config, $session) { } public function connect(array $config) { - if (array_key_exists('type', $config) && $config['type'] == 'jmap') { - $this->connection = new Hm_JMAP(); + if (array_key_exists('type', $config) && $config['type'] == 'smtp') { + $this->type = self::TYPE_SMTP; + $this->connection = new Hm_SMTP($config); + return $this->connection->connect(); + } elseif (array_key_exists('type', $config) && $config['type'] == 'jmap') { $this->type = self::TYPE_JMAP; - } - elseif (array_key_exists('type', $config) && $config['type'] == 'ews') { - $this->connection = new Hm_EWS(); + $this->connection = new Hm_JMAP(); + return $this->connection->connect($config); + } elseif (array_key_exists('type', $config) && $config['type'] == 'ews') { $this->type = self::TYPE_EWS; - } - else { - $this->connection = new Hm_IMAP(); + $this->connection = new Hm_EWS(); + return $this->connection->connect($config); + } else { $this->type = self::TYPE_IMAP; + $this->connection = new Hm_IMAP(); + return $this->connection->connect($config); } - return $this->connection->connect($config); } public function get_connection() { @@ -50,7 +55,11 @@ public function get_connection() { } public function is_imap() { - return $this->type !== self::TYPE_EWS; + return $this->type === self::TYPE_IMAP || $this->type === self::TYPE_JMAP; + } + + public function is_smtp() { + return $this->type === self::TYPE_SMTP; } public function server_type() { @@ -67,11 +76,23 @@ public function server_type() { public function authed() { if ($this->is_imap()) { return $this->connection->get_state() == 'authenticated' || $this->connection->get_state() == 'selected'; + } elseif ($this->is_smtp()) { + return $this->connection->state == 'authed'; } else { return $this->connection->authed(); } } + public function state() { + if ($this->is_imap()) { + return $this->connection->get_state(); + } elseif ($this->is_smtp()) { + return $this->connection->state; + } else { + return null; + } + } + public function get_folder_status($folder) { if (! $this->authed()) { return; @@ -331,7 +352,7 @@ public function store_message($folder, $msg, $seen = true, $draft = false) { } } } else { - // TODO: EWS + return $this->connection->store_message($folder, $msg, $seen, $draft); } return false; } @@ -340,16 +361,14 @@ public function delete_message($folder, $msg_id, $trash_folder) { if (! $this->select_folder($folder)) { return; } - if ($trash_folder && $trash_folder != $folder) { + if ($this->is_imap() && $trash_folder && $trash_folder != $folder) { if ($this->connection->message_action('MOVE', [$msg_id], $trash_folder)) { return true; } } else { if ($this->connection->message_action('DELETE', array($msg_id))) { - if ($this->is_imap()) { - $this->connection->message_action('EXPUNGE', array($msg_id)); - } + $this->connection->message_action('EXPUNGE', array($msg_id)); return true; } } @@ -415,7 +434,8 @@ public function remove_attachment($folder, $msg_id, $part_id) { if ($this->is_imap()) { $msg = $this->connection->get_message_content($msg_id, 0, false, false); if ($msg) { - $attachment_id = get_attachment_id_for_mail_parser($this->connection, $msg_id, $part_id); + $struct = $this->connection->get_message_structure($msg_id); + $attachment_id = get_attachment_id_for_mail_parser($struct, $part_id); if ($attachment_id !== false) { $msg = remove_attachment($attachment_id, $msg); if ($this->connection->append_start($folder, mb_strlen($msg))) { @@ -430,7 +450,17 @@ public function remove_attachment($folder, $msg_id, $part_id) { } } } else { - // TODO: EWS + $message = $this->connection->get_mime_message_by_id($msg_id); + $result = $this->connection->get_structured_message($msg_id, false, false); + $struct = $result[0]; + $attachment_id = get_attachment_id_for_mail_parser($struct, $part_id); + if ($attachment_id !== false) { + $message->removeAttachmentPart($attachment_id); + if ($this->connection->store_message($folder, (string) $message)) { + $this->connection->message_action('HARDDELETE', [$msg_id]); + return true; + } + } } } @@ -515,6 +545,21 @@ public function get_message_list($folder, $msg_ids) { } } + public function send_message($from, $recipients, $message, $delivery_receipt = false) { + if ($this->is_smtp()) { + if ($delivery_receipt) { + $from_params = 'RET=HDRS'; + $recipients_params = 'NOTIFY=SUCCESS,FAILURE'; + } else { + $from_params = ''; + $recipients_params = ''; + } + return $this->connection->send_message($from, $recipients, $message, $from_params, $recipients_params); + } else { + return $this->connection->send_message($from, $recipients, $message, $delivery_receipt); + } + } + protected function select_folder($folder) { if ($this->is_imap()) { if (isset($this->connection->selected_mailbox['name']) && $this->connection->selected_mailbox['name'] == $folder) { diff --git a/modules/core/modules.php b/modules/core/modules.php index dd980474e8..8f520565d2 100644 --- a/modules/core/modules.php +++ b/modules/core/modules.php @@ -17,3 +17,4 @@ require APP_PATH.'modules/core/message_list_functions.php'; require APP_PATH.'modules/core/handler_modules.php'; require APP_PATH.'modules/core/output_modules.php'; +require_once APP_PATH.'modules/core/hm-mailbox.php'; diff --git a/modules/imap/functions.php b/modules/imap/functions.php index cb57622825..0c4b56c54b 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -504,20 +504,18 @@ function remove_attachment($att_id, $msg) { /* * ZBateson\MailMimeParser uses 0-based index for attachments * Which mixes embedded images and attachments - * @param Hm_IMAP $imap Imap object - * @param int $msg message id - * @param string $msg_id message part + * @param array $struct Imap structure + * @param string $part_id message part * @return int */ if (!hm_exists('get_attachment_id_for_mail_parser')) { - function get_attachment_id_for_mail_parser($imap, $uid, $msg_id) { + function get_attachment_id_for_mail_parser($struct, $part_id) { $count = -1; $id = false; - $struct = $imap->get_message_structure($uid); foreach ($struct[0]['subs'] as $key => $sub) { if (! empty($sub['file_attributes'])) { $count++; - if ($key == $msg_id && isset($sub['file_attributes']['attachment'])) { + if ($key == $part_id && isset($sub['file_attributes']['attachment'])) { $id = $count; break; } @@ -1067,16 +1065,6 @@ function clear_existing_reply_details($session) { } }} -/** - * @subpackage imap/functions - * @param object $imap imap library object - * @return bool - */ -if (!hm_exists('imap_authed')) { -function imap_authed($imap) { - return is_object($imap) && ($imap->get_state() == 'authenticated' || $imap->get_state() == 'selected'); -}} - /** * @subpackage imap/functions */ diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index bae3098e42..ccd7a0b9a9 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -401,7 +401,7 @@ public function process() { if (count($path) == 3 && $path[0] == 'imap') { $mailbox = Hm_IMAP_List::get_connected_mailbox($path[1], $this->cache); if ($mailbox && $mailbox->authed()) { - $this->out('folder_status', array('imap_'.$path[1].'_'.$path[2] => $mailbox->getFolderState())); + $this->out('folder_status', array('imap_'.$path[1].'_'.$path[2] => $mailbox->get_folder_state())); $mailbox->message_action(hex2bin($path[2]), 'ANSWERED', array($form['compose_msg_uid'])); } } @@ -423,7 +423,7 @@ public function process() { if ($success) { $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); if ($mailbox && $mailbox->authed()) { - $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->getFolderState())); + $this->out('folder_status', array('imap_'.$form['imap_server_id'].'_'.$form['folder'] => $mailbox->get_folder_state())); $mailbox->message_action(hex2bin($form['folder']), 'READ', array($form['imap_msg_uid'])); } } @@ -1101,7 +1101,7 @@ public function process() { foreach ($folders as $folder => $uids) { $status['imap_'.$server.'_'.$folder] = $imap->folder_state; - if ($form['action_type'] == 'delete' && $trash_folder && $trash_folder != hex2bin($folder)) { + if ($mailbox->is_imap() && $form['action_type'] == 'delete' && $trash_folder && $trash_folder != hex2bin($folder)) { if (! $mailbox->message_action(hex2bin($folder), 'MOVE', $uids, $trash_folder)) { $errs++; } @@ -1111,7 +1111,7 @@ public function process() { } } } - elseif ($form['action_type'] == 'archive' && $archive_folder && $archive_folder != hex2bin($folder)) { + elseif ($mailbox->is_imap() && $form['action_type'] == 'archive' && $archive_folder && $archive_folder != hex2bin($folder)) { /* path according to original option setting */ if ($this->user_config->get('original_folder_setting', false)) { $archive_folder .= '/' . hex2bin($folder); @@ -1435,11 +1435,8 @@ public function process() { 'ews_profile_reply_to', 'ews_profile_is_default', )); - // var_dump($success); - // var_dump($form); - // exit; if ($success) { - $server_id = connect_to_imap_server( + $imap_server_id = connect_to_imap_server( $form['ews_server'], $form['ews_profile_name'], null, @@ -1453,20 +1450,29 @@ public function process() { $form['ews_hide_from_c_page'], $form['ews_server_id'], ); - if(empty($server_id)) { + if(empty($imap_server_id)) { Hm_Msgs::add("ERRCould not save server"); return; - }; - // TODO: EWS check if we shouldn't add the same server to smtp server list for the profile - if ($form['ews_create_profile']) { - add_profile($form['ews_profile_name'], $form['ews_profile_signature'], $form['ews_profile_reply_to'], $form['ews_profile_is_default'], $form['ews_email'], $form['ews_server'], $server_id, $server_id, $this); + } + $smtp_server_id = connect_to_smtp_server( + $form['ews_server'], + $form['ews_profile_name'], + null, + $form['ews_email'], + $form['ews_password'], + null, + 'ews', + $form['ews_server_id'], + ); + if ($form['ews_create_profile'] && $imap_server_id && $smtp_server_id) { + add_profile($form['ews_profile_name'], $form['ews_profile_signature'], $form['ews_profile_reply_to'], $form['ews_profile_is_default'], $form['ews_email'], $form['ews_server'], $smtp_server_id, $imap_server_id, $this); } // auto-assign special folders - $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); + $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); if (is_object($mailbox) && $mailbox->authed()) { $specials = $this->user_config->get('special_imap_folders', array()); $exposed = $mailbox->get_special_use_mailboxes(); - $specials[$server_id] = [ + $specials[$imap_server_id] = [ 'sent' => $exposed['sent'] ?? '', 'draft' => $exposed['drafts'] ?? '', 'trash' => $exposed['trash'] ?? '', @@ -1475,6 +1481,7 @@ public function process() { ]; $this->user_config->set('special_imap_folders', $specials); } + $this->session->record_unsaved('EWS server added'); } } } @@ -2002,11 +2009,21 @@ public function process() { if (isset($this->request->post['imap_delete'])) { list($success, $form) = $this->process_form(array('imap_server_id')); if ($success) { + $type = imap_server_type($form['imap_server_id']); + if (strtolower($type) == 'ews') { + $details = Hm_IMAP_List::dump($form['imap_server_id']); + foreach (Hm_Profiles::getAll() as $profile) { + if ($details['user'] == $profile['user'] && $details['server'] == $profile['server']) { + Hm_Profiles::del($profile['id']); + Hm_SMTP_List::del($profile['smtp_id']); + } + } + } $res = Hm_IMAP_List::del($form['imap_server_id']); if ($res) { $this->out('deleted_server_id', $form['imap_server_id']); Hm_Msgs::add('Server deleted'); - $this->session->record_unsaved(sprintf('%s server deleted', imap_server_type($form['imap_server_id']))); + $this->session->record_unsaved(sprintf('%s server deleted', $type)); } } else { diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index acff2a277e..a6e0f665ba 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -3,7 +3,7 @@ /** * EWS integration * @package modules - * @subpackage imap + * @subpackage core * * This is a drop-in replacment of IMAP, JMAP and SMTP classes that allows usage of Exchange Web Services (EWS) * in all functions provided in imap and smtp modules - accessing mailbox, folders, reading messages, @@ -34,6 +34,9 @@ class Hm_EWS { const PID_TAG_FLAG_FLAGGED = 0x00000002; const PID_TAG_ICON_INDEX = 0x1080; const PID_TAG_ICON_REPLIED = 0x00000105; + const PID_TAG_MESSAGE_FLAGS = 0x0E07; + const PID_TAG_MESSAGE_READ = 0x00000001; + const PID_TAG_MESSAGE_DRAFT = 0x00000008; public function connect(array $config) { try { @@ -109,7 +112,7 @@ public function get_special_use_folders($folder = false) { 'flagged' => false, 'all' => false, 'junk' => Enumeration\DistinguishedFolderIdNameType::JUNK, - 'archive' => false, // TODO: check if Enumeration\DistinguishedFolderIdNameType::ARCHIVEMSGFOLDERROOT should be used - it is outside of MESSAGE_ROOT, however. + 'archive' => false, 'drafts' => Enumeration\DistinguishedFolderIdNameType::DRAFTS, ]; foreach ($special as $type => $folderId) { @@ -208,6 +211,58 @@ public function delete_folder($folder) { } } + public function send_message($from, $recipients, $message, $delivery_receipt = false) { + try { + $msg = new Type\MessageType(); + $msg->setFrom($from); + $msg->setToRecipients($recipients); + $msg->setMimeContent(base64_encode($message)); + if ($delivery_receipt) { + $msg->setIsDeliveryReceiptRequested($delivery_receipt); + } + $this->api->sendMail($msg, [ + 'MessageDisposition' => 'SendOnly', // saving to sent items is handled by the imap module depending on the chosen sent folder + ]); + return; + } catch (\Exception $e) { + return $e->getMessage(); + } + } + + public function store_message($folder, $message, $seen = true, $draft = false) { + try { + if ($this->is_distinguished_folder($folder)) { + $folder = new Type\DistinguishedFolderIdType($folder); + } else { + $folder = new Type\FolderIdType($folder); + } + $msg = new Type\MessageType(); + $msg->setMimeContent(base64_encode($message)); + $flags = 0; + if ($seen) { + $flags |= self::PID_TAG_MESSAGE_READ; + } + if ($draft) { + $flags |= self::PID_TAG_MESSAGE_DRAFT; + } + $msg->addExtendedProperty(Type\ExtendedPropertyType::buildFromArray([ + 'ExtendedFieldURI' => [ + 'PropertyTag' => self::PID_TAG_MESSAGE_FLAGS, + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, + ], + 'Value' => $flags, + ])); + $this->api->sendMail($msg, [ + 'MessageDisposition' => 'SaveOnly', + 'SavedItemFolderId' => $folder->toArray(true), + ]); + return true; + } catch (\Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + return false; + } + } + /** * Performs an EWS search using FindItem operation and supplies sorting + pagination arguments. * Search can be perfomed using Advanced Query Syntax when keyword is an array containing terms @@ -310,7 +365,6 @@ public function search($folder, $sort, $reverse, $flag_filter, $offset, $limit, case 'SEEN': $qs[] = 'isRead:true'; break; - // TODO: case 'FLAGGED': $qs[] = 'isFlagged:true'; break; @@ -322,7 +376,7 @@ public function search($folder, $sort, $reverse, $flag_filter, $offset, $limit, 'IsEqualTo' => [ 'ExtendedFieldURI' => [ 'PropertyTag' => self::PID_TAG_ICON_INDEX, - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], 'FieldURIOrConstant' => [ 'Constant' => ['Value' => self::PID_TAG_ICON_REPLIED], @@ -335,7 +389,7 @@ public function search($folder, $sort, $reverse, $flag_filter, $offset, $limit, 'IsNotEqualTo' => [ 'ExtendedFieldURI' => [ 'PropertyTag' => self::PID_TAG_ICON_INDEX, - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], 'FieldURIOrConstant' => [ 'Constant' => ['Value' => self::PID_TAG_ICON_REPLIED], @@ -396,11 +450,11 @@ public function get_message_list($itemIds) { 'ExtendedFieldURI' => [ [ 'PropertyTag' => self::PID_TAG_FLAG_STATUS, //check flagged msg - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], [ 'PropertyTag' => self::PID_TAG_ICON_INDEX, // check if replied/answered - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], ], ], @@ -418,27 +472,7 @@ public function get_message_list($itemIds) { } $messages = []; foreach ($result as $message) { - // note about flags: EWS - doesn't support the \Deleted flag - $flags = []; - if ($message->get('isRead')) { - $flags[] = '\\Seen'; - } - if ($message->get('isDraft')) { - $flags[] = '\\Draft'; - } - if ($extended_properties = $message->get('extendedProperty')) { - if ($extended_properties instanceof Type\ExtendedPropertyType) { - $extended_properties = [$extended_properties]; - } - foreach ($extended_properties as $prop) { - if (hexdec($prop->get('extendedFieldURI')->get('propertyTag')) == self::PID_TAG_FLAG_STATUS && $prop->get('value') > 0) { - $flags[] = '\\Flagged'; - } - if (hexdec($prop->get('extendedFieldURI')->get('propertyTag')) == self::PID_TAG_ICON_INDEX && $prop->get('value') == self::PID_TAG_ICON_REPLIED) { - $flags[] = '\\Answered'; - } - } - } + $flags = $this->extract_flags($message); $uid = bin2hex($message->get('itemId')->get('id')); $msg = [ 'uid' => $uid, @@ -493,6 +527,8 @@ public function message_action($action, $itemIds, $folder=false, $keyword=false) return $this->archive_items($itemIds); case 'DELETE': return $this->delete_items($itemIds); + case 'HARDDELETE': + return $this->delete_items($itemIds, true); case 'COPY': return $this->copy_items($itemIds, $folder); case 'MOVE': @@ -508,13 +544,13 @@ public function message_action($action, $itemIds, $folder=false, $keyword=false) 'SetItemField' => [ 'ExtendedFieldURI' => [ 'PropertyTag' => self::PID_TAG_FLAG_STATUS, - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], 'Message' => [ 'ExtendedProperty' => [ 'ExtendedFieldURI' => [ 'PropertyTag' => self::PID_TAG_FLAG_STATUS, - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], 'Value' => self::PID_TAG_FLAG_FLAGGED, ], @@ -527,7 +563,7 @@ public function message_action($action, $itemIds, $folder=false, $keyword=false) 'DeleteItemField' => [ 'ExtendedFieldURI' => [ 'PropertyTag' => self::PID_TAG_FLAG_STATUS, - 'PropertyType' => 'Integer', + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, ], ], ]; @@ -539,7 +575,7 @@ public function message_action($action, $itemIds, $folder=false, $keyword=false) $change = null; break; case 'EXPUNGE': - // IMAP-only, not supported or needed by EWS + // not needed for EWS return true; default: $change = null; @@ -563,6 +599,18 @@ public function get_message_headers($itemId) { $request = array( 'ItemShape' => array( 'BaseShape' => 'AllProperties', + 'AdditionalProperties' => [ + 'ExtendedFieldURI' => [ + [ + 'PropertyTag' => self::PID_TAG_FLAG_STATUS, //check flagged msg + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, + ], + [ + 'PropertyTag' => self::PID_TAG_ICON_INDEX, // check if replied/answered + 'PropertyType' => Enumeration\MapiPropertyTypeType::INTEGER, + ], + ], + ], ), 'ItemIds' => [ 'ItemId' => ['Id' => hex2bin($itemId)], @@ -570,9 +618,19 @@ public function get_message_headers($itemId) { ); $request = Type::buildFromArray($request); $message = $this->ews->GetItem($request); + $sender = $message->get('sender'); + $from = $message->get('from'); $headers = []; $headers['Arrival Date'] = $message->get('dateTimeCreated'); - $headers['From'] = $message->get('sender')->get('mailbox')->get('name') . ' <' . $message->get('from')->get('mailbox')->get('emailAddress') . '>'; + if ($sender && $from) { + $headers['From'] = $message->get('sender')->get('mailbox')->get('name') . ' <' . $message->get('from')->get('mailbox')->get('emailAddress') . '>'; + } elseif ($sender) { + $headers['From'] = $this->extract_mailbox($sender); + } elseif ($from) { + $headers['From'] = $this->extract_mailbox($from); + } else { + $headers['From'] = null; + } $headers['To'] = $this->extract_mailbox($message->get('toRecipients')); if ($message->get('ccRecipients')) { $headers['Cc'] = $this->extract_mailbox($message->get('ccRecipients')); @@ -580,6 +638,7 @@ public function get_message_headers($itemId) { if ($message->get('bccRecipients')) { $headers['Bcc'] = $this->extract_mailbox($message->get('bccRecipients')); } + $headers['Flags'] = implode(' ', $this->extract_flags($message)); foreach ($message->get('internetMessageHeaders')->InternetMessageHeader as $header) { $name = $header->get('headerName'); if (isset($headers[$name])) { @@ -591,6 +650,11 @@ public function get_message_headers($itemId) { $headers[$name] = (string) $header; } } + if (! $message->isRead()) { + $this->api->updateMailItem($message->getItemId(), [ + 'IsRead' => true, + ]); + } return $headers; } @@ -673,6 +737,27 @@ public function get_structured_message($itemId, $part, $text_only) { return [$msg_struct, $msg_struct_current, $msg_text, $part]; } + public function get_mime_message_by_id($itemId) { + $request = array( + 'ItemShape' => array( + 'BaseShape' => 'IdOnly', + 'IncludeMimeContent' => true, + ), + 'ItemIds' => [ + 'ItemId' => ['Id' => hex2bin($itemId)], + ], + ); + $request = Type::buildFromArray($request); + $message = $this->ews->GetItem($request); + $mime = $message->get('mimeContent'); + $content = base64_decode($mime); + if (strtoupper($mime->get('characterSet')) != 'UTF-8') { + $content = mb_convert_encoding($content, 'UTF-8', $mime->get('characterSet')); + } + $parser = new MailMimeParser(); + return $parser->parse($content, false); + } + protected function parse_mime_part($part, &$struct, $part_num) { $struct[$part_num] = []; list($struct[$part_num]['type'], $struct[$part_num]['subtype']) = explode('/', $part->getContentType()); @@ -697,7 +782,14 @@ protected function parse_mime_part($part, &$struct, $part_num) { $struct[$part_num]['lines'] = substr_count($content, "\n"); $struct[$part_num]['md5'] = ''; $struct[$part_num]['disposition'] = $part->getContentDisposition(); - $struct[$part_num]['file_attributes'] = ''; + if ($filename = $part->getFilename()) { + $struct[$part_num]['file_attributes'] = ['filename' => $filename]; + if ($part->getContentDisposition() == 'attachment') { + $struct[$part_num]['file_attributes']['attachment'] = true; + } + } else { + $struct[$part_num]['file_attributes'] = ''; + } $struct[$part_num]['language'] = ''; $struct[$part_num]['location'] = ''; } @@ -742,27 +834,6 @@ protected function search_mime_part_in_struct($struct, $conditions, $all = false return $found; } - protected function get_mime_message_by_id($itemId) { - $request = array( - 'ItemShape' => array( - 'BaseShape' => 'IdOnly', - 'IncludeMimeContent' => true, - ), - 'ItemIds' => [ - 'ItemId' => ['Id' => hex2bin($itemId)], - ], - ); - $request = Type::buildFromArray($request); - $message = $this->ews->GetItem($request); - $mime = $message->get('mimeContent'); - $content = base64_decode($mime); - if (strtoupper($mime->get('characterSet')) != 'UTF-8') { - $content = mb_convert_encoding($content, 'UTF-8', $mime->get('characterSet')); - } - $parser = new MailMimeParser(); - return $parser->parse($content, false); - } - protected function extract_mailbox($data) { if (is_array($data)) { $result = []; @@ -779,6 +850,31 @@ protected function extract_mailbox($data) { } } + protected function extract_flags($message) { + // note about flags: EWS - doesn't support the \Deleted flag + $flags = []; + if ($message->get('isRead')) { + $flags[] = '\\Seen'; + } + if ($message->get('isDraft')) { + $flags[] = '\\Draft'; + } + if ($extended_properties = $message->get('extendedProperty')) { + if ($extended_properties instanceof Type\ExtendedPropertyType) { + $extended_properties = [$extended_properties]; + } + foreach ($extended_properties as $prop) { + if (hexdec($prop->get('extendedFieldURI')->get('propertyTag')) == self::PID_TAG_FLAG_STATUS && $prop->get('value') > 0) { + $flags[] = '\\Flagged'; + } + if (hexdec($prop->get('extendedFieldURI')->get('propertyTag')) == self::PID_TAG_ICON_INDEX && $prop->get('value') == self::PID_TAG_ICON_REPLIED) { + $flags[] = '\\Answered'; + } + } + } + return $flags; + } + protected function is_distinguished_folder($folder) { $oClass = new ReflectionClass(new Enumeration\DistinguishedFolderIdNameType()); $constants = $oClass->getConstants(); @@ -786,32 +882,54 @@ protected function is_distinguished_folder($folder) { } protected function archive_items($itemIds) { - $itemIds = array_map(function($itemId) { - return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); - }, $itemIds); - $parent = $this->get_parent_folder_of_items($itemIds); - $request = [ - 'ArchiveSourceFolderId' => (new Type\FolderIdType($parent))->toArray(true), - 'ItemIds' => [ - 'ItemId' => $itemIds, - ] - ]; - $request = Type::buildFromArray($request); - try { - $result = $this->ews->ArchiveItem($request); - } catch (\Exception $e) { - Hm_Msgs::add('ERR' . $e->getMessage()); - $result = false; + $result = true; + $folders = $this->get_parent_folders_of_items($itemIds); + foreach ($folders as $folder => $itemIds) { + if ($this->is_distinguished_folder($folder)) { + $folder = new Type\DistinguishedFolderIdType($folder); + } else { + $folder = new Type\FolderIdType($folder); + } + $request = [ + 'ArchiveSourceFolderId' => $folder->toArray(true), + 'ItemIds' => [ + 'ItemId' => $itemIds = array_map(function($itemId) { + return (new Type\ItemIdType($itemId))->toArray(); + }, $itemIds), + ] + ]; + $request = Type::buildFromArray($request); + try { + $result = $result && $this->ews->ArchiveItem($request); + } catch (\Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); + $result = false; + } } return $result; } - protected function delete_items($itemIds) { + protected function delete_items($itemIds, $hard = false) { + $result = true; try { - // TODO: use HardDelete type for items stored in deleted items or trash folder - $result = $this->api->deleteItems(array_map(function($itemId) { - return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); - }, $itemIds)); + if ($hard) { + $result = $this->api->deleteItems(array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds), [ + 'DeleteType' => 'HardDelete', + ]); + } else { + $trash = $this->api->getFolderByDistinguishedId(Type\DistinguishedFolderIdNameType::DELETED); + $folders = $this->get_parent_folders_of_items($itemIds); + foreach ($folders as $folder => $itemIds) { + if ($trash && $folder == $trash->get('folderId')->get('id')) { + $options = ['DeleteType' => 'HardDelete']; + } else { + $options = []; + } + $result = $result && $this->api->deleteItems($itemIds, $options); + } + } } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); $result = false; @@ -820,8 +938,13 @@ protected function delete_items($itemIds) { } protected function copy_items($itemIds, $folder) { + if ($this->is_distinguished_folder($folder)) { + $folder = new Type\DistinguishedFolderIdType($folder); + } else { + $folder = new Type\FolderIdType($folder); + } $request = [ - 'ToFolderId' => (new Type\FolderIdType($folder))->toArray(true), + 'ToFolderId' => $folder->toArray(true), 'ItemIds' => [ 'ItemId' => array_map(function($itemId) { return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); @@ -839,8 +962,13 @@ protected function copy_items($itemIds, $folder) { } protected function move_items($itemIds, $folder) { + if ($this->is_distinguished_folder($folder)) { + $folder = new Type\DistinguishedFolderIdType($folder); + } else { + $folder = new Type\FolderIdType($folder); + } $request = [ - 'ToFolderId' => (new Type\FolderIdType($folder))->toArray(true), + 'ToFolderId' => $folder->toArray(true), 'ItemIds' => [ 'ItemId' => array_map(function($itemId) { return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); @@ -858,8 +986,11 @@ protected function move_items($itemIds, $folder) { return $result; } - protected function get_parent_folder_of_items($itemIdTypes) { - $folder = null; + protected function get_parent_folders_of_items($itemIds) { + $itemIds = array_map(function($itemId) { + return (new Type\ItemIdType(hex2bin($itemId)))->toArray(); + }, $itemIds); + $folders = null; $request = [ 'ItemShape' => [ 'BaseShape' => 'IdOnly', @@ -868,7 +999,7 @@ protected function get_parent_folder_of_items($itemIdTypes) { ], ], 'ItemIds' => [ - 'ItemId' => $itemIdTypes, + 'ItemId' => $itemIds, ], ]; $request = Type::buildFromArray($request); @@ -877,13 +1008,12 @@ protected function get_parent_folder_of_items($itemIdTypes) { $result = [$result]; } foreach ($result as $message) { - if (! $folder) { - $folder = $message->get('ParentFolderId')->get('id'); - } - if ($folder != $message->get('ParentFolderId')->get('id')) { - throw new Exception('Parent folder of items stored in different folders cannot be determinted.'); + $folder = $message->get('ParentFolderId')->get('id'); + if (! isset($folders[$folder])) { + $folders[$folder] = []; } + $folders[$folder][] = $message->getItemId(); } - return $folder; + return $folders; } } diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index e649fe3313..3b59dd917e 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -12,7 +12,6 @@ require_once('hm-imap-bodystructure.php'); require_once('hm-jmap.php'); require_once('hm-ews.php'); -require_once('hm-mailbox.php'); /** * IMAP connection manager diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index 8a45b323ae..f4ead64db5 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -181,7 +181,7 @@ protected function output() { if ($this->get('msg_headers')) { $txt = ''; $small_headers = array('subject', 'x-snoozed', 'date', 'from', 'to', 'reply-to', 'cc', 'flags'); - $reply_args = sprintf('&list_path=%s&uid=%d', + $reply_args = sprintf('&list_path=%s&uid=%s', $this->html_safe($this->get('msg_list_path')), $this->html_safe($this->get('msg_text_uid')) ); @@ -1481,7 +1481,7 @@ protected function output() {
- +
@@ -1495,7 +1495,7 @@ protected function output() {
- +
diff --git a/modules/imap/setup.php b/modules/imap/setup.php index ba368b15ed..3c9f5e58a1 100644 --- a/modules/imap/setup.php +++ b/modules/imap/setup.php @@ -19,6 +19,8 @@ add_output('info', 'imap_server_ids', true, 'imap', 'page_js', 'before'); /* servers page data */ +add_handler('servers', 'profile_data', true, 'profiles', 'load_user_data', 'after'); +add_handler('servers', 'compose_profile_data', true, 'profiles', 'profile_data', 'after'); add_handler('servers', 'process_add_imap_server', true, 'imap', 'message_list_type', 'after'); add_handler('servers', 'process_add_jmap_server', true, 'imap', 'process_add_imap_server', 'after'); add_handler('servers', 'save_imap_servers', true, 'imap', 'process_add_jmap_server', 'after'); @@ -116,6 +118,10 @@ /* ajax server setup callback data */ setup_base_ajax_page('ajax_imap_debug', 'core'); +add_handler('ajax_imap_debug', 'profile_data', true, 'profiles', 'load_user_data', 'after'); +add_handler('ajax_imap_debug', 'compose_profile_data', true, 'profiles', 'profile_data', 'after'); +add_handler('ajax_imap_debug', 'profile_data', true, 'smtp', 'compose_profile_data', 'after'); +add_handler('ajax_imap_debug', 'load_smtp_servers_from_config', true, 'smtp', 'profile_data', 'after'); add_handler('ajax_imap_debug', 'load_imap_servers_from_config', true); add_handler('ajax_imap_debug', 'imap_oauth2_token_check', true); add_handler('ajax_imap_debug', 'imap_hide', true); diff --git a/modules/imap/site.js b/modules/imap/site.js index 42fc8c4d66..8dd9403408 100644 --- a/modules/imap/site.js +++ b/modules/imap/site.js @@ -857,14 +857,19 @@ var imap_setup_message_view_page = function(uid, details, list_path, callback) { } else { $('.msg_text').html(msg_content); - document.title = $('.header_subject th').text(); - $('.header_subject th').append(''); - $('.close_inline_msg').on("click", function() { msg_inline_close(); }); + var cb = function() { + document.title = $('.header_subject th').text(); + $('.header_subject th').append(''); + $('.close_inline_msg').on("click", function() { msg_inline_close(); }); + $('.msg_part_link').on("click", function() { return get_message_content($(this).data('messagePart'), uid, list_path, details, cb, false, true); }); + + $('.reply_link, .reply_all_link, .forward_link').each(function() { + $(this).data("href", $(this).attr("href")).removeAttr("href"); + $(this).addClass('disabled_link'); + }); + } + cb(); - $('.reply_link, .reply_all_link, .forward_link').each(function() { - $(this).data("href", $(this).attr("href")).removeAttr("href"); - $(this).addClass('disabled_link'); - }); imap_message_view_finished(); get_message_content(false, uid, list_path, details, callback, true); } diff --git a/modules/profiles/functions.php b/modules/profiles/functions.php index bb7abe0148..88b12f987d 100644 --- a/modules/profiles/functions.php +++ b/modules/profiles/functions.php @@ -16,7 +16,9 @@ function add_profile($name, $signature, $reply_to, $is_default, $email, $server_ 'user' => $email, 'type' => 'imap' ); - - Hm_Profiles::add($profile); + $id = Hm_Profiles::add($profile); + if ($is_default) { + Hm_Profiles::setDefault($id); + } } } diff --git a/modules/smtp/functions.php b/modules/smtp/functions.php index 260ad84e87..68a8db3b16 100644 --- a/modules/smtp/functions.php +++ b/modules/smtp/functions.php @@ -4,10 +4,11 @@ if (!defined('DEBUG_MODE')) { die(); } if (!hm_exists('connect_to_smtp_server')) { - function connect_to_smtp_server($address, $name, $port, $user, $pass, $tls, $server_id = false) { + function connect_to_smtp_server($address, $name, $port, $user, $pass, $tls, $type, $server_id = false) { $smtp_list = array( 'name' => $name, 'server' => $address, + 'type' => $type, 'hide' => false, 'port' => $port, 'user' => $user, @@ -25,13 +26,13 @@ function connect_to_smtp_server($address, $name, $port, $user, $pass, $tls, $ser } } else { $smtp_server_id = Hm_SMTP_List::add($smtp_list); - if (! can_save_last_added_server('Hm_SMTP_List', $user)) { + if ($type != 'ews' && ! can_save_last_added_server('Hm_SMTP_List', $user)) { return; } } - $smtp = Hm_SMTP_List::connect($smtp_server_id, false); - if (smtp_authed($smtp)) { + $mailbox = Hm_SMTP_List::connect($smtp_server_id, false); + if ($mailbox->authed()) { return $smtp_server_id; } else { diff --git a/modules/smtp/hm-smtp.php b/modules/smtp/hm-smtp.php index 9aab4b00ea..9355039d83 100644 --- a/modules/smtp/hm-smtp.php +++ b/modules/smtp/hm-smtp.php @@ -14,8 +14,13 @@ class Hm_SMTP_List { use Hm_Server_List; + protected static $user_config; + protected static $session; + public static function init($user_config, $session) { self::initRepo('smtp_servers', $user_config, $session, self::$server_list); + self::$user_config = $user_config; + self::$session = $session; } public static function service_connect($id, $server, $user, $pass, $cache=false) { @@ -25,7 +30,8 @@ public static function service_connect($id, $server, $user, $pass, $cache=false) 'port' => $server['port'], 'tls' => $server['tls'], 'username' => $user, - 'password' => $pass + 'password' => $pass, + 'type' => array_key_exists('type', $server) ? $server['type'] : 'smtp', ); if (array_key_exists('auth', $server)) { $config['auth'] = $server['auth']; @@ -33,16 +39,17 @@ public static function service_connect($id, $server, $user, $pass, $cache=false) if (array_key_exists('no_auth', $server)) { $config['no_auth'] = true; } - self::$server_list[$id]['object'] = new Hm_SMTP($config); - - if (!self::$server_list[$id]['object']->connect()) { + self::$server_list[$id]['object'] = new Hm_Mailbox($id, self::$user_config, self::$session); + if (! self::$server_list[$id]['object']->connect($config)) { return self::$server_list[$id]['object']; } return false; } + public static function get_cache($session, $id) { return false; } + public static function address_list() { $addrs = array(); foreach (self::$server_list as $server) { diff --git a/modules/smtp/modules.php b/modules/smtp/modules.php index 7dbdef5cd4..18bce97391 100644 --- a/modules/smtp/modules.php +++ b/modules/smtp/modules.php @@ -481,8 +481,8 @@ public function process() { Hm_Msgs::add('ERRThis server and username are already configured'); return; } - $smtp = Hm_SMTP_List::connect($form['smtp_server_id'], false, $form['smtp_user'], $form['smtp_pass'], true); - if (smtp_authed($smtp)) { + $mailbox = Hm_SMTP_List::connect($form['smtp_server_id'], false, $form['smtp_user'], $form['smtp_pass'], true); + if ($mailbox && $mailbox->authed()) { $just_saved_credentials = true; Hm_Msgs::add("Server saved"); $this->session->record_unsaved('SMTP server saved'); @@ -555,15 +555,15 @@ public function process() { } } if ($success) { - $smtp = Hm_SMTP_List::connect($form['smtp_server_id'], false, $form['smtp_user'], $form['smtp_pass']); + $mailbox = Hm_SMTP_List::connect($form['smtp_server_id'], false, $form['smtp_user'], $form['smtp_pass']); } elseif (isset($form['smtp_server_id'])) { - $smtp = Hm_SMTP_List::connect($form['smtp_server_id'], false); + $mailbox = Hm_SMTP_List::connect($form['smtp_server_id'], false); } - if ($smtp && $smtp->state == 'authed') { + if ($mailbox && $mailbox->authed()) { Hm_Msgs::add("Successfully authenticated to the SMTP server"); } - elseif ($smtp && $smtp->state == 'connected') { + elseif ($mailbox && $mailbox->state() == 'connected') { Hm_Msgs::add("ERRConnected, but failed to authenticate to the SMTP server"); } else { @@ -704,8 +704,6 @@ public function process() { 'draft_subject' => $form['compose_subject'], 'draft_smtp' => $smtp_id ); - $from_params = ''; - $recipients_params = ''; /* parse attachments */ $uploaded_files = []; @@ -723,11 +721,6 @@ public function process() { /* msg details */ list($body, $cc, $bcc, $in_reply_to, $draft) = get_outbound_msg_detail($this->request->post, $draft, $body_type); - if (!empty($this->request->post['compose_delivery_receipt'])) { - $from_params = 'RET=HDRS'; - $recipients_params = 'NOTIFY=SUCCESS,FAILURE'; - } - /* smtp server details */ $smtp_details = Hm_SMTP_List::dump($smtp_id, true); if (!$smtp_details) { @@ -747,8 +740,8 @@ public function process() { list($from, $reply_to) = outbound_address_check($this, $from, $reply_to); /* try to connect */ - $smtp = Hm_SMTP_List::connect($smtp_id, false); - if (!smtp_authed($smtp)) { + $mailbox = Hm_SMTP_List::connect($smtp_id, false); + if (! $mailbox || ! $mailbox->authed()) { Hm_Msgs::add("ERRFailed to authenticate to the SMTP server"); repopulate_compose_form($draft, $this); return; @@ -770,7 +763,7 @@ public function process() { } /* send the message */ - $err_msg = $smtp->send_message($from, $recipients, $mime->get_mime_msg(), $from_params, $recipients_params); + $err_msg = $mailbox->send_message($from, $recipients, $mime->get_mime_msg(), ! empty($this->request->post['compose_delivery_receipt'])); if ($err_msg) { Hm_Msgs::add(sprintf("ERR%s", $err_msg)); repopulate_compose_form($draft, $this); @@ -781,7 +774,7 @@ public function process() { $auto_bcc = $this->user_config->get('smtp_auto_bcc_setting', DEFAULT_SMTP_AUTO_BCC); if ($auto_bcc) { $mime->set_auto_bcc($from); - $bcc_err_msg = $smtp->send_message($from, array($from), $mime->get_mime_msg()); + $bcc_err_msg = $mailbox->send_message($from, array($from), $mime->get_mime_msg()); } /* check for associated IMAP server to save a copy */ @@ -1425,6 +1418,10 @@ protected function output() { $no_edit = false; + if (array_key_exists('type', $vals) && $vals['type'] == 'ews') { + continue; + } + if (array_key_exists('user', $vals) && !array_key_exists('nopass', $vals)) { $disabled = 'disabled="disabled"'; $user_pc = $vals['user']; @@ -2150,14 +2147,6 @@ function profile_from_compose_smtp_id($profiles, $id) { return false; }} -/** - * @subpackage smtp/functions - */ -if (!hm_exists('smtp_authed')) { -function smtp_authed($smtp) { - return is_object($smtp) && $smtp->state == 'authed'; -}} - /** * @subpackage smtp/functions */ @@ -2200,6 +2189,7 @@ function default_smtp_server($user_config, $session, $request, $config, $user, $ $attributes = array( 'name' => $config->get('default_smtp_name', 'Default'), 'default' => true, + 'type' => 'smtp', 'server' => $smtp_server, 'port' => $smtp_port, 'tls' => $smtp_tls, diff --git a/modules/smtp/setup.php b/modules/smtp/setup.php index 2b56fe5f28..f7e6440e65 100644 --- a/modules/smtp/setup.php +++ b/modules/smtp/setup.php @@ -27,7 +27,7 @@ add_handler('profiles', 'add_smtp_servers_to_page_data', true, 'smtp', 'load_smtp_servers_from_config', 'after'); /* servers page */ -add_handler('servers', 'load_smtp_servers_from_config', true, 'smtp', 'language', 'after'); +add_handler('servers', 'load_smtp_servers_from_config', true, 'smtp', 'load_user_data', 'after'); add_handler('servers', 'process_add_smtp_server', true, 'smtp', 'load_smtp_servers_from_config', 'after'); add_handler('servers', 'add_smtp_servers_to_page_data', true, 'smtp', 'process_add_smtp_server', 'after'); add_handler('servers', 'save_smtp_servers', true, 'smtp', 'add_smtp_servers_to_page_data', 'after'); @@ -119,7 +119,7 @@ 'reply_all' => FILTER_VALIDATE_INT, 'forward' => FILTER_VALIDATE_INT, 'forward_as_attachment' => FILTER_VALIDATE_INT, - 'draft_id' => FILTER_VALIDATE_INT, + 'draft_id' => FILTER_DEFAULT, 'hm_ajax_hook' => FILTER_DEFAULT, 'compose_to' => FILTER_DEFAULT, 'mailto_uri' => FILTER_DEFAULT, @@ -138,7 +138,7 @@ 'allowed_output' => array( 'file_details' => array(FILTER_UNSAFE_RAW, false), 'draft_subject' => array(FILTER_DEFAULT, false), - 'draft_id' => array(FILTER_VALIDATE_INT, false), + 'draft_id' => array(FILTER_DEFAULT, false), 'profile_value' => array(FILTER_DEFAULT, false), 'msg_sent_and_archived' => array(FILTER_VALIDATE_BOOLEAN, false), 'sent_msg_id' => array(FILTER_VALIDATE_BOOLEAN, false), @@ -171,7 +171,7 @@ 'compose_bcc' => FILTER_UNSAFE_RAW, 'compose_delivery_receipt' => FILTER_VALIDATE_BOOLEAN, 'compose_smtp_id' => FILTER_DEFAULT, - 'draft_id' => FILTER_VALIDATE_INT, + 'draft_id' => FILTER_DEFAULT, 'draft_body' => FILTER_UNSAFE_RAW, 'draft_subject' => FILTER_UNSAFE_RAW, 'draft_to' => FILTER_UNSAFE_RAW, diff --git a/modules/smtp/site.js b/modules/smtp/site.js index 066bb1f938..985d709ea8 100644 --- a/modules/smtp/site.js +++ b/modules/smtp/site.js @@ -512,7 +512,7 @@ $(function () { // if contact_cc not exist in contact list for user var checkInList = ""; - if (list_emails) { + if (typeof list_emails != 'undefined') { checkInList = check_cc_exist_in_contacts_list(e); if (checkInList) { modalContentHeadline = "Adress mail not exist in your contact liste"; From 6ed6fe9c1d2bc81e5617aae92d266835e7f867e8 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 1 Nov 2024 15:56:40 +0200 Subject: [PATCH 21/35] EWS: composer alphabetical list and switch to latest php-ews master --- composer.json | 4 +- composer.lock | 387 ++++++++++++++++++++++++++------------------------ 2 files changed, 204 insertions(+), 187 deletions(-) diff --git a/composer.json b/composer.json index 4cb7b48dcf..2f380cca5c 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "ext-openssl": "*", "ext-session": "*", "ezyang/htmlpurifier": "^4.17", + "garethp/php-ews": "dev-master", "henrique-borba/php-sieve-manager": "^1.0", "league/commonmark": "^2.4", "paragonie/random_compat": "^2.0.18", @@ -55,8 +56,7 @@ "twbs/bootstrap": "^5.3", "twbs/bootstrap-icons": "^1.11", "webklex/composer-info": "^0.0.1", - "zbateson/mail-mime-parser": "^2.4", - "garethp/php-ews": "^0.10.1" + "zbateson/mail-mime-parser": "^2.4" }, "require-dev": { "phpunit/phpunit": "^10.5" diff --git a/composer.lock b/composer.lock index 909e66ac8d..0215a5f045 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ad61b5e9e80d518e0ffbd8c67609fa7a", + "content-hash": "4d31bd501d7ee863550c2d2342d8cf6b", "packages": [ { "name": "bacon/bacon-qr-code", @@ -121,23 +121,23 @@ }, { "name": "dasprid/enum", - "version": "1.0.5", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", "shasum": "" }, "require": { "php": ">=7.1 <9.0" }, "require-dev": { - "phpunit/phpunit": "^7 | ^8 | ^9", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", "squizlabs/php_codesniffer": "*" }, "type": "library", @@ -165,22 +165,22 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" }, - "time": "2023-08-25T16:18:39+00:00" + "time": "2024-08-09T14:30:48+00:00" }, { "name": "dflydev/dot-access-data", - "version": "v3.0.2", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "f41715465d65213d644d3141a6a93081be5d3549" + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", - "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", "shasum": "" }, "require": { @@ -240,26 +240,26 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2022-10-27T11:44:00+00:00" + "time": "2024-07-08T12:26:09+00:00" }, { "name": "ezyang/htmlpurifier", - "version": "v4.17.0", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + "reference": "cb56001e54359df7ae76dc522d08845dc741621b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", + "reference": "cb56001e54359df7ae76dc522d08845dc741621b", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -301,9 +301,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" }, - "time": "2023-11-17T15:01:25+00:00" + "time": "2024-11-01T03:51:45+00:00" }, { "name": "garethp/http-playback", @@ -345,16 +345,16 @@ }, { "name": "garethp/php-ews", - "version": "v0.10.1", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/Garethp/php-ews.git", - "reference": "6fbc1b17d0c04fe313c40c5d712090638f6a1c84" + "reference": "2ca0d30ddd3439bc0ec0b41de4fcb737ee0d4438" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Garethp/php-ews/zipball/6fbc1b17d0c04fe313c40c5d712090638f6a1c84", - "reference": "6fbc1b17d0c04fe313c40c5d712090638f6a1c84", + "url": "https://api.github.com/repos/Garethp/php-ews/zipball/2ca0d30ddd3439bc0ec0b41de4fcb737ee0d4438", + "reference": "2ca0d30ddd3439bc0ec0b41de4fcb737ee0d4438", "shasum": "" }, "require": { @@ -368,10 +368,11 @@ "require-dev": { "goetas/xsd-reader": "^2.0-dev", "goetas/xsd2php": "^2.1", - "mockery/mockery": "^1.4", - "phpunit/phpunit": "~9.5", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^9.4 || ^8.5", "squizlabs/php_codesniffer": "~3.6.0" }, + "default-branch": true, "type": "library", "autoload": { "files": [ @@ -390,30 +391,42 @@ "BSD-3-Clause" ], "description": "A PHP Library to interact with the Exchange SOAP service", + "keywords": [ + "Microsoft Exchange", + "NTLM", + "calendar", + "contacts", + "email", + "ews", + "exchange web services", + "php", + "push notification", + "soap" + ], "support": { "issues": "https://github.com/Garethp/php-ews/issues", - "source": "https://github.com/Garethp/php-ews/tree/v0.10.1" + "source": "https://github.com/Garethp/php-ews/tree/master" }, - "time": "2021-09-17T15:21:51+00:00" + "time": "2024-08-16T08:57:08+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", + "version": "7.9.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -424,9 +437,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -504,7 +517,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" }, "funding": [ { @@ -520,20 +533,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-24T11:22:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { @@ -587,7 +600,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.3" + "source": "https://github.com/guzzle/promises/tree/2.0.4" }, "funding": [ { @@ -603,20 +616,20 @@ "type": "tidelift" } ], - "time": "2024-07-18T10:29:17+00:00" + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -631,8 +644,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -703,7 +716,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -719,7 +732,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "henrique-borba/php-sieve-manager", @@ -792,16 +805,16 @@ }, { "name": "league/commonmark", - "version": "2.4.2", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" + "reference": "b650144166dfa7703e62a22e493b853b58d874b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", - "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0", + "reference": "b650144166dfa7703e62a22e493b853b58d874b0", "shasum": "" }, "require": { @@ -814,8 +827,8 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.3", - "commonmark/commonmark.js": "0.30.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", "erusev/parsedown": "^1.0", @@ -837,7 +850,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "2.6-dev" } }, "autoload": { @@ -894,7 +907,7 @@ "type": "tidelift" } ], - "time": "2024-02-02T11:59:32+00:00" + "time": "2024-08-16T11:46:16+00:00" }, { "name": "league/config", @@ -980,31 +993,31 @@ }, { "name": "nette/schema", - "version": "v1.2.5", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", - "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", "shasum": "" }, "require": { - "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": "7.1 - 8.3" + "nette/utils": "^4.0", + "php": "8.1 - 8.4" }, "require-dev": { - "nette/tester": "^2.3 || ^2.4", + "nette/tester": "^2.5.2", "phpstan/phpstan-nette": "^1.0", - "tracy/tracy": "^2.7" + "tracy/tracy": "^2.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -1036,35 +1049,36 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.5" + "source": "https://github.com/nette/schema/tree/v1.3.2" }, - "time": "2023-10-05T20:37:59+00:00" + "time": "2024-10-06T23:10:23+00:00" }, { "name": "nette/utils", - "version": "v3.2.10", + "version": "v4.0.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", - "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", "shasum": "" }, "require": { - "php": ">=7.2 <8.4" + "php": "8.0 - 8.4" }, "conflict": { - "nette/di": "<3.0.6" + "nette/finder": "<3", + "nette/schema": "<1.2.2" }, "require-dev": { "jetbrains/phpstorm-attributes": "dev-master", - "nette/tester": "~2.0", + "nette/tester": "^2.5", "phpstan/phpstan": "^1.0", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.9" }, "suggest": { "ext-gd": "to use Image", @@ -1072,13 +1086,12 @@ "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", - "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", - "ext-xml": "to use Strings::length() etc. when mbstring is not available" + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1122,9 +1135,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.10" + "source": "https://github.com/nette/utils/tree/v4.0.5" }, - "time": "2023-07-30T15:38:18+00:00" + "time": "2024-08-07T15:39:19+00:00" }, { "name": "paragonie/random_compat", @@ -1390,20 +1403,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -1427,7 +1440,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -1439,9 +1452,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -1542,25 +1555,25 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.3", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1589,7 +1602,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -1605,7 +1618,7 @@ "type": "tidelift" } ], - "time": "2023-01-24T14:02:46+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/dotenv", @@ -1680,20 +1693,20 @@ }, { "name": "symfony/polyfill-iconv", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f" + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-iconv": "*" @@ -1740,7 +1753,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" }, "funding": [ { @@ -1756,24 +1769,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -1820,7 +1833,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -1836,24 +1849,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1900,7 +1913,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -1916,7 +1929,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "thomaspark/bootswatch", @@ -2316,16 +2329,16 @@ "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -2333,11 +2346,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -2363,7 +2377,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -2371,30 +2385,31 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { + "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -2426,9 +2441,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "phar-io/manifest", @@ -2550,32 +2565,32 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.14", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -2587,7 +2602,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -2616,7 +2631,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -2624,7 +2639,7 @@ "type": "github" } ], - "time": "2024-03-12T15:33:41+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2871,16 +2886,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.20", + "version": "10.5.38", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3" + "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/547d314dc24ec1e177720d45c6263fb226cc2ae3", - "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132", + "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132", "shasum": "" }, "require": { @@ -2890,26 +2905,26 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.5", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.1", - "sebastian/global-state": "^6.0.1", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -2952,7 +2967,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38" }, "funding": [ { @@ -2968,7 +2983,7 @@ "type": "tidelift" } ], - "time": "2024-04-24T06:32:35+00:00" + "time": "2024-10-28T13:06:21+00:00" }, { "name": "sebastian/cli-parser", @@ -3140,16 +3155,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.1", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "" }, "require": { @@ -3160,7 +3175,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -3205,7 +3220,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" }, "funding": [ { @@ -3213,7 +3228,7 @@ "type": "github" } ], - "time": "2023-08-14T13:18:12+00:00" + "time": "2024-10-18T14:56:07+00:00" }, { "name": "sebastian/complexity", @@ -3939,7 +3954,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "garethp/php-ews": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From 43a5dfab16c0e5173f51c914c076c8c04cd535f6 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 13 Nov 2024 13:07:11 +0200 Subject: [PATCH 22/35] use another junkemail distinguished folder ID and catch exceptions when getting special folders --- modules/imap/hm-ews.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 22bc9707ce..4baf0366ae 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -116,15 +116,19 @@ public function get_special_use_folders($folder = false) { 'sent' => Enumeration\DistinguishedFolderIdNameType::SENT, 'flagged' => false, 'all' => false, - 'junk' => Enumeration\DistinguishedFolderIdNameType::JUNK, + 'junk' => Enumeration\DistinguishedFolderIdNameType::JUNKEMAIL, 'archive' => false, 'drafts' => Enumeration\DistinguishedFolderIdNameType::DRAFTS, ]; foreach ($special as $type => $folderId) { if ($folderId) { - $distinguishedFolder = $this->api->getFolderByDistinguishedId($folderId); - if ($distinguishedFolder) { - $special[$type] = $distinguishedFolder->get('folderId')->get('id'); + try { + $distinguishedFolder = $this->api->getFolderByDistinguishedId($folderId); + if ($distinguishedFolder) { + $special[$type] = $distinguishedFolder->get('folderId')->get('id'); + } + } catch (\Exception $e) { + Hm_Msgs::add('ERR' . $e->getMessage()); } } } From 7a3176bf9062a006cd165e37cb44a4ecde11d0a7 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 13 Nov 2024 16:49:24 +0200 Subject: [PATCH 23/35] remove forget EWS details functionality as it was removed from imap as well; strip tags on preview body to handle html more gracefully --- modules/imap/hm-ews.php | 2 +- modules/imap/output_modules.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index fdfb9f9215..8090185840 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -519,7 +519,7 @@ public function get_message_list($itemIds, $include_preview = false) { } } $msg['charset'] = $cset; - $msg['preview_msg'] = $include_preview ? $message->get('body') : ""; + $msg['preview_msg'] = $include_preview ? strip_tags($message->get('body')) : ""; $messages[$uid] = $msg; } return $messages; diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index 6d67e8df1f..4819cb46c4 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -1596,7 +1596,6 @@ protected function output() { $res .= 'html_safe(json_encode($serverDetails)).'\' data-id="'.$this->html_safe($serverDetails['name']).'" data-type="ews" />'; $res .= ''; $res .= ''; - $res .= ''; } // Hide/Unhide Buttons From 6739ca2cfe3c54ca130161fbaf1604a4ab4492e5 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 13 Nov 2024 16:58:22 +0200 Subject: [PATCH 24/35] homepage listing jmap and ews sources besides imap and smtp --- modules/nux/modules.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/nux/modules.php b/modules/nux/modules.php index b1dfec6a40..ff17213df9 100644 --- a/modules/nux/modules.php +++ b/modules/nux/modules.php @@ -65,6 +65,8 @@ class Hm_Handler_nux_homepage_data extends Hm_Handler_Module { public function process() { $imap_servers = NULL; + $jmap_servers = NULL; + $ews_servers = NULL; $smtp_servers = NULL; $feed_servers = NULL; $profiles = NULL; @@ -72,13 +74,17 @@ public function process() { $modules = $this->config->get_modules(); if (data_source_available($modules, 'imap')) { - $imap_servers = count(Hm_IMAP_List::dump(false)); + $servers = Hm_IMAP_List::dump(false); + $imap_servers = count(array_filter($servers, function($server) { return empty($server['type']) || $server['type'] === 'imap'; })); + $jmap_servers = count(array_filter($servers, function($server) { return @$server['type'] === 'jmap'; })); + $ews_servers = count(array_filter($servers, function($server) { return @$server['type'] === 'ews'; })); } if (data_source_available($modules, 'feeds')) { $feed_servers = count(Hm_Feed_List::dump(false)); } if (data_source_available($modules, 'smtp')) { - $smtp_servers = count(Hm_SMTP_List::dump(false)); + $servers = Hm_SMTP_List::dump(false); + $smtp_servers = count(array_filter($servers, function($server) { return empty($server['type']) || $server['type'] === 'smtp'; })); } if (data_source_available($modules, 'profiles')) { Hm_Profiles::init($this); @@ -87,6 +93,8 @@ public function process() { $this->out('nux_server_setup', array( 'imap' => $imap_servers, + 'jmap' => $jmap_servers, + 'ews' => $ews_servers, 'feeds' => $feed_servers, 'smtp' => $smtp_servers, 'profiles' => $profiles @@ -558,7 +566,7 @@ protected function output() { } $server_data = $this->get('nux_server_setup', array()); $tz = $this->get('tzone'); - $protos = array('imap', 'smtp', 'feeds', 'profiles'); + $protos = array('imap', 'jmap', 'ews', 'smtp', 'feeds', 'profiles'); $res = '

'.$this->trans('Welcome to Cypht').'

'; $res .= '

'.$this->trans('Add a popular E-mail source quickly and easily').'

'; From 716c77287d6fadd1ad1380bc66a6ef7065074b7a Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Wed, 13 Nov 2024 17:17:43 +0200 Subject: [PATCH 25/35] fix failing unit tests --- tests/phpunit/helpers.php | 5 +++-- tests/phpunit/modules/core/message_list_functions.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/helpers.php b/tests/phpunit/helpers.php index 513671fc6f..07028b61f6 100644 --- a/tests/phpunit/helpers.php +++ b/tests/phpunit/helpers.php @@ -98,10 +98,11 @@ public static function change_state($val) { class Hm_IMAP_List extends Hm_Server_Wrapper { public static $state = false; public static function connect($id, $cache=false, $user=false, $pass=false, $save_credentials=false) { + global $user_config, $session; Hm_IMAP::$allow_connection = self::$state; Hm_IMAP::$allow_auth = self::$state; - self::$server_list[$id]['object'] = new Hm_IMAP(); - self::$server_list[$id]['object']->connect(); + self::$server_list[$id]['object'] = new Hm_Mailbox($id, $user_config, $session); + self::$server_list[$id]['object']->connect([]); self::$server_list[$id]['connected'] = true; return self::$server_list[$id]['object']; } diff --git a/tests/phpunit/modules/core/message_list_functions.php b/tests/phpunit/modules/core/message_list_functions.php index 9d8330ec35..4da49bec64 100644 --- a/tests/phpunit/modules/core/message_list_functions.php +++ b/tests/phpunit/modules/core/message_list_functions.php @@ -124,7 +124,7 @@ public function test_message_since_dropdown() { public function test_list_sources() { $mod = new Hm_Output_Test(array('foo' => 'bar', 'bar' => 'foo'), array('bar')); $this->assertEquals('
Sources
', list_sources(array(array('group' => 'background', 'type' => 'imap', 'folder' => 'foo')), $mod)); - $this->assertEquals('
Sources
imap blah foo
', list_sources(array(array('name' => 'blah', 'type' => 'imap', 'folder' => bin2hex('foo'))), $mod)); + $this->assertEquals('
Sources
imap blah foo
', list_sources(array(array('name' => 'blah', 'type' => 'imap', 'folder' => bin2hex('foo'), 'folder_name' => 'foo')), $mod)); $this->assertEquals('
Sources
imap blah INBOX
', list_sources(array(array('name' => 'blah', 'type' => 'imap')), $mod)); } /** From 5cc766fb0d064a55980a873b8e2780dfc5109676 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 15 Nov 2024 14:09:05 +0200 Subject: [PATCH 26/35] folder name shortening work on actual name and not on the escaped html --- modules/imap/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/imap/functions.php b/modules/imap/functions.php index 69c3573631..07e6c63e07 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -142,10 +142,10 @@ function format_imap_folder_section($folders, $id, $output_mod, $with_input = fa '" href="?page=message_list&list_path='. urlencode('imap_'.$id.'_'.$output_mod->html_safe($folder_name)).'"'; } - if (mb_strlen($output_mod->html_safe($folder['basename']))>15) { + if (mb_strlen($folder['basename'])>15) { $results .= ''.mb_substr($output_mod->html_safe($folder['basename']),0,15).'...'; + '">'.$output_mod->html_safe(mb_substr($folder['basename'],0,15)).'...'; } else { $results .= ''.$output_mod->html_safe($folder['basename']).''; From 5c34e813bd3b5f0c75d31a0b2eb271acbefddb96 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 15 Nov 2024 14:14:46 +0200 Subject: [PATCH 27/35] give 5 minutes tolerance between sent/received datetime and local server datetime when we display 'just now' date string --- modules/core/message_list_functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/message_list_functions.php b/modules/core/message_list_functions.php index 12d5c968af..02c2d61dea 100644 --- a/modules/core/message_list_functions.php +++ b/modules/core/message_list_functions.php @@ -167,10 +167,10 @@ function human_readable_interval($date_str) { $t['month'] = $t['day']*30; $t['year'] = $t['week']*52; - if ($interval < 0) { + if ($interval < -300) { return 'From the future!'; } - elseif ($interval == 0) { + elseif ($interval <= 0) { return 'Just now'; } foreach (array_reverse($t) as $name => $val) { From 7eba7f7fc3eb44966c88500093dad5fa90573d4b Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 19 Nov 2024 08:49:21 +0200 Subject: [PATCH 28/35] ews fixes: profile creation when username-only is supplied, append the server address to get the email address, so that we can populate the send from address; mime part (message source) view of the full mime content; fix email sending JS issues and uploaded files issues encoded multiple times --- modules/core/handler_modules.php | 2 +- modules/imap/handler_modules.php | 8 +++++++- modules/imap/hm-ews.php | 5 +---- modules/nux/modules.php | 1 + modules/profiles/functions.php | 6 +++--- modules/smtp/modules.php | 4 +++- modules/smtp/site.js | 16 +++++++++------- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/modules/core/handler_modules.php b/modules/core/handler_modules.php index 050d50c541..a42115653a 100644 --- a/modules/core/handler_modules.php +++ b/modules/core/handler_modules.php @@ -1133,7 +1133,7 @@ public function process() { return; } - add_profile($profileName, $profileSignature, $profileReplyTo, $profileIsDefault, $email, $imapAddress, $this->smtp_server_id, $this->imap_server_id, $this); + add_profile($profileName, $profileSignature, $profileReplyTo, $profileIsDefault, $email, $imapAddress, $email, $this->smtp_server_id, $this->imap_server_id, $this); } if ($this->module_is_supported('imap_folders')) { diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index 48d9100d83..f719eef425 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -1479,7 +1479,12 @@ public function process() { $form['ews_server_id'], ); if ($form['ews_create_profile'] && $imap_server_id && $smtp_server_id) { - add_profile($form['ews_profile_name'], $form['ews_profile_signature'], $form['ews_profile_reply_to'], $form['ews_profile_is_default'], $form['ews_email'], $form['ews_server'], $smtp_server_id, $imap_server_id, $this); + if (! strstr($form['ews_email'], '@')) { + $address = $form['ews_email'] . '@' . $form['ews_server']; + } else { + $address = $form['ews_email']; + } + add_profile($form['ews_profile_name'], $form['ews_profile_signature'], $form['ews_profile_reply_to'], $form['ews_profile_is_default'], $address, $form['ews_server'], $form['ews_email'], $smtp_server_id, $imap_server_id, $this); } // auto-assign special folders $mailbox = Hm_IMAP_List::get_connected_mailbox($imap_server_id, $this->cache); @@ -1496,6 +1501,7 @@ public function process() { $this->user_config->set('special_imap_folders', $specials); } $this->session->record_unsaved('EWS server added'); + $this->session->secure_cookie($this->request, 'hm_reload_folders', '1'); } } } diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 8090185840..887c050ac2 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -674,10 +674,7 @@ public function get_message_content($itemId, $part) { return $msg_text; } else { $message = $this->get_mime_message_by_id($itemId); - $content = $message->getHtmlContent(); - if (empty($content)) { - $content = $message->getTextContent(); - } + $content = (string) $message; return $content; } } diff --git a/modules/nux/modules.php b/modules/nux/modules.php index 2514702897..6c384316d8 100644 --- a/modules/nux/modules.php +++ b/modules/nux/modules.php @@ -412,6 +412,7 @@ public function process() $server['profile']['is_default'], $server['username'], ($server['jmap']['server'] ?? $server['imap']['server']), + $server['username'], $smtp_server_id, ($jmap_server_id ?? $imap_server_id), $this diff --git a/modules/profiles/functions.php b/modules/profiles/functions.php index 88b12f987d..dcfc100056 100644 --- a/modules/profiles/functions.php +++ b/modules/profiles/functions.php @@ -3,7 +3,7 @@ if (!defined('DEBUG_MODE')) { die(); } if (!hm_exists('add_profile')) { - function add_profile($name, $signature, $reply_to, $is_default, $email, $server_mail, $smtp_server_id, $imap_server_id, $context, $remark = '') { + function add_profile($name, $signature, $reply_to, $is_default, $email, $server, $user, $smtp_server_id, $imap_server_id, $context, $remark = '') { $profile = array( 'name' => $name, 'sig' => $signature, @@ -12,8 +12,8 @@ function add_profile($name, $signature, $reply_to, $is_default, $email, $server_ 'replyto' => $reply_to, 'default' => $is_default, 'address' => $email, - 'server' => $server_mail, - 'user' => $email, + 'server' => $server, + 'user' => $user, 'type' => 'imap' ); $id = Hm_Profiles::add($profile); diff --git a/modules/smtp/modules.php b/modules/smtp/modules.php index aa42e4b7c7..5929a48807 100644 --- a/modules/smtp/modules.php +++ b/modules/smtp/modules.php @@ -1862,7 +1862,9 @@ function get_uploaded_files_from_array($uploaded_files) { } function prepare_draft_mime($atts, $uploaded_files, $from = false, $name = '') { - $uploaded_files = get_uploaded_files_from_array($uploaded_files); + if (! empty($uploaded_files) && ! is_array($uploaded_files[0])) { + $uploaded_files = get_uploaded_files_from_array($uploaded_files); + } $mime = new Hm_MIME_Msg( $atts['draft_to'], $atts['draft_subject'], diff --git a/modules/smtp/site.js b/modules/smtp/site.js index 44c62b2bb5..30a6bce694 100644 --- a/modules/smtp/site.js +++ b/modules/smtp/site.js @@ -336,13 +336,14 @@ var is_valid_recipient = function(recipient) { var process_compose_form = function(){ var msg_uid = getMessageUidParam(); - var detail = Hm_Utils.parse_folder_path(getListPathParam(), 'imap'); - var class_name = 'imap_' + detail.server_id + '_' + msg_uid + '_' + detail.folder; - var key = 'imap_' + Hm_Utils.get_url_page_number() + '_' + getListPathParam(); - var next_message = Hm_Message_List.prev_next_links(key, class_name)[1]; - - if (next_message) { - $('.compose_next_email_data').val(next_message); + if (getListPathParam()) { + var detail = Hm_Utils.parse_folder_path(getListPathParam(), 'imap'); + var class_name = 'imap_' + detail.server_id + '_' + msg_uid + '_' + detail.folder; + var key = 'imap_' + Hm_Utils.get_url_page_number() + '_' + getListPathParam(); + var next_message = Hm_Message_List.prev_next_links(key, class_name)[1]; + if (next_message) { + $('.compose_next_email_data').val(next_message); + } } var uploaded_files = $("input[name='uploaded_files[]']").map(function () { return $(this).val(); }).get(); @@ -352,6 +353,7 @@ var process_compose_form = function(){ $('.smtp_send_archive').addClass('disabled_input'); $('.smtp_send').on("click", function () { return false; }); } + var force_send_message = function() { // Check if the force_send input already exists var forceSendInput = document.getElementById('force_send'); From ec0aefeed6318a7c7051d21b0dd847a6d1c81b9b Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Thu, 28 Nov 2024 20:09:58 +0200 Subject: [PATCH 29/35] EWS: folder rename fixes, move folder to other parent implementation, sieve fixes for ews --- modules/core/hm-mailbox.php | 3 ++ modules/imap/hm-ews.php | 61 ++++++++++++++++++++++---------- modules/imap_folders/modules.php | 4 +-- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/modules/core/hm-mailbox.php b/modules/core/hm-mailbox.php index 24483f47fa..c6633c9c23 100644 --- a/modules/core/hm-mailbox.php +++ b/modules/core/hm-mailbox.php @@ -135,6 +135,9 @@ public function rename_folder($folder, $new_name, $parent = null) { return $this->connection->rename_mailbox($old_folder, $new_folder); } else { $folder = decode_folder_str($folder); + if ($parent) { + $parent = decode_folder_str($parent); + } return $this->connection->rename_folder($folder, $new_name, $parent); } } diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 887c050ac2..c63faa8a28 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -17,6 +17,7 @@ use garethp\ews\API\ItemUpdateBuilder; use garethp\ews\API\Type; use garethp\ews\MailAPI; +use garethp\ews\Utilities; use ZBateson\MailMimeParser\MailMimeParser; @@ -40,7 +41,7 @@ class Hm_EWS { public function connect(array $config) { try { - $this->ews = ExchangeWebServices::fromUsernameAndPassword($config['server'], $config['username'], $config['password'], ['version' => ExchangeWebServices::VERSION_2016]); + $this->ews = ExchangeWebServices::fromUsernameAndPassword($config['server'], $config['username'], $config['password'], ['version' => ExchangeWebServices::VERSION_2016, 'trace' => 1]); $this->api = new MailAPI($this->ews); $this->api->getFolderByDistinguishedId(Enumeration\DistinguishedFolderIdNameType::INBOX); $this->authed = true; @@ -181,34 +182,58 @@ public function create_folder($folder, $parent = null) { public function rename_folder($folder, $new_name, $parent = null) { $result = []; + if ($this->is_distinguished_folder($folder)) { + $folder = new Type\DistinguishedFolderIdType($folder); + } else { + $folder = new Type\FolderIdType($folder); + } $new_folder = new Type\FolderType(); $new_folder->displayName = $new_name; - if ($parent) { - $new_folder->parentFolderId = new Type\FolderIdType($parent); - } - $setFolderField = new Type\SetFolderFieldType(); - $setFolderField->folder = $new_folder; - $fieldURI = new Type\FieldURI(); - $fieldURI->fieldURI = 'folder:displayName'; - $setFolderField->fieldURI = $fieldURI; - $updates = new Type\NonEmptyArrayOfFolderChangeDescriptionsType(); - $updates->set('setFolderField', [$setFolderField]); - $change = new Type\FolderChangeType(); - $change->folderId = new Type\FolderIdType($folder); - $change->updates = $updates; $request = [ 'FolderChanges' => [ - $change + 'FolderChange' => [ + 'FolderId' => $folder->toArray(false), + 'Updates' => [ + 'SetFolderField' => [ + 'FieldURI' => [ + 'FieldURI' => 'folder:DisplayName', + ], + 'Folder' => $new_folder, + ], + ], + ], ], ]; try { - $resp = $this->ews->UpdateFolder($request); - // TODO: EWS: resolve internal server error issue and return status - return true; + $request = Type::buildFromArray($request); + $this->ews->UpdateFolder($request); } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; } + if ($parent) { + if ($this->is_distinguished_folder($parent)) { + $parent = new Type\DistinguishedFolderIdType($parent); + } else { + $parent = new Type\FolderIdType($parent); + } + $request = [ + 'FolderIds' => Utilities\getFolderIds([$folder]), + 'ToFolderId' => $parent->toArray(true), + ]; + try { + $request = Type::buildFromArray($request); + $this->ews->MoveFolder($request); + } catch (\Exception $e) { + var_dump($folder->toArray(true)); + echo $this->ews->getClient()->__getLastRequest(); + echo $this->ews->getClient()->__getLastResponse(); + exit; + Hm_Msgs::add('ERR' . $e->getMessage()); + return false; + } + } + return true; } public function delete_folder($folder) { diff --git a/modules/imap_folders/modules.php b/modules/imap_folders/modules.php index 02716058d5..7daa5f9823 100644 --- a/modules/imap_folders/modules.php +++ b/modules/imap_folders/modules.php @@ -206,7 +206,7 @@ public function process() { $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); if ($mailbox && $mailbox->authed()) { if ($form['folder'] && $form['new_folder'] && $mailbox->rename_folder($form['folder'], $form['new_folder'], $parent)) { - if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER)) { + if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER) && $mailbox->is_imap()) { $imap_servers = $this->user_config->get('imap_servers'); $imap_account = $imap_servers[$form['imap_server_id']]; $linked_mailboxes = get_sieve_linked_mailbox($imap_account, $this); @@ -263,7 +263,7 @@ public function process() { if ($success) { $mailbox = Hm_IMAP_List::get_connected_mailbox($form['imap_server_id'], $this->cache); if ($mailbox && $mailbox->authed()) { - if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER)) { + if ($this->module_is_supported('sievefilters') && $this->user_config->get('enable_sieve_filter_setting', DEFAULT_ENABLE_SIEVE_FILTER) && $mailbox->is_imap()) { $del_folder = prep_folder_name($mailbox->get_connection(), $form['folder'], true); if (is_mailbox_linked_with_filters($del_folder, $form['imap_server_id'], $this)) { Hm_Msgs::add('ERRThis folder can\'t be deleted because it is used in a filter.'); From 693c4f5912e600b93bfa4cafd3999448ffe80cbc Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 29 Nov 2024 14:29:04 +0200 Subject: [PATCH 30/35] remove debugging code --- modules/imap/hm-ews.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index c63faa8a28..3763339bfb 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -225,10 +225,6 @@ public function rename_folder($folder, $new_name, $parent = null) { $request = Type::buildFromArray($request); $this->ews->MoveFolder($request); } catch (\Exception $e) { - var_dump($folder->toArray(true)); - echo $this->ews->getClient()->__getLastRequest(); - echo $this->ews->getClient()->__getLastResponse(); - exit; Hm_Msgs::add('ERR' . $e->getMessage()); return false; } From f250f56947626833b4739b0f94ef1119913bc2ff Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 29 Nov 2024 18:38:40 +0200 Subject: [PATCH 31/35] ews: remove pending todo items, remove debugging info, return folder and item ids when certain operations are performed; fix snooze/unsnooze operations --- modules/core/hm-mailbox.php | 8 ++++++-- modules/imap/functions.php | 7 +++++-- modules/imap/handler_modules.php | 4 +++- modules/imap/hm-ews.php | 33 ++++++++++++++++++++------------ modules/imap/hm-imap.php | 1 + 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/modules/core/hm-mailbox.php b/modules/core/hm-mailbox.php index 1b24382838..eddcc63940 100644 --- a/modules/core/hm-mailbox.php +++ b/modules/core/hm-mailbox.php @@ -119,7 +119,11 @@ public function create_folder($folder, $parent = null) { } if ($this->is_imap()) { $new_folder = prep_folder_name($this->connection, $folder, false, $parent); - return $this->connection->create_mailbox($new_folder); + if ($this->connection->create_mailbox($new_folder)) { + return $new_folder; + } else { + return false; + } } else { return $this->connection->create_folder($folder, $parent); } @@ -249,7 +253,7 @@ public function get_special_use_mailboxes($folder = false) { * Get messages in a folder applying filters, sorting and pagination * @return array - [total results found, results for a single page] */ - public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, $limit=0, $keyword=false, $trusted_senders=[], $include_preview = false) { + public function get_messages($folder, $sort, $reverse, $flag_filter, $offset=0, $limit=50, $keyword=false, $trusted_senders=[], $include_preview = false) { if (! $this->select_folder($folder)) { return; } diff --git a/modules/imap/functions.php b/modules/imap/functions.php index 3364985bc5..5d4146703f 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -1335,8 +1335,11 @@ function snooze_message($mailbox, $msg_id, $folder, $snooze_tag) { $res = false; $snooze_folder = 'Snoozed'; if ($snooze_tag) { - if (!count($mailbox->get_folder_status($snooze_folder))) { - $mailbox->create_folder($snooze_folder); + $status = $mailbox->get_folder_status($snooze_folder); + if (! count($status)) { + $snooze_folder = $mailbox->create_folder($snooze_folder); + } else { + $snooze_folder = $status['id']; } if ($mailbox->store_message($snooze_folder, $msg)) { $deleteResult = $mailbox->message_action($folder, 'DELETE', array($msg_id)); diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index b23efc690c..4f7d53dbbc 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -1083,9 +1083,11 @@ public function process() { $mailbox = Hm_IMAP_List::get_connected_mailbox($server_id, $this->cache); if ($mailbox && $mailbox->authed()) { $folder = 'Snoozed'; - if (! count($mailbox->get_folder_status($folder))) { + $status = $mailbox->get_folder_status($folder); + if (! count($status)) { continue; } + $folder = $status['id']; $ret = $mailbox->get_messages($folder, 'DATE', false, 'ALL'); foreach ($ret[1] as $msg) { $msg_headers = $mailbox->get_message_headers($folder, $msg['uid']); diff --git a/modules/imap/hm-ews.php b/modules/imap/hm-ews.php index 5ebf0c6ed0..9b34fa7022 100644 --- a/modules/imap/hm-ews.php +++ b/modules/imap/hm-ews.php @@ -96,9 +96,8 @@ public function get_folders($folder = null, $only_subscribed = false, $unsubscri 'basename' => $name, 'realname' => $name, 'namespace' => '', - // TODO - flags - 'marked' => false, - 'noselect' => false, + 'marked' => false, // doesn't seem to be used anywhere but imap returns it + 'noselect' => false, // all EWS folders are selectable 'can_have_kids' => true, 'has_kids' => $folder->get('childFolderCount') > 0, 'children' => $folder->get('childFolderCount'), @@ -145,11 +144,18 @@ public function get_folder_status($folder) { try { if ($this->is_distinguished_folder($folder)) { $folder = new Type\DistinguishedFolderIdType($folder); - } else { + $result = $this->api->getFolder($folder->toArray(true)); + } elseif (base64_encode(base64_decode($folder, true)) === $folder) { $folder = new Type\FolderIdType($folder); + $result = $this->api->getFolder($folder->toArray(true)); + } else { + $result = $this->api->getFolderByDisplayName($folder, Enumeration\DistinguishedFolderIdNameType::MESSAGE_ROOT); + if (! $result) { + throw new Exception('Folder not found.'); + } } - $result = $this->api->getFolder($folder->toArray(true)); return [ + 'id' => $result->get('folderId')->get('id'), 'name' => $result->get('displayName'), 'messages' => $result->get('totalCount'), 'uidvalidity' => false, @@ -173,9 +179,14 @@ public function create_folder($folder, $parent = null) { $parent = new Type\FolderIdType($parent); } try { - $result = $this->api->createFolders([$folder], $parent); - print_r($result); - exit; + $request = [ + 'Folders' => ['Folder' => [ + 'DisplayName' => $folder + ]], + 'ParentFolderId' => $parent->toArray(true), + ]; + $result = $this->ews->CreateFolder($request); + return $result->get('id'); } catch(\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; @@ -288,9 +299,7 @@ public function store_message($folder, $message, $seen = true, $draft = false) { 'MessageDisposition' => 'SaveOnly', 'SavedItemFolderId' => $folder->toArray(true), ]); - print_r($result); - exit; - return true; + return $result->get('id'); } catch (\Exception $e) { Hm_Msgs::add('ERR' . $e->getMessage()); return false; @@ -386,7 +395,7 @@ public function search($folder, $sort, $reverse, $flag_filter, $offset, $limit, $qs[] = "Subject:($term[1])"; break; default: - // TODO: check for other types of terms + // noop } } } elseif (! empty($keyword)) { diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index 87b78ac2be..13e842665c 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -837,6 +837,7 @@ public function get_mailbox_status($mailbox, $args=array('UNSEEN', 'UIDVALIDITY' if ($this->check_response($response, true)) { $attributes = $this->parse_status_response($response); $this->check_mailbox_state_change($attributes); + $attributes['id'] = $mailbox; } return $attributes; } From c6a820541e186c20333b7a899c4650d6f181b992 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Fri, 29 Nov 2024 18:55:02 +0200 Subject: [PATCH 32/35] ews: fix reported errors --- modules/core/hm-mailbox.php | 6 +++++- modules/imap/output_modules.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/core/hm-mailbox.php b/modules/core/hm-mailbox.php index eddcc63940..1e88cdf216 100644 --- a/modules/core/hm-mailbox.php +++ b/modules/core/hm-mailbox.php @@ -507,7 +507,11 @@ public function dump_cache($type = 'string') { } public function get_state() { - return $this->connection->get_state(); + if ($this->is_imap()) { + return $this->connection->get_state(); + } else { + return $this->authed() ? 'authenticated' : 'disconnected'; + } } public function get_capability() { diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index 110f135e42..c72420fc6c 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -729,7 +729,7 @@ class Hm_Output_display_imap_status extends Hm_Output_Module { protected function output() { $res = ''; foreach ($this->get('imap_servers', array()) as $index => $vals) { - $res .= 'IMAP'.$vals['name'].''. + $res .= ''.(strtoupper($vals['type'] ?? 'IMAP')).''.$vals['name'].''. ''; } return $res; From 6bb83b64ee34dca5dbcd97050c6612cf09c31a44 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Mon, 2 Dec 2024 16:04:41 +0200 Subject: [PATCH 33/35] forward as attachment: fix attachment name to inherit from msg subject and crypt content on disk, so it can be loaded later by hm_mime_msg --- modules/smtp/modules.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/smtp/modules.php b/modules/smtp/modules.php index dbe474d870..5142ce3e3e 100644 --- a/modules/smtp/modules.php +++ b/modules/smtp/modules.php @@ -141,7 +141,7 @@ public function process() { if (!is_dir($file_dir)) { mkdir($file_dir); } - $name = $msg_header['subject'] . '.eml'; + $name = $msg_header['Subject'] . '.eml'; $file_path = $file_dir . $name; $attached_files[$this->request->get['uid']][] = array( 'name' => $name, @@ -149,8 +149,9 @@ public function process() { 'size' => strlen($content), 'tmp_name' => $file_path, 'filename' => $file_path, - 'basename' => $msg_header['subject'] + 'basename' => $msg_header['Subject'] ); + $content = Hm_Crypt::ciphertext($content, Hm_Request_Key::generate()); file_put_contents($file_path, $content); $this->session->set('uploaded_files', $attached_files); $this->out('as_attr', true); From 8375185173dd6d5b0212bb64e49d564e173e0a1f Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 3 Dec 2024 09:52:31 +0200 Subject: [PATCH 34/35] tags - fix message ids and ews handling --- modules/core/hm-mailbox.php | 5 ++++- modules/tags/handler_modules.php | 9 ++++----- modules/tags/hm-tags.php | 12 ++---------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/modules/core/hm-mailbox.php b/modules/core/hm-mailbox.php index 1e88cdf216..1a5a9ea63b 100644 --- a/modules/core/hm-mailbox.php +++ b/modules/core/hm-mailbox.php @@ -545,7 +545,7 @@ public function search($folder, $target='ALL', $uids=false, $terms=array(), $ese public function get_message_list($folder, $msg_ids) { if (! $this->select_folder($folder)) { - return; + return []; } if ($this->is_imap()) { return $this->connection->get_message_list($msg_ids); @@ -579,6 +579,9 @@ protected function select_folder($folder) { } } else { $this->folder_state = $this->get_folder_status($folder); + if (! $this->folder_state) { + return false; + } $this->selected_folder = ['id' => $folder, 'name' => $this->folder_state['name'], 'detail' => []]; } return true; diff --git a/modules/tags/handler_modules.php b/modules/tags/handler_modules.php index d0f95353ac..a44b6b4ea5 100644 --- a/modules/tags/handler_modules.php +++ b/modules/tags/handler_modules.php @@ -33,6 +33,7 @@ public function process() { foreach ($ids as $msg_part) { list($imap_server_id, $msg_id, $folder) = explode('_', $msg_part); $folder = hex2bin($folder); + $msg_id = hex2bin($msg_id); $tagged = Hm_Tags::addMessage($form['tag_id'], $imap_server_id, $folder, $msg_id); if ($tagged) { $taged_messages++; @@ -135,18 +136,16 @@ public function process() { foreach ($ids as $serverId) { $folders = Hm_Tags::getFolders($tag_id, $serverId); if (!empty($folders)) { - $cache = Hm_IMAP_List::get_cache($this->cache, $serverId); - $imap = Hm_IMAP_List::connect($serverId, $cache); + $mailbox = Hm_IMAP_List::get_connected_mailbox($serverId, $this->cache); $server_details = Hm_IMAP_List::dump($serverId); - if (imap_authed($imap)) { + if ($mailbox && $mailbox->authed()) { foreach ($folders as $folder => $messageIds) { - $imap->select_mailbox($folder); $messages = array_map(function($msg) use ($serverId, $folder, $server_details) { $msg['server_id'] = $serverId; $msg['folder'] = bin2hex($folder); $msg['server_name'] = $server_details['name']; return $msg; - }, $imap->get_message_list($messageIds)); + }, $mailbox->get_message_list($folder, $messageIds)); $msg_list = array_merge($msg_list, $messages); } } diff --git a/modules/tags/hm-tags.php b/modules/tags/hm-tags.php index 5155b8632c..d79580f790 100644 --- a/modules/tags/hm-tags.php +++ b/modules/tags/hm-tags.php @@ -39,16 +39,8 @@ public static function addMessage($tagId, $serverId, $folder, $messageId) { public static function registerFolder($tag_id, $serverId, $folder) { $tag = self::get($tag_id); - if (isset($tag['server'])) { - if (isset($tag['server'][$serverId])) { - if (!in_array($folder, $tag['server'][$serverId])) { - $tag['server'][$serverId][] = $folder; - } - } else { - $tag['server'][$serverId] = array($folder); - } - } else { - $tag['server'] = [$serverId => [$folder]]; + if (! isset($tag['server'][$serverId][$folder])) { + $tag['server'][$serverId][$folder] = []; } self::edit($tag_id, $tag); } From c1f82d08f780b0568ab05ecec5ebab596f64eda3 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Mon, 16 Dec 2024 14:03:29 +0200 Subject: [PATCH 35/35] upgrade to latest php-ews master --- composer.lock | 105 +++++++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/composer.lock b/composer.lock index 5d9e35291d..d9357206d1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4d31bd501d7ee863550c2d2342d8cf6b", + "content-hash": "6a75aa856405dde3a37887d21821e90a", "packages": [ { "name": "bacon/bacon-qr-code", @@ -349,12 +349,12 @@ "source": { "type": "git", "url": "https://github.com/Garethp/php-ews.git", - "reference": "2ca0d30ddd3439bc0ec0b41de4fcb737ee0d4438" + "reference": "3d0d9e23e1e3f9077403266c256fb3c1219eef16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Garethp/php-ews/zipball/2ca0d30ddd3439bc0ec0b41de4fcb737ee0d4438", - "reference": "2ca0d30ddd3439bc0ec0b41de4fcb737ee0d4438", + "url": "https://api.github.com/repos/Garethp/php-ews/zipball/3d0d9e23e1e3f9077403266c256fb3c1219eef16", + "reference": "3d0d9e23e1e3f9077403266c256fb3c1219eef16", "shasum": "" }, "require": { @@ -405,9 +405,9 @@ ], "support": { "issues": "https://github.com/Garethp/php-ews/issues", - "source": "https://github.com/Garethp/php-ews/tree/master" + "source": "https://github.com/Garethp/php-ews/tree/v0.10.2" }, - "time": "2024-08-16T08:57:08+00:00" + "time": "2024-12-14T14:36:26+00:00" }, { "name": "guzzlehttp/guzzle", @@ -805,16 +805,16 @@ }, { "name": "league/commonmark", - "version": "2.5.3", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "b650144166dfa7703e62a22e493b853b58d874b0" + "reference": "d150f911e0079e90ae3c106734c93137c184f932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0", - "reference": "b650144166dfa7703e62a22e493b853b58d874b0", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d150f911e0079e90ae3c106734c93137c184f932", + "reference": "d150f911e0079e90ae3c106734c93137c184f932", "shasum": "" }, "require": { @@ -839,8 +839,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 || ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -850,7 +851,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.6-dev" + "dev-main": "2.7-dev" } }, "autoload": { @@ -907,7 +908,7 @@ "type": "tidelift" } ], - "time": "2024-08-16T11:46:16+00:00" + "time": "2024-12-07T15:34:16+00:00" }, { "name": "league/config", @@ -1555,16 +1556,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -1602,7 +1603,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -1618,7 +1619,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/dotenv", @@ -1693,20 +1694,20 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -1717,8 +1718,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1752,7 +1753,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -1768,7 +1769,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-iconv", @@ -1876,8 +1877,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1950,8 +1951,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2012,16 +2013,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.8", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "52903de178d542850f6f341ba92995d3d63e60c9" + "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9", - "reference": "52903de178d542850f6f341ba92995d3d63e60c9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", + "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", "shasum": "" }, "require": { @@ -2064,7 +2065,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.8" + "source": "https://github.com/symfony/yaml/tree/v6.4.13" }, "funding": [ { @@ -2080,7 +2081,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "thomaspark/bootswatch", @@ -2480,16 +2481,16 @@ "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -2528,7 +2529,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -2536,7 +2537,7 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", @@ -3037,16 +3038,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.38", + "version": "10.5.39", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132" + "reference": "4e89eff200b801db58f3d580ad7426431949eaa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132", - "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4e89eff200b801db58f3d580ad7426431949eaa9", + "reference": "4e89eff200b801db58f3d580ad7426431949eaa9", "shasum": "" }, "require": { @@ -3056,7 +3057,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -3118,7 +3119,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.39" }, "funding": [ { @@ -3134,7 +3135,7 @@ "type": "tidelift" } ], - "time": "2024-10-28T13:06:21+00:00" + "time": "2024-12-11T10:51:07+00:00" }, { "name": "sebastian/cli-parser",