From 562b349fd4ade4f11f04c31ef168215cefa63404 Mon Sep 17 00:00:00 2001 From: Alfred Syatsukwa Date: Sat, 18 Jan 2025 10:43:58 +0200 Subject: [PATCH] fix(other): subfolders not appearing in IMAP accounts with XEAMS mail server --- modules/core/hm-mailbox.php | 2 +- modules/imap/hm-imap-parser.php | 9 ++- modules/imap/hm-imap.php | 115 ++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/modules/core/hm-mailbox.php b/modules/core/hm-mailbox.php index 61d9340254..6fc0fc5186 100644 --- a/modules/core/hm-mailbox.php +++ b/modules/core/hm-mailbox.php @@ -205,7 +205,7 @@ public function get_folders($only_subscribed = false) { return; } if ($this->is_imap()) { - return $this->connection->get_mailbox_list($only_subscribed); + return $this->connection->get_mailbox_list($only_subscribed, children_capability: $this->connection->server_support_children_capability()); } else { return $this->connection->get_folders(null, $only_subscribed, $this->user_config->get('unsubscribed_folders')[$this->server_id] ?? []); } diff --git a/modules/imap/hm-imap-parser.php b/modules/imap/hm-imap-parser.php index 7c66fd8ec9..4a69c08b24 100644 --- a/modules/imap/hm-imap-parser.php +++ b/modules/imap/hm-imap-parser.php @@ -257,7 +257,7 @@ protected function check_mailbox_state_change($attributes, $cached_state=false, * @param bool $lsub flag to use LSUB * @return array IMAP LIST/LSUB commands */ - protected function build_list_commands($lsub, $mailbox, $keyword) { + protected function build_list_commands($lsub, $mailbox, $keyword, $children_capability=true) { $commands = array(); if ($lsub) { $imap_command = 'LSUB'; @@ -297,7 +297,12 @@ protected function build_list_commands($lsub, $mailbox, $keyword) { else { $status = ''; } - $commands[] = array($imap_command.' "'.$namespace."\" \"$keyword\"$status\r\n", $namespace); + + if (!$children_capability && $namespace) { + $commands[] = array($imap_command.' " " "'.$namespace."$keyword\"$status\r\n", $namespace); + } else { + $commands[] = array($imap_command.' "'.$namespace."\" \"$keyword\"$status\r\n", $namespace); + } } return $commands; } diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index 9d50ad4351..0a304b63f4 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -526,14 +526,15 @@ public function get_special_use_mailboxes($type=false) { * @param bool $lsub flag to limit results to subscribed folders only * @return array associative array of folder details */ - public function get_mailbox_list($lsub=false, $mailbox='', $keyword='*') { + public function get_mailbox_list($lsub=false, $mailbox='', $keyword='*', $children_capability=true) { /* defaults */ $folders = array(); $excluded = array(); $parents = array(); $delim = false; $inbox = false; - $commands = $this->build_list_commands($lsub, $mailbox, $keyword); + $commands = $this->build_list_commands($lsub, $mailbox, $keyword, $children_capability); + $cache_command = implode('', array_map(function($v) { return $v[0]; }, $commands)).(string)$mailbox.(string)$keyword; $cache = $this->check_cache($cache_command); if ($cache !== false) { @@ -547,6 +548,11 @@ public function get_mailbox_list($lsub=false, $mailbox='', $keyword='*') { $this->send_command($command); $result = $this->get_response($this->folder_max, true); + if (!$children_capability) { + $delim = $result[0][count($result[0]) - 2]; + $result = $this->preprocess_folders($result, $mailbox, $delim); + } + /* loop through the "parsed" response. Each iteration is one folder */ foreach ($result as $vals) { @@ -702,6 +708,68 @@ public function get_mailbox_list($lsub=false, $mailbox='', $keyword='*') { return $this->cache_return_val($folders, $cache_command); } + /** + * Preprocess the folder list to determine if a folder has children + * @param array $result the folder list + * @param string $mailbox the mailbox to limit the results to + * @param string $delim the folder delimiter + * @return array the processed folder list + */ + function preprocess_folders($result, $mailbox, $delim) { + $folderPaths = []; + $processedResult = []; + + // Step 1: Extract all folder paths from the array (using the last element in each sub-array) + foreach ($result as $entry) { + if (isset($entry[count($entry) - 1]) && is_string($entry[count($entry) - 1])) { + $folderPaths[] = $entry[count($entry) - 1]; + } + } + + // Step 2: Process each folder to determine if it has subfolders + foreach ($result as $entry) { + if (isset($entry[count($entry) - 1]) && is_string($entry[count($entry) - 1])) { + $currentFolder = $entry[count($entry) - 1]; + $hasChildren = false; + + // Check if any other folder starts with the current folder followed by the delimiter + foreach ($folderPaths as $path) { + if (strpos($path, $currentFolder . $delim) === 0) { + $hasChildren = true; + break; + } + } + + // Add the appropriate flag (\HasChildren or \HasNoChildren) + $entry = array_merge( + array_slice($entry, 0, 3), + [$hasChildren ? "\\HasChildren" : "\\HasNoChildren"], + array_slice($entry, 3) + ); + + // Root folder processing + if (empty($mailbox)) { + if (strpos($currentFolder, $delim) === false) { + $processedResult[] = $entry; + } + } else { + // Process subfolders of the given mailbox + $expectedPrefix = $mailbox . $delim; + if (strpos($currentFolder, $expectedPrefix) === 0) { + $remainingPath = substr($currentFolder, strlen($expectedPrefix)); + // Include only direct subfolders (no further delimiters in the remaining path) + if (strpos($remainingPath, $delim) === false) { + $processedResult[] = $entry; + } + } + } + } else { + $processedResult[] = $entry; + } + } + return $processedResult; + } + /** * Sort a folder list with the inbox at the top */ @@ -2411,7 +2479,13 @@ public function get_mailbox_page($mailbox, $sort, $rev, $filter, $offset=0, $lim */ public function get_folder_list_by_level($level='', $only_subscribed=false, $with_input = false) { $result = array(); - $folders = $this->get_mailbox_list($only_subscribed, $level, '%'); + $folders = array(); + if ($this->server_support_children_capability()) { + $folders = $this->get_mailbox_list($only_subscribed, $level, '%'); + } else { + $folders = $this->get_mailbox_list($only_subscribed, $level, "*", false); + } + foreach ($folders as $name => $folder) { $result[$name] = array( 'name' => $folder['name'], @@ -2428,7 +2502,7 @@ public function get_folder_list_by_level($level='', $only_subscribed=false, $wit } } if ($only_subscribed || $with_input) { - $subscribed_folders = array_column($this->get_mailbox_list(true), 'name'); + $subscribed_folders = array_column($this->get_mailbox_list(true, children_capability:$this->server_support_children_capability()), 'name'); foreach ($result as $key => $folder) { $result[$key]['subscribed'] = in_array($folder['name'], $subscribed_folders); if (!$with_input) { @@ -2486,5 +2560,38 @@ protected function server_supports_custom_headers() { return true; } + + protected function server_support_children_capability() { + $test_command = 'CAPABILITY'."\r\n"; + $this->send_command($test_command); + $response = $this->get_response(false, true); + $status = $this->check_response($response, true); + + // Keywords that indicate the header search is not supported + $keywords = ['CHILDREN']; + + if (!$status) { + return false; + } + + // Flatten the response array to a single array of strings + $flattened_response = array_reduce($response, 'array_merge', []); + + // Check if all keywords are present in the flattened response + $sequence_match = true; + foreach ($keywords as $keyword) { + if (!in_array($keyword, $flattened_response)) { + $sequence_match = false; + break; + } + } + + // If all keywords are found, the header search is not supported + if ($sequence_match) { + return true; + } + + return false; + } } }