diff --git a/dev/App/User.js b/dev/App/User.js index 7fa35f0ab..8b9a99e39 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -657,20 +657,20 @@ class AppUser extends AbstractApp { } /** - * @param {string} sFullNameHash + * @param {string} sFullName * @param {boolean} bExpanded */ - setExpandedFolder(sFullNameHash, bExpanded) { + setExpandedFolder(sFullName, bExpanded) { let aExpandedList = Local.get(ClientSideKeyName.ExpandedFolders); if (!isArray(aExpandedList)) { aExpandedList = []; } if (bExpanded) { - if (!aExpandedList.includes(sFullNameHash)) - aExpandedList.push(sFullNameHash); + if (!aExpandedList.includes(sFullName)) + aExpandedList.push(sFullName); } else { - aExpandedList = aExpandedList.filter(value => value !== sFullNameHash); + aExpandedList = aExpandedList.filter(value => value !== sFullName); } Local.set(ClientSideKeyName.ExpandedFolders, aExpandedList); diff --git a/dev/Common/Cache.js b/dev/Common/Cache.js index 2fb32866e..445e12988 100644 --- a/dev/Common/Cache.js +++ b/dev/Common/Cache.js @@ -3,8 +3,6 @@ import { isArray } from 'Common/Utils'; let FOLDERS_CACHE = {}, FOLDERS_NAME_CACHE = {}, - FOLDERS_HASH_CACHE = {}, - FOLDERS_UID_NEXT_CACHE = {}, MESSAGE_FLAGS_CACHE = {}, NEW_MESSAGE_CACHE = {}, REQUESTED_MESSAGE_CACHE = {}, @@ -17,8 +15,6 @@ export const clearCache = () => { FOLDERS_CACHE = {}; FOLDERS_NAME_CACHE = {}; - FOLDERS_HASH_CACHE = {}; - FOLDERS_UID_NEXT_CACHE = {}; MESSAGE_FLAGS_CACHE = {}; NEW_MESSAGE_CACHE = {}; REQUESTED_MESSAGE_CACHE = {}; @@ -89,9 +85,10 @@ export const * @param {string} folderFullName * @param {?FolderModel} folder */ - setFolder = (folderHash, folderFullName, folder) => { - FOLDERS_CACHE[folderFullName] = folder; - FOLDERS_NAME_CACHE[folderHash] = folderFullName; + setFolder = folder => { + folder.hash = ''; + FOLDERS_CACHE[folder.fullName] = folder; + FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName; }, /** @@ -99,37 +96,35 @@ export const * @returns {string} */ getFolderHash = folderFullName => - folderFullName && FOLDERS_HASH_CACHE[folderFullName] ? FOLDERS_HASH_CACHE[folderFullName] : '', + FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '', /** * @param {string} folderFullName * @param {string} folderHash */ setFolderHash = (folderFullName, folderHash) => - folderFullName && (FOLDERS_HASH_CACHE[folderFullName] = folderHash), + FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash), /** * @param {string} folderFullName * @returns {string} */ getFolderUidNext = folderFullName => - folderFullName && FOLDERS_UID_NEXT_CACHE[folderFullName] - ? FOLDERS_UID_NEXT_CACHE[folderFullName] - : '', + FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0, /** * @param {string} folderFullName * @param {string} uidNext */ setFolderUidNext = (folderFullName, uidNext) => - FOLDERS_UID_NEXT_CACHE[folderFullName] = uidNext, + FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext), /** * @param {string} folderFullName * @returns {?FolderModel} */ getFolderFromCacheList = folderFullName => - folderFullName && FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null, + FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null, /** * @param {string} folderFullName @@ -156,9 +151,7 @@ export class MessageFlagsCache * @returns {?Array} */ static getFor(folderFullName, uid) { - return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid] - ? MESSAGE_FLAGS_CACHE[folderFullName][uid] - : null; + return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]; } /** diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index f88139997..8c6dae0bc 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -132,7 +132,7 @@ ko.bindingHandlers.dropmessages = { if (folder && folder.collapsed()) { dragTimer.start(() => { folder.collapsed(false); - rl.app.setExpandedFolder(folder.fullNameHash, true); + rl.app.setExpandedFolder(folder.fullName, true); }, 500); } } diff --git a/dev/Model/FolderCollection.js b/dev/Model/FolderCollection.js index 17cdda917..76d4b70dd 100644 --- a/dev/Model/FolderCollection.js +++ b/dev/Model/FolderCollection.js @@ -1,7 +1,7 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { UNUSED_OPTION_VALUE } from 'Common/Consts'; -import { isArray, getKeyByValue, forEachObjectEntry } from 'Common/Utils'; +import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils'; import { ClientSideKeyName, FolderType, FolderMetadataKeys } from 'Common/EnumsUser'; import * as Cache from 'Common/Cache'; import { Settings, SettingsGet } from 'Common/Globals'; @@ -79,7 +79,7 @@ export class FolderCollectionModel extends AbstractCollectionModel oCacheFolder.type(FolderType.Inbox); Cache.setFolderInboxName(oFolder.FullName); } - Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullName, oCacheFolder); + Cache.setFolder(oCacheFolder); } if (1 < type) { @@ -88,7 +88,7 @@ export class FolderCollectionModel extends AbstractCollectionModel oCacheFolder.collapsed(!expandedFolders || !isArray(expandedFolders) - || !expandedFolders.includes(oCacheFolder.fullNameHash)); + || !expandedFolders.includes(oCacheFolder.fullName)); if (oFolder.Extended) { if (oFolder.Extended.Hash) { @@ -180,7 +180,6 @@ export class FolderModel extends AbstractModel { super(); this.fullName = ''; - this.fullNameHash = ''; this.delimiter = ''; this.deep = 0; this.expires = 0; @@ -188,6 +187,9 @@ export class FolderModel extends AbstractModel { this.exists = true; +// this.hash = ''; +// this.uidNext = 0; + this.addObservables({ name: '', type: FolderType.User, @@ -218,6 +220,14 @@ export class FolderModel extends AbstractModel { this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 }); } + /** + * For url safe '/#/mailbox/...' path + */ + get fullNameHash() { + return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe); +// return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName); + } + /** * @static * @param {FetchJsonFolder} json diff --git a/dev/Screen/User/MailBox.js b/dev/Screen/User/MailBox.js index 6f92edf58..92dfb8de5 100644 --- a/dev/Screen/User/MailBox.js +++ b/dev/Screen/User/MailBox.js @@ -135,10 +135,11 @@ export class MailBoxUserScreen extends AbstractScreen { return [ [/^([^/]*)$/, { normalize_: fNormS }], - [/^([a-zA-Z0-9~]+)\/(.+)\/?$/, { normalize_: (request, vals) => + // Regex the fullNameHash + [/^([a-zA-Z0-9.~_-]+)\/(.+)\/?$/, { normalize_: (request, vals) => [folder(request, vals), 1, decodeURI(pString(vals[1]))] }], - [/^([a-zA-Z0-9~]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }] + [/^([a-zA-Z0-9.~_-]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }] ]; } } diff --git a/dev/Settings/User/Folders.js b/dev/Settings/User/Folders.js index acdedec2d..88c0320de 100644 --- a/dev/Settings/User/Folders.js +++ b/dev/Settings/User/Folders.js @@ -5,7 +5,7 @@ import { ClientSideKeyName, FolderMetadataKeys } from 'Common/EnumsUser'; import { Settings } from 'Common/Globals'; import { getNotification } from 'Common/Translator'; -import { removeFolderFromCacheList } from 'Common/Cache'; +import { setFolder, removeFolderFromCacheList } from 'Common/Cache'; import { Capa } from 'Common/Enums'; import { defaultOptionsAfterRender } from 'Common/Utils'; import { initOnStartOrLangChange, i18n } from 'Common/Translator'; @@ -71,18 +71,28 @@ export class FoldersUserSettings /*extends AbstractViewSettings*/ { if (nameToEdit && folder.name() !== nameToEdit) { Local.set(ClientSideKeyName.FoldersLashHash, ''); - - rl.app.foldersPromisesActionHelper( - Remote.post('FolderRename', FolderUserStore.foldersRenaming, { + Remote + .post('FolderRename', FolderUserStore.foldersRenaming, { Folder: folder.fullName, NewFolderName: nameToEdit - }), - Notification.CantRenameFolder - ); - - removeFolderFromCacheList(folder.fullName); - - folder.name(nameToEdit); + }) + .then(data => { + folder.name(nameToEdit/*data.Name*/); + if (folder.subFolders.length) { + Remote.foldersReloadWithTimeout(); + // rename all subfolders folder.delimiter + } else { + removeFolderFromCacheList(folder.fullName); + data = data.Result; + folder.fullName = data.FullName; + setFolder(folder); + } + }) + .catch(error => { + FolderUserStore.folderListError( + getNotification(error.code, '', Notification.CantRenameFolder) + + '.\n' + error.message); + }); } folder.edited(false); diff --git a/dev/View/User/MailBox/FolderList.js b/dev/View/User/MailBox/FolderList.js index bdfcfedda..46b9fda51 100644 --- a/dev/View/User/MailBox/FolderList.js +++ b/dev/View/User/MailBox/FolderList.js @@ -69,7 +69,7 @@ export class MailFolderList extends AbstractViewLeft { const folder = ko.dataFor(el); if (folder) { const collapsed = folder.collapsed(); - rl.app.setExpandedFolder(folder.fullNameHash, collapsed); + rl.app.setExpandedFolder(folder.fullName, collapsed); folder.collapsed(!collapsed); event.preventDefault(); @@ -151,7 +151,7 @@ export class MailFolderList extends AbstractViewLeft { folder = item && ko.dataFor(item); if (folder) { const collapsed = folder.collapsed(); - rl.app.setExpandedFolder(folder.fullNameHash, collapsed); + rl.app.setExpandedFolder(folder.fullName, collapsed); folder.collapsed(!collapsed); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index 75031ea81..c59bd66ec 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -1361,22 +1361,43 @@ class MailClient */ public function FolderMove(string $sPrevFolderFullName, string $sNextFolderFullNameInUtf, bool $bSubscribeOnMove = true) : self { - return $this->folderModify($sPrevFolderFullName, $sNextFolderFullNameInUtf, false, $bSubscribeOnMove); + if (!$this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName)) { + // TODO: Translate + throw new Exceptions\RuntimeException('Cannot move non-existent folder.'); + } + return $this->folderModify($sPrevFolderFullName, $sNextFolderFullNameInUtf, $bSubscribeOnMove); } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException */ - public function FolderRename(string $sPrevFolderFullName, string $sNewTopFolderNameInUtf, bool $bSubscribeOnRename = true) : self + public function FolderRename(string $sPrevFolderFullName, string $sNewTopFolderNameInUtf, bool $bSubscribeOnRename = true) : string { - return $this->folderModify($sPrevFolderFullName, $sNewTopFolderNameInUtf, true, $bSubscribeOnRename); + $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName); + if (!$sDelimiter) { + // TODO: Translate + throw new Exceptions\RuntimeException('Cannot rename non-existent folder.'); + } + + if (\strlen($sDelimiter) && false !== \strpos($sNewTopFolderNameInUtf, $sDelimiter)) { + // TODO: Translate + throw new Exceptions\RuntimeException('New folder name contains delimiter.'); + } + + $iLast = \strrpos($sPrevFolderFullName, $sDelimiter); + $sNewFolderFullName = (false === $iLast ? '' : \substr($sPrevFolderFullName, 0, $iLast + 1)) + . $sNewTopFolderNameInUtf; + + $this->folderModify($sPrevFolderFullName, $sNewFolderFullName, $bSubscribeOnRename); + + return $sNewFolderFullName; } /** * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Base\Exceptions\RuntimeException */ - protected function folderModify(string $sPrevFolderFullName, string $sNewFolderFullName, bool $bRename, bool $bSubscribeOnModify) : self + protected function folderModify(string $sPrevFolderFullName, string $sNewFolderFullName, bool $bSubscribe) : self { if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) { @@ -1384,7 +1405,7 @@ class MailClient } $aSubscribeFolders = array(); - if ($bSubscribeOnModify) + if ($bSubscribe) { $aSubscribeFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*'); foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) @@ -1393,27 +1414,6 @@ class MailClient } } - if ($bRename) - { - $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName); - if (!$sDelimiter) - { - // TODO: Translate - throw new Exceptions\RuntimeException('Cannot '.($bRename?'rename':'move').' non-existent folder.'); - } - - $iLast = \strrpos($sPrevFolderFullName, $sDelimiter); - - if (\strlen($sDelimiter) && false !== \strpos($sNewFolderFullName, $sDelimiter)) - { - // TODO: Translate - throw new Exceptions\RuntimeException('New folder name contains delimiter.'); - } - - $sNewFolderFullName = (false === $iLast ? '' : \substr($sPrevFolderFullName, 0, $iLast + 1)) - . $sNewFolderFullName; - } - $this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName); foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php index ce1335c12..3e6c966b4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php @@ -320,11 +320,12 @@ trait Folders { $this->initMailClientConnection(); + $sName = $this->GetActionParam('NewFolderName', ''); try { - $this->MailClient()->FolderRename( + $sFullName = $this->MailClient()->FolderRename( $this->GetActionParam('Folder', ''), - $this->GetActionParam('NewFolderName', ''), + $sName, !!$this->Config()->Get('labs', 'use_imap_list_subscribe', true) ); } @@ -333,7 +334,11 @@ trait Folders throw new ClientException(Notifications::CantRenameFolder, $oException); } - return $this->TrueResponse(__FUNCTION__); +// FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array()) + return $this->DefaultResponse(__FUNCTION__, array( + 'Name' => $sName, + 'FullName' => $sFullName, + )); } /** diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index d5cdb024b..22c7a7de3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -139,12 +139,6 @@ trait Response return $bResult; } - private function hashFolderFullName(string $sFolderFullName) : string - { -// return \strspn(\mb_strtolower($sFolderFullName), ':/#?') ? \md5($sFolderFullName) : $sFolderFullName; - return \preg_match('/^[a-z0-9]+$/iu', $sFolderFullName) ? $sFolderFullName : \md5($sFolderFullName); - } - /** * @param mixed $mResponse * @@ -382,7 +376,6 @@ trait Response return \array_merge( $mResponse->jsonSerialize(), array( - 'FullNameHash' => $this->hashFolderFullName($mResponse->FullName()), 'Checkable' => \in_array($mResponse->FullName(), $this->aCheckableFolder), 'Extended' => $aExtended, 'SubFolders' => $this->responseObject($mResponse->SubFolders(), $sParent, $aParameters)