+Depends: nginx | apache2 | httpd, php-fpm | libapache2-mod-php, php-json, php-mbstring
+Recommends: php-intl, php-sodium, php-uuid
+Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
+Architecture: all
+Homepage: https://snappymail.eu
+Vcs-Browser: https://github.com/the-djmaze/snappymail
+Vcs-Git: https://github.com/the-djmaze/snappymail.git
+Description: SnappyMail is a PHP-based simple, modern, lightweight & fast web-based email client with no database requirements.
+ It supports IMAP, SMTP and Sieve protocols, multiple accounts and identities, an admin panel for configuration.
+ Plugins can be installed to further extend functionality.
+ Emails are not stored locally, but are accessed through IMAP.
diff --git a/build/deb/DEBIAN/postinst b/build/deb/DEBIAN/postinst
new file mode 100755
index 000000000..5c3c4b63b
--- /dev/null
+++ b/build/deb/DEBIAN/postinst
@@ -0,0 +1,2 @@
+#!/bin/sh
+chown -R www-data:www-data /var/lib/snappymail
diff --git a/build/plugins.php b/build/plugins.php
new file mode 100755
index 000000000..e2c5d3507
--- /dev/null
+++ b/build/plugins.php
@@ -0,0 +1,77 @@
+getConstants() as $key => $value) {
+ $key = \strtolower($key);
+ if (in_array($key, $keys)) {
+ $manifest_item[$key] = $value;
+ }
+ }
+ $version = $manifest_item['version'];
+ if (0 < floatval($version)) {
+ echo "+ {$name} {$version}\n";
+ $manifest_item['type'] = 'plugin';
+ $manifest_item['id'] = $name;
+ $manifest_item['file'] = "{$dir}-{$version}.tgz";
+ ksort($manifest_item);
+ $manifest[$name] = $manifest_item;
+ $tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
+ $tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
+ @unlink($tgz_destination);
+ @unlink("{$tar_destination}.gz");
+ $tar = new PharData($tar_destination);
+ $tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}", '/') . '/');
+ $tar->compress(Phar::GZ);
+ unlink($tar_destination);
+ rename("{$tar_destination}.gz", $tgz_destination);
+ if (Phar::canWrite()) {
+ $phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
+ @unlink($phar_destination);
+ $tar = new Phar($phar_destination);
+ $tar->buildFromDirectory("./plugins/{$name}/");
+ $tar->compress(Phar::GZ);
+ unlink($phar_destination);
+ rename("{$phar_destination}.gz", $phar_destination);
+ }
+ } else {
+ echo "- {$name} {$version}\n";
+ }
+ } else {
+ echo "- {$name}\n";
+ }
+}
+
+ksort($manifest);
+$manifest = json_encode(array_values($manifest));
+$manifest = str_replace('{"', "\n\t{\n\t\t\"", $manifest);
+$manifest = str_replace('"}', "\"\n\t}", $manifest);
+$manifest = str_replace('}]', "}\n]", $manifest);
+$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
+$manifest = str_replace('\/', '/', $manifest);
+file_put_contents(PLUGINS_DEST_DIR . "/packages.json", $manifest);
+exit;
diff --git a/dev/App/Abstract.js b/dev/App/Abstract.js
index 99ddc4a5d..6ea1d7307 100644
--- a/dev/App/Abstract.js
+++ b/dev/App/Abstract.js
@@ -1,9 +1,7 @@
import ko from 'ko';
-import {
- elementById,
- Settings
-} from 'Common/Globals';
+import { Settings, SettingsGet } from 'Common/Globals';
+import { changeTheme } from 'Common/Utils';
import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
@@ -25,12 +23,9 @@ export class AbstractApp {
this.Remote = Remote;
}
- logoutReload(close = false) {
+ logoutReload() {
const url = logoutLink();
- rl.hash.clear();
- close && window.close && window.close();
-
if (location.href !== url) {
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
} else {
@@ -38,25 +33,27 @@ export class AbstractApp {
}
}
+ refresh() {
+// rl.adminArea() || !translatorReload(false, );
+ rl.adminArea() || (
+ LanguageStore.language(SettingsGet('Language'))
+ & ThemeStore.populate()
+ & changeTheme(SettingsGet('Theme'))
+ );
+
+ this.start();
+ }
+
bootstart() {
const register = (key, ClassObject, templateID) => ko.components.register(key, {
template: { element: templateID || (key + 'Component') },
viewModel: {
createViewModel: (params, componentInfo) => {
params = params || {};
- params.element = null;
-
- if (componentInfo && componentInfo.element) {
- params.component = componentInfo;
- params.element = componentInfo.element;
-
- i18nToNodes(componentInfo.element);
-
- if (params.inline && ko.unwrap(params.inline)) {
- params.element.style.display = 'inline-block';
- }
+ i18nToNodes(componentInfo.element);
+ if (params.inline) {
+ componentInfo.element.style.display = 'inline-block';
}
-
return new ClassObject(params);
}
}
@@ -72,14 +69,7 @@ export class AbstractApp {
LanguageStore.populate();
ThemeStore.populate();
- }
- /**
- * @returns {void}
- */
- hideLoading() {
- elementById('rl-content').hidden = false;
- elementById('rl-loading').remove();
+ this.start();
}
-
}
diff --git a/dev/App/Admin.js b/dev/App/Admin.js
index b7073b9e7..bbf3e5f66 100644
--- a/dev/App/Admin.js
+++ b/dev/App/Admin.js
@@ -1,4 +1,4 @@
-import 'External/Admin/ko';
+import 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals';
@@ -16,12 +16,8 @@ class AdminApp extends AbstractApp {
this.weakPassword = ko.observable(false);
}
- bootstart() {
- super.bootstart();
-
- this.hideLoading();
-
- if (!Settings.app('allowAdminPanel')) {
+ start() {
+ if (!Settings.app('adminAllowed')) {
rl.route.root();
setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) {
@@ -30,8 +26,6 @@ class AdminApp extends AbstractApp {
} else {
startScreens([LoginAdminScreen]);
}
-
- progressJs.end();
}
}
diff --git a/dev/App/User.js b/dev/App/User.js
index c2b9bd087..75f12ec51 100644
--- a/dev/App/User.js
+++ b/dev/App/User.js
@@ -1,29 +1,23 @@
import 'External/User/ko';
-import { isArray, isNonEmptyArray, pInt, pString } from 'Common/Utils';
-import { isPosNumeric, delegateRunOnDestroy, mailToHelper } from 'Common/UtilsUser';
+import { isArray, pString } from 'Common/Utils';
+import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
import {
- Capa,
- Scope
-} from 'Common/Enums';
-
-import {
- Layout,
FolderType,
SetSystemFoldersNotification,
- MessageSetAction,
- ClientSideKeyName
+ ClientSideKeyNameFolderListSize
} from 'Common/EnumsUser';
import {
doc,
elementById,
- createElement,
$htmlCL,
Settings,
SettingsGet,
- leftPanelDisabled
+ leftPanelDisabled,
+ addEventsListener,
+ addShortcut
} from 'Common/Globals';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
@@ -36,43 +30,28 @@ import {
getFolderFromCacheList
} from 'Common/Cache';
-import {
- userBackground,
- mailBox,
- root,
- openPgpWorkerJs,
- openPgpJs
-} from 'Common/Links';
-
-import { getNotification, i18n } from 'Common/Translator';
+import { i18n, reloadTime } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { NotificationUserStore } from 'Stores/User/Notification';
import { AccountUserStore } from 'Stores/User/Account';
import { ContactUserStore } from 'Stores/User/Contact';
import { IdentityUserStore } from 'Stores/User/Identity';
-import { TemplateUserStore } from 'Stores/User/Template';
import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp';
-import { MessageUserStore } from 'Stores/User/Message';
-import { QuotaUserStore } from 'Stores/User/Quota';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
-import * as Local from 'Storage/Client';
-
import Remote from 'Remote/User/Fetch';
-import { EmailModel } from 'Model/Email';
import { AccountModel } from 'Model/Account';
import { IdentityModel } from 'Model/Identity';
-import { TemplateModel } from 'Model/Template';
-import { OpenPgpKeyModel } from 'Model/OpenPgpKey';
import { LoginUserScreen } from 'Screen/User/Login';
import { MailBoxUserScreen } from 'Screen/User/MailBox';
import { SettingsUserScreen } from 'Screen/User/Settings';
-import { startScreens, showScreenPopup } from 'Knoin/Knoin';
+import { startScreens, showScreenPopup, arePopupsVisible } from 'Knoin/Knoin';
import { AbstractApp } from 'App/Abstract';
@@ -80,228 +59,47 @@ import { ComposePopupView } from 'View/Popup/Compose';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { AskPopupView } from 'View/Popup/Ask';
-// Every 5 minutes
-const refreshFolders = 300000;
+import {
+ folderInformationMultiply,
+ refreshFoldersInterval,
+ messagesMoveHelper,
+ messagesDeleteHelper,
+ fetchFolderInformation
+} from 'Common/Folders';
+import { loadFolders } from 'Model/FolderCollection';
class AppUser extends AbstractApp {
constructor() {
super(Remote);
- this.moveCache = {};
-
- this.quotaDebounce = this.quota.debounce(30000);
- this.moveOrDeleteResponseHelper = this.moveOrDeleteResponseHelper.bind(this);
-
- this.messagesMoveTrigger = this.messagesMoveTrigger.debounce(500);
-
// wakeUp
const interval = 3600000; // 60m
- var lastTime = Date.now();
+ let lastTime = Date.now();
setInterval(() => {
const currentTime = Date.now();
if (currentTime > (lastTime + interval + 1000)) {
- if (rl.hash.check()) {
- this.reload();
- }
- Remote.jsVersion(iError => {
- if (100 < iError) {
- this.reload();
- }
- }, Settings.app('version'));
+ Remote.request('Version',
+ iError => (100 < iError) && (Settings.app('inIframe') ? parent : window).location.reload(),
+ { Version: Settings.app('version') }
+ );
}
lastTime = currentTime;
}, interval);
- if (rl.hash.check()) {
- this.reload();
- }
-
- if (SettingsGet('UserBackgroundHash')) {
- setTimeout(() => {
- const img = userBackground(SettingsGet('UserBackgroundHash'));
- if (img) {
- $htmlCL.add('UserBackground');
- doc.body.style.backgroundImage = "url("+img+")";
- }
- }, 1000);
- }
-
const fn = (ev=>$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey)).debounce(500);
- ['keydown','keyup'].forEach(t => doc.addEventListener(t, fn));
+ addEventsListener(doc, ['keydown','keyup'], fn);
- shortcuts.add('escape,enter', '', Scope.All, () => rl.Dropdowns.detectVisibility());
- }
-
- reload() {
- (Settings.app('inIframe') ? parent : window).location.reload();
- }
-
- reloadFlagsCurrentMessageListAndMessageFromCache() {
- MessageUserStore.list.forEach(message =>
- MessageFlagsCache.initMessage(message)
- );
- MessageFlagsCache.initMessage(MessageUserStore.message());
- MessageUserStore.messageViewTrigger(!MessageUserStore.messageViewTrigger());
- }
-
- /**
- * @param {boolean=} bDropPagePosition = false
- * @param {boolean=} bDropCurrenFolderCache = false
- */
- reloadMessageList(bDropPagePosition = false, bDropCurrenFolderCache = false) {
- let iOffset = (MessageUserStore.listPage() - 1) * SettingsUserStore.messagesPerPage();
-
- if (bDropCurrenFolderCache) {
- setFolderHash(FolderUserStore.currentFolderFullNameRaw(), '');
- }
-
- if (bDropPagePosition) {
- MessageUserStore.listPage(1);
- MessageUserStore.listPageBeforeThread(1);
- iOffset = 0;
-
- rl.route.setHash(
- mailBox(
- FolderUserStore.currentFolderFullNameHash(),
- MessageUserStore.listPage(),
- MessageUserStore.listSearch(),
- MessageUserStore.listThreadUid()
- ),
- true,
- true
- );
- }
-
- MessageUserStore.listLoading(true);
- MessageUserStore.listError('');
- Remote.messageList(
- (iError, oData, bCached) => {
- if (iError) {
- if (Notification.RequestAborted !== iError) {
- MessageUserStore.list([]);
- MessageUserStore.listError(getNotification(iError));
- }
- } else {
- MessageUserStore.setMessageList(oData, bCached);
- }
- MessageUserStore.listLoading(false);
- },
- {
- Folder: FolderUserStore.currentFolderFullNameRaw(),
- Offset: iOffset,
- Limit: SettingsUserStore.messagesPerPage(),
- Search: MessageUserStore.listSearch(),
- ThreadUid: MessageUserStore.listThreadUid()
- }
- );
- }
-
- recacheInboxMessageList() {
- Remote.messageList(null, {Folder: getFolderInboxName()}, true);
- }
-
- /**
- * @param {Function} fResultFunc
- * @returns {boolean}
- */
- contactsSync(fResultFunc) {
- if (
- ContactUserStore.importing() ||
- ContactUserStore.syncing() ||
- !ContactUserStore.enableSync() ||
- !ContactUserStore.allowSync()
- ) {
- return false;
- }
-
- ContactUserStore.syncing(true);
-
- Remote.contactsSync((sResult, oData) => {
- ContactUserStore.syncing(false);
-
- if (fResultFunc) {
- fResultFunc(sResult, oData);
- }
- });
-
- return true;
- }
-
- messagesMoveTrigger() {
- const sTrashFolder = FolderUserStore.trashFolder(),
- sSpamFolder = FolderUserStore.spamFolder();
-
- Object.values(this.moveCache).forEach(item => {
- const isSpam = sSpamFolder === item.To,
- isTrash = sTrashFolder === item.To,
- isHam = !isSpam && sSpamFolder === item.From && getFolderInboxName() === item.To;
-
- Remote.messagesMove(
- this.moveOrDeleteResponseHelper,
- item.From,
- item.To,
- item.Uid,
- isSpam ? 'SPAM' : isHam ? 'HAM' : '',
- isSpam || isTrash
- );
- });
-
- this.moveCache = {};
- }
-
- messagesMoveHelper(fromFolderFullNameRaw, toFolderFullNameRaw, uidsForMove) {
- const hash = '$$' + fromFolderFullNameRaw + '$$' + toFolderFullNameRaw + '$$';
- if (!this.moveCache[hash]) {
- this.moveCache[hash] = {
- From: fromFolderFullNameRaw,
- To: toFolderFullNameRaw,
- Uid: []
- };
- }
-
- this.moveCache[hash].Uid = this.moveCache[hash].Uid.concat(uidsForMove).unique();
- this.messagesMoveTrigger();
- }
-
- messagesCopyHelper(sFromFolderFullNameRaw, sToFolderFullNameRaw, aUidForCopy) {
- Remote.messagesCopy(this.moveOrDeleteResponseHelper, sFromFolderFullNameRaw, sToFolderFullNameRaw, aUidForCopy);
- }
-
- messagesDeleteHelper(sFromFolderFullNameRaw, aUidForRemove) {
- Remote.messagesDelete(this.moveOrDeleteResponseHelper, sFromFolderFullNameRaw, aUidForRemove);
- }
-
- moveOrDeleteResponseHelper(iError, oData) {
- if (iError) {
- setFolderHash(FolderUserStore.currentFolderFullNameRaw(), '');
- alert(getNotification(iError));
- } else if (FolderUserStore.currentFolder()) {
- if (isArray(oData.Result) && 2 === oData.Result.length) {
- setFolderHash(oData.Result[0], oData.Result[1]);
- } else {
- setFolderHash(FolderUserStore.currentFolderFullNameRaw(), '');
- }
- this.reloadMessageList(!MessageUserStore.list.length);
- this.quotaDebounce();
- }
- }
-
- /**
- * @param {string} sFromFolderFullNameRaw
- * @param {Array} aUidForRemove
- */
- deleteMessagesFromFolderWithoutCheck(sFromFolderFullNameRaw, aUidForRemove) {
- this.messagesDeleteHelper(sFromFolderFullNameRaw, aUidForRemove);
- MessageUserStore.removeMessagesFromList(sFromFolderFullNameRaw, aUidForRemove);
+ addShortcut('escape,enter', '', dropdownsDetectVisibility);
+ addEventListener('click', dropdownsDetectVisibility);
}
/**
* @param {number} iDeleteType
- * @param {string} sFromFolderFullNameRaw
+ * @param {string} sFromFolderFullName
* @param {Array} aUidForRemove
* @param {boolean=} bUseFolder = true
*/
- deleteMessagesFromFolder(iDeleteType, sFromFolderFullNameRaw, aUidForRemove, bUseFolder) {
+ deleteMessagesFromFolder(iDeleteType, sFromFolderFullName, aUidForRemove, bUseFolder) {
let oMoveFolder = null,
nSetSystemFoldersNotification = null;
@@ -340,125 +138,19 @@ class AppUser extends AbstractApp {
} else if (
!bUseFolder ||
(FolderType.Trash === iDeleteType &&
- (sFromFolderFullNameRaw === FolderUserStore.spamFolder()
- || sFromFolderFullNameRaw === FolderUserStore.trashFolder()))
+ (sFromFolderFullName === FolderUserStore.spamFolder()
+ || sFromFolderFullName === FolderUserStore.trashFolder()))
) {
showScreenPopup(AskPopupView, [
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
() => {
- this.messagesDeleteHelper(sFromFolderFullNameRaw, aUidForRemove);
- MessageUserStore.removeMessagesFromList(sFromFolderFullNameRaw, aUidForRemove);
+ messagesDeleteHelper(sFromFolderFullName, aUidForRemove);
+ MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, aUidForRemove);
}
]);
} else if (oMoveFolder) {
- this.messagesMoveHelper(sFromFolderFullNameRaw, oMoveFolder.fullNameRaw, aUidForRemove);
- MessageUserStore.removeMessagesFromList(sFromFolderFullNameRaw, aUidForRemove, oMoveFolder.fullNameRaw);
- }
- }
-
- /**
- * @param {string} sFromFolderFullNameRaw
- * @param {Array} aUidForMove
- * @param {string} sToFolderFullNameRaw
- * @param {boolean=} bCopy = false
- */
- moveMessagesToFolder(sFromFolderFullNameRaw, aUidForMove, sToFolderFullNameRaw, bCopy) {
- if (sFromFolderFullNameRaw !== sToFolderFullNameRaw && isArray(aUidForMove) && aUidForMove.length) {
- const oFromFolder = getFolderFromCacheList(sFromFolderFullNameRaw),
- oToFolder = getFolderFromCacheList(sToFolderFullNameRaw);
-
- if (oFromFolder && oToFolder) {
- if (undefined === bCopy ? false : !!bCopy) {
- this.messagesCopyHelper(oFromFolder.fullNameRaw, oToFolder.fullNameRaw, aUidForMove);
- } else {
- this.messagesMoveHelper(oFromFolder.fullNameRaw, oToFolder.fullNameRaw, aUidForMove);
- }
-
- MessageUserStore.removeMessagesFromList(oFromFolder.fullNameRaw, aUidForMove, oToFolder.fullNameRaw, bCopy);
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * @param {Function=} callback = null
- */
- foldersReload(callback = null) {
- Remote.foldersReload(callback);
- }
-
- foldersPromisesActionHelper(promise, errorDefCode) {
- Remote.abort('Folders')
- .fastResolve(true)
- .then(() => promise)
- .then(
- () => Remote.foldersReloadWithTimeout(),
- errorCode => {
- FolderUserStore.folderListError(getNotification(errorCode, '', errorDefCode));
- Remote.foldersReloadWithTimeout();
- }
- );
- }
-
- reloadOpenPgpKeys() {
- if (PgpUserStore.capaOpenPGP()) {
- const keys = [],
- email = new EmailModel(),
- openpgpKeyring = PgpUserStore.openpgpKeyring,
- openpgpKeys = openpgpKeyring ? openpgpKeyring.getAllKeys() : [];
-
- openpgpKeys.forEach((oItem, iIndex) => {
- if (oItem && oItem.primaryKey) {
- const aEmails = [],
- aUsers = [],
- primaryUser = oItem.getPrimaryUser(),
- user =
- primaryUser && primaryUser.user
- ? primaryUser.user.userId.userid
- : oItem.users && oItem.users[0]
- ? oItem.users[0].userId.userid
- : '';
-
- if (oItem.users) {
- oItem.users.forEach(item => {
- if (item.userId) {
- email.clear();
- email.parse(item.userId.userid);
- if (email.validate()) {
- aEmails.push(email.email);
- aUsers.push(item.userId.userid);
- }
- }
- });
- }
-
- if (aEmails.length) {
- keys.push(
- new OpenPgpKeyModel(
- iIndex,
- oItem.primaryKey.getFingerprint(),
- oItem.primaryKey
- .getKeyId()
- .toHex()
- .toLowerCase(),
- oItem.getKeyIds()
- .map(item => (item && item.toHex ? item.toHex() : null))
- .validUnique(),
- aUsers,
- aEmails,
- oItem.isPrivate(),
- oItem.armor(),
- user
- )
- );
- }
- }
- });
-
- delegateRunOnDestroy(PgpUserStore.openpgpkeys());
- PgpUserStore.openpgpkeys(keys);
+ messagesMoveHelper(sFromFolderFullName, oMoveFolder.fullName, aUidForRemove);
+ MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, aUidForRemove, oMoveFolder.fullName);
}
}
@@ -466,37 +158,35 @@ class AppUser extends AbstractApp {
AccountUserStore.loading(true);
IdentityUserStore.loading(true);
- Remote.accountsAndIdentities((iError, oData) => {
+ Remote.request('AccountsAndIdentities', (iError, oData) => {
AccountUserStore.loading(false);
IdentityUserStore.loading(false);
if (!iError) {
- const counts = {},
- sAccountEmail = AccountUserStore.email();
- let parentEmail = SettingsGet('ParentEmail') || sAccountEmail;
+ const
+// counts = {},
+ accounts = oData.Result.Accounts,
+ mainEmail = SettingsGet('MainEmail');
- if (isArray(oData.Result.Accounts)) {
- AccountUserStore.accounts.forEach(oAccount =>
- counts[oAccount.email] = oAccount.count()
- );
-
- delegateRunOnDestroy(AccountUserStore.accounts());
+ if (isArray(accounts)) {
+// AccountUserStore.accounts.forEach(oAccount => counts[oAccount.email] = oAccount.count());
AccountUserStore.accounts(
- oData.Result.Accounts.map(
- sValue => new AccountModel(sValue, sValue !== parentEmail, counts[sValue] || 0)
+ accounts.map(
+ sValue => new AccountModel(sValue/*, counts[sValue]*/)
)
);
+// accounts.length &&
+ AccountUserStore.accounts.unshift(new AccountModel(mainEmail/*, counts[mainEmail]*/, false));
}
if (isArray(oData.Result.Identities)) {
- delegateRunOnDestroy(IdentityUserStore());
-
IdentityUserStore(
oData.Result.Identities.map(identityData => {
- const id = pString(identityData.Id),
- email = pString(identityData.Email),
- identity = new IdentityModel(id, email);
+ const identity = new IdentityModel(
+ pString(identityData.Id),
+ pString(identityData.Email)
+ );
identity.name(pString(identityData.Name));
identity.replyTo(pString(identityData.ReplyTo));
@@ -512,107 +202,52 @@ class AppUser extends AbstractApp {
});
}
- templates() {
- TemplateUserStore.templates.loading(true);
-
- Remote.templates((iError, data) => {
- TemplateUserStore.templates.loading(false);
-
- if (!iError && isArray(data.Result.Templates)) {
- delegateRunOnDestroy(TemplateUserStore.templates());
-
- TemplateUserStore.templates(
- data.Result.Templates.map(templateData =>
- TemplateModel.reviveFromJson(templateData)
- ).filter(v => v)
- );
- }
- });
- }
-
- quota() {
- Remote.quota((iError, data) => {
- if (
- !iError &&
- isArray(data.Result) &&
- 1 < data.Result.length &&
- isPosNumeric(data.Result[0], true) &&
- isPosNumeric(data.Result[1], true)
- ) {
- QuotaUserStore.populateData(pInt(data.Result[1]), pInt(data.Result[0]));
- }
- });
- }
-
/**
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation(folder, list) {
if (folder && folder.trim()) {
- Remote.folderInformation(
+ fetchFolderInformation(
(iError, data) => {
- if (!iError && data.Result.Hash && data.Result.Folder) {
- let uid = '',
- check = false,
- unreadCountChange = false;
-
- const folderFromCache = getFolderFromCacheList(data.Result.Folder);
+ if (!iError && data.Result) {
+ const result = data.Result,
+ hash = getFolderHash(result.Folder),
+ folderFromCache = getFolderFromCacheList(result.Folder);
if (folderFromCache) {
folderFromCache.expires = Date.now();
- if (data.Result.Hash) {
- setFolderHash(data.Result.Folder, data.Result.Hash);
- }
+ setFolderHash(result.Folder, result.Hash);
- if (null != data.Result.MessageCount) {
- folderFromCache.messageCountAll(data.Result.MessageCount);
- }
+ folderFromCache.messageCountAll(result.MessageCount);
- if (null != data.Result.MessageUnseenCount) {
- if (pInt(folderFromCache.messageCountUnread()) !== pInt(data.Result.MessageUnseenCount)) {
- unreadCountChange = true;
- }
+ let unreadCountChange = (folderFromCache.messageCountUnread() !== result.MessageUnseenCount);
- folderFromCache.messageCountUnread(data.Result.MessageUnseenCount);
- }
+ folderFromCache.messageCountUnread(result.MessageUnseenCount);
if (unreadCountChange) {
- MessageFlagsCache.clearFolder(folderFromCache.fullNameRaw);
+ MessageFlagsCache.clearFolder(folderFromCache.fullName);
}
- if (data.Result.Flags) {
- for (uid in data.Result.Flags) {
- if (Object.prototype.hasOwnProperty.call(data.Result.Flags, uid)) {
- check = true;
- const flags = data.Result.Flags[uid];
- MessageFlagsCache.storeByFolderAndUid(folderFromCache.fullNameRaw, uid.toString(), [
- !!flags.IsUnseen,
- !!flags.IsFlagged,
- !!flags.IsAnswered,
- !!flags.IsForwarded,
- !!flags.IsReadReceipt
- ]);
- }
- }
+ if (result.Flags.length) {
+ result.Flags.forEach(message =>
+ MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
+ );
- if (check) {
- this.reloadFlagsCurrentMessageListAndMessageFromCache();
- }
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
}
- MessageUserStore.initUidNextAndNewMessages(
- folderFromCache.fullNameRaw,
- data.Result.UidNext,
- data.Result.NewMessages
+ MessagelistUserStore.initUidNextAndNewMessages(
+ folderFromCache.fullName,
+ result.UidNext,
+ result.NewMessages
);
- const hash = getFolderHash(data.Result.Folder);
- if (!hash || unreadCountChange || data.Result.Hash !== hash) {
- if (folderFromCache.fullNameRaw === FolderUserStore.currentFolderFullNameRaw()) {
- this.reloadMessageList();
- } else if (getFolderInboxName() === folderFromCache.fullNameRaw) {
- this.recacheInboxMessageList();
+ if (!hash || unreadCountChange || result.Hash !== hash) {
+ if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
+ MessagelistUserStore.reload();
+ } else if (getFolderInboxName() === folderFromCache.fullName) {
+ Remote.messageList(null, {Folder: getFolderInboxName()}, true);
}
}
}
@@ -624,403 +259,105 @@ class AppUser extends AbstractApp {
}
}
- /**
- * @param {boolean=} boot = false
- */
- folderInformationMultiply(boot = false) {
- const folders = FolderUserStore.getNextFolderNames(refreshFolders);
- if (isNonEmptyArray(folders)) {
- Remote.folderInformationMultiply((iError, oData) => {
- if (!iError && isNonEmptyArray(oData.Result.List)) {
- const utc = Date.now();
- oData.Result.List.forEach(item => {
- const hash = getFolderHash(item.Folder),
- folder = getFolderFromCacheList(item.Folder);
- let unreadCountChange = false;
-
- if (folder) {
- folder.expires = utc;
-
- if (item.Hash) {
- setFolderHash(item.Folder, item.Hash);
- }
-
- if (null != item.MessageCount) {
- folder.messageCountAll(item.MessageCount);
- }
-
- if (null != item.MessageUnseenCount) {
- if (pInt(folder.messageCountUnread()) !== pInt(item.MessageUnseenCount)) {
- unreadCountChange = true;
- }
-
- folder.messageCountUnread(item.MessageUnseenCount);
- }
-
- if (unreadCountChange) {
- MessageFlagsCache.clearFolder(folder.fullNameRaw);
- }
-
- if (!hash || item.Hash !== hash) {
- if (folder.fullNameRaw === FolderUserStore.currentFolderFullNameRaw()) {
- this.reloadMessageList();
- }
- } else if (unreadCountChange
- && folder.fullNameRaw === FolderUserStore.currentFolderFullNameRaw()
- && MessageUserStore.list.length) {
- this.folderInformation(folder.fullNameRaw, MessageUserStore.list());
- }
- }
- });
-
- if (boot) {
- setTimeout(() => this.folderInformationMultiply(true), 2000);
- }
- }
- }, folders);
- }
- }
-
- /**
- * @param {string} sFolderFullNameRaw
- * @param {number} iSetAction
- * @param {Array=} messages = null
- */
- messageListAction(sFolderFullNameRaw, iSetAction, messages) {
- let folder = null,
- alreadyUnread = 0,
- rootUids = [];
-
- if (undefined === messages || !messages) {
- messages = MessageUserStore.listChecked();
- }
-
- rootUids = messages.map(oMessage => oMessage && oMessage.uid ? oMessage.uid : null)
- .validUnique();
-
- if (sFolderFullNameRaw && rootUids.length) {
- switch (iSetAction) {
- case MessageSetAction.SetSeen:
- rootUids.forEach(sSubUid =>
- alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullNameRaw, sSubUid, iSetAction)
- );
-
- folder = getFolderFromCacheList(sFolderFullNameRaw);
- if (folder) {
- folder.messageCountUnread(folder.messageCountUnread() - alreadyUnread);
- }
-
- Remote.messageSetSeen(()=>{}, sFolderFullNameRaw, rootUids, true);
- break;
-
- case MessageSetAction.UnsetSeen:
- rootUids.forEach(sSubUid =>
- alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullNameRaw, sSubUid, iSetAction)
- );
-
- folder = getFolderFromCacheList(sFolderFullNameRaw);
- if (folder) {
- folder.messageCountUnread(folder.messageCountUnread() - alreadyUnread + rootUids.length);
- }
-
- Remote.messageSetSeen(()=>{}, sFolderFullNameRaw, rootUids, false);
- break;
-
- case MessageSetAction.SetFlag:
- rootUids.forEach(sSubUid =>
- MessageFlagsCache.storeBySetAction(sFolderFullNameRaw, sSubUid, iSetAction)
- );
-
- Remote.messageSetFlagged(()=>{}, sFolderFullNameRaw, rootUids, true);
- break;
-
- case MessageSetAction.UnsetFlag:
- rootUids.forEach(sSubUid =>
- MessageFlagsCache.storeBySetAction(sFolderFullNameRaw, sSubUid, iSetAction)
- );
-
- Remote.messageSetFlagged(()=>{}, sFolderFullNameRaw, rootUids, false);
- break;
- // no default
- }
-
- this.reloadFlagsCurrentMessageListAndMessageFromCache();
- }
- }
-
- /**
- * @param {string} query
- * @param {Function} autocompleteCallback
- */
- getAutocomplete(query, autocompleteCallback) {
- Remote.suggestions((iError, data) => {
- if (!iError && isArray(data.Result)) {
- autocompleteCallback(
- data.Result.map(item => (item && item[0] ? new EmailModel(item[0], item[1]) : null)).filter(v => v)
- );
- } else if (Notification.RequestAborted !== iError) {
- autocompleteCallback([]);
- }
- }, query);
- }
-
- /**
- * @param {string} sFullNameHash
- * @param {boolean} bExpanded
- */
- setExpandedFolder(sFullNameHash, bExpanded) {
- let aExpandedList = Local.get(ClientSideKeyName.ExpandedFolders);
- if (!isArray(aExpandedList)) {
- aExpandedList = [];
- }
-
- if (bExpanded) {
- if (!aExpandedList.includes(sFullNameHash))
- aExpandedList.push(sFullNameHash);
- } else {
- aExpandedList = aExpandedList.filter(value => value !== sFullNameHash);
- }
-
- Local.set(ClientSideKeyName.ExpandedFolders, aExpandedList);
- }
-
- setLayoutResizer(source, target, sClientSideKeyName, mode) {
- if (mode) {
- source.classList.add('resizable');
- if (!source.querySelector('.resizer')) {
- const resizer = createElement('div', {'class':'resizer'}),
- cssint = s => parseFloat(getComputedStyle(source, null).getPropertyValue(s).replace('px', ''));
- source.append(resizer);
- resizer.addEventListener('mousedown', {
- source: source,
- mode: mode,
- handleEvent: function(e) {
- if ('mousedown' == e.type) {
- e.preventDefault();
- this.pos = ('width' == this.mode) ? e.pageX : e.pageY;
- this.min = cssint('min-'+this.mode);
- this.max = cssint('max-'+this.mode);
- this.org = cssint(this.mode);
- addEventListener('mousemove', this);
- addEventListener('mouseup', this);
- } else if ('mousemove' == e.type) {
- const length = this.org + (('width' == this.mode ? e.pageX : e.pageY) - this.pos);
- if (length >= this.min && length <= this.max ) {
- this.source.style[this.mode] = length + 'px';
- }
- } else if ('mouseup' == e.type) {
- removeEventListener('mousemove', this);
- removeEventListener('mouseup', this);
- }
- }
- });
- if ('width' == mode) {
- source.observer = new ResizeObserver(() => {
- target.style.left = source.offsetWidth + 'px';
- Local.set(sClientSideKeyName, source.offsetWidth);
- });
- } else {
- source.observer = new ResizeObserver(() => {
- target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
- Local.set(sClientSideKeyName, source.offsetHeight);
- });
- }
- }
- source.observer.observe(source, { box: 'border-box' });
- const length = Local.get(sClientSideKeyName);
- if (length) {
- if ('width' == mode) {
- source.style.width = length + 'px';
- } else {
- source.style.height = length + 'px';
- }
- }
- } else {
- source.observer && source.observer.disconnect();
- source.classList.remove('resizable');
- target.removeAttribute('style');
- source.removeAttribute('style');
- }
- }
-
- initHorizontalLayoutResizer() {
- const top = doc.querySelector('.b-message-list-wrapper'),
- bottom = doc.querySelector('.b-message-view-wrapper'),
- fToggle = () => {
- this.setLayoutResizer(top, bottom, ClientSideKeyName.MessageListSize,
- (ThemeStore.isMobile() || Layout.BottomPreview !== SettingsUserStore.layout()) ? null : 'height');
- };
- if (top && bottom) {
- fToggle();
- addEventListener('rl-layout', fToggle);
- }
- }
-
- initVerticalLayoutResizer() {
- const left = elementById('rl-left'),
- right = elementById('rl-right'),
- fToggle = () =>
- this.setLayoutResizer(left, right, ClientSideKeyName.FolderListSize,
- (ThemeStore.isMobile() || leftPanelDisabled()) ? null : 'width');
- if (left && right) {
- fToggle();
- leftPanelDisabled.subscribe(fToggle);
- }
- }
-
- /**
- * @param {string} link
- * @returns {boolean}
- */
- download(link) {
- if (ThemeStore.isMobile()) {
- open(link, '_self');
- focus();
- } else {
- const oLink = createElement('a');
- oLink.href = link;
- doc.body.appendChild(oLink).click();
- oLink.remove();
- }
- return true;
- }
-
logout() {
- Remote.logout(() => this.logoutReload((SettingsGet('ParentEmail')||{length:0}).length));
- }
-
- bootend() {
- progressJs.end();
- this.hideLoading();
+ Remote.request('Logout', () => {
+ const customLogoutLink = Settings.app('customLogoutLink');
+ if (customLogoutLink) {
+ ((window.parent && Settings.app('inIframe')) ? window.parent : window).location.href = customLogoutLink;
+ } else {
+ rl.logoutReload()
+ }
+ });
}
bootstart() {
super.bootstart();
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
+ addEventListener('beforeunload', event => {
+ if (arePopupsVisible()) {
+ event.preventDefault();
+ return event.returnValue = "Are you sure you want to exit?";
+ }
+ }, {capture: true});
+ }
- NotificationUserStore.populate();
- AccountUserStore.populate();
- ContactUserStore.populate();
-
- let contactsSyncInterval = pInt(SettingsGet('ContactsSyncInterval'));
-
- const startupUrl = pString(SettingsGet('StartupUrl'));
-
- progressJs.set(90);
-
- rl.setWindowTitle();
+ start() {
if (SettingsGet('Auth')) {
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
- this.foldersReload(value => {
+ NotificationUserStore.enableSoundNotification(!!SettingsGet('SoundNotification'));
+ NotificationUserStore.enableDesktopNotification(!!SettingsGet('DesktopNotifications'));
+
+ AccountUserStore.email(SettingsGet('Email'));
+
+ SettingsUserStore.init();
+ ContactUserStore.init();
+
+ loadFolders(value => {
try {
- this.bootend();
-
if (value) {
- if (startupUrl) {
- rl.route.setHash(root(startupUrl), true);
- }
-
- if (window.crypto && crypto.getRandomValues && Settings.capa(Capa.OpenPGP)) {
- const openpgpCallback = () => {
- if (!window.openpgp) {
- return false;
- }
- PgpUserStore.openpgp = openpgp;
-
- if (window.Worker) {
- try {
- PgpUserStore.openpgp.initWorker({ path: openPgpWorkerJs() });
- } catch (e) {
- console.error(e);
- }
- }
-
- PgpUserStore.openpgpKeyring = new openpgp.Keyring();
- PgpUserStore.capaOpenPGP(true);
-
- this.reloadOpenPgpKeys();
-
- return true;
- };
-
- if (!openpgpCallback()) {
- const script = createElement('script', {src:openPgpJs()});
- script.onload = openpgpCallback;
- script.onerror = () => console.error(script.src);
- doc.head.append(script);
- }
- } else {
- PgpUserStore.capaOpenPGP(false);
- }
-
startScreens([
MailBoxUserScreen,
- Settings.capa(Capa.Settings) ? SettingsUserScreen : null
- // false ? AboutUserScreen : null
+ SettingsUserScreen
]);
setInterval(() => {
- const cF = FolderUserStore.currentFolderFullNameRaw(),
+ const cF = FolderUserStore.currentFolderFullName(),
iF = getFolderInboxName();
this.folderInformation(iF);
if (iF !== cF) {
this.folderInformation(cF);
}
- this.folderInformationMultiply();
- }, refreshFolders);
+ folderInformationMultiply();
+ }, refreshFoldersInterval);
- // Every 15 minutes
- setInterval(this.quota, 900000);
- // Every 20 minutes
- setInterval(this.foldersReload, 1200000);
+ ContactUserStore.init();
- setTimeout(this.contactsSync, 10000);
- contactsSyncInterval = 5 <= contactsSyncInterval ? contactsSyncInterval : 20;
- contactsSyncInterval = 320 >= contactsSyncInterval ? contactsSyncInterval : 320;
- setInterval(this.contactsSync, contactsSyncInterval * 60000 + 5000);
-
- this.accountsAndIdentities(true);
+ this.accountsAndIdentities();
setTimeout(() => {
- const cF = FolderUserStore.currentFolderFullNameRaw();
+ const cF = FolderUserStore.currentFolderFullName();
if (getFolderInboxName() !== cF) {
this.folderInformation(cF);
}
- this.folderInformationMultiply(true);
+ FolderUserStore.hasCapability('LIST-STATUS') || folderInformationMultiply(true);
}, 1000);
- setTimeout(this.quota, 5000);
- setTimeout(() => Remote.appDelayStart(()=>{}), 35000);
+ setTimeout(() => Remote.request('AppDelayStart'), 35000);
- // When auto-login is active
- if (
- !!SettingsGet('AccountSignMe') &&
- navigator.registerProtocolHandler &&
- Settings.capa(Capa.Composer)
- ) {
- setTimeout(() => {
- try {
- navigator.registerProtocolHandler(
- 'mailto',
- location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
- (SettingsGet('Title') || 'SnappyMail')
- );
- } catch (e) {} // eslint-disable-line no-empty
-
- if (SettingsGet('MailToEmail')) {
- mailToHelper(SettingsGet('MailToEmail'));
- }
- }, 500);
- }
-
- ['touchstart','mousedown','mousemove','keydown'].forEach(
- t => doc.addEventListener(t, SettingsUserStore.delayLogout, {passive:true})
- );
+ // add pointermove ?
+ addEventsListener(doc, ['touchstart','mousemove','keydown'], SettingsUserStore.delayLogout, {passive:true});
SettingsUserStore.delayLogout();
- setTimeout(() => this.initVerticalLayoutResizer(), 1);
+ // initLeftSideLayoutResizer
+ setTimeout(() => {
+ const left = elementById('rl-left'),
+ right = elementById('rl-right'),
+ fToggle = () =>
+ setLayoutResizer(left, right, ClientSideKeyNameFolderListSize,
+ (ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
+ if (left && right) {
+ fToggle();
+ leftPanelDisabled.subscribe(fToggle);
+ }
+ }, 1);
+
+ setInterval(reloadTime(), 60000);
+
+ PgpUserStore.init();
+
+ // When auto-login is active
+ if (navigator.registerProtocolHandler) {
+ try {
+ navigator.registerProtocolHandler(
+ 'mailto',
+ location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
+ (SettingsGet('Title') || 'SnappyMail')
+ );
+ } catch (e) {} // eslint-disable-line no-empty
+ }
+ setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
} else {
this.logout();
}
@@ -1030,16 +367,13 @@ class AppUser extends AbstractApp {
});
} else {
- this.bootend();
startScreens([LoginUserScreen]);
}
-
- setInterval(() => dispatchEvent(new CustomEvent('reload-time')), 60000);
}
showMessageComposer(params = [])
{
- Settings.capa(Capa.Composer) && showScreenPopup(ComposePopupView, params);
+ showScreenPopup(ComposePopupView, params);
}
}
diff --git a/dev/Common/Audio.js b/dev/Common/Audio.js
index d6d9f5aeb..a77efbcd5 100644
--- a/dev/Common/Audio.js
+++ b/dev/Common/Audio.js
@@ -1,5 +1,5 @@
import * as Links from 'Common/Links';
-import { doc } from 'Common/Globals';
+import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
let notificator = null,
player = null,
@@ -12,7 +12,7 @@ let notificator = null,
player.src = url;
player.play();
name = name.trim();
- dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
+ fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
}
},
@@ -73,8 +73,7 @@ export const SMAudio = new class {
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
if (player) {
const stopFn = () => this.pause();
- player.addEventListener('ended', stopFn);
- player.addEventListener('error', stopFn);
+ addEventsListener(player, ['ended','error'], stopFn);
addEventListener('audio.api.stop', stopFn);
}
}
@@ -89,7 +88,7 @@ export const SMAudio = new class {
pause() {
player && player.pause();
- dispatchEvent(new CustomEvent('audio.stop'));
+ fireEvent('audio.stop');
}
playMp3(url, name) {
@@ -106,11 +105,11 @@ export const SMAudio = new class {
playNotification(silent) {
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
- if (!notificator) {
- notificator = createNewObject();
- notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
- }
+ notificator = notificator || createNewObject();
if (notificator) {
+ notificator.src = Links.staticLink('sounds/'
+ + SettingsGet('NotificationSound')
+ + (this.supportedMp3 ? '.mp3' : '.ogg'));
notificator.volume = silent ? 0.01 : 1;
notificator.play();
}
diff --git a/dev/Common/Cache.js b/dev/Common/Cache.js
index 2a65a11cc..c41c197b3 100644
--- a/dev/Common/Cache.js
+++ b/dev/Common/Cache.js
@@ -1,174 +1,157 @@
import { MessageSetAction } from 'Common/EnumsUser';
-import { isNonEmptyArray, pInt } from 'Common/Utils';
+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 = {},
inboxFolderName = 'INBOX';
-const REQUESTED_MESSAGE_CACHE = {};
+export const
+ /**
+ * @returns {void}
+ */
+ clearCache = () => {
+ FOLDERS_CACHE = {};
+ FOLDERS_NAME_CACHE = {};
+ MESSAGE_FLAGS_CACHE = {};
+ NEW_MESSAGE_CACHE = {};
+ REQUESTED_MESSAGE_CACHE = {};
+ },
-/**
- * @returns {void}
- */
-export function clear() {
- FOLDERS_CACHE = {};
- FOLDERS_NAME_CACHE = {};
- FOLDERS_HASH_CACHE = {};
- FOLDERS_UID_NEXT_CACHE = {};
- MESSAGE_FLAGS_CACHE = {};
-}
+ /**
+ * @param {string} folderFullName
+ * @param {string} uid
+ * @returns {string}
+ */
+ getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
-/**
- * @param {string} folderFullNameRaw
- * @param {string} uid
- * @returns {string}
- */
-export function getMessageKey(folderFullNameRaw, uid) {
- return `${folderFullNameRaw}#${uid}`;
-}
+ /**
+ * @param {string} folder
+ * @param {string} uid
+ */
+ addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
-/**
- * @param {string} folder
- * @param {string} uid
- */
-export function addRequestedMessage(folder, uid) {
- REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
-}
+ /**
+ * @param {string} folder
+ * @param {string} uid
+ * @returns {boolean}
+ */
+ hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
-/**
- * @param {string} folder
- * @param {string} uid
- * @returns {boolean}
- */
-export function hasRequestedMessage(folder, uid) {
- return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
-}
+ /**
+ * @param {string} folderFullName
+ * @param {string} uid
+ */
+ addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
-/**
- * @param {string} folderFullNameRaw
- * @param {string} uid
- */
-export function addNewMessageCache(folderFullNameRaw, uid) {
- NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
-}
+ /**
+ * @param {string} folderFullName
+ * @param {string} uid
+ */
+ hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
+ if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
+ NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
+ return true;
+ }
+ return false;
+ },
-/**
- * @param {string} folderFullNameRaw
- * @param {string} uid
- */
-export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
- if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
- NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
- return true;
- }
- return false;
-}
+ /**
+ * @returns {void}
+ */
+ clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
-/**
- * @returns {void}
- */
-export function clearNewMessageCache() {
- NEW_MESSAGE_CACHE = {};
-}
+ /**
+ * @returns {string}
+ */
+ getFolderInboxName = () => inboxFolderName,
-/**
- * @returns {string}
- */
-export function getFolderInboxName() {
- return inboxFolderName;
-}
+ /**
+ * @returns {string}
+ */
+ setFolderInboxName = name => inboxFolderName = name,
-/**
- * @returns {string}
- */
-export function setFolderInboxName(name) {
- inboxFolderName = name;
-}
+ /**
+ * @param {string} folderHash
+ * @returns {string}
+ */
+ getFolderFullName = folderHash =>
+ folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
-/**
- * @param {string} folderHash
- * @returns {string}
- */
-export function getFolderFullNameRaw(folderHash) {
- return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
-}
+ /**
+ * @param {string} folderHash
+ * @param {string} folderFullName
+ * @param {?FolderModel} folder
+ */
+ setFolder = folder => {
+ folder.hash = '';
+ FOLDERS_CACHE[folder.fullName] = folder;
+ FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
+ },
-/**
- * @param {string} folderHash
- * @param {string} folderFullNameRaw
- * @param {?FolderModel} folder
- */
-export function setFolder(folderHash, folderFullNameRaw, folder) {
- FOLDERS_CACHE[folderFullNameRaw] = folder;
- FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
-}
+ /**
+ * @param {string} folderFullName
+ * @returns {string}
+ */
+ getFolderHash = folderFullName =>
+ FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
-/**
- * @param {string} folderFullNameRaw
- * @returns {string}
- */
-export function getFolderHash(folderFullNameRaw) {
- return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
-}
+ /**
+ * @param {string} folderFullName
+ * @param {string} folderHash
+ */
+ setFolderHash = (folderFullName, folderHash) =>
+ FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
-/**
- * @param {string} folderFullNameRaw
- * @param {string} folderHash
- */
-export function setFolderHash(folderFullNameRaw, folderHash) {
- if (folderFullNameRaw) {
- FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
- }
-}
+ /**
+ * @param {string} folderFullName
+ * @returns {string}
+ */
+ getFolderUidNext = folderFullName =>
+ FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
-/**
- * @param {string} folderFullNameRaw
- * @returns {string}
- */
-export function getFolderUidNext(folderFullNameRaw) {
- return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
- ? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
- : '';
-}
+ /**
+ * @param {string} folderFullName
+ * @param {string} uidNext
+ */
+ setFolderUidNext = (folderFullName, uidNext) =>
+ FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
-/**
- * @param {string} folderFullNameRaw
- * @param {string} uidNext
- */
-export function setFolderUidNext(folderFullNameRaw, uidNext) {
- FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
-}
+ /**
+ * @param {string} folderFullName
+ * @returns {?FolderModel}
+ */
+ getFolderFromCacheList = folderFullName =>
+ FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
-/**
- * @param {string} folderFullNameRaw
- * @returns {?FolderModel}
- */
-export function getFolderFromCacheList(folderFullNameRaw) {
- return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
-}
-
-/**
- * @param {string} folderFullNameRaw
- */
-export function removeFolderFromCacheList(folderFullNameRaw) {
- delete FOLDERS_CACHE[folderFullNameRaw];
-}
+ /**
+ * @param {string} folderFullName
+ */
+ removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
export class MessageFlagsCache
{
+ /**
+ * @param {string} folderFullName
+ * @param {string} uid
+ * @param {string} flag
+ * @returns {bool}
+ */
+ static hasFlag(folderFullName, uid, flag) {
+ return MESSAGE_FLAGS_CACHE[folderFullName]
+ && MESSAGE_FLAGS_CACHE[folderFullName][uid]
+ && MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
+ }
+
/**
* @param {string} folderFullName
* @param {string} uid
* @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];
}
/**
@@ -176,11 +159,13 @@ export class MessageFlagsCache
* @param {string} uid
* @param {Array} flagsCache
*/
- static setFor(folderFullName, uid, flagsCache) {
- if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
- MESSAGE_FLAGS_CACHE[folderFullName] = {};
+ static setFor(folderFullName, uid, flags) {
+ if (isArray(flags)) {
+ if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
+ MESSAGE_FLAGS_CACHE[folderFullName] = {};
+ }
+ MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
}
- MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
}
/**
@@ -196,39 +181,24 @@ export class MessageFlagsCache
static initMessage(message) {
if (message) {
const uid = message.uid,
- flags = this.getFor(message.folder, uid);
+ flags = this.getFor(message.folder, uid),
+ thread = message.threads;
- if (flags && flags.length) {
- message.isFlagged(!!flags[1]);
-
- if (!message.isSimpleMessage) {
- message.isUnseen(!!flags[0]);
- message.isAnswered(!!flags[2]);
- message.isForwarded(!!flags[3]);
- message.isReadReceipt(!!flags[4]);
- message.isDeleted(!!flags[5]);
- }
+ if (isArray(flags)) {
+ message.flags(flags);
}
- if (message.threads.length) {
- const unseenSubUid = message.threads.find(sSubUid => {
- if (uid !== sSubUid) {
- const subFlags = this.getFor(message.folder, sSubUid);
- return subFlags && subFlags.length && !!subFlags[0];
- }
- return false;
- });
+ if (thread.length) {
+ const unseenSubUid = thread.find(iSubUid =>
+ (uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
+ );
- const flaggedSubUid = message.threads.find(sSubUid => {
- if (uid !== sSubUid) {
- const subFlags = this.getFor(message.folder, sSubUid);
- return subFlags && subFlags.length && !!subFlags[1];
- }
- return false;
- });
+ const flaggedSubUid = thread.find(iSubUid =>
+ (uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
+ );
- message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
- message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
+ message.hasUnseenSubMessage(!!unseenSubUid);
+ message.hasFlaggedSubMessage(!!flaggedSubUid);
}
}
}
@@ -238,25 +208,7 @@ export class MessageFlagsCache
*/
static store(message) {
if (message) {
- this.setFor(message.folder, message.uid, [
- message.isUnseen(),
- message.isFlagged(),
- message.isAnswered(),
- message.isForwarded(),
- message.isReadReceipt(),
- message.isDeleted()
- ]);
- }
- }
-
- /**
- * @param {string} folder
- * @param {string} uid
- * @param {Array} flags
- */
- static storeByFolderAndUid(folder, uid, flags) {
- if (isNonEmptyArray(flags)) {
- this.setFor(folder, uid, flags);
+ this.setFor(message.folder, message.uid, message.flags());
}
}
@@ -266,33 +218,30 @@ export class MessageFlagsCache
* @param {number} setAction
*/
static storeBySetAction(folder, uid, setAction) {
- let unread = 0;
- const flags = this.getFor(folder, uid);
+ let flags = this.getFor(folder, uid) || [];
+ const
+ unread = flags.includes('\\seen') ? 0 : 1,
+ add = item => flags.includes(item) || flags.push(item),
+ remove = item => flags = flags.filter(flag => flag != item);
- if (isNonEmptyArray(flags)) {
- if (flags[0]) {
- unread = 1;
- }
-
- switch (setAction) {
- case MessageSetAction.SetSeen:
- flags[0] = false;
- break;
- case MessageSetAction.UnsetSeen:
- flags[0] = true;
- break;
- case MessageSetAction.SetFlag:
- flags[1] = true;
- break;
- case MessageSetAction.UnsetFlag:
- flags[1] = false;
- break;
- // no default
- }
-
- this.setFor(folder, uid, flags);
+ switch (setAction) {
+ case MessageSetAction.SetSeen:
+ add('\\seen');
+ break;
+ case MessageSetAction.UnsetSeen:
+ remove('\\seen');
+ break;
+ case MessageSetAction.SetFlag:
+ add('\\flagged');
+ break;
+ case MessageSetAction.UnsetFlag:
+ remove('\\flagged');
+ break;
+ // no default
}
+ this.setFor(folder, uid, flags);
+
return unread;
}
diff --git a/dev/Common/Consts.js b/dev/Common/Consts.js
index a5d57752d..ccb6c2d05 100644
--- a/dev/Common/Consts.js
+++ b/dev/Common/Consts.js
@@ -1,3 +1 @@
-export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
-
export const UNUSED_OPTION_VALUE = '__UNUSE__';
diff --git a/dev/Common/Enums.js b/dev/Common/Enums.js
index eafe441df..f9ce77143 100644
--- a/dev/Common/Enums.js
+++ b/dev/Common/Enums.js
@@ -1,85 +1,53 @@
/* eslint quote-props: 0 */
-/**
- * @enum {string}
- */
-export const Capa = {
- OpenPGP: 'OPEN_PGP',
- Prefetch: 'PREFETCH',
- Composer: 'COMPOSER',
- Contacts: 'CONTACTS',
- Reload: 'RELOAD',
- Search: 'SEARCH',
- SearchAdv: 'SEARCH_ADV',
- MessageActions: 'MESSAGE_ACTIONS',
- MessageListActions: 'MESSAGELIST_ACTIONS',
- AttachmentsActions: 'ATTACHMENTS_ACTIONS',
- DangerousActions: 'DANGEROUS_ACTIONS',
- Settings: 'SETTINGS',
- Help: 'HELP',
- Themes: 'THEMES',
- UserBackground: 'USER_BACKGROUND',
- Sieve: 'SIEVE',
- AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
- Templates: 'TEMPLATES',
- AutoLogout: 'AUTOLOGOUT',
- AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
- Identities: 'IDENTITIES'
-};
+export const
/**
* @enum {string}
*/
-export const Scope = {
- All: 'all',
- None: 'none',
- Contacts: 'Contacts',
+Scope = {
MessageList: 'MessageList',
FolderList: 'FolderList',
MessageView: 'MessageView',
- Compose: 'Compose',
- Settings: 'Settings',
- Menu: 'Menu',
- ComposeOpenPgp: 'ComposeOpenPgp',
- MessageOpenPgp: 'MessageOpenPgp',
- ViewOpenPgpKey: 'ViewOpenPgpKey',
- KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
- Ask: 'Ask'
-};
+ Settings: 'Settings'
+},
/**
* @enum {number}
*/
-export const UploadErrorCode = {
+UploadErrorCode = {
Normal: 0,
FileIsTooBig: 1,
- FilePartiallyUploaded: 2,
- NoFileUploaded: 3,
- MissingTempFolder: 4,
- OnSavingFile: 5,
+ FilePartiallyUploaded: 3,
+ NoFileUploaded: 4,
+ MissingTempFolder: 6,
+ OnSavingFile: 7,
FileType: 98,
Unknown: 99
-};
+},
/**
* @enum {number}
*/
-export const SaveSettingsStep = {
+SaveSettingsStep = {
Animate: -2,
Idle: -1,
TrueResult: 1,
FalseResult: 0
-};
+},
/**
* @enum {number}
*/
-export const Notification = {
+Notification = {
RequestError: 1,
RequestAborted: 2,
+ // Global
InvalidToken: 101,
AuthError: 102,
+
+ // User
ConnectionError: 104,
DomainNotAllowed: 109,
AccountNotAllowed: 110,
@@ -110,20 +78,15 @@ export const Notification = {
CantDeleteNonEmptyFolder: 405,
// CantSaveSettings: 501,
- CantSavePluginSettings: 502,
DomainAlreadyExists: 601,
- CantInstallPackage: 701,
- CantDeletePackage: 702,
- InvalidPluginPackage: 703,
- UnsupportedPluginPackage: 704,
-
DemoSendMessageError: 750,
DemoAccountError: 751,
AccountAlreadyExists: 801,
AccountDoesNotExist: 802,
+ AccountSwitchFailed: 803,
MailServerError: 901,
ClientViewError: 902,
@@ -134,5 +97,12 @@ export const Notification = {
// JsonTimeout: 953,
UnknownNotification: 998,
- UnknownError: 999
+ UnknownError: 999,
+
+ // Admin
+ CantInstallPackage: 701,
+ CantDeletePackage: 702,
+ InvalidPluginPackage: 703,
+ UnsupportedPluginPackage: 704,
+ CantSavePluginSettings: 705
};
diff --git a/dev/Common/EnumsUser.js b/dev/Common/EnumsUser.js
index 9ede882ee..032ed7f2a 100644
--- a/dev/Common/EnumsUser.js
+++ b/dev/Common/EnumsUser.js
@@ -4,14 +4,28 @@
* @enum {number}
*/
export const FolderType = {
- Inbox: 10,
- SentItems: 11,
- Draft: 12,
- Trash: 13,
- Spam: 14,
- Archive: 15,
- NotSpam: 80,
- User: 99
+ User: 0,
+ Inbox: 1,
+ Sent: 2,
+ Drafts: 3,
+ Spam: 4, // JUNK
+ Trash: 5,
+ Archive: 6,
+ NotSpam: 80
+};
+
+/**
+ * @enum {string}
+ */
+export const FolderMetadataKeys = {
+ // RFC 5464
+ Comment: '/private/comment',
+ CommentShared: '/shared/comment',
+ // RFC 6154
+ SpecialUse: '/private/specialuse',
+ // Kolab
+ KolabFolderType: '/private/vendor/kolab/folder-type',
+ KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
};
/**
@@ -34,13 +48,13 @@ export const FolderSortMode = {
* @enum {string}
*/
export const ComposeType = {
- Empty: 'empty',
- Reply: 'reply',
- ReplyAll: 'replyall',
- Forward: 'forward',
- ForwardAsAttachment: 'forward-as-attachment',
- Draft: 'draft',
- EditAsNew: 'editasnew'
+ Empty: 0,
+ Reply: 1,
+ ReplyAll: 2,
+ Forward: 3,
+ ForwardAsAttachment: 4,
+ Draft: 5,
+ EditAsNew: 6
};
/**
@@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
/**
* @enum {number}
*/
-export const ClientSideKeyName = {
- FoldersLashHash: 0,
- MessagesInboxLastHash: 1,
- MailBoxListSize: 2,
- ExpandedFolders: 3,
- FolderListSize: 4,
- MessageListSize: 5,
- LastReplyAction: 6,
- LastSignMe: 7,
- ComposeLastIdentityID: 8,
- MessageHeaderFullInfo: 9,
- MessageAttachmentControls: 10
-};
+export const
+ ClientSideKeyNameExpandedFolders = 3,
+ ClientSideKeyNameFolderListSize = 4,
+ ClientSideKeyNameMessageListSize = 5,
+ ClientSideKeyNameLastReplyAction = 6,
+ ClientSideKeyNameLastSignMe = 7,
+ ClientSideKeyNameMessageHeaderFullInfo = 9,
+ ClientSideKeyNameMessageAttachmentControls = 10;
/**
* @enum {number}
diff --git a/dev/Common/File.js b/dev/Common/File.js
index 28bc2943f..aab390c4d 100644
--- a/dev/Common/File.js
+++ b/dev/Common/File.js
@@ -1,7 +1,7 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
-import { isNonEmptyArray } from 'Common/Utils';
+import { arrayLength } from 'Common/Utils';
const
cache = {},
@@ -9,6 +9,7 @@ const
msOffice = app+'vnd.openxmlformats-officedocument.',
openDoc = app+'vnd.oasis.opendocument.',
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
+ lowerCase = text => text.toLowerCase().trim(),
exts = {
eml: 'message/rfc822',
@@ -123,13 +124,13 @@ export const FileInfo = {
* @returns {string}
*/
getExtension: fileName => {
- fileName = fileName.toLowerCase().trim();
+ fileName = lowerCase(fileName);
const result = fileName.split('.').pop();
return result === fileName ? '' : result;
},
getContentType: fileName => {
- fileName = fileName.toLowerCase().trim();
+ fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) {
return app + 'ms-tnef';
}
@@ -158,8 +159,8 @@ export const FileInfo = {
* @returns {string}
*/
getType: (ext, mimeType) => {
- ext = ext.toLowerCase().trim();
- mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
+ ext = lowerCase(ext);
+ mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
let key = ext + mimeType;
if (cache[key]) {
@@ -249,10 +250,10 @@ export const FileInfo = {
* @param {string} sFileType
* @returns {string}
*/
- getCombinedIconClass: data => {
- if (isNonEmptyArray(data)) {
+ getAttachmentsIconClass: data => {
+ if (arrayLength(data)) {
let icons = data
- .map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
+ .map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
.validUnique();
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
diff --git a/dev/Common/Folders.js b/dev/Common/Folders.js
new file mode 100644
index 000000000..1a9ff9287
--- /dev/null
+++ b/dev/Common/Folders.js
@@ -0,0 +1,259 @@
+import { isArray, arrayLength } from 'Common/Utils';
+import {
+ MessageFlagsCache,
+ setFolderHash,
+ getFolderHash,
+ getFolderInboxName,
+ getFolderFromCacheList,
+ getFolderUidNext
+} from 'Common/Cache';
+import { SettingsUserStore } from 'Stores/User/Settings';
+import { FolderUserStore } from 'Stores/User/Folder';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
+import { getNotification } from 'Common/Translator';
+
+import Remote from 'Remote/User/Fetch';
+
+export const
+
+sortFolders = folders => {
+ try {
+ let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
+ folders.sort((a, b) =>
+ a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
+ );
+ } catch (e) {
+ console.error(e);
+ }
+},
+
+/**
+ * @param {?Function} fCallback
+ * @param {string} folder
+ * @param {Array=} list = []
+ */
+fetchFolderInformation = (fCallback, folder, list = []) => {
+ let fetch = !arrayLength(list);
+ const uids = [];
+
+ if (!fetch) {
+ list.forEach(messageListItem => {
+ if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
+ uids.push(messageListItem.uid);
+ }
+
+ if (messageListItem.threads.length) {
+ messageListItem.threads.forEach(uid => {
+ if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
+ uids.push(uid);
+ }
+ });
+ }
+ });
+ fetch = uids.length;
+ }
+
+ if (fetch) {
+ Remote.request('FolderInformation', fCallback, {
+ Folder: folder,
+ FlagsUids: uids,
+ UidNext: getFolderUidNext(folder) // Used to check for new messages
+ });
+ } else if (SettingsUserStore.useThreads()) {
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
+ }
+},
+
+/**
+ * @param {Array=} aDisabled
+ * @param {Array=} aHeaderLines
+ * @param {Function=} fRenameCallback
+ * @param {Function=} fDisableCallback
+ * @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
+ * @returns {Array}
+ */
+folderListOptionsBuilder = (
+ aDisabled,
+ aHeaderLines,
+ fRenameCallback,
+ fDisableCallback,
+ bNoSelectSelectable,
+ aList = FolderUserStore.folderList()
+) => {
+ const
+ aResult = [],
+ sDeepPrefix = '\u00A0\u00A0\u00A0',
+ // FolderSystemPopupView should always be true
+ showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
+
+ foldersWalk = folders => {
+ folders.forEach(oItem => {
+ if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
+ aResult.push({
+ id: oItem.fullName,
+ name:
+ sDeepPrefix.repeat(oItem.deep) +
+ fRenameCallback(oItem),
+ system: false,
+ disabled: !bNoSelectSelectable && (
+ !oItem.selectable() ||
+ aDisabled.includes(oItem.fullName) ||
+ fDisableCallback(oItem))
+ });
+ }
+
+ if (oItem.subFolders.length) {
+ foldersWalk(oItem.subFolders());
+ }
+ });
+ };
+
+
+ fDisableCallback = fDisableCallback || (() => false);
+ fRenameCallback = fRenameCallback || (oItem => oItem.name());
+ isArray(aDisabled) || (aDisabled = []);
+
+ isArray(aHeaderLines) && aHeaderLines.forEach(line =>
+ aResult.push({
+ id: line[0],
+ name: line[1],
+ system: false,
+ disabled: false
+ })
+ );
+
+ foldersWalk(aList);
+
+ return aResult;
+},
+
+// Every 5 minutes
+refreshFoldersInterval = 300000,
+
+/**
+ * @param {boolean=} boot = false
+ */
+folderInformationMultiply = (boot = false) => {
+ const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
+ if (arrayLength(folders)) {
+ Remote.request('FolderInformationMultiply', (iError, oData) => {
+ if (!iError && arrayLength(oData.Result)) {
+ const utc = Date.now();
+ oData.Result.forEach(item => {
+ const hash = getFolderHash(item.Folder),
+ folder = getFolderFromCacheList(item.Folder);
+
+ if (folder) {
+ folder.expires = utc;
+
+ setFolderHash(item.Folder, item.Hash);
+
+ folder.messageCountAll(item.MessageCount);
+
+ let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
+
+ folder.messageCountUnread(item.MessageUnseenCount);
+
+ if (unreadCountChange) {
+ MessageFlagsCache.clearFolder(folder.fullName);
+ }
+
+ if (!hash || item.Hash !== hash) {
+ if (folder.fullName === FolderUserStore.currentFolderFullName()) {
+ MessagelistUserStore.reload();
+ }
+ } else if (unreadCountChange
+ && folder.fullName === FolderUserStore.currentFolderFullName()
+ && MessagelistUserStore.length) {
+ rl.app.folderInformation(folder.fullName, MessagelistUserStore());
+ }
+ }
+ });
+
+ if (boot) {
+ setTimeout(() => folderInformationMultiply(true), 2000);
+ }
+ }
+ }, {
+ Folders: folders
+ });
+ }
+},
+
+moveOrDeleteResponseHelper = (iError, oData) => {
+ if (iError) {
+ setFolderHash(FolderUserStore.currentFolderFullName(), '');
+ alert(getNotification(iError));
+ } else if (FolderUserStore.currentFolder()) {
+ if (2 === arrayLength(oData.Result)) {
+ setFolderHash(oData.Result[0], oData.Result[1]);
+ } else {
+ setFolderHash(FolderUserStore.currentFolderFullName(), '');
+ }
+ MessagelistUserStore.reload(!MessagelistUserStore.length);
+ }
+},
+
+messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
+ const
+ sSpamFolder = FolderUserStore.spamFolder(),
+ isSpam = sSpamFolder === toFolderFullName,
+ isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
+
+ Remote.request('MessageMove',
+ moveOrDeleteResponseHelper,
+ {
+ FromFolder: fromFolderFullName,
+ ToFolder: toFolderFullName,
+ Uids: uidsForMove.join(','),
+ MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
+ Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
+ },
+ null,
+ '',
+ ['MessageList']
+ );
+},
+
+messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
+ Remote.request('MessageDelete',
+ moveOrDeleteResponseHelper,
+ {
+ Folder: sFromFolderFullName,
+ Uids: aUidForRemove.join(',')
+ },
+ null,
+ '',
+ ['MessageList']
+ );
+},
+
+/**
+ * @param {string} sFromFolderFullName
+ * @param {Array} aUidForMove
+ * @param {string} sToFolderFullName
+ * @param {boolean=} bCopy = false
+ */
+moveMessagesToFolder = (sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) => {
+ if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
+ const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
+ oToFolder = getFolderFromCacheList(sToFolderFullName);
+
+ if (oFromFolder && oToFolder) {
+ if (bCopy) {
+ Remote.request('MessageCopy', null, {
+ FromFolder: oFromFolder.fullName,
+ ToFolder: oToFolder.fullName,
+ Uids: aUidForMove.join(',')
+ });
+ } else {
+ messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
+ }
+
+ MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
+ return true;
+ }
+ }
+
+ return false;
+};
diff --git a/dev/Common/Globals.js b/dev/Common/Globals.js
index a2653cfc3..db6c82324 100644
--- a/dev/Common/Globals.js
+++ b/dev/Common/Globals.js
@@ -1,58 +1,78 @@
import ko from 'ko';
-import { Scope } from 'Common/Enums';
-export const doc = document;
+let keyScopeFake = 'all';
-export const $htmlCL = doc.documentElement.classList;
+export const
+ ScopeMenu = 'Menu',
-export const elementById = id => doc.getElementById(id);
+ doc = document,
-export const Settings = rl.settings;
-export const SettingsGet = rl.settings.get;
+ $htmlCL = doc.documentElement.classList,
-export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
+ elementById = id => doc.getElementById(id),
-export const moveAction = ko.observable(false);
-export const leftPanelDisabled = ko.observable(false);
+ exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
+ getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
-export const createElement = (name, attr) => {
- let el = doc.createElement(name);
- attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
- return el;
-};
+ Settings = rl.settings,
+ SettingsGet = Settings.get,
+ SettingsCapa = Settings.capa,
+ dropdowns = [],
+ dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
+
+ moveAction = ko.observable(false),
+ leftPanelDisabled = ko.observable(false),
+
+ createElement = (name, attr) => {
+ let el = doc.createElement(name);
+ attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
+ return el;
+ },
+
+ fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
+
+ formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'),
+
+ addShortcut = (...args) => shortcuts.add(...args),
+
+ registerShortcut = (keys, modifiers, scopes, method) =>
+ addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
+
+ addEventsListener = (element, events, fn, options) =>
+ events.forEach(event => element.addEventListener(event, fn, options)),
+
+ addEventsListeners = (element, events) =>
+ Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
+
+ // keys / shortcuts
+ keyScopeReal = ko.observable('all'),
+ keyScope = value => {
+ if (!value) {
+ return keyScopeFake;
+ }
+ if (ScopeMenu !== value) {
+ keyScopeFake = value;
+ if (dropdownVisibility()) {
+ value = ScopeMenu;
+ }
+ }
+ keyScopeReal(value);
+ shortcuts.setScope(value);
+ };
+
+dropdownVisibility.subscribe(value => {
+ if (value) {
+ keyScope(ScopeMenu);
+ } else if (ScopeMenu === shortcuts.getScope()) {
+ keyScope(keyScopeFake);
+ }
+});
+
+leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false);
$htmlCL.toggle('rl-left-panel-disabled', value);
});
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
-
-// keys
-export const keyScopeReal = ko.observable(Scope.All);
-
-export const keyScope = (()=>{
- let keyScopeFake = Scope.All;
- dropdownVisibility.subscribe(value => {
- if (value) {
- keyScope(Scope.Menu);
- } else if (Scope.Menu === shortcuts.getScope()) {
- keyScope(keyScopeFake);
- }
- });
- return ko.computed({
- read: () => keyScopeFake,
- write: value => {
- if (Scope.Menu !== value) {
- keyScopeFake = value;
- if (dropdownVisibility()) {
- value = Scope.Menu;
- }
- }
-
- keyScopeReal(value);
- }
- });
-})();
-
-keyScopeReal.subscribe(value => shortcuts.setScope(value));
diff --git a/dev/Common/Html.js b/dev/Common/Html.js
index a1b82528a..c9bd57c92 100644
--- a/dev/Common/Html.js
+++ b/dev/Common/Html.js
@@ -1,4 +1,9 @@
+import { createElement, SettingsGet } from 'Common/Globals';
+import { forEachObjectEntry, pInt } from 'Common/Utils';
+import { proxy } from 'Common/Links';
+
const
+ tpl = createElement('template'),
htmlre = /[&<>"']/g,
htmlmap = {
'&': '&',
@@ -6,17 +11,489 @@ const
'>': '>',
'"': '"',
"'": '''
+ },
+
+ replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
+
+ // Strip utm_* tracking
+ stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
+
+export const
+
+ /**
+ * @param {string} text
+ * @returns {string}
+ */
+ encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
+
+ /**
+ * Clears the Message Html for viewing
+ * @param {string} text
+ * @returns {string}
+ */
+ cleanHtml = (html, contentLocationUrls, removeColors) => {
+ const
+ debug = false, // Config()->Get('debug', 'enable', false);
+ useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
+ detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
+
+ result = {
+ hasExternals: false,
+ foundCIDs: [],
+ foundContentLocationUrls: []
+ },
+
+ // convert body attributes to CSS
+ tasks = {
+ link: value => {
+ if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
+ tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
+ }
+ },
+ text: (value, node) => node.style.color = value,
+ topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
+ leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
+ bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
+ rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
+ },
+ allowedAttributes = [
+ // defaults
+ 'name',
+ 'dir', 'lang', 'style', 'title',
+ 'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
+ 'border', 'bordercolor', 'charset', 'direction',
+ // a
+ 'download', 'hreflang',
+ // body
+ 'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
+ // col
+ 'align', 'valign',
+ // font
+ 'color', 'face', 'size',
+ // hr
+ 'noshade',
+ // img
+ 'hspace', 'sizes', 'srcset', 'vspace',
+ // meter
+ 'low', 'high', 'optimum', 'value',
+ // ol
+ 'reversed', 'start',
+ // table
+ 'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
+ // th
+ 'abbr', 'scope',
+ // td
+ 'colspan', 'rowspan', 'headers'
+ ],
+ disallowedTags = [
+ 'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
+ 'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
+ 'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
+ ],
+ nonEmptyTags = [
+ 'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
+ ];
+
+ tpl.innerHTML = html
+// .replace(/]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n '))
+ .replace(/]*>/gi, '')
+ .replace(/<\?xml[^>]*\?>/gi, '')
+ // Not supported by element
+ .replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
+ .replace(/<\/?(html|head)[^>]*>/gi, '')
+ .trim();
+ html = '';
+
+ // \MailSo\Base\HtmlUtils::ClearComments()
+ // https://github.com/the-djmaze/snappymail/issues/187
+ const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
+ while (nodeIterator.nextNode()) {
+ nodeIterator.referenceNode.remove();
+ }
+
+ tpl.content.querySelectorAll('*').forEach(oElement => {
+ const name = oElement.tagName,
+ oStyle = oElement.style;
+
+ // \MailSo\Base\HtmlUtils::ClearTags()
+ if (disallowedTags.includes(name)
+ || 'none' == oStyle.display
+ || 'hidden' == oStyle.visibility
+// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
+// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
+// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
+// || ('0' === oStyle.opacity
+ || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
+ ) {
+ oElement.remove();
+ return;
+ }
+// if (['CENTER','FORM'].includes(name)) {
+ if ('FORM' === name || 'O:P' === name) {
+ replaceWithChildren(oElement);
+ return;
+ }
+/*
+ // Idea to allow CSS
+ if ('STYLE' === name) {
+ msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
+ oElement.textContent = oElement.textContent
+ .replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
+ .replace(/(background-)color:[^};]+/g, '');
+ return;
+ }
+*/
+ const aAttrsForRemove = [],
+ hasAttribute = name => oElement.hasAttribute(name),
+ getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
+ setAttribute = (name, value) => oElement.setAttribute(name, value),
+ delAttribute = name => oElement.removeAttribute(name);
+
+ if ('mail-body' === oElement.className) {
+ forEachObjectEntry(tasks, (name, cb) => {
+ if (hasAttribute(name)) {
+ cb(getAttribute(name), oElement);
+ delAttribute(name);
+ }
+ });
+ }
+
+ if (oElement.hasAttributes()) {
+ let i = oElement.attributes.length;
+ while (i--) {
+ let sAttrName = oElement.attributes[i].name.toLowerCase();
+ if (!allowedAttributes.includes(sAttrName)) {
+ delAttribute(sAttrName);
+ aAttrsForRemove.push(sAttrName);
+ }
+ }
+ }
+
+ let value;
+
+ if ('TABLE' === name) {
+ if (hasAttribute('width')) {
+ oStyle.width = getAttribute('width');
+ delAttribute('width');
+ }
+ value = oStyle.width;
+ if (value && !value.includes('%')) {
+ oStyle.maxWidth = value;
+ oStyle.width = '100%';
+ }
+ }
+
+ else if ('A' === name) {
+ value = oElement.href;
+ if (!/^([a-z]+):/i.test(value)) {
+ setAttribute('data-x-broken-href', value);
+ delAttribute('href');
+ } else {
+ oElement.href = stripTracking(value);
+ setAttribute('target', '_blank');
+ setAttribute('rel', 'external nofollow noopener noreferrer');
+ }
+ setAttribute('tabindex', '-1');
+ }
+
+ // SVG xlink:href
+ /*
+ if (hasAttribute('xlink:href')) {
+ delAttribute('xlink:href');
+ }
+ */
+
+ let skipStyle = false;
+ if (hasAttribute('src')) {
+ value = getAttribute('src');
+ delAttribute('src');
+
+ if (detectHiddenImages
+ && 'IMG' === name
+ && (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
+ || ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
+ || [
+ 'email.microsoftemail.com/open',
+ 'github.com/notifications/beacon/',
+ 'mandrillapp.com/track/open',
+ 'list-manage.com/track/open'
+ ].filter(uri => value.toLowerCase().includes(uri)).length
+ )) {
+ skipStyle = true;
+ setAttribute('style', 'display:none');
+ setAttribute('data-x-hidden-src', value);
+ }
+ else if (contentLocationUrls[value])
+ {
+ setAttribute('data-x-src-location', value);
+ result.foundContentLocationUrls.push(value);
+ }
+ else if ('cid:' === value.slice(0, 4))
+ {
+ setAttribute('data-x-src-cid', value.slice(4));
+ result.foundCIDs.push(value.slice(4));
+ }
+ else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
+ {
+ setAttribute('data-x-src', useProxy ? proxy(value) : value);
+ result.hasExternals = true;
+ }
+ else if ('data:image/' === value.slice(0, 11))
+ {
+ setAttribute('src', value);
+ }
+ else
+ {
+ setAttribute('data-x-broken-src', value);
+ }
+ }
+
+ if (hasAttribute('background')) {
+ oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
+ delAttribute('background');
+ }
+
+ if (hasAttribute('bgcolor')) {
+ oStyle.backgroundColor = getAttribute('bgcolor');
+ delAttribute('bgcolor');
+ }
+
+ if (hasAttribute('color')) {
+ oStyle.color = getAttribute('color');
+ delAttribute('color');
+ }
+
+ if (!skipStyle) {
+/*
+ if ('fixed' === oStyle.position) {
+ oStyle.position = 'absolute';
+ }
+*/
+ oStyle.removeProperty('behavior');
+ oStyle.removeProperty('cursor');
+ oStyle.removeProperty('min-width');
+
+ const urls = {
+ cid: [], // 'data-x-style-cid'
+ remote: [], // 'data-x-style-url'
+ broken: [] // 'data-x-broken-style-src'
+ };
+ ['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
+ if (oStyle[property]) {
+ let value = oStyle[property],
+ found = value.match(/url\s*\(([^)]+)\)/gi);
+ if (found) {
+ oStyle[property] = null;
+ found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
+ let lowerUrl = found.toLowerCase();
+ if ('cid:' === lowerUrl.slice(0, 4)) {
+ found = found.slice(4);
+ urls.cid[property] = found
+ result.foundCIDs.push(found);
+ } else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
+ result.hasExternals = true;
+ urls.remote[property] = useProxy ? proxy(found) : found;
+ } else if ('data:image/' === lowerUrl.slice(0, 11)) {
+ oStyle[property] = value;
+ } else {
+ urls.broken[property] = found;
+ }
+ }
+ }
+ });
+// oStyle.removeProperty('background-image');
+// oStyle.removeProperty('list-style-image');
+
+ if (urls.cid.length) {
+ setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
+ }
+ if (urls.remote.length) {
+ setAttribute('data-x-style-url', JSON.stringify(urls.remote));
+ }
+ if (urls.broken.length) {
+ setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
+ }
+
+ if (11 > pInt(oStyle.fontSize)) {
+ oStyle.removeProperty('font-size');
+ }
+
+ // Removes background and color
+ // Many e-mails incorrectly only define one, not both
+ // And in dark theme mode this kills the readability
+ if (removeColors) {
+ oStyle.removeProperty('background-color');
+ oStyle.removeProperty('background-image');
+ oStyle.removeProperty('color');
+ }
+
+ // Drop Microsoft Office style properties
+// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
+ }
+
+ if (debug && aAttrsForRemove.length) {
+ setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
+ }
+ });
+
+// return tpl.content.firstChild;
+ result.html = tpl.innerHTML.trim();
+ return result;
+ },
+
+ /**
+ * @param {string} html
+ * @returns {string}
+ */
+ htmlToPlain = html => {
+ const
+ hr = '⎯'.repeat(64),
+ forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
+ blockquotes = node => {
+ let bq;
+ while ((bq = node.querySelector('blockquote'))) {
+ // Convert child blockquote first
+ blockquotes(bq);
+ // Convert blockquote
+// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '> ');
+// replaceWithChildren(bq);
+ bq.replaceWith(
+ '\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
+ );
+ }
+ };
+
+ html = html
+ .replace(/]*>([\s\S]*?)<\/pre>/gim, (...args) =>
+ 1 < args.length ? args[1].toString().replace(/\n/g, ' ') : '')
+ .replace(/\r?\n/g, '')
+ .replace(/\s+/gm, ' ');
+
+ while (/<(div|tr)[\s>]/i.test(html)) {
+ html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
+ }
+ while (/<\/(div|tr)[\s>]/i.test(html)) {
+ html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
+ }
+
+ tpl.innerHTML = html
+ .replace(//gi, '\t')
+ .replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
+
+ // Convert line-breaks
+ forEach('br', br => br.replaceWith('\n'));
+
+ // lines
+ forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
+
+ // headings
+ forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
+
+ // paragraphs
+ forEach('p', node => {
+ node.prepend('\n\n');
+ if ('' == node.textContent.trim()) {
+ node.remove();
+ } else {
+ node.after('\n\n');
+ }
+ });
+
+ // proper indenting and numbering of (un)ordered lists
+ forEach('ol,ul', node => {
+ let prefix = '',
+ parent = node,
+ ordered = 'OL' == node.tagName,
+ i = 0;
+ while (parent && parent.parentNode && parent.parentNode.closest) {
+ parent = parent.parentNode.closest('ol,ul');
+ parent && (prefix = ' ' + prefix);
+ }
+ node.querySelectorAll(':scope > li').forEach(li => {
+ li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
+ });
+ node.prepend('\n\n');
+ node.after('\n\n');
+ });
+
+ // Convert anchors
+ forEach('a', a => {
+ let txt = a.textContent, href = a.href;
+ return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
+ });
+
+ // Bold
+ forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
+ // Italic
+ forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
+
+ // Blockquotes must be last
+ blockquotes(tpl.content);
+
+ return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
+ },
+
+ /**
+ * @param {string} plain
+ * @param {boolean} findEmailAndLinksInText = false
+ * @returns {string}
+ */
+ plainToHtml = plain => {
+ plain = stripTracking(plain)
+ .toString()
+ .replace(/\r/g, '')
+ .replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
+
+ let bIn = false,
+ bDo = true,
+ bStart = true,
+ aNextText = [],
+ aText = plain.split('\n');
+
+ do {
+ bDo = false;
+ aNextText = [];
+ aText.forEach(sLine => {
+ bStart = '>' === sLine.slice(0, 1);
+ if (bStart && !bIn) {
+ bDo = true;
+ bIn = true;
+ aNextText.push('~~~blockquote~~~');
+ aNextText.push(sLine.slice(1));
+ } else if (!bStart && bIn) {
+ if (sLine) {
+ bIn = false;
+ aNextText.push('~~~/blockquote~~~');
+ aNextText.push(sLine);
+ } else {
+ aNextText.push(sLine);
+ }
+ } else if (bStart && bIn) {
+ aNextText.push(sLine.slice(1));
+ } else {
+ aNextText.push(sLine);
+ }
+ });
+
+ if (bIn) {
+ bIn = false;
+ aNextText.push('~~~/blockquote~~~');
+ }
+
+ aText = aNextText;
+ } while (bDo);
+
+ return aText.join('\n')
+ // .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
+ .replace(/&/g, '&')
+ .replace(/>/g, '>')
+ .replace(/')
+ .replace(/\s*~~~\/blockquote~~~/g, '')
+ .replace(/\n/g, ' ');
};
-/**
- * @param {string} text
- * @returns {string}
- */
-export function encodeHtml(text) {
- return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]);
-}
-
-class HtmlEditor {
+export class HtmlEditor {
/**
* @param {Object} element
* @param {Function=} onBlur
@@ -24,25 +501,37 @@ class HtmlEditor {
* @param {Function=} onModeChange
*/
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
- this.editor;
this.blurTimer = 0;
- this.__resizable = false;
- this.__inited = false;
-
this.onBlur = onBlur;
- this.onReady = onReady;
this.onModeChange = onModeChange;
- this.element = element;
+ if (element) {
+ let editor;
- this.resize = (() => {
- try {
- this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
- } catch (e) {} // eslint-disable-line no-empty
- }).throttle(100);
+ onReady = onReady ? [onReady] : [];
+ this.onReady = fn => onReady.push(fn);
+ const readyCallback = () => {
+ this.editor = editor;
+ this.onReady = fn => fn();
+ onReady.forEach(fn => fn());
+ };
- this.init();
+ if (rl.createWYSIWYG) {
+ editor = rl.createWYSIWYG(element, readyCallback);
+ }
+ if (!editor) {
+ editor = new SquireUI(element);
+ setTimeout(readyCallback, 1);
+ }
+
+ editor.on('blur', () => this.blurTrigger());
+ editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
+ editor.on('mode', () => {
+ this.blurTrigger();
+ this.onModeChange && this.onModeChange(!this.isPlain());
+ });
+ }
}
blurTrigger() {
@@ -70,9 +559,9 @@ class HtmlEditor {
* @returns {void}
*/
clearCachedSignature() {
- this.editor && this.editor.execCommand('insertSignature', {
+ this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true
- });
+ }));
}
/**
@@ -82,75 +571,66 @@ class HtmlEditor {
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
- this.editor && this.editor.execCommand('insertSignature', {
+ this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
- });
+ }));
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
- getData(wrapIsHtml = false) {
+ getData() {
let result = '';
if (this.editor) {
try {
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData();
} else {
- result = wrapIsHtml
- ? '' +
- this.editor.getData() +
- '
'
- : this.editor.getData();
+ result = this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
-
return result;
}
/**
- * @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
- getDataWithHtmlMark(wrapIsHtml = false) {
- return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
+ getDataWithHtmlMark() {
+ return (this.isHtml() ? ':HTML:' : '') + this.getData();
}
modeWysiwyg() {
- try {
- this.editor && this.editor.setMode('wysiwyg');
- } catch (e) { console.error(e); }
+ this.onReady(() => this.editor.setMode('wysiwyg'));
}
modePlain() {
- try {
- this.editor && this.editor.setMode('plain');
- } catch (e) { console.error(e); }
+ this.onReady(() => this.editor.setMode('plain'));
}
setHtmlOrPlain(text) {
- if (':HTML:' === text.substr(0, 6)) {
- this.setHtml(text.substr(6));
+ if (':HTML:' === text.slice(0, 6)) {
+ this.setHtml(text.slice(6));
} else {
this.setPlain(text);
}
}
setData(mode, data) {
- if (this.editor && this.__inited) {
+ this.onReady(() => {
+ const editor = this.editor;
this.clearCachedSignature();
try {
- this.editor.setMode(mode);
- if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
- this.editor.__plain.setRawData(data);
+ editor.setMode(mode);
+ if (this.isPlain() && editor.plugins.plain && editor.__plain) {
+ editor.__plain.setRawData(data);
} else {
- this.editor.setData(data);
+ editor.setData(data);
}
} catch (e) { console.error(e); }
- }
+ });
}
setHtml(html) {
@@ -161,46 +641,8 @@ class HtmlEditor {
this.setData('plain', txt);
}
- init() {
- if (this.element && !this.editor) {
- const onReady = () => {
- if (this.editor.removeMenuItem) {
- this.editor.removeMenuItem('cut');
- this.editor.removeMenuItem('copy');
- this.editor.removeMenuItem('paste');
- }
-
- this.__resizable = true;
- this.__inited = true;
-
- this.resize();
-
- this.onReady && this.onReady();
- };
-
- if (rl.createWYSIWYG) {
- this.editor = rl.createWYSIWYG(this.element, onReady);
- }
- if (!this.editor) {
- this.editor = new SquireUI(this.element);
- setTimeout(onReady,1);
- }
-
- if (this.editor) {
- this.editor.on('blur', () => this.blurTrigger());
- this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
- this.editor.on('mode', () => {
- this.blurTrigger();
- this.onModeChange && this.onModeChange(!this.isPlain());
- });
- }
- }
- }
-
focus() {
- try {
- this.editor && this.editor.focus();
- } catch (e) {} // eslint-disable-line no-empty
+ this.onReady(() => this.editor.focus());
}
hasFocus() {
@@ -212,14 +654,15 @@ class HtmlEditor {
}
blur() {
- try {
- this.editor && this.editor.focusManager.blur(true);
- } catch (e) {} // eslint-disable-line no-empty
+ this.onReady(() => this.editor.focusManager.blur(true));
}
clear() {
- this.setHtml('');
+ this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
}
}
-export { HtmlEditor, HtmlEditor as default };
+rl.Utils = {
+ htmlToPlain: htmlToPlain,
+ plainToHtml: plainToHtml
+};
diff --git a/dev/Common/Links.js b/dev/Common/Links.js
index c53d87b1d..cae3e49f6 100644
--- a/dev/Common/Links.js
+++ b/dev/Common/Links.js
@@ -1,191 +1,140 @@
import { pString, pInt } from 'Common/Utils';
-import { Settings, SettingsGet } from 'Common/Globals';
+import { Settings } from 'Common/Globals';
const
- ROOT = './',
HASH_PREFIX = '#/',
SERVER_PREFIX = './?',
VERSION = Settings.app('version'),
- VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
+ VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
- getHash = () => SettingsGet('AuthAccountHash') || '0';
+ adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
-/**
- * @returns {string}
- */
-export const SUB_QUERY_PREFIX = '&q[]=';
+ prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
-/**
- * @param {string=} startupUrl
- * @returns {string}
- */
-export function root(startupUrl = '') {
- return HASH_PREFIX + pString(startupUrl);
-}
+export const
+ SUB_QUERY_PREFIX = '&q[]=',
-/**
- * @returns {string}
- */
-export function logoutLink() {
- return (rl.adminArea() && !Settings.app('adminHostUse'))
- ? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
- : ROOT;
-}
+ /**
+ * @param {string=} startupUrl
+ * @returns {string}
+ */
+ root = () => HASH_PREFIX,
-/**
- * @param {string} type
- * @param {string} hash
- * @param {string=} customSpecSuffix
- * @returns {string}
- */
-export function serverRequestRaw(type, hash, customSpecSuffix) {
- return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
- + (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
+ /**
+ * @returns {string}
+ */
+ logoutLink = () => adminPath() ? prefix() : './',
+
+ /**
+ * @param {string} type
+ * @param {string} hash
+ * @param {string=} customSpecSuffix
+ * @returns {string}
+ */
+ serverRequestRaw = (type, hash) =>
+ SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ + '0/' // AuthAccountHash ?
+ (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
- : '')
- ;
-}
+ : ''),
-/**
- * @param {string} download
- * @param {string=} customSpecSuffix
- * @returns {string}
- */
-export function attachmentDownload(download, customSpecSuffix) {
- return serverRequestRaw('Download', download, customSpecSuffix);
-}
+ /**
+ * @param {string} download
+ * @param {string=} customSpecSuffix
+ * @returns {string}
+ */
+ attachmentDownload = (download, customSpecSuffix) =>
+ serverRequestRaw('Download', download, customSpecSuffix),
-/**
- * @param {string} type
- * @returns {string}
- */
-export function serverRequest(type) {
- return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
-}
+ proxy = url =>
+ SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
+/*
+ return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
+ 'Rnd' => \md5(\microtime(true)),
+ 'Token' => Utils::GetConnectionToken(),
+ 'Url' => $sUrl
+ )).'/';
+*/
-/**
- * @param {string} email
- * @returns {string}
- */
-export function change(email) {
- return serverRequest('Change') + encodeURIComponent(email) + '/';
-}
+ /**
+ * @param {string} type
+ * @returns {string}
+ */
+ serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
-/**
- * @param {string} hash
- * @returns {string}
- */
-export function userBackground(hash) {
- return serverRequestRaw('UserBackground', hash);
-}
+ /**
+ * @param {string} lang
+ * @param {boolean} isAdmin
+ * @returns {string}
+ */
+ langLink = (lang, isAdmin) =>
+ SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
-/**
- * @param {string} lang
- * @param {boolean} isAdmin
- * @returns {string}
- */
-export function langLink(lang, isAdmin) {
- return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
-}
+ /**
+ * @param {string} path
+ * @returns {string}
+ */
+ staticLink = path => VERSION_PREFIX() + 'static/' + path,
-/**
- * @param {string} path
- * @returns {string}
- */
-export function staticLink(path) {
- return VERSION_PREFIX + 'static/' + path;
-}
+ /**
+ * @param {string} theme
+ * @returns {string}
+ */
+ themePreviewLink = theme => {
+ let prefix = VERSION_PREFIX();
+ if ('@custom' === theme.slice(-7)) {
+ theme = theme.slice(0, theme.length - 7).trim();
+ prefix = Settings.app('webPath') || '';
+ }
-/**
- * @returns {string}
- */
-export function openPgpJs() {
- return staticLink('js/min/openpgp.min.js');
-}
+ return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
+ },
-/**
- * @returns {string}
- */
-export function openPgpWorkerJs() {
- return staticLink('js/min/openpgp.worker.min.js');
-}
+ /**
+ * @param {string} inboxFolderName = 'INBOX'
+ * @returns {string}
+ */
+ mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
-/**
- * @param {string} theme
- * @returns {string}
- */
-export function themePreviewLink(theme) {
- let prefix = VERSION_PREFIX;
- if ('@custom' === theme.substr(-7)) {
- theme = theme.substr(0, theme.length - 7).trim();
- prefix = Settings.app('webPath') || '';
- }
+ /**
+ * @param {string=} screenName = ''
+ * @returns {string}
+ */
+ settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
- return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
-}
+ /**
+ * @param {string=} screenName
+ * @returns {string}
+ */
+ admin = screenName => HASH_PREFIX + (
+ 'AdminDomains' == screenName ? 'domains'
+ : 'AdminSecurity' == screenName ? 'security'
+ : ''
+ ),
-/**
- * @param {string} inboxFolderName = 'INBOX'
- * @returns {string}
- */
-export function mailbox(inboxFolderName = 'INBOX') {
- return HASH_PREFIX + 'mailbox/' + inboxFolderName;
-}
+ /**
+ * @param {string} folder
+ * @param {number=} page = 1
+ * @param {string=} search = ''
+ * @param {number=} threadUid = 0
+ * @returns {string}
+ */
+ mailBox = (folder, page, search, threadUid) => {
+ let result = [HASH_PREFIX + 'mailbox'];
-/**
- * @param {string=} screenName = ''
- * @returns {string}
- */
-export function settings(screenName = '') {
- return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
-}
+ if (folder) {
+ result.push(folder + (threadUid ? '~' + threadUid : ''));
+ }
-/**
- * @param {string} screenName
- * @returns {string}
- */
-export function admin(screenName) {
- let result = HASH_PREFIX;
- switch (screenName) {
- case 'AdminDomains':
- result += 'domains';
- break;
- case 'AdminSecurity':
- result += 'security';
- break;
- // no default
- }
+ page = pInt(page, 1);
+ if (1 < page) {
+ result.push('p' + page);
+ }
- return result;
-}
-
-/**
- * @param {string} folder
- * @param {number=} page = 1
- * @param {string=} search = ''
- * @param {string=} threadUid = ''
- * @returns {string}
- */
-export function mailBox(folder, page = 1, search = '', threadUid = '') {
- page = pInt(page, 1);
- search = pString(search);
-
- let result = HASH_PREFIX + 'mailbox/';
-
- if (folder) {
- const resultThreadUid = pInt(threadUid);
- result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
- }
-
- if (1 < page) {
- result = result.replace(/\/+$/, '') + '/p' + page;
- }
-
- if (search) {
- result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
- }
-
- return result;
-}
+ search = pString(search);
+ if (search) {
+ result.push(encodeURI(search));
+ }
+ return result.join('/');
+ };
diff --git a/dev/Common/Momentor.js b/dev/Common/Momentor.js
deleted file mode 100644
index 6da0a88ce..000000000
--- a/dev/Common/Momentor.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { doc } from 'Common/Globals';
-import { i18n } from 'Common/Translator';
-
-export function timestampToString(timeStampInUTC, formatStr) {
- const now = Date.now(),
- time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
-
- if (31536000000 < time) {
- const m = new Date(time);
- switch (formatStr) {
- case 'FROMNOW':
- return m.fromNow();
- case 'SHORT': {
- if (4 >= (now - time) / 3600000)
- return m.fromNow();
- const mt = m.getTime(), date = new Date,
- dt = date.setHours(0,0,0,0);
- if (mt > dt)
- return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
- if (mt > dt - 86400000)
- return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
- if (date.getFullYear() === m.getFullYear())
- return m.format('d M');
- return m.format('LL');
- }
- case 'FULL':
- return m.format('LLL');
- default:
- return m.format(formatStr);
- }
- }
-
- return '';
-}
-
-export function timeToNode(element, time) {
- try {
- time = time || (Date.parse(element.dateTime) / 1000);
- if (time) {
- element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
-
- let key = element.dataset.momentFormat;
- if (key) {
- element.textContent = timestampToString(time, key);
- }
-
- if ((key = element.dataset.momentFormatTitle)) {
- element.title = timestampToString(time, key);
- }
- }
- } catch (e) {
- // prevent knockout crashes
- console.error(e);
- }
-}
-
-addEventListener('reload-time', () => setTimeout(() =>
- doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
- , 1)
-);
diff --git a/dev/Common/Plugins.js b/dev/Common/Plugins.js
index 01a8bcd68..584995b9c 100644
--- a/dev/Common/Plugins.js
+++ b/dev/Common/Plugins.js
@@ -1,5 +1,6 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals';
+import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { isArray, isFunction } from 'Common/Utils';
const SIMPLE_HOOKS = {},
@@ -36,7 +37,7 @@ export function runHook(name, args = []) {
* @param {?number=} timeout
*/
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
- rl.app && rl.app.Remote.defaultRequest(callback, 'Plugin' + action, parameters, timeout);
+ rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
};
/**
@@ -78,3 +79,5 @@ rl.pluginSettingsGet = (pluginSection, name) => {
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
};
+
+rl.pluginPopupView = AbstractViewPopup;
diff --git a/dev/Common/Selector.js b/dev/Common/Selector.js
index 109b30135..f413fd76c 100644
--- a/dev/Common/Selector.js
+++ b/dev/Common/Selector.js
@@ -1,5 +1,16 @@
import ko from 'ko';
+import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils';
+import { koComputable } from 'External/ko';
+
+/*
+ oCallbacks:
+ ItemSelect
+ MiddleClick
+ AutoSelect
+ ItemGetUid
+ UpOrDown
+*/
export class Selector {
/**
@@ -19,8 +30,8 @@ export class Selector {
sItemFocusedSelector
) {
this.list = koList;
- this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
- this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
+ this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
+ this.isListChecked = koComputable(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null);
@@ -209,11 +220,9 @@ export class Selector {
itemSelected(item) {
if (this.isListChecked()) {
- if (!item) {
- (this.oCallbacks.onItemSelect || (()=>{}))(item || null);
- }
+ item || (this.oCallbacks.ItemSelect || (()=>0))(null);
} else if (item) {
- (this.oCallbacks.onItemSelect || (()=>{}))(item);
+ (this.oCallbacks.ItemSelect || (()=>0))(item);
}
}
@@ -226,13 +235,17 @@ export class Selector {
this.oContentScrollable = contentScrollable;
if (contentScrollable) {
- contentScrollable.addEventListener('click', event => {
- let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
- el && this.actionClick(ko.dataFor(el), event);
+ let getItem = selector => {
+ let el = event.target.closestWithin(selector, contentScrollable);
+ return el ? ko.dataFor(el) : null;
+ };
- el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
- if (el) {
- const item = ko.dataFor(el);
+ addEventsListeners(contentScrollable, {
+ click: event => {
+ let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
+ el && this.actionClick(ko.dataFor(el), event);
+
+ const item = getItem(this.sItemCheckedSelector);
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
@@ -241,26 +254,33 @@ export class Selector {
item.checked(!item.checked());
}
}
+ },
+ auxclick: event => {
+ if (1 == event.button) {
+ const item = getItem(this.sItemSelector);
+ if (item) {
+ this.focusedItem(item);
+ (this.oCallbacks.MiddleClick || (()=>0))(item);
+ }
+ }
}
});
- shortcuts.add('enter,open', '', keyScope, () => {
+ registerShortcut('enter,open', '', keyScope, () => {
const focused = this.focusedItem();
if (focused && !focused.selected()) {
this.actionClick(focused);
return false;
}
-
- return true;
});
- shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false);
+ addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
- shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
+ addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
this.newSelectPosition(event.key, true);
return false;
});
- shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
+ registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
this.newSelectPosition(event.key, false);
return false;
});
@@ -271,7 +291,7 @@ export class Selector {
* @returns {boolean}
*/
autoSelect() {
- return !!(this.oCallbacks.onAutoSelect || (()=>true))();
+ return !!(this.oCallbacks.AutoSelect || (()=>1))();
}
/**
@@ -281,7 +301,7 @@ export class Selector {
getItemUid(item) {
let uid = '';
- const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
+ const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
if (getItemUidCallback && item) {
uid = getItemUidCallback(item);
}
@@ -312,7 +332,7 @@ export class Selector {
} else if (++i < listLen) {
result = list[i];
}
- result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
+ result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
} else if ('Home' === sEventKey) {
result = list[0];
} else if ('End' === sEventKey) {
diff --git a/dev/Common/Translator.js b/dev/Common/Translator.js
index a979eb817..852bd36d1 100644
--- a/dev/Common/Translator.js
+++ b/dev/Common/Translator.js
@@ -2,158 +2,211 @@ import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals';
+import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
-let I18N_DATA = window.snappymailI18N || {};
+let I18N_DATA = {};
-export const trigger = ko.observable(false);
-
-/**
- * @param {string} key
- * @param {Object=} valueList
- * @param {string=} defaulValue
- * @returns {string}
- */
-export function i18n(key, valueList, defaulValue) {
- let path = key.split('/');
- if (!I18N_DATA[path[0]] || !path[1]) {
- return defaulValue || key;
- }
- let result = I18N_DATA[path[0]][path[1]] || defaulValue || key;
- if (valueList) {
- Object.entries(valueList).forEach(([key, value]) => {
- result = result.replace('%' + key + '%', value);
- });
- }
- return result;
-}
-
-const i18nToNode = element => {
- const key = element.dataset.i18n;
- if (key) {
- if ('[' === key.substr(0, 1)) {
- switch (key.substr(0, 6)) {
- case '[html]':
- element.innerHTML = i18n(key.substr(6));
- break;
- case '[place':
- element.placeholder = i18n(key.substr(13));
- break;
- case '[title':
- element.title = i18n(key.substr(7));
- break;
- // no default
+const
+ i18nToNode = element => {
+ const key = element.dataset.i18n;
+ if (key) {
+ if ('[' === key.slice(0, 1)) {
+ switch (key.slice(0, 6)) {
+ case '[html]':
+ element.innerHTML = i18n(key.slice(6));
+ break;
+ case '[place':
+ element.placeholder = i18n(key.slice(13));
+ break;
+ case '[title':
+ element.title = i18n(key.slice(7));
+ break;
+ // no default
+ }
+ } else {
+ element.textContent = i18n(key);
}
- } else {
- element.textContent = i18n(key);
}
- }
-},
+ },
+
+ init = () => {
+ if (rl.I18N) {
+ I18N_DATA = rl.I18N;
+ Date.defineRelativeTimeFormat(rl.relativeTime || {});
+ rl.I18N = null;
+ return 1;
+ }
+ },
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
- getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
+ getNotificationMessage = code => {
+ let key = getKeyByValue(Notification, code);
+ return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
+ };
-/**
- * @param {Object} elements
- * @param {boolean=} animate = false
- */
-export function i18nToNodes(element) {
- setTimeout(() =>
- element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
- , 1);
-}
+export const
+ trigger = ko.observable(false),
-/**
- * @param {Function} startCallback
- * @param {Function=} langCallback = null
- */
-export function initOnStartOrLangChange(startCallback, langCallback = null) {
- startCallback && startCallback();
- startCallback && trigger.subscribe(startCallback);
- langCallback && trigger.subscribe(langCallback);
-}
-
-function getNotificationMessage(code) {
- let key = getKeyByValue(Notification, code);
- if (key) {
- key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
- return I18N_DATA.NOTIFICATIONS[key];
- }
-}
-
-/**
- * @param {number} code
- * @param {*=} message = ''
- * @param {*=} defCode = null
- * @returns {string}
- */
-export function getNotification(code, message = '', defCode = 0) {
- code = parseInt(code, 10) || 0;
- if (Notification.ClientViewError === code && message) {
- return message;
- }
-
- return getNotificationMessage(code)
- || getNotificationMessage(parseInt(defCode, 10))
- || '';
-}
-
-/**
- * @param {*} code
- * @returns {string}
- */
-export function getUploadErrorDescByCode(code) {
- let result = 'UNKNOWN';
- code = parseInt(code, 10) || 0;
- switch (code) {
- case UploadErrorCode.FileIsTooBig:
- case UploadErrorCode.FilePartiallyUploaded:
- case UploadErrorCode.NoFileUploaded:
- case UploadErrorCode.MissingTempFolder:
- case UploadErrorCode.OnSavingFile:
- case UploadErrorCode.FileType:
- result = i18nKey(getKeyByValue(UploadErrorCode, code));
- break;
- }
- return i18n('UPLOAD/ERROR_' + result);
-}
-
-/**
- * @param {boolean} admin
- * @param {string} language
- */
-export function reload(admin, language) {
- return new Promise((resolve, reject) => {
- const script = createElement('script');
- script.onload = () => {
- // reload the data
- if (window.snappymailI18N) {
- I18N_DATA = window.snappymailI18N;
- i18nToNodes(doc);
- dispatchEvent(new CustomEvent('reload-time'));
- trigger(!trigger());
+ /**
+ * @param {string} key
+ * @param {Object=} valueList
+ * @param {string=} defaulValue
+ * @returns {string}
+ */
+ i18n = (key, valueList, defaulValue) => {
+ let result = defaulValue || key;
+ if (key) {
+ let path = key.split('/');
+ if (I18N_DATA[path[0]] && path[1]) {
+ result = I18N_DATA[path[0]][path[1]] || result;
}
- window.snappymailI18N = null;
- script.remove();
- resolve();
- };
- script.onerror = () => reject(new Error('Language '+language+' failed'));
- script.src = langLink(language, admin);
-// script.async = true;
- doc.head.append(script);
- });
-}
+ }
+ if (valueList) {
+ forEachObjectEntry(valueList, (key, value) => {
+ result = result.replace('%' + key + '%', value);
+ });
+ }
+ return result;
+ },
-/**
- *
- * @param {string} language
- * @param {boolean=} isEng = false
- * @returns {string}
- */
-export function convertLangName(language, isEng = false) {
- return i18n(
- 'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
- null,
- language
- );
-}
+ /**
+ * @param {Object} elements
+ * @param {boolean=} animate = false
+ */
+ i18nToNodes = element =>
+ setTimeout(() =>
+ element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
+ , 1),
+
+ timestampToString = (timeStampInUTC, formatStr) => {
+ const now = Date.now(),
+ time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
+
+ if (31536000000 < time) {
+ const m = new Date(time);
+ switch (formatStr) {
+ case 'FROMNOW':
+ return m.fromNow();
+ case 'SHORT': {
+ if (4 >= (now - time) / 3600000)
+ return m.fromNow();
+ const mt = m.getTime(), date = new Date,
+ dt = date.setHours(0,0,0,0);
+ if (mt > dt)
+ return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
+ if (mt > dt - 86400000)
+ return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
+ if (date.getFullYear() === m.getFullYear())
+ return m.format('d M');
+ return m.format('LL');
+ }
+ case 'FULL':
+ return m.format('LLL');
+ default:
+ return m.format(formatStr);
+ }
+ }
+
+ return '';
+ },
+
+ timeToNode = (element, time) => {
+ try {
+ if (time) {
+ element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
+ } else {
+ time = Date.parse(element.dateTime) / 1000;
+ }
+
+ let key = element.dataset.momentFormat;
+ if (key) {
+ element.textContent = timestampToString(time, key);
+ }
+
+ if ((key = element.dataset.momentFormatTitle)) {
+ element.title = timestampToString(time, key);
+ }
+ } catch (e) {
+ // prevent knockout crashes
+ console.error(e);
+ }
+ },
+
+ reloadTime = () => setTimeout(() =>
+ doc.querySelectorAll('time').forEach(element => timeToNode(element))
+ , 1),
+
+ /**
+ * @param {Function} startCallback
+ * @param {Function=} langCallback = null
+ */
+ initOnStartOrLangChange = (startCallback, langCallback = null) => {
+ startCallback && startCallback();
+ startCallback && trigger.subscribe(startCallback);
+ langCallback && trigger.subscribe(langCallback);
+ },
+
+ /**
+ * @param {number} code
+ * @param {*=} message = ''
+ * @param {*=} defCode = null
+ * @returns {string}
+ */
+ getNotification = (code, message = '', defCode = 0) => {
+ code = parseInt(code, 10) || 0;
+ if (Notification.ClientViewError === code && message) {
+ return message;
+ }
+
+ return getNotificationMessage(code)
+ || getNotificationMessage(parseInt(defCode, 10))
+ || '';
+ },
+
+ /**
+ * @param {*} code
+ * @returns {string}
+ */
+ getUploadErrorDescByCode = code => {
+ let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
+ return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
+ },
+
+ /**
+ * @param {boolean} admin
+ * @param {string} language
+ */
+ translatorReload = (admin, language) =>
+ new Promise((resolve, reject) => {
+ const script = createElement('script');
+ script.onload = () => {
+ // reload the data
+ if (init()) {
+ i18nToNodes(doc);
+ admin || reloadTime();
+ trigger(!trigger());
+ }
+ script.remove();
+ resolve();
+ };
+ script.onerror = () => reject(new Error('Language '+language+' failed'));
+ script.src = langLink(language, admin);
+ // script.async = true;
+ doc.head.append(script);
+ }),
+
+ /**
+ *
+ * @param {string} language
+ * @param {boolean=} isEng = false
+ * @returns {string}
+ */
+ convertLangName = (language, isEng = false) =>
+ i18n(
+ 'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
+ null,
+ language
+ );
+
+init();
diff --git a/dev/Common/Utils.js b/dev/Common/Utils.js
index 89b55658a..98cc9cffd 100644
--- a/dev/Common/Utils.js
+++ b/dev/Common/Utils.js
@@ -1,107 +1,50 @@
import { SaveSettingsStep } from 'Common/Enums';
-import { doc, createElement, elementById } from 'Common/Globals';
-
-export const
- isArray = Array.isArray,
- isNonEmptyArray = array => isArray(array) && array.length,
- isFunction = v => typeof v === 'function';
-
-/**
- * @param {*} value
- * @param {number=} defaultValue = 0
- * @returns {number}
- */
-export function pInt(value, defaultValue = 0) {
- value = parseInt(value, 10);
- return isNaN(value) || !isFinite(value) ? defaultValue : value;
-}
-
-/**
- * @param {*} value
- * @returns {string}
- */
-export function pString(value) {
- return null != value ? '' + value : '';
-}
-
-/**
- * @returns {boolean}
- */
-export function inFocus() {
- try {
- return doc.activeElement && doc.activeElement.matches(
- 'input,textarea,.cke_editable'
- );
- } catch (e) {
- return false;
- }
-}
-
-/**
- * @param {string} theme
- * @returns {string}
- */
-export const convertThemeName = theme => {
- if ('@custom' === theme.substr(-7)) {
- theme = theme.substr(0, theme.length - 7).trim();
- }
-
- return theme
- .replace(/([A-Z])/g, ' $1')
- .replace(/[^a-zA-Z0-9]+/g, ' ')
- .trim();
-};
-
-/**
- * @param {object} domOption
- * @param {object} item
- * @returns {void}
- */
-export function defaultOptionsAfterRender(domItem, item) {
- if (item && undefined !== item.disabled && domItem) {
- domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
- }
-}
-
-/**
- * @param {object} koTrigger
- * @param {mixed} context
- * @returns {mixed}
- */
-export function settingsSaveHelperSimpleFunction(koTrigger, context) {
- return iError => {
- koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
- setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
- };
-}
+import { elementById } from 'Common/Globals';
let __themeTimer = 0,
__themeJson = null;
-/**
- * @param {string} value
- * @param {function=} themeTrigger = noop
- * @returns {void}
- */
-export function changeTheme(value, themeTrigger = ()=>{}) {
- const themeLink = elementById('app-theme-link'),
- clearTimer = () => {
- __themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
- __themeJson = null;
- };
+export const
+ isArray = Array.isArray,
+ arrayLength = array => isArray(array) && array.length,
+ isFunction = v => typeof v === 'function',
+ pString = value => null != value ? '' + value : '',
- let themeStyle = elementById('app-theme-style'),
- url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
+ forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
- if (url) {
- url = url.toString()
- .replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
- .replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
- .replace(/\/Hash\/[^/]+\//, '/Hash/-/');
+ forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
- if ('Json/' !== url.substr(-5)) {
- url += 'Json/';
- }
+ pInt = (value, defaultValue = 0) => {
+ value = parseInt(value, 10);
+ return isNaN(value) || !isFinite(value) ? defaultValue : value;
+ },
+
+ convertThemeName = theme => theme
+ .replace(/@custom$/, '')
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
+ .trim(),
+
+ defaultOptionsAfterRender = (domItem, item) =>
+ domItem && item && undefined !== item.disabled
+ && domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
+
+ // unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
+ b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
+/* // Without deprecated 'unescape':
+ b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
+ /%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
+ )),
+*/
+ b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
+
+ changeTheme = (value, themeTrigger = ()=>0) => {
+ const themeStyle = elementById('app-theme-style'),
+ clearTimer = () => {
+ __themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
+ __themeJson = null;
+ },
+ url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
clearTimeout(__themeTimer);
@@ -117,36 +60,12 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
}
rl.fetchJSON(url, init)
.then(data => {
- if (data && isArray(data) && 2 === data.length) {
- if (themeLink && !themeStyle) {
- themeStyle = createElement('style');
- themeStyle.id = 'app-theme-style';
- themeLink.after(themeStyle);
- themeLink.remove();
- }
-
- if (themeStyle) {
- themeStyle.textContent = data[1];
- themeStyle.dataset.href = url;
- themeStyle.dataset.theme = data[0];
- }
-
+ if (2 === arrayLength(data)) {
+ themeStyle.textContent = data[1];
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
- }
-}
+ },
-export function addObservablesTo(target, observables) {
- Object.entries(observables).forEach(([key, value]) =>
- target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) );
-}
-
-export function addComputablesTo(target, computables) {
- Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn));
-}
-
-export function addSubscribablesTo(target, subscribables) {
- Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn));
-}
+ getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
diff --git a/dev/Common/UtilsUser.js b/dev/Common/UtilsUser.js
index 93fed0663..a8d4ca7ee 100644
--- a/dev/Common/UtilsUser.js
+++ b/dev/Common/UtilsUser.js
@@ -1,341 +1,46 @@
-import { ComposeType, FolderType } from 'Common/EnumsUser';
+import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
+import { Notification } from 'Common/Enums';
+import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
+import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
+import { plainToHtml } from 'Common/Html';
+import { getNotification } from 'Common/Translator';
import { EmailModel } from 'Model/Email';
-import { encodeHtml } from 'Common/Html';
-import { isArray } from 'Common/Utils';
-import { createElement } from 'Common/Globals';
+import { MessageModel } from 'Model/Message';
+import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
+import { SettingsUserStore } from 'Stores/User/Settings';
+import * as Local from 'Storage/Client';
+import { ThemeStore } from 'Stores/Theme';
+import Remote from 'Remote/User/Fetch';
+
+export const
+
+dropdownsDetectVisibility = (() =>
+ dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
+).debounce(50),
/**
- * @param {(string|number)} value
- * @param {boolean=} includeZero = true
+ * @param {string} link
* @returns {boolean}
*/
-export function isPosNumeric(value, includeZero = true) {
- return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
-}
-
-/**
- * @param {string} text
- * @param {number=} len = 100
- * @returns {string}
- */
-function splitPlainText(text, len = 100) {
- let prefix = '',
- subText = '',
- result = text,
- spacePos = 0,
- newLinePos = 0;
-
- while (result.length > len) {
- subText = result.substr(0, len);
- spacePos = subText.lastIndexOf(' ');
- newLinePos = subText.lastIndexOf('\n');
-
- if (-1 !== newLinePos) {
- spacePos = newLinePos;
- }
-
- if (-1 === spacePos) {
- spacePos = len;
- }
-
- prefix += subText.substr(0, spacePos) + '\n';
- result = result.substr(spacePos + 1);
+download = (link, name = "") => {
+ if (ThemeStore.isMobile()) {
+ open(link, '_self');
+ focus();
+ } else {
+ const oLink = createElement('a');
+ oLink.href = link;
+ oLink.target = '_blank';
+ oLink.download = name;
+ doc.body.appendChild(oLink).click();
+ oLink.remove();
}
-
- return prefix + result;
-}
-
-/**
- * @param {string} html
- * @returns {string}
- */
-export function htmlToPlain(html) {
- let pos = 0,
- limit = 0,
- iP1 = 0,
- iP2 = 0,
- iP3 = 0,
- text = '';
-
- const convertBlockquote = (blockquoteText) => {
- blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
- return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
- args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
- );
- };
-
- const convertDivs = (...args) => {
- if (args && 1 < args.length) {
- let divText = args[1].trim();
- if (divText.length) {
- divText = divText.replace(/]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
- divText = '\n' + divText.trim() + '\n';
- }
-
- return divText;
- }
-
- return '';
- };
-
- const
- tpl = createElement('template'),
- convertPre = (...args) =>
- args && 1 < args.length
- ? args[1]
- .toString()
- .replace(/[\n]/gm, '
')
- .replace(/[\r]/gm, '')
- : '',
- fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + encodeHtml(args[2]) : ''),
- convertLinks = (...args) => (args && 1 < args.length ? args[1].trim() : '');
-
- tpl.innerHTML = html
- .replace(/
]*><\/p>/gi, '')
- .replace(/
]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
- .replace(/[\s]+/gm, ' ')
- .replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
- .replace(/ ]*>/gim, '\n')
- .replace(/<\/h[\d]>/gi, '\n')
- .replace(/<\/p>/gi, '\n\n')
- .replace(/ ]*>/gim, '\n')
- .replace(/<\/ul>/gi, '\n')
- .replace(/]*>/gim, ' * ')
- .replace(/<\/li>/gi, '\n')
- .replace(/<\/td>/gi, '\n')
- .replace(/<\/tr>/gi, '\n')
- .replace(/ ]*>/gim, '\n_______________________________\n\n')
- .replace(/]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
- .replace(/
]*>/gim, '\n__bq__start__\n')
- .replace(/<\/blockquote>/gim, '\n__bq__end__\n')
- .replace(/]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
- .replace(/<\/div>/gi, '\n')
- .replace(/ /gi, ' ')
- .replace(/"/gi, '"')
- .replace(/<[^>]*>/gm, '');
-
- text = splitPlainText(tpl.content.textContent
- .replace(/\n[ \t]+/gm, '\n')
- .replace(/[\n]{3,}/gm, '\n\n')
- .replace(/>/gi, '>')
- .replace(/</gi, '<')
- .replace(/&/gi, '&')
- );
-
- pos = 0;
- limit = 800;
-
- while (0 < limit) {
- --limit;
- iP1 = text.indexOf('__bq__start__', pos);
- if (-1 < iP1) {
- iP2 = text.indexOf('__bq__start__', iP1 + 5);
- iP3 = text.indexOf('__bq__end__', iP1 + 5);
-
- if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
- text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
- pos = 0;
- } else if (-1 < iP2 && iP2 < iP3) {
- pos = iP2 - 1;
- } else {
- pos = 0;
- }
- } else {
- break;
- }
- }
-
- return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
-}
-
-/**
- * @param {string} plain
- * @param {boolean} findEmailAndLinksInText = false
- * @returns {string}
- */
-export function plainToHtml(plain) {
- plain = plain.toString().replace(/\r/g, '');
- plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
-
- let bIn = false,
- bDo = true,
- bStart = true,
- aNextText = [],
- aText = plain.split('\n');
-
- do {
- bDo = false;
- aNextText = [];
- aText.forEach(sLine => {
- bStart = '>' === sLine.substr(0, 1);
- if (bStart && !bIn) {
- bDo = true;
- bIn = true;
- aNextText.push('~~~blockquote~~~');
- aNextText.push(sLine.substr(1));
- } else if (!bStart && bIn) {
- if (sLine) {
- bIn = false;
- aNextText.push('~~~/blockquote~~~');
- aNextText.push(sLine);
- } else {
- aNextText.push(sLine);
- }
- } else if (bStart && bIn) {
- aNextText.push(sLine.substr(1));
- } else {
- aNextText.push(sLine);
- }
- });
-
- if (bIn) {
- bIn = false;
- aNextText.push('~~~/blockquote~~~');
- }
-
- aText = aNextText;
- } while (bDo);
-
- return aText.join('\n')
- // .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
- .replace(/&/g, '&')
- .replace(/>/g, '>')
- .replace(/')
- .replace(/[\s]*~~~\/blockquote~~~/g, ' ')
- .replace(/\n/g, '
');
-}
-
-rl.Utils = {
- htmlToPlain: htmlToPlain,
- plainToHtml: plainToHtml
-};
-
-/**
- * @param {Array} aSystem
- * @param {Array} aList
- * @param {Array=} aDisabled
- * @param {Array=} aHeaderLines
- * @param {Function=} fDisableCallback
- * @param {Function=} fRenameCallback
- * @param {boolean=} bSystem
- * @param {boolean=} bBuildUnvisible
- * @returns {Array}
- */
-export function folderListOptionsBuilder(
- aSystem,
- aList,
- aDisabled,
- aHeaderLines,
- fDisableCallback,
- fRenameCallback,
- bSystem,
- bBuildUnvisible
-) {
- let /**
- * @type {?FolderModel}
- */
- bSep = false,
- aResult = [];
-
- const sDeepPrefix = '\u00A0\u00A0\u00A0';
-
- bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
- bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
- fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
- fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
-
- if (!isArray(aDisabled)) {
- aDisabled = [];
- }
-
- if (!isArray(aHeaderLines)) {
- aHeaderLines = [];
- }
-
- aHeaderLines.forEach(line => {
- aResult.push({
- id: line[0],
- name: line[1],
- system: false,
- dividerbar: false,
- disabled: false
- });
- });
-
- bSep = true;
- aSystem.forEach(oItem => {
- aResult.push({
- id: oItem.fullNameRaw,
- name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
- system: true,
- dividerbar: bSep,
- disabled:
- !oItem.selectable ||
- aDisabled.includes(oItem.fullNameRaw) ||
- (fDisableCallback ? fDisableCallback(oItem) : false)
- });
- bSep = false;
- });
-
- bSep = true;
- aList.forEach(oItem => {
- // if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
- if (
- (oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
- (oItem.selectable || oItem.hasSubscribedSubfolders())
- ) {
- if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
- aResult.push({
- id: oItem.fullNameRaw,
- name:
- sDeepPrefix.repeat(oItem.deep + 1) +
- (fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
- system: false,
- dividerbar: bSep,
- disabled:
- !oItem.selectable ||
- aDisabled.includes(oItem.fullNameRaw) ||
- (fDisableCallback ? fDisableCallback(oItem) : false)
- });
- bSep = false;
- }
- }
-
- if (oItem.subscribed() && oItem.subFolders.length) {
- aResult = aResult.concat(
- folderListOptionsBuilder(
- [],
- oItem.subFolders(),
- aDisabled,
- [],
- fDisableCallback,
- fRenameCallback,
- bSystem,
- bBuildUnvisible
- )
- );
- }
- });
-
- return aResult;
-}
-
-/**
- * Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
- * @param {Object|Array} objectOrObjects
- * @returns {void}
- */
-export function delegateRunOnDestroy(objectOrObjects) {
- objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
- obj => obj.onDestroy && obj.onDestroy()
- );
-}
+},
/**
* @returns {function}
*/
-export function computedPaginatorHelper(koCurrentPage, koPageCount) {
+computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return () => {
const currentPage = koCurrentPage(),
pageCount = koPageCount(),
@@ -395,13 +100,13 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
if (3 === prev) {
fAdd(2, false);
} else if (3 < prev) {
- fAdd(Math.round((prev - 1) / 2), false, '...');
+ fAdd(Math.round((prev - 1) / 2), false, '…');
}
if (pageCount - 2 === next) {
fAdd(pageCount - 1, true);
} else if (pageCount - 2 > next) {
- fAdd(Math.round((pageCount + next) / 2), true, '...');
+ fAdd(Math.round((pageCount + next) / 2), true, '…');
}
// first and last
@@ -416,81 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
return result;
};
-}
+},
/**
* @param {string} mailToUrl
* @returns {boolean}
*/
-export function mailToHelper(mailToUrl) {
- if (
- mailToUrl &&
- 'mailto:' ===
- mailToUrl
- .toString()
- .substr(0, 7)
- .toLowerCase()
- ) {
- mailToUrl = mailToUrl.toString().substr(7);
+mailToHelper = mailToUrl => {
+ if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
+ mailToUrl = mailToUrl.slice(7).split('?');
- let to = [],
- cc = null,
- bcc = null,
- params = {};
-
- const email = mailToUrl.replace(/\?.+$/, ''),
- query = mailToUrl.replace(/^[^?]*\?/, '');
-
- query.split('&').forEach(temp => {
- temp = temp.split('=');
- params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
- });
-
- if (undefined !== params.to) {
- to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
- to = Object.values(
- to.reduce((result, value) => {
- if (value) {
- if (result[value.email]) {
- if (!result[value.email].name) {
- result[value.email] = value;
- }
- } else {
- result[value.email] = value;
- }
- }
- return result;
- }, {})
- );
- } else {
- to = EmailModel.parseEmailLine(email);
- }
-
- if (undefined !== params.cc) {
- cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
- }
-
- if (undefined !== params.bcc) {
- bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
- }
+ const
+ email = mailToUrl[0],
+ params = new URLSearchParams(mailToUrl[1]),
+ toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
showMessageComposer([
ComposeType.Empty,
null,
- to,
- cc,
- bcc,
- null == params.subject ? null : decodeURIComponent(params.subject),
- null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
+ params.get('to')
+ ? Object.values(
+ toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
+ if (value) {
+ if (result[value.email]) {
+ if (!result[value.email].name) {
+ result[value.email] = value;
+ }
+ } else {
+ result[value.email] = value;
+ }
+ }
+ return result;
+ }, {})
+ )
+ : EmailModel.parseEmailLine(email),
+ toEmailModel(params.get('cc')),
+ toEmailModel(params.get('bcc')),
+ params.get('subject'),
+ plainToHtml(params.get('body') || '')
]);
return true;
}
return false;
-}
+},
-export function showMessageComposer(params = [])
+showMessageComposer = (params = []) =>
{
rl.app.showMessageComposer(params);
-}
+},
+
+initFullscreen = (el, fn) =>
+{
+ let event = 'fullscreenchange';
+ if (!el.requestFullscreen && el.webkitRequestFullscreen) {
+ el.requestFullscreen = el.webkitRequestFullscreen;
+ event = 'webkit'+event;
+ }
+ if (el.requestFullscreen) {
+ el.addEventListener(event, fn);
+ return el;
+ }
+},
+
+setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
+{
+ if (source.layoutResizer && source.layoutResizer.mode != mode) {
+ target.removeAttribute('style');
+ source.removeAttribute('style');
+ }
+// source.classList.toggle('resizable', mode);
+ if (mode) {
+ const length = Local.get(sClientSideKeyName+mode);
+ if (!source.layoutResizer) {
+ const resizer = createElement('div', {'class':'resizer'}),
+ size = {},
+ store = () => {
+ if ('Width' == resizer.mode) {
+ target.style.left = source.offsetWidth + 'px';
+ Local.set(resizer.key+resizer.mode, source.offsetWidth);
+ } else {
+ target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
+ Local.set(resizer.key+resizer.mode, source.offsetHeight);
+ }
+ },
+ cssint = s => {
+ let value = getComputedStyle(source, null)[s].replace('px', '');
+ if (value.includes('%')) {
+ value = source.parentElement['offset'+resizer.mode]
+ * value.replace('%', '') / 100;
+ }
+ return parseFloat(value);
+ };
+ source.layoutResizer = resizer;
+ source.append(resizer);
+ resizer.addEventListener('mousedown', {
+ handleEvent: function(e) {
+ if ('mousedown' == e.type) {
+ const lmode = resizer.mode.toLowerCase();
+ e.preventDefault();
+ size.pos = ('width' == lmode) ? e.pageX : e.pageY;
+ size.min = cssint('min-'+lmode);
+ size.max = cssint('max-'+lmode);
+ size.org = cssint(lmode);
+ addEventListener('mousemove', this);
+ addEventListener('mouseup', this);
+ } else if ('mousemove' == e.type) {
+ const lmode = resizer.mode.toLowerCase(),
+ length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
+ if (length >= size.min && length <= size.max ) {
+ source.style[lmode] = length + 'px';
+ source.observer || store();
+ }
+ } else if ('mouseup' == e.type) {
+ removeEventListener('mousemove', this);
+ removeEventListener('mouseup', this);
+ }
+ }
+ });
+ source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
+ }
+ source.layoutResizer.mode = mode;
+ source.layoutResizer.key = sClientSideKeyName;
+ source.observer && source.observer.observe(source, { box: 'border-box' });
+ if (length) {
+ source.style[mode] = length + 'px';
+ }
+ } else {
+ source.observer && source.observer.disconnect();
+ }
+},
+
+populateMessageBody = (oMessage, preload) => {
+ if (oMessage) {
+ preload || MessageUserStore.hideMessageBodies();
+ preload || MessageUserStore.loading(true);
+ Remote.message((iError, oData/*, bCached*/) => {
+ if (iError) {
+ if (Notification.RequestAborted !== iError && !preload) {
+ MessageUserStore.message(null);
+ MessageUserStore.error(getNotification(iError));
+ }
+ } else {
+ oMessage = preload ? oMessage : null;
+ let
+ isNew = false,
+ json = oData && oData.Result,
+ message = oMessage || MessageUserStore.message();
+
+ if (
+ json &&
+ MessageModel.validJson(json) &&
+ message &&
+ message.folder === json.Folder
+ ) {
+ const threads = message.threads(),
+ messagesDom = MessageUserStore.bodiesDom();
+ if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
+ message = MessageModel.reviveFromJson(json);
+ if (message) {
+ message.threads(threads);
+ MessageFlagsCache.initMessage(message);
+
+ // Set clone
+ MessageUserStore.message(MessageModel.fromMessageListItem(message));
+ message = MessageUserStore.message();
+
+ isNew = true;
+ }
+ }
+
+ if (message && message.uid == json.Uid) {
+ oMessage || MessageUserStore.error('');
+/*
+ if (bCached) {
+ delete json.Flags;
+ }
+*/
+ isNew || message.revivePropertiesFromJson(json);
+ addRequestedMessage(message.folder, message.uid);
+ if (messagesDom) {
+ let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
+ body = elementById(id);
+ if (body) {
+ message.body = body;
+ message.isHtml(body.classList.contains('html'));
+ message.hasImages(body.rlHasImages);
+ } else {
+ body = Element.fromHTML('
'
+ + '
');
+ message.body = body;
+ if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
+ message.viewPlain();
+ }
+
+ MessageUserStore.purgeMessageBodyCache();
+ }
+
+ messagesDom.append(body);
+
+ if (!oMessage) {
+ MessageUserStore.activeDom(message.body);
+ MessageUserStore.hideMessageBodies();
+ message.body.hidden = false;
+ }
+ oMessage && message.viewPopupMessage();
+ }
+
+ MessageFlagsCache.initMessage(message);
+ if (message.isUnseen()) {
+ MessageUserStore.MessageSeenTimer = setTimeout(
+ () => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
+ SettingsUserStore.messageReadDelay() * 1000 // seconds
+ );
+ }
+
+ if (message && isNew) {
+ let selectedMessage = MessagelistUserStore.selectedMessage();
+ if (
+ selectedMessage &&
+ (message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
+ ) {
+ MessagelistUserStore.selectedMessage(null);
+ if (1 === MessagelistUserStore.length) {
+ MessagelistUserStore.focusedMessage(null);
+ }
+ } else if (!selectedMessage) {
+ selectedMessage = MessagelistUserStore.find(
+ subMessage =>
+ subMessage &&
+ subMessage.folder === message.folder &&
+ subMessage.uid == message.uid
+ );
+
+ if (selectedMessage) {
+ MessagelistUserStore.selectedMessage(selectedMessage);
+ MessagelistUserStore.focusedMessage(selectedMessage);
+ }
+ }
+ }
+ }
+ }
+ }
+ preload || MessageUserStore.loading(false);
+ }, oMessage.folder, oMessage.uid);
+ }
+};
diff --git a/dev/Component/Abstract.js b/dev/Component/Abstract.js
deleted file mode 100644
index 848414571..000000000
--- a/dev/Component/Abstract.js
+++ /dev/null
@@ -1,14 +0,0 @@
-
-export class AbstractComponent {
- constructor() {
- this.disposable = [];
- }
-
- dispose() {
- this.disposable.forEach((funcToDispose) => {
- if (funcToDispose && funcToDispose.dispose) {
- funcToDispose.dispose();
- }
- });
- }
-}
diff --git a/dev/Component/AbstractCheckbox.js b/dev/Component/AbstractCheckbox.js
index e84824299..812da3108 100644
--- a/dev/Component/AbstractCheckbox.js
+++ b/dev/Component/AbstractCheckbox.js
@@ -1,27 +1,15 @@
import ko from 'ko';
-import { AbstractComponent } from 'Component/Abstract';
-class AbstractCheckbox extends AbstractComponent {
+export class AbstractCheckbox {
/**
* @param {Object} params = {}
*/
constructor(params = {}) {
- super();
+ this.value = ko.isObservable(params.value) ? params.value
+ : ko.observable(!!params.value);
- this.value = params.value;
- if (undefined === this.value || !this.value.subscribe) {
- this.value = ko.observable(!!this.value);
- }
-
- this.enable = params.enable;
- if (undefined === this.enable || !this.enable.subscribe) {
- this.enable = ko.observable(undefined === this.enable || !!this.enable);
- }
-
- this.disable = params.disable;
- if (undefined === this.disable || !this.disable.subscribe) {
- this.disable = ko.observable(!!this.disable);
- }
+ this.enable = ko.isObservable(params.enable) ? params.enable
+ : ko.observable(undefined === params.enable || !!params.enable);
this.label = params.label || '';
this.inline = !!params.inline;
@@ -30,10 +18,6 @@ class AbstractCheckbox extends AbstractComponent {
}
click() {
- if (this.enable() && !this.disable()) {
- this.value(!this.value());
- }
+ this.enable() && this.value(!this.value());
}
}
-
-export { AbstractCheckbox };
diff --git a/dev/Component/AbstractInput.js b/dev/Component/AbstractInput.js
index 57ac7aa16..63a8830db 100644
--- a/dev/Component/AbstractInput.js
+++ b/dev/Component/AbstractInput.js
@@ -1,61 +1,57 @@
import ko from 'ko';
import { pInt } from 'Common/Utils';
import { SaveSettingsStep } from 'Common/Enums';
-import { AbstractComponent } from 'Component/Abstract';
+import { koComputable } from 'External/ko';
+import { dispose } from 'External/ko';
-class AbstractInput extends AbstractComponent {
+export class AbstractInput {
/**
* @param {Object} params
*/
constructor(params) {
- super();
-
this.value = params.value || '';
- this.size = params.size || 0;
this.label = params.label || '';
- this.preLabel = params.preLabel || '';
- this.enable = undefined === params.enable ? true : params.enable;
+ this.enable = null == params.enable ? true : params.enable;
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
this.placeholder = params.placeholder || '';
- this.labeled = undefined !== params.label;
- this.preLabeled = undefined !== params.preLabel;
- this.triggered = undefined !== params.trigger && !!this.trigger;
-
- this.classForTrigger = ko.observable('');
-
- this.className = ko.computed(() => {
- const size = ko.unwrap(this.size),
- suffixValue = this.trigger ? ' ' + ('settings-saved-trigger-input ' + this.classForTrigger()).trim() : '';
- return (0 < size ? 'span' + size : '') + suffixValue;
- });
-
- if (undefined !== params.width && params.element) {
- params.element.querySelectorAll('input,select,textarea').forEach(node => node.style.width = params.width);
- }
-
- this.disposable.push(this.className);
+ this.labeled = null != params.label;
+ let size = 0 < params.size ? 'span' + params.size : '';
if (this.trigger) {
- this.setTriggerState(this.trigger());
+ const
+ classForTrigger = ko.observable(''),
+ setTriggerState = value => {
+ switch (pInt(value)) {
+ case SaveSettingsStep.TrueResult:
+ classForTrigger('success');
+ break;
+ case SaveSettingsStep.FalseResult:
+ classForTrigger('error');
+ break;
+ default:
+ classForTrigger('');
+ break;
+ }
+ };
- this.disposable.push(this.trigger.subscribe(this.setTriggerState, this));
+ setTriggerState(this.trigger());
+
+ this.className = koComputable(() =>
+ (size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
+ );
+
+ this.disposables = [
+ this.trigger.subscribe(setTriggerState, this),
+ this.className
+ ];
+ } else {
+ this.className = size;
+ this.disposables = [];
}
}
- setTriggerState(value) {
- switch (pInt(value)) {
- case SaveSettingsStep.TrueResult:
- this.classForTrigger('success');
- break;
- case SaveSettingsStep.FalseResult:
- this.classForTrigger('error');
- break;
- default:
- this.classForTrigger('');
- break;
- }
+ dispose() {
+ this.disposables.forEach(dispose);
}
}
-
-export { AbstractInput };
diff --git a/dev/Component/EmailAddresses.js b/dev/Component/EmailAddresses.js
index ac7813361..f0a9e1244 100644
--- a/dev/Component/EmailAddresses.js
+++ b/dev/Component/EmailAddresses.js
@@ -1,5 +1,4 @@
-import { doc, createElement } from 'Common/Globals';
-import { isArray } from 'Common/Utils';
+import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email';
const contentType = 'snappymail/emailaddress',
@@ -17,7 +16,7 @@ export class EmailAddressesComponent {
doc.body.append(datalist);
}
- var self = this,
+ const self = this,
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
@@ -42,49 +41,55 @@ export class EmailAddressesComponent {
// Create the elements
self.ul = createElement('ul',{class:"emailaddresses"});
- self.ul.addEventListener('click', e => self._focus(e));
- self.ul.addEventListener('dblclick', e => self._editTag(e));
- self.ul.addEventListener("dragenter", fnDrag);
- self.ul.addEventListener("dragover", fnDrag);
- self.ul.addEventListener("drop", e => {
- if (validDropzone() && dragAddress.value) {
- e.preventDefault();
- dragAddress.source._removeDraggedTag(dragAddress.li);
- self._parseValue(dragAddress.value);
+ addEventsListeners(self.ul, {
+ click: e => self._focus(e),
+ dblclick: e => self._editTag(e),
+ dragenter: fnDrag,
+ dragover: fnDrag,
+ drop: e => {
+ if (validDropzone() && dragAddress.value) {
+ e.preventDefault();
+ dragAddress.source._removeDraggedTag(dragAddress.li);
+ self._parseValue(dragAddress.value);
+ }
}
});
self.input = createElement('input',{type:"text", list:datalist.id,
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
- self.input.addEventListener('focus', () => self._focusTrigger(true));
- self.input.addEventListener('blur', () => {
- // prevent autoComplete menu click from causing a false 'blur'
- self._parseInput(true);
- self._focusTrigger(false);
- });
- self.input.addEventListener('keydown', e => {
- if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
- // if our input contains no value and backspace has been pressed, select the last tag
- var lastTag = self.inputCont.previousElementSibling,
- input = self.input;
- if (lastTag && (!input.value
- || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
- ) {
- e.preventDefault();
- lastTag.querySelector('a').focus();
- }
- self._updateDatalist();
- } else if (e.key == 'Enter') {
- e.preventDefault();
+ addEventsListeners(self.input, {
+ focus: () => {
+ self._focusTrigger(true);
+ self.input.value || self._resetDatalist();
+ },
+ blur: () => {
+ // prevent autoComplete menu click from causing a false 'blur'
self._parseInput(true);
+ self._focusTrigger(false);
+ },
+ keydown: e => {
+ if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
+ // if our input contains no value and backspace has been pressed, select the last tag
+ var lastTag = self.inputCont.previousElementSibling,
+ input = self.input;
+ if (lastTag && (!input.value
+ || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
+ ) {
+ e.preventDefault();
+ lastTag.querySelector('a').focus();
+ }
+ self._updateDatalist();
+ } else if (e.key == 'Enter') {
+ e.preventDefault();
+ self._parseInput(true);
+ }
+ },
+ input: () => {
+ self._parseInput();
+ self._updateDatalist();
}
});
- self.input.addEventListener('input', () => {
- self._parseInput();
- self._updateDatalist();
- });
- self.input.addEventListener('focus', () => self.input.value || self._resetDatalist());
// define starting placeholder
if (element.placeholder) {
@@ -116,7 +121,7 @@ export class EmailAddressesComponent {
)
}
}).throttle(500)
- : () => {};
+ : () => 0;
}
_focusTrigger(bValue) {
@@ -138,20 +143,56 @@ export class EmailAddressesComponent {
_parseValue(val) {
if (val) {
- var self = this,
- values = [];
-
- const v = val.trim(),
- hook = (v && [',', ';', '\n'].includes(v.substr(-1)))
- ? EmailModel.splitEmailLine(val)
- : null;
-
- values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
- .flat(Infinity)
- .map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
+ const self = this,
+ v = val.trim(),
+ hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
+ values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
+ .flat(Infinity)
+ .map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
if (values.length) {
- self._setChosen(values);
+ values.forEach(a => {
+ var v = a[0].trim(),
+ exists = false,
+ lastIndex = -1,
+ obj = {
+ key : '',
+ obj : null,
+ value : ''
+ };
+
+ self._chosenValues.forEach((vv, kk) => {
+ if (vv.value === self._lastEdit) {
+ lastIndex = kk;
+ }
+
+ vv.value === v && (exists = true);
+ });
+
+ if (v !== '' && a[1] && !exists) {
+
+ obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
+ obj.value = v;
+ obj.obj = a[1];
+
+ if (-1 < lastIndex) {
+ self._chosenValues.splice(lastIndex, 0, obj);
+ } else {
+ self._chosenValues.push(obj);
+ }
+
+ self._lastEdit = '';
+ self._renderTags();
+ }
+ });
+
+ if (values.length === 1 && values[0] === '' && self._lastEdit !== '') {
+ self._lastEdit = '';
+ self._renderTags();
+ }
+
+ self._setValue(self._buildValue());
+
return true;
}
}
@@ -202,56 +243,6 @@ export class EmailAddressesComponent {
self._resizeInput(ev);
}
- _setChosen(valArr) {
- var self = this;
-
- if (!isArray(valArr)){
- return false;
- }
-
- valArr.forEach(a => {
- var v = a[0].trim(),
- exists = false,
- lastIndex = -1,
- obj = {
- key : '',
- obj : null,
- value : ''
- };
-
- self._chosenValues.forEach((vv, kk) => {
- if (vv.value === self._lastEdit) {
- lastIndex = kk;
- }
-
- vv.value === v && (exists = true);
- });
-
- if (v !== '' && a && a[1] && !exists) {
-
- obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
- obj.value = v;
- obj.obj = a[1];
-
- if (-1 < lastIndex) {
- self._chosenValues.splice(lastIndex, 0, obj);
- } else {
- self._chosenValues.push(obj);
- }
-
- self._lastEdit = '';
- self._renderTags();
- }
- });
-
- if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') {
- self._lastEdit = '';
- self._renderTags();
- }
-
- self._setValue(self._buildValue());
- }
-
_buildValue() {
return this._chosenValues.map(v => v.value).join(',');
}
@@ -276,66 +267,70 @@ export class EmailAddressesComponent {
el = createElement('a',{href:'#', class:'ficon'});
el.append('✖');
- el.addEventListener('click', e => self._removeTag(e, li));
- el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
- el.addEventListener('blur', () => li.className = null);
- el.addEventListener('keydown', e => {
- switch (e.key) {
- case 'Delete':
- case 'Backspace':
- self._removeTag(e, li);
- break;
+ addEventsListeners(el, {
+ click: e => self._removeTag(e, li),
+ focus: () => li.className = 'emailaddresses-selected',
+ blur: () => li.className = null,
+ keydown: e => {
+ switch (e.key) {
+ case 'Delete':
+ case 'Backspace':
+ self._removeTag(e, li);
+ break;
- // 'e' - edit tag (removes tag and places value into visible input
- case 'e':
- case 'Enter':
- self._editTag(e);
- break;
+ // 'e' - edit tag (removes tag and places value into visible input
+ case 'e':
+ case 'Enter':
+ self._editTag(e);
+ break;
- case 'ArrowLeft':
- // select the previous tag or input if no more tags exist
- var previous = el.closest('li').previousElementSibling;
- if (previous.matches('li')) {
- previous.querySelector('a').focus();
- } else {
- self.focus();
- }
- break;
+ case 'ArrowLeft':
+ // select the previous tag or input if no more tags exist
+ var previous = el.closest('li').previousElementSibling;
+ if (previous.matches('li')) {
+ previous.querySelector('a').focus();
+ } else {
+ self.focus();
+ }
+ break;
- case 'ArrowRight':
- // select the next tag or input if no more tags exist
- var next = el.closest('li').nextElementSibling;
- if (next !== this.inputCont) {
- next.querySelector('a').focus();
- } else {
- this.focus();
- }
- break;
+ case 'ArrowRight':
+ // select the next tag or input if no more tags exist
+ var next = el.closest('li').nextElementSibling;
+ if (next !== this.inputCont) {
+ next.querySelector('a').focus();
+ } else {
+ this.focus();
+ }
+ break;
- case 'ArrowDown':
- self._focus(e);
- break;
+ case 'ArrowDown':
+ self._focus(e);
+ break;
+ }
}
});
li.append(el);
li.emailaddress = v;
- li.addEventListener("dragstart", e => {
- dragAddress = {
- source: self,
- li: li,
- value: li.emailaddress.obj.toLine()
- };
-// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
- e.dataTransfer.setData('text/plain', contentType);
-// e.dataTransfer.setDragImage(li, 0, 0);
- e.dataTransfer.effectAllowed = 'move';
- li.style.opacity = 0.25;
- });
- li.addEventListener("dragend", () => {
- dragAddress = null;
- li.style.cssText = '';
+ addEventsListeners(li, {
+ dragstart: e => {
+ dragAddress = {
+ source: self,
+ li: li,
+ value: li.emailaddress.obj.toLine()
+ };
+// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
+ e.dataTransfer.setData('text/plain', contentType);
+// e.dataTransfer.setDragImage(li, 0, 0);
+ e.dataTransfer.effectAllowed = 'move';
+ li.style.opacity = 0.25;
+ },
+ dragend: () => {
+ dragAddress = null;
+ li.style.cssText = '';
+ }
});
self.inputCont.before(li);
diff --git a/dev/Component/MaterialDesign/Checkbox.js b/dev/Component/MaterialDesign/Checkbox.js
index 547fb1b69..48cdcd72d 100644
--- a/dev/Component/MaterialDesign/Checkbox.js
+++ b/dev/Component/MaterialDesign/Checkbox.js
@@ -1,41 +1,3 @@
-import ko from 'ko';
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
-export class CheckboxMaterialDesignComponent extends AbstractCheckbox {
- /**
- * @param {Object} params
- */
- constructor(params) {
- super(params);
-
- this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
- this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
-
- this.animationBoxSetTrue = this.animationBoxSetTrue.bind(this);
- this.animationCheckmarkSetTrue = this.animationCheckmarkSetTrue.bind(this);
-
- this.disposable.push(
- this.value.subscribe((value) => {
- this.triggerAnimation(value);
- }, this)
- );
- }
-
- animationBoxSetTrue() {
- this.animationBox(true);
- }
-
- animationCheckmarkSetTrue() {
- this.animationCheckmark(true);
- }
-
- triggerAnimation(box) {
- if (box) {
- this.animationBoxSetTrue();
- setTimeout(this.animationCheckmarkSetTrue, 200);
- } else {
- this.animationCheckmarkSetTrue();
- setTimeout(this.animationBoxSetTrue, 200);
- }
- }
-}
+export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}
diff --git a/dev/Component/Select.js b/dev/Component/Select.js
index b7390f7ba..482138e08 100644
--- a/dev/Component/Select.js
+++ b/dev/Component/Select.js
@@ -13,11 +13,7 @@ export class SelectComponent extends AbstractInput {
this.optionsText = params.optionsText || null;
this.optionsValue = params.optionsValue || null;
- this.optionsCaption = params.optionsCaption || null;
-
- if (this.optionsCaption) {
- this.optionsCaption = i18n(this.optionsCaption);
- }
+ this.optionsCaption = i18n(params.optionsCaption || null);
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
}
diff --git a/dev/External/Admin/ko.js b/dev/External/Admin/ko.js
deleted file mode 100644
index e1dd98910..000000000
--- a/dev/External/Admin/ko.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import 'External/ko';
-import ko from 'ko';
-import { SaveSettingsStep } from 'Common/Enums';
-
-// functions
-
-ko.observable.fn.idleTrigger = function() {
- this.trigger = ko.observable(SaveSettingsStep.Idle);
- return this;
-};
diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js
index c81a12927..b4e5efefb 100644
--- a/dev/External/SquireUI.js
+++ b/dev/External/SquireUI.js
@@ -9,13 +9,16 @@ const
i18n = (str, def) => rl.i18n(str) || def,
- ctrlKey = shortcuts.getMetaKey().replace('meta','⌘') + ' + ',
+ ctrlKey = shortcuts.getMetaKey() + ' + ',
- tpl = doc.createElement('template'),
- clr = doc.createElement('input'),
+ createElement = name => doc.createElement(name),
+
+ tpl = createElement('template'),
+ clr = createElement('input'),
trimLines = html => html.trim().replace(/^(
\s*
\s*<\/div>)+/, '').trim(),
- clearHtmlLine = html => rl.Utils.htmlToPlain(html).trim(),
+ htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
+ plainToHtml = text => rl.Utils.plainToHtml(text),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
@@ -33,13 +36,12 @@ const
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
- tpl.innerHTML = html
+ tpl.innerHTML = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(//g,'')
.replace(/
]*>\s*<\/span>/gi,'')
.trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
- tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
if (isPaste) {
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
tpl.querySelectorAll('*').forEach(el => {
@@ -57,66 +59,6 @@ const
}
return tpl.content;
}
- },
-
- rl_signature_replacer = (editor, text, signature, isHtml, insertBefore) => {
- let
- prevSignature = editor.__previous_signature,
- skipInsert = false,
- isEmptyText = false;
-
- isEmptyText = !text.trim();
- if (!isEmptyText && isHtml) {
- isEmptyText = !clearHtmlLine(text);
- }
-
- if (prevSignature && !isEmptyText) {
- if (isHtml && !prevSignature.isHtml) {
- prevSignature = {
- body: rl.Utils.plainToHtml(prevSignature.body),
- isHtml: true
- };
- } else if (!isHtml && prevSignature.isHtml) {
- prevSignature = {
- body: rl.Utils.htmlToPlain(prevSignature.body),
- isHtml: true
- };
- }
-
- if (isHtml) {
- var clearSig = clearHtmlLine(prevSignature.body);
- text = text.replace(/([\s\S]*)<\/signature>/igm, all => {
- var c = clearSig === clearHtmlLine(all);
- if (!c) {
- skipInsert = true;
- }
- return c ? '' : all;
- });
- } else {
- var textLen = text.length;
- text = text
- .replace(prevSignature.body, '')
- .replace(prevSignature.body, '');
- skipInsert = textLen === text.length;
- }
- }
-
- if (!skipInsert) {
- signature = (isHtml ? '' : "\n\n") + signature + (isHtml ? ' ' : '');
-
- text = insertBefore ? signature + text : text + signature;
-
- if (10 < signature.length) {
- prevSignature = {
- body: signature,
- isHtml: isHtml
- };
- }
- }
-
- editor.__previous_signature = prevSignature;
-
- return text;
};
clr.type = "color";
@@ -127,9 +69,9 @@ class SquireUI
{
constructor(container) {
const
- doClr = fn => () => {
+ doClr = name => () => {
clr.value = '';
- clr.onchange = () => squire[fn](clr.value);
+ clr.onchange = () => squire.setStyle({[name]:clr.value});
clr.click();
},
@@ -171,29 +113,15 @@ class SquireUI
colors: {
textColor: {
html: 'A▾ ',
- cmd: doClr('setTextColor'),
+ cmd: doClr('color'),
hint: 'Text color'
},
backgroundColor: {
html: '🎨', /* ▧ */
- cmd: doClr('setBackgroundColor'),
+ cmd: doClr('backgroundColor'),
hint: 'Background color'
},
},
-/*
- bidi: {
- bdoLtr: {
- html: '𝐁',
- cmd: () => this.doAction('bold','B'),
- hint: 'Bold'
- },
- bdoRtl: {
- html: '𝐁',
- cmd: () => this.doAction('bold','B'),
- hint: 'Bold'
- }
- },
-*/
inline: {
bold: {
html: 'B',
@@ -318,10 +246,10 @@ class SquireUI
}
},
- plain = doc.createElement('textarea'),
- wysiwyg = doc.createElement('div'),
- toolbar = doc.createElement('div'),
- browseImage = doc.createElement('input'),
+ plain = createElement('textarea'),
+ wysiwyg = createElement('div'),
+ toolbar = createElement('div'),
+ browseImage = createElement('input'),
squire = new Squire(wysiwyg, SquireDefaultConfig);
browseImage.type = 'file';
@@ -336,7 +264,7 @@ class SquireUI
}
plain.className = 'squire-plain';
- wysiwyg.className = 'squire-wysiwyg cke_editable';
+ wysiwyg.className = 'squire-wysiwyg';
this.mode = ''; // 'plain' | 'wysiwyg'
this.__plain = {
getRawData: () => this.plain.value,
@@ -349,25 +277,24 @@ class SquireUI
this.wysiwyg = wysiwyg;
toolbar.className = 'squire-toolbar btn-toolbar';
- let touchTap;
- for (let group in actions) {
+ let group, action, touchTap;
+ for (group in actions) {
+/*
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
continue;
}
- let toolgroup = doc.createElement('div');
+*/
+ let toolgroup = createElement('div');
toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group;
- for (let action in actions[group]) {
- if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) {
- continue;
- }
+ for (action in actions[group]) {
let cfg = actions[group][action], input, ev = 'click';
if (cfg.input) {
- input = doc.createElement('input');
+ input = createElement('input');
input.type = cfg.input;
ev = 'change';
} else if (cfg.select) {
- input = doc.createElement('select');
+ input = createElement('select');
input.className = 'btn';
if (Array.isArray(cfg.select)) {
cfg.select.forEach(value => {
@@ -377,7 +304,7 @@ class SquireUI
});
} else {
Object.entries(cfg.select).forEach(([label, options]) => {
- let group = doc.createElement('optgroup');
+ let group = createElement('optgroup');
group.label = label;
Object.entries(options).forEach(([text, value]) => {
var option = new Option(text, value);
@@ -389,7 +316,7 @@ class SquireUI
}
ev = 'input';
} else {
- input = doc.createElement('button');
+ input = createElement('button');
input.type = 'button';
input.className = 'btn';
input.innerHTML = cfg.html;
@@ -412,6 +339,7 @@ class SquireUI
input.title = ctrlKey + cfg.key;
}
input.dataset.action = action;
+ input.tabIndex = -1;
cfg.input = input;
toolgroup.append(input);
}
@@ -425,8 +353,8 @@ class SquireUI
changes.redo.input.disabled = !state.canRedo;
});
- squire.addEventListener('focus', () => shortcuts.off());
- squire.addEventListener('blur', () => shortcuts.on());
+// squire.addEventListener('focus', () => shortcuts.off());
+// squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain);
@@ -477,9 +405,9 @@ class SquireUI
let cl = this.container.classList;
cl.remove('squire-mode-'+this.mode);
if ('plain' == mode) {
- this.plain.value = rl.Utils.htmlToPlain(this.squire.getHTML(), true).trim();
+ this.plain.value = htmlToPlain(this.squire.getHTML(), true);
} else {
- this.setData(rl.Utils.plainToHtml(this.plain.value, true));
+ this.setData(plainToHtml(this.plain.value, true));
mode = 'wysiwyg';
}
this.mode = mode; // 'wysiwyg' or 'plain'
@@ -509,19 +437,32 @@ class SquireUI
}, cfg);
if (cfg.clearCache) {
- this.__previous_signature = null;
+ this._prev_txt_sig = null;
} else try {
+ const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
if ('plain' === this.mode) {
- if (cfg.isHtml) {
- cfg.signature = rl.Utils.htmlToPlain(cfg.signature);
+ let
+ text = this.plain.value,
+ prevSignature = this._prev_txt_sig;
+ if (prevSignature) {
+ text = text.replace(prevSignature, '').trim();
}
- this.plain.value = rl_signature_replacer(this, this.plain.value, cfg.signature, false, cfg.insertBefore);
+ this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
} else {
- if (!cfg.isHtml) {
- cfg.signature = rl.Utils.plainToHtml(cfg.signature);
- }
- this.setData(rl_signature_replacer(this, this.getData(), cfg.signature, true, cfg.insertBefore));
+ const squire = this.squire,
+ root = squire.getRoot(),
+ range = squire.getSelection(),
+ div = createElement('div');
+ div.className = 'rl-signature';
+ div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
+ root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
+ cfg.insertBefore ? root.prepend(div) : root.append(div);
+ // Move cursor above signature
+ range.setStart(div, 0);
+ range.setEnd(div, 0);
+ squire.setSelection( range );
}
+ this._prev_txt_sig = signature;
} catch (e) {
console.error(e);
}
@@ -540,12 +481,6 @@ class SquireUI
focus() {
('plain' == this.mode ? this.plain : this.squire).focus();
}
-
- resize(width, height) {
- height = Math.max(200, (height - this.wysiwyg.offsetTop)) + 'px';
- this.wysiwyg.style.height = height;
- this.plain.style.height = height;
- }
}
this.SquireUI = SquireUI;
diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js
index 97b98bb8c..ba91b414e 100644
--- a/dev/External/User/ko.js
+++ b/dev/External/User/ko.js
@@ -1,11 +1,14 @@
import 'External/ko';
import ko from 'ko';
import { HtmlEditor } from 'Common/Html';
-import { timeToNode } from 'Common/Momentor';
-import { elementById } from 'Common/Globals';
+import { timeToNode } from 'Common/Translator';
+import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
import { isArray } from 'Common/Utils';
+import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme';
+import { moveMessagesToFolder } from 'Common/Folders';
+import { setExpandedFolder } from 'Model/FolderCollection';
const rlContentType = 'snappymail/action',
@@ -27,233 +30,223 @@ const rlContentType = 'snappymail/action',
id: 0,
stop: () => clearTimeout(dragTimer.id),
start: fn => dragTimer.id = setTimeout(fn, 500)
- };
+ },
+
+ ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
let dragImage,
dragData;
-ko.bindingHandlers.editor = {
- init: (element, fValueAccessor) => {
- let editor = null;
+Object.assign(ko.bindingHandlers, {
- const fValue = fValueAccessor(),
- fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
- fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
- fOnReady = () => {
- fValue.__editor = editor;
- fUpdateEditorValue();
- };
+ editor: {
+ init: (element, fValueAccessor) => {
+ let editor = null;
- if (ko.isObservable(fValue) && HtmlEditor) {
- editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
+ const fValue = fValueAccessor(),
+ fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
+ fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
+ fOnReady = () => {
+ fValue.__editor = editor;
+ fUpdateEditorValue();
+ };
- fValue.__fetchEditorValue = fUpdateKoValue;
+ if (ko.isObservable(fValue) && HtmlEditor) {
+ editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
- fValue.subscribe(fUpdateEditorValue);
+ fValue.__fetchEditorValue = fUpdateKoValue;
- // ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
- // });
- }
- }
-};
+ fValue.subscribe(fUpdateEditorValue);
-let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
-ko.bindingHandlers.moment = {
- init: ttn,
- update: ttn
-};
-
-ko.bindingHandlers.emailsTags = {
- init: (element, fValueAccessor, fAllBindingsAccessor) => {
- const fValue = fValueAccessor(),
- fAllBindings = fAllBindingsAccessor();
-
- element.addresses = new EmailAddressesComponent(element, {
- focusCallback: value => fValue.focused && fValue.focused(!!value),
- autoCompleteSource: fAllBindings.autoCompleteSource || null,
- onChange: value => fValue(value)
- });
-
- if (fValue.focused && fValue.focused.subscribe) {
- fValue.focused.subscribe(value =>
- element.addresses[value ? 'focus' : 'blur']()
- );
+ // ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+ // });
+ }
}
},
- update: (element, fValueAccessor) => {
- element.addresses.value = ko.unwrap(fValueAccessor());
- }
-};
-// Start dragging selected messages
-ko.bindingHandlers.dragmessages = {
- init: (element, fValueAccessor) => {
- element.addEventListener("dragstart", e => {
- let data = fValueAccessor()(e);
- dragImage || (dragImage = elementById('messagesDragImage'));
- if (data && dragImage && !ThemeStore.isMobile()) {
- dragImage.querySelector('.text').textContent = data.uids.length;
- let img = dragImage.querySelector('i');
- img.classList.toggle('icon-copy', e.ctrlKey);
- img.classList.toggle('icon-mail', !e.ctrlKey);
+ moment: {
+ init: ttn,
+ update: ttn
+ },
- // Else Chrome doesn't show it
- dragImage.style.left = e.clientX + 'px';
- dragImage.style.top = e.clientY + 'px';
- dragImage.style.right = 'auto';
+ emailsTags: {
+ init: (element, fValueAccessor, fAllBindings) => {
+ const fValue = fValueAccessor();
- setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
+ element.addresses = new EmailAddressesComponent(element, {
+ focusCallback: value => fValue.focused && fValue.focused(!!value),
+ autoCompleteSource: fAllBindings.get('autoCompleteSource'),
+ onChange: value => fValue(value)
+ });
- // Remove the Chrome visibility
- dragImage.style.cssText = '';
- } else {
- e.preventDefault();
+ if (fValue.focused && fValue.focused.subscribe) {
+ fValue.focused.subscribe(value =>
+ element.addresses[value ? 'focus' : 'blur']()
+ );
}
+ },
+ update: (element, fValueAccessor) => {
+ element.addresses.value = ko.unwrap(fValueAccessor());
+ }
+ },
- }, false);
- element.addEventListener("dragend", () => dragData = null);
- element.setAttribute('draggable', true);
- }
-};
+ // Start dragging selected messages
+ dragmessages: {
+ init: (element, fValueAccessor) => {
+ element.addEventListener("dragstart", e => {
+ let data = fValueAccessor()(e);
+ dragImage || (dragImage = elementById('messagesDragImage'));
+ if (data && dragImage && !ThemeStore.isMobile()) {
+ dragImage.querySelector('.text').textContent = data.uids.length;
+ let img = dragImage.querySelector('i');
+ img.classList.toggle('icon-copy', e.ctrlKey);
+ img.classList.toggle('icon-mail', !e.ctrlKey);
-// Drop selected messages on folder
-ko.bindingHandlers.dropmessages = {
- init: (element, fValueAccessor) => {
- const folder = fValueAccessor(),
-// folder = ko.dataFor(element),
- fnStop = e => {
- e.preventDefault();
- element.classList.remove('droppableHover');
- dragTimer.stop();
- },
- fnHover = e => {
- if ('messages' === getDragAction(e)) {
- fnStop(e);
- element.classList.add('droppableHover');
- if (folder && folder.collapsed()) {
- dragTimer.start(() => {
- folder.collapsed(false);
- rl.app.setExpandedFolder(folder.fullNameHash, true);
- }, 500);
- }
- }
- };
- element.addEventListener("dragenter", fnHover);
- element.addEventListener("dragover", fnHover);
- element.addEventListener("dragleave", fnStop);
- element.addEventListener("drop", e => {
- fnStop(e);
- if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
- let data = dragData.data;
- if (folder && data && data.folder && isArray(data.uids)) {
- rl.app.moveMessagesToFolder(data.folder, data.uids, folder.fullNameRaw, data.copy && e.ctrlKey);
- }
- }
- });
- }
-};
+ // Else Chrome doesn't show it
+ dragImage.style.left = e.clientX + 'px';
+ dragImage.style.top = e.clientY + 'px';
+ dragImage.style.right = 'auto';
-ko.bindingHandlers.sortableItem = {
- init: (element, fValueAccessor) => {
- let options = ko.unwrap(fValueAccessor()) || {},
- parent = element.parentNode,
- fnHover = e => {
- if ('sortable' === getDragAction(e)) {
+ setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
+
+ // Remove the Chrome visibility
+ dragImage.style.cssText = '';
+ } else {
e.preventDefault();
- let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
- if (node && node !== dragData.data && parent.contains(node)) {
- let rect = node.getBoundingClientRect();
- if (rect.top + (rect.height / 2) <= e.clientY) {
- if (node.nextElementSibling !== dragData.data) {
- node.after(dragData.data);
- }
- } else if (node.previousElementSibling !== dragData.data) {
- node.before(dragData.data);
+ }
+
+ }, false);
+ element.addEventListener("dragend", () => dragData = null);
+ element.setAttribute('draggable', true);
+ }
+ },
+
+ // Drop selected messages on folder
+ dropmessages: {
+ init: (element, fValueAccessor) => {
+ const folder = fValueAccessor(),
+ // folder = ko.dataFor(element),
+ fnStop = e => {
+ e.preventDefault();
+ element.classList.remove('droppableHover');
+ dragTimer.stop();
+ },
+ fnHover = e => {
+ if ('messages' === getDragAction(e)) {
+ fnStop(e);
+ element.classList.add('droppableHover');
+ if (folder && folder.collapsed()) {
+ dragTimer.start(() => {
+ folder.collapsed(false);
+ setExpandedFolder(folder.fullName, true);
+ }, 500);
+ }
+ }
+ };
+ addEventsListeners(element, {
+ dragenter: fnHover,
+ dragover: fnHover,
+ dragleave: fnStop,
+ drop: e => {
+ fnStop(e);
+ if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
+ let data = dragData.data;
+ if (folder && data && data.folder && isArray(data.uids)) {
+ moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
}
}
}
- };
- element.addEventListener("dragstart", e => {
- dragData = {
- action: 'sortable',
- element: element
- };
- setDragAction(e, 'sortable', 'move', element, element);
- element.style.opacity = 0.25;
- });
- element.addEventListener("dragend", e => {
- element.style.opacity = null;
- if ('sortable' === getDragAction(e)) {
- dragData.data.style.cssText = '';
- let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
- if (row != dragData.data) {
- row.before(dragData.data);
- }
- dragData = null;
- }
- });
- if (!parent.sortable) {
- parent.sortable = true;
- parent.addEventListener("dragenter", fnHover);
- parent.addEventListener("dragover", fnHover);
- parent.addEventListener("drop", e => {
- if ('sortable' === getDragAction(e)) {
- e.preventDefault();
- let data = ko.dataFor(dragData.data),
- from = options.list.indexOf(data),
- to = [...parent.children].indexOf(dragData.data);
- if (from != to) {
- let arr = options.list();
- arr.splice(to, 0, ...arr.splice(from, 1));
- options.list(arr);
- }
- dragData = null;
- options.afterMove && options.afterMove();
- }
});
}
- }
-};
+ },
-ko.bindingHandlers.initDom = {
- init: (element, fValueAccessor) => fValueAccessor()(element)
-};
-
-ko.bindingHandlers.onEsc = {
- init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
- let fn = event => {
- if ('Escape' == event.key) {
- element.dispatchEvent(new Event('change'));
- fValueAccessor().call(viewModel);
+ sortableItem: {
+ init: (element, fValueAccessor) => {
+ let options = ko.unwrap(fValueAccessor()) || {},
+ parent = element.parentNode,
+ fnHover = e => {
+ if ('sortable' === getDragAction(e)) {
+ e.preventDefault();
+ let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
+ if (node && node !== dragData.data && parent.contains(node)) {
+ let rect = node.getBoundingClientRect();
+ if (rect.top + (rect.height / 2) <= e.clientY) {
+ if (node.nextElementSibling !== dragData.data) {
+ node.after(dragData.data);
+ }
+ } else if (node.previousElementSibling !== dragData.data) {
+ node.before(dragData.data);
+ }
+ }
+ }
+ };
+ addEventsListeners(element, {
+ dragstart: e => {
+ dragData = {
+ action: 'sortable',
+ element: element
+ };
+ setDragAction(e, 'sortable', 'move', element, element);
+ element.style.opacity = 0.25;
+ },
+ dragend: e => {
+ element.style.opacity = null;
+ if ('sortable' === getDragAction(e)) {
+ dragData.data.style.cssText = '';
+ let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
+ if (row != dragData.data) {
+ row.before(dragData.data);
+ }
+ dragData = null;
+ }
+ }
+ });
+ if (!parent.sortable) {
+ parent.sortable = true;
+ addEventsListeners(parent, {
+ dragenter: fnHover,
+ dragover: fnHover,
+ drop: e => {
+ if ('sortable' === getDragAction(e)) {
+ e.preventDefault();
+ let data = ko.dataFor(dragData.data),
+ from = options.list.indexOf(data),
+ to = [...parent.children].indexOf(dragData.data);
+ if (from != to) {
+ let arr = options.list();
+ arr.splice(to, 0, ...arr.splice(from, 1));
+ options.list(arr);
+ }
+ dragData = null;
+ options.afterMove && options.afterMove();
+ }
+ }
+ });
}
- };
- element.addEventListener('keyup', fn);
- ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
- }
-};
+ }
+ },
-ko.bindingHandlers.registerBootstrapDropdown = {
- init: element => {
- rl.Dropdowns.register(element);
- element.ddBtn = new BSN.Dropdown(element.querySelector('[data-toggle="dropdown"]'));
- }
-};
+ initDom: {
+ init: (element, fValueAccessor) => fValueAccessor()(element)
+ },
-ko.bindingHandlers.openDropdownTrigger = {
- update: (element, fValueAccessor) => {
- if (ko.unwrap(fValueAccessor())) {
- const el = element.ddBtn;
- el.open || el.toggle();
-// el.focus();
+ registerBootstrapDropdown: {
+ init: element => {
+ dropdowns.push(element);
+ element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
+ }
+ },
- rl.Dropdowns.detectVisibility();
- fValueAccessor()(false);
+ openDropdownTrigger: {
+ update: (element, fValueAccessor) => {
+ if (ko.unwrap(fValueAccessor())) {
+ const el = element.ddBtn;
+ el.open || el.toggle();
+ // el.focus();
+
+ dropdownsDetectVisibility();
+ fValueAccessor()(false);
+ }
}
}
-};
-
-ko.bindingHandlers.dropdownCloser = {
- init: element => element.closest('.dropdown').addEventListener('click', event =>
- event.target.closestWithin('.e-item', element) && element.ddBtn.toggle()
- )
-};
+});
diff --git a/dev/External/ifvisible.js b/dev/External/ifvisible.js
index a7c3dd984..23fb02269 100644
--- a/dev/External/ifvisible.js
+++ b/dev/External/ifvisible.js
@@ -1,37 +1,36 @@
(doc => {
- let visible = "visible",
+ let idle = 'idle',
+ visible = 'visible',
status = visible,
timer = 0,
wakeUp = () => {
clearTimeout(timer);
- if (status !== visible) {
- status = visible;
- }
+ status = visible;
timer = setTimeout(() => {
if (status === visible) {
- status = "idle";
- dispatchEvent(new CustomEvent("idle"));
+ status = idle;
+ dispatchEvent(new CustomEvent(idle));
}
}, 10000);
},
init = () => {
- init = ()=>{};
+ init = () => 0;
// Safari
- addEventListener('pagehide', () => status = "hidden");
+ addEventListener('pagehide', () => status = 'hidden');
// Else
- doc.addEventListener("visibilitychange", () => {
+ doc.addEventListener('visibilitychange', () => {
status = doc.visibilityState;
doc.hidden || wakeUp();
});
wakeUp();
- ["mousemove","keyup","touchstart"].forEach(t => doc.addEventListener(t, wakeUp));
- ["scroll","pageshow"].forEach(t => addEventListener(t, wakeUp));
+ ['mousemove','keyup','touchstart'].forEach(t => doc.addEventListener(t, wakeUp));
+ ['scroll','pageshow'].forEach(t => addEventListener(t, wakeUp));
};
this.ifvisible = {
idle: callback => {
init();
- addEventListener("idle", callback);
+ addEventListener(idle, callback);
},
now: () => {
init();
diff --git a/dev/External/ko.js b/dev/External/ko.js
index b776995b1..56167710e 100644
--- a/dev/External/ko.js
+++ b/dev/External/ko.js
@@ -1,157 +1,171 @@
+import ko from 'ko';
import { i18nToNodes } from 'Common/Translator';
import { doc, createElement } from 'Common/Globals';
import { SaveSettingsStep } from 'Common/Enums';
-import { isNonEmptyArray, isFunction } from 'Common/Utils';
+import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils';
-const
- koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value);
+export const
+ errorTip = (element, value) => value
+ ? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
+ : element.removeAttribute('data-rainloopErrorTip'),
-ko.bindingHandlers.tooltipErrorTip = {
- init: element => {
- doc.addEventListener('click', () => element.removeAttribute('data-rainloopErrorTip'));
- },
- update: (element, fValueAccessor) => {
- const value = koValue(fValueAccessor());
- if (value) {
- setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100);
- } else {
- element.removeAttribute('data-rainloopErrorTip');
+ /**
+ * The value of the pureComputed observable shouldn’t vary based on the
+ * number of evaluations or other “hidden” information. Its value should be
+ * based solely on the values of other observables in the application
+ */
+ koComputable = fn => ko.computed(fn, {'pure':true}),
+
+ addObservablesTo = (target, observables) =>
+ forEachObjectEntry(observables, (key, value) =>
+ target[key] || (target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value)) ),
+
+ addComputablesTo = (target, computables) =>
+ forEachObjectEntry(computables, (key, fn) => target[key] = koComputable(fn)),
+
+ addSubscribablesTo = (target, subscribables) =>
+ forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
+
+ dispose = disposable => disposable && isFunction(disposable.dispose) && disposable.dispose(),
+
+ // With this we don't need delegateRunOnDestroy
+ koArrayWithDestroy = data => {
+ data = ko.observableArray(data);
+ data.subscribe(changes =>
+ changes.forEach(item =>
+ 'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
+ )
+ , data, 'arrayChange');
+ return data;
+ };
+
+Object.assign(ko.bindingHandlers, {
+ tooltipErrorTip: {
+ init: (element, fValueAccessor) => {
+ doc.addEventListener('click', () => {
+ let value = fValueAccessor();
+ ko.isObservable(value) && !ko.isComputed(value) && value('');
+ errorTip(element);
+ });
+ },
+ update: (element, fValueAccessor) => {
+ let value = ko.unwrap(fValueAccessor());
+ value = isFunction(value) ? value() : value;
+ errorTip(element, value);
}
- }
-};
+ },
-ko.bindingHandlers.onEnter = {
- init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
- let fn = event => {
- if ('Enter' == event.key) {
- element.dispatchEvent(new Event('change'));
- fValueAccessor().call(viewModel);
+ onEnter: {
+ init: (element, fValueAccessor, fAllBindings, viewModel) => {
+ let fn = event => {
+ if ('Enter' == event.key) {
+ element.dispatchEvent(new Event('change'));
+ fValueAccessor().call(viewModel);
+ }
+ };
+ element.addEventListener('keydown', fn);
+ ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
+ }
+ },
+
+ onSpace: {
+ init: (element, fValueAccessor, fAllBindings, viewModel) => {
+ let fn = event => {
+ if (' ' == event.key) {
+ fValueAccessor().call(viewModel, event);
+ }
+ };
+ element.addEventListener('keyup', fn);
+ ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
+ }
+ },
+
+ i18nInit: {
+ init: element => i18nToNodes(element)
+ },
+
+ i18nUpdate: {
+ update: (element, fValueAccessor) => {
+ ko.unwrap(fValueAccessor());
+ i18nToNodes(element);
+ }
+ },
+
+ title: {
+ update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
+ },
+
+ command: {
+ init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
+ const command = fValueAccessor();
+
+ if (!command || !command.canExecute) {
+ throw new Error('Value should be a command');
}
- };
- element.addEventListener('keydown', fn);
- ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
- }
-};
-ko.bindingHandlers.onSpace = {
- init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
- let fn = event => {
- if (' ' == event.key) {
- fValueAccessor().call(viewModel, event);
+ ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
+ element,
+ fValueAccessor,
+ fAllBindings,
+ viewModel,
+ bindingContext
+ );
+ },
+ update: (element, fValueAccessor) => {
+ const cl = element.classList,
+ command = fValueAccessor();
+
+ let disabled = !command.canExecute();
+ cl.toggle('disabled', disabled);
+
+ if (element.matches('INPUT,TEXTAREA,BUTTON')) {
+ element.disabled = disabled;
}
- };
- element.addEventListener('keyup', fn);
- ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
- }
-};
-
-ko.bindingHandlers.modal = {
- init: (element, fValueAccessor) => {
- const close = element.querySelector('.close'),
- click = () => fValueAccessor()(false);
- close && close.addEventListener('click.koModal', click);
-
- ko.utils.domNodeDisposal.addDisposeCallback(element, () =>
- close.removeEventListener('click.koModal', click)
- );
- }
-};
-
-ko.bindingHandlers.i18nInit = {
- init: element => i18nToNodes(element)
-};
-
-ko.bindingHandlers.i18nUpdate = {
- update: (element, fValueAccessor) => {
- ko.unwrap(fValueAccessor());
- i18nToNodes(element);
- }
-};
-
-ko.bindingHandlers.title = {
- update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
-};
-
-ko.bindingHandlers.command = {
- init: (element, fValueAccessor, fAllBindingsAccessor, viewModel, bindingContext) => {
- const command = fValueAccessor();
-
- if (!command || !command.enabled || !command.canExecute) {
- throw new Error('Value should be a command');
}
-
- ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
- element,
- fValueAccessor,
- fAllBindingsAccessor,
- viewModel,
- bindingContext
- );
},
- update: (element, fValueAccessor) => {
- const cl = element.classList,
- command = fValueAccessor();
- let disabled = !command.enabled();
-
- disabled = disabled || !command.canExecute();
- cl.toggle('disabled', disabled);
-
- if (element.matches('INPUT,TEXTAREA,BUTTON')) {
- element.disabled = disabled;
- }
- }
-};
-
-ko.bindingHandlers.saveTrigger = {
- init: (element) => {
- let icon = element;
- if (element.matches('input,select,textarea')) {
- element.classList.add('settings-saved-trigger-input');
- element.after(element.saveTriggerIcon = icon = createElement('span'));
- }
- icon.classList.add('settings-save-trigger');
- },
- update: (element, fValueAccessor) => {
- const value = parseInt(ko.unwrap(fValueAccessor()),10);
- let cl = (element.saveTriggerIcon || element).classList;
- if (element.saveTriggerIcon) {
- cl.toggle('saving', value === SaveSettingsStep.Animate);
+ saveTrigger: {
+ init: (element) => {
+ let icon = element;
+ if (element.matches('input,select,textarea')) {
+ element.classList.add('settings-saved-trigger-input');
+ element.after(element.saveTriggerIcon = icon = createElement('span'));
+ }
+ icon.classList.add('settings-save-trigger');
+ },
+ update: (element, fValueAccessor) => {
+ const value = parseInt(ko.unwrap(fValueAccessor()),10);
+ let cl = (element.saveTriggerIcon || element).classList;
+ if (element.saveTriggerIcon) {
+ cl.toggle('saving', value === SaveSettingsStep.Animate);
+ cl.toggle('success', value === SaveSettingsStep.TrueResult);
+ cl.toggle('error', value === SaveSettingsStep.FalseResult);
+ }
+ cl = element.classList;
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
- cl = element.classList;
- cl.toggle('success', value === SaveSettingsStep.TrueResult);
- cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
-};
+});
// extenders
ko.extenders.limitedList = (target, limitedList) => {
const result = ko
- .computed({
- read: target,
- write: (newValue) => {
- const currentValue = ko.unwrap(target),
- list = ko.unwrap(limitedList);
-
- if (isNonEmptyArray(list)) {
- if (list.includes(newValue)) {
- target(newValue);
- } else if (list.includes(currentValue, list)) {
- target(currentValue + ' ');
- target(currentValue);
- } else {
- target(list[0] + ' ');
- target(list[0]);
- }
- } else {
- target('');
- }
+ .computed({
+ read: target,
+ write: newValue => {
+ let currentValue = target(),
+ list = ko.unwrap(limitedList);
+ list = arrayLength(list) ? list : [''];
+ if (!list.includes(newValue)) {
+ newValue = list.includes(currentValue, list) ? currentValue : list[0];
+ target(newValue + ' ');
}
- })
- .extend({ notify: 'always' });
+ target(newValue);
+ }
+ })
+ .extend({ notify: 'always' });
result(target());
@@ -184,6 +198,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions
-ko.observable.fn.deleteAccessHelper = function() {
- return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'deleteAccess'] });
+ko.observable.fn.askDeleteHelper = function() {
+ return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
};
diff --git a/dev/Html/PreviewMessage.html b/dev/Html/PreviewMessage.html
index 7632cf6fe..a9bbb736c 100644
--- a/dev/Html/PreviewMessage.html
+++ b/dev/Html/PreviewMessage.html
@@ -1,31 +1,24 @@
-
- {{subject}}
+
-
-
- {{subject}}
- {{date}}
- {{fromCreds}}
- {{toLabel}}: {{toCreds}}
- {{ccLabel}}: {{ccCreds}}
-
- {{html}}
-
+
diff --git a/dev/Knoin/AbstractModel.js b/dev/Knoin/AbstractModel.js
index 6ac13305d..95d0b2600 100644
--- a/dev/Knoin/AbstractModel.js
+++ b/dev/Knoin/AbstractModel.js
@@ -1,23 +1,20 @@
-import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils';
-
-function dispose(disposable) {
- if (disposable && isFunction(disposable.dispose)) {
- disposable.dispose();
- }
-}
+import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
+import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
function typeCast(curValue, newValue) {
- switch (typeof curValue)
- {
- case 'boolean': return 0 != newValue && !!newValue;
- case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
- case 'string': return null != newValue ? '' + newValue : '';
- case 'object':
- if (curValue.constructor.reviveFromJson) {
- return curValue.constructor.reviveFromJson(newValue);
+ if (null != curValue) {
+ switch (typeof curValue)
+ {
+ case 'boolean': return 0 != newValue && !!newValue;
+ case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
+ case 'string': return null != newValue ? '' + newValue : '';
+ case 'object':
+ if (curValue.constructor.reviveFromJson) {
+ return curValue.constructor.reviveFromJson(newValue);
+ }
+ if (isArray(curValue) && !isArray(newValue))
+ return [];
}
- if (isArray(curValue) && !isArray(newValue))
- return [];
}
return newValue;
}
@@ -29,7 +26,7 @@ export class AbstractModel {
throw new Error("Can't instantiate AbstractModel!");
}
*/
- this.subscribables = [];
+ this.disposables = [];
}
addObservables(observables) {
@@ -41,25 +38,26 @@ export class AbstractModel {
}
addSubscribables(subscribables) {
- Object.entries(subscribables).forEach(([key, fn]) => this.subscribables.push( this[key].subscribe(fn) ) );
+// addSubscribablesTo(this, subscribables);
+ forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
}
/** Called by delegateRunOnDestroy */
onDestroy() {
/** dispose ko subscribables */
- this.subscribables.forEach(dispose);
+ this.disposables.forEach(dispose);
/** clear object entries */
-// Object.entries(this).forEach(([key, value]) => {
- Object.values(this).forEach(value => {
+// forEachObjectEntry(this, (key, value) => {
+ forEachObjectValue(this, value => {
/** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value;
- arr && arr.onDestroy && value.onDestroy();
+ arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */
- dispose(value);
+// dispose(value);
/** clear object value */
// this[key] = null; // TODO: issue with Contacts view
});
-// this.subscribables = [];
+// this.disposables = [];
}
/**
@@ -84,39 +82,37 @@ export class AbstractModel {
revivePropertiesFromJson(json) {
let model = this.constructor;
- try {
- if (!model.validJson(json)) {
- return false;
- }
- Object.entries(json).forEach(([key, value]) => {
- if ('@' !== key[0]) {
- key = key[0].toLowerCase() + key.substr(1);
- switch (typeof this[key])
- {
- case 'function':
- if (ko.isObservable(this[key])) {
- this[key](typeCast(this[key](), value));
-// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
- }
-// else console.log(model.name + '.' + key + ' is a function');
- break;
- case 'boolean':
- case 'number':
- case 'object':
- case 'string':
- this[key] = typeCast(this[key], value);
- break;
- // fall through
- case 'undefined':
- default:
-// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
- }
- }
- });
- } catch (e) {
- console.log(model.name);
- console.error(e);
+ if (!model.validJson(json)) {
+ return false;
}
+ forEachObjectEntry(json, (key, value) => {
+ if ('@' !== key[0]) try {
+ key = key[0].toLowerCase() + key.slice(1);
+ switch (typeof this[key])
+ {
+ case 'function':
+ if (ko.isObservable(this[key])) {
+ this[key](typeCast(this[key](), value));
+// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
+ }
+// else console.log(model.name + '.' + key + ' is a function');
+ break;
+ case 'boolean':
+ case 'number':
+ case 'object':
+ case 'string':
+ this[key] = typeCast(this[key], value);
+ break;
+ // fall through
+ case 'undefined':
+ default:
+// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
+ }
+ } catch (e) {
+ console.log(model.name + '.' + key);
+ console.error(e);
+ }
+ });
return true;
}
diff --git a/dev/Knoin/AbstractScreen.js b/dev/Knoin/AbstractScreen.js
index 77a781447..d1797364f 100644
--- a/dev/Knoin/AbstractScreen.js
+++ b/dev/Knoin/AbstractScreen.js
@@ -1,24 +1,10 @@
-import { isArray, isNonEmptyArray } from 'Common/Utils';
+import { isArray, arrayLength } from 'Common/Utils';
export class AbstractScreen {
constructor(screenName, viewModels = []) {
- this.oCross = null;
- this.sScreenName = screenName;
- this.aViewModels = isArray(viewModels) ? viewModels : [];
- }
-
- /**
- * @returns {Array}
- */
- get viewModels() {
- return this.aViewModels;
- }
-
- /**
- * @returns {string}
- */
- screenName() {
- return this.sScreenName;
+ this.__cross = null;
+ this.screenName = screenName;
+ this.viewModels = isArray(viewModels) ? viewModels : [];
}
/**
@@ -28,28 +14,26 @@ export class AbstractScreen {
return null;
}
- /**
- * @returns {?Object}
- */
- get __cross() {
- return this.oCross;
- }
+/*
+ onBuild(viewModelDom) {}
+ onShow() {}
+ onHide() {}
+ __started
+ __builded
+*/
/**
* @returns {void}
*/
onStart() {
- if (!this.__started) {
- this.__started = true;
- const routes = this.routes();
- if (isNonEmptyArray(routes)) {
- let route = new Crossroads(),
- fMatcher = (this.onRoute || (()=>{})).bind(this);
+ const routes = this.routes();
+ if (arrayLength(routes)) {
+ let route = new Crossroads(),
+ fMatcher = (this.onRoute || (()=>0)).bind(this);
- routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
+ routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
- this.oCross = route;
- }
+ this.__cross = route;
}
}
}
diff --git a/dev/Knoin/AbstractViews.js b/dev/Knoin/AbstractViews.js
index 39661750d..783108649 100644
--- a/dev/Knoin/AbstractViews.js
+++ b/dev/Knoin/AbstractViews.js
@@ -1,45 +1,38 @@
import ko from 'ko';
-import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
-import { Scope } from 'Common/Enums';
-import { keyScope } from 'Common/Globals';
-import { ViewType } from 'Knoin/Knoin';
+import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
+import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
+import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
+
+import { SaveSettingsStep } from 'Common/Enums';
class AbstractView {
- constructor(name, templateID, type)
+ constructor(templateID, type)
{
- this.viewModelName = 'View/' + name;
- this.viewModelTemplateID = templateID;
- this.viewModelPosition = type;
-
- this.bDisabeCloseOnEsc = false;
- this.sDefaultScope = Scope.None;
- this.sCurrentScope = Scope.None;
-
- this.viewModelVisible = false;
- this.modalVisibility = ko.observable(false).extend({ rateLimit: 0 });
-
- this.viewModelName = '';
+// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
+ this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
+ this.viewType = type;
this.viewModelDom = null;
+
+ this.keyScope = {
+ scope: 'none',
+ previous: 'none',
+ set: function() {
+ this.previous = keyScope();
+ keyScope(this.scope);
+ },
+ unset: function() {
+ keyScope(this.previous);
+ }
+ };
}
- /**
- * @returns {void}
- */
- storeAndSetScope() {
- this.sCurrentScope = keyScope();
- keyScope(this.sDefaultScope);
- }
-
- /**
- * @returns {void}
- */
- restoreScope() {
- keyScope(this.sCurrentScope);
- }
-
- cancelCommand() {}
- closeCommand() {}
+/*
+ onBuild() {}
+ beforeShow() {} // Happens before: hidden = false
+ onShow() {} // Happens after: hidden = false
+ onHide() {}
+*/
querySelector(selectors) {
return this.viewModelDom.querySelector(selectors);
@@ -63,51 +56,115 @@ export class AbstractViewPopup extends AbstractView
{
constructor(name)
{
- super('Popup/' + name, 'Popups' + name, ViewType.Popup);
- if (name in Scope) {
- this.sDefaultScope = Scope[name];
- }
- }
-
- /**
- * @returns {void}
- */
- registerPopupKeyDown() {
- addEventListener('keydown', event => {
- if (event && this.modalVisibility && this.modalVisibility()) {
- if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
- this.cancelCommand && this.cancelCommand();
- return false;
- } else if ('Backspace' == event.key && !inFocus()) {
- return false;
- }
+ super('Popups' + name, ViewTypePopup);
+ this.keyScope.scope = name;
+ this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
+ shortcuts.add('escape,close', '', name, () => {
+ if (this.modalVisible() && false !== this.onClose()) {
+ this.close();
+ return false;
}
-
return true;
});
}
+
+ // Happens when user hits Escape or Close key
+ // return false to prevent closing
+ onClose() {}
+
+/*
+ beforeShow() {} // Happens before showModal()
+ onShow() {} // Happens after showModal()
+ afterShow() {} // Happens after showModal() animation transitionend
+ onHide() {} // Happens before animation transitionend
+ afterHide() {} // Happens after animation transitionend
+
+ close() {}
+*/
}
-export class AbstractViewCenter extends AbstractView
-{
- constructor(name, templateID)
- {
- super(name, templateID, ViewType.Center);
- }
+AbstractViewPopup.showModal = function(params = []) {
+ showScreenPopup(this, params);
+}
+
+AbstractViewPopup.hidden = function() {
+ return !this.__vm || !this.__vm.modalVisible();
}
export class AbstractViewLeft extends AbstractView
{
- constructor(name, templateID)
+ constructor(templateID)
{
- super(name, templateID, ViewType.Left);
+ super(templateID, 'Left');
+ this.leftPanelDisabled = leftPanelDisabled;
}
}
export class AbstractViewRight extends AbstractView
{
- constructor(name, templateID)
+ constructor(templateID)
{
- super(name, templateID, ViewType.Right);
+ super(templateID, 'Right');
+ }
+}
+
+export class AbstractViewSettings
+{
+/*
+ onBuild(viewModelDom) {}
+ beforeShow() {}
+ onShow() {}
+ onHide() {}
+ viewModelDom
+*/
+ addSetting(name, valueCb)
+ {
+ let prop = name[0].toLowerCase() + name.slice(1),
+ trigger = prop + 'Trigger';
+ addObservablesTo(this, {
+ [prop]: SettingsGet(name),
+ [trigger]: SaveSettingsStep.Idle,
+ });
+ addSubscribablesTo(this, {
+ [prop]: (value => {
+ this[trigger](SaveSettingsStep.Animate);
+ valueCb && valueCb(value);
+ rl.app.Remote.saveSetting(name, value,
+ iError => {
+ this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
+ setTimeout(() => this[trigger](SaveSettingsStep.Idle), 1000);
+ }
+ );
+ }).debounce(999),
+ });
+ }
+
+ addSettings(names)
+ {
+ names.forEach(name => {
+ let prop = name[0].toLowerCase() + name.slice(1);
+ this[prop] || (this[prop] = ko.observable(SettingsGet(name)));
+ this[prop].subscribe(value => rl.app.Remote.saveSetting(name, value));
+ });
+ }
+}
+
+export class AbstractViewLogin extends AbstractView {
+ constructor(templateID) {
+ super(templateID, 'Content');
+ this.hideSubmitButton = SettingsGet('hideSubmitButton');
+ this.formError = ko.observable(false).extend({ falseTimeout: 500 });
+ }
+
+ onBuild(dom) {
+ dom.classList.add('LoginView');
+ }
+
+ onShow() {
+ rl.route.off();
+ }
+
+ submitForm() {
+// return false;
}
}
diff --git a/dev/Knoin/Knoin.js b/dev/Knoin/Knoin.js
index 62c18990f..a029fd3df 100644
--- a/dev/Knoin/Knoin.js
+++ b/dev/Knoin/Knoin.js
@@ -1,370 +1,310 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
+import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
+import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
-import { doc, $htmlCL } from 'Common/Globals';
-import { runHook } from 'Common/Plugins';
-import { isNonEmptyArray, isFunction } from 'Common/Utils';
-
-let currentScreen = null,
+let
+ SCREENS = {},
+ currentScreen = null,
defaultScreenName = '';
-const SCREENS = {},
+const
autofocus = dom => {
const af = dom.querySelector('[autofocus]');
af && af.focus();
- };
+ },
-export const popupVisibilityNames = ko.observableArray([]);
+ visiblePopups = new Set,
-export const ViewType = {
- Popup: 'Popups',
- Left: 'Left',
- Right: 'Right',
- Center: 'Center'
-};
+ /**
+ * @param {string} screenName
+ * @returns {?Object}
+ */
+ screen = screenName => screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null,
-/**
- * @param {Function} fExecute
- * @param {(Function|boolean|null)=} fCanExecute = true
- * @returns {Function}
- */
-export function createCommand(fExecute, fCanExecute = true) {
- let fResult = null;
+ /**
+ * @param {Function} ViewModelClass
+ * @param {Object=} vmScreen
+ * @returns {*}
+ */
+ buildViewModel = (ViewModelClass, vmScreen) => {
+ if (ViewModelClass && !ViewModelClass.__builded) {
+ let vmDom = null;
+ const
+ vm = new ViewModelClass(vmScreen),
+ id = vm.viewModelTemplateID,
+ position = vm.viewType || '',
+ dialog = ViewTypePopup === position,
+ vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
- fResult = fExecute
- ? (...args) => {
- if (fResult && fResult.canExecute && fResult.canExecute()) {
- fExecute.apply(null, args);
- }
- return false;
- } : ()=>{};
- fResult.enabled = ko.observable(true);
- fResult.isCommand = true;
+ ViewModelClass.__builded = true;
+ ViewModelClass.__vm = vm;
- if (isFunction(fCanExecute)) {
- fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null));
- } else {
- fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute);
- }
+ if (vmPlace) {
+ vmDom = Element.fromHTML(dialog
+ ? ' '
+ : '
');
+ vmPlace.append(vmDom);
- return fResult;
-}
+ vm.viewModelDom = vmDom;
+ ViewModelClass.__dom = vmDom;
-/**
- * @param {string} screenName
- * @returns {?Object}
- */
-function screen(screenName) {
- return screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null;
-}
+ if (ViewTypePopup === position) {
+ vm.close = () => hideScreenPopup(ViewModelClass);
-/**
- * @param {Function} ViewModelClassToHide
- * @returns {void}
- */
-export function hideScreenPopup(ViewModelClassToHide) {
- if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
- ViewModelClassToHide.__vm.modalVisibility(false);
- }
-}
+ // Firefox / Safari HTMLDialogElement not defined
+ if (!vmDom.showModal) {
+ vmDom.classList.add('polyfill');
+ vmDom.showModal = () => {
+ vmDom.backdrop ||
+ vmDom.before(vmDom.backdrop = Element.fromHTML('
'));
+ vmDom.setAttribute('open','');
+ vmDom.open = true;
+ vmDom.returnValue = null;
+ vmDom.backdrop.hidden = false;
+ };
+ vmDom.close = v => {
+ vmDom.backdrop.hidden = true;
+ vmDom.returnValue = v;
+ vmDom.removeAttribute('open', null);
+ vmDom.open = false;
+ };
+ }
-/**
- * @param {Function} ViewModelClass
- * @param {Object=} vmScreen
- * @returns {*}
- */
-function buildViewModel(ViewModelClass, vmScreen) {
- if (ViewModelClass && !ViewModelClass.__builded) {
- let vmDom = null;
- const vm = new ViewModelClass(vmScreen),
- position = vm.viewModelPosition || '',
- vmPlace = position ? doc.querySelector('#rl-content #rl-' + position.toLowerCase()) : null;
-
- ViewModelClass.__builded = true;
- ViewModelClass.__vm = vm;
-
- if (vmPlace) {
- vmDom = Element.fromHTML('
');
- vmPlace.append(vmDom);
-
- vm.viewModelDom = vmDom;
- ViewModelClass.__dom = vmDom;
-
- if (ViewType.Popup === position) {
- vm.cancelCommand = vm.closeCommand = createCommand(() => {
- hideScreenPopup(ViewModelClass);
- });
-
- // show/hide popup/modal
- const endShowHide = e => {
- if (e.target === vmDom) {
- if (vmDom.classList.contains('show')) {
- autofocus(vmDom);
- vm.onShowWithDelay && vm.onShowWithDelay();
- } else {
- vmDom.hidden = true;
- vm.onHideWithDelay && vm.onHideWithDelay();
+ // show/hide popup/modal
+ const endShowHide = e => {
+ if (e.target === vmDom) {
+ if (vmDom.classList.contains('animate')) {
+ autofocus(vmDom);
+ vm.afterShow && vm.afterShow();
+ } else {
+ vmDom.close();
+ vm.afterHide && vm.afterHide();
+ }
}
- }
- };
+ };
- vm.modalVisibility.subscribe(value => {
- if (value) {
- vmDom.style.zIndex = 3000 + popupVisibilityNames().length + 10;
- vmDom.hidden = false;
- vm.storeAndSetScope();
- popupVisibilityNames.push(vm.viewModelName);
- requestAnimationFrame(() => { // wait just before the next paint
- vmDom.offsetHeight; // force a reflow
- vmDom.classList.add('show'); // trigger the transitions
- });
- } else {
- vm.onHide && vm.onHide();
- vmDom.classList.remove('show');
- vm.restoreScope();
- popupVisibilityNames(popupVisibilityNames.filter(v=>v!==vm.viewModelName));
- }
- vmDom.setAttribute('aria-hidden', !value);
- });
- if ('ontransitionend' in vmDom) {
+ vm.modalVisible.subscribe(value => {
+ if (value) {
+ visiblePopups.add(vm);
+ vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
+ vmDom.showModal();
+ if (vmDom.backdrop) {
+ vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
+ }
+ vm.keyScope.set();
+ requestAnimationFrame(() => { // wait just before the next paint
+ vmDom.offsetHeight; // force a reflow
+ vmDom.classList.add('animate'); // trigger the transitions
+ });
+ } else {
+ visiblePopups.delete(vm);
+ vm.onHide && vm.onHide();
+ vm.keyScope.unset();
+ vmDom.classList.remove('animate'); // trigger the transitions
+ }
+ arePopupsVisible(0 < visiblePopups.size);
+ });
vmDom.addEventListener('transitionend', endShowHide);
- } else {
- // For Edge < 79 and mobile browsers
- vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
}
- }
- ko.applyBindingAccessorsToNode(
- vmDom,
- {
- i18nInit: true,
- template: () => ({ name: vm.viewModelTemplateID })
- },
- vm
- );
-
- vm.onBuild && vm.onBuild(vmDom);
- if (vm && ViewType.Popup === position) {
- vm.registerPopupKeyDown();
- }
- } else {
- console.log('Cannot find view model position: ' + position);
- }
- }
-
- return ViewModelClass && ViewModelClass.__vm;
-}
-
-function getScreenPopupViewModel(ViewModelClassToShow) {
- return (buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm;
-}
-
-/**
- * @param {Function} ViewModelClassToShow
- * @param {Array=} params
- * @returns {void}
- */
-export function showScreenPopup(ViewModelClassToShow, params = []) {
- const vm = getScreenPopupViewModel(ViewModelClassToShow);
- if (vm) {
- params = params || [];
-
- vm.onBeforeShow && vm.onBeforeShow(...params);
-
- vm.modalVisibility(true);
-
- vm.onShow && vm.onShow(...params);
-
- runHook('view-model-on-show', [vm.name, vm.__vm, params]);
- }
-}
-
-/**
- * @param {Function} ViewModelClassToShow
- * @returns {void}
- */
-export function warmUpScreenPopup(ViewModelClassToShow) {
- const vm = getScreenPopupViewModel(ViewModelClassToShow);
- vm && vm.onWarmUp && vm.onWarmUp();
-}
-
-/**
- * @param {Function} ViewModelClassToShow
- * @returns {boolean}
- */
-export function isPopupVisible(ViewModelClassToShow) {
- return ViewModelClassToShow && ViewModelClassToShow.__vm && ViewModelClassToShow.__vm.modalVisibility();
-}
-
-/**
- * @param {string} screenName
- * @param {string} subPart
- * @returns {void}
- */
-function screenOnRoute(screenName, subPart) {
- let vmScreen = null,
- isSameScreen = false;
-
- if (null == screenName || '' == screenName) {
- screenName = defaultScreenName;
- }
-
- if (screenName) {
- vmScreen = screen(screenName);
- if (!vmScreen) {
- vmScreen = screen(defaultScreenName);
- if (vmScreen) {
- subPart = screenName + '/' + subPart;
- screenName = defaultScreenName;
- }
- }
-
- if (vmScreen && vmScreen.__started) {
- isSameScreen = currentScreen && vmScreen === currentScreen;
-
- if (!vmScreen.__builded) {
- vmScreen.__builded = true;
-
- vmScreen.viewModels.forEach(ViewModelClass =>
- buildViewModel(ViewModelClass, vmScreen)
+ ko.applyBindingAccessorsToNode(
+ vmDom,
+ {
+ i18nInit: true,
+ template: () => ({ name: id })
+ },
+ vm
);
- vmScreen.onBuild && vmScreen.onBuild();
+ vm.onBuild && vm.onBuild(vmDom);
+
+ fireEvent('rl-view-model', vm);
+ } else {
+ console.log('Cannot find view model position: ' + position);
+ }
+ }
+
+ return ViewModelClass && ViewModelClass.__vm;
+ },
+
+ forEachViewModel = (screen, fn) => {
+ screen.viewModels.forEach(ViewModelClass => {
+ if (
+ ViewModelClass.__vm &&
+ ViewModelClass.__dom &&
+ ViewTypePopup !== ViewModelClass.__vm.viewType
+ ) {
+ fn(ViewModelClass.__vm, ViewModelClass.__dom);
+ }
+ });
+ },
+
+ hideScreen = (screenToHide, destroy) => {
+ screenToHide.onHide && screenToHide.onHide();
+ forEachViewModel(screenToHide, (vm, dom) => {
+ dom.hidden = true;
+ vm.onHide && vm.onHide();
+ destroy && vm.viewModelDom.remove();
+ });
+ },
+
+ /**
+ * @param {Function} ViewModelClassToHide
+ * @returns {void}
+ */
+ hideScreenPopup = ViewModelClassToHide => {
+ if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
+ ViewModelClassToHide.__vm.modalVisible(false);
+ }
+ },
+
+ /**
+ * @param {string} screenName
+ * @param {string} subPart
+ * @returns {void}
+ */
+ screenOnRoute = (screenName, subPart) => {
+ let vmScreen = null,
+ isSameScreen = false;
+
+ if (null == screenName || '' == screenName) {
+ screenName = defaultScreenName;
+ }
+
+ // Close all popups
+ for (let vm of visiblePopups) {
+ false === vm.onClose() || vm.close();
+ }
+
+ if (screenName) {
+ vmScreen = screen(screenName);
+ if (!vmScreen) {
+ vmScreen = screen(defaultScreenName);
+ if (vmScreen) {
+ subPart = screenName + '/' + subPart;
+ screenName = defaultScreenName;
+ }
}
- setTimeout(() => {
- // hide screen
- if (currentScreen && !isSameScreen) {
- currentScreen.onHide && currentScreen.onHide();
- currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500);
+ if (vmScreen && vmScreen.__started) {
+ isSameScreen = currentScreen && vmScreen === currentScreen;
- if (isNonEmptyArray(currentScreen.viewModels)) {
- currentScreen.viewModels.forEach(ViewModelClass => {
- if (
- ViewModelClass.__vm &&
- ViewModelClass.__dom &&
- ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
- ) {
- ViewModelClass.__dom.hidden = true;
- ViewModelClass.__vm.viewModelVisible = false;
+ if (!vmScreen.__builded) {
+ vmScreen.__builded = true;
- ViewModelClass.__vm.onHide && ViewModelClass.__vm.onHide();
- ViewModelClass.__vm.onHideWithDelay && setTimeout(()=>ViewModelClass.__vm.onHideWithDelay(), 500);
- }
+ vmScreen.viewModels.forEach(ViewModelClass =>
+ buildViewModel(ViewModelClass, vmScreen)
+ );
+
+ vmScreen.onBuild && vmScreen.onBuild();
+ }
+
+ setTimeout(() => {
+ // hide screen
+ if (currentScreen && !isSameScreen) {
+ hideScreen(currentScreen);
+ }
+ // --
+
+ currentScreen = vmScreen;
+
+ // show screen
+ if (!isSameScreen) {
+ vmScreen.onShow && vmScreen.onShow();
+
+ forEachViewModel(vmScreen, (vm, dom) => {
+ vm.beforeShow && vm.beforeShow();
+ dom.hidden = false;
+ vm.onShow && vm.onShow();
+ autofocus(dom);
});
}
- }
- // --
+ // --
- currentScreen = vmScreen;
-
- // show screen
- if (currentScreen && !isSameScreen) {
- currentScreen.onShow && currentScreen.onShow();
-
- if (isNonEmptyArray(currentScreen.viewModels)) {
- currentScreen.viewModels.forEach(ViewModelClass => {
- if (
- ViewModelClass.__vm &&
- ViewModelClass.__dom &&
- ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
- ) {
- ViewModelClass.__vm.onBeforeShow && ViewModelClass.__vm.onBeforeShow();
-
- ViewModelClass.__dom.hidden = false;
- ViewModelClass.__vm.viewModelVisible = true;
-
- ViewModelClass.__vm.onShow && ViewModelClass.__vm.onShow();
-
- autofocus(ViewModelClass.__dom);
-
- ViewModelClass.__vm.onShowWithDelay && setTimeout(()=>ViewModelClass.__vm.onShowWithDelay, 200);
-
- runHook('view-model-on-show', [ViewModelClass.name, ViewModelClass.__vm, params]);
- }
- });
- }
- }
- // --
-
- vmScreen && vmScreen.__cross && vmScreen.__cross.parse(subPart);
- }, 1);
+ vmScreen.__cross && vmScreen.__cross.parse(subPart);
+ }, 1);
+ }
}
- }
-}
+ };
-/**
- * @param {Array} screensClasses
- * @returns {void}
- */
-export function startScreens(screensClasses) {
- screensClasses.forEach(CScreen => {
- if (CScreen) {
- const vmScreen = new CScreen(),
- screenName = vmScreen && vmScreen.screenName();
- if (screenName) {
+export const
+ ViewTypePopup = 'Popups',
+
+ /**
+ * @param {Function} ViewModelClassToShow
+ * @param {Array=} params
+ * @returns {void}
+ */
+ showScreenPopup = (ViewModelClassToShow, params = []) => {
+ const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
+
+ if (vm) {
+ params = params || [];
+
+ vm.beforeShow && vm.beforeShow(...params);
+
+ vm.modalVisible(true);
+
+ vm.onShow && vm.onShow(...params);
+ }
+ },
+
+ arePopupsVisible = ko.observable(false),
+
+ /**
+ * @param {Array} screensClasses
+ * @returns {void}
+ */
+ startScreens = screensClasses => {
+ hasher.clear();
+ forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
+ SCREENS = {};
+ currentScreen = null,
+ defaultScreenName = '';
+
+ screensClasses.forEach(CScreen => {
+ if (CScreen) {
+ const vmScreen = new CScreen(),
+ screenName = vmScreen.screenName;
defaultScreenName || (defaultScreenName = screenName);
-
SCREENS[screenName] = vmScreen;
}
- }
- });
+ });
- Object.values(SCREENS).forEach(vmScreen =>
- vmScreen && vmScreen.onStart && vmScreen.onStart()
- );
-
- const cross = new Crossroads();
- cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
-
- hasher.changed.add(cross.parse.bind(cross));
- hasher.init();
-
- setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
- setTimeout(() => $htmlCL.add('rl-started-delay'), 200);
-}
-
-function decorateKoCommands(thisArg, commands) {
- Object.entries(commands).forEach(([key, canExecute]) => {
- let command = thisArg[key],
- fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args);
-
-// fn.__realCanExecute = canExecute;
-// fn.isCommand = true;
-
- fn.enabled = ko.observable(true);
-
- fn.canExecute = (typeof canExecute === 'function')
- ? ko.computed(() => fn.enabled() && canExecute.call(thisArg, thisArg))
- : ko.computed(() => fn.enabled());
-
- thisArg[key] = fn;
- });
-}
-ko.decorateCommands = decorateKoCommands;
-
-/**
- * @param {miced} $items
- * @returns {Function}
- */
-function settingsMenuKeysHandler(items) {
- return ((event, handler)=>{
- let index = items.length;
- if (event && index) {
- while (index-- && !items[index].matches('.selected'));
- if (handler && 'arrowup' === handler.shortcut) {
- index && --index;
- } else if (index < items.length - 1) {
- ++index;
+ forEachObjectValue(SCREENS, vmScreen => {
+ if (!vmScreen.__started) {
+ vmScreen.onStart();
+ vmScreen.__started = true;
}
+ });
- const resultHash = items[index].href;
- resultHash && rl.route.setHash(resultHash, false, true);
- }
- }).throttle(200);
-}
+ const cross = new Crossroads();
+ cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
-export {
- decorateKoCommands,
- settingsMenuKeysHandler
-};
+ hasher.add(cross.parse.bind(cross));
+ hasher.init();
+
+ setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
+
+ const c = elementById('rl-content'), l = elementById('rl-loading');
+ c && (c.hidden = false);
+ l && l.remove();
+ },
+
+ /**
+ * Used by ko.bindingHandlers.command (template data-bind="command: ")
+ * to enable/disable click/submit action.
+ */
+ decorateKoCommands = (thisArg, commands) =>
+ forEachObjectEntry(commands, (key, canExecute) => {
+ let command = thisArg[key],
+ fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
+
+ fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
+
+ thisArg[key] = fn;
+ });
+
+ko.decorateCommands = decorateKoCommands;
diff --git a/dev/Mime/Parser.js b/dev/Mime/Parser.js
new file mode 100644
index 000000000..59ed4c408
--- /dev/null
+++ b/dev/Mime/Parser.js
@@ -0,0 +1,132 @@
+
+const
+ // RFC2045
+ QPDecodeIn = /=([0-9A-F]{2})/g,
+ QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
+
+export function ParseMime(text)
+{
+ class MimePart
+ {
+ header(name) {
+ return this.headers && this.headers[name];
+ }
+
+ headerValue(name) {
+ return (this.header(name) || {value:null}).value;
+ }
+
+ get raw() {
+ return text.slice(this.start, this.end);
+ }
+
+ get bodyRaw() {
+ return text.slice(this.bodyStart, this.bodyEnd);
+ }
+
+ get body() {
+ let body = this.bodyRaw,
+ encoding = this.headerValue('content-transfer-encoding');
+ if ('quoted-printable' == encoding) {
+ body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
+ } else if ('base64' == encoding) {
+ body = atob(body.replace(/\r?\n/g, ''));
+ }
+ return body;
+ }
+
+ get dataUrl() {
+ let body = this.bodyRaw,
+ encoding = this.headerValue('content-transfer-encoding');
+ if ('base64' == encoding) {
+ body = body.replace(/\r?\n/g, '');
+ } else {
+ if ('quoted-printable' == encoding) {
+ body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
+ }
+ body = btoa(body);
+ }
+ return 'data:' + this.headerValue('content-type') + ';base64,' + body;
+ }
+
+ forEach(fn) {
+ fn(this);
+ if (this.parts) {
+ this.parts.forEach(part => part.forEach(fn));
+ }
+ }
+
+ getByContentType(type) {
+ if (type == this.headerValue('content-type')) {
+ return this;
+ }
+ if (this.parts) {
+ let i = 0, p = this.parts, part;
+ for (i; i < p.length; ++i) {
+ if ((part = p[i].getByContentType(type))) {
+ return part;
+ }
+ }
+ }
+ }
+ }
+
+ const ParsePart = (mimePart, start_pos = 0, id = '') =>
+ {
+ let part = new MimePart;
+ if (id) {
+ part.id = id;
+ part.start = start_pos;
+ part.end = start_pos + mimePart.length;
+ }
+
+ // get headers
+ let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/);
+ if (head) {
+ head = head[0];
+ let headers = {};
+ head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
+ let match = header.match(/^([^:]+):\s*([^;]+)/),
+ params = {};
+ [...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
+ params[param[1].trim().toLowerCase()] = param[2].trim()
+ );
+ headers[match[1].trim().toLowerCase()] = {
+ value: match[2].trim(),
+ params: params
+ };
+ });
+ part.headers = headers;
+
+ // get body
+ part.bodyStart = start_pos + head.length;
+ part.bodyEnd = start_pos + mimePart.length;
+
+ // get child parts
+ let boundary = headers['content-type'].params.boundary;
+ if (boundary) {
+ part.boundary = boundary;
+ part.parts = [];
+ let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
+ body = mimePart.slice(head.length),
+ bodies = body.split(regex),
+ pos = part.bodyStart;
+ [...body.matchAll(regex)].forEach(([boundary], index) => {
+ if (!index) {
+ // Mostly something like: "This is a multi-part message in MIME format."
+ part.bodyText = bodies[0];
+ }
+ // Not the end?
+ if ('--' != boundary.trim().slice(-2)) {
+ pos += bodies[index].length + boundary.length;
+ part.parts.push(ParsePart(bodies[1+index], pos, ((id ? id + '.' : '') + (1+index))));
+ }
+ });
+ }
+ }
+
+ return part;
+ };
+
+ return ParsePart(text);
+}
diff --git a/dev/Mime/Utils.js b/dev/Mime/Utils.js
new file mode 100644
index 000000000..07d836c63
--- /dev/null
+++ b/dev/Mime/Utils.js
@@ -0,0 +1,76 @@
+
+import { ParseMime } from 'Mime/Parser';
+import { AttachmentModel } from 'Model/Attachment';
+import { FileInfo } from 'Common/File';
+import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
+
+/**
+ * @param string data
+ * @param MessageModel message
+ */
+export function MimeToMessage(data, message)
+{
+ let signed;
+ const struct = ParseMime(data);
+ if (struct.headers) {
+ let html = struct.getByContentType('text/html');
+ html = html ? html.body : '';
+
+ struct.forEach(part => {
+ let cd = part.header('content-disposition'),
+ cid = part.header('content-id'),
+ type = part.header('content-type');
+ if (cid || cd) {
+ // if (cd && 'attachment' === cd.value) {
+ let attachment = new AttachmentModel;
+ attachment.mimeType = type.value;
+ attachment.fileName = type.name || (cd && cd.params.filename) || '';
+ attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
+ attachment.fileType = FileInfo.getType('', type.value);
+ attachment.url = part.dataUrl;
+ attachment.friendlySize = FileInfo.friendlySize(part.body.length);
+/*
+ attachment.isThumbnail = false;
+ attachment.contentLocation = '';
+ attachment.download = '';
+ attachment.folder = '';
+ attachment.uid = '';
+ attachment.mimeIndex = part.id;
+ attachment.framed = false;
+*/
+ attachment.cid = cid ? cid.value : '';
+ if (cid && html) {
+ let cid = 'cid:' + attachment.contentId(),
+ found = html.includes(cid);
+ attachment.isInline(found);
+ attachment.isLinked(found);
+ found && (html = html
+ .replace('src="' + cid + '"', 'src="' + attachment.url + '"')
+ .replace("src='" + cid + "'", "src='" + attachment.url + "'")
+ );
+ } else {
+ message.attachments.push(attachment);
+ }
+ } else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
+ signed = {
+ MicAlg: type.micalg,
+ BodyPart: part.parts[0],
+ SigPart: part.parts[1]
+ };
+ }
+ });
+
+ const text = struct.getByContentType('text/plain');
+ message.plain(text ? text.body : '');
+ message.html(html);
+ } else {
+ message.plain(data);
+ }
+
+ if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
+ signed = true;
+ }
+ message.pgpSigned(signed);
+
+ // TODO: Verify instantly?
+}
diff --git a/dev/Model/AbstractCollection.js b/dev/Model/AbstractCollection.js
index 9ec8c08bf..85dd57449 100644
--- a/dev/Model/AbstractCollection.js
+++ b/dev/Model/AbstractCollection.js
@@ -1,4 +1,4 @@
-import { isArray } from 'Common/Utils';
+import { isArray, forEachObjectEntry } from 'Common/Utils';
export class AbstractCollectionModel extends Array
{
@@ -24,8 +24,7 @@ export class AbstractCollectionModel extends Array
const result = new this();
if (json) {
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
- Object.entries(json).forEach(([key, value]) => '@' !== key[0] && (result[key] = value));
-// json[@Count]
+ forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
json = json['@Collection'];
}
if (isArray(json)) {
diff --git a/dev/Model/Account.js b/dev/Model/Account.js
index c0789b790..adfc5feec 100644
--- a/dev/Model/Account.js
+++ b/dev/Model/Account.js
@@ -1,5 +1,3 @@
-import { change } from 'Common/Links';
-
import { AbstractModel } from 'Knoin/AbstractModel';
export class AccountModel extends AbstractModel {
@@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
* @param {boolean=} canBeDelete = true
* @param {number=} count = 0
*/
- constructor(email, canBeDelete = true, count = 0) {
+ constructor(email/*, count = 0*/, isAdditional = true) {
super();
this.email = email;
this.addObservables({
- count: count,
- deleteAccess: false,
- canBeDeleted: !!canBeDelete
+// count: count || 0,
+ askDelete: false,
+ isAdditional: isAdditional
});
- this.canBeEdit = this.canBeDeleted;
}
- /**
- * @returns {string}
- */
- changeAccountLink() {
- return change(this.email);
- }
}
diff --git a/dev/Model/Attachment.js b/dev/Model/Attachment.js
index 4c2573857..c5522a1dc 100644
--- a/dev/Model/Attachment.js
+++ b/dev/Model/Attachment.js
@@ -21,17 +21,20 @@ export class AttachmentModel extends AbstractModel {
this.fileNameExt = '';
this.fileType = FileType.Unknown;
this.friendlySize = '';
- this.isInline = false;
- this.isLinked = false;
this.isThumbnail = false;
this.cid = '';
- this.cidWithoutTags = '';
this.contentLocation = '';
this.download = '';
this.folder = '';
this.uid = '';
+ this.url = '';
this.mimeIndex = '';
this.framed = false;
+
+ this.addObservables({
+ isInline: false,
+ isLinked: false
+ });
}
/**
@@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
const attachment = super.reviveFromJson(json);
if (attachment) {
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
- attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
@@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
return attachment;
}
+ contentId() {
+ return this.cid.replace(/^<+|>+$/g, '');
+ }
+
/**
* @returns {boolean}
*/
@@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
* @returns {string}
*/
linkDownload() {
- return attachmentDownload(this.download);
+ return this.url || attachmentDownload(this.download);
}
/**
* @returns {string}
*/
linkPreview() {
- return serverRequestRaw('View', this.download);
- }
-
- /**
- * @returns {string}
- */
- linkThumbnail() {
- return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
+ return this.url || serverRequestRaw('View', this.download);
}
/**
* @returns {string}
*/
linkThumbnailPreviewStyle() {
- const link = this.linkThumbnail();
- return link ? 'background:url(' + link + ')' : '';
+ return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
}
/**
@@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
const localEvent = event.originalEvent || event;
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
let link = this.linkDownload();
- if ('http' !== link.substr(0, 4)) {
+ if ('http' !== link.slice(0, 4)) {
link = location.protocol + '//' + location.host + location.pathname + link;
}
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
diff --git a/dev/Model/AttachmentCollection.js b/dev/Model/AttachmentCollection.js
index 36a97534b..b799dc8dc 100644
--- a/dev/Model/AttachmentCollection.js
+++ b/dev/Model/AttachmentCollection.js
@@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/
static reviveFromJson(items) {
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
+/*
+ const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
+ if (attachments) {
+ attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
+ }
+ return attachments;
+*/
}
/**
* @returns {boolean}
*/
hasVisible() {
- return !!this.find(item => !item.isLinked);
+ return !!this.filter(item => !item.isLinked()).length;
}
/**
@@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
* @returns {*}
*/
findByCid(cid) {
- cid = cid.replace(/^<+|>+$/, '');
- return this.find(item => cid === item.cidWithoutTags);
+ cid = cid.replace(/^<+|>+$/g, '');
+ return this.find(item => cid === item.contentId());
}
}
diff --git a/dev/Model/ComposeAttachment.js b/dev/Model/ComposeAttachment.js
index 231e1deb4..99f3c87b3 100644
--- a/dev/Model/ComposeAttachment.js
+++ b/dev/Model/ComposeAttachment.js
@@ -65,8 +65,8 @@ export class ComposeAttachmentModel extends AbstractModel {
item.download,
item.fileName,
item.estimatedSize,
- item.isInline,
- item.isLinked,
+ item.isInline(),
+ item.isLinked(),
item.cid,
item.contentLocation
);
diff --git a/dev/Model/Contact.js b/dev/Model/Contact.js
index 5bbc0de51..edff787bd 100644
--- a/dev/Model/Contact.js
+++ b/dev/Model/Contact.js
@@ -1,4 +1,4 @@
-import { isNonEmptyArray } from 'Common/Utils';
+import { arrayLength } from 'Common/Utils';
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
import { AbstractModel } from 'Knoin/AbstractModel';
@@ -26,7 +26,7 @@ export class ContactModel extends AbstractModel {
let name = '',
email = '';
- if (isNonEmptyArray(this.properties)) {
+ if (arrayLength(this.properties)) {
this.properties.forEach(property => {
if (property) {
if (ContactPropertyType.FirstName === property.type()) {
@@ -52,7 +52,7 @@ export class ContactModel extends AbstractModel {
const contact = super.reviveFromJson(json);
if (contact) {
let list = [];
- if (isNonEmptyArray(json.properties)) {
+ if (arrayLength(json.properties)) {
json.properties.forEach(property => {
property = ContactPropertyModel.reviveFromJson(property);
property && list.push(property);
diff --git a/dev/Model/Email.js b/dev/Model/Email.js
index 3257b1d91..02d0ea96a 100644
--- a/dev/Model/Email.js
+++ b/dev/Model/Email.js
@@ -260,7 +260,7 @@ class Tokenizer
}
}
-class EmailModel extends AbstractModel {
+export class EmailModel extends AbstractModel {
/**
* @param {string=} email = ''
* @param {string=} name = ''
@@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
return false;
}
}
-
-export { EmailModel, EmailModel as default };
diff --git a/dev/Model/FolderCollection.js b/dev/Model/FolderCollection.js
index 0b4112f9d..b3fec9770 100644
--- a/dev/Model/FolderCollection.js
+++ b/dev/Model/FolderCollection.js
@@ -1,42 +1,112 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
-import { isArray, pInt } from 'Common/Utils';
-import { ClientSideKeyName, FolderType } from 'Common/EnumsUser';
-import * as Cache from 'Common/Cache';
-import { Settings, SettingsGet } from 'Common/Globals';
+import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
+import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
+import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
+import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import * as Local from 'Storage/Client';
import { AppUserStore } from 'Stores/User/App';
import { FolderUserStore } from 'Stores/User/Folder';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import ko from 'ko';
-import { isPosNumeric } from 'Common/UtilsUser';
+import { sortFolders } from 'Common/Folders';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel';
+import { koComputable } from 'External/ko';
+
+//import { mailBox } from 'Common/Links';
+
+import Remote from 'Remote/User/Fetch';
+
const
- ServerFolderType = {
- USER: 0,
- INBOX: 1,
- SENT: 2,
- DRAFTS: 3,
- JUNK: 4,
- TRASH: 5,
- IMPORTANT: 10,
- FLAGGED: 11,
- ALL: 12
+ isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
+
+ normalizeFolder = sFolderFullName => ('' === sFolderFullName
+ || UNUSED_OPTION_VALUE === sFolderFullName
+ || null !== getFolderFromCacheList(sFolderFullName))
+ ? sFolderFullName
+ : '',
+
+ SystemFolders = {
+ Inbox: 0,
+ Sent: 0,
+ Drafts: 0,
+ Spam: 0,
+ Trash: 0,
+ Archive: 0
},
-normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
- || UNUSED_OPTION_VALUE === sFolderFullNameRaw
- || null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
- ? sFolderFullNameRaw
- : '';
+ kolabTypes = {
+ configuration: 'CONFIGURATION',
+ event: 'CALENDAR',
+ contact: 'CONTACTS',
+ task: 'TASKS',
+ note: 'NOTES',
+ file: 'FILES',
+ journal: 'JOURNAL'
+ },
+
+ getKolabFolderName = type => kolabTypes[type] ? 'Kolab ' + i18n('SETTINGS_FOLDERS/TYPE_' + kolabTypes[type]) : '',
+
+ getSystemFolderName = (type, def) => {
+ switch (type) {
+ case FolderType.Inbox:
+ case FolderType.Sent:
+ case FolderType.Drafts:
+ case FolderType.Trash:
+ case FolderType.Archive:
+ return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
+ case FolderType.Spam:
+ return i18n('GLOBAL/SPAM');
+ // no default
+ }
+ return def;
+ };
+
+export const
+ /**
+ * @param {string} sFullName
+ * @param {boolean} bExpanded
+ */
+ setExpandedFolder = (sFullName, bExpanded) => {
+ let aExpandedList = Local.get(ClientSideKeyNameExpandedFolders);
+ if (!isArray(aExpandedList)) {
+ aExpandedList = [];
+ }
+
+ if (bExpanded) {
+ aExpandedList.includes(sFullName) || aExpandedList.push(sFullName);
+ } else {
+ aExpandedList = aExpandedList.filter(value => value !== sFullName);
+ }
+
+ Local.set(ClientSideKeyNameExpandedFolders, aExpandedList);
+ },
+
+ /**
+ * @param {?Function} fCallback
+ */
+ loadFolders = fCallback => {
+// clearTimeout(this.foldersTimeout);
+ Remote.abort('Folders')
+ .post('Folders', FolderUserStore.foldersLoading)
+ .then(data => {
+ data = FolderCollectionModel.reviveFromJson(data.Result);
+ data && data.storeIt();
+ fCallback && fCallback(true);
+ // Repeat every 15 minutes?
+// this.foldersTimeout = setTimeout(loadFolders, 900000);
+ })
+ .catch(() => fCallback && setTimeout(fCallback, 1, false));
+ };
export class FolderCollectionModel extends AbstractCollectionModel
{
@@ -44,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
constructor() {
super();
this.CountRec
- this.FoldersHash
this.IsThreadsSupported
this.Namespace;
this.Optimized
this.SystemFolders
+ this.Capabilities
}
*/
@@ -57,164 +127,162 @@ export class FolderCollectionModel extends AbstractCollectionModel
* @returns {FolderCollectionModel}
*/
static reviveFromJson(object) {
- const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders);
- return super.reviveFromJson(object, (oFolder, self) => {
- let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw);
-/*
- if (oCacheFolder) {
- oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
- oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
- }
-*/
- if (!oCacheFolder && (oCacheFolder = FolderModel.reviveFromJson(oFolder))) {
- if (oFolder.FullNameRaw == self.SystemFolders[ServerFolderType.INBOX]) {
+ const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
+ if (object && object.SystemFolders) {
+ forEachObjectEntry(SystemFolders, key =>
+ SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
+ );
+ }
+
+ const result = super.reviveFromJson(object, oFolder => {
+ let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
+ type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
+
+ if (!oCacheFolder) {
+ oCacheFolder = FolderModel.reviveFromJson(oFolder);
+ if (!oCacheFolder)
+ return null;
+
+ if (1 == type) {
oCacheFolder.type(FolderType.Inbox);
- Cache.setFolderInboxName(oFolder.FullNameRaw);
+ setFolderInboxName(oFolder.FullName);
}
- Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder);
+ setFolder(oCacheFolder);
}
- if (oCacheFolder) {
- oCacheFolder.collapsed(!expandedFolders
- || !isArray(expandedFolders)
- || !expandedFolders.includes(oCacheFolder.fullNameHash));
+ if (1 < type) {
+ oCacheFolder.type(type);
+ }
- if (oFolder.Extended) {
- if (oFolder.Extended.Hash) {
- Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash);
- }
+ oCacheFolder.collapsed(!expandedFolders
+ || !isArray(expandedFolders)
+ || !expandedFolders.includes(oCacheFolder.fullName));
- if (null != oFolder.Extended.MessageCount) {
- oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
- }
+ if (oFolder.Extended) {
+ if (oFolder.Extended.Hash) {
+ setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
+ }
- if (null != oFolder.Extended.MessageUnseenCount) {
- oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
- }
+ if (null != oFolder.Extended.MessageCount) {
+ oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
+ }
+
+ if (null != oFolder.Extended.MessageUnseenCount) {
+ oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
}
}
return oCacheFolder;
});
+
+ let i = result.length;
+ if (i) {
+ sortFolders(result);
+ try {
+ while (i--) {
+ let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
+ if (parent) {
+ parent.subFolders.unshift(folder);
+ result.splice(i,1);
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ return result;
}
storeIt() {
- const cnt = pInt(this.CountRec);
+ FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
- let limit = pInt(Settings.app('folderSpecLimit'));
- limit = 100 < limit ? 100 : 10 > limit ? 10 : limit;
-
- FolderUserStore.displaySpecSetting(0 >= cnt || limit < cnt);
+ if (!(
+ SettingsGet('SentFolder') +
+ SettingsGet('DraftsFolder') +
+ SettingsGet('SpamFolder') +
+ SettingsGet('TrashFolder') +
+ SettingsGet('ArchiveFolder')
+ )
+ ) {
+ FolderUserStore.saveSystemFolders(SystemFolders);
+ }
FolderUserStore.folderList(this);
- if (undefined !== this.Namespace) {
- FolderUserStore.namespace = this.Namespace;
- }
+ FolderUserStore.namespace = this.Namespace;
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
FolderUserStore.folderListOptimized(!!this.Optimized);
- FolderUserStore.sortSupported(!!this.IsSortSupported);
+ FolderUserStore.quotaUsage(this.quotaUsage);
+ FolderUserStore.quotaLimit(this.quotaLimit);
+ FolderUserStore.capabilities(this.Capabilities);
- let update = false;
+ FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
+ FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
+ FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
+ FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
+ FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
- if (
- this.SystemFolders &&
- !('' +
- SettingsGet('SentFolder') +
- SettingsGet('DraftFolder') +
- SettingsGet('SpamFolder') +
- SettingsGet('TrashFolder') +
- SettingsGet('ArchiveFolder'))
- ) {
- Settings.set('SentFolder', this.SystemFolders[ServerFolderType.SENT] || null);
- Settings.set('DraftFolder', this.SystemFolders[ServerFolderType.DRAFTS] || null);
- Settings.set('SpamFolder', this.SystemFolders[ServerFolderType.JUNK] || null);
- Settings.set('TrashFolder', this.SystemFolders[ServerFolderType.TRASH] || null);
- Settings.set('ArchiveFolder', this.SystemFolders[ServerFolderType.ALL] || null);
-
- update = true;
- }
-
- FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder')));
- FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder')));
- FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder')));
- FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder')));
- FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder')));
-
- if (update) {
- rl.app.Remote.saveSystemFolders(()=>{}, {
- SentFolder: FolderUserStore.sentFolder(),
- DraftFolder: FolderUserStore.draftFolder(),
- SpamFolder: FolderUserStore.spamFolder(),
- TrashFolder: FolderUserStore.trashFolder(),
- ArchiveFolder: FolderUserStore.archiveFolder()
- });
- }
-
- Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
+// FolderUserStore.folderList.valueHasMutated();
}
}
-function getSystemFolderName(type, def)
-{
- switch (type) {
- case FolderType.Inbox:
- return i18n('FOLDER_LIST/INBOX_NAME');
- case FolderType.SentItems:
- return i18n('FOLDER_LIST/SENT_NAME');
- case FolderType.Draft:
- return i18n('FOLDER_LIST/DRAFTS_NAME');
- case FolderType.Spam:
- return i18n('GLOBAL/SPAM');
- case FolderType.Trash:
- return i18n('FOLDER_LIST/TRASH_NAME');
- case FolderType.Archive:
- return i18n('FOLDER_LIST/ARCHIVE_NAME');
- // no default
- }
- return def;
-}
-
export class FolderModel extends AbstractModel {
constructor() {
super();
this.fullName = '';
- this.fullNameRaw = '';
- this.fullNameHash = '';
this.delimiter = '';
- this.namespace = '';
this.deep = 0;
this.expires = 0;
+ this.metadata = {};
- this.selectable = false;
this.exists = true;
+// this.hash = '';
+// this.uidNext = 0;
+
this.addObservables({
name: '',
type: FolderType.User,
+ selectable: false,
focused: false,
selected: false,
edited: false,
subscribed: true,
- checkable: false,
- deleteAccess: false,
+ checkable: false, // Check for new messages
+ askDelete: false,
nameForEdit: '',
+ errorMsg: '',
privateMessageCountAll: 0,
privateMessageCountUnread: 0,
- collapsedPrivate: true
+ kolabType: null,
+
+ collapsed: true
+ });
+
+ this.addSubscribables({
+ kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue
});
this.subFolders = ko.observableArray(new FolderCollectionModel);
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
@@ -223,12 +291,22 @@ export class FolderModel extends AbstractModel {
static reviveFromJson(json) {
const folder = super.reviveFromJson(json);
if (folder) {
- folder.deep = json.FullNameRaw.split(folder.delimiter).length - 1;
+ const path = folder.fullName.split(folder.delimiter),
+ type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
+ || folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
+ || ''
+ ).split('.')[0];
- folder.messageCountAll = ko.computed({
+ folder.deep = path.length - 1;
+ path.pop();
+ folder.parentName = path.join(folder.delimiter);
+
+ type && 'mail' != type && folder.kolabType(type);
+
+ folder.messageCountAll = koComputable({
read: folder.privateMessageCountAll,
write: (iValue) => {
- if (isPosNumeric(iValue, true)) {
+ if (isPosNumeric(iValue)) {
folder.privateMessageCountAll(iValue);
} else {
folder.privateMessageCountAll.valueHasMutated();
@@ -237,10 +315,10 @@ export class FolderModel extends AbstractModel {
})
.extend({ notify: 'always' });
- folder.messageCountUnread = ko.computed({
+ folder.messageCountUnread = koComputable({
read: folder.privateMessageCountUnread,
write: (value) => {
- if (isPosNumeric(value, true)) {
+ if (isPosNumeric(value)) {
folder.privateMessageCountUnread(value);
} else {
folder.privateMessageCountUnread.valueHasMutated();
@@ -253,61 +331,68 @@ export class FolderModel extends AbstractModel {
isInbox: () => FolderType.Inbox === folder.type(),
- hasSubscribedSubfolders:
- () =>
- !!folder.subFolders.find(
- oFolder => (oFolder.subscribed() || oFolder.hasSubscribedSubfolders()) && !oFolder.isSystemFolder()
- ),
+ isFlagged: () => FolderUserStore.currentFolder() === folder
+ && MessagelistUserStore.listSearch().includes('flagged'),
- canBeEdited: () => FolderType.User === folder.type() && folder.exists && folder.selectable,
+ hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
+ hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
+ oFolder => {
+ const subscribed = oFolder.hasSubscriptions();
+ return !oFolder.isSystemFolder() && subscribed;
+ }
+ ),
+
+ canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
+
+ isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
+
+ canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
+
+ canBeDeleted: () => folder.canBeSelected() && folder.exists,
+
+ canBeSubscribed: () => folder.selectable()
+ && !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
+
+ /**
+ * Folder is visible when:
+ * - hasVisibleSubfolders()
+ * Or when all below conditions are true:
+ * - selectable()
+ * - subscribed() OR hideUnsubscribed = false
+ * - FolderType.User
+ * - not kolabType()
+ */
visible: () => {
- const isSubscribed = folder.subscribed(),
- isSubFolders = folder.hasSubscribedSubfolders();
-
- return isSubscribed || (isSubFolders && (!folder.exists || !folder.selectable));
+ const selectable = folder.canBeSelected(),
+ visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
+ return folder.hasVisibleSubfolders() | visible;
},
- isSystemFolder: () => FolderType.User !== folder.type(),
-
- hidden: () => {
- const isSystem = folder.isSystemFolder(),
- isSubFolders = folder.hasSubscribedSubfolders();
-
- return (isSystem && !isSubFolders) || (!folder.selectable && !isSubFolders);
- },
+ hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
printableUnreadCount: () => {
const count = folder.messageCountAll(),
unread = folder.messageCountUnread(),
type = folder.type();
- if (0 < count) {
- if (FolderType.Draft === type) {
- return '' + count;
+ if (count) {
+ if (FolderType.Drafts === type) {
+ return count;
}
if (
- 0 < unread &&
+ unread &&
FolderType.Trash !== type &&
FolderType.Archive !== type &&
- FolderType.SentItems !== type
+ FolderType.Sent !== type
) {
- return '' + unread;
+ return unread;
}
}
- return '';
+ return null;
},
- canBeDeleted: () => !folder.isSystemFolder() && !folder.subFolders.length,
-
- canBeSubscribed: () => !folder.isSystemFolder()
- && SettingsUserStore.hideUnsubscribed()
- && folder.selectable
- && Settings.app('useImapSubscribe'),
-
- canBeSelected: () => !folder.isSystemFolder() && folder.selectable,
-
localName: () => {
let name = folder.name();
if (folder.isSystemFolder()) {
@@ -320,28 +405,22 @@ export class FolderModel extends AbstractModel {
manageFolderSystemName: () => {
if (folder.isSystemFolder()) {
translatorTrigger();
- let suffix = getSystemFolderName(folder.type(), '');
+ let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
return '(' + suffix + ')';
}
}
-
return '';
},
- collapsed: {
- read: () => !folder.hidden() && folder.collapsedPrivate(),
- write: (value) => {
- folder.collapsedPrivate(value);
- }
- },
-
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
hasSubscribedUnreadMessagesSubfolders: () =>
- !!folder.subFolders.find(
- folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
- )
+ !!folder.subFolders().find(
+ folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
+ )
+
+// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
});
folder.addSubscribables({
@@ -351,7 +430,7 @@ export class FolderModel extends AbstractModel {
messageCountUnread: unread => {
if (FolderType.Inbox === folder.type()) {
- dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
+ fireEvent('mailbox.inbox-unread-count', unread);
}
}
});
@@ -363,16 +442,9 @@ export class FolderModel extends AbstractModel {
* @returns {string}
*/
collapsedCss() {
- return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
+ return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
: 'icon-none'
);
}
-
- /**
- * @returns {string}
- */
- printableFullName() {
- return this.fullName.split(this.delimiter).join(' / ');
- }
}
diff --git a/dev/Model/Identity.js b/dev/Model/Identity.js
index 3fa72b9f4..bb3f4379f 100644
--- a/dev/Model/Identity.js
+++ b/dev/Model/Identity.js
@@ -1,4 +1,4 @@
-import ko from 'ko';
+import { koComputable } from 'External/ko';
import { AbstractModel } from 'Knoin/AbstractModel';
@@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
signature: '',
signatureInsertBefore: false,
- deleteAccess: false
+ askDelete: false
});
- this.canBeDeleted = ko.computed(() => !!this.id());
+ this.canBeDeleted = koComputable(() => !!this.id());
}
/**
@@ -34,6 +34,6 @@ export class IdentityModel extends AbstractModel {
const name = this.name(),
email = this.email();
- return name ? name + ' (' + email + ')' : email;
+ return name ? name + ' <' + email + '>' : email;
}
}
diff --git a/dev/Model/Message.js b/dev/Model/Message.js
index a5b4b0dd1..094599373 100644
--- a/dev/Model/Message.js
+++ b/dev/Model/Message.js
@@ -3,12 +3,13 @@ import ko from 'ko';
import { MessagePriority } from 'Common/EnumsUser';
import { i18n } from 'Common/Translator';
-import { encodeHtml } from 'Common/Html';
-import { isArray, isNonEmptyArray } from 'Common/Utils';
-
+import { doc } from 'Common/Globals';
+import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
+import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw } from 'Common/Links';
import { FolderUserStore } from 'Stores/User/Folder';
+import { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo } from 'Common/File';
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
@@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html';
const
- SignedVerifyStatus = {
- UnknownPublicKeys: -4,
- UnknownPrivateKey: -3,
- Unverified: -2,
- Error: -1,
- None: 0,
- Success: 1
+ // eslint-disable-next-line max-len
+ url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
+ // eslint-disable-next-line max-len
+ email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
+
+ hcont = Element.fromHTML('
'),
+ getRealHeight = el => {
+ hcont.innerHTML = el.outerHTML;
+ const result = hcont.clientHeight;
+ hcont.innerHTML = '';
+ return result;
},
replyHelper = (emails, unic, localEmails) => {
@@ -36,6 +41,8 @@ const
});
};
+doc.body.append(hcont);
+
export class MessageModel extends AbstractModel {
constructor() {
super();
@@ -44,38 +51,34 @@ export class MessageModel extends AbstractModel {
this.addObservables({
subject: '',
+ plain: '',
+ html: '',
size: 0,
spamScore: 0,
spamResult: '',
isSpam: false,
+ hasVirus: null, // or boolean when scanned
dateTimeStampInUTC: 0,
priority: MessagePriority.Normal,
senderEmailsString: '',
senderClearEmailsString: '',
- newForAnimation: false,
-
deleted: false,
- isDeleted: false,
- isUnseen: false,
- isFlagged: false,
- isAnswered: false,
- isForwarded: false,
- isReadReceipt: false,
focused: false,
selected: false,
checked: false,
- hasAttachments: false,
isHtml: false,
hasImages: false,
+ hasExternals: false,
- isPgpSigned: false,
- isPgpEncrypted: false,
- pgpSignedVerifyStatus: SignedVerifyStatus.None,
- pgpSignedVerifyUser: '',
+ pgpSigned: null,
+ pgpVerified: null,
+
+ pgpEncrypted: null,
+ pgpDecrypted: false,
readReceipt: '',
@@ -84,22 +87,32 @@ export class MessageModel extends AbstractModel {
});
this.attachments = ko.observableArray(new AttachmentCollectionModel);
- this.attachmentsSpecData = ko.observableArray();
this.threads = ko.observableArray();
+ this.unsubsribeLinks = ko.observableArray();
+ this.flags = ko.observableArray();
this.addComputables({
- attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []),
+ attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
threadsLen: () => this.threads().length,
isImportant: () => MessagePriority.High === this.priority(),
+ hasAttachments: () => this.attachments().hasVisible(),
+
+ isDeleted: () => this.flags().includes('\\deleted'),
+ isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
+ isFlagged: () => this.flags().includes('\\flagged'),
+ isAnswered: () => this.flags().includes('\\answered'),
+ isForwarded: () => this.flags().includes('$forwarded'),
+ isReadReceipt: () => this.flags().includes('$mdnsent')
+// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
+// isPhishing: () => this.flags().includes('$phishing')
});
}
_reset() {
this.folder = '';
- this.uid = '';
+ this.uid = 0;
this.hash = '';
this.requestHash = '';
- this.externalProxy = false;
this.emails = [];
this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel;
@@ -107,7 +120,6 @@ export class MessageModel extends AbstractModel {
this.bcc = new EmailCollectionModel;
this.replyTo = new EmailCollectionModel;
this.deliveredTo = new EmailCollectionModel;
- this.unsubsribeLinks = [];
this.body = null;
this.draftInfo = [];
this.messageId = '';
@@ -118,49 +130,50 @@ export class MessageModel extends AbstractModel {
clear() {
this._reset();
this.subject('');
+ this.html('');
+ this.plain('');
this.size(0);
this.spamScore(0);
this.spamResult('');
this.isSpam(false);
+ this.hasVirus(null);
this.dateTimeStampInUTC(0);
this.priority(MessagePriority.Normal);
this.senderEmailsString('');
this.senderClearEmailsString('');
- this.newForAnimation(false);
-
this.deleted(false);
- this.isDeleted(false);
- this.isUnseen(false);
- this.isFlagged(false);
- this.isAnswered(false);
- this.isForwarded(false);
- this.isReadReceipt(false);
this.selected(false);
this.checked(false);
- this.hasAttachments(false);
- this.attachmentsSpecData([]);
this.isHtml(false);
this.hasImages(false);
+ this.hasExternals(false);
this.attachments(new AttachmentCollectionModel);
- this.isPgpSigned(false);
- this.isPgpEncrypted(false);
- this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
- this.pgpSignedVerifyUser('');
+ this.pgpSigned(null);
+ this.pgpVerified(null);
+
+ this.pgpEncrypted(null);
+ this.pgpDecrypted(false);
this.priority(MessagePriority.Normal);
this.readReceipt('');
this.threads([]);
+ this.unsubsribeLinks([]);
this.hasUnseenSubMessage(false);
this.hasFlaggedSubMessage(false);
}
+ spamStatus() {
+ let spam = this.spamResult();
+ return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
+ }
+
/**
* @param {Array} properties
* @returns {Array}
@@ -179,7 +192,7 @@ export class MessageModel extends AbstractModel {
}
computeSenderEmail() {
- const list = [FolderUserStore.sentFolder(), FolderUserStore.draftFolder()].includes(this.folder) ? 'to' : 'from';
+ const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from';
this.senderEmailsString(this[list].toString(true));
this.senderClearEmailsString(this[list].toStringClear());
}
@@ -189,13 +202,12 @@ export class MessageModel extends AbstractModel {
* @returns {boolean}
*/
revivePropertiesFromJson(json) {
- if ('Priority' in json) {
- let p = parseInt(json.Priority, 10);
- json.Priority = MessagePriority.High == p || MessagePriority.Low == p ? p : MessagePriority.Normal;
+ if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
+ json.Priority = MessagePriority.Normal;
}
if (super.revivePropertiesFromJson(json)) {
-// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : [];
-// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs));
+// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
+// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
this.computeSenderEmail();
}
@@ -205,14 +217,14 @@ export class MessageModel extends AbstractModel {
* @returns {boolean}
*/
hasUnsubsribeLinks() {
- return this.unsubsribeLinks && this.unsubsribeLinks.length;
+ return this.unsubsribeLinks().length;
}
/**
* @returns {string}
*/
getFirstUnsubsribeLink() {
- return this.unsubsribeLinks && this.unsubsribeLinks.length ? this.unsubsribeLinks[0] || '' : '';
+ return this.unsubsribeLinks()[0] || '';
}
/**
@@ -229,7 +241,7 @@ export class MessageModel extends AbstractModel {
*/
fromDkimData() {
let result = ['none', ''];
- if (isNonEmptyArray(this.from) && 1 === this.from.length && this.from[0] && this.from[0].dkimStatus) {
+ if (1 === arrayLength(this.from) && this.from[0] && this.from[0].dkimStatus) {
result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
}
@@ -277,7 +289,7 @@ export class MessageModel extends AbstractModel {
*/
lineAsCss() {
let classes = [];
- Object.entries({
+ forEachObjectEntry({
deleted: this.deleted(),
'deleted-mark': this.isDeleted(),
selected: this.selected(),
@@ -288,13 +300,12 @@ export class MessageModel extends AbstractModel {
forwarded: this.isForwarded(),
focused: this.focused(),
important: this.isImportant(),
- withAttachments: this.hasAttachments(),
- new: this.newForAnimation(),
+ withAttachments: !!this.attachments().length,
emptySubject: !this.subject(),
// hasChildrenMessage: 1 < this.threadsLen(),
hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
- }).forEach(([key, value]) => value && classes.push(key));
+ }, (key, value) => value && classes.push(key));
return classes.join(' ');
}
@@ -367,35 +378,138 @@ export class MessageModel extends AbstractModel {
return [toResult, ccResult];
}
- /**
- * @param {boolean=} print = false
- */
+ viewHtml() {
+ const body = this.body;
+ if (body && this.html()) {
+ const contentLocationUrls = {},
+ oAttachments = this.attachments();
+
+ // Get contentLocationUrls
+ oAttachments.forEach(oAttachment => {
+ if (oAttachment.cid && oAttachment.contentLocation) {
+ contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation;
+ }
+ });
+
+ let result = cleanHtml(this.html(), contentLocationUrls, SettingsUserStore.removeColors());
+ this.hasExternals(result.hasExternals);
+// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length;
+ this.hasImages(body.rlHasImages = !!result.hasExternals);
+
+ // Hide valid inline attachments in message view 'attachments' section
+ oAttachments.forEach(oAttachment => {
+ let cid = oAttachment.contentId(),
+ found = result.foundCIDs.includes(cid);
+ oAttachment.isInline(found);
+ oAttachment.isLinked(found || result.foundContentLocationUrls.includes(oAttachment.contentLocation));
+ });
+
+ body.innerHTML = result.html;
+
+ body.classList.toggle('html', 1);
+ body.classList.toggle('plain', 0);
+
+ // showInternalImages
+ const findAttachmentByCid = cid => this.attachments().findByCid(cid);
+ body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
+ const data = el.dataset;
+ if (data.xSrcCid) {
+ const attachment = findAttachmentByCid(data.xSrcCid);
+ if (attachment && attachment.download) {
+ el.src = attachment.linkPreview();
+ }
+ } else if (data.xSrcLocation) {
+ const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
+ || findAttachmentByCid(data.xSrcLocation);
+ if (attachment && attachment.download) {
+ el.loading = 'lazy';
+ el.src = attachment.linkPreview();
+ }
+ } else if (data.xStyleCid) {
+ forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => {
+ const attachment = findAttachmentByCid(cid);
+ if (attachment && attachment.linkPreview && name) {
+ el.style[name] = "url('" + attachment.linkPreview() + "')";
+ }
+ });
+ }
+ });
+
+ if (SettingsUserStore.showImages()) {
+ this.showExternalImages();
+ }
+
+ this.isHtml(true);
+ this.initView();
+ return true;
+ }
+ }
+
+ viewPlain() {
+ const body = this.body;
+ if (body && this.plain()) {
+ body.classList.toggle('html', 0);
+ body.classList.toggle('plain', 1);
+ body.innerHTML = plainToHtml(
+ this.plain()
+ .replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
+ .trim()
+ )
+ .replace(url, '$1$2 ')
+ .replace(email, '$1$2 ');
+ this.isHtml(false);
+ this.hasImages(false);
+ this.initView();
+ return true;
+ }
+ }
+
+ initView() {
+ // init BlockquoteSwitcher
+ this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
+ node.removeAttribute('style')
+ if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
+ let h = node.clientHeight || getRealHeight(node);
+ if (0 === h || 100 < h) {
+ const el = Element.fromHTML('••• ');
+ node.classList.add('rl-bq-switcher','hidden-bq');
+ node.before(el);
+ el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
+ }
+ }
+ });
+ }
+
viewPopupMessage(print) {
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.ccToLine(false),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
win = open(''),
- doc = win.document;
- doc.write(PreviewHTML
- .replace(/{{subject}}/g, encodeHtml(this.subject()))
- .replace('{{date}}', encodeHtml(m ? m.format('LLL') : ''))
- .replace('{{fromCreds}}', encodeHtml(this.fromToLine(false)))
- .replace('{{toCreds}}', encodeHtml(this.toToLine(false)))
- .replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO')))
- .replace('{{ccHide}}', ccLine ? '' : 'hidden=""')
- .replace('{{ccCreds}}', encodeHtml(ccLine))
- .replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC')))
- .replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
- .replace('{{html}}', this.bodyAsHTML())
+ sdoc = win.document;
+ let subject = encodeHtml(this.subject()),
+ mode = this.isHtml() ? 'div' : 'pre',
+ cc = ccLine ? `${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}
` : '',
+ style = getComputedStyle(doc.querySelector('.messageView')),
+ prop = property => style.getPropertyValue(property);
+ sdoc.write(PreviewHTML
+ .replace('', ''+subject)
+ // eslint-disable-next-line max-len
+ .replace(' ', `${subject} ${encodeHtml(m ? m.format('LLL') : '')} ${encodeHtml(this.fromToLine(false))}
${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine(false))}
${cc} <${mode}>${this.bodyAsHTML()}${mode}>`)
);
-
- doc.close();
+ sdoc.close();
if (print) {
setTimeout(() => win.print(), 100);
}
}
+ /**
+ * @param {boolean=} print = false
+ */
+ popupMessage() {
+ this.viewPopupMessage(false);
+ }
+
printMessage() {
this.viewPopupMessage(true);
}
@@ -411,70 +525,61 @@ export class MessageModel extends AbstractModel {
* @param {MessageModel} message
* @returns {MessageModel}
*/
- populateByMessageListItem(message) {
- if (message) {
- this.folder = message.folder;
- this.uid = message.uid;
- this.hash = message.hash;
- this.requestHash = message.requestHash;
- this.subject(message.subject());
-
- this.size(message.size());
- this.spamScore(message.spamScore());
- this.spamResult(message.spamResult());
- this.isSpam(message.isSpam());
- this.dateTimeStampInUTC(message.dateTimeStampInUTC());
- this.priority(message.priority());
-
- this.externalProxy = message.externalProxy;
-
- this.emails = message.emails;
-
- this.from = message.from;
- this.to = message.to;
- this.cc = message.cc;
- this.bcc = message.bcc;
- this.replyTo = message.replyTo;
- this.deliveredTo = message.deliveredTo;
- this.unsubsribeLinks = message.unsubsribeLinks;
-
- this.isUnseen(message.isUnseen());
- this.isFlagged(message.isFlagged());
- this.isAnswered(message.isAnswered());
- this.isForwarded(message.isForwarded());
- this.isReadReceipt(message.isReadReceipt());
- this.isDeleted(message.isDeleted());
-
- this.priority(message.priority());
-
- this.selected(message.selected());
- this.checked(message.checked());
- this.hasAttachments(message.hasAttachments());
- this.attachmentsSpecData(message.attachmentsSpecData());
- }
-
- this.body = null;
-
- this.draftInfo = [];
- this.messageId = '';
- this.inReplyTo = '';
- this.references = '';
+ static fromMessageListItem(message) {
+ let self = new MessageModel();
if (message) {
- this.threads(message.threads());
+ self.folder = message.folder;
+ self.uid = message.uid;
+ self.hash = message.hash;
+ self.requestHash = message.requestHash;
+ self.subject(message.subject());
+ self.plain(message.plain());
+ self.html(message.html());
+
+ self.size(message.size());
+ self.spamScore(message.spamScore());
+ self.spamResult(message.spamResult());
+ self.isSpam(message.isSpam());
+ self.hasVirus(message.hasVirus());
+ self.dateTimeStampInUTC(message.dateTimeStampInUTC());
+ self.priority(message.priority());
+
+ self.hasExternals(message.hasExternals());
+
+ self.emails = message.emails;
+
+ self.from = message.from;
+ self.to = message.to;
+ self.cc = message.cc;
+ self.bcc = message.bcc;
+ self.replyTo = message.replyTo;
+ self.deliveredTo = message.deliveredTo;
+ self.unsubsribeLinks(message.unsubsribeLinks);
+
+ self.flags(message.flags());
+
+ self.priority(message.priority());
+
+ self.selected(message.selected());
+ self.checked(message.checked());
+ self.attachments(message.attachments());
+
+ self.threads(message.threads());
}
- this.computeSenderEmail();
+ self.computeSenderEmail();
- return this;
+ return self;
}
showExternalImages() {
- if (this.body && this.body.rlHasImages) {
+ const body = this.body;
+ if (body && this.hasImages()) {
this.hasImages(false);
- this.body.rlHasImages = false;
+ body.rlHasImages = false;
- let body = this.body, attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src';
+ let attr = 'data-x-src';
body.querySelectorAll('[' + attr + ']').forEach(node => {
if (node.matches('img')) {
node.loading = 'lazy';
@@ -482,81 +587,28 @@ export class MessageModel extends AbstractModel {
node.src = node.getAttribute(attr);
});
- attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
- body.querySelectorAll('[' + attr + ']').forEach(node => {
- node.setAttribute('style', ((node.getAttribute('style')||'')
- + ';' + node.getAttribute(attr))
- .replace(/^[;\s]+/,''));
+ body.querySelectorAll('[data-x-style-url]').forEach(node => {
+ forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
});
}
}
- showInternalImages() {
- const body = this.body;
- if (body && !body.rlInitInternalImages) {
- const findAttachmentByCid = cid => this.attachments().findByCid(cid);
-
- body.rlInitInternalImages = true;
-
- body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
- const data = el.dataset;
- if (data.xSrcCid) {
- const attachment = findAttachmentByCid(data.xSrcCid);
- if (attachment && attachment.download) {
- el.src = attachment.linkPreview();
- }
- } else if (data.xSrcLocation) {
- const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
- || findAttachmentByCid(data.xSrcLocation);
- if (attachment && attachment.download) {
- el.loading = 'lazy';
- el.src = attachment.linkPreview();
- }
- } else if (data.xStyleCid) {
- const name = data.xStyleCidName,
- attachment = findAttachmentByCid(data.xStyleCid);
- if (attachment && attachment.linkPreview && name) {
- el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');"
- + (el.getAttribute('style') || ''));
- }
- }
- });
- }
- }
-
- storeDataInDom() {
- if (this.body) {
- this.body.rlIsHtml = !!this.isHtml();
- this.body.rlHasImages = !!this.hasImages();
- }
- }
-
- fetchDataFromDom() {
- if (this.body) {
- this.isHtml(!!this.body.rlIsHtml);
- this.hasImages(!!this.body.rlHasImages);
- }
- }
-
/**
* @returns {string}
*/
bodyAsHTML() {
- if (this.body) {
- let clone = this.body.cloneNode(true),
- attr = 'data-html-editor-font-wrapper';
+// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
+ if (this.body && !this.body.querySelector('iframe')) {
+ let clone = this.body.cloneNode(true);
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
node => node.classList.remove('rl-bq-switcher','hidden-bq')
);
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
node => node.remove()
);
- clone.querySelectorAll('['+attr+']').forEach(
- node => node.removeAttribute(attr)
- );
return clone.innerHTML;
}
- return '';
+ return this.html() || plainToHtml(this.plain());
}
/**
@@ -573,4 +625,5 @@ export class MessageModel extends AbstractModel {
this.isReadReceipt()
].join(',');
}
+
}
diff --git a/dev/Model/MessageCollection.js b/dev/Model/MessageCollection.js
index f4ad0e70e..425284866 100644
--- a/dev/Model/MessageCollection.js
+++ b/dev/Model/MessageCollection.js
@@ -39,7 +39,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
if (message) {
if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) {
++newCount;
- message.newForAnimation(true);
}
message.deleted(false);
diff --git a/dev/Model/OpenPgpKey.js b/dev/Model/OpenPgpKey.js
deleted file mode 100644
index 1ae0a623f..000000000
--- a/dev/Model/OpenPgpKey.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import ko from 'ko';
-
-import { isNonEmptyArray } from 'Common/Utils';
-import { AbstractModel } from 'Knoin/AbstractModel';
-import { PgpUserStore } from 'Stores/User/Pgp';
-
-export class OpenPgpKeyModel extends AbstractModel {
- /**
- * @param {string} index
- * @param {string} guID
- * @param {string} ID
- * @param {array} IDs
- * @param {array} userIDs
- * @param {array} emails
- * @param {boolean} isPrivate
- * @param {string} armor
- * @param {string} userID
- */
- constructor(index, guID, ID, IDs, userIDs, emails, isPrivate, armor, userID) {
- super();
-
- this.index = index;
- this.id = ID;
- this.ids = isNonEmptyArray(IDs) ? IDs : [ID];
- this.guid = guID;
- this.user = '';
- this.users = userIDs;
- this.email = '';
- this.emails = emails;
- this.armor = armor;
- this.isPrivate = !!isPrivate;
-
- this.selectUser(userID);
-
- this.deleteAccess = ko.observable(false);
- }
-
- getNativeKey() {
- let key = null;
- try {
- key = PgpUserStore.openpgp.key.readArmored(this.armor);
- if (key && !key.err && key.keys && key.keys[0]) {
- return key;
- }
- } catch (e) {
- console.log(e);
- }
-
- return null;
- }
-
- getNativeKeys() {
- const key = this.getNativeKey();
- return key && key.keys ? key.keys : null;
- }
-
- select(pattern, property) {
- if (this[property]) {
- const index = this[property].indexOf(pattern);
- if (-1 !== index) {
- this.user = this.users[index];
- this.email = this.emails[index];
- }
- }
- }
-
- selectUser(user) {
- this.select(user, 'users');
- }
-
- selectEmail(email) {
- this.select(email, 'emails');
- }
-}
diff --git a/dev/Model/Template.js b/dev/Model/Template.js
deleted file mode 100644
index 942749fdb..000000000
--- a/dev/Model/Template.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import ko from 'ko';
-
-import { AbstractModel } from 'Knoin/AbstractModel';
-
-export class TemplateModel extends AbstractModel {
- /**
- * @param {string} id
- * @param {string} name
- * @param {string} body
- */
- constructor(id = '', name = '', body = '') {
- super();
-
- this.id = id;
- this.name = name;
- this.body = body;
- this.populated = true;
-
- this.deleteAccess = ko.observable(false);
- }
-
-// static reviveFromJson(json) {}
-}
diff --git a/dev/Remote/AbstractFetch.js b/dev/Remote/AbstractFetch.js
index 57afb5257..ea1ca1644 100644
--- a/dev/Remote/AbstractFetch.js
+++ b/dev/Remote/AbstractFetch.js
@@ -1,41 +1,30 @@
import { Notification } from 'Common/Enums';
-import { Settings } from 'Common/Globals';
import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links';
import { runHook } from 'Common/Plugins';
+import { getNotification } from 'Common/Translator';
-let iJsonErrorCount = 0,
- iTokenErrorCount = 0;
+let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + add,
-updateToken = data => {
- if (data.UpdateToken) {
- rl.hash.set();
- Settings.set('AuthAccountHash', data.UpdateToken);
- }
-},
-
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
- if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) {
+ if (Notification.InvalidToken === err) {
+ alert(getNotification(err));
rl.logoutReload();
- } else {
- if ([
- Notification.AuthError,
- Notification.ConnectionError,
- Notification.DomainNotAllowed,
- Notification.AccountNotAllowed,
- Notification.MailServerError,
- Notification.UnknownNotification,
- Notification.UnknownError
- ].includes(err)
- ) {
- ++iJsonErrorCount;
- }
- if (data.ClearAuth || data.Logout || 7 < iJsonErrorCount) {
- rl.hash.clear();
- data.ClearAuth || rl.logoutReload();
+ } else if ([
+ Notification.AuthError,
+ Notification.ConnectionError,
+ Notification.DomainNotAllowed,
+ Notification.AccountNotAllowed,
+ Notification.MailServerError,
+ Notification.UnknownNotification,
+ Notification.UnknownError
+ ].includes(err)
+ ) {
+ if (7 < ++iJsonErrorCount) {
+ rl.logoutReload();
}
}
},
@@ -57,7 +46,11 @@ abort = (sAction, bClearOnly) => {
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
sGetAdd = pString(sGetAdd);
params = params || {};
- params.Action = action;
+ if (params instanceof FormData) {
+ params.set('Action', action);
+ } else {
+ params.Action = action;
+ }
let init = {};
if (window.AbortController) {
abort(action);
@@ -69,6 +62,14 @@ fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
return rl.fetchJSON(getURL(sGetAdd), init, sGetAdd ? null : params).then(jsonCallback);
};
+class FetchError extends Error
+{
+ constructor(code, message) {
+ super(message);
+ this.code = code || Notification.JsonFalse;
+ }
+}
+
export class AbstractFetchRemote
{
abort(sAction, bClearOnly) {
@@ -76,6 +77,45 @@ export class AbstractFetchRemote
return this;
}
+ /**
+ * Allows quicker visual responses to the user.
+ * Can be used to stream lines of json encoded data, but does not work on all servers.
+ * Apache needs 'flushpackets' like in
+ */
+ streamPerLine(fCallback, sGetAdd) {
+ rl.fetch(getURL(sGetAdd))
+ .then(response => response.body)
+ .then(body => {
+ // Firefox TextDecoderStream is not defined
+ // const reader = body.pipeThrough(new TextDecoderStream()).getReader();
+ const reader = body.getReader(),
+ re = /\r\n|\n|\r/gm,
+ utf8decoder = new TextDecoder();
+ let buffer = '';
+ function processText({ done, value }) {
+ buffer += value ? utf8decoder.decode(value, {stream: true}) : '';
+ for (;;) {
+ let result = re.exec(buffer);
+ if (!result) {
+ if (done) {
+ break;
+ }
+ reader.read().then(processText);
+ return;
+ }
+ fCallback(buffer.slice(0, result.index));
+ buffer = buffer.slice(result.index + 1);
+ re.lastIndex = 0;
+ }
+ if (buffer.length) {
+ // last line didn't end in a newline char
+ fCallback(buffer);
+ }
+ }
+ reader.read().then(processText);
+ })
+ }
+
/**
* @param {?Function} fCallback
* @param {string} sAction
@@ -84,7 +124,7 @@ export class AbstractFetchRemote
* @param {string=} sGetAdd = ''
* @param {Array=} aAbortActions = []
*/
- defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) {
+ request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
params = params || {};
const start = Date.now();
@@ -100,12 +140,8 @@ export class AbstractFetchRemote
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
let cached = false;
- if (data) {
- if (data.Time) {
- cached = pInt(data.Time) > Date.now() - start;
- }
-
- updateToken(data);
+ if (data && data.Time) {
+ cached = pInt(data.Time) > Date.now() - start;
}
let iError = 0;
@@ -123,7 +159,7 @@ export class AbstractFetchRemote
}
*/
if (data.Result) {
- iJsonErrorCount = iTokenErrorCount = 0;
+ iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError
@@ -153,32 +189,11 @@ export class AbstractFetchRemote
});
}
- /**
- * @param {?Function} fCallback
- */
- noop(fCallback) {
- this.defaultRequest(fCallback, 'Noop');
- }
-
/**
* @param {?Function} fCallback
*/
getPublicKey(fCallback) {
- this.defaultRequest(fCallback, 'GetPublicKey');
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sVersion
- */
- jsVersion(fCallback, sVersion) {
- this.defaultRequest(fCallback, 'Version', {
- Version: sVersion
- });
- }
-
- fastResolve(mData) {
- return Promise.resolve(mData);
+ this.request('GetPublicKey', fCallback);
}
setTrigger(trigger, value) {
@@ -190,17 +205,15 @@ export class AbstractFetchRemote
}
}
- postRequest(action, fTrigger, params, timeOut) {
+ post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true);
return fetchJSON(action, '', params, pInt(timeOut, 30000),
data => {
abort(action, true);
if (!data) {
- return Promise.reject(Notification.JsonParse);
+ return Promise.reject(new FetchError(Notification.JsonParse));
}
-
- updateToken(data);
/*
let isCached = false, type = '';
if (data && data.Time) {
@@ -223,8 +236,10 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) {
checkResponseError(data);
- const err = data ? data.ErrorCode : 0;
- return Promise.reject(err || Notification.JsonFalse);
+ return Promise.reject(new FetchError(
+ data ? data.ErrorCode : 0,
+ data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
+ ));
}
return data;
diff --git a/dev/Remote/Admin/Fetch.js b/dev/Remote/Admin/Fetch.js
index 5d0922ac6..fa37da351 100644
--- a/dev/Remote/Admin/Fetch.js
+++ b/dev/Remote/Admin/Fetch.js
@@ -1,212 +1,14 @@
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
class RemoteAdminFetch extends AbstractFetchRemote {
- /**
- * @param {?Function} fCallback
- * @param {string} sLogin
- * @param {string} sPassword
- */
- adminLogin(fCallback, sLogin, sPassword) {
- this.defaultRequest(fCallback, 'AdminLogin', {
- Login: sLogin,
- Password: sPassword
- });
- }
/**
+ * @param {string} key
+ * @param {?scalar} value
* @param {?Function} fCallback
*/
- adminLogout(fCallback) {
- this.defaultRequest(fCallback, 'AdminLogout');
- }
-
- /**
- * @param {?Function} fCallback
- * @param {?} oData
- */
- saveAdminConfig(fCallback, oData) {
- this.defaultRequest(fCallback, 'AdminSettingsUpdate', oData);
- }
-
- /**
- * @param {?Function} fCallback
- * @param {boolean=} bIncludeAliases = true
- */
- domainList(fCallback, bIncludeAliases = true) {
- this.defaultRequest(fCallback, 'AdminDomainList', {
- IncludeAliases: bIncludeAliases ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- */
- pluginList(fCallback) {
- this.defaultRequest(fCallback, 'AdminPluginList');
- }
-
- /**
- * @param {?Function} fCallback
- */
- packagesList(fCallback) {
- this.defaultRequest(fCallback, 'AdminPackagesList');
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Object} oPackage
- */
- packageInstall(fCallback, oPackage) {
- this.defaultRequest(
- fCallback,
- 'AdminPackageInstall',
- {
- Id: oPackage.id,
- Type: oPackage.type,
- File: oPackage.file
- },
- 60000
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Object} oPackage
- */
- packageDelete(fCallback, oPackage) {
- this.defaultRequest(fCallback, 'AdminPackageDelete', {
- Id: oPackage.id
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sName
- */
- domain(fCallback, sName) {
- this.defaultRequest(fCallback, 'AdminDomainLoad', {
- Name: sName
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sName
- */
- plugin(fCallback, sName) {
- this.defaultRequest(fCallback, 'AdminPluginLoad', {
- Name: sName
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sName
- */
- domainDelete(fCallback, sName) {
- this.defaultRequest(fCallback, 'AdminDomainDelete', {
- Name: sName
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sName
- * @param {boolean} bDisabled
- */
- domainDisable(fCallback, sName, bDisabled) {
- this.defaultRequest(fCallback, 'AdminDomainDisable', {
- Name: sName,
- Disabled: bDisabled ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Object} oConfig
- */
- pluginSettingsUpdate(fCallback, oConfig) {
- this.defaultRequest(fCallback, 'AdminPluginSettingsUpdate', oConfig);
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sName
- * @param {boolean} bDisabled
- */
- pluginDisable(fCallback, sName, bDisabled) {
- this.defaultRequest(fCallback, 'AdminPluginDisable', {
- Name: sName,
- Disabled: bDisabled ? 1 : 0
- });
- }
-
- createDomainAlias(fCallback, sName, sAlias) {
- this.defaultRequest(fCallback, 'AdminDomainAliasSave', {
- Name: sName,
- Alias: sAlias
- });
- }
-
- createOrUpdateDomain(fCallback, oDomain) {
- this.defaultRequest(fCallback, 'AdminDomainSave', {
- Create: oDomain.edit() ? 0 : 1,
- Name: oDomain.name(),
-
- IncHost: oDomain.imapServer(),
- IncPort: oDomain.imapPort(),
- IncSecure: oDomain.imapSecure(),
- IncShortLogin: oDomain.imapShortLogin() ? 1 : 0,
-
- UseSieve: oDomain.useSieve() ? 1 : 0,
- SieveHost: oDomain.sieveServer(),
- SievePort: oDomain.sievePort(),
- SieveSecure: oDomain.sieveSecure(),
-
- OutHost: oDomain.smtpServer(),
- OutPort: oDomain.smtpPort(),
- OutSecure: oDomain.smtpSecure(),
- OutShortLogin: oDomain.smtpShortLogin() ? 1 : 0,
- OutAuth: oDomain.smtpAuth() ? 1 : 0,
- OutSetSender: oDomain.smtpSetSender() ? 1 : 0,
- OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0,
-
- WhiteList: oDomain.whiteList()
- });
- }
-
- testConnectionForDomain(fCallback, oDomain) {
- this.defaultRequest(fCallback, 'AdminDomainTest', {
- Name: oDomain.name(),
- IncHost: oDomain.imapServer(),
- IncPort: oDomain.imapPort(),
- IncSecure: oDomain.imapSecure(),
- UseSieve: oDomain.useSieve() ? 1 : 0,
- SieveHost: oDomain.sieveServer(),
- SievePort: oDomain.sievePort(),
- SieveSecure: oDomain.sieveSecure(),
- OutHost: oDomain.smtpServer(),
- OutPort: oDomain.smtpPort(),
- OutSecure: oDomain.smtpSecure(),
- OutAuth: oDomain.smtpAuth() ? 1 : 0,
- OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {?} oData
- */
- testContacts(fCallback, oData) {
- this.defaultRequest(fCallback, 'AdminContactsTest', oData);
- }
-
- /**
- * @param {?Function} fCallback
- * @param {?} oData
- */
- saveNewAdminPassword(fCallback, oData) {
- this.defaultRequest(fCallback, 'AdminPasswordUpdate', oData);
+ saveSetting(key, value, fCallback) {
+ this.request('AdminSettingsUpdate', fCallback, {[key]: value});
}
}
diff --git a/dev/Remote/User/Fetch.js b/dev/Remote/User/Fetch.js
index 9f272e60f..32366fd04 100644
--- a/dev/Remote/User/Fetch.js
+++ b/dev/Remote/User/Fetch.js
@@ -1,11 +1,9 @@
-import { isArray, isNonEmptyArray, pString, pInt } from 'Common/Utils';
+import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
import {
getFolderHash,
- getFolderInboxName,
getFolderUidNext,
- getFolderFromCacheList,
- MessageFlagsCache
+ getFolderFromCacheList
} from 'Common/Cache';
import { SettingsGet } from 'Common/Globals';
@@ -17,224 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
-import { FolderCollectionModel } from 'Model/FolderCollection';
-
-//const toUTF8 = window.TextEncoder
-// ? text => String.fromCharCode(...new TextEncoder().encode(text))
-// : text => unescape(encodeURIComponent(text)),
-const urlsafeArray = array => btoa(unescape(encodeURIComponent(array.join('\x00').replace(/\r\n/g, '\n'))))
- .replace('+', '-')
- .replace('/', '_')
- .replace('=', '');
-
class RemoteUserFetch extends AbstractFetchRemote {
- /**
- * @param {?Function} fCallback
- */
- folders(fCallback) {
- this.defaultRequest(
- fCallback,
- 'Folders',
- {
- SentFolder: SettingsGet('SentFolder'),
- DraftFolder: SettingsGet('DraftFolder'),
- SpamFolder: SettingsGet('SpamFolder'),
- TrashFolder: SettingsGet('TrashFolder'),
- ArchiveFolder: SettingsGet('ArchiveFolder')
- },
- null,
- '',
- ['Folders']
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sEmail
- * @param {string} sLogin
- * @param {string} sPassword
- * @param {boolean} bSignMe
- * @param {string=} sLanguage
- */
- login(fCallback, sEmail, sPassword, bSignMe, sLanguage) {
- this.defaultRequest(fCallback, 'Login', {
- Email: sEmail,
- Login: '',
- Password: sPassword,
- Language: sLanguage || '',
- SignMe: bSignMe ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- */
- contactsSync(fCallback) {
- this.defaultRequest(fCallback, 'ContactsSync', null, 200000);
- }
-
- /**
- * @param {?Function} fCallback
- * @param {boolean} bEnable
- * @param {string} sUrl
- * @param {string} sUser
- * @param {string} sPassword
- */
- saveContactsSyncData(fCallback, bEnable, sUrl, sUser, sPassword) {
- this.defaultRequest(fCallback, 'SaveContactsSyncData', {
- Enable: bEnable ? 1 : 0,
- Url: sUrl,
- User: sUser,
- Password: sPassword
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sEmail
- * @param {string} sPassword
- * @param {boolean=} bNew
- */
- accountSetup(fCallback, sEmail, sPassword, bNew = true) {
- this.defaultRequest(fCallback, 'AccountSetup', {
- Email: sEmail,
- Password: sPassword,
- New: bNew ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sEmailToDelete
- */
- accountDelete(fCallback, sEmailToDelete) {
- this.defaultRequest(fCallback, 'AccountDelete', {
- EmailToDelete: sEmailToDelete
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Array} aAccounts
- * @param {Array} aIdentities
- */
- accountsAndIdentitiesSortOrder(fCallback, aAccounts, aIdentities) {
- this.defaultRequest(fCallback, 'AccountsAndIdentitiesSortOrder', {
- Accounts: aAccounts,
- Identities: aIdentities
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sId
- * @param {string} sEmail
- * @param {string} sName
- * @param {string} sReplyTo
- * @param {string} sBcc
- * @param {string} sSignature
- * @param {boolean} bSignatureInsertBefore
- */
- identityUpdate(fCallback, sId, sEmail, sName, sReplyTo, sBcc, sSignature, bSignatureInsertBefore) {
- this.defaultRequest(fCallback, 'IdentityUpdate', {
- Id: sId,
- Email: sEmail,
- Name: sName,
- ReplyTo: sReplyTo,
- Bcc: sBcc,
- Signature: sSignature,
- SignatureInsertBefore: bSignatureInsertBefore ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sIdToDelete
- */
- identityDelete(fCallback, sIdToDelete) {
- this.defaultRequest(fCallback, 'IdentityDelete', {
- IdToDelete: sIdToDelete
- });
- }
-
- /**
- * @param {?Function} fCallback
- */
- accountsAndIdentities(fCallback) {
- this.defaultRequest(fCallback, 'AccountsAndIdentities');
- }
-
- /**
- * @param {?Function} fCallback
- * @param {SieveScriptModel} script
- */
- filtersScriptSave(fCallback, script) {
- this.defaultRequest(fCallback, 'FiltersScriptSave', script.toJson());
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} name
- */
- filtersScriptActivate(fCallback, name) {
- this.defaultRequest(fCallback, 'FiltersScriptActivate', {name:name});
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} name
- */
- filtersScriptDelete(fCallback, name) {
- this.defaultRequest(fCallback, 'FiltersScriptDelete', {name:name});
- }
-
- /**
- * @param {?Function} fCallback
- */
- filtersGet(fCallback) {
- this.defaultRequest(fCallback, 'Filters', {});
- }
-
- /**
- * @param {?Function} fCallback
- */
- templates(fCallback) {
- this.defaultRequest(fCallback, 'Templates', {});
- }
-
- /**
- * @param {Function} fCallback
- * @param {string} sID
- */
- templateGetById(fCallback, sID) {
- this.defaultRequest(fCallback, 'TemplateGetByID', {
- ID: sID
- });
- }
-
- /**
- * @param {Function} fCallback
- * @param {string} sID
- */
- templateDelete(fCallback, sID) {
- this.defaultRequest(fCallback, 'TemplateDelete', {
- IdToDelete: sID
- });
- }
-
- /**
- * @param {Function} fCallback
- * @param {string} sID
- * @param {string} sName
- * @param {string} sBody
- */
- templateSetup(fCallback, sID, sName, sBody) {
- this.defaultRequest(fCallback, 'TemplateSetup', {
- ID: sID,
- Name: sName,
- Body: sBody
- });
- }
/**
* @param {Function} fCallback
@@ -243,23 +24,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
*/
messageList(fCallback, params, bSilent = false) {
const
- sFolderFullNameRaw = pString(params.Folder),
- folderHash = getFolderHash(sFolderFullNameRaw),
- useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
- inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
+ sFolderFullName = pString(params.Folder),
+ folderHash = getFolderHash(sFolderFullName);
- params.Folder = sFolderFullNameRaw;
- params.ThreadUid = useThreads ? params.ThreadUid : '';
params = Object.assign({
- Folder: '',
Offset: 0,
Limit: SettingsUserStore.messagesPerPage(),
Search: '',
- UidNext: inboxUidNext,
- UseThreads: useThreads,
- ThreadUid: '',
- Sort: FolderUserStore.sortMode()
+ UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
+ Sort: FolderUserStore.sortMode(),
+ Hash: folderHash + SettingsGet('AccountHash')
}, params);
+ params.Folder = sFolderFullName;
+ if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
+ params.UseThreads = 1;
+ } else {
+ params.ThreadUid = 0;
+ }
let sGetAdd = '';
@@ -267,13 +48,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
sGetAdd = 'MessageList/' +
SUB_QUERY_PREFIX +
'/' +
- urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params)));
+ b64EncodeJSONSafe(params);
params = {};
}
- this.defaultRequest(
+ this.request('MessageList',
fCallback,
- 'MessageList',
params,
30000,
sGetAdd,
@@ -283,43 +63,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
- * @param {Array} aDownloads
- */
- messageUploadAttachments(fCallback, aDownloads) {
- this.defaultRequest(
- fCallback,
- 'MessageUploadAttachments',
- {
- Attachments: aDownloads
- },
- 999000
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
+ * @param {string} sFolderFullName
* @param {number} iUid
* @returns {boolean}
*/
- message(fCallback, sFolderFullNameRaw, iUid) {
- sFolderFullNameRaw = pString(sFolderFullNameRaw);
+ message(fCallback, sFolderFullName, iUid) {
+ sFolderFullName = pString(sFolderFullName);
iUid = pInt(iUid);
- if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) {
- this.defaultRequest(
+ if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
+ this.request('Message',
fCallback,
- 'Message',
{},
null,
'Message/' +
SUB_QUERY_PREFIX +
'/' +
- urlsafeArray([
- sFolderFullNameRaw,
+ b64EncodeJSONSafe([
+ sFolderFullName,
iUid,
- SettingsGet('ProjectHash'),
- AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0
+ AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
+ SettingsGet('AccountHash')
]),
['Message']
);
@@ -330,186 +94,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
return false;
}
- /**
- * @param {?Function} fCallback
- * @param {Array} aExternals
- */
- composeUploadExternals(fCallback, aExternals) {
- this.defaultRequest(
- fCallback,
- 'ComposeUploadExternals',
- {
- Externals: aExternals
- },
- 999000
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sUrl
- * @param {string} sAccessToken
- */
- composeUploadDrive(fCallback, sUrl, sAccessToken) {
- this.defaultRequest(
- fCallback,
- 'ComposeUploadDrive',
- {
- AccessToken: sAccessToken,
- Url: sUrl
- },
- 999000
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} folder
- * @param {Array=} list = []
- */
- folderInformation(fCallback, folder, list = []) {
- let request = true;
- const uids = [];
-
- if (isNonEmptyArray(list)) {
- request = false;
- list.forEach(messageListItem => {
- if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
- uids.push(messageListItem.uid);
- }
-
- if (messageListItem.threads.length) {
- messageListItem.threads.forEach(uid => {
- if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
- uids.push(uid);
- }
- });
- }
- });
-
- if (uids.length) {
- request = true;
- }
- }
-
- if (request) {
- this.defaultRequest(fCallback, 'FolderInformation', {
- Folder: folder,
- FlagsUids: isArray(uids) ? uids.join(',') : '',
- UidNext: getFolderInboxName() === folder ? getFolderUidNext(folder) : ''
- });
- } else if (SettingsUserStore.useThreads()) {
- rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
- }
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Array} aFolders
- */
- folderInformationMultiply(fCallback, aFolders) {
- this.defaultRequest(fCallback, 'FolderInformationMultiply', {
- Folders: aFolders
- });
- }
-
- /**
- * @param {?Function} fCallback
- */
- logout(fCallback) {
- this.defaultRequest(fCallback, 'Logout');
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
- * @param {Array} aUids
- * @param {boolean} bSetFlagged
- */
- messageSetFlagged(fCallback, sFolderFullNameRaw, aUids, bSetFlagged) {
- this.defaultRequest(fCallback, 'MessageSetFlagged', {
- Folder: sFolderFullNameRaw,
- Uids: aUids.join(','),
- SetAction: bSetFlagged ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
- * @param {Array} aUids
- * @param {boolean} bSetSeen
- */
- messageSetSeen(fCallback, sFolderFullNameRaw, aUids, bSetSeen) {
- this.defaultRequest(fCallback, 'MessageSetSeen', {
- Folder: sFolderFullNameRaw,
- Uids: aUids.join(','),
- SetAction: bSetSeen ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
- * @param {boolean} bSetSeen
- * @param {Array} aThreadUids = null
- */
- messageSetSeenToAll(fCallback, sFolderFullNameRaw, bSetSeen, aThreadUids = null) {
- this.defaultRequest(fCallback, 'MessageSetSeenToAll', {
- Folder: sFolderFullNameRaw,
- SetAction: bSetSeen ? 1 : 0,
- ThreadUids: aThreadUids ? aThreadUids.join(',') : ''
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Object} oData
- */
- saveMessage(fCallback, oData) {
- this.defaultRequest(fCallback, 'SaveMessage', oData, 200000);
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sMessageFolder
- * @param {string} sMessageUid
- * @param {string} sReadReceipt
- * @param {string} sSubject
- * @param {string} sText
- */
- sendReadReceiptMessage(fCallback, sMessageFolder, sMessageUid, sReadReceipt, sSubject, sText) {
- this.defaultRequest(fCallback, 'SendReadReceiptMessage', {
- MessageFolder: sMessageFolder,
- MessageUid: sMessageUid,
- ReadReceipt: sReadReceipt,
- Subject: sSubject,
- Text: sText
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Object} oData
- */
- sendMessage(fCallback, oData) {
- this.defaultRequest(fCallback, 'SendMessage', oData, 30000);
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Object} oData
- */
- saveSystemFolders(fCallback, oData) {
- this.defaultRequest(fCallback, 'SystemFoldersUpdate', oData);
- }
-
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
saveSettings(fCallback, oData) {
- this.defaultRequest(fCallback, 'SettingsUpdate', oData);
+ this.request('SettingsUpdate', fCallback, oData);
}
/**
@@ -523,234 +113,15 @@ class RemoteUserFetch extends AbstractFetchRemote {
});
}
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
- */
- folderClear(fCallback, sFolderFullNameRaw) {
- this.defaultRequest(fCallback, 'FolderClear', {
- Folder: sFolderFullNameRaw
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
- * @param {boolean} bSubscribe
- */
- folderSetSubscribe(fCallback, sFolderFullNameRaw, bSubscribe) {
- this.defaultRequest(fCallback, 'FolderSubscribe', {
- Folder: sFolderFullNameRaw,
+/*
+ folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
+ return this.post('FolderMove', FolderUserStore.foldersRenaming, {
+ Folder: sPrevFolderFullName,
+ NewFolder: sNewFolderFullName,
Subscribe: bSubscribe ? 1 : 0
});
}
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolderFullNameRaw
- * @param {boolean} bCheckable
- */
- folderSetCheckable(fCallback, sFolderFullNameRaw, bCheckable) {
- this.defaultRequest(fCallback, 'FolderCheckable', {
- Folder: sFolderFullNameRaw,
- Checkable: bCheckable ? 1 : 0
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolder
- * @param {string} sToFolder
- * @param {Array} aUids
- * @param {string=} sLearning
- * @param {boolean=} bMarkAsRead
- */
- messagesMove(fCallback, sFolder, sToFolder, aUids, sLearning, bMarkAsRead) {
- this.defaultRequest(
- fCallback,
- 'MessageMove',
- {
- FromFolder: sFolder,
- ToFolder: sToFolder,
- Uids: aUids.join(','),
- MarkAsRead: bMarkAsRead ? 1 : 0,
- Learning: sLearning || ''
- },
- null,
- '',
- ['MessageList']
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolder
- * @param {string} sToFolder
- * @param {Array} aUids
- */
- messagesCopy(fCallback, sFolder, sToFolder, aUids) {
- this.defaultRequest(fCallback, 'MessageCopy', {
- FromFolder: sFolder,
- ToFolder: sToFolder,
- Uids: aUids.join(',')
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sFolder
- * @param {Array} aUids
- */
- messagesDelete(fCallback, sFolder, aUids) {
- this.defaultRequest(
- fCallback,
- 'MessageDelete',
- {
- Folder: sFolder,
- Uids: aUids.join(',')
- },
- null,
- '',
- ['MessageList']
- );
- }
-
- /**
- * @param {?Function} fCallback
- */
- appDelayStart(fCallback) {
- this.defaultRequest(fCallback, 'AppDelayStart');
- }
-
- /**
- * @param {?Function} fCallback
- */
- quota(fCallback) {
- this.defaultRequest(fCallback, 'Quota');
- }
-
- /**
- * @param {?Function} fCallback
- * @param {number} iOffset
- * @param {number} iLimit
- * @param {string} sSearch
- */
- contacts(fCallback, iOffset, iLimit, sSearch) {
- this.defaultRequest(
- fCallback,
- 'Contacts',
- {
- Offset: iOffset,
- Limit: iLimit,
- Search: sSearch
- },
- null,
- '',
- ['Contacts']
- );
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sRequestUid
- * @param {string} sUid
- * @param {Array} aProperties
- */
- contactSave(fCallback, sRequestUid, sUid, aProperties) {
- this.defaultRequest(fCallback, 'ContactSave', {
- RequestUid: sRequestUid,
- Uid: sUid,
- Properties: aProperties
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {Array} aUids
- */
- contactsDelete(fCallback, aUids) {
- this.defaultRequest(fCallback, 'ContactsDelete', {
- Uids: aUids.join(',')
- });
- }
-
- /**
- * @param {?Function} fCallback
- * @param {string} sQuery
- * @param {number} iPage
- */
- suggestions(fCallback, sQuery, iPage) {
- this.defaultRequest(
- fCallback,
- 'Suggestions',
- {
- Query: sQuery,
- Page: iPage
- },
- null,
- '',
- ['Suggestions']
- );
- }
-
- /**
- * @param {?Function} fCallback
- */
- clearUserBackground(fCallback) {
- this.defaultRequest(fCallback, 'ClearUserBackground');
- }
-
- foldersReload(fCallback) {
- this.abort('Folders')
- .postRequest('Folders', FolderUserStore.foldersLoading)
- .then(data => {
- data = FolderCollectionModel.reviveFromJson(data.Result);
- data && data.storeIt();
- fCallback && fCallback(true);
- })
- .catch(() => fCallback && setTimeout(() => fCallback(false), 1));
- }
-
- foldersReloadWithTimeout() {
- this.setTrigger(FolderUserStore.foldersLoading, true);
-
- clearTimeout(this.foldersTimeout);
- this.foldersTimeout = setTimeout(() => this.foldersReload(), 500);
- }
-
- folderDelete(sFolderFullNameRaw) {
- return this.postRequest('FolderDelete', FolderUserStore.foldersDeleting, {
- Folder: sFolderFullNameRaw
- });
- }
-
- folderCreate(sNewFolderName, sParentName) {
- return this.postRequest('FolderCreate', FolderUserStore.foldersCreating, {
- Folder: sNewFolderName,
- Parent: sParentName
- });
- }
-
- folderMove(sPrevFolderFullNameRaw, sNewFolderFullName) {
- return this.postRequest('FolderMove', FolderUserStore.foldersRenaming, {
- Folder: sPrevFolderFullNameRaw,
- NewFolder: sNewFolderFullName
- });
- }
-
- folderRename(sPrevFolderFullNameRaw, sNewFolderName) {
- return this.postRequest('FolderRename', FolderUserStore.foldersRenaming, {
- Folder: sPrevFolderFullNameRaw,
- NewFolderName: sNewFolderName
- });
- }
-
- attachmentsActions(sAction, aHashes, fTrigger) {
- return this.postRequest('AttachmentsActions', fTrigger, {
- Do: sAction,
- Hashes: aHashes
- });
- }
+*/
}
export default new RemoteUserFetch();
diff --git a/dev/Screen/AbstractSettings.js b/dev/Screen/AbstractSettings.js
index 04c2bc4da..fd00ca9ff 100644
--- a/dev/Screen/AbstractSettings.js
+++ b/dev/Screen/AbstractSettings.js
@@ -2,7 +2,7 @@ import ko from 'ko';
import { pString } from 'Common/Utils';
import { settings } from 'Common/Links';
-import { doc, elementById } from 'Common/Globals';
+import { createElement, elementById } from 'Common/Globals';
import { AbstractScreen } from 'Knoin/AbstractScreen';
@@ -18,52 +18,38 @@ export class AbstractSettingsScreen extends AbstractScreen {
this.menu = ko.observableArray();
this.oCurrentSubScreen = null;
-
- this.setupSettings();
- }
-
- /**
- * @param {Function=} fCallback
- */
- setupSettings(fCallback = null) {
- fCallback && fCallback();
}
onRoute(subName) {
let settingsScreen = null,
- RoutedSettingsViewModel = null,
- viewModelDom = null;
-
- RoutedSettingsViewModel = VIEW_MODELS.find(
- SettingsViewModel =>
- SettingsViewModel && SettingsViewModel.__rlSettingsData && subName === SettingsViewModel.__rlSettingsData.Route
- );
+ viewModelDom = null,
+ RoutedSettingsViewModel = VIEW_MODELS.find(
+ SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
+ );
if (RoutedSettingsViewModel) {
- if (RoutedSettingsViewModel.__builded && RoutedSettingsViewModel.__vm) {
+ if (RoutedSettingsViewModel.__vm) {
settingsScreen = RoutedSettingsViewModel.__vm;
} else {
const vmPlace = elementById('rl-settings-subscreen');
if (vmPlace) {
- settingsScreen = new RoutedSettingsViewModel();
-
- viewModelDom = Element.fromHTML('
');
+ viewModelDom = createElement('div',{
+ id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
+ hidden: ''
+ })
vmPlace.append(viewModelDom);
+ settingsScreen = new RoutedSettingsViewModel();
settingsScreen.viewModelDom = viewModelDom;
- settingsScreen.__rlSettingsData = RoutedSettingsViewModel.__rlSettingsData;
-
RoutedSettingsViewModel.__dom = viewModelDom;
- RoutedSettingsViewModel.__builded = true;
RoutedSettingsViewModel.__vm = settingsScreen;
- const tmpl = { name: RoutedSettingsViewModel.__rlSettingsData.Template };
ko.applyBindingAccessorsToNode(
viewModelDom,
{
i18nInit: true,
- template: () => tmpl
+ template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
},
settingsScreen
);
@@ -75,75 +61,55 @@ export class AbstractSettingsScreen extends AbstractScreen {
}
if (settingsScreen) {
- const o = this;
setTimeout(() => {
// hide
- if (o.oCurrentSubScreen) {
- o.oCurrentSubScreen.onHide && o.oCurrentSubScreen.onHide();
- o.oCurrentSubScreen.viewModelDom.hidden = true;
- }
+ this.onHide();
// --
- o.oCurrentSubScreen = settingsScreen;
+ this.oCurrentSubScreen = settingsScreen;
// show
- if (o.oCurrentSubScreen) {
- o.oCurrentSubScreen.onBeforeShow && o.oCurrentSubScreen.onBeforeShow();
- o.oCurrentSubScreen.viewModelDom.hidden = false;
- o.oCurrentSubScreen.onShow && o.oCurrentSubScreen.onShow();
+ settingsScreen.beforeShow && settingsScreen.beforeShow();
+ settingsScreen.viewModelDom.hidden = false;
+ settingsScreen.onShow && settingsScreen.onShow();
- o.menu.forEach(item => {
- item.selected(
- settingsScreen &&
- settingsScreen.__rlSettingsData &&
- item.route === settingsScreen.__rlSettingsData.Route
- );
- });
+ this.menu.forEach(item => {
+ item.selected(
+ item.route === RoutedSettingsViewModel.__rlSettingsData.route
+ );
+ });
- doc.querySelector('#rl-content .b-settings .b-content').scrollTop = 0;
- }
+ (elementById('rl-settings-subscreen') || {}).scrollTop = 0;
// --
}, 1);
}
} else {
- rl.route.setHash(settings(), false, true);
+ hasher.replaceHash(settings());
}
}
onHide() {
- if (this.oCurrentSubScreen && this.oCurrentSubScreen.viewModelDom) {
- this.oCurrentSubScreen.onHide && this.oCurrentSubScreen.onHide();
- this.oCurrentSubScreen.viewModelDom.hidden = true;
+ let subScreen = this.oCurrentSubScreen;
+ if (subScreen) {
+ subScreen.onHide && subScreen.onHide();
+ subScreen.viewModelDom.hidden = true;
}
}
onBuild() {
- VIEW_MODELS.forEach(SettingsViewModel => {
- if (
- SettingsViewModel &&
- SettingsViewModel.__rlSettingsData
- ) {
- this.menu.push({
- route: SettingsViewModel.__rlSettingsData.Route,
- label: SettingsViewModel.__rlSettingsData.Label,
- selected: ko.observable(false),
- disabled: false
- });
- }
- });
+ VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
}
routes() {
const DefaultViewModel = VIEW_MODELS.find(
- SettingsViewModel =>
- SettingsViewModel && SettingsViewModel.__rlSettingsData && SettingsViewModel.__rlSettingsData.IsDefault
+ SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
),
defaultRoute =
- DefaultViewModel && DefaultViewModel.__rlSettingsData ? DefaultViewModel.__rlSettingsData.Route : 'general',
+ DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
rules = {
subname: /^(.*)$/,
normalize_: (rquest, vals) => {
- vals.subname = undefined === vals.subname ? defaultRoute : pString(vals.subname);
+ vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
return [vals.subname];
}
};
@@ -165,11 +131,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
* @returns {void}
*/
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
+ let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
SettingsViewModelClass.__rlSettingsData = {
- Label: labelName,
- Template: template,
- Route: route,
- IsDefault: !!isDefault
+ label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
+ route: route || name.toLowerCase(),
+ selected: ko.observable(false),
+ template: template || SettingsViewModelClass.name,
+ isDefault: !!isDefault
};
VIEW_MODELS.push(SettingsViewModelClass);
diff --git a/dev/Screen/Admin/Login.js b/dev/Screen/Admin/Login.js
index 12ebb1838..7d0301b2f 100644
--- a/dev/Screen/Admin/Login.js
+++ b/dev/Screen/Admin/Login.js
@@ -1,10 +1,10 @@
import { AbstractScreen } from 'Knoin/AbstractScreen';
-import { LoginAdminView } from 'View/Admin/Login';
+import { AdminLoginView } from 'View/Admin/Login';
export class LoginAdminScreen extends AbstractScreen {
constructor() {
- super('login', [LoginAdminView]);
+ super('login', [AdminLoginView]);
}
onShow() {
diff --git a/dev/Screen/Admin/Settings.js b/dev/Screen/Admin/Settings.js
index 31a1f64bd..a57cc8219 100644
--- a/dev/Screen/Admin/Settings.js
+++ b/dev/Screen/Admin/Settings.js
@@ -2,15 +2,15 @@ import { runSettingsViewModelHooks } from 'Common/Plugins';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
-import { GeneralAdminSettings } from 'Settings/Admin/General';
-import { DomainsAdminSettings } from 'Settings/Admin/Domains';
-import { LoginAdminSettings } from 'Settings/Admin/Login';
-import { ContactsAdminSettings } from 'Settings/Admin/Contacts';
-import { SecurityAdminSettings } from 'Settings/Admin/Security';
-import { PluginsAdminSettings } from 'Settings/Admin/Plugins';
-import { PackagesAdminSettings } from 'Settings/Admin/Packages';
-import { AboutAdminSettings } from 'Settings/Admin/About';
-import { BrandingAdminSettings } from 'Settings/Admin/Branding';
+import { AdminSettingsGeneral } from 'Settings/Admin/General';
+import { AdminSettingsDomains } from 'Settings/Admin/Domains';
+import { AdminSettingsLogin } from 'Settings/Admin/Login';
+import { AdminSettingsContacts } from 'Settings/Admin/Contacts';
+import { AdminSettingsSecurity } from 'Settings/Admin/Security';
+import { AdminSettingsPackages } from 'Settings/Admin/Packages';
+import { AdminSettingsAbout } from 'Settings/Admin/About';
+import { AdminSettingsBranding } from 'Settings/Admin/Branding';
+import { AdminSettingsConfig } from 'Settings/Admin/Config';
import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu';
import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
@@ -18,41 +18,22 @@ import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
export class SettingsAdminScreen extends AbstractSettingsScreen {
constructor() {
super([MenuSettingsAdminView, PaneSettingsAdminView]);
- }
-
- /**
- * @param {Function=} fCallback = null
- */
- setupSettings(fCallback = null) {
- settingsAddViewModel(
- GeneralAdminSettings,
- 'AdminSettingsGeneral',
- 'TABS_LABELS/LABEL_GENERAL_NAME',
- 'general',
- true
- );
[
- [DomainsAdminSettings, 'Domains'],
- [LoginAdminSettings, 'Login'],
- [BrandingAdminSettings, 'Branding'],
- [ContactsAdminSettings, 'Contacts'],
- [SecurityAdminSettings, 'Security'],
- [PluginsAdminSettings, 'Plugins'],
- [PackagesAdminSettings, 'Packages'],
- [AboutAdminSettings, 'About'],
- ].forEach(item =>
- settingsAddViewModel(
- item[0],
- 'AdminSettings'+item[1],
- 'TABS_LABELS/LABEL_'+item[1].toUpperCase()+'_NAME',
- item[1].toLowerCase()
- )
+ AdminSettingsGeneral,
+ AdminSettingsDomains,
+ AdminSettingsLogin,
+ AdminSettingsBranding,
+ AdminSettingsContacts,
+ AdminSettingsSecurity,
+ AdminSettingsPackages,
+ AdminSettingsConfig,
+ AdminSettingsAbout
+ ].forEach((item, index) =>
+ settingsAddViewModel(item, 0, 0, 0, 0 === index)
);
runSettingsViewModelHooks(true);
-
- fCallback && fCallback();
}
onShow() {
diff --git a/dev/Screen/User/MailBox.js b/dev/Screen/User/MailBox.js
index c445a3876..851e23ff4 100644
--- a/dev/Screen/User/MailBox.js
+++ b/dev/Screen/User/MailBox.js
@@ -1,33 +1,32 @@
import { Scope } from 'Common/Enums';
-import { doc, leftPanelDisabled, moveAction, Settings } from 'Common/Globals';
+import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
+import { doc, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils';
-import { getFolderFromCacheList, getFolderFullNameRaw, getFolderInboxName } from 'Common/Cache';
+import { setLayoutResizer } from 'Common/UtilsUser';
+import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { i18n } from 'Common/Translator';
+import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
-import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
-import { SystemDropDownMailBoxUserView } from 'View/User/MailBox/SystemDropDown';
-import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList';
-import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList';
-import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView';
-
-import { warmUpScreenPopup } from 'Knoin/Knoin';
+import { SystemDropDownUserView } from 'View/User/SystemDropDown';
+import { MailFolderList } from 'View/User/MailBox/FolderList';
+import { MailMessageList } from 'View/User/MailBox/MessageList';
+import { MailMessageView } from 'View/User/MailBox/MessageView';
import { AbstractScreen } from 'Knoin/AbstractScreen';
-import { ComposePopupView } from 'View/Popup/Compose';
-
export class MailBoxUserScreen extends AbstractScreen {
constructor() {
super('mailbox', [
- SystemDropDownMailBoxUserView,
- FolderListMailBoxUserView,
- MessageListMailBoxUserView,
- MessageViewMailBoxUserView
+ SystemDropDownUserView,
+ MailFolderList,
+ MailMessageList,
+ MailMessageView
]);
}
@@ -52,7 +51,7 @@ export class MailBoxUserScreen extends AbstractScreen {
onShow() {
this.updateWindowTitle();
- AppUserStore.focusedState(Scope.None);
+ AppUserStore.focusedState('none');
AppUserStore.focusedState(Scope.MessageList);
ThemeStore.isMobile() && leftPanelDisabled(true);
@@ -65,20 +64,17 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onRoute(folderHash, page, search) {
- const folder = getFolderFromCacheList(getFolderFullNameRaw(folderHash.replace(/~([\d]+)$/, '')));
+ const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
if (folder) {
- let threadUid = folderHash.replace(/^.+~([\d]+)$/, '$1');
- if (folderHash === threadUid) {
- threadUid = '';
- }
+ let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
FolderUserStore.currentFolder(folder);
- MessageUserStore.listPage(1 > page ? 1 : page);
- MessageUserStore.listSearch(search);
- MessageUserStore.listThreadUid(threadUid);
+ MessagelistUserStore.page(1 > page ? 1 : page);
+ MessagelistUserStore.listSearch(search);
+ MessagelistUserStore.threadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
- rl.app.reloadMessageList();
+ MessagelistUserStore.reload();
}
}
@@ -86,28 +82,41 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onStart() {
- if (!this.__started) {
- super.onStart();
- setTimeout(() => warmUpScreenPopup(ComposePopupView), 500);
+ super.onStart();
- addEventListener('mailbox.inbox-unread-count', e => {
- FolderUserStore.foldersInboxUnreadCount(e.detail);
-
- const email = AccountUserStore.email();
- AccountUserStore.accounts.forEach(item =>
- item && email === item.email && item.count(e.detail)
- );
-
- this.updateWindowTitle();
- });
- }
+ addEventListener('mailbox.inbox-unread-count', e => {
+ FolderUserStore.foldersInboxUnreadCount(e.detail);
+/* // Disabled in SystemDropDown.html
+ const email = AccountUserStore.email();
+ AccountUserStore.accounts.forEach(item =>
+ item && email === item.email && item.count(e.detail)
+ );
+*/
+ this.updateWindowTitle();
+ });
}
/**
* @returns {void}
*/
onBuild() {
- setTimeout(() => rl.app.initHorizontalLayoutResizer(), 1);
+ setTimeout(() => {
+ // initMailboxLayoutResizer
+ const top = elementById('V-MailMessageList'),
+ bottom = elementById('V-MailMessageView'),
+ fToggle = () => {
+ let layout = SettingsUserStore.layout();
+ setLayoutResizer(top, bottom, ClientSideKeyNameMessageListSize,
+ (ThemeStore.isMobile() || Layout.NoPreview === layout)
+ ? 0
+ : (Layout.SidePreview === layout ? 'Width' : 'Height')
+ );
+ };
+ if (top && bottom) {
+ fToggle();
+ addEventListener('rl-layout', fToggle);
+ }
+ }, 1);
doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false)
@@ -115,19 +124,23 @@ export class MailBoxUserScreen extends AbstractScreen {
}
/**
+ * Parse link as generated by mailBox()
* @returns {Array}
*/
routes() {
const
- folder = (request, vals) => request ? decodeURI(pString(vals[0])) : getFolderInboxName(),
+ folder = (request, vals) => request ? pString(vals[0]) : getFolderInboxName(),
fNormS = (request, vals) => [folder(request, vals), request ? pInt(vals[1]) : 1, decodeURI(pString(vals[2]))];
return [
+ // Folder: INBOX | INBOX.sub | Sent | fullNameHash
[/^([^/]*)$/, { normalize_: fNormS }],
- [/^([a-zA-Z0-9~]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
+ // Search: {folder}/{string}
+ [/^([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 }]
+ // Page: {folder}/p{int}(/{search})?
+ [/^([a-zA-Z0-9.~_-]+)\/p([1-9][0-9]*)(?:\/(.+))?$/, { normalize_: fNormS }]
];
}
}
diff --git a/dev/Screen/User/Settings.js b/dev/Screen/User/Settings.js
index 70cce520f..00f19ab67 100644
--- a/dev/Screen/User/Settings.js
+++ b/dev/Screen/User/Settings.js
@@ -1,5 +1,5 @@
-import { Capa, Scope } from 'Common/Enums';
-import { keyScope, leftPanelDisabled, Settings } from 'Common/Globals';
+import { Scope } from 'Common/Enums';
+import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
@@ -9,23 +9,53 @@ import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
-import { GeneralUserSettings } from 'Settings/User/General';
-import { ContactsUserSettings } from 'Settings/User/Contacts';
-import { AccountsUserSettings } from 'Settings/User/Accounts';
-import { FiltersUserSettings } from 'Settings/User/Filters';
-import { SecurityUserSettings } from 'Settings/User/Security';
-import { TemplatesUserSettings } from 'Settings/User/Templates';
-import { FoldersUserSettings } from 'Settings/User/Folders';
-import { ThemesUserSettings } from 'Settings/User/Themes';
-import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
+import { UserSettingsGeneral } from 'Settings/User/General';
+import { UserSettingsContacts } from 'Settings/User/Contacts';
+import { UserSettingsAccounts } from 'Settings/User/Accounts';
+import { UserSettingsFilters } from 'Settings/User/Filters';
+import { UserSettingsSecurity } from 'Settings/User/Security';
+import { UserSettingsFolders } from 'Settings/User/Folders';
+import { UserSettingsThemes } from 'Settings/User/Themes';
-import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown';
-import { MenuSettingsUserView } from 'View/User/Settings/Menu';
-import { PaneSettingsUserView } from 'View/User/Settings/Pane';
+import { SystemDropDownUserView } from 'View/User/SystemDropDown';
+import { SettingsMenuUserView } from 'View/User/Settings/Menu';
+import { SettingsPaneUserView } from 'View/User/Settings/Pane';
export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() {
- super([SystemDropDownSettingsUserView, MenuSettingsUserView, PaneSettingsUserView]);
+ super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
+
+ const views = [
+ UserSettingsGeneral
+ ];
+
+ if (AppUserStore.allowContacts()) {
+ views.push(UserSettingsContacts);
+ }
+
+ if (SettingsCapa('AdditionalAccounts') || SettingsCapa('Identities')) {
+ views.push(UserSettingsAccounts);
+ }
+
+ if (SettingsCapa('Sieve')) {
+ views.push(UserSettingsFilters);
+ }
+
+ if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) {
+ views.push(UserSettingsSecurity);
+ }
+
+ views.push(UserSettingsFolders);
+
+ if (SettingsCapa('Themes')) {
+ views.push(UserSettingsThemes);
+ }
+
+ views.forEach((item, index) =>
+ settingsAddViewModel(item, item.name.replace('User', ''), 0, 0, 0 === index)
+ );
+
+ runSettingsViewModelHooks(false);
initOnStartOrLangChange(
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
@@ -33,67 +63,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
);
}
- /**
- * @param {Function=} fCallback
- */
- setupSettings(fCallback = null) {
- if (!Settings.capa(Capa.Settings)) {
- fCallback && fCallback();
-
- return false;
- }
-
- settingsAddViewModel(GeneralUserSettings, 'SettingsGeneral', 'SETTINGS_LABELS/LABEL_GENERAL_NAME', 'general', true);
-
- if (AppUserStore.allowContacts()) {
- settingsAddViewModel(ContactsUserSettings, 'SettingsContacts', 'SETTINGS_LABELS/LABEL_CONTACTS_NAME', 'contacts');
- }
-
- if (Settings.capa(Capa.AdditionalAccounts) || Settings.capa(Capa.Identities)) {
- settingsAddViewModel(
- AccountsUserSettings,
- 'SettingsAccounts',
- Settings.capa(Capa.AdditionalAccounts)
- ? 'SETTINGS_LABELS/LABEL_ACCOUNTS_NAME'
- : 'SETTINGS_LABELS/LABEL_IDENTITIES_NAME',
- 'accounts'
- );
- }
-
- if (Settings.capa(Capa.Sieve)) {
- settingsAddViewModel(FiltersUserSettings, 'SettingsFilters', 'SETTINGS_LABELS/LABEL_FILTERS_NAME', 'filters');
- }
-
- if (Settings.capa(Capa.AutoLogout)) {
- settingsAddViewModel(SecurityUserSettings, 'SettingsSecurity', 'SETTINGS_LABELS/LABEL_SECURITY_NAME', 'security');
- }
-
- if (Settings.capa(Capa.Templates)) {
- settingsAddViewModel(
- TemplatesUserSettings,
- 'SettingsTemplates',
- 'SETTINGS_LABELS/LABEL_TEMPLATES_NAME',
- 'templates'
- );
- }
-
- settingsAddViewModel(FoldersUserSettings, 'SettingsFolders', 'SETTINGS_LABELS/LABEL_FOLDERS_NAME', 'folders');
-
- if (Settings.capa(Capa.Themes)) {
- settingsAddViewModel(ThemesUserSettings, 'SettingsThemes', 'SETTINGS_LABELS/LABEL_THEMES_NAME', 'themes');
- }
-
- if (Settings.capa(Capa.OpenPGP)) {
- settingsAddViewModel(OpenPgpUserSettings, 'SettingsOpenPGP', 'OpenPGP', 'openpgp');
- }
-
- runSettingsViewModelHooks(false);
-
- fCallback && fCallback();
-
- return true;
- }
-
onShow() {
this.setSettingsTitle();
keyScope(Scope.Settings);
diff --git a/dev/Settings/Admin/About.js b/dev/Settings/Admin/About.js
index 6a9fed445..c6754eb74 100644
--- a/dev/Settings/Admin/About.js
+++ b/dev/Settings/Admin/About.js
@@ -1,8 +1,15 @@
import ko from 'ko';
import { Settings } from 'Common/Globals';
+import Remote from 'Remote/Admin/Fetch';
-export class AboutAdminSettings {
+export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
constructor() {
- this.version = ko.observable(Settings.app('version'));
+ this.version = Settings.app('version');
+ this.phpextensions = ko.observableArray();
}
+
+ onBuild() {
+ Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
+ }
+
}
diff --git a/dev/Settings/Admin/Branding.js b/dev/Settings/Admin/Branding.js
index a957c8604..c8ad7aea1 100644
--- a/dev/Settings/Admin/Branding.js
+++ b/dev/Settings/Admin/Branding.js
@@ -1,32 +1,10 @@
-import ko from 'ko';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
-import { SettingsGet } from 'Common/Globals';
-import { settingsSaveHelperSimpleFunction } from 'Common/Utils';
-
-import Remote from 'Remote/Admin/Fetch';
-
-export class BrandingAdminSettings {
+export class AdminSettingsBranding extends AbstractViewSettings {
constructor() {
- this.title = ko.observable(SettingsGet('Title')).idleTrigger();
- this.loadingDesc = ko.observable(SettingsGet('LoadingDescription')).idleTrigger();
- this.faviconUrl = ko.observable(SettingsGet('FaviconUrl')).idleTrigger();
-
- this.title.subscribe(value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.title.trigger, this), {
- Title: value.trim()
- })
- );
-
- this.loadingDesc.subscribe(value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.loadingDesc.trigger, this), {
- LoadingDescription: value.trim()
- })
- );
-
- this.faviconUrl.subscribe(value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.faviconUrl.trigger, this), {
- FaviconUrl: value.trim()
- })
- );
+ super();
+ this.addSetting('Title');
+ this.addSetting('LoadingDescription');
+ this.addSetting('FaviconUrl');
}
}
diff --git a/dev/Settings/Admin/Config.js b/dev/Settings/Admin/Config.js
new file mode 100644
index 000000000..0a7bd3037
--- /dev/null
+++ b/dev/Settings/Admin/Config.js
@@ -0,0 +1,48 @@
+import ko from 'ko';
+
+import Remote from 'Remote/Admin/Fetch';
+import { forEachObjectEntry } from 'Common/Utils';
+
+export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
+
+ constructor() {
+ this.config = ko.observableArray();
+ }
+
+ beforeShow() {
+ Remote.request('AdminSettingsGet', (iError, data) => {
+ if (!iError) {
+ const cfg = [],
+ getInputType = (value, pass) => {
+ switch (typeof value)
+ {
+ case 'boolean': return 'checkbox';
+ case 'number': return 'number';
+ }
+ return pass ? 'password' : 'text';
+ };
+ forEachObjectEntry(data.Result, (key, items) => {
+ const section = {
+ name: key,
+ items: []
+ };
+ forEachObjectEntry(items, (skey, item) => {
+ section.items.push({
+ key: `config[${key}][${skey}]`,
+ name: skey,
+ value: item[0],
+ type: getInputType(item[0], skey.includes('password')),
+ comment: item[1]
+ });
+ });
+ cfg.push(section);
+ });
+ this.config(cfg);
+ }
+ });
+ }
+
+ saveConfig(form) {
+ Remote.post('AdminSettingsSet', null, new FormData(form));
+ }
+}
diff --git a/dev/Settings/Admin/Contacts.js b/dev/Settings/Admin/Contacts.js
index 9bcd91f56..1cb0d0d1d 100644
--- a/dev/Settings/Admin/Contacts.js
+++ b/dev/Settings/Admin/Contacts.js
@@ -1,35 +1,30 @@
import ko from 'ko';
-import { SaveSettingsStep } from 'Common/Enums';
import { SettingsGet } from 'Common/Globals';
-import {
- settingsSaveHelperSimpleFunction,
- defaultOptionsAfterRender,
- addObservablesTo,
- addSubscribablesTo
-} from 'Common/Utils';
+import { defaultOptionsAfterRender } from 'Common/Utils';
+import { addObservablesTo } from 'External/ko';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
-export class ContactsAdminSettings {
+export class AdminSettingsContacts extends AbstractViewSettings {
constructor() {
+ super();
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
+ this.addSetting('ContactsPdoDsn');
+ this.addSetting('ContactsPdoUser');
+ this.addSetting('ContactsPdoPassword');
+ this.addSetting('ContactsPdoType', () => {
+ this.testContactsSuccess(false);
+ this.testContactsError(false);
+ this.testContactsErrorMessage('');
+ });
+
+ this.addSettings(['ContactsEnable','ContactsSync']);
+
addObservablesTo(this, {
- enableContacts: !!SettingsGet('ContactsEnable'),
- contactsSync: !!SettingsGet('ContactsSync'),
- contactsType: SettingsGet('ContactsPdoType'),
-
- pdoDsn: SettingsGet('ContactsPdoDsn'),
- pdoUser: SettingsGet('ContactsPdoUser'),
- pdoPassword: SettingsGet('ContactsPdoPassword'),
-
- pdoDsnTrigger: SaveSettingsStep.Idle,
- pdoUserTrigger: SaveSettingsStep.Idle,
- pdoPasswordTrigger: SaveSettingsStep.Idle,
- contactsTypeTrigger: SaveSettingsStep.Idle,
-
testing: false,
testContactsSuccess: false,
testContactsError: false,
@@ -54,61 +49,23 @@ export class ContactsAdminSettings {
this.mainContactsType = ko
.computed({
- read: this.contactsType,
+ read: this.contactsPdoType,
write: value => {
- if (value !== this.contactsType()) {
+ if (value !== this.contactsPdoType()) {
if (supportedTypes.includes(value)) {
- this.contactsType(value);
+ this.contactsPdoType(value);
} else if (types.length) {
- this.contactsType('');
+ this.contactsPdoType('');
}
} else {
- this.contactsType.valueHasMutated();
+ this.contactsPdoType.valueHasMutated();
}
}
})
.extend({ notify: 'always' });
- addSubscribablesTo(this, {
- enableContacts: value =>
- Remote.saveAdminConfig(null, {
- ContactsEnable: value ? 1 : 0
- }),
-
- contactsSync: value =>
- Remote.saveAdminConfig(null, {
- ContactsSync: value ? 1 : 0
- }),
-
- contactsType: value => {
- this.testContactsSuccess(false);
- this.testContactsError(false);
- this.testContactsErrorMessage('');
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.contactsTypeTrigger, this), {
- ContactsPdoType: value.trim()
- })
- },
-
- pdoDsn: value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoDsnTrigger, this), {
- ContactsPdoDsn: value.trim()
- }),
-
- pdoUser: value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoUserTrigger, this), {
- ContactsPdoUser: value.trim()
- }),
-
- pdoPassword: value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoPasswordTrigger, this), {
- ContactsPdoPassword: value.trim()
- })
- })
-
- this.onTestContactsResponse = this.onTestContactsResponse.bind(this);
-
decorateKoCommands(this, {
- testContactsCommand: self => self.pdoDsn() && self.pdoUser()
+ testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
});
}
@@ -118,31 +75,31 @@ export class ContactsAdminSettings {
this.testContactsErrorMessage('');
this.testing(true);
- Remote.testContacts(this.onTestContactsResponse, {
- ContactsPdoType: this.contactsType(),
- ContactsPdoDsn: this.pdoDsn(),
- ContactsPdoUser: this.pdoUser(),
- ContactsPdoPassword: this.pdoPassword()
- });
- }
-
- onTestContactsResponse(iError, data) {
- this.testContactsSuccess(false);
- this.testContactsError(false);
- this.testContactsErrorMessage('');
-
- if (!iError && data.Result.Result) {
- this.testContactsSuccess(true);
- } else {
- this.testContactsError(true);
- if (data && data.Result) {
- this.testContactsErrorMessage(data.Result.Message || '');
- } else {
+ Remote.request('AdminContactsTest',
+ (iError, data) => {
+ this.testContactsSuccess(false);
+ this.testContactsError(false);
this.testContactsErrorMessage('');
- }
- }
- this.testing(false);
+ if (!iError && data.Result.Result) {
+ this.testContactsSuccess(true);
+ } else {
+ this.testContactsError(true);
+ if (data && data.Result) {
+ this.testContactsErrorMessage(data.Result.Message || '');
+ } else {
+ this.testContactsErrorMessage('');
+ }
+ }
+
+ this.testing(false);
+ }, {
+ ContactsPdoType: this.contactsPdoType(),
+ ContactsPdoDsn: this.contactsPdoDsn(),
+ ContactsPdoUser: this.contactsPdoUser(),
+ ContactsPdoPassword: this.contactsPdoPassword()
+ }
+ );
}
onShow() {
diff --git a/dev/Settings/Admin/Domains.js b/dev/Settings/Admin/Domains.js
index 6e93797de..d14f5dad8 100644
--- a/dev/Settings/Admin/Domains.js
+++ b/dev/Settings/Admin/Domains.js
@@ -8,14 +8,11 @@ import Remote from 'Remote/Admin/Fetch';
import { DomainPopupView } from 'View/Popup/Domain';
import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
-export class DomainsAdminSettings {
+export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
constructor() {
this.domains = DomainAdminStore;
- this.domainForDeletion = ko.observable(null).deleteAccessHelper();
-
- this.onDomainListChangeRequest = this.onDomainListChangeRequest.bind(this);
- this.onDomainLoadRequest = this.onDomainLoadRequest.bind(this);
+ this.domainForDeletion = ko.observable(null).askDeleteHelper();
}
createDomain() {
@@ -28,30 +25,32 @@ export class DomainsAdminSettings {
deleteDomain(domain) {
DomainAdminStore.remove(domain);
- Remote.domainDelete(this.onDomainListChangeRequest, domain.name);
+ Remote.domainDelete(DomainAdminStore.fetch, domain.name);
+ Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
+ Name: domain.name
+ });
}
disableDomain(domain) {
domain.disabled(!domain.disabled());
- Remote.domainDisable(this.onDomainListChangeRequest, domain.name, domain.disabled());
+ Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
+ Name: domain.name,
+ Disabled: domain.disabled() ? 1 : 0
+ });
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
- let el = event.target.closestWithin('.b-admin-domains-list-table .e-item .e-action', oDom);
- el && ko.dataFor(el) && Remote.domain(this.onDomainLoadRequest, ko.dataFor(el).name);
+ let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
+ el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
+ (iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
+ {
+ Name: ko.dataFor(el).name
+ }
+ );
+
});
DomainAdminStore.fetch();
}
-
- onDomainLoadRequest(iError, oData) {
- if (!iError) {
- showScreenPopup(DomainPopupView, [oData.Result]);
- }
- }
-
- onDomainListChangeRequest() {
- DomainAdminStore.fetch();
- }
}
diff --git a/dev/Settings/Admin/General.js b/dev/Settings/Admin/General.js
index 7a0f459d6..66127ca21 100644
--- a/dev/Settings/Admin/General.js
+++ b/dev/Settings/Admin/General.js
@@ -2,28 +2,29 @@ import ko from 'ko';
import {
isArray,
- pInt,
- settingsSaveHelperSimpleFunction,
changeTheme,
- convertThemeName,
- addObservablesTo,
- addSubscribablesTo
+ convertThemeName
} from 'Common/Utils';
-import { Capa, SaveSettingsStep } from 'Common/Enums';
-import { Settings, SettingsGet } from 'Common/Globals';
-import { reload as translatorReload, convertLangName } from 'Common/Translator';
+import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
+import { SaveSettingsStep } from 'Common/Enums';
+import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
+import { translatorReload, convertLangName } from 'Common/Translator';
+
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin';
import Remote from 'Remote/Admin/Fetch';
import { ThemeStore } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language';
-import LanguagesPopupView from 'View/Popup/Languages';
+import { LanguagesPopupView } from 'View/Popup/Languages';
-export class GeneralAdminSettings {
+export class AdminSettingsGeneral extends AbstractViewSettings {
constructor() {
+ super();
+
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
@@ -36,18 +37,16 @@ export class GeneralAdminSettings {
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
+ this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
+
addObservablesTo(this, {
- allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
- newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
attachmentLimitTrigger: SaveSettingsStep.Idle,
- languageTrigger: SaveSettingsStep.Idle,
themeTrigger: SaveSettingsStep.Idle,
- capaThemes: Settings.capa(Capa.Themes),
- capaUserBackground: Settings.capa(Capa.UserBackground),
- capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts),
- capaIdentities: Settings.capa(Capa.Identities),
- capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails),
- capaTemplates: Settings.capa(Capa.Templates),
+ capaThemes: SettingsCapa('Themes'),
+ capaUserBackground: SettingsCapa('UserBackground'),
+ capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
+ capaIdentities: SettingsCapa('Identities'),
+ capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
dataFolderAccess: false
});
@@ -59,10 +58,14 @@ export class GeneralAdminSettings {
}
*/
- this.mainAttachmentLimit = ko
- .observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024))
+ this.attachmentLimit = ko
+ .observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 });
+ this.addSetting('Language');
+ this.addSetting('AttachmentLimit');
+ this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
+
this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadDataDesc =
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
@@ -74,12 +77,12 @@ export class GeneralAdminSettings {
].join('')
: '';
- this.themesOptions = ko.computed(() =>
- this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) }))
- );
+ addComputablesTo(this, {
+ themesOptions: () => this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })),
- this.languageFullName = ko.computed(() => convertLangName(this.language()));
- this.languageAdminFullName = ko.computed(() => convertLangName(this.languageAdmin()));
+ languageFullName: () => convertLangName(this.language()),
+ languageAdminFullName: () => convertLangName(this.languageAdmin())
+ });
this.languageAdminTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
@@ -87,55 +90,25 @@ export class GeneralAdminSettings {
this.languageAdminTrigger(saveSettingsStep);
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
},
- fSaveBoolHelper = (key, fn) =>
- value => {
- const data = {};
- data[key] = value ? 1 : 0;
- Remote.saveAdminConfig(fn, data);
- };
+ fSaveHelper = key => value => Remote.saveSetting(key, value);
addSubscribablesTo(this, {
- mainAttachmentLimit: value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.attachmentLimitTrigger, this), {
- AttachmentLimit: pInt(value)
- }),
-
- language: value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.languageTrigger, this), {
- Language: value.trim()
- }),
-
languageAdmin: value => {
this.languageAdminTrigger(SaveSettingsStep.Animate);
translatorReload(true, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
- .then(() => Remote.saveAdminConfig(null, {
- LanguageAdmin: value.trim()
- }));
+ .then(() => Remote.saveSetting('LanguageAdmin', value));
},
- theme: value => {
- changeTheme(value, this.themeTrigger);
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
- Theme: value.trim()
- });
- },
+ capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
- capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'),
+ capaIdentities: fSaveHelper('CapaIdentities'),
- capaIdentities: fSaveBoolHelper('CapaIdentities'),
+ capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
- capaTemplates: fSaveBoolHelper('CapaTemplates'),
+ capaThemes: fSaveHelper('CapaThemes'),
- capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'),
-
- capaThemes: fSaveBoolHelper('CapaThemes'),
-
- capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
-
- allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
-
- newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
+ capaUserBackground: fSaveHelper('CapaUserBackground')
});
}
diff --git a/dev/Settings/Admin/Login.js b/dev/Settings/Admin/Login.js
index 2e0be8ba8..cfc1feba0 100644
--- a/dev/Settings/Admin/Login.js
+++ b/dev/Settings/Admin/Login.js
@@ -1,46 +1,9 @@
-import ko from 'ko';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
-import { Settings, SettingsGet } from 'Common/Globals';
-import { settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
-
-import Remote from 'Remote/Admin/Fetch';
-
-export class LoginAdminSettings {
+export class AdminSettingsLogin extends AbstractViewSettings {
constructor() {
- addObservablesTo(this, {
- determineUserLanguage: !!SettingsGet('DetermineUserLanguage'),
- determineUserDomain: !!SettingsGet('DetermineUserDomain'),
- allowLanguagesOnLogin: !!SettingsGet('AllowLanguagesOnLogin'),
- hideSubmitButton: !!Settings.app('hideSubmitButton')
- });
-
- this.defaultDomain = ko.observable(SettingsGet('LoginDefaultDomain')).idleTrigger();
-
- addSubscribablesTo(this, {
- determineUserLanguage: value =>
- Remote.saveAdminConfig(null, {
- DetermineUserLanguage: value ? 1 : 0
- }),
-
- determineUserDomain: value =>
- Remote.saveAdminConfig(null, {
- DetermineUserDomain: value ? 1 : 0
- }),
-
- allowLanguagesOnLogin: value =>
- Remote.saveAdminConfig(null, {
- AllowLanguagesOnLogin: value ? 1 : 0
- }),
-
- hideSubmitButton: value =>
- Remote.saveAdminConfig(null, {
- hideSubmitButton: value ? 1 : 0
- }),
-
- defaultDomain: value =>
- Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.defaultDomain.trigger, this), {
- LoginDefaultDomain: value.trim()
- })
- });
+ super();
+ this.addSetting('LoginDefaultDomain');
+ this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin','hideSubmitButton']);
}
}
diff --git a/dev/Settings/Admin/Packages.js b/dev/Settings/Admin/Packages.js
index f2d04f6c1..7a161e5b1 100644
--- a/dev/Settings/Admin/Packages.js
+++ b/dev/Settings/Admin/Packages.js
@@ -6,42 +6,58 @@ import { getNotification } from 'Common/Translator';
import { PackageAdminStore } from 'Stores/Admin/Package';
import Remote from 'Remote/Admin/Fetch';
-export class PackagesAdminSettings {
+import { showScreenPopup } from 'Knoin/Knoin';
+import { PluginPopupView } from 'View/Popup/Plugin';
+import { addObservablesTo, addComputablesTo } from 'External/ko';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
+
+export class AdminSettingsPackages extends AbstractViewSettings {
constructor() {
- this.packagesError = ko.observable('');
+ super();
+
+ this.addSettings(['EnabledPlugins']);
+
+ addObservablesTo(this, {
+ packagesError: ''
+ });
this.packages = PackageAdminStore;
- this.packagesCurrent = ko.computed(() =>
- PackageAdminStore.filter(item => item && item.installed && !item.compare)
- );
- this.packagesAvailableForUpdate = ko.computed(() =>
- PackageAdminStore.filter(item => item && item.installed && !!item.compare)
- );
- this.packagesAvailableForInstallation = ko.computed(() =>
- PackageAdminStore.filter(item => item && !item.installed)
- );
+ addComputablesTo(this, {
+ packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
+ packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
+ packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
- this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden'));
+ visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
+ });
}
onShow() {
this.packagesError('');
}
- onBuild() {
+ onBuild(oDom) {
PackageAdminStore.fetch();
+
+ oDom.addEventListener('click', event => {
+ // configurePlugin
+ let el = event.target.closestWithin('.package-configure', oDom),
+ data = el ? ko.dataFor(el) : 0;
+ data && Remote.request('AdminPluginLoad',
+ (iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
+ {
+ Id: data.id
+ }
+ );
+ // disablePlugin
+ el = event.target.closestWithin('.package-active', oDom);
+ data = el ? ko.dataFor(el) : 0;
+ data && this.disablePlugin(data);
+ });
}
requestHelper(packageToRequest, install) {
return (iError, data) => {
- if (iError) {
- this.packagesError(
- getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
-// ':\n' + getNotification(iError);
- );
- }
-
PackageAdminStore.forEach(item => {
if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) {
packageToRequest.loading(false);
@@ -49,7 +65,12 @@ export class PackagesAdminSettings {
}
});
- if (!iError && data.Result.Reload) {
+ if (iError) {
+ this.packagesError(
+ getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
+ + (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
+ );
+ } else if (data.Result.Reload) {
location.reload();
} else {
PackageAdminStore.fetch();
@@ -60,14 +81,49 @@ export class PackagesAdminSettings {
deletePackage(packageToDelete) {
if (packageToDelete) {
packageToDelete.loading(true);
- Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete);
+ Remote.request('AdminPackageDelete',
+ this.requestHelper(packageToDelete, false),
+ {
+ Id: packageToDelete.id
+ }
+ );
}
}
installPackage(packageToInstall) {
if (packageToInstall) {
packageToInstall.loading(true);
- Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall);
+ Remote.request('AdminPackageInstall',
+ this.requestHelper(packageToInstall, true),
+ {
+ Id: packageToInstall.id,
+ Type: packageToInstall.type,
+ File: packageToInstall.file
+ },
+ 60000
+ );
}
}
+
+ disablePlugin(plugin) {
+ let disable = plugin.enabled();
+ plugin.enabled(!disable);
+ Remote.request('AdminPluginDisable',
+ (iError, data) => {
+ if (iError) {
+ plugin.enabled(disable);
+ this.packagesError(
+ (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
+ ? data.ErrorMessage
+ : getNotification(iError)
+ );
+ }
+// PackageAdminStore.fetch();
+ }, {
+ Id: plugin.id,
+ Disabled: disable ? 1 : 0
+ }
+ );
+ }
+
}
diff --git a/dev/Settings/Admin/Plugins.js b/dev/Settings/Admin/Plugins.js
deleted file mode 100644
index 4cd2eb49c..000000000
--- a/dev/Settings/Admin/Plugins.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import ko from 'ko';
-
-import { Notification } from 'Common/Enums';
-import { SettingsGet } from 'Common/Globals';
-import { getNotification } from 'Common/Translator';
-
-import { showScreenPopup } from 'Knoin/Knoin';
-
-import { PluginAdminStore } from 'Stores/Admin/Plugin';
-
-import Remote from 'Remote/Admin/Fetch';
-
-import { PluginPopupView } from 'View/Popup/Plugin';
-
-export class PluginsAdminSettings {
- constructor() {
- this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
-
- this.plugins = PluginAdminStore;
- this.pluginsError = PluginAdminStore.error;
-
- this.onPluginLoadRequest = this.onPluginLoadRequest.bind(this);
- this.onPluginDisableRequest = this.onPluginDisableRequest.bind(this);
-
- this.enabledPlugins.subscribe(value =>
- Remote.saveAdminConfig(null, {
- EnabledPlugins: value ? 1 : 0
- })
- );
- }
-
- disablePlugin(plugin) {
- plugin.disabled(!plugin.disabled());
- Remote.pluginDisable(this.onPluginDisableRequest, plugin.name, plugin.disabled());
- }
-
- configurePlugin(plugin) {
- Remote.plugin(this.onPluginLoadRequest, plugin.name);
- }
-
- onBuild(oDom) {
- oDom.addEventListener('click', event => {
- let el = event.target.closestWithin('.e-item .configure-plugin-action', oDom);
- el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el));
-
- el = event.target.closestWithin('.e-item .disabled-plugin', oDom);
- el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el));
- });
- }
-
- onShow() {
- PluginAdminStore.error('');
- PluginAdminStore.fetch();
- }
-
- onPluginLoadRequest(iError, data) {
- if (!iError) {
- showScreenPopup(PluginPopupView, [data.Result]);
- }
- }
-
- onPluginDisableRequest(iError, data) {
- if (iError) {
- if (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) {
- PluginAdminStore.error(data.ErrorMessage);
- } else {
- PluginAdminStore.error(getNotification(iError));
- }
- }
-
- PluginAdminStore.fetch();
- }
-}
diff --git a/dev/Settings/Admin/Security.js b/dev/Settings/Admin/Security.js
index b1a582db2..a1ba65b6f 100644
--- a/dev/Settings/Admin/Security.js
+++ b/dev/Settings/Admin/Security.js
@@ -1,34 +1,40 @@
-import { Capa } from 'Common/Enums';
-import { Settings, SettingsGet } from 'Common/Globals';
-import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
+import { SettingsGet, SettingsCapa } from 'Common/Globals';
+import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
-export class SecurityAdminSettings {
+export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
+ super();
+
+ this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
+
this.weakPassword = rl.app.weakPassword;
addObservablesTo(this, {
- useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
-
- verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
- allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
-
adminLogin: SettingsGet('AdminLogin'),
adminLoginError: false,
adminPassword: '',
adminPasswordNew: '',
adminPasswordNew2: '',
adminPasswordNewError: false,
+ adminTOTP: SettingsGet('AdminTOTP'),
adminPasswordUpdateError: false,
adminPasswordUpdateSuccess: false,
- capaOpenPGP: Settings.capa(Capa.OpenPGP)
+ capaOpenPGP: SettingsCapa('OpenPGP')
});
+ const reset = () => {
+ this.adminPasswordUpdateError(false);
+ this.adminPasswordUpdateSuccess(false);
+ this.adminPasswordNewError(false);
+ };
+
addSubscribablesTo(this, {
adminPassword: () => {
this.adminPasswordUpdateError(false);
@@ -37,43 +43,13 @@ export class SecurityAdminSettings {
adminLogin: () => this.adminLoginError(false),
- adminPasswordNew: () => {
- this.adminPasswordUpdateError(false);
- this.adminPasswordUpdateSuccess(false);
- this.adminPasswordNewError(false);
- },
+ adminPasswordNew: reset,
- adminPasswordNew2: () => {
- this.adminPasswordUpdateError(false);
- this.adminPasswordUpdateSuccess(false);
- this.adminPasswordNewError(false);
- },
+ adminPasswordNew2: reset,
- capaOpenPGP: value =>
- Remote.saveAdminConfig(null, {
- CapaOpenPGP: value ? 1 : 0
- }),
-
- useLocalProxyForExternalImages: value =>
- Remote.saveAdminConfig(null, {
- UseLocalProxyForExternalImages: value ? 1 : 0
- }),
-
- verifySslCertificate: value => {
- value => value || this.allowSelfSigned(true);
- Remote.saveAdminConfig(null, {
- VerifySslCertificate: value ? 1 : 0
- });
- },
-
- allowSelfSigned: value =>
- Remote.saveAdminConfig(null, {
- AllowSelfSigned: value ? 1 : 0
- })
+ capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
});
- this.onNewAdminPasswordResponse = this.onNewAdminPasswordResponse.bind(this);
-
decorateKoCommands(this, {
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword()
});
@@ -93,29 +69,28 @@ export class SecurityAdminSettings {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
- Remote.saveNewAdminPassword(this.onNewAdminPasswordResponse, {
+ Remote.request('AdminPasswordUpdate', (iError, data) => {
+ if (iError) {
+ this.adminPasswordUpdateError(true);
+ } else {
+ this.adminPassword('');
+ this.adminPasswordNew('');
+ this.adminPasswordNew2('');
+
+ this.adminPasswordUpdateSuccess(true);
+
+ this.weakPassword(!!data.Result.Weak);
+ }
+ }, {
'Login': this.adminLogin(),
'Password': this.adminPassword(),
- 'NewPassword': this.adminPasswordNew()
+ 'NewPassword': this.adminPasswordNew(),
+ 'TOTP': this.adminTOTP()
});
return true;
}
- onNewAdminPasswordResponse(iError, data) {
- if (iError) {
- this.adminPasswordUpdateError(true);
- } else {
- this.adminPassword('');
- this.adminPasswordNew('');
- this.adminPasswordNew2('');
-
- this.adminPasswordUpdateSuccess(true);
-
- this.weakPassword(!!data.Result.Weak);
- }
- }
-
onHide() {
this.adminPassword('');
this.adminPasswordNew('');
diff --git a/dev/Settings/User/Accounts.js b/dev/Settings/User/Accounts.js
index a55e9a09a..b7baa37da 100644
--- a/dev/Settings/User/Accounts.js
+++ b/dev/Settings/User/Accounts.js
@@ -1,7 +1,6 @@
import ko from 'ko';
-import { Capa } from 'Common/Enums';
-import { Settings } from 'Common/Globals';
+import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
@@ -12,17 +11,18 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account';
import { IdentityPopupView } from 'View/Popup/Identity';
-export class AccountsUserSettings {
+export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() {
- this.allowAdditionalAccount = Settings.capa(Capa.AdditionalAccounts);
- this.allowIdentities = Settings.capa(Capa.Identities);
+ this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
+ this.allowIdentities = SettingsCapa('Identities');
this.accounts = AccountUserStore.accounts;
this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore;
+ this.mainEmail = SettingsGet('MainEmail');
- this.accountForDeletion = ko.observable(null).deleteAccessHelper();
- this.identityForDeletion = ko.observable(null).deleteAccessHelper();
+ this.accountForDeletion = ko.observable(null).askDeleteHelper();
+ this.identityForDeletion = ko.observable(null).askDeleteHelper();
}
addNewAccount() {
@@ -30,7 +30,7 @@ export class AccountsUserSettings {
}
editAccount(account) {
- if (account && account.canBeEdit()) {
+ if (account && account.isAdditional()) {
showScreenPopup(AccountPopupView, [account]);
}
}
@@ -48,19 +48,21 @@ export class AccountsUserSettings {
* @returns {void}
*/
deleteAccount(accountToRemove) {
- if (accountToRemove && accountToRemove.deleteAccess()) {
+ if (accountToRemove && accountToRemove.askDelete()) {
this.accountForDeletion(null);
if (accountToRemove) {
this.accounts.remove((account) => accountToRemove === account);
- Remote.accountDelete((iError, data) => {
+ Remote.request('AccountDelete', (iError, data) => {
if (!iError && data.Reload) {
rl.route.root();
setTimeout(() => location.reload(), 1);
} else {
rl.app.accountsAndIdentities();
}
- }, accountToRemove.email);
+ }, {
+ EmailToDelete: accountToRemove.email
+ });
}
}
}
@@ -70,26 +72,31 @@ export class AccountsUserSettings {
* @returns {void}
*/
deleteIdentity(identityToRemove) {
- if (identityToRemove && identityToRemove.deleteAccess()) {
+ if (identityToRemove && identityToRemove.askDelete()) {
this.identityForDeletion(null);
if (identityToRemove) {
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
- Remote.identityDelete(() => rl.app.accountsAndIdentities(), identityToRemove.id());
+ Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
+ IdToDelete: identityToRemove.id()
+ });
}
}
}
accountsAndIdentitiesAfterMove() {
- Remote.accountsAndIdentitiesSortOrder(null, AccountUserStore.getEmailAddresses(), IdentityUserStore.getIDS());
+ Remote.request('AccountsAndIdentitiesSortOrder', null, {
+ Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')),
+ Identities: IdentityUserStore.getIDS()
+ });
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
- let el = event.target.closestWithin('.accounts-list .account-item .e-action', oDom);
+ let el = event.target.closestWithin('.accounts-list .e-action', oDom);
el && ko.dataFor(el) && this.editAccount(ko.dataFor(el));
- el = event.target.closestWithin('.identities-list .identity-item .e-action', oDom);
+ el = event.target.closestWithin('.identities-list .e-action', oDom);
el && ko.dataFor(el) && this.editIdentity(ko.dataFor(el));
});
}
diff --git a/dev/Settings/User/Contacts.js b/dev/Settings/User/Contacts.js
index 2769b6320..22be1cdbe 100644
--- a/dev/Settings/User/Contacts.js
+++ b/dev/Settings/User/Contacts.js
@@ -1,10 +1,11 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals';
import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch';
-export class ContactsUserSettings {
+export class UserSettingsContacts /*extends AbstractViewSettings*/ {
constructor() {
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
@@ -14,8 +15,7 @@ export class ContactsUserSettings {
this.contactsSyncUser = ContactUserStore.syncUser;
this.contactsSyncPass = ContactUserStore.syncPass;
- this.saveTrigger = ko
- .computed(() =>
+ this.saveTrigger = koComputable(() =>
[
ContactUserStore.enableSync() ? '1' : '0',
ContactUserStore.syncUrl(),
@@ -26,19 +26,16 @@ export class ContactsUserSettings {
.extend({ debounce: 500 });
this.contactsAutosave.subscribe(value =>
- Remote.saveSettings(null, {
- ContactsAutosave: value ? 1 : 0
- })
+ Remote.saveSettings(null, { ContactsAutosave: value })
);
this.saveTrigger.subscribe(() =>
- Remote.saveContactsSyncData(
- null,
- ContactUserStore.enableSync(),
- ContactUserStore.syncUrl(),
- ContactUserStore.syncUser(),
- ContactUserStore.syncPass()
- )
+ Remote.request('SaveContactsSyncData', null, {
+ Enable: ContactUserStore.enableSync() ? 1 : 0,
+ Url: ContactUserStore.syncUrl(),
+ User: ContactUserStore.syncUser(),
+ Password: ContactUserStore.syncPass()
+ })
);
}
}
diff --git a/dev/Settings/User/Filters.js b/dev/Settings/User/Filters.js
index 212302659..7010603bc 100644
--- a/dev/Settings/User/Filters.js
+++ b/dev/Settings/User/Filters.js
@@ -1,100 +1,44 @@
-import ko from 'ko';
+import { addObservablesTo } from 'External/ko';
+import { FolderUserStore } from 'Stores/User/Folder';
+import { SettingsGet } from 'Common/Globals';
-import { getNotification } from 'Common/Translator';
-import { addObservablesTo } from 'Common/Utils';
-import { delegateRunOnDestroy } from 'Common/UtilsUser';
-
-import { SieveUserStore } from 'Stores/User/Sieve';
-import Remote from 'Remote/User/Fetch';
-
-import { SieveScriptModel } from 'Model/SieveScript';
-
-import { showScreenPopup } from 'Knoin/Knoin';
-
-import { SieveScriptPopupView } from 'View/Popup/SieveScript';
-
-export class FiltersUserSettings {
+//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
+export class UserSettingsFilters /*extends AbstractViewSettings*/ {
constructor() {
- this.scripts = SieveUserStore.scripts;
- this.loading = ko.observable(false).extend({ debounce: 200 });
-
+ this.scripts = ko.observableArray();
+ this.loading = ko.observable(true).extend({ debounce: 200 });
addObservablesTo(this, {
serverError: false,
serverErrorDesc: ''
});
- this.scriptForDeletion = ko.observable(null).deleteAccessHelper();
- }
+ rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/sieve.')).then(() => {
+ const Sieve = window.Sieve;
+ Sieve.folderList = FolderUserStore.folderList;
+ Sieve.serverError.subscribe(value => this.serverError(value));
+ Sieve.serverErrorDesc.subscribe(value => this.serverErrorDesc(value));
+ Sieve.loading.subscribe(value => this.loading(value));
+ Sieve.scripts.subscribe(value => this.scripts(value));
+ Sieve.updateList();
+ }).catch(e => console.error(e));
- setError(text) {
- this.serverError(true);
- this.serverErrorDesc(text);
- }
-
- updateList() {
- if (!this.loading()) {
- this.loading(true);
- this.serverError(false);
-
- Remote.filtersGet((iError, data) => {
- this.loading(false);
- this.scripts([]);
-
- if (iError) {
- SieveUserStore.capa([]);
- this.setError(getNotification(iError));
- } else {
- SieveUserStore.capa(data.Result.Capa);
-/*
- this.scripts(
- data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
- );
-*/
- Object.values(data.Result.Scripts).forEach(value => {
- value = SieveScriptModel.reviveFromJson(value);
- value && this.scripts.push(value)
- });
- }
- });
- }
+ this.scriptForDeletion = ko.observable(null).askDeleteHelper();
}
addScript() {
- showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]);
+ this.editScript();
}
editScript(script) {
- showScreenPopup(SieveScriptPopupView, [script]);
+ window.Sieve.ScriptView.showModal(script ? [script] : null);
}
deleteScript(script) {
- this.serverError(false);
- Remote.filtersScriptDelete(
- (iError, data) => {
- if (iError) {
- this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
- } else {
- this.scripts.remove(script);
- delegateRunOnDestroy(script);
- }
- },
- script.name()
- );
+ window.Sieve.deleteScript(script);
}
toggleScript(script) {
- let name = script.active() ? '' : script.name();
- this.serverError(false);
- Remote.filtersScriptActivate(
- (iError, data) => {
- if (iError) {
- this.setError((data && data.ErrorMessageAdditional) || iError)
- } else {
- this.scripts.forEach(script => script.active(script.name() === name));
- }
- },
- name
- );
+ window.Sieve.toggleScript(script);
}
onBuild(oDom) {
@@ -106,6 +50,6 @@ export class FiltersUserSettings {
}
onShow() {
- this.updateList();
+ window.Sieve && window.Sieve.updateList();
}
}
diff --git a/dev/Settings/User/Folders.js b/dev/Settings/User/Folders.js
index 7f30d88cb..d5e2b9aa2 100644
--- a/dev/Settings/User/Folders.js
+++ b/dev/Settings/User/Folders.js
@@ -1,13 +1,15 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums';
-import { ClientSideKeyName } from 'Common/EnumsUser';
-import { Settings } from 'Common/Globals';
+import { FolderMetadataKeys } from 'Common/EnumsUser';
+import { SettingsCapa } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
-import { removeFolderFromCacheList } from 'Common/Cache';
-
-import * as Local from 'Storage/Client';
+import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
+import { defaultOptionsAfterRender } from 'Common/Utils';
+import { sortFolders } from 'Common/Folders';
+import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
@@ -18,16 +20,36 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
+import { loadFolders } from 'Model/FolderCollection';
-export class FoldersUserSettings {
+const folderForDeletion = ko.observable(null).askDeleteHelper();
+
+export class UserSettingsFolders /*extends AbstractViewSettings*/ {
constructor() {
+ this.showKolab = koComputable(() => FolderUserStore.hasCapability('METADATA') && SettingsCapa('Kolab'));
+ this.defaultOptionsAfterRender = defaultOptionsAfterRender;
+ this.kolabTypeOptions = ko.observableArray();
+ let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
+ initOnStartOrLangChange(()=>{
+ this.kolabTypeOptions([
+ { id: '', name: '' },
+ { id: 'event', name: i18nFilter('CALENDAR') },
+ { id: 'contact', name: i18nFilter('CONTACTS') },
+ { id: 'task', name: i18nFilter('TASKS') },
+ { id: 'note', name: i18nFilter('NOTES') },
+ { id: 'file', name: i18nFilter('FILES') },
+ { id: 'journal', name: i18nFilter('JOURNAL') },
+ { id: 'configuration', name: i18nFilter('CONFIGURATION') }
+ ]);
+ });
+
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
this.folderList = FolderUserStore.folderList;
this.folderListOptimized = FolderUserStore.folderListOptimized;
this.folderListError = FolderUserStore.folderListError;
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
- this.loading = ko.computed(() => {
+ this.loading = koComputable(() => {
const loading = FolderUserStore.foldersLoading(),
creating = FolderUserStore.foldersCreating(),
deleting = FolderUserStore.foldersDeleting(),
@@ -36,28 +58,43 @@ export class FoldersUserSettings {
return loading || creating || deleting || renaming;
});
- this.folderForDeletion = ko.observable(null).deleteAccessHelper();
+ this.folderForDeletion = folderForDeletion;
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
- this.useImapSubscribe = Settings.app('useImapSubscribe');
- this.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value ? 1 : 0));
+ SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
}
folderEditOnEnter(folder) {
const nameToEdit = folder ? folder.nameForEdit().trim() : '';
if (nameToEdit && folder.name() !== nameToEdit) {
- Local.set(ClientSideKeyName.FoldersLashHash, '');
-
- rl.app.foldersPromisesActionHelper(
- Remote.folderRename(folder.fullNameRaw, nameToEdit),
- Notification.CantRenameFolder
- );
-
- removeFolderFromCacheList(folder.fullNameRaw);
-
- folder.name(nameToEdit);
+ Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
+ Folder: folder.fullName,
+ NewFolderName: nameToEdit,
+ Subscribe: folder.subscribed() ? 1 : 0
+ })
+ .then(data => {
+ folder.name(nameToEdit/*data.Name*/);
+ if (folder.subFolders.length) {
+ Remote.setTrigger(FolderUserStore.foldersLoading, true);
+// clearTimeout(Remote.foldersTimeout);
+// Remote.foldersTimeout = setTimeout(loadFolders, 500);
+ setTimeout(loadFolders, 500);
+ // TODO: rename all subfolders with folder.delimiter to prevent reload?
+ } else {
+ removeFolderFromCacheList(folder.fullName);
+ folder.fullName = data.Result.FullName;
+ setFolder(folder);
+ const parent = getFolderFromCacheList(folder.parentName);
+ sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
+ }
+ })
+ .catch(error => {
+ FolderUserStore.folderListError(
+ getNotification(error.code, '', Notification.CantRenameFolder)
+ + '.\n' + error.message);
+ });
}
folder.edited(false);
@@ -85,58 +122,69 @@ export class FoldersUserSettings {
}
deleteFolder(folderToRemove) {
- if (
- folderToRemove &&
- folderToRemove.canBeDeleted() &&
- folderToRemove.deleteAccess() &&
- 0 === folderToRemove.privateMessageCountAll()
+ if (folderToRemove
+ && folderToRemove.canBeDeleted()
+ && folderToRemove.askDelete()
) {
- this.folderForDeletion(null);
+ if (0 < folderToRemove.privateMessageCountAll()) {
+// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
+ folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
+ } else {
+ folderForDeletion(null);
- if (folderToRemove) {
- const fRemoveFolder = function(folder) {
- if (folderToRemove === folder) {
- return true;
- }
- folder.subFolders.remove(fRemoveFolder);
- return false;
- };
-
- Local.set(ClientSideKeyName.FoldersLashHash, '');
-
- FolderUserStore.folderList.remove(fRemoveFolder);
-
- rl.app.foldersPromisesActionHelper(
- Remote.folderDelete(folderToRemove.fullNameRaw),
- Notification.CantDeleteFolder
- );
-
- removeFolderFromCacheList(folderToRemove.fullNameRaw);
+ if (folderToRemove) {
+ Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
+ Folder: folderToRemove.fullName
+ }).then(
+ () => {
+// folderToRemove.flags.push('\\nonexistent');
+ folderToRemove.selectable(false);
+// folderToRemove.subscribed(false);
+// folderToRemove.checkable(false);
+ if (!folderToRemove.subFolders.length) {
+ removeFolderFromCacheList(folderToRemove.fullName);
+ const folder = getFolderFromCacheList(folderToRemove.parentName);
+ (folder ? folder.subFolders : FolderUserStore.folderList).remove(folderToRemove);
+ }
+ },
+ error => {
+ FolderUserStore.folderListError(
+ getNotification(error.code, '', Notification.CantDeleteFolder)
+ + '.\n' + error.message
+ );
+ }
+ );
+ }
}
- } else if (0 < folderToRemove.privateMessageCountAll()) {
- FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
}
}
- subscribeFolder(folder) {
- Local.set(ClientSideKeyName.FoldersLashHash, '');
- Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, true);
- folder.subscribed(true);
+ toggleFolderKolabType(folder, event) {
+ let type = event.target.value;
+ // TODO: append '.default' ?
+ Remote.request('FolderSetMetadata', null, {
+ Folder: folder.fullName,
+ Key: FolderMetadataKeys.KolabFolderType,
+ Value: type
+ });
+ folder.kolabType(type);
}
- unSubscribeFolder(folder) {
- Local.set(ClientSideKeyName.FoldersLashHash, '');
- Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, false);
- folder.subscribed(false);
+ toggleFolderSubscription(folder) {
+ let subscribe = !folder.subscribed();
+ Remote.request('FolderSubscribe', null, {
+ Folder: folder.fullName,
+ Subscribe: subscribe ? 1 : 0
+ });
+ folder.subscribed(subscribe);
}
- checkableTrueFolder(folder) {
- Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, true);
- folder.checkable(true);
- }
-
- checkableFalseFolder(folder) {
- Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, false);
- folder.checkable(false);
+ toggleFolderCheckable(folder) {
+ let checkable = !folder.checkable();
+ Remote.request('FolderCheckable', null, {
+ Folder: folder.fullName,
+ Checkable: checkable ? 1 : 0
+ });
+ folder.checkable(checkable);
}
}
diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js
index ed49b0af6..28cb69176 100644
--- a/dev/Settings/User/General.js
+++ b/dev/Settings/User/General.js
@@ -1,12 +1,13 @@
import ko from 'ko';
-import { MESSAGES_PER_PAGE_VALUES } from 'Common/Consts';
import { SaveSettingsStep } from 'Common/Enums';
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
-import { SettingsGet } from 'Common/Globals';
-import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
-import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator';
+import { Settings, SettingsGet } from 'Common/Globals';
+import { isArray } from 'Common/Utils';
+import { addSubscribablesTo, addComputablesTo } from 'External/ko';
+import { i18n, trigger as translatorTrigger, translatorReload, convertLangName } from 'Common/Translator';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin';
import { AppUserStore } from 'Stores/User/App';
@@ -15,27 +16,33 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification';
import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages';
-export class GeneralUserSettings {
+export class UserSettingsGeneral extends AbstractViewSettings {
constructor() {
+ super();
+
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
+ this.messageReadDelay = SettingsUserStore.messageReadDelay;
this.messagesPerPage = SettingsUserStore.messagesPerPage;
- this.messagesPerPageArray = MESSAGES_PER_PAGE_VALUES;
this.editorDefaultType = SettingsUserStore.editorDefaultType;
this.layout = SettingsUserStore.layout;
- this.enableSoundNotification = NotificationUserStore.enableSoundNotification;
+ this.soundNotification = NotificationUserStore.enableSoundNotification;
+ this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
+ this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
- this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification;
- this.isDesktopNotificationDenied = NotificationUserStore.isDesktopNotificationDenied;
+ this.desktopNotification = NotificationUserStore.enableDesktopNotification;
+ this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
+ this.viewHTML = SettingsUserStore.viewHTML;
this.showImages = SettingsUserStore.showImages;
this.removeColors = SettingsUserStore.removeColors;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
@@ -44,97 +51,87 @@ export class GeneralUserSettings {
this.replySameFolder = SettingsUserStore.replySameFolder;
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
- this.languageFullName = ko.computed(() => convertLangName(this.language()));
- this.languageTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
-
- addObservablesTo(this, {
- mppTrigger: SaveSettingsStep.Idle,
- editorDefaultTypeTrigger: SaveSettingsStep.Idle,
- layoutTrigger: SaveSettingsStep.Idle
- });
+ this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
this.identities = IdentityUserStore;
- this.identityMain = ko.computed(() => {
- const list = this.identities();
- return isArray(list) ? list.find(item => item && !item.id()) : null;
+ addComputablesTo(this, {
+ languageFullName: () => convertLangName(this.language()),
+
+ identityMain: () => {
+ const list = this.identities();
+ return isArray(list) ? list.find(item => item && !item.id()) : null;
+ },
+
+ identityMainDesc: () => {
+ const identity = this.identityMain();
+ return identity ? identity.formattedName() : '---';
+ },
+
+ editorDefaultTypes: () => {
+ translatorTrigger();
+ return [
+ { id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
+ { id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
+ { id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
+ { id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
+ ];
+ },
+
+ layoutTypes: () => {
+ translatorTrigger();
+ return [
+ { id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
+ { id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
+ { id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
+ ];
+ }
});
- this.identityMainDesc = ko.computed(() => {
- const identity = this.identityMain();
- return identity ? identity.formattedName() : '---';
- });
+ this.addSetting('EditorDefaultType');
+ this.addSetting('MessageReadDelay');
+ this.addSetting('MessagesPerPage');
+ this.addSetting('Layout', () => MessagelistUserStore([]));
- this.editorDefaultTypes = ko.computed(() => {
- translatorTrigger();
- return [
- { id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
- { id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
- { id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
- { id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
- ];
- });
-
- this.layoutTypes = ko.computed(() => {
- translatorTrigger();
- return [
- { id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
- { id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
- { id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
- ];
- });
+ this.addSettings(['ViewHTML', 'ShowImages', 'UseCheckboxesInList', 'ReplySameFolder',
+ 'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => {
this.languageTrigger(saveSettingsStep);
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
};
+
addSubscribablesTo(this, {
language: value => {
this.languageTrigger(SaveSettingsStep.Animate);
translatorReload(false, value)
- .then(fReloadLanguageHelper(SaveSettingsStep.TrueResult),
- fReloadLanguageHelper(SaveSettingsStep.FalseResult))
+ .then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveSetting('Language', value));
},
- editorDefaultType: value => Remote.saveSetting('EditorDefaultType', value,
- settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this)),
-
- messagesPerPage: value => Remote.saveSetting('MPP', value,
- settingsSaveHelperSimpleFunction(this.mppTrigger, this)),
-
- showImages: value => Remote.saveSetting('ShowImages', value ? 1 : 0),
-
removeColors: value => {
- MessageUserStore.messagesBodiesDom().innerHTML = '';
- Remote.saveSetting('RemoveColors', value ? 1 : 0);
+ let dom = MessageUserStore.bodiesDom();
+ if (dom) {
+ dom.innerHTML = '';
+ }
+ Remote.saveSetting('RemoveColors', value);
},
- useCheckboxesInList: value => Remote.saveSetting('UseCheckboxesInList', value ? 1 : 0),
-
- enableDesktopNotification: value => Remote.saveSetting('DesktopNotifications', value ? 1 : 0),
-
- enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0),
-
- replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
+ notificationSound: value => {
+ Remote.saveSetting('NotificationSound', value);
+ Settings.set('NotificationSound', value);
+ },
useThreads: value => {
- MessageUserStore.list([]);
- Remote.saveSetting('UseThreads', value ? 1 : 0);
- },
-
- layout: value => {
- MessageUserStore.list([]);
- Remote.saveSetting('Layout', value, settingsSaveHelperSimpleFunction(this.layoutTrigger, this));
+ MessagelistUserStore([]);
+ Remote.saveSetting('UseThreads', value);
}
});
}
editMainIdentity() {
const identity = this.identityMain();
- if (identity) {
- showScreenPopup(IdentityPopupView, [identity]);
- }
+ identity && showScreenPopup(IdentityPopupView, [identity]);
}
testSoundNotification() {
diff --git a/dev/Settings/User/OpenPgp.js b/dev/Settings/User/OpenPgp.js
deleted file mode 100644
index 7a50de7bf..000000000
--- a/dev/Settings/User/OpenPgp.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import ko from 'ko';
-
-import { delegateRunOnDestroy } from 'Common/UtilsUser';
-
-import { PgpUserStore } from 'Stores/User/Pgp';
-import { SettingsUserStore } from 'Stores/User/Settings';
-
-import Remote from 'Remote/User/Fetch';
-
-import { showScreenPopup } from 'Knoin/Knoin';
-
-import { AddOpenPgpKeyPopupView } from 'View/Popup/AddOpenPgpKey';
-import { NewOpenPgpKeyPopupView } from 'View/Popup/NewOpenPgpKey';
-import { ViewOpenPgpKeyPopupView } from 'View/Popup/ViewOpenPgpKey';
-
-export class OpenPgpUserSettings {
- constructor() {
- this.openpgpkeys = PgpUserStore.openpgpkeys;
- this.openpgpkeysPublic = PgpUserStore.openpgpkeysPublic;
- this.openpgpkeysPrivate = PgpUserStore.openpgpkeysPrivate;
-
- this.openPgpKeyForDeletion = ko.observable(null).deleteAccessHelper();
-
- this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
-
- this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value ? 1 : 0))
- }
-
- addOpenPgpKey() {
- showScreenPopup(AddOpenPgpKeyPopupView);
- }
-
- generateOpenPgpKey() {
- showScreenPopup(NewOpenPgpKeyPopupView);
- }
-
- viewOpenPgpKey(openPgpKey) {
- if (openPgpKey) {
- showScreenPopup(ViewOpenPgpKeyPopupView, [openPgpKey]);
- }
- }
-
- /**
- * @param {OpenPgpKeyModel} openPgpKeyToRemove
- * @returns {void}
- */
- deleteOpenPgpKey(openPgpKeyToRemove) {
- if (openPgpKeyToRemove && openPgpKeyToRemove.deleteAccess()) {
- this.openPgpKeyForDeletion(null);
-
- if (openPgpKeyToRemove && PgpUserStore.openpgpKeyring) {
- const findedItem = PgpUserStore.openpgpkeys.find(key => openPgpKeyToRemove === key);
- if (findedItem) {
- PgpUserStore.openpgpkeys.remove(findedItem);
- delegateRunOnDestroy(findedItem);
-
- PgpUserStore.openpgpKeyring[findedItem.isPrivate ? 'privateKeys' : 'publicKeys'].removeForId(findedItem.guid);
-
- PgpUserStore.openpgpKeyring.store();
- }
-
- rl.app.reloadOpenPgpKeys();
- }
- }
- }
-}
diff --git a/dev/Settings/User/Security.js b/dev/Settings/User/Security.js
index 6c37c3a4f..798be119b 100644
--- a/dev/Settings/User/Security.js
+++ b/dev/Settings/User/Security.js
@@ -1,23 +1,32 @@
-import ko from 'ko';
+import { koComputable } from 'External/ko';
-import { pInt, settingsSaveHelperSimpleFunction } from 'Common/Utils';
-import { Capa, SaveSettingsStep } from 'Common/Enums';
-import { Settings } from 'Common/Globals';
+import { SettingsCapa } from 'Common/Globals';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
+import { AbstractViewSettings } from 'Knoin/AbstractViews';
+
import { SettingsUserStore } from 'Stores/User/Settings';
+import { GnuPGUserStore } from 'Stores/User/GnuPG';
+import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
+
import Remote from 'Remote/User/Fetch';
-export class SecurityUserSettings {
+import { showScreenPopup } from 'Knoin/Knoin';
+
+import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
+import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
+
+export class UserSettingsSecurity extends AbstractViewSettings {
constructor() {
- this.capaAutoLogout = Settings.capa(Capa.AutoLogout);
+ super();
+
+ this.capaAutoLogout = SettingsCapa('AutoLogout');
this.autoLogout = SettingsUserStore.autoLogout;
- this.autoLogoutTrigger = ko.observable(SaveSettingsStep.Idle);
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
- this.autoLogoutOptions = ko.computed(() => {
+ this.autoLogoutOptions = koComputable(() => {
translatorTrigger();
return [
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
@@ -32,10 +41,37 @@ export class SecurityUserSettings {
});
if (this.capaAutoLogout) {
- this.autoLogout.subscribe(value => Remote.saveSetting(
- 'AutoLogout', pInt(value),
- settingsSaveHelperSimpleFunction(this.autoLogoutTrigger, this)
- ));
+ this.addSetting('AutoLogout');
}
+
+ this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
+ this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
+
+ this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
+ this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
+
+ this.canOpenPGP = SettingsCapa('OpenPGP');
+ this.canGnuPG = GnuPGUserStore.isSupported();
+ this.canMailvelope = !!window.mailvelope;
+
+ this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
+
+ this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
+ }
+
+ addOpenPgpKey() {
+ showScreenPopup(OpenPgpImportPopupView);
+ }
+
+ generateOpenPgpKey() {
+ showScreenPopup(OpenPgpGeneratePopupView);
+ }
+
+ onBuild() {
+ /**
+ * Create an iframe to display the Mailvelope keyring settings.
+ * The iframe will be injected into the container identified by selector.
+ */
+ window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
}
}
diff --git a/dev/Settings/User/Templates.js b/dev/Settings/User/Templates.js
deleted file mode 100644
index ad7b45981..000000000
--- a/dev/Settings/User/Templates.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import ko from 'ko';
-
-import { i18n } from 'Common/Translator';
-
-import { TemplateUserStore } from 'Stores/User/Template';
-import Remote from 'Remote/User/Fetch';
-
-import { showScreenPopup } from 'Knoin/Knoin';
-
-import { TemplatePopupView } from 'View/Popup/Template';
-
-export class TemplatesUserSettings {
- constructor() {
- this.templates = TemplateUserStore.templates;
-
- this.processText = ko.computed(() =>
- TemplateUserStore.templates.loading() ? i18n('SETTINGS_TEMPLETS/LOADING_PROCESS') : ''
- );
- this.visibility = ko.computed(() => this.processText() ? 'visible' : 'hidden');
-
- this.templateForDeletion = ko.observable(null).deleteAccessHelper();
- }
-
- addNewTemplate() {
- showScreenPopup(TemplatePopupView);
- }
-
- editTemplate(oTemplateItem) {
- if (oTemplateItem) {
- showScreenPopup(TemplatePopupView, [oTemplateItem]);
- }
- }
-
- deleteTemplate(templateToRemove) {
- if (templateToRemove && templateToRemove.deleteAccess()) {
- this.templateForDeletion(null);
-
- if (templateToRemove) {
- this.templates.remove((template) => templateToRemove === template);
-
- Remote.templateDelete(() => {
- this.reloadTemplates();
- }, templateToRemove.id);
- }
- }
- }
-
- reloadTemplates() {
- rl.app.templates();
- }
-
- onBuild(oDom) {
- oDom.addEventListener('click', event => {
- const el = event.target.closestWithin('.templates-list .template-item .e-action', oDom);
- el && ko.dataFor(el) && this.editTemplate(ko.dataFor(el));
- });
-
- this.reloadTemplates();
- }
-}
diff --git a/dev/Settings/User/Themes.js b/dev/Settings/User/Themes.js
index bc5d9ee69..5e61f8b97 100644
--- a/dev/Settings/User/Themes.js
+++ b/dev/Settings/User/Themes.js
@@ -1,33 +1,37 @@
-import ko from 'ko';
+import { addObservablesTo } from 'External/ko';
-import { SaveSettingsStep, UploadErrorCode, Capa } from 'Common/Enums';
+import { SaveSettingsStep, UploadErrorCode } from 'Common/Enums';
import { changeTheme, convertThemeName } from 'Common/Utils';
-import { userBackground, themePreviewLink, serverRequest } from 'Common/Links';
+import { themePreviewLink, serverRequest } from 'Common/Links';
import { i18n } from 'Common/Translator';
-import { doc, $htmlCL, Settings } from 'Common/Globals';
+import { SettingsCapa } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
-export class ThemesUserSettings {
+const themeBackground = {
+ name: ThemeStore.userBackgroundName,
+ hash: ThemeStore.userBackgroundHash
+};
+addObservablesTo(themeBackground, {
+ uploaderButton: null,
+ loading: false,
+ error: ''
+});
+
+export class UserSettingsThemes /*extends AbstractViewSettings*/ {
constructor() {
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
this.themesObjects = ko.observableArray();
- this.background = {};
- this.background.name = ThemeStore.userBackgroundName;
- this.background.hash = ThemeStore.userBackgroundHash;
- this.background.uploaderButton = ko.observable(null);
- this.background.loading = ko.observable(false);
- this.background.error = ko.observable('');
-
- this.capaUserBackground = ko.observable(Settings.capa(Capa.UserBackground));
+ themeBackground.enabled = SettingsCapa('UserBackground');
+ this.background = themeBackground;
this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
- this.theme.subscribe((value) => {
+ ThemeStore.theme.subscribe(value => {
this.themesObjects.forEach(theme => {
theme.selected(value === theme.name);
});
@@ -38,23 +42,13 @@ export class ThemesUserSettings {
Theme: value
});
});
-
- this.background.hash.subscribe((value) => {
- if (!value) {
- $htmlCL.remove('UserBackground');
- doc.body.removeAttribute('style');
- } else {
- $htmlCL.add('UserBackground');
- doc.body.style.backgroundImage = "url("+userBackground(value)+")";
- }
- });
}
onBuild() {
- const currentTheme = this.theme();
+ const currentTheme = ThemeStore.theme();
this.themesObjects(
- this.themes.map(theme => ({
+ ThemeStore.themes.map(theme => ({
name: theme,
nameDisplay: convertThemeName(theme),
selected: ko.observable(theme === currentTheme),
@@ -62,48 +56,30 @@ export class ThemesUserSettings {
}))
);
- this.initUploader();
- }
+ // initUploader
- onShow() {
- this.background.error('');
- }
-
- clearBackground() {
- if (this.capaUserBackground()) {
- Remote.clearUserBackground(() => {
- this.background.name('');
- this.background.hash('');
- });
- }
- }
-
- initUploader() {
- if (this.background.uploaderButton() && this.capaUserBackground()) {
+ if (themeBackground.uploaderButton() && themeBackground.enabled) {
const oJua = new Jua({
action: serverRequest('UploadBackground'),
- name: 'uploader',
- queueSize: 1,
- multipleSizeLimit: 1,
- disableMultiple: true,
- clickElement: this.background.uploaderButton()
+ limit: 1,
+ clickElement: themeBackground.uploaderButton()
});
oJua
.on('onStart', () => {
- this.background.loading(true);
- this.background.error('');
+ themeBackground.loading(true);
+ themeBackground.error('');
return true;
})
.on('onComplete', (id, result, data) => {
- this.background.loading(false);
+ themeBackground.loading(false);
if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) {
- this.background.name(data.Result.Name);
- this.background.hash(data.Result.Hash);
+ themeBackground.name(data.Result.Name);
+ themeBackground.hash(data.Result.Hash);
} else {
- this.background.name('');
- this.background.hash('');
+ themeBackground.name('');
+ themeBackground.hash('');
let errorMsg = '';
if (data.ErrorCode) {
@@ -122,11 +98,24 @@ export class ThemesUserSettings {
errorMsg = data.ErrorMessage;
}
- this.background.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
+ themeBackground.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
}
return true;
});
}
}
+
+ onShow() {
+ themeBackground.error('');
+ }
+
+ clearBackground() {
+ if (themeBackground.enabled) {
+ Remote.request('ClearUserBackground', () => {
+ themeBackground.name('');
+ themeBackground.hash('');
+ });
+ }
+ }
}
diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js
new file mode 100644
index 000000000..4f701d45d
--- /dev/null
+++ b/dev/Sieve/Commands.js
@@ -0,0 +1,185 @@
+/**
+ * https://tools.ietf.org/html/rfc5228#section-2.9
+ */
+
+import { capa } from 'Sieve/Utils';
+
+import {
+ GrammarCommand,
+ GrammarString,
+ GrammarStringList,
+ GrammarQuotedString
+} from 'Sieve/Grammar';
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-3.1
+ * Usage:
+ * if
+ * elsif
+ * else
+ */
+export class ConditionalCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.test = null;
+ }
+
+ toString()
+ {
+ return this.identifier + ' ' + this.test + ' ' + this.commands;
+ }
+/*
+ public function pushArguments(array $args): void
+ {
+ print_r($args);
+ exit;
+ }
+*/
+}
+
+export class IfCommand extends ConditionalCommand
+{
+}
+
+export class ElsIfCommand extends ConditionalCommand
+{
+}
+
+export class ElseCommand extends ConditionalCommand
+{
+ toString()
+ {
+ return this.identifier + ' ' + this.commands;
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-3.2
+ */
+export class RequireCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.capabilities = new GrammarStringList();
+ }
+
+ toString()
+ {
+ return 'require ' + this.capabilities.toString() + ';';
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarStringList) {
+ this.capabilities = args[0];
+ } else if (args[0] instanceof GrammarQuotedString) {
+ this.capabilities.push(args[0]);
+ }
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-3.3
+ */
+export class StopCommand extends GrammarCommand
+{
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-4.1
+ */
+export class FileIntoCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ // QuotedString / MultiLine
+ this._mailbox = new GrammarQuotedString();
+ }
+
+ get require() { return 'fileinto'; }
+
+ toString()
+ {
+ return 'fileinto '
+ // https://datatracker.ietf.org/doc/html/rfc3894
+ + ((this.copy && capa.includes('copy')) ? ':copy ' : '')
+ // https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
+ + ((this.create && capa.includes('mailbox')) ? ':create ' : '')
+ + this._mailbox
+ + ';';
+ }
+
+ get mailbox()
+ {
+ return this._mailbox.value;
+ }
+
+ set mailbox(value)
+ {
+ this._mailbox.value = value;
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarString) {
+ this._mailbox = args[0];
+ }
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-4.2
+ */
+export class RedirectCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ // QuotedString / MultiLine
+ this._address = new GrammarQuotedString();
+ }
+
+ toString()
+ {
+ return 'redirect '
+ // https://datatracker.ietf.org/doc/html/rfc3894
+ + ((this.copy && capa.includes('copy')) ? ':copy ' : '')
+ + this._address
+ + ';';
+ }
+
+ get address()
+ {
+ return this._address.value;
+ }
+
+ set address(value)
+ {
+ this._address.value = value;
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarString) {
+ this._address = args[0];
+ }
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-4.3
+ */
+export class KeepCommand extends GrammarCommand
+{
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-4.4
+ */
+export class DiscardCommand extends GrammarCommand
+{
+}
diff --git a/dev/Sieve/Extensions/rfc5173.js b/dev/Sieve/Extensions/rfc5173.js
new file mode 100644
index 000000000..31b299053
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5173.js
@@ -0,0 +1,45 @@
+/**
+ * https://tools.ietf.org/html/rfc5173
+ */
+
+import {
+ GrammarString,
+ GrammarStringList,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+export class BodyTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.body_transform = ''; // :raw, :content , :text
+ this.key_list = new GrammarStringList;
+ }
+
+ get require() { return 'body'; }
+
+ toString()
+ {
+ return 'body'
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.body_transform
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ args.forEach((arg, i) => {
+ if (':raw' === arg || ':text' === arg) {
+ this.body_transform = arg;
+ } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
+ if (':content' === args[i-1]) {
+ this.body_transform = ':content ' + arg;
+ } else {
+ this[args[i+1] ? 'content_list' : 'key_list'] = arg;
+ }
+ }
+ });
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5183.js b/dev/Sieve/Extensions/rfc5183.js
new file mode 100644
index 000000000..1279f1f48
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5183.js
@@ -0,0 +1,36 @@
+/**
+ * https://tools.ietf.org/html/rfc5183
+ */
+
+import {
+ GrammarQuotedString,
+ GrammarStringList,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+export class EnvironmentTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.name = new GrammarQuotedString;
+ this.key_list = new GrammarStringList;
+ }
+
+ get require() { return 'environment'; }
+
+ toString()
+ {
+ return 'environment'
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.name
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.name = args.pop();
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5229.js b/dev/Sieve/Extensions/rfc5229.js
new file mode 100644
index 000000000..08b4aea8e
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5229.js
@@ -0,0 +1,71 @@
+/**
+ * https://tools.ietf.org/html/rfc5229
+ */
+
+import {
+ GrammarCommand,
+ GrammarQuotedString,
+ GrammarStringList,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+export class SetCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.modifiers = [];
+ this._name = new GrammarQuotedString;
+ this._value = new GrammarQuotedString;
+ }
+
+ get require() { return 'variables'; }
+
+ toString()
+ {
+ return 'set'
+ + ' ' + this.modifiers.join(' ')
+ + ' ' + this._name
+ + ' ' + this._value;
+ }
+
+ get name() { return this._name.value; }
+ set name(str) { this._name.value = str; }
+
+ get value() { return this._value.value; }
+ set value(str) { this._value.value = str; }
+
+ pushArguments(args)
+ {
+ [':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
+ args.includes(modifier) && this.modifiers.push(modifier);
+ });
+ this._value = args.pop();
+ this._name = args.pop();
+ }
+}
+
+export class StringTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.source = new GrammarStringList;
+ this.key_list = new GrammarStringList;
+ }
+
+ toString()
+ {
+ return 'string'
+ + ' ' + this.match_type
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.source.toString()
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.source = args.pop();
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5230.js b/dev/Sieve/Extensions/rfc5230.js
new file mode 100644
index 000000000..85da557e9
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5230.js
@@ -0,0 +1,87 @@
+/**
+ * https://tools.ietf.org/html/rfc5230
+ * https://tools.ietf.org/html/rfc6131
+ */
+
+import { capa } from 'Sieve/Utils';
+
+import {
+ GrammarCommand,
+ GrammarNumber,
+ GrammarQuotedString,
+ GrammarStringList
+} from 'Sieve/Grammar';
+
+export class VacationCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this._days = new GrammarNumber;
+ this._seconds = new GrammarNumber;
+ this._subject = new GrammarQuotedString;
+ this._from = new GrammarQuotedString;
+ this.addresses = new GrammarStringList;
+ this.mime = false;
+ this._handle = new GrammarQuotedString;
+ this._reason = new GrammarQuotedString; // QuotedString / MultiLine
+ }
+
+// get require() { return ['vacation','vacation-seconds']; }
+ get require() { return 'vacation'; }
+
+ toString()
+ {
+ let result = 'vacation';
+ if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
+ result += ' :seconds ' + this._seconds;
+ } else if (0 < this._days.value) {
+ result += ' :days ' + this._days;
+ }
+ if (this._subject.length) {
+ result += ' :subject ' + this._subject;
+ }
+ if (this._from.length) {
+ result += ' :from ' + this.arguments[':from'];
+ }
+ if (this.addresses.length) {
+ result += ' :addresses ' + this.addresses.toString();
+ }
+ if (this.mime) {
+ result += ' :mime';
+ }
+ if (this._handle.length) {
+ result += ' :handle ' + this._handle;
+ }
+ return result + ' ' + this._reason;
+ }
+
+ get days() { return this._days.value; }
+ get seconds() { return this._seconds.value; }
+ get subject() { return this._subject.value; }
+ get from() { return this._from.value; }
+ get handle() { return this._handle.value; }
+ get reason() { return this._reason.value; }
+
+ set days(int) { this._days.value = int; }
+ set seconds(int) { this._seconds.value = int; }
+ set subject(str) { this._subject.value = str; }
+ set from(str) { this._from.value = str; }
+ set handle(str) { this._handle.value = str; }
+ set reason(str) { this._reason.value = str; }
+
+ pushArguments(args)
+ {
+ this._reason.value = args.pop().value; // GrammarQuotedString
+ args.forEach((arg, i) => {
+ if (':mime' === arg) {
+ this.mime = true;
+ } else if (':addresses' === args[i-1]) {
+ this.addresses = arg; // GrammarStringList
+ } else if (':' === args[i-1][0]) {
+ // :days, :seconds, :subject, :from, :handle
+ this[args[i-1].replace(':','_')].value = arg.value;
+ }
+ });
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5232.js b/dev/Sieve/Extensions/rfc5232.js
new file mode 100644
index 000000000..3e5f378e3
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5232.js
@@ -0,0 +1,94 @@
+/**
+ * https://tools.ietf.org/html/rfc5232
+ */
+
+import {
+ GrammarCommand,
+ GrammarQuotedString,
+ GrammarString,
+ GrammarStringList,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+class FlagCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this._variablename = new GrammarQuotedString;
+ this.list_of_flags = new GrammarStringList;
+ }
+
+ get require() { return 'imap4flags'; }
+
+ toString()
+ {
+ return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags.toString() + ';';
+ }
+
+ get variablename()
+ {
+ return this._variablename.value;
+ }
+
+ set variablename(value)
+ {
+ this._variablename.value = value;
+ }
+
+ pushArguments(args)
+ {
+ if (args[1]) {
+ if (args[0] instanceof GrammarQuotedString) {
+ this._variablename = args[0];
+ }
+ if (args[1] instanceof GrammarString) {
+ this.list_of_flags = args[1];
+ }
+ } else if (args[0] instanceof GrammarString) {
+ this.list_of_flags = args[0];
+ }
+ }
+}
+
+export class SetFlagCommand extends FlagCommand
+{
+}
+
+export class AddFlagCommand extends FlagCommand
+{
+}
+
+export class RemoveFlagCommand extends FlagCommand
+{
+}
+
+export class HasFlagTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.variable_list = new GrammarStringList;
+ this.list_of_flags = new GrammarStringList;
+ }
+
+ get require() { return 'imap4flags'; }
+
+ toString()
+ {
+ return 'hasflag'
+ + ' ' + this.match_type
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.variable_list.toString()
+ + ' ' + this.list_of_flags.toString();
+ }
+
+ pushArguments(args)
+ {
+ args.forEach((arg, i) => {
+ if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
+ this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg;
+ }
+ });
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5235.js b/dev/Sieve/Extensions/rfc5235.js
new file mode 100644
index 000000000..0147159f7
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5235.js
@@ -0,0 +1,70 @@
+/**
+ * https://tools.ietf.org/html/rfc5235
+ */
+
+import {
+ GrammarQuotedString,
+ GrammarString,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+export class SpamTestTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.percent = false, // 0 - 100 else 0 - 10
+ this.value = new GrammarQuotedString;
+ }
+
+// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
+ get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
+
+ toString()
+ {
+ return 'spamtest'
+ + (this.percent ? ' :percent' : '')
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.value;
+ }
+
+ pushArguments(args)
+ {
+ args.forEach(arg => {
+ if (':percent' === arg) {
+ this.percent = true;
+ } else if (arg instanceof GrammarString) {
+ this.value = arg;
+ }
+ });
+ }
+}
+
+export class VirusTestTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.value = new GrammarQuotedString; // 1 - 5
+ }
+
+ get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
+
+ toString()
+ {
+ return 'virustest'
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.value;
+ }
+
+ pushArguments(args)
+ {
+ args.forEach(arg => {
+ if (arg instanceof GrammarString) {
+ this.value = arg;
+ }
+ });
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5260.js b/dev/Sieve/Extensions/rfc5260.js
new file mode 100644
index 000000000..5c7868adc
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5260.js
@@ -0,0 +1,93 @@
+/**
+ * https://tools.ietf.org/html/rfc5260
+ */
+
+import {
+ GrammarNumber,
+ GrammarQuotedString,
+ GrammarStringList,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+export class DateTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.zone = new GrammarQuotedString;
+ this.originalzone = false;
+ this.header_name = new GrammarQuotedString;
+ this.date_part = new GrammarQuotedString;
+ this.key_list = new GrammarStringList;
+ // rfc5260#section-6
+ this.index = new GrammarNumber;
+ this.last = false;
+ }
+
+// get require() { return ['date','index']; }
+ get require() { return 'date'; }
+
+ toString()
+ {
+ return 'date'
+ + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ + (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.header_name
+ + ' ' + this.date_part
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.date_part = args.pop();
+ this.header_name = args.pop();
+ args.forEach((arg, i) => {
+ if (':originalzone' === arg) {
+ this.originalzone = true;
+ } else if (':last' === arg) {
+ this.last = true;
+ } else if (':zone' === args[i-1]) {
+ this.zone.value = arg.value;
+ } else if (':index' === args[i-1]) {
+ this.index.value = arg.value;
+ }
+ });
+ }
+}
+
+export class CurrentDateTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.zone = new GrammarQuotedString;
+ this.date_part = new GrammarQuotedString;
+ this.key_list = new GrammarStringList;
+ }
+
+ get require() { return 'date'; }
+
+ toString()
+ {
+ return 'currentdate'
+ + (this.zone.length ? ' :zone ' + this.zone : '')
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.date_part
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.date_part = args.pop();
+ args.forEach((arg, i) => {
+ if (':zone' === args[i-1]) {
+ this.zone.value = arg.value;
+ }
+ });
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5293.js b/dev/Sieve/Extensions/rfc5293.js
new file mode 100644
index 000000000..36411fb2d
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5293.js
@@ -0,0 +1,85 @@
+/**
+ * https://tools.ietf.org/html/rfc5293
+ */
+
+import {
+ GrammarCommand,
+ GrammarNumber,
+ GrammarQuotedString,
+ GrammarString,
+ GrammarStringList
+} from 'Sieve/Grammar';
+
+export class AddHeaderCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.last = false;
+ this.field_name = new GrammarQuotedString;
+ this.value = new GrammarQuotedString;
+ }
+
+ get require() { return 'editheader'; }
+
+ toString()
+ {
+ return this.identifier
+ + (this.last ? ' :last' : '')
+ + ' ' + this.field_name
+ + ' ' + this.value + ';';
+ }
+
+ pushArguments(args)
+ {
+ this.value = args.pop();
+ this.field_name = args.pop();
+ this.last = args.includes(':last');
+ }
+}
+
+export class DeleteHeaderCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.index = new GrammarNumber;
+ this.last = false;
+ this.comparator = '',
+ this.match_type = ':is',
+ this.field_name = new GrammarQuotedString;
+ this.value_patterns = new GrammarStringList;
+ }
+
+ get require() { return 'editheader'; }
+
+ toString()
+ {
+ return this.identifier
+ + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.field_name
+ + ' ' + this.value_patterns + ';';
+ }
+
+ pushArguments(args)
+ {
+ let l = args.length - 1;
+ args.forEach((arg, i) => {
+ if (':last' === arg) {
+ this.last = true;
+ } else if (':index' === args[i-1]) {
+ this.index.value = arg.value;
+ args[i] = null;
+ }
+ });
+
+ if (args[l-1] instanceof GrammarString) {
+ this.field_name = args[l-1];
+ this.value_patterns = args[l];
+ } else {
+ this.field_name = args[l];
+ }
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5429.js b/dev/Sieve/Extensions/rfc5429.js
new file mode 100644
index 000000000..b4bca755a
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5429.js
@@ -0,0 +1,81 @@
+/**
+ * https://tools.ietf.org/html/rfc5429
+ */
+
+import {
+ GrammarCommand,
+ GrammarQuotedString,
+ GrammarString
+} from 'Sieve/Grammar';
+
+/**
+ * https://tools.ietf.org/html/rfc5429#section-2.1
+ */
+export class ErejectCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this._reason = new GrammarQuotedString;
+ }
+
+ get require() { return 'ereject'; }
+
+ toString()
+ {
+ return 'ereject ' + this._reason + ';';
+ }
+
+ get reason()
+ {
+ return this._reason.value;
+ }
+
+ set reason(value)
+ {
+ this._reason.value = value;
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarString) {
+ this._reason = args[0];
+ }
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5429#section-2.2
+ */
+export class RejectCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this._reason = new GrammarQuotedString;
+ }
+
+ get require() { return 'reject'; }
+
+ toString()
+ {
+ return 'reject ' + this._reason + ';';
+ }
+
+ get reason()
+ {
+ return this._reason.value;
+ }
+
+ set reason(value)
+ {
+ this._reason.value = value;
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarString) {
+ this._reason = args[0];
+ }
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5435.js b/dev/Sieve/Extensions/rfc5435.js
new file mode 100644
index 000000000..4c1939456
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5435.js
@@ -0,0 +1,123 @@
+/**
+ * https://tools.ietf.org/html/rfc5435
+ */
+
+import {
+ GrammarCommand,
+ GrammarNumber,
+ GrammarQuotedString,
+ GrammarStringList,
+ GrammarTest
+} from 'Sieve/Grammar';
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5435#section-3
+ */
+export class NotifyCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this._method = new GrammarQuotedString;
+ this._from = new GrammarQuotedString;
+ this._importance = new GrammarNumber;
+ this.options = new GrammarStringList;
+ this._message = new GrammarQuotedString;
+ }
+
+ get method() { return this._method.value; }
+ get from() { return this._from.value; }
+ get importance() { return this._importance.value; }
+ get message() { return this._message.value; }
+
+ set method(str) { this._method.value = str; }
+ set from(str) { this._from.value = str; }
+ set importance(int) { this._importance.value = int; }
+ set message(str) { this._message.value = str; }
+
+ get require() { return 'enotify'; }
+
+ toString()
+ {
+ let result = 'notify';
+ if (this._from.value) {
+ result += ' :from ' + this._from;
+ }
+ if (0 < this._importance.value) {
+ result += ' :importance ' + this._importance;
+ }
+ if (this.options.length) {
+ result += ' :options ' + this.options;
+ }
+ if (this._message.value) {
+ result += ' :message ' + this._message;
+ }
+ return result + ' ' + this._method;
+ }
+
+ pushArguments(args)
+ {
+ this._method.value = args.pop().value; // GrammarQuotedString
+ args.forEach((arg, i) => {
+ if (':options' === args[i-1]) {
+ this.options = arg; // GrammarStringList
+ } else if (':' === args[i-1][0]) {
+ // :from, :importance, :message
+ this[args[i-1].replace(':','_')].value = arg.value;
+ }
+ });
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5435#section-4
+ */
+export class ValidNotifyMethodTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.notification_uris = new GrammarStringList;
+ }
+
+ toString()
+ {
+ return 'valid_notify_method ' + this.notification_uris;
+ }
+
+ pushArguments(args)
+ {
+ this.notification_uris = args.pop();
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5435#section-5
+ */
+export class NotifyMethodCapabilityTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.notification_uri = new GrammarQuotedString;
+ this.notification_capability = new GrammarQuotedString;
+ this.key_list = new GrammarStringList;
+ }
+
+ toString()
+ {
+ return 'valid_notify_method '
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + (this.match_type ? ' ' + this.match_type : '')
+ + this.notification_uri
+ + this.notification_capability
+ + this.key_list;
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.notification_capability = args.pop();
+ this.notification_uri = args.pop();
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5463.js b/dev/Sieve/Extensions/rfc5463.js
new file mode 100644
index 000000000..967363d0b
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5463.js
@@ -0,0 +1,58 @@
+/**
+ * https://tools.ietf.org/html/rfc5463
+ */
+
+import {
+ GrammarCommand,
+ GrammarTest,
+ GrammarQuotedString,
+ GrammarStringList
+} from 'Sieve/Grammar';
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5463#section-4
+ */
+export class IHaveTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.capabilities = new GrammarStringList;
+ }
+
+ get require() { return 'ihave'; }
+
+ toString()
+ {
+ return 'ihave ' + this.capabilities;
+ }
+
+ pushArguments(args)
+ {
+ this.capabilities = args.pop();
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5463#section-5
+ */
+export class ErrorCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.message = new GrammarQuotedString;
+ }
+
+ get require() { return 'ihave'; }
+
+ toString()
+ {
+ return 'error ' + this.message + ';';
+ }
+
+ pushArguments(args)
+ {
+ this.message = args.pop();
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc5490.js b/dev/Sieve/Extensions/rfc5490.js
new file mode 100644
index 000000000..aa836c813
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc5490.js
@@ -0,0 +1,151 @@
+/**
+ * https://tools.ietf.org/html/rfc5490
+ */
+
+import {
+ GrammarTest,
+ GrammarQuotedString,
+ GrammarStringList
+} from 'Sieve/Grammar';
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5490#section-3.1
+ */
+export class MailboxExistsTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.mailbox_names = new GrammarStringList;
+ }
+
+ get require() { return 'mailbox'; }
+
+ toString()
+ {
+ return 'mailboxexists ' + this.mailbox_names + ';';
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarStringList) {
+ this.mailbox_names = args[0];
+ }
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5490#section-3.3
+ */
+export class MetadataTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.mailbox = new GrammarQuotedString;
+ this.annotation_name = new GrammarQuotedString;
+ this.key_list = new GrammarStringList;
+ }
+
+ get require() { return 'mboxmetadata'; }
+
+ toString()
+ {
+ return 'metadata '
+ + ' ' + this.match_type
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.mailbox
+ + ' ' + this.annotation_name
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.annotation_name = args.pop();
+ this.mailbox = args.pop();
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5490#section-3.4
+ */
+export class MetadataExistsTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.mailbox = new GrammarQuotedString;
+ this.annotation_names = new GrammarStringList;
+ }
+
+ get require() { return 'mboxmetadata'; }
+
+ toString()
+ {
+ return 'metadataexists '
+ + ' ' + this.mailbox
+ + ' ' + this.annotation_names;
+ }
+
+ pushArguments(args)
+ {
+ this.annotation_names = args.pop();
+ this.mailbox = args.pop();
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5490#section-3.3
+ */
+export class ServerMetadataTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.annotation_name = new GrammarQuotedString;
+ this.key_list = new GrammarStringList;
+ }
+
+ get require() { return 'servermetadata'; }
+
+ toString()
+ {
+ return 'servermetadata '
+ + ' ' + this.match_type
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.annotation_name
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ this.key_list = args.pop();
+ this.annotation_name = args.pop();
+ }
+}
+
+/**
+ * https://datatracker.ietf.org/doc/html/rfc5490#section-3.4
+ */
+export class ServerMetadataExistsTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.annotation_names = new GrammarStringList;
+ }
+
+ get require() { return 'servermetadata'; }
+
+ toString()
+ {
+ return 'servermetadataexists '
+ + ' ' + this.annotation_names;
+ }
+
+ pushArguments(args)
+ {
+ this.annotation_names = args.pop();
+ }
+}
diff --git a/dev/Sieve/Extensions/rfc6609.js b/dev/Sieve/Extensions/rfc6609.js
new file mode 100644
index 000000000..35c8c6fc6
--- /dev/null
+++ b/dev/Sieve/Extensions/rfc6609.js
@@ -0,0 +1,47 @@
+/**
+ * https://tools.ietf.org/html/rfc6609
+ */
+
+import {
+ GrammarCommand,
+ GrammarQuotedString
+} from 'Sieve/Grammar';
+
+export class IncludeCommand extends GrammarCommand
+{
+ constructor()
+ {
+ super();
+ this.global = false; // ':personal' / ':global';
+ this.once = false;
+ this.optional = false;
+ this.value = new GrammarQuotedString;
+ }
+
+ get require() { return 'include'; }
+
+ toString()
+ {
+ return this.identifier
+ + (this.global ? ' :global' : '')
+ + (this.once ? ' :once' : '')
+ + (this.optional ? ' :optional' : '')
+ + ' ' + this.value + ';';
+ }
+
+ pushArguments(args)
+ {
+ args.forEach(arg => {
+ if (':global' === arg || ':once' === arg || ':optional' === arg) {
+ this[arg.substr(1)] = true;
+ } else if (arg instanceof GrammarQuotedString) {
+ this.value = arg;
+ }
+ });
+ }
+}
+
+export class ReturnCommand extends GrammarCommand
+{
+ get require() { return 'include'; }
+}
diff --git a/dev/Sieve/Grammar.js b/dev/Sieve/Grammar.js
new file mode 100644
index 000000000..b656b3ca8
--- /dev/null
+++ b/dev/Sieve/Grammar.js
@@ -0,0 +1,277 @@
+/**
+ * https://tools.ietf.org/html/rfc5228#section-8.2
+ */
+
+import {
+ QUOTED_TEXT,
+ HASH_COMMENT,
+ MULTILINE_LITERAL,
+ MULTILINE_DOTSTART
+} from 'Sieve/RegEx';
+
+import {
+ arrayToString
+} from 'Sieve/Utils';
+
+/**
+ * abstract
+ */
+export class GrammarString /*extends String*/
+{
+ constructor(value = '')
+ {
+ this._value = value;
+ }
+
+ toString()
+ {
+ return this._value;
+ }
+
+ get value()
+ {
+ return this._value;
+ }
+
+ set value(value)
+ {
+ this._value = value;
+ }
+
+ get length()
+ {
+ return this._value.length;
+ }
+}
+
+/**
+ * abstract
+ */
+export class GrammarComment extends GrammarString
+{
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-2.9
+ */
+export class GrammarCommand
+{
+ constructor(identifier)
+ {
+ this.identifier = identifier || this.constructor.name.toLowerCase().replace('command', '');
+ this.arguments = [];
+ this.commands = new GrammarCommands;
+ }
+
+ toString()
+ {
+ let result = this.identifier;
+ if (this.arguments.length) {
+ result += ' ' + arrayToString(this.arguments, ' ');
+ }
+ return result + (
+ this.commands.length ? ' ' + this.commands.toString() : ';'
+ );
+ }
+
+ getComparators()
+ {
+ return ['i;ascii-casemap'];
+ }
+
+ getMatchTypes()
+ {
+ return [':is', ':contains', ':matches'];
+ }
+
+ pushArguments(args)
+ {
+ this.arguments = args;
+ }
+}
+
+export class GrammarCommands extends Array
+{
+ toString()
+ {
+ return this.length
+ ? '{\r\n\t' + arrayToString(this, '\r\n\t') + '\r\n}'
+ : '{}';
+ }
+
+ push(value)
+ {
+ if (value instanceof GrammarCommand || value instanceof GrammarComment) {
+ super.push(value);
+ }
+ }
+}
+
+export class GrammarBracketComment extends GrammarComment
+{
+ toString()
+ {
+ return '/* ' + super.toString() + ' */';
+ }
+}
+
+export class GrammarHashComment extends GrammarComment
+{
+ toString()
+ {
+ return '# ' + super.toString();
+ }
+}
+
+export class GrammarNumber /*extends Number*/
+{
+ constructor(value = '0')
+ {
+ this._value = value;
+ }
+
+ toString()
+ {
+ return this._value;
+ }
+
+ get value()
+ {
+ return this._value;
+ }
+
+ set value(value)
+ {
+ this._value = value;
+ }
+}
+
+export class GrammarStringList extends Array
+{
+ toString()
+ {
+ if (1 < this.length) {
+ return '[' + this.join(',') + ']';
+ }
+ return this.length ? this[0] : '';
+ }
+
+ push(value)
+ {
+ if (!(value instanceof GrammarString)) {
+ value = new GrammarQuotedString(value);
+ }
+ super.push(value);
+ }
+}
+
+const StringListRegEx = RegExp('(?:^\\s*|\\s*,\\s*)(?:"(' + QUOTED_TEXT + ')"|text:[ \\t]*('
+ + HASH_COMMENT + ')?\\r\\n'
+ + '((?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*)'
+ + '\\.\\r\\n)', 'gm');
+GrammarStringList.fromString = list => {
+ let string,
+ obj = new GrammarStringList;
+ list = list.replace(/^[\r\n\t[]+/, '');
+ while ((string = StringListRegEx.exec(list))) {
+ if (string[3]) {
+ obj.push(new GrammarMultiLine(string[3], string[2]));
+ } else {
+ obj.push(new GrammarQuotedString(string[1]));
+ }
+ }
+ return obj;
+}
+
+export class GrammarQuotedString extends GrammarString
+{
+ toString()
+ {
+ return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
+// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-8.1
+ */
+export class GrammarMultiLine extends GrammarString
+{
+ constructor(value, comment = '')
+ {
+ super();
+ this.value = value;
+ this.comment = comment;
+ }
+
+ toString()
+ {
+ return 'text:'
+ + (this.comment ? '# ' + this.comment : '') + "\r\n"
+ + this.value
+ + "\r\n.\r\n";
+ }
+}
+
+const MultiLineRegEx = RegExp('text:[ \\t]*(' + HASH_COMMENT + ')?\\r\\n'
+ + '((?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*)'
+ + '\\.\\r\\n', 'm');
+GrammarMultiLine.fromString = string => {
+ string = string.match(MultiLineRegEx);
+ if (string[2]) {
+ return new GrammarMultiLine(string[2].replace(/\r\n$/, ''), string[1]);
+ }
+ return new GrammarMultiLine();
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5
+ */
+export class GrammarTest
+{
+ constructor(identifier)
+ {
+ this.identifier = identifier || this.constructor.name.toLowerCase().replace(/test$/, '');
+ // Almost every test has a comparator and match_type, so define them here
+ this.comparator = '',
+ this.match_type = ':is',
+ this.arguments = [];
+ }
+
+ toString()
+ {
+ return (this.identifier
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + (this.match_type ? ' ' + this.match_type : '')
+ + ' ' + arrayToString(this.arguments, ' ')).trim();
+ }
+
+ pushArguments(args)
+ {
+ this.arguments = args;
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.2
+ * https://tools.ietf.org/html/rfc5228#section-5.3
+ */
+export class GrammarTestList extends Array
+{
+ toString()
+ {
+ if (1 < this.length) {
+// return '(\r\n\t' + arrayToString(this, ',\r\n\t') + '\r\n)';
+ return '(' + this.join(', ') + ')';
+ }
+ return this.length ? this[0] : '';
+ }
+
+ push(value)
+ {
+ if (!(value instanceof GrammarTest)) {
+ throw 'Not an instanceof Test';
+ }
+ super.push(value);
+ }
+}
diff --git a/dev/Sieve/Model/Abstract.js b/dev/Sieve/Model/Abstract.js
new file mode 100644
index 000000000..4df08e039
--- /dev/null
+++ b/dev/Sieve/Model/Abstract.js
@@ -0,0 +1,121 @@
+import { forEachObjectValue, forEachObjectEntry, koComputable } from 'Sieve/Utils';
+
+function typeCast(curValue, newValue) {
+ if (null != curValue) {
+ switch (typeof curValue)
+ {
+ case 'boolean': return 0 != newValue && !!newValue;
+ case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
+ case 'string': return null != newValue ? '' + newValue : '';
+ case 'object':
+ if (curValue.constructor.reviveFromJson) {
+ return curValue.constructor.reviveFromJson(newValue);
+ }
+ if (Array.isArray(curValue) && !Array.isArray(newValue))
+ return [];
+ }
+ }
+ return newValue;
+}
+
+export class AbstractModel {
+ constructor() {
+/*
+ if (new.target === AbstractModel) {
+ throw new Error("Can't instantiate AbstractModel!");
+ }
+*/
+ this.disposables = [];
+ }
+
+ addObservables(observables) {
+ forEachObjectEntry(observables, (key, value) =>
+ this[key] || (this[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value))
+ );
+ }
+
+ addComputables(computables) {
+ forEachObjectEntry(computables, (key, fn) => this[key] = koComputable(fn));
+ }
+
+ addSubscribables(subscribables) {
+ forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
+ }
+
+ /** Called by delegateRunOnDestroy */
+ onDestroy() {
+ /** dispose ko subscribables */
+ this.disposables.forEach(disposable => {
+ disposable && typeof disposable.dispose === 'function' && disposable.dispose();
+ });
+ /** clear object entries */
+// forEachObjectEntry(this, (key, value) => {
+ forEachObjectValue(this, value => {
+ /** clear CollectionModel */
+ let arr = ko.isObservableArray(value) ? value() : value;
+ arr && arr.onDestroy && arr.onDestroy();
+ /** destroy ko.observable/ko.computed? */
+// dispose(value);
+ /** clear object value */
+// this[key] = null; // TODO: issue with Contacts view
+ });
+// this.disposables = [];
+ }
+
+ /**
+ * @static
+ * @param {FetchJson} json
+ * @returns {boolean}
+ */
+ static validJson(json) {
+ return !!(json && ('Object/'+this.name.replace('Model', '') === json['@Object']));
+ }
+
+ /**
+ * @static
+ * @param {FetchJson} json
+ * @returns {*Model}
+ */
+ static reviveFromJson(json) {
+ let obj = this.validJson(json) ? new this() : null;
+ obj && obj.revivePropertiesFromJson(json);
+ return obj;
+ }
+
+ revivePropertiesFromJson(json) {
+ let model = this.constructor;
+ if (!model.validJson(json)) {
+ return false;
+ }
+ forEachObjectEntry(json, (key, value) => {
+ if ('@' !== key[0]) try {
+ key = key[0].toLowerCase() + key.slice(1);
+ switch (typeof this[key])
+ {
+ case 'function':
+ if (ko.isObservable(this[key])) {
+ this[key](typeCast(this[key](), value));
+// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
+ }
+// else console.log(model.name + '.' + key + ' is a function');
+ break;
+ case 'boolean':
+ case 'number':
+ case 'object':
+ case 'string':
+ this[key] = typeCast(this[key], value);
+ break;
+ // fall through
+ case 'undefined':
+ default:
+// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
+ }
+ } catch (e) {
+ console.log(model.name + '.' + key);
+ console.error(e);
+ }
+ });
+ return true;
+ }
+
+}
diff --git a/dev/Model/Filter.js b/dev/Sieve/Model/Filter.js
similarity index 79%
rename from dev/Model/Filter.js
rename to dev/Sieve/Model/Filter.js
index b53656c55..809e0030d 100644
--- a/dev/Model/Filter.js
+++ b/dev/Sieve/Model/Filter.js
@@ -1,14 +1,11 @@
-import ko from 'ko';
+import { koArrayWithDestroy } from 'Sieve/Utils';
-import { isNonEmptyArray, pString } from 'Common/Utils';
-import { delegateRunOnDestroy } from 'Common/UtilsUser';
-import { i18n } from 'Common/Translator';
-import { getFolderFromCacheList } from 'Common/Cache';
+//import { getFolderFromCacheList } from 'Common/Cache';
-import { AccountUserStore } from 'Stores/User/Account';
+//import { AccountUserStore } from 'Stores/User/Account';
-import { FilterConditionModel } from 'Model/FilterCondition';
-import { AbstractModel } from 'Knoin/AbstractModel';
+import { FilterConditionModel } from 'Sieve/Model/FilterCondition';
+import { AbstractModel } from 'Sieve/Model/Abstract';
/**
* @enum {string}
@@ -38,7 +35,7 @@ export class FilterModel extends AbstractModel {
this.addObservables({
enabled: true,
- deleteAccess: false,
+ askDelete: false,
canBeDeleted: true,
name: '',
@@ -65,11 +62,12 @@ export class FilterModel extends AbstractModel {
actionType: FilterAction.MoveTo
});
- this.conditions = ko.observableArray();
+ this.conditions = koArrayWithDestroy();
- const fGetRealFolderName = (folderFullNameRaw) => {
- const folder = getFolderFromCacheList(folderFullNameRaw);
- return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullNameRaw;
+ const fGetRealFolderName = folderFullName => {
+// const folder = getFolderFromCacheList(folderFullName);
+// return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName;
+ return folderFullName;
};
this.addComputables({
@@ -79,23 +77,23 @@ export class FilterModel extends AbstractModel {
switch (this.actionType()) {
case FilterAction.MoveTo:
- result = i18n(root + 'MOVE_TO', {
+ result = rl.i18n(root + 'MOVE_TO', {
FOLDER: fGetRealFolderName(actionValue)
});
break;
case FilterAction.Forward:
- result = i18n(root + 'FORWARD_TO', {
+ result = rl.i18n(root + 'FORWARD_TO', {
EMAIL: actionValue
});
break;
case FilterAction.Vacation:
- result = i18n(root + 'VACATION_MESSAGE');
+ result = rl.i18n(root + 'VACATION_MESSAGE');
break;
case FilterAction.Reject:
- result = i18n(root + 'REJECT');
+ result = rl.i18n(root + 'REJECT');
break;
case FilterAction.Discard:
- result = i18n(root + 'DISCARD');
+ result = rl.i18n(root + 'DISCARD');
break;
// no default
}
@@ -211,11 +209,10 @@ export class FilterModel extends AbstractModel {
removeCondition(oConditionToDelete) {
this.conditions.remove(oConditionToDelete);
- delegateRunOnDestroy(oConditionToDelete);
}
setRecipients() {
- this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
+// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
}
/**
@@ -226,16 +223,10 @@ export class FilterModel extends AbstractModel {
static reviveFromJson(json) {
const filter = super.reviveFromJson(json);
if (filter) {
- filter.id = pString(json.ID);
-
- filter.conditions([]);
-
- if (isNonEmptyArray(json.Conditions)) {
- filter.conditions(
- json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
- );
- }
-
+ filter.id = filter.id ? '' + filter.id : '';
+ filter.conditions(
+ json.Conditions ? json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) : []
+ );
filter.actionKeep(0 != json.Keep);
filter.actionNoStop(0 == json.Stop);
filter.actionMarkAsRead(1 == json.MarkAsRead);
diff --git a/dev/Model/FilterCondition.js b/dev/Sieve/Model/FilterCondition.js
similarity index 93%
rename from dev/Model/FilterCondition.js
rename to dev/Sieve/Model/FilterCondition.js
index c4a06387a..5ee0806aa 100644
--- a/dev/Model/FilterCondition.js
+++ b/dev/Sieve/Model/FilterCondition.js
@@ -1,6 +1,6 @@
-import ko from 'ko';
+import { koComputable } from 'Sieve/Utils';
-import { AbstractModel } from 'Knoin/AbstractModel';
+import { AbstractModel } from 'Sieve/Model/Abstract';
/**
* @enum {string}
@@ -43,7 +43,7 @@ export class FilterConditionModel extends AbstractModel {
valueSecondError: false
});
- this.template = ko.computed(() => {
+ this.template = koComputable(() => {
const template = 'SettingsFiltersCondition';
switch (this.field()) {
case FilterConditionField.Body:
diff --git a/dev/Model/SieveScript.js b/dev/Sieve/Model/Script.js
similarity index 91%
rename from dev/Model/SieveScript.js
rename to dev/Sieve/Model/Script.js
index b37bee73f..2314b27bf 100644
--- a/dev/Model/SieveScript.js
+++ b/dev/Sieve/Model/Script.js
@@ -1,8 +1,6 @@
-import ko from 'ko';
-
-import { AbstractModel } from 'Knoin/AbstractModel';
-import { FilterModel } from 'Model/Filter';
-import { isNonEmptyArray, pString } from 'Common/Utils';
+import { AbstractModel } from 'Sieve/Model/Abstract';
+import { FilterModel } from 'Sieve/Model/Filter';
+import { koArrayWithDestroy } from 'Sieve/Utils';
const SIEVE_FILE_NAME = 'rainloop.user';
@@ -154,12 +152,12 @@ function filtersToSieveScript(filters)
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
}
- paramValue = pString(filter.actionValueThird()).trim();
+ paramValue = (filter.actionValueThird() || '').trim();
if (paramValue.length) {
days = Math.max(1, parseInt(paramValue, 10));
}
- paramValue = pString(filter.actionValueFourth()).trim()
+ paramValue = (filter.actionValueFourth() || '').trim()
if (paramValue.length) {
paramValue = paramValue.split(',').map(email =>
email.trim().length ? quote(email) : ''
@@ -247,13 +245,6 @@ function sieveScriptToFilters(script)
}
}
}
-/*
- // TODO: branch sieveparser
- // https://github.com/the-djmaze/snappymail/tree/sieveparser/dev/Sieve
- else if (script.length && window.Sieve) {
- let tree = window.Sieve.parseScript(script);
- }
-*/
return filters;
}
@@ -269,13 +260,12 @@ export class SieveScriptModel extends AbstractModel
exists: false,
nameError: false,
- bodyError: false,
- deleteAccess: false,
+ askDelete: false,
canBeDeleted: true,
hasChanges: false
});
- this.filters = ko.observableArray();
+ this.filters = koArrayWithDestroy();
// this.saving = ko.observable(false).extend({ debounce: 200 });
this.addSubscribables({
@@ -297,8 +287,7 @@ export class SieveScriptModel extends AbstractModel
verify() {
this.nameError(!this.name().trim());
- this.bodyError(this.allowFilters() ? !this.filters.length : !this.body().trim());
- return !this.nameError() && !this.bodyError();
+ return !this.nameError();
}
toJson() {
@@ -327,7 +316,7 @@ export class SieveScriptModel extends AbstractModel
if (script) {
if (script.allowFilters()) {
script.filters(
- isNonEmptyArray(json.filters)
+ Array.isArray(json.filters) && json.filters.length
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
: sieveScriptToFilters(script.body())
);
diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js
new file mode 100644
index 000000000..cbc2587a7
--- /dev/null
+++ b/dev/Sieve/Parser.js
@@ -0,0 +1,407 @@
+/**
+ * https://tools.ietf.org/html/rfc5228#section-8
+ */
+
+import { capa, forEachObjectEntry } from 'Sieve/Utils';
+
+import {
+ BRACKET_COMMENT,
+ HASH_COMMENT,
+ IDENTIFIER,
+ MULTI_LINE,
+ NUMBER,
+ QUOTED_STRING,
+ STRING_LIST,
+ TAG
+} from 'Sieve/RegEx';
+
+import {
+ GrammarBracketComment,
+ GrammarCommand,
+ GrammarHashComment,
+ GrammarMultiLine,
+ GrammarNumber,
+ GrammarQuotedString,
+ GrammarStringList,
+ GrammarTest,
+ GrammarTestList
+} from 'Sieve/Grammar';
+
+import {
+ ConditionalCommand,
+ DiscardCommand,
+ ElsIfCommand,
+ ElseCommand,
+ FileIntoCommand,
+ IfCommand,
+ KeepCommand,
+ RedirectCommand,
+ RequireCommand,
+ StopCommand
+} from 'Sieve/Commands';
+
+import {
+ AddressTest,
+ AllOfTest,
+ AnyOfTest,
+ EnvelopeTest,
+ ExistsTest,
+ FalseTest,
+ HeaderTest,
+ NotTest,
+ SizeTest,
+ TrueTest
+} from 'Sieve/Tests';
+
+import { BodyTest } from 'Sieve/Extensions/rfc5173';
+import { EnvironmentTest } from 'Sieve/Extensions/rfc5183';
+import { SetCommand, StringTest } from 'Sieve/Extensions/rfc5229';
+import { VacationCommand } from 'Sieve/Extensions/rfc5230';
+
+import {
+ SetFlagCommand,
+ AddFlagCommand,
+ RemoveFlagCommand,
+ HasFlagTest
+} from 'Sieve/Extensions/rfc5232';
+
+import { SpamTestTest, VirusTestTest } from 'Sieve/Extensions/rfc5235';
+import { DateTest, CurrentDateTest } from 'Sieve/Extensions/rfc5260';
+import { AddHeaderCommand, DeleteHeaderCommand } from 'Sieve/Extensions/rfc5293';
+import { ErejectCommand, RejectCommand } from 'Sieve/Extensions/rfc5429';
+import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from 'Sieve/Extensions/rfc5435';
+import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
+import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
+import { IncludeCommand, ReturnCommand } from 'Sieve/Extensions/rfc6609';
+
+const
+ AllCommands = {
+ // Control commands
+ if: IfCommand,
+ elsif: ElsIfCommand,
+ else: ElseCommand,
+ conditional: ConditionalCommand,
+ require: RequireCommand,
+ stop: StopCommand,
+ // Action commands
+ discard: DiscardCommand,
+ fileinto: FileIntoCommand,
+ keep: KeepCommand,
+ redirect: RedirectCommand,
+ // Test commands
+ address: AddressTest,
+ allof: AllOfTest,
+ anyof: AnyOfTest,
+ envelope: EnvelopeTest,
+ exists: ExistsTest,
+ false: FalseTest,
+ header: HeaderTest,
+ not: NotTest,
+ size: SizeTest,
+ true: TrueTest,
+ // rfc5173
+ body: BodyTest,
+ // rfc5183
+ environment: EnvironmentTest,
+ // rfc5229
+ set: SetCommand,
+ string: StringTest,
+ // rfc5230
+ vacation: VacationCommand,
+ // rfc5232
+ setflag: SetFlagCommand,
+ addflag: AddFlagCommand,
+ removeflag: RemoveFlagCommand,
+ hasflag: HasFlagTest,
+ // rfc5235
+ spamtest: SpamTestTest,
+ virustest: VirusTestTest,
+ // rfc5260
+ date: DateTest,
+ currentdate: CurrentDateTest,
+ // rfc5293
+ AddHeaderCommand,
+ DeleteHeaderCommand,
+ // rfc5429
+ ereject: ErejectCommand,
+ reject: RejectCommand,
+ // rfc5435
+ notify: NotifyCommand,
+ valid_notify_method: ValidNotifyMethodTest,
+ notify_method_capability: NotifyMethodCapabilityTest,
+ // rfc5463
+ ihave: IHaveTest,
+ error: ErrorCommand,
+ // rfc5490
+ mailboxexists: MailboxExistsTest,
+ metadata: MetadataTest,
+ metadataexists: MetadataExistsTest,
+ // rfc6609
+ include: IncludeCommand,
+ return: ReturnCommand
+ },
+
+ T_UNKNOWN = 0,
+ T_STRING_LIST = 1,
+ T_QUOTED_STRING = 2,
+ T_MULTILINE_STRING = 3,
+ T_HASH_COMMENT = 4,
+ T_BRACKET_COMMENT = 5,
+ T_BLOCK_START = 6,
+ T_BLOCK_END = 7,
+ T_LEFT_PARENTHESIS = 8,
+ T_RIGHT_PARENTHESIS = 9,
+ T_COMMA = 10,
+ T_SEMICOLON = 11,
+ T_TAG = 12,
+ T_IDENTIFIER = 13,
+ T_NUMBER = 14,
+ T_WHITESPACE = 15,
+
+ TokensRegEx = '(' + [
+ /* T_STRING_LIST */ STRING_LIST,
+ /* T_QUOTED_STRING */ QUOTED_STRING,
+ /* T_MULTILINE_STRING */ MULTI_LINE,
+ /* T_HASH_COMMENT */ HASH_COMMENT,
+ /* T_BRACKET_COMMENT */ BRACKET_COMMENT,
+ /* T_BLOCK_START */ '\\{',
+ /* T_BLOCK_END */ '\\}',
+ /* T_LEFT_PARENTHESIS */ '\\(', // anyof / allof
+ /* T_RIGHT_PARENTHESIS */ '\\)', // anyof / allof
+ /* T_COMMA */ ',',
+ /* T_SEMICOLON */ ';',
+ /* T_TAG */ TAG,
+ /* T_IDENTIFIER */ IDENTIFIER,
+ /* T_NUMBER */ NUMBER,
+ /* T_WHITESPACE */ '(?: |\\r\\n|\\t)+',
+ /* T_UNKNOWN */ '[^ \\r\\n\\t]+'
+ ].join(')|(') + ')';
+
+export const parseScript = (script, name = 'script.sieve') => {
+ script = script.replace(/\r?\n/g, '\r\n');
+
+ // Only activate available commands
+ const Commands = {};
+ forEachObjectEntry(AllCommands, (key, cmd) => {
+ const requires = (new cmd).require;
+ if (!requires
+ || (Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string))
+ ) {
+ Commands[key] = cmd;
+ }
+ });
+
+ let match,
+ line = 1,
+ tree = [],
+
+ // Create one regex to find the tokens
+ // Use exec() to forward since lastIndex
+ regex = RegExp(TokensRegEx, 'gm'),
+
+ levels = [],
+ command = null,
+ requires = [],
+ args = [];
+
+ const
+ error = message => {
+// throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, name, line)
+ throw new SyntaxError(message + ' on line ' + line
+ + ' around:\n\n' + script.substr(regex.lastIndex - 20, 30), name, line)
+ },
+ pushArg = arg => {
+ command || error('Argument not part of command');
+ let prev_arg = args[args.length-1];
+ if (':is' === arg || ':contains' === arg || ':matches' === arg) {
+ command.match_type = arg;
+ } else if (':value' === prev_arg || ':count' === prev_arg) {
+ // Sieve relational [RFC5231] match types
+ /^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
+ command.match_type = prev_arg + ' ' + arg;
+ --args.length;
+// requires.push('relational');
+ } else if (':comparator' === prev_arg) {
+ command.comparator = arg;
+ --args.length;
+ } else {
+ args.push(arg);
+ }
+ },
+ pushArgs = () => {
+ if (args.length) {
+ command && command.pushArguments(args);
+ args = [];
+ }
+ };
+
+ levels.last = () => levels[levels.length - 1];
+
+ while ((match = regex.exec(script))) {
+ // the last element in match will contain the matched value and the key will be the type
+ let type = match.findIndex((v,i) => 0 < i && undefined !== v),
+ value = match[type];
+
+ // create the part
+ switch (type)
+ {
+ case T_IDENTIFIER: {
+ pushArgs();
+ value = value.toLowerCase();
+ let new_command;
+ if ('if' === value) {
+ new_command = new ConditionalCommand(value);
+ } else if ('elsif' === value || 'else' === value) {
+// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
+ new_command = new ConditionalCommand(value);
+ } else if (Commands[value]) {
+ if ('allof' === value || 'anyof' === value) {
+// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
+ }
+ new_command = new Commands[value]();
+ } else {
+ console.error('Unknown command: ' + value);
+ if (command && (
+ command instanceof ConditionalCommand
+ || command instanceof NotTest
+ || command.tests instanceof GrammarTestList)) {
+ new_command = new GrammarTest(value);
+ } else {
+ new_command = new GrammarCommand(value);
+ }
+ }
+
+ if (new_command instanceof GrammarTest) {
+ if (command instanceof ConditionalCommand || command instanceof NotTest) {
+ // if/elsif/else new_command
+ // not new_command
+ command.test = new_command;
+ } else if (command.tests instanceof GrammarTestList) {
+ // allof/anyof .tests[] new_command
+ command.tests.push(new_command);
+ } else {
+ error('Test "' + value + '" not allowed in "' + command.identifier + '" command');
+ }
+ } else if (command) {
+ if (command.commands) {
+ command.commands.push(new_command);
+ } else {
+ error('commands not allowed in "' + command.identifier + '" command');
+ }
+ } else {
+ tree.push(new_command);
+ }
+ levels.push(new_command);
+ command = new_command;
+ if (command.require) {
+ (Array.isArray(command.require) ? command.require : [command.require])
+ .forEach(string => requires.push(string));
+ }
+ if (command.comparator) {
+ requires.push('comparator-' + command.comparator);
+ }
+ break; }
+
+ // Arguments
+ case T_TAG:
+ pushArg(value.toLowerCase());
+ break;
+ case T_STRING_LIST:
+ pushArg(GrammarStringList.fromString(value));
+ break;
+ case T_MULTILINE_STRING:
+ pushArg(GrammarMultiLine.fromString(value));
+ break;
+ case T_QUOTED_STRING:
+ pushArg(new GrammarQuotedString(value.substr(1,value.length-2)));
+ break;
+ case T_NUMBER:
+ pushArg(new GrammarNumber(value));
+ break;
+
+ // Comments
+ case T_BRACKET_COMMENT:
+ case T_HASH_COMMENT: {
+ let obj = (T_HASH_COMMENT == type)
+ ? new GrammarHashComment(value.substr(1).trim())
+ : new GrammarBracketComment(value.substr(2, value.length-4));
+ if (command) {
+ if (!command.comments) {
+ command.comments = [];
+ }
+ (command.commands || command.comments).push(obj);
+ } else {
+ tree.push(obj);
+ }
+ break; }
+
+ case T_WHITESPACE:
+// (command ? command.commands : tree).push(value.trim());
+ command || tree.push(value.trim());
+ break;
+
+ // Command end
+ case T_SEMICOLON:
+ command || error('Semicolon not at end of command');
+ pushArgs();
+ if (command instanceof RequireCommand) {
+ command.capabilities.forEach(string => requires.push(string.value));
+ }
+ levels.pop();
+ command = levels.last();
+ break;
+
+ // Command block
+ case T_BLOCK_START:
+ pushArgs();
+ // https://tools.ietf.org/html/rfc5228#section-2.9
+ // Action commands do not take tests or blocks
+ while (command && !(command instanceof ConditionalCommand)) {
+ levels.pop();
+ command = levels.last();
+ }
+ command || error('Block start not part of control command');
+ break;
+ case T_BLOCK_END:
+ (command instanceof ConditionalCommand) || error('Block end has no matching block start');
+ levels.pop();
+// prev_command = command;
+ command = levels.last();
+ break;
+
+ // anyof / allof ( ... , ... )
+ case T_LEFT_PARENTHESIS:
+ pushArgs();
+ while (command && !(command.tests instanceof GrammarTestList)) {
+ levels.pop();
+ command = levels.last();
+ }
+ command || error('Test start not part of anyof/allof test');
+ break;
+ case T_RIGHT_PARENTHESIS:
+ pushArgs();
+ levels.pop();
+ command = levels.last();
+ (command.tests instanceof GrammarTestList) || error('Test end not part of test-list');
+ break;
+ case T_COMMA:
+ pushArgs();
+ // Must be inside PARENTHESIS aka test-list
+ while (command && !(command.tests instanceof GrammarTestList)) {
+ levels.pop();
+ command = levels.last();
+ }
+ command || error('Comma not part of test-list');
+ break;
+
+ case T_UNKNOWN:
+ error('Invalid token ' + value);
+ }
+
+ // Set current script position
+ line += (value.split('\n').length - 1); // (value.match(/\n/g) || []).length;
+ }
+
+ tree.requires = requires;
+ return tree;
+};
diff --git a/dev/Sieve/README.md b/dev/Sieve/README.md
new file mode 100644
index 000000000..e1988527f
--- /dev/null
+++ b/dev/Sieve/README.md
@@ -0,0 +1,33 @@
+https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml
+
+- [ ] RFC2852 envelope-deliverby / redirect-deliverby
+- [ ] RFC3461 envelope-dsn / redirect-dsn
+- [x] RFC3894 copy
+- [ ] RFC4790 comparator-*
+- [x] RFC5173 body
+- [x] RFC5183 environment
+- [x] RFC5228 encoded-character / envelope / fileinto
+- [x] RFC5229 variables
+- [x] RFC5230 vacation
+- [x] RFC5231 relational
+- [x] RFC5232 imap4flags
+- [x] RFC5233 subaddress
+- [x] RFC5235 spamtest / spamtestplus / virustest
+- [x] RFC5260 date / index
+- [x] RFC5293 editheader
+- [x] RFC5429 ereject / reject
+- [x] RFC5435 enotify
+- [x] RFC5463 ihave
+- [x] RFC5490 mailbox / mboxmetadata / servermetadata
+- [ ] RFC5703 enclose / extracttext / foreverypart / mime / replace
+- [x] RFC6131 vacation-seconds
+- [ ] RFC6134 extlists
+- [ ] RFC6558 convert
+- [x] RFC6609 include
+- [ ] RFC6785 imapsieve
+- [ ] RFC7352 duplicate
+- [ ] RFC8579 special-use
+- [ ] RFC8580 fcc
+- [ ] RFC regex https://tools.ietf.org/html/draft-ietf-sieve-regex-01
+- [ ] vnd.cyrus.*
+- [ ] vnd.dovecot.*
diff --git a/dev/Sieve/RegEx.js b/dev/Sieve/RegEx.js
new file mode 100644
index 000000000..6a883e0ca
--- /dev/null
+++ b/dev/Sieve/RegEx.js
@@ -0,0 +1,203 @@
+/**
+ * https://tools.ietf.org/html/rfc5228#section-8
+ */
+
+export const
+ /**************************************************
+ * https://tools.ietf.org/html/rfc5228#section-8.1
+ **************************************************/
+
+ /**
+ * octet-not-crlf = %x01-09 / %x0B-0C / %x0E-FF
+ * a single octet other than NUL, CR, or LF
+ */
+ OCTET_NOT_CRLF = '[^\\x00\\r\\n]',
+
+ /**
+ * octet-not-period = %x01-09 / %x0B-0C / %x0E-2D / %x2F-FF
+ * a single octet other than NUL, CR, LF, or period
+ */
+ OCTET_NOT_PERIOD = '[^\\x00\\r\\n\\.]',
+
+ /**
+ * octet-not-qspecial = %x01-09 / %x0B-0C / %x0E-21 / %x23-5B / %x5D-FF
+ * a single octet other than NUL, CR, LF, double-quote, or backslash
+ */
+ OCTET_NOT_QSPECIAL = '[^\\x00\\r\\n"\\\\]',
+
+ /**
+ * hash-comment = "#" *octet-not-crlf CRLF
+ */
+ HASH_COMMENT = '#' + OCTET_NOT_CRLF + '*\\r\\n',
+
+ /**
+ * QUANTIFIER = "K" / "M" / "G"
+ */
+ QUANTIFIER = '[KMGkmg]',
+
+ /**
+ * quoted-safe = CRLF / octet-not-qspecial
+ * either a CRLF pair, OR a single octet other than NUL, CR, LF, double-quote, or backslash
+ */
+ QUOTED_SAFE = '\\r\\n|' + OCTET_NOT_QSPECIAL,
+
+ /**
+ * quoted-special = "\" (DQUOTE / "\")
+ * represents just a double-quote or backslash
+ */
+ QUOTED_SPECIAL = '\\\\\\\\|\\\\"',
+
+ /**
+ * quoted-text = *(quoted-safe / quoted-special / quoted-other)
+ */
+ QUOTED_TEXT = '(?:' + QUOTED_SAFE + '|' + QUOTED_SPECIAL + ')*',
+
+ /**
+ * multiline-literal = [ octet-not-period *octet-not-crlf ] CRLF
+ */
+ MULTILINE_LITERAL = OCTET_NOT_PERIOD + OCTET_NOT_CRLF + '*\\r\\n',
+
+ /**
+ * multiline-dotstart = "." 1*octet-not-crlf CRLF
+ ; A line containing only "." ends the multi-line.
+ ; Remove a leading '.' if followed by another '.'.
+ */
+ MULTILINE_DOTSTART = '\\.' + OCTET_NOT_CRLF + '+\\r\\n',
+
+ /**
+ * not-star = CRLF / %x01-09 / %x0B-0C / %x0E-29 / %x2B-FF
+ * either a CRLF pair, OR a single octet other than NUL, CR, LF, or star
+ */
+// NOT_STAR: '\\r\\n|[^\\x00\\r\\n*]',
+
+ /**
+ * not-star-slash = CRLF / %x01-09 / %x0B-0C / %x0E-29 / %x2B-2E / %x30-FF
+ * either a CRLF pair, OR a single octet other than NUL, CR, LF, star, or slash
+ */
+// NOT_STAR_SLASH: '\\r\\n|[^\\x00\\r\\n*\\\\]',
+
+ /**
+ * STAR = "*"
+ */
+// STAR = '\\*',
+
+ /**
+ * bracket-comment = "/*" *not-star 1*STAR *(not-star-slash *not-star 1*STAR) "/"
+ */
+ BRACKET_COMMENT = '/\\*[\\s\\S]*?\\*/',
+
+ /**
+ * identifier = (ALPHA / "_") *(ALPHA / DIGIT / "_")
+ */
+ IDENTIFIER = '[a-zA-Z_][a-zA-Z0-9_]*',
+
+ /**
+ * multi-line = "text:" *(SP / HTAB) (hash-comment / CRLF)
+ *(multiline-literal / multiline-dotstart)
+ "." CRLF
+ */
+ MULTI_LINE = 'text:[ \\t]*(?:' + HASH_COMMENT + ')?\\r\\n'
+ + '(?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*'
+ + '\\.\\r\\n',
+
+ /**
+ * number = 1*DIGIT [ QUANTIFIER ]
+ */
+ NUMBER = '[0-9]+' + QUANTIFIER + '?',
+
+ /**
+ * quoted-string = DQUOTE quoted-text DQUOTE
+ */
+ QUOTED_STRING = '"' + QUOTED_TEXT + '"',
+
+ /**
+ * tag = ":" identifier
+ */
+ TAG = ':[a-zA-Z_][a-zA-Z0-9_]*',
+
+ /**
+ * comment = bracket-comment / hash-comment
+ */
+// COMMENT = BRACKET_COMMENT + '|' + HASH_COMMENT;
+
+ /**************************************************
+ * https://tools.ietf.org/html/rfc5228#section-8.2
+ **************************************************/
+
+ /**
+ * string = quoted-string / multi-line
+ */
+ STRING = QUOTED_STRING + '|' + MULTI_LINE,
+
+ /**
+ * string-list = "[" string *("," string) "]" / string
+ * if there is only a single string, the brackets are optional
+ */
+ STRING_LIST = '\\[\\s*(?:' + STRING + ')(?:\\s*,\\s*(?:' + STRING + '))*\\s*\\]',
+// + '|(?:' + STRING + ')',
+
+ /**
+ * argument = string-list / number / tag
+ */
+ ARGUMENT = STRING_LIST + '|' + STRING + '|' + NUMBER + '|' + TAG;
+
+ /**
+ * arguments = *argument [ test / test-list ]
+ * This is not possible with regular expressions
+ */
+// ARGUMENTS = '(?:\\s+' . self::ARGUMENT . ')*(\\s+?:' . self::TEST . '|' . self::TEST_LIST . ')?',
+
+ /**
+ * block = "{" commands "}"
+ * This is not possible with regular expressions
+ */
+// BLOCK = '{' . self::COMMANDS . '}',
+
+ /**
+ * command = identifier arguments (";" / block)
+ * This is not possible with regular expressions
+ */
+// COMMAND = self::IDENTIFIER . self::ARGUMENTS . '\\s+(?:;|' . self::BLOCK . ')',
+
+ /**
+ * commands = *command
+ * This is not possible with regular expressions
+ */
+// COMMANDS = '(?:' . self::COMMAND . ')*',
+
+ /**
+ * start = commands
+ * This is not possible with regular expressions
+ */
+// START = self::COMMANDS,
+
+ /**
+ * test = identifier arguments
+ * This is not possible with regular expressions
+ */
+// TEST = self::IDENTIFIER . self::ARGUMENTS,
+
+ /**
+ * test-list = "(" test *("," test) ")"
+ * This is not possible with regular expressions
+ */
+// TEST_LIST = '\\(\\s*' . self::TEST . '(?:\\s*,\\s*' . self::TEST . ')*\\s*\\)',
+
+ /**************************************************
+ * https://tools.ietf.org/html/rfc5228#section-8.3
+ **************************************************/
+
+ /**
+ * ADDRESS-PART = ":localpart" / ":domain" / ":all"
+ */
+// ADDRESS_PART = ':localpart|:domain|:all',
+
+ /**
+ * COMPARATOR = ":comparator" string
+ */
+// COMPARATOR = ':comparator\\s+(?:' + STRING + ')';
+
+ /**
+ * MATCH-TYPE = ":is" / ":contains" / ":matches"
+ */
+// MATCH_TYPE = ':is|:contains|:matches'
diff --git a/dev/Sieve/Tests.js b/dev/Sieve/Tests.js
new file mode 100644
index 000000000..30949f0ed
--- /dev/null
+++ b/dev/Sieve/Tests.js
@@ -0,0 +1,288 @@
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5
+ */
+
+import {
+ GrammarNumber,
+ GrammarString,
+ GrammarStringList,
+ GrammarTest,
+ GrammarTestList
+} from 'Sieve/Grammar';
+
+const
+ isAddressPart = tag => ':localpart' === tag || ':domain' === tag || ':all' === tag || isSubAddressPart(tag),
+ // https://tools.ietf.org/html/rfc5233
+ isSubAddressPart = tag => ':user' === tag || ':detail' === tag;
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.1
+ */
+export class AddressTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.address_part = ':all';
+ this.header_list = new GrammarStringList;
+ this.key_list = new GrammarStringList;
+ // rfc5260#section-6
+// this.index = new GrammarNumber;
+// this.last = false;
+ }
+
+ get require() {
+ let requires = [];
+ isSubAddressPart(this.address_part) && requires.push('subaddress');
+ (this.last || (this.index && this.index.value)) && requires.push('index');
+ return requires;
+ }
+
+ toString()
+ {
+ return 'address'
+// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.address_part
+ + ' ' + this.match_type
+ + ' ' + this.header_list.toString()
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ args.forEach((arg, i) => {
+ if (isAddressPart(arg)) {
+ this.address_part = arg;
+ } else if (':last' === arg) {
+ this.last = true;
+ } else if (':index' === args[i-1]) {
+ this.index.value = arg.value;
+ } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
+ this[args[i+1] ? 'header_list' : 'key_list'] = arg;
+// (args[i+1] ? this.header_list : this.key_list) = arg;
+ }
+ });
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.2
+ */
+export class AllOfTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.tests = new GrammarTestList;
+ }
+
+ toString()
+ {
+ return 'allof ' + this.tests.toString();
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.3
+ */
+export class AnyOfTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.tests = new GrammarTestList;
+ }
+
+ toString()
+ {
+ return 'anyof ' + this.tests.toString();
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.4
+ */
+export class EnvelopeTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.address_part = ':all';
+ this.envelope_part = new GrammarStringList;
+ this.key_list = new GrammarStringList;
+ }
+
+ get require() { return isSubAddressPart(this.address_part) ? ['envelope','subaddress'] : 'envelope'; }
+
+ toString()
+ {
+ return 'envelope'
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.address_part
+ + ' ' + this.match_type
+ + ' ' + this.envelope_part.toString()
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ args.forEach((arg, i) => {
+ if (isAddressPart(arg)) {
+ this.address_part = arg;
+ } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
+ this[args[i+1] ? 'envelope_part' : 'key_list'] = arg;
+// (args[i+1] ? this.envelope_part : this.key_list) = arg;
+ }
+ });
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.5
+ */
+export class ExistsTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.header_names = new GrammarStringList;
+ }
+
+ toString()
+ {
+ return 'exists ' + this.header_names.toString();
+ }
+
+ pushArguments(args)
+ {
+ if (args[0] instanceof GrammarStringList) {
+ this.header_names = args;
+ } else if (args[0] instanceof GrammarString) {
+ this.header_names.push(args[0].value);
+ }
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.6
+ */
+export class FalseTest extends GrammarTest
+{
+ toString()
+ {
+ return "false";
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.7
+ */
+export class HeaderTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.address_part = ':all';
+ this.header_names = new GrammarStringList;
+ this.key_list = new GrammarStringList;
+ // rfc5260#section-6
+// this.index = new GrammarNumber;
+// this.last = false;
+ }
+
+ get require() {
+ let requires = [];
+ isSubAddressPart(this.address_part) && requires.push('subaddress');
+ (this.last || (this.index && this.index.value)) && requires.push('index');
+ return requires;
+ }
+
+ toString()
+ {
+ return 'header'
+// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + ' ' + this.match_type
+ + ' ' + this.header_names.toString()
+ + ' ' + this.key_list.toString();
+ }
+
+ pushArguments(args)
+ {
+ args.forEach((arg, i) => {
+ if (isAddressPart(arg)) {
+ this.address_part = arg;
+ } else if (':last' === arg) {
+ this.last = true;
+ } else if (':index' === args[i-1]) {
+ this.index.value = arg.value;
+ } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
+ this[args[i+1] ? 'header_names' : 'key_list'] = arg;
+// (args[i+1] ? this.header_names : this.key_list) = arg;
+ }
+ });
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.8
+ */
+export class NotTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.test = new GrammarTest;
+ }
+
+ toString()
+ {
+ return 'not ' + this.test;
+ }
+
+ pushArguments()
+ {
+ throw 'No arguments';
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.9
+ */
+export class SizeTest extends GrammarTest
+{
+ constructor()
+ {
+ super();
+ this.mode = ':over'; // :under
+ this.limit = 0;
+ }
+
+ toString()
+ {
+ return 'size ' + this.mode + ' ' + this.limit;
+ }
+
+ pushArguments(args)
+ {
+ args.forEach(arg => {
+ if (':over' === arg || ':under' === arg) {
+ this.mode = arg;
+ } else if (arg instanceof GrammarNumber) {
+ this.limit = arg;
+ }
+ });
+ }
+}
+
+/**
+ * https://tools.ietf.org/html/rfc5228#section-5.10
+ */
+export class TrueTest extends GrammarTest
+{
+ toString()
+ {
+ return 'true';
+ }
+}
diff --git a/dev/Sieve/Utils.js b/dev/Sieve/Utils.js
new file mode 100644
index 000000000..9b71a4cd5
--- /dev/null
+++ b/dev/Sieve/Utils.js
@@ -0,0 +1,59 @@
+export const
+ // import { i18n } from 'Common/Translator';
+ i18n = rl.i18n,
+
+ // import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
+ forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
+ forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
+
+ // import { koArrayWithDestroy } from 'External/ko';
+ // With this we don't need delegateRunOnDestroy
+ koArrayWithDestroy = data => {
+ data = ko.observableArray(data);
+ data.subscribe(changes =>
+ changes.forEach(item =>
+ 'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
+ )
+ , data, 'arrayChange');
+ return data;
+ },
+
+ // import { koComputable } from 'External/ko';
+ koComputable = fn => ko.computed(fn, {'pure':true}),
+
+ arrayToString = (arr, separator) =>
+ arr.map(item => item.toString ? item.toString() : item).join(separator),
+/*
+ getNotificationMessage = code => {
+ let key = getKeyByValue(Notification, code);
+ return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
+ rl.i18n('NOTIFICATIONS/')
+ },
+ getNotification = (code, message = '', defCode = 0) => {
+ code = parseInt(code, 10) || 0;
+ if (Notification.ClientViewError === code && message) {
+ return message;
+ }
+
+ return getNotificationMessage(code)
+ || getNotificationMessage(parseInt(defCode, 10))
+ || '';
+ },
+*/
+ getNotification = code => 'ERROR ' + code,
+
+ Remote = rl.app.Remote,
+
+ // capabilities
+ capa = ko.observableArray(),
+
+ // Sieve scripts SieveScriptModel
+ scripts = koArrayWithDestroy(),
+
+ loading = ko.observable(false),
+ serverError = ko.observable(false),
+ serverErrorDesc = ko.observable(''),
+ setError = text => {
+ serverError(true);
+ serverErrorDesc(text);
+ };
diff --git a/dev/Sieve/View/Filter.js b/dev/Sieve/View/Filter.js
new file mode 100644
index 000000000..9c3a5493b
--- /dev/null
+++ b/dev/Sieve/View/Filter.js
@@ -0,0 +1,204 @@
+import { FilterAction } from 'Sieve/Model/Filter';
+import { FilterConditionField, FilterConditionType } from 'Sieve/Model/FilterCondition';
+/*
+import { SettingsUserStore } from 'Stores/User/Settings';
+*/
+
+import {
+ capa,
+ i18n,
+ koComputable
+} from 'Sieve/Utils';
+
+const
+ // import { defaultOptionsAfterRender } from 'Common/Utils';
+ defaultOptionsAfterRender = (domItem, item) =>
+ domItem && item && undefined !== item.disabled
+ && domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
+
+ // import { folderListOptionsBuilder } from 'Common/Folders';
+ /**
+ * @param {Array=} aDisabled
+ * @param {Array=} aHeaderLines
+ * @param {Function=} fRenameCallback
+ * @returns {Array}
+ */
+ folderListOptionsBuilder = (
+ aDisabled,
+ aHeaderLines,
+ fRenameCallback
+ ) => {
+ const
+ aResult = [],
+ sDeepPrefix = '\u00A0\u00A0\u00A0',
+ showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
+
+ foldersWalk = folders => {
+ folders.forEach(oItem => {
+ if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
+ aResult.push({
+ id: oItem.fullName,
+ name:
+ sDeepPrefix.repeat(oItem.deep) +
+ fRenameCallback(oItem),
+ system: false,
+ disabled: !oItem.selectable() || aDisabled.includes(oItem.fullName)
+ });
+ }
+
+ if (oItem.subFolders.length) {
+ foldersWalk(oItem.subFolders());
+ }
+ });
+ };
+
+
+ fRenameCallback = fRenameCallback || (oItem => oItem.name());
+ Array.isArray(aDisabled) || (aDisabled = []);
+
+ Array.isArray(aHeaderLines) && aHeaderLines.forEach(line =>
+ aResult.push({
+ id: line[0],
+ name: line[1],
+ system: false,
+ disabled: false
+ })
+ );
+
+ // FolderUserStore.folderList()
+ foldersWalk(window.Sieve.folderList() || []);
+
+ return aResult;
+ };
+
+export class FilterPopupView extends rl.pluginPopupView {
+ constructor() {
+ super('Filter');
+
+ this.addObservables({
+ isNew: true,
+ filter: null,
+ allowMarkAsRead: false,
+ selectedFolderValue: ''
+ });
+
+ this.defaultOptionsAfterRender = defaultOptionsAfterRender;
+ this.folderSelectList = koComputable(() =>
+ folderListOptionsBuilder(
+ [rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'],
+ [['', '']],
+ item => item ? item.localName() : ''
+ )
+ );
+
+ this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
+
+ ['actionTypeOptions','fieldOptions','typeOptions','typeOptionsSize','typeOptionsBody'].forEach(
+ key => this[key] = ko.observableArray()
+ );
+
+ this.populateOptions();
+ }
+
+ saveFilter() {
+ if (FilterAction.MoveTo === this.filter().actionType()) {
+ this.filter().actionValue(this.selectedFolderValue());
+ }
+
+ if (this.filter().verify()) {
+ this.fTrueCallback();
+ this.close();
+ }
+ }
+
+ populateOptions() {
+ this.actionTypeOptions([]);
+
+ let i18nFilter = key => i18n('POPUPS_FILTER/SELECT_' + key);
+
+ this.fieldOptions([
+ { id: FilterConditionField.From, name: i18n('GLOBAL/FROM') },
+ { id: FilterConditionField.Recipient, name: i18nFilter('FIELD_RECIPIENTS') },
+ { id: FilterConditionField.Subject, name: i18n('GLOBAL/SUBJECT') },
+ { id: FilterConditionField.Size, name: i18nFilter('FIELD_SIZE') },
+ { id: FilterConditionField.Header, name: i18nFilter('FIELD_HEADER') }
+ ]);
+
+ this.typeOptions([
+ { id: FilterConditionType.Contains, name: i18nFilter('TYPE_CONTAINS') },
+ { id: FilterConditionType.NotContains, name: i18nFilter('TYPE_NOT_CONTAINS') },
+ { id: FilterConditionType.EqualTo, name: i18nFilter('TYPE_EQUAL_TO') },
+ { id: FilterConditionType.NotEqualTo, name: i18nFilter('TYPE_NOT_EQUAL_TO') }
+ ]);
+
+ // this.actionTypeOptions.push({id: FilterAction.None,
+ // name: i18n('GLOBAL/NONE')});
+ if (capa) {
+ this.allowMarkAsRead(capa.includes('imap4flags'));
+
+ if (capa.includes('fileinto')) {
+ this.actionTypeOptions.push({
+ id: FilterAction.MoveTo,
+ name: i18nFilter('ACTION_MOVE_TO')
+ });
+ this.actionTypeOptions.push({
+ id: FilterAction.Forward,
+ name: i18nFilter('ACTION_FORWARD_TO')
+ });
+ }
+
+ if (capa.includes('reject')) {
+ this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') });
+ }
+
+ if (capa.includes('vacation')) {
+ this.actionTypeOptions.push({
+ id: FilterAction.Vacation,
+ name: i18nFilter('ACTION_VACATION_MESSAGE')
+ });
+ }
+
+ if (capa.includes('body')) {
+ this.fieldOptions.push({ id: FilterConditionField.Body, name: i18nFilter('FIELD_BODY') });
+ }
+
+ if (capa.includes('regex')) {
+ this.typeOptions.push({ id: FilterConditionType.Regex, name: 'Regex' });
+ }
+ }
+
+ this.actionTypeOptions.push({ id: FilterAction.Discard, name: i18nFilter('ACTION_DISCARD') });
+
+ this.typeOptionsSize([
+ { id: FilterConditionType.Over, name: i18nFilter('TYPE_OVER') },
+ { id: FilterConditionType.Under, name: i18nFilter('TYPE_UNDER') }
+ ]);
+
+ this.typeOptionsBody([
+ { id: FilterConditionType.Text, name: i18nFilter('TYPE_TEXT') },
+ { id: FilterConditionType.Raw, name: i18nFilter('TYPE_RAW') }
+ ]);
+ }
+
+ removeCondition(oConditionToDelete) {
+ this.filter().removeCondition(oConditionToDelete);
+ }
+
+ beforeShow(oFilter, fTrueCallback, bEdit) {
+// onShow(oFilter, fTrueCallback, bEdit) {
+ this.isNew(!bEdit);
+
+ this.fTrueCallback = fTrueCallback;
+ this.filter(oFilter);
+
+ this.selectedFolderValue(oFilter.actionValue());
+
+ bEdit || oFilter.nameFocused(true);
+
+ this.populateOptions();
+ }
+
+ afterShow() {
+ this.isNew() && this.filter().nameFocused(true);
+ }
+}
diff --git a/dev/Sieve/View/Script.js b/dev/Sieve/View/Script.js
new file mode 100644
index 000000000..71a755fd5
--- /dev/null
+++ b/dev/Sieve/View/Script.js
@@ -0,0 +1,155 @@
+import { FilterModel } from 'Sieve/Model/Filter';
+import { SieveScriptModel } from 'Sieve/Model/Script';
+
+import { FilterPopupView } from 'Sieve/View/Filter';
+
+import { parseScript } from 'Sieve/Parser';
+
+import {
+ capa,
+ scripts,
+ getNotification,
+ Remote
+} from 'Sieve/Utils';
+
+export class SieveScriptPopupView extends rl.pluginPopupView {
+ constructor() {
+ super('SieveScript');
+
+ this.addObservables({
+ saveError: false,
+ errorText: '',
+ rawActive: false,
+ allowToggle: false,
+ script: null
+ });
+
+ this.sieveCapabilities = capa.join(' ');
+ this.saving = false;
+
+ this.filterForDeletion = ko.observable(null).askDeleteHelper();
+ }
+
+ validateScript() {
+ try {
+ this.errorText('');
+ parseScript(this.script().body());
+ } catch (e) {
+ this.errorText(e.message);
+ }
+ }
+
+ saveScript() {
+ let self = this,
+ script = self.script();
+ if (!self.saving/* && script.hasChanges()*/) {
+ this.errorText('');
+ self.saveError(false);
+
+ if (!script.verify()) {
+ return;
+ }
+
+ if (!script.exists() && scripts.find(item => item.name() === script.name())) {
+ script.nameError(true);
+ return;
+ }
+
+ try {
+ parseScript(this.script().body());
+ } catch (e) {
+ this.errorText(e.message);
+ return;
+ }
+
+ self.saving = true;
+
+ if (self.allowToggle()) {
+ script.body(script.filtersToRaw());
+ }
+
+ Remote.request('FiltersScriptSave',
+ (iError, data) => {
+ self.saving = false;
+
+ if (iError) {
+ self.saveError(true);
+ self.errorText((data && data.ErrorMessageAdditional) || getNotification(iError));
+ } else {
+ script.exists() || scripts.push(script);
+ script.exists(true);
+ script.hasChanges(false);
+ }
+ },
+ script.toJson()
+ );
+ }
+ }
+
+ deleteFilter(filter) {
+ this.script().filters.remove(filter);
+ }
+
+ addFilter() {
+ /* this = SieveScriptModel */
+ const filter = new FilterModel();
+ filter.generateID();
+ FilterPopupView.showModal([
+ filter,
+ () => this.filters.push(filter)
+ ]);
+ }
+
+ editFilter(filter) {
+ const clonedFilter = filter.cloneSelf();
+ FilterPopupView.showModal([
+ clonedFilter,
+ () => {
+ const script = this.script(),
+ filters = script.filters(),
+ index = filters.indexOf(filter);
+ if (-1 < index) {
+// script.filters.splice(index, 1, clonedFilter);
+ filters[index] = clonedFilter;
+ script.filters(filters);
+ }
+ },
+ true
+ ]);
+ }
+
+ toggleFiltersRaw() {
+ let script = this.script(), notRaw = !this.rawActive();
+ if (notRaw) {
+ script.body(script.filtersToRaw());
+ script.hasChanges(script.hasChanges());
+ }
+ this.rawActive(notRaw);
+ }
+
+ onBuild(oDom) {
+ oDom.addEventListener('click', event => {
+ const el = event.target.closestWithin('td.e-action', oDom),
+ filter = el && ko.dataFor(el);
+ filter && this.editFilter(filter);
+ });
+ }
+
+ beforeShow(oScript) {
+// onShow(oScript) {
+ oScript = oScript || new SieveScriptModel();
+ let raw = !oScript.allowFilters();
+ this.script(oScript);
+ this.rawActive(raw);
+ this.allowToggle(!raw);
+ this.saveError(false);
+ this.errorText('');
+
+/*
+ // TODO: Sieve GUI
+ let tree = parseScript(oScript.body(), oScript.name());
+ console.dir(tree);
+ console.log(tree.join('\r\n'));
+*/
+ }
+}
diff --git a/dev/Storage/Client.js b/dev/Storage/Client.js
index 30e753d19..b8bb55060 100644
--- a/dev/Storage/Client.js
+++ b/dev/Storage/Client.js
@@ -1,13 +1,37 @@
-const storage = localStorage,
-CLIENT_SIDE_STORAGE_INDEX_NAME = 'rlcsc',
-getStorage = () => {
- try {
- const value = storage.getItem(CLIENT_SIDE_STORAGE_INDEX_NAME) || null;
- return null == value ? null : JSON.parse(value);
- } catch (e) {
- return null;
- }
-};
+const
+ win = window,
+ CLIENT_SIDE_STORAGE_INDEX_NAME = 'rlcsc',
+ sName = 'localStorage',
+ getStorage = () => {
+ try {
+ const value = localStorage.getItem(CLIENT_SIDE_STORAGE_INDEX_NAME);
+ return value ? JSON.parse(value) : null;
+ } catch (e) {
+ return null;
+ }
+ };
+
+// Storage
+try {
+ win[sName].setItem(sName, '');
+ win[sName].getItem(sName);
+ win[sName].removeItem(sName);
+} catch (e) {
+ console.error(e);
+ // initialise if there's already data
+ let data = document.cookie.match(/(^|;) ?localStorage=([^;]+)/);
+ data = data ? decodeURIComponent(data[2]) : null;
+ data = data ? JSON.parse(data) : {};
+ win[sName] = {
+ getItem: key => data[key] == null ? null : data[key],
+ setItem: (key, value) => {
+ data[key] = ''+value; // forces the value to a string
+ document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
+ +"; expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
+ +"; path=/; samesite=strict";
+ }
+ };
+}
/**
* @param {number} key
@@ -19,7 +43,7 @@ export function set(key, data) {
storageResult['p' + key] = data;
try {
- storage.setItem(CLIENT_SIDE_STORAGE_INDEX_NAME, JSON.stringify(storageResult));
+ localStorage.setItem(CLIENT_SIDE_STORAGE_INDEX_NAME, JSON.stringify(storageResult));
return true;
} catch (e) {
return false;
@@ -32,10 +56,7 @@ export function set(key, data) {
*/
export function get(key) {
try {
- key = 'p' + key;
- const storageResult = getStorage();
-
- return storageResult && null != storageResult[key] ? storageResult[key] : null;
+ return (getStorage() || {})['p' + key];
} catch (e) {
return null;
}
diff --git a/dev/Stores/Admin/Domain.js b/dev/Stores/Admin/Domain.js
index 37d1c62ba..195ff2b77 100644
--- a/dev/Stores/Admin/Domain.js
+++ b/dev/Stores/Admin/Domain.js
@@ -7,17 +7,19 @@ DomainAdminStore.loading = ko.observable(false);
DomainAdminStore.fetch = () => {
DomainAdminStore.loading(true);
- Remote.domainList((iError, data) => {
- DomainAdminStore.loading(false);
- if (!iError) {
- DomainAdminStore(
- Object.entries(data.Result).map(([name, [enabled, alias]]) => ({
- name: name,
- disabled: ko.observable(!enabled),
- alias: alias,
- deleteAccess: ko.observable(false)
- }))
- );
- }
- });
+ Remote.request('AdminDomainList',
+ (iError, data) => {
+ DomainAdminStore.loading(false);
+ if (!iError) {
+ DomainAdminStore(
+ data.Result.map(item => {
+ item.disabled = ko.observable(item.disabled);
+ item.askDelete = ko.observable(false);
+ return item;
+ })
+ );
+ }
+ }, {
+ IncludeAliases: 1
+ });
};
diff --git a/dev/Stores/Admin/Package.js b/dev/Stores/Admin/Package.js
index 2753dea47..3f4136338 100644
--- a/dev/Stores/Admin/Package.js
+++ b/dev/Stores/Admin/Package.js
@@ -8,35 +8,35 @@ PackageAdminStore.real = ko.observable(true);
PackageAdminStore.loading = ko.observable(false);
+PackageAdminStore.error = ko.observable('');
+
PackageAdminStore.fetch = () => {
PackageAdminStore.loading(true);
- Remote.packagesList((iError, data) => {
+ Remote.request('AdminPackagesList', (iError, data) => {
PackageAdminStore.loading(false);
- if (!iError) {
+ if (iError) {
+ PackageAdminStore.real(false);
+ } else {
PackageAdminStore.real(!!data.Result.Real);
+ PackageAdminStore.error(data.Result.Error);
- let list = [];
const loading = {};
-
PackageAdminStore.forEach(item => {
if (item && item.loading()) {
loading[item.file] = item;
}
});
+ let list = [];
if (isArray(data.Result.List)) {
- list = data.Result.List.map(item => {
- if (item) {
- item.loading = ko.observable(loading[item.file] !== undefined);
- return 'core' === item.type && !item.canBeInstalled ? null : item;
- }
- return null;
- }).filter(v => v);
+ list = data.Result.List.filter(v => v).map(item => {
+ item.loading = ko.observable(loading[item.file] !== undefined);
+ item.enabled = ko.observable(item.enabled);
+ return item;
+ });
}
PackageAdminStore(list);
- } else {
- PackageAdminStore.real(false);
}
});
};
diff --git a/dev/Stores/Admin/Plugin.js b/dev/Stores/Admin/Plugin.js
deleted file mode 100644
index 6b4bc50e2..000000000
--- a/dev/Stores/Admin/Plugin.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import ko from 'ko';
-import Remote from 'Remote/Admin/Fetch';
-
-export const PluginAdminStore = ko.observableArray();
-
-PluginAdminStore.loading = ko.observable(false);
-
-PluginAdminStore.error = ko.observable('');
-
-PluginAdminStore.fetch = () => {
- PluginAdminStore.loading(true);
- Remote.pluginList((iError, data) => {
- PluginAdminStore.loading(false);
- if (!iError) {
- PluginAdminStore(
- data.Result.map(item => ({
- name: item.Name,
- disabled: ko.observable(!item.Enabled),
- configured: ko.observable(!!item.Configured)
- }))
- );
- }
- });
-};
diff --git a/dev/Stores/Theme.js b/dev/Stores/Theme.js
index 3a11d63c9..4864ef04f 100644
--- a/dev/Stores/Theme.js
+++ b/dev/Stores/Theme.js
@@ -1,6 +1,7 @@
import ko from 'ko';
-import { $htmlCL, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
+import { doc, $htmlCL, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
import { isArray } from 'Common/Utils';
+import { serverRequestRaw } from 'Common/Links';
export const ThemeStore = {
themes: ko.observableArray(),
@@ -8,20 +9,30 @@ export const ThemeStore = {
userBackgroundHash: ko.observable(''),
isMobile: ko.observable($htmlCL.contains('rl-mobile')),
- populate: function(){
+ populate: () => {
const themes = Settings.app('themes');
- this.themes(isArray(themes) ? themes : []);
- this.theme(SettingsGet('Theme'));
- if (!this.isMobile()) {
- this.userBackgroundName(SettingsGet('UserBackgroundName'));
- this.userBackgroundHash(SettingsGet('UserBackgroundHash'));
+ ThemeStore.themes(isArray(themes) ? themes : []);
+ ThemeStore.theme(SettingsGet('Theme'));
+ if (!ThemeStore.isMobile()) {
+ ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName'));
+ ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash'));
}
- leftPanelDisabled(this.isMobile());
+ leftPanelDisabled(ThemeStore.isMobile());
}
};
ThemeStore.theme = ko.observable('').extend({ limitedList: ThemeStore.themes });
ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
+
+ThemeStore.userBackgroundHash.subscribe(value => {
+ if (value) {
+ $htmlCL.add('UserBackground');
+ doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")";
+ } else {
+ $htmlCL.remove('UserBackground');
+ doc.body.removeAttribute('style');
+ }
+});
diff --git a/dev/Stores/User/Account.js b/dev/Stores/User/Account.js
index d1f0d3913..4dc389326 100644
--- a/dev/Stores/User/Account.js
+++ b/dev/Stores/User/Account.js
@@ -1,32 +1,13 @@
-import ko from 'ko';
-import { SettingsGet } from 'Common/Globals';
-import { addObservablesTo } from 'Common/Utils';
+import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
export const AccountUserStore = {
- accounts: ko.observableArray(),
+ accounts: koArrayWithDestroy(),
loading: ko.observable(false).extend({ debounce: 100 }),
- getEmailAddresses: () => AccountUserStore.accounts.map(item => item ? item.email : null).filter(v => v),
-
- accountsUnreadCount: ko.computed(() => 0),
- // accountsUnreadCount: ko.computed(() => {
- // let result = 0;
- // AccountUserStore.accounts().forEach(item => {
- // if (item) {
- // result += item.count();
- // }
- // });
- // return result;
- // }),
-
- populate: () => {
- AccountUserStore.email(SettingsGet('Email'));
- AccountUserStore.parentEmail(SettingsGet('ParentEmail'));
- }
+ getEmailAddresses: () => AccountUserStore.accounts.map(item => item.email)
};
addObservablesTo(AccountUserStore, {
email: '',
- parentEmail: '',
signature: ''
});
diff --git a/dev/Stores/User/App.js b/dev/Stores/User/App.js
index 5600e6d61..a38880570 100644
--- a/dev/Stores/User/App.js
+++ b/dev/Stores/User/App.js
@@ -1,6 +1,5 @@
-import { Scope } from 'Common/Enums';
-import { keyScope, leftPanelDisabled, SettingsGet } from 'Common/Globals';
-import { addObservablesTo } from 'Common/Utils';
+import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
+import { addObservablesTo } from 'External/ko';
import { ThemeStore } from 'Stores/Theme';
export const AppUserStore = {
@@ -8,7 +7,7 @@ export const AppUserStore = {
};
addObservablesTo(AppUserStore, {
- focusedState: Scope.None,
+ focusedState: 'none',
threadsAllowed: false,
@@ -16,12 +15,12 @@ addObservablesTo(AppUserStore, {
});
AppUserStore.focusedState.subscribe(value => {
- switch (value) {
- case Scope.MessageList:
- case Scope.MessageView:
- case Scope.FolderList:
+ ['FolderList','MessageList','MessageView'].forEach(name => {
+ if (name === value) {
keyScope(value);
- ThemeStore.isMobile() && leftPanelDisabled(Scope.FolderList !== value);
- break;
- }
+ ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
+ }
+ let dom = elementById('V-Mail'+name);
+ dom && dom.classList.toggle('focused', name === value);
+ });
});
diff --git a/dev/Stores/User/Contact.js b/dev/Stores/User/Contact.js
index e5f0fedfc..e2e19f8f5 100644
--- a/dev/Stores/User/Contact.js
+++ b/dev/Stores/User/Contact.js
@@ -1,26 +1,51 @@
import ko from 'ko';
import { SettingsGet } from 'Common/Globals';
-import { addObservablesTo } from 'Common/Utils';
+import { pInt } from 'Common/Utils';
+import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
+import Remote from 'Remote/User/Fetch';
-export const ContactUserStore = ko.observableArray();
+export const ContactUserStore = koArrayWithDestroy();
ContactUserStore.loading = ko.observable(false).extend({ debounce: 200 });
ContactUserStore.importing = ko.observable(false).extend({ debounce: 200 });
ContactUserStore.syncing = ko.observable(false).extend({ debounce: 200 });
addObservablesTo(ContactUserStore, {
- allowSync: false,
+ allowSync: false, // Admin setting
enableSync: false,
syncUrl: '',
syncUser: '',
syncPass: ''
});
-ContactUserStore.populate = function() {
- this.allowSync(!!SettingsGet('ContactsSyncIsAllowed'));
- this.enableSync(!!SettingsGet('EnableContactsSync'));
-
- this.syncUrl(SettingsGet('ContactsSyncUrl'));
- this.syncUser(SettingsGet('ContactsSyncUser'));
- this.syncPass(SettingsGet('ContactsSyncPassword'));
+/**
+ * @param {Function} fResultFunc
+ * @returns {void}
+ */
+ContactUserStore.sync = fResultFunc => {
+ if (ContactUserStore.enableSync()
+ && !ContactUserStore.importing()
+ && !ContactUserStore.syncing()
+ ) {
+ ContactUserStore.syncing(true);
+ Remote.request('ContactsSync', (iError, oData) => {
+ ContactUserStore.syncing(false);
+ fResultFunc && fResultFunc(iError, oData);
+ }, null, 200000);
+ }
+};
+
+ContactUserStore.init = () => {
+ let value = !!SettingsGet('ContactsSyncIsAllowed');
+ ContactUserStore.allowSync(value);
+ if (value) {
+ ContactUserStore.enableSync(!!SettingsGet('EnableContactsSync'));
+ ContactUserStore.syncUrl(SettingsGet('ContactsSyncUrl'));
+ ContactUserStore.syncUser(SettingsGet('ContactsSyncUser'));
+ ContactUserStore.syncPass(SettingsGet('ContactsSyncPassword'));
+ setTimeout(ContactUserStore.sync, 10000);
+ value = pInt(SettingsGet('ContactsSyncInterval'));
+ value = 5 <= value ? (320 >= value ? value : 320) : 20;
+ setInterval(ContactUserStore.sync, value * 60000 + 5000);
+ }
};
diff --git a/dev/Stores/User/Folder.js b/dev/Stores/User/Folder.js
index 34ed105bf..d1c51862b 100644
--- a/dev/Stores/User/Folder.js
+++ b/dev/Stores/User/Folder.js
@@ -1,15 +1,18 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
import { FolderType, FolderSortMode } from 'Common/EnumsUser';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
-import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
-import { folderListOptionsBuilder } from 'Common/UtilsUser';
+import { forEachObjectEntry } from 'Common/Utils';
+import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
-import { SettingsGet } from 'Common/Globals';
+import { Settings } from 'Common/Globals';
+//import Remote from 'Remote/User/Fetch'; // Circular dependency
export const FolderUserStore = new class {
constructor() {
- addObservablesTo(this, {
+ const self = this;
+ addObservablesTo(self, {
/**
* To use "checkable" option in /#/settings/folders
* When true, getNextFolderNames only lists system and "checkable" folders
@@ -19,14 +22,13 @@ export const FolderUserStore = new class {
*/
displaySpecSetting: false,
- /**
- * If the IMAP server supports SORT
- */
- sortSupported: false,
// sortMode: '',
+ quotaLimit: 0,
+ quotaUsage: 0,
+
sentFolder: '',
- draftFolder: '',
+ draftsFolder: '',
spamFolder: '',
trashFolder: '',
archiveFolder: '',
@@ -42,120 +44,77 @@ export const FolderUserStore = new class {
foldersInboxUnreadCount: 0
});
- this.sortMode = ko.observable('').extend({ limitedList: Object.values(FolderSortMode) });
+ self.sortMode = ko.observable('').extend({ limitedList: Object.values(FolderSortMode) });
- this.namespace = '';
+ self.namespace = '';
- this.folderList = ko.observableArray();
+ self.folderList = ko.observableArray(/*new FolderCollectionModel*/);
- this.currentFolder = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'selected'] });
+ self.capabilities = ko.observableArray();
- this.sieveAllowFileintoInbox = !!SettingsGet('SieveAllowFileintoInbox');
+ self.currentFolder = ko.observable(null).extend({ toggleSubscribeProperty: [self, 'selected'] });
- this.draftFolderNotEnabled = ko.computed(
- () => !this.draftFolder() || UNUSED_OPTION_VALUE === this.draftFolder()
- );
+ addComputablesTo(self, {
- this.foldersListWithSingleInboxRootFolder = ko.computed(
- () => !this.folderList.find(folder => folder && !folder.isSystemFolder() && folder.visible())
- );
+ draftsFolderNotEnabled: () => !self.draftsFolder() || UNUSED_OPTION_VALUE === self.draftsFolder(),
- this.currentFolderFullNameRaw = ko.computed(() => (this.currentFolder() ? this.currentFolder().fullNameRaw : ''));
+ currentFolderFullName: () => (self.currentFolder() ? self.currentFolder().fullName : ''),
+ currentFolderFullNameHash: () => (self.currentFolder() ? self.currentFolder().fullNameHash : ''),
- this.currentFolderFullName = ko.computed(() => (this.currentFolder() ? this.currentFolder().fullName : ''));
- this.currentFolderFullNameHash = ko.computed(() => (this.currentFolder() ? this.currentFolder().fullNameHash : ''));
+ foldersChanging: () =>
+ self.foldersLoading() | self.foldersCreating() | self.foldersDeleting() | self.foldersRenaming(),
- this.foldersChanging = ko.computed(() => {
- const loading = this.foldersLoading(),
- creating = this.foldersCreating(),
- deleting = this.foldersDeleting(),
- renaming = this.foldersRenaming();
+ folderListSystemNames: () => {
+ const list = [getFolderInboxName()],
+ others = [self.sentFolder(), self.draftsFolder(), self.spamFolder(), self.trashFolder(), self.archiveFolder()];
- return loading || creating || deleting || renaming;
+ self.folderList().length &&
+ others.forEach(name => name && UNUSED_OPTION_VALUE !== name && list.push(name));
+
+ return list;
+ },
+
+ folderListSystem: () =>
+ self.folderListSystemNames().map(name => getFolderFromCacheList(name)).filter(v => v)
});
- this.folderListSystemNames = ko.computed(() => {
- const list = [getFolderInboxName()],
- sentFolder = this.sentFolder(),
- draftFolder = this.draftFolder(),
- spamFolder = this.spamFolder(),
- trashFolder = this.trashFolder(),
- archiveFolder = this.archiveFolder();
-
- if (this.folderList.length) {
- if (sentFolder && UNUSED_OPTION_VALUE !== sentFolder) {
- list.push(sentFolder);
- }
- if (draftFolder && UNUSED_OPTION_VALUE !== draftFolder) {
- list.push(draftFolder);
- }
- if (spamFolder && UNUSED_OPTION_VALUE !== spamFolder) {
- list.push(spamFolder);
- }
- if (trashFolder && UNUSED_OPTION_VALUE !== trashFolder) {
- list.push(trashFolder);
- }
- if (archiveFolder && UNUSED_OPTION_VALUE !== archiveFolder) {
- list.push(archiveFolder);
- }
- }
-
- return list;
- });
-
- this.folderListSystem = ko.computed(() =>
- this.folderListSystemNames().map(name => getFolderFromCacheList(name)).filter(v => v)
- );
-
- this.folderMenuForMove = ko.computed(() =>
- folderListOptionsBuilder(
- this.folderListSystem(),
- this.folderList(),
- [this.currentFolderFullNameRaw()],
- [],
- null,
- (item) => (item ? item.localName() : '')
- )
- );
-
- this.folderMenuForFilters = ko.computed(() =>
- folderListOptionsBuilder(
- this.folderListSystem(),
- this.folderList(),
- [this.sieveAllowFileintoInbox ? '' : 'INBOX'],
- [['', '']],
- null,
- (item) => (item ? item.localName() : '')
- )
- );
-
const
- fRemoveSystemFolderType = (observable) => () => {
- const folder = getFolderFromCacheList(observable());
- if (folder) {
- folder.type(FolderType.User);
- }
+ subscribeRemoveSystemFolder = observable => {
+ observable.subscribe(() => {
+ const folder = getFolderFromCacheList(observable());
+ folder && folder.type(FolderType.User);
+ }, self, 'beforeChange');
},
fSetSystemFolderType = type => value => {
const folder = getFolderFromCacheList(value);
- if (folder) {
- folder.type(type);
- }
+ folder && folder.type(type);
};
- this.sentFolder.subscribe(fRemoveSystemFolderType(this.sentFolder), this, 'beforeChange');
- this.draftFolder.subscribe(fRemoveSystemFolderType(this.draftFolder), this, 'beforeChange');
- this.spamFolder.subscribe(fRemoveSystemFolderType(this.spamFolder), this, 'beforeChange');
- this.trashFolder.subscribe(fRemoveSystemFolderType(this.trashFolder), this, 'beforeChange');
- this.archiveFolder.subscribe(fRemoveSystemFolderType(this.archiveFolder), this, 'beforeChange');
+ subscribeRemoveSystemFolder(self.sentFolder);
+ subscribeRemoveSystemFolder(self.draftsFolder);
+ subscribeRemoveSystemFolder(self.spamFolder);
+ subscribeRemoveSystemFolder(self.trashFolder);
+ subscribeRemoveSystemFolder(self.archiveFolder);
- addSubscribablesTo(this, {
- sentFolder: fSetSystemFolderType(FolderType.SentItems),
- draftFolder: fSetSystemFolderType(FolderType.Draft),
+ addSubscribablesTo(self, {
+ sentFolder: fSetSystemFolderType(FolderType.Sent),
+ draftsFolder: fSetSystemFolderType(FolderType.Drafts),
spamFolder: fSetSystemFolderType(FolderType.Spam),
trashFolder: fSetSystemFolderType(FolderType.Trash),
archiveFolder: fSetSystemFolderType(FolderType.Archive)
});
+
+ self.quotaPercentage = koComputable(() => {
+ const quota = self.quotaLimit(), usage = self.quotaUsage();
+ return 0 < quota ? Math.ceil((usage / quota) * 100) : 0;
+ });
+ }
+
+ /**
+ * If the IMAP server supports SORT, METADATA
+ */
+ hasCapability(name) {
+ return this.capabilities().includes(name);
}
/**
@@ -172,12 +131,12 @@ export const FolderUserStore = new class {
list.forEach(folder => {
if (
folder &&
- folder.selectable &&
+ folder.selectable() &&
folder.exists &&
timeout > folder.expires &&
(folder.isSystemFolder() || (folder.subscribed() && (folder.checkable() || !bDisplaySpecSetting)))
) {
- timeouts.push([folder.expires, folder.fullNameRaw]);
+ timeouts.push([folder.expires, folder.fullName]);
}
if (folder && folder.subFolders.length) {
@@ -202,4 +161,16 @@ export const FolderUserStore = new class {
return result.filter((value, index, self) => self.indexOf(value) == index);
}
+
+ saveSystemFolders(folders) {
+ folders = folders || {
+ Sent: FolderUserStore.sentFolder(),
+ Drafts: FolderUserStore.draftsFolder(),
+ Spam: FolderUserStore.spamFolder(),
+ Trash: FolderUserStore.trashFolder(),
+ Archive: FolderUserStore.archiveFolder()
+ };
+ forEachObjectEntry(folders, (k,v)=>Settings.set(k+'Folder',v));
+ rl.app.Remote.request('SystemFoldersUpdate', null, folders);
+ }
};
diff --git a/dev/Stores/User/GnuPG.js b/dev/Stores/User/GnuPG.js
new file mode 100644
index 000000000..be05406c1
--- /dev/null
+++ b/dev/Stores/User/GnuPG.js
@@ -0,0 +1,220 @@
+import ko from 'ko';
+
+import { SettingsCapa } from 'Common/Globals';
+
+//import { EmailModel } from 'Model/Email';
+//import { OpenPgpKeyModel } from 'Model/OpenPgpKey';
+
+import Remote from 'Remote/User/Fetch';
+
+import { showScreenPopup } from 'Knoin/Knoin';
+import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
+import { AskPopupView } from 'View/Popup/Ask';
+
+const
+ askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') =>
+ await AskPopupView.password('GnuPG key ' + privateKey.id + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt),
+
+ findGnuPGKey = (keys, query, sign) =>
+ keys.find(key =>
+ key[sign ? 'can_sign' : 'can_decrypt']
+ && (key.emails.includes(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
+ );
+
+export const GnuPGUserStore = new class {
+ constructor() {
+ /**
+ * PECL gnupg / PEAR Crypt_GPG
+ * [ {email, can_encrypt, can_sign}, ... ]
+ */
+ this.keyring;
+ this.publicKeys = ko.observableArray();
+ this.privateKeys = ko.observableArray();
+ }
+
+ loadKeyrings() {
+ this.keyring = null;
+ this.publicKeys([]);
+ this.privateKeys([]);
+ Remote.request('GnupgGetKeys',
+ (iError, oData) => {
+ if (oData && oData.Result) {
+ this.keyring = oData.Result;
+ const initKey = (key, isPrivate) => {
+ const aEmails = [];
+ key.id = key.subkeys[0].keyid;
+ key.fingerprint = key.subkeys[0].fingerprint;
+ key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
+ key.emails = aEmails;
+ key.askDelete = ko.observable(false);
+ key.openForDeletion = ko.observable(null).askDeleteHelper();
+ key.remove = () => {
+ if (key.askDelete()) {
+ Remote.request('GnupgDeleteKey',
+ (iError, oData) => {
+ if (oData && oData.Result) {
+ if (isPrivate) {
+ this.privateKeys.remove(key);
+ } else {
+ this.publicKeys.remove(key);
+ }
+ }
+ }, {
+ KeyId: key.id,
+ isPrivate: isPrivate
+ }
+ );
+ }
+ };
+ key.view = () => {
+ const fetch = pass => Remote.request('GnupgExportKey',
+ (iError, oData) => {
+ if (oData && oData.Result) {
+ key.armor = oData.Result;
+ showScreenPopup(OpenPgpKeyPopupView, [key]);
+ }
+ }, {
+ KeyId: key.id,
+ isPrivate: isPrivate,
+ Passphrase: pass
+ }
+ );
+ if (isPrivate) {
+ askPassphrase(key, 'POPUP_VIEW_TITLE').then(passphrase => {
+ (null !== passphrase) && fetch(passphrase);
+ });
+ } else {
+ fetch('');
+ }
+ };
+ return key;
+ };
+ this.publicKeys(oData.Result.public.map(key => initKey(key, 0)));
+ this.privateKeys(oData.Result.private.map(key => initKey(key, 1)));
+ console.log('gnupg ready');
+ }
+ }
+ );
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ isSupported() {
+ return SettingsCapa('GnuPG');
+ }
+
+ importKey(key, callback) {
+ Remote.request('GnupgImportKey',
+ (iError, oData) => {
+ if (oData && oData.Result) {
+// this.gnupgKeyring = oData.Result;
+ }
+ callback && callback(iError, oData);
+ }, {
+ Key: key
+ }
+ );
+ }
+
+ /**
+ keyPair.privateKey
+ keyPair.publicKey
+ keyPair.revocationCertificate
+ keyPair.onServer
+ keyPair.inGnuPG
+ */
+ storeKeyPair(keyPair, callback) {
+ Remote.request('PgpStoreKeyPair',
+ (iError, oData) => {
+ if (oData && oData.Result) {
+// this.gnupgKeyring = oData.Result;
+ }
+ callback && callback(iError, oData);
+ }, keyPair
+ );
+ }
+
+ /**
+ * Checks if verifying/encrypting a message is possible with given email addresses.
+ */
+ hasPublicKeyForEmails(recipients) {
+ const count = recipients.length,
+ length = count ? recipients.filter(email =>
+// (key.can_verify || key.can_encrypt) &&
+ this.publicKeys.find(key => key.emails.includes(email))
+ ).length : 0;
+ return length && length === count;
+ }
+
+ getPublicKeyFingerprints(recipients) {
+ const fingerprints = [];
+ recipients.forEach(email => {
+ fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint);
+ });
+ return fingerprints;
+ }
+
+ getPrivateKeyFor(query, sign) {
+ return findGnuPGKey(this.privateKeys, query, sign);
+ }
+
+ async decrypt(message) {
+ const
+ pgpInfo = message.pgpEncrypted();
+ if (pgpInfo) {
+ let ids = [message.to[0].email].concat(pgpInfo.KeyIds),
+ i = ids.length, key;
+ while (i--) {
+ key = findGnuPGKey(this.privateKeys, ids[i]);
+ if (key) {
+ break;
+ }
+ }
+ if (key) {
+ // Also check message.from[0].email
+ let params = {
+ Folder: message.folder,
+ Uid: message.uid,
+ PartId: pgpInfo.PartId,
+ KeyId: key.id,
+ Passphrase: await askPassphrase(key, 'BUTTON_DECRYPT'),
+ Data: '' // message.plain() optional
+ }
+ if (null !== params.Passphrase) {
+ const result = await Remote.post('GnupgDecrypt', null, params);
+ if (result && result.Result) {
+ return result.Result;
+ }
+ }
+ }
+ }
+ }
+
+ async verify(message) {
+ let data = message.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
+ if (data) {
+ data = { ...data }; // clone
+// const sender = message.from[0].email;
+// let mode = await this.hasPublicKeyForEmails([sender]);
+ data.Folder = message.folder;
+ data.Uid = message.uid;
+ if (data.BodyPart) {
+ data.BodyPart = data.BodyPart.raw;
+ data.SigPart = data.SigPart.body;
+ }
+ let response = await Remote.post('MessagePgpVerify', null, data);
+ if (response && response.Result) {
+ return {
+ fingerprint: response.Result.fingerprint,
+ success: 0 == response.Result.status // GOODSIG
+ };
+ }
+ }
+ }
+
+ async sign(privateKey) {
+ return await askPassphrase(privateKey);
+ }
+
+};
diff --git a/dev/Stores/User/Identity.js b/dev/Stores/User/Identity.js
index 4e0f3cbf1..cc4794718 100644
--- a/dev/Stores/User/Identity.js
+++ b/dev/Stores/User/Identity.js
@@ -1,6 +1,6 @@
-import ko from 'ko';
+import { koArrayWithDestroy } from 'External/ko';
-export const IdentityUserStore = ko.observableArray();
+export const IdentityUserStore = koArrayWithDestroy();
IdentityUserStore.getIDS = () => IdentityUserStore.map(item => (item ? item.id() : null))
.filter(value => null !== value);
diff --git a/dev/Stores/User/Message.js b/dev/Stores/User/Message.js
index 33fda90f1..9a491c2bd 100644
--- a/dev/Stores/User/Message.js
+++ b/dev/Stores/User/Message.js
@@ -1,205 +1,30 @@
-import ko from 'ko';
-
-import { Scope, Notification } from 'Common/Enums';
-import { MessageSetAction } from 'Common/EnumsUser';
-import { doc, createElement, elementById } from 'Common/Globals';
-import { isNonEmptyArray, pInt, pString, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
-import { plainToHtml } from 'Common/UtilsUser';
-
-import {
- getFolderInboxName,
- addNewMessageCache,
- setFolderUidNext,
- getFolderFromCacheList,
- setFolderHash,
- MessageFlagsCache,
- addRequestedMessage,
- clearNewMessageCache
-} from 'Common/Cache';
-
-import { mailBox } from 'Common/Links';
-import { i18n, getNotification } from 'Common/Translator';
-
-import { EmailCollectionModel } from 'Model/EmailCollection';
-import { MessageModel } from 'Model/Message';
-import { MessageCollectionModel } from 'Model/MessageCollection';
+import { Scope } from 'Common/Enums';
+import { elementById } from 'Common/Globals';
+import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import { AppUserStore } from 'Stores/User/App';
-import { AccountUserStore } from 'Stores/User/Account';
-import { FolderUserStore } from 'Stores/User/Folder';
-import { PgpUserStore } from 'Stores/User/Pgp';
import { SettingsUserStore } from 'Stores/User/Settings';
-import { NotificationUserStore } from 'Stores/User/Notification';
-
-import Remote from 'Remote/User/Fetch';
-
-const
- hcont = Element.fromHTML('
'),
- getRealHeight = el => {
- hcont.innerHTML = el.outerHTML;
- const result = hcont.clientHeight;
- hcont.innerHTML = '';
- return result;
- },
- /*eslint-disable max-len*/
- url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
- email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
- findEmailAndLinks = html => html
- .replace(url, '$1$2 ')
- .replace(email, '$1$2 '),
- isChecked = item => item.checked(),
-
- // Removes background and color
- // Many e-mails incorrectly only define one, not both
- // And in dark theme mode this kills the readability
- removeColors = html => {
- let l;
- do {
- l = html.length;
- html = html
- .replace(/(<[^>]+[;"'])\s*background(-[a-z]+)?\s*:[^;"']+/gi, '$1')
- .replace(/(<[^>]+[;"'])\s*color\s*:[^;"']+/gi, '$1')
- .replace(/(<[^>]+)\s(bg)?color=("[^"]+"|'[^']+')/gi, '$1');
- } while (l != html.length)
- return html;
- };
-
-doc.body.append(hcont);
export const MessageUserStore = new class {
constructor() {
- this.staticMessage = new MessageModel();
-
- this.list = ko.observableArray().extend({ debounce: 0 });
-
addObservablesTo(this, {
- listCount: 0,
- listSearch: '',
- listThreadUid: '',
- listPage: 1,
- listPageBeforeThread: 1,
- listError: '',
-
- listEndFolder: '',
- listEndSearch: '',
- listEndThreadUid: '',
- listEndPage: 1,
-
- listLoading: false,
- listLoadingAnimation: false,
- listIsNotCompleted: false,
- listCompleteLoading: false,
-
- selectorMessageSelected: null,
- selectorMessageFocused: null,
-
// message viewer
message: null,
- messageViewTrigger: false,
- messageError: '',
- messageLoading: false,
- messageFullScreenMode: false,
+ error: '',
+ loading: false,
+ fullScreen: false,
// Cache mail bodies
- messagesBodiesDom: null,
- messageActiveDom: null
- });
-
- this.listDisableAutoSelect = ko.observable(false).extend({ falseTimeout: 500 });
-
- // Computed Observables
-
- this.listEndHash = ko.computed(
- () =>
- this.listEndFolder() +
- '|' +
- this.listEndSearch() +
- '|' +
- this.listEndThreadUid() +
- '|' +
- this.listEndPage()
- );
-
- this.listPageCount = ko.computed(() =>
- Math.max(1, Math.ceil(this.listCount() / SettingsUserStore.messagesPerPage()))
- );
-
- this.mainMessageListSearch = ko.computed({
- read: this.listSearch,
- write: value =>
- rl.route.setHash(
- mailBox(FolderUserStore.currentFolderFullNameHash(), 1, value.toString().trim(), this.listThreadUid())
- )
- });
-
- this.isMessageSelected = ko.computed(() => null !== this.message());
-
- this.listChecked = ko
- .computed(() => this.list.filter(isChecked))
- .extend({ rateLimit: 0 });
-
- this.hasCheckedMessages = ko
- .computed(() => !!this.list.find(isChecked))
- .extend({ rateLimit: 0 });
-
- this.hasCheckedOrSelected = ko
- .computed(() => !!(this.selectorMessageSelected()
- || this.selectorMessageFocused()
- || this.list.find(item => item.checked())))
- .extend({ rateLimit: 50 });
-
- this.listCheckedOrSelected = ko.computed(() => {
- const
- selectedMessage = this.selectorMessageSelected(),
- focusedMessage = this.selectorMessageFocused(),
- checked = this.list.filter(item => isChecked(item) || item === selectedMessage);
- return checked.length ? checked : (focusedMessage ? [focusedMessage] : []);
- });
-
- this.listCheckedOrSelectedUidsWithSubMails = ko.computed(() => {
- let result = [];
- this.listCheckedOrSelected().forEach(message => {
- result.push(message.uid);
- if (1 < message.threadsLen()) {
- result = result.concat(message.threads()).unique();
- }
- });
- return result;
+ bodiesDom: null,
+ activeDom: null
});
// Subscribers
- let timer = 0, fn = this.listLoadingAnimation;
-
addSubscribablesTo(this, {
- listCompleteLoading: value => {
- if (value) {
- fn(value);
- } else if (fn()) {
- clearTimeout(timer);
- timer = setTimeout(() => {
- fn(value);
- timer = 0;
- }, 700);
- } else {
- fn(value);
- }
- },
-
- listLoading: value =>
- this.listCompleteLoading(value || this.listIsNotCompleted()),
-
- listIsNotCompleted: value =>
- this.listCompleteLoading(value || this.listLoading()),
-
- list:
- (list => {
- list.forEach(item =>
- item && item.newForAnimation() && item.newForAnimation(false)
- )
- }).debounce(500),
-
message: message => {
+ clearTimeout(this.MessageSeenTimer);
+ elementById('rl-right').classList.toggle('message-selected', !!message);
if (message) {
if (!SettingsUserStore.usePreviewPane()) {
AppUserStore.focusedState(Scope.MessageView);
@@ -207,26 +32,21 @@ export const MessageUserStore = new class {
} else {
AppUserStore.focusedState(Scope.MessageList);
- this.messageFullScreenMode(false);
+ this.fullScreen(false);
this.hideMessageBodies();
}
},
-
- listEndFolder: folder => {
- const message = this.message();
- if (message && folder && folder !== message.folder) {
- this.message(null);
- }
- }
});
- this.onMessageResponse = this.onMessageResponse.bind(this);
+ this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000);
+ }
- this.purgeMessageBodyCacheThrottle = this.purgeMessageBodyCache.throttle(30000);
+ toggleFullScreen() {
+ MessageUserStore.fullScreen(!MessageUserStore.fullScreen());
}
purgeMessageBodyCache() {
- const messagesDom = this.messagesBodiesDom(),
+ const messagesDom = this.bodiesDom(),
children = messagesDom && messagesDom.children;
if (children) {
while (15 < children.length) {
@@ -235,486 +55,8 @@ export const MessageUserStore = new class {
}
}
- initUidNextAndNewMessages(folder, uidNext, newMessages) {
- if (getFolderInboxName() === folder && uidNext) {
- if (isNonEmptyArray(newMessages)) {
- newMessages.forEach(item => addNewMessageCache(folder, item.Uid));
-
- NotificationUserStore.playSoundNotification();
-
- const len = newMessages.length;
- if (3 < len) {
- NotificationUserStore.displayDesktopNotification(
- AccountUserStore.email(),
- i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
- COUNT: len
- }),
- { Url: mailBox(newMessages[0].Folder, 1) }
- );
- } else {
- newMessages.forEach(item => {
- NotificationUserStore.displayDesktopNotification(
- EmailCollectionModel.reviveFromJson(item.From).toString(),
- item.Subject,
- { Folder: item.Folder, Uid: item.Uid }
- );
- });
- }
- }
-
- setFolderUidNext(folder, uidNext);
- }
- }
-
hideMessageBodies() {
- const messagesDom = this.messagesBodiesDom();
+ const messagesDom = this.bodiesDom();
messagesDom && Array.from(messagesDom.children).forEach(el => el.hidden = true);
}
-
- /**
- * @param {string} fromFolderFullNameRaw
- * @param {Array} uidForRemove
- * @param {string=} toFolderFullNameRaw = ''
- * @param {boolean=} copy = false
- */
- removeMessagesFromList(fromFolderFullNameRaw, uidForRemove, toFolderFullNameRaw = '', copy = false) {
- uidForRemove = uidForRemove.map(mValue => pInt(mValue));
-
- let unseenCount = 0,
- messageList = this.list,
- currentMessage = this.message();
-
- const trashFolder = FolderUserStore.trashFolder(),
- spamFolder = FolderUserStore.spamFolder(),
- fromFolder = getFolderFromCacheList(fromFolderFullNameRaw),
- toFolder = toFolderFullNameRaw ? getFolderFromCacheList(toFolderFullNameRaw) : null,
- currentFolderFullNameRaw = FolderUserStore.currentFolderFullNameRaw(),
- messages =
- currentFolderFullNameRaw === fromFolderFullNameRaw
- ? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
- : [];
-
- messages.forEach(item => {
- if (item && item.isUnseen()) {
- ++unseenCount;
- }
- });
-
- if (fromFolder && !copy) {
- fromFolder.messageCountAll(
- 0 <= fromFolder.messageCountAll() - uidForRemove.length ? fromFolder.messageCountAll() - uidForRemove.length : 0
- );
-
- if (0 < unseenCount) {
- fromFolder.messageCountUnread(
- 0 <= fromFolder.messageCountUnread() - unseenCount ? fromFolder.messageCountUnread() - unseenCount : 0
- );
- }
- }
-
- if (toFolder) {
- if (trashFolder === toFolder.fullNameRaw || spamFolder === toFolder.fullNameRaw) {
- unseenCount = 0;
- }
-
- toFolder.messageCountAll(toFolder.messageCountAll() + uidForRemove.length);
- if (0 < unseenCount) {
- toFolder.messageCountUnread(toFolder.messageCountUnread() + unseenCount);
- }
-
- toFolder.actionBlink(true);
- }
-
- if (messages.length) {
- if (copy) {
- messages.forEach(item => item.checked(false));
- } else {
- this.listIsNotCompleted(true);
-
- messages.forEach(item => {
- if (currentMessage && currentMessage.hash === item.hash) {
- currentMessage = null;
- this.message(null);
- }
-
- item.deleted(true);
- });
-
- setTimeout(() => messages.forEach(item => messageList.remove(item)), 350);
- }
- }
-
- if (fromFolderFullNameRaw) {
- setFolderHash(fromFolderFullNameRaw, '');
- }
-
- if (toFolderFullNameRaw) {
- setFolderHash(toFolderFullNameRaw, '');
- }
-
- if (this.listThreadUid()) {
- if (
- messageList.length &&
- !!messageList.find(item => !!(item && item.deleted() && item.uid === this.listThreadUid()))
- ) {
- const message = messageList.find(item => item && !item.deleted());
- if (message && this.listThreadUid() !== pString(message.uid)) {
- this.listThreadUid(pString(message.uid));
-
- rl.route.setHash(
- mailBox(
- FolderUserStore.currentFolderFullNameHash(),
- this.listPage(),
- this.listSearch(),
- this.listThreadUid()
- ),
- true,
- true
- );
- } else if (!message) {
- if (1 < this.listPage()) {
- this.listPage(this.listPage() - 1);
-
- rl.route.setHash(
- mailBox(
- FolderUserStore.currentFolderFullNameHash(),
- this.listPage(),
- this.listSearch(),
- this.listThreadUid()
- ),
- true,
- true
- );
- } else {
- this.listThreadUid('');
-
- rl.route.setHash(
- mailBox(
- FolderUserStore.currentFolderFullNameHash(),
- this.listPageBeforeThread(),
- this.listSearch()
- ),
- true,
- true
- );
- }
- }
- }
- }
- }
-
- /**
- * @param {Object} messageTextBody
- */
- initBlockquoteSwitcher(messageTextBody) {
- messageTextBody && messageTextBody.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
- if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
- let h = node.clientHeight || getRealHeight(node);
- if (0 === h || 100 < h) {
- const el = Element.fromHTML('••• ');
- node.classList.add('rl-bq-switcher','hidden-bq');
- node.before(el);
- el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
- }
- }
- });
- }
-
- /**
- * @param {Object} messageTextBody
- * @param {Object} message
- */
- initOpenPgpControls(messageTextBody, message) {
- messageTextBody && messageTextBody.querySelectorAll('.b-plain-openpgp:not(.inited)').forEach(node =>
- PgpUserStore.initMessageBodyControls(node, message)
- );
- }
-
- setMessage(data, cached) {
- let isNew = false,
- body = null,
- json = data && data.Result,
- id = '',
- plain = '',
- resultHtml = '',
- pgpSigned = false,
- messagesDom = this.messagesBodiesDom(),
- selectedMessage = this.selectorMessageSelected(),
- message = this.message();
-
- if (
- json &&
- MessageModel.validJson(json) &&
- message &&
- message.folder === json.Folder
- ) {
- const threads = message.threads();
- if (message.uid !== json.Uid && 1 < threads.length && threads.includes(json.Uid)) {
- message = MessageModel.reviveFromJson(json);
- if (message) {
- message.threads(threads);
- MessageFlagsCache.initMessage(message);
-
- this.message(this.staticMessage.populateByMessageListItem(message));
- message = this.message();
-
- isNew = true;
- }
- }
-
- if (message && message.uid === json.Uid) {
- this.messageError('');
-
- if (cached) {
- delete json.IsSeen;
- delete json.IsFlagged;
- delete json.IsAnswered;
- delete json.IsForwarded;
- delete json.IsReadReceipt;
- delete json.IsDeleted;
- }
-
- message.revivePropertiesFromJson(json);
- addRequestedMessage(message.folder, message.uid);
-
- if (messagesDom) {
- id = 'rl-mgs-' + message.hash.replace(/[^a-zA-Z0-9]/g, '');
-
- const textBody = elementById(id);
- if (textBody) {
- message.body = textBody;
- message.fetchDataFromDom();
- messagesDom.append(textBody);
- } else {
- let isHtml = !!json.Html;
- if (isHtml) {
- resultHtml = json.Html.toString();
- if (SettingsUserStore.removeColors()) {
- resultHtml = removeColors(resultHtml);
- }
- } else if (json.Plain) {
- resultHtml = plainToHtml(json.Plain.toString());
-
- if ((message.isPgpSigned() || message.isPgpEncrypted()) && PgpUserStore.capaOpenPGP()) {
- plain = pString(json.Plain);
-
- const isPgpEncrypted = /---BEGIN PGP MESSAGE---/.test(plain);
- if (!isPgpEncrypted) {
- pgpSigned =
- /-----BEGIN PGP SIGNED MESSAGE-----/.test(plain) && /-----BEGIN PGP SIGNATURE-----/.test(plain);
- }
-
- const pre = createElement('pre');
- if (pgpSigned && message.isPgpSigned()) {
- pre.className = 'b-plain-openpgp signed';
- pre.textContent = plain;
- } else if (isPgpEncrypted && message.isPgpEncrypted()) {
- pre.className = 'b-plain-openpgp encrypted';
- pre.textContent = plain;
- } else {
- pre.innerHTML = resultHtml;
- }
- resultHtml = pre.outerHTML;
-
- message.isPgpSigned(pgpSigned);
- message.isPgpEncrypted(isPgpEncrypted);
- } else {
- resultHtml = '' + resultHtml + ' ';
- }
- } else {
- resultHtml = '' + resultHtml + ' ';
- }
-
- body = Element.fromHTML(''
- + findEmailAndLinks(resultHtml)
- + '
');
-
- // Drop Microsoft Office style properties
- const rgbRE = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/g,
- hex = n => ('0' + parseInt(n).toString(16)).slice(-2);
- body.querySelectorAll('[style*=mso]').forEach(el =>
- el.setAttribute('style', el.style.cssText.replace(rgbRE, (m,r,g,b) => '#' + hex(r) + hex(g) + hex(b)))
- );
-
- message.body = body;
-
- message.isHtml(isHtml);
- message.hasImages(!!json.HasExternals);
-
- messagesDom.append(body);
-
- message.storeDataInDom();
-
- if (json.HasInternals) {
- message.showInternalImages();
- }
-
- if (message.hasImages() && SettingsUserStore.showImages()) {
- message.showExternalImages();
- }
-
- this.purgeMessageBodyCacheThrottle();
- }
-
- this.messageActiveDom(message.body);
-
- this.hideMessageBodies();
-
- if (body) {
- this.initOpenPgpControls(body, message);
-
- this.initBlockquoteSwitcher(body);
- }
-
- message.body.hidden = false;
- }
-
- MessageFlagsCache.initMessage(message);
- if (message.isUnseen() || message.hasUnseenSubMessage()) {
- rl.app.messageListAction(message.folder, MessageSetAction.SetSeen, [message]);
- }
-
- if (isNew) {
- message = this.message();
-
- if (
- selectedMessage &&
- message &&
- (message.folder !== selectedMessage.folder || message.uid !== selectedMessage.uid)
- ) {
- this.selectorMessageSelected(null);
- if (1 === this.list.length) {
- this.selectorMessageFocused(null);
- }
- } else if (!selectedMessage && message) {
- selectedMessage = this.list.find(
- subMessage =>
- subMessage &&
- subMessage.folder === message.folder &&
- subMessage.uid === message.uid
- );
-
- if (selectedMessage) {
- this.selectorMessageSelected(selectedMessage);
- this.selectorMessageFocused(selectedMessage);
- }
- }
- }
- }
- }
- }
-
- selectMessage(oMessage) {
- if (oMessage) {
- this.message(this.staticMessage.populateByMessageListItem(oMessage));
- this.populateMessageBody(this.message());
- } else {
- this.message(null);
- }
- }
-
- selectMessageByFolderAndUid(sFolder, sUid) {
- if (sFolder && sUid) {
- this.message(this.staticMessage.populateByMessageListItem(null));
- this.message().folder = sFolder;
- this.message().uid = sUid;
-
- this.populateMessageBody(this.message());
- } else {
- this.message(null);
- }
- }
-
- populateMessageBody(oMessage) {
- if (oMessage) {
- this.hideMessageBodies();
- this.messageLoading(true);
- Remote.message(this.onMessageResponse, oMessage.folder, oMessage.uid);
- }
- }
-
- /**
- * @param {string} sResult
- * @param {FetchJsonDefaultResponse} oData
- * @param {boolean} bCached
- */
- onMessageResponse(iError, oData, bCached) {
- if (iError) {
- if (Notification.RequestAborted !== iError) {
- this.message(null);
- this.messageError(getNotification(iError));
- }
- } else {
- this.setMessage(oData, bCached);
- }
- this.messageLoading(false);
- }
-
- /**
- * @param {Array} list
- * @returns {string}
- */
- calculateMessageListHash(list) {
- return list.map(message => '' + message.hash + '_' + message.threadsLen() + '_' + message.flagHash()).join(
- '|'
- );
- }
-
- setMessageList(data, cached) {
- const collection = MessageCollectionModel.reviveFromJson(data.Result, cached);
- if (collection) {
- let unreadCountChange = false;
-
- const iCount = collection.MessageResultCount,
- iOffset = collection.Offset,
- folder = getFolderFromCacheList(collection.Folder);
-
- if (folder && !cached) {
- folder.expires = Date.now();
-
- setFolderHash(collection.Folder, collection.FolderHash);
-
- if (null != collection.MessageCount) {
- folder.messageCountAll(collection.MessageCount);
- }
-
- if (null != collection.MessageUnseenCount) {
- if (pInt(folder.messageCountUnread()) !== pInt(collection.MessageUnseenCount)) {
- unreadCountChange = true;
- MessageFlagsCache.clearFolder(folder.fullNameRaw);
- }
-
- folder.messageCountUnread(collection.MessageUnseenCount);
- }
-
- this.initUidNextAndNewMessages(folder.fullNameRaw, collection.UidNext, collection.NewMessages);
- }
-
- this.listCount(iCount);
- this.listSearch(pString(collection.Search));
- this.listPage(Math.ceil(iOffset / SettingsUserStore.messagesPerPage() + 1));
- this.listThreadUid(pString(data.Result.ThreadUid));
-
- this.listEndFolder(collection.Folder);
- this.listEndSearch(this.listSearch());
- this.listEndThreadUid(this.listThreadUid());
- this.listEndPage(this.listPage());
-
- this.listDisableAutoSelect(true);
-
- this.list(collection);
- this.listIsNotCompleted(false);
-
- clearNewMessageCache();
-
- if (folder && (cached || unreadCountChange || SettingsUserStore.useThreads())) {
- rl.app.folderInformation(folder.fullNameRaw, collection);
- }
- } else {
- this.listCount(0);
- this.list([]);
- this.listError(getNotification(Notification.CantGetMessageList));
- }
- }
};
diff --git a/dev/Stores/User/Messagelist.js b/dev/Stores/User/Messagelist.js
new file mode 100644
index 000000000..d24848ade
--- /dev/null
+++ b/dev/Stores/User/Messagelist.js
@@ -0,0 +1,435 @@
+import { koComputable } from 'External/ko';
+
+import { Notification } from 'Common/Enums';
+import { MessageSetAction } from 'Common/EnumsUser';
+import { $htmlCL } from 'Common/Globals';
+import { arrayLength, pInt, pString } from 'Common/Utils';
+import { addObservablesTo, addComputablesTo } from 'External/ko';
+
+import {
+ getFolderInboxName,
+ addNewMessageCache,
+ setFolderUidNext,
+ getFolderFromCacheList,
+ setFolderHash,
+ MessageFlagsCache,
+ clearNewMessageCache
+} from 'Common/Cache';
+
+import { mailBox } from 'Common/Links';
+import { i18n, getNotification } from 'Common/Translator';
+
+import { EmailCollectionModel } from 'Model/EmailCollection';
+import { MessageCollectionModel } from 'Model/MessageCollection';
+
+import { AccountUserStore } from 'Stores/User/Account';
+import { FolderUserStore } from 'Stores/User/Folder';
+import { MessageUserStore } from 'Stores/User/Message';
+import { NotificationUserStore } from 'Stores/User/Notification';
+import { SettingsUserStore } from 'Stores/User/Settings';
+
+import Remote from 'Remote/User/Fetch';
+
+const
+ isChecked = item => item.checked(),
+ replaceHash = hash => {
+ rl.route.off();
+ hasher.replaceHash(hash);
+ rl.route.on();
+ }
+
+export const MessagelistUserStore = ko.observableArray().extend({ debounce: 0 });
+
+addObservablesTo(MessagelistUserStore, {
+ count: 0,
+ listSearch: '',
+ threadUid: 0,
+ page: 1,
+ pageBeforeThread: 1,
+ error: '',
+
+ endHash: '',
+ endThreadUid: 0,
+
+ loading: false,
+ // Happens when message(s) removed from list
+ isIncomplete: false,
+
+ selectedMessage: null,
+ focusedMessage: null
+});
+
+MessagelistUserStore.disableAutoSelect = ko.observable(false).extend({ falseTimeout: 500 });
+
+// Computed Observables
+
+addComputablesTo(MessagelistUserStore, {
+ isLoading: () => {
+ const value = MessagelistUserStore.loading() | MessagelistUserStore.isIncomplete();
+ $htmlCL.toggle('list-loading', value);
+ return value;
+ },
+
+ pageCount: () => Math.max(1, Math.ceil(MessagelistUserStore.count() / SettingsUserStore.messagesPerPage())),
+
+ mainSearch: {
+ read: MessagelistUserStore.listSearch,
+ write: value => hasher.setHash(
+ mailBox(FolderUserStore.currentFolderFullNameHash(), 1,
+ value.toString().trim(), MessagelistUserStore.threadUid())
+ )
+ },
+
+ listCheckedOrSelected: () => {
+ const
+ selectedMessage = MessagelistUserStore.selectedMessage(),
+ focusedMessage = MessagelistUserStore.focusedMessage(),
+ checked = MessagelistUserStore.filter(item => isChecked(item) || item === selectedMessage);
+ return checked.length ? checked : (focusedMessage ? [focusedMessage] : []);
+ },
+
+ listCheckedOrSelectedUidsWithSubMails: () => {
+ let result = [];
+ MessagelistUserStore.listCheckedOrSelected().forEach(message => {
+ result.push(message.uid);
+ if (1 < message.threadsLen()) {
+ result = result.concat(message.threads()).unique();
+ }
+ });
+ return result;
+ }
+});
+
+MessagelistUserStore.listChecked = koComputable(
+ () => MessagelistUserStore.filter(isChecked)).extend({ rateLimit: 0 }
+);
+
+MessagelistUserStore.hasCheckedMessages = koComputable(
+ () => !!MessagelistUserStore.find(isChecked)
+).extend({ rateLimit: 0 });
+
+MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
+ !!(MessagelistUserStore.selectedMessage()
+ || MessagelistUserStore.focusedMessage()
+ || MessagelistUserStore.find(item => item.checked()))
+ ).extend({ rateLimit: 50 });
+
+MessagelistUserStore.initUidNextAndNewMessages = (folder, uidNext, newMessages) => {
+ if (getFolderInboxName() === folder && uidNext) {
+ if (arrayLength(newMessages)) {
+ newMessages.forEach(item => addNewMessageCache(folder, item.Uid));
+
+ NotificationUserStore.playSoundNotification();
+
+ const len = newMessages.length;
+ if (3 < len) {
+ NotificationUserStore.displayDesktopNotification(
+ AccountUserStore.email(),
+ i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
+ COUNT: len
+ }),
+ { Url: mailBox(newMessages[0].Folder) }
+ );
+ } else {
+ newMessages.forEach(item => {
+ NotificationUserStore.displayDesktopNotification(
+ EmailCollectionModel.reviveFromJson(item.From).toString(),
+ item.Subject,
+ { Folder: item.Folder, Uid: item.Uid }
+ );
+ });
+ }
+ }
+
+ setFolderUidNext(folder, uidNext);
+ }
+}
+
+/**
+ * @param {boolean=} bDropPagePosition = false
+ * @param {boolean=} bDropCurrenFolderCache = false
+ */
+MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrenFolderCache = false) => {
+ let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage();
+
+ if (bDropCurrenFolderCache) {
+ setFolderHash(FolderUserStore.currentFolderFullName(), '');
+ }
+
+ if (bDropPagePosition) {
+ MessagelistUserStore.page(1);
+ MessagelistUserStore.pageBeforeThread(1);
+ iOffset = 0;
+
+ replaceHash(
+ mailBox(
+ FolderUserStore.currentFolderFullNameHash(),
+ MessagelistUserStore.page(),
+ MessagelistUserStore.listSearch(),
+ MessagelistUserStore.threadUid()
+ )
+ );
+ }
+
+ MessagelistUserStore.loading(true);
+ MessagelistUserStore.error('');
+ Remote.messageList(
+ (iError, oData, bCached) => {
+ if (iError) {
+ if (Notification.RequestAborted !== iError) {
+ MessagelistUserStore([]);
+ MessagelistUserStore.error(getNotification(iError));
+ }
+ } else {
+ const collection = MessageCollectionModel.reviveFromJson(oData.Result, bCached);
+ if (collection) {
+ let unreadCountChange = false;
+
+ const
+ folder = getFolderFromCacheList(collection.Folder);
+
+ if (folder && !bCached) {
+ folder.expires = Date.now();
+
+ setFolderHash(collection.Folder, collection.FolderHash);
+
+ if (null != collection.MessageCount) {
+ folder.messageCountAll(collection.MessageCount);
+ }
+
+ if (null != collection.MessageUnseenCount) {
+ if (pInt(folder.messageCountUnread()) !== pInt(collection.MessageUnseenCount)) {
+ unreadCountChange = true;
+ MessageFlagsCache.clearFolder(folder.fullName);
+ }
+
+ folder.messageCountUnread(collection.MessageUnseenCount);
+ }
+
+ MessagelistUserStore.initUidNextAndNewMessages(folder.fullName, collection.UidNext, collection.NewMessages);
+ }
+
+ MessagelistUserStore.count(collection.MessageResultCount);
+ MessagelistUserStore.listSearch(pString(collection.Search));
+ MessagelistUserStore.page(Math.ceil(collection.Offset / SettingsUserStore.messagesPerPage() + 1));
+ MessagelistUserStore.threadUid(collection.ThreadUid);
+
+ MessagelistUserStore.endHash(
+ collection.Folder +
+ '|' + collection.Search +
+ '|' + MessagelistUserStore.threadUid() +
+ '|' + MessagelistUserStore.page()
+ );
+ MessagelistUserStore.endThreadUid(collection.ThreadUid);
+ const message = MessageUserStore.message();
+ if (message && collection.Folder !== message.folder) {
+ MessageUserStore.message(null);
+ }
+
+ MessagelistUserStore.disableAutoSelect(true);
+
+ MessagelistUserStore(collection);
+ MessagelistUserStore.isIncomplete(false);
+
+ clearNewMessageCache();
+
+ if (folder && (bCached || unreadCountChange || SettingsUserStore.useThreads())) {
+ rl.app.folderInformation(folder.fullName, collection);
+ }
+ } else {
+ MessagelistUserStore.count(0);
+ MessagelistUserStore([]);
+ MessagelistUserStore.error(getNotification(Notification.CantGetMessageList));
+ }
+ }
+ MessagelistUserStore.loading(false);
+ },
+ {
+ Folder: FolderUserStore.currentFolderFullName(),
+ Offset: iOffset,
+ Limit: SettingsUserStore.messagesPerPage(),
+ Search: MessagelistUserStore.listSearch(),
+ ThreadUid: MessagelistUserStore.threadUid()
+ }
+ );
+};
+
+/**
+ * @param {string} sFolderFullName
+ * @param {number} iSetAction
+ * @param {Array=} messages = null
+ */
+MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
+ messages = messages || MessagelistUserStore.listChecked();
+
+ let folder = null,
+ alreadyUnread = 0,
+ rootUids = messages.map(oMessage => oMessage && oMessage.uid ? oMessage.uid : null)
+ .validUnique(),
+ length = rootUids.length;
+
+ if (sFolderFullName && length) {
+ switch (iSetAction) {
+ case MessageSetAction.SetSeen:
+ length = 0;
+ // fallthrough is intentionally
+ case MessageSetAction.UnsetSeen:
+ rootUids.forEach(sSubUid =>
+ alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
+ );
+
+ folder = getFolderFromCacheList(sFolderFullName);
+ if (folder) {
+ folder.messageCountUnread(folder.messageCountUnread() - alreadyUnread + length);
+ }
+
+ Remote.request('MessageSetSeen', null, {
+ Folder: sFolderFullName,
+ Uids: rootUids.join(','),
+ SetAction: iSetAction == MessageSetAction.SetSeen ? 1 : 0
+ });
+ break;
+
+ case MessageSetAction.SetFlag:
+ case MessageSetAction.UnsetFlag:
+ rootUids.forEach(sSubUid =>
+ MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
+ );
+ Remote.request('MessageSetFlagged', null, {
+ Folder: sFolderFullName,
+ Uids: rootUids.join(','),
+ SetAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
+ });
+ break;
+ // no default
+ }
+
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
+ }
+};
+
+/**
+ * @param {string} fromFolderFullName
+ * @param {Array} uidForRemove
+ * @param {string=} toFolderFullName = ''
+ * @param {boolean=} copy = false
+ */
+MessagelistUserStore.removeMessagesFromList = (
+ fromFolderFullName, uidForRemove, toFolderFullName = '', copy = false
+) => {
+ uidForRemove = uidForRemove.map(mValue => pInt(mValue));
+
+ let unseenCount = 0,
+ messageList = MessagelistUserStore,
+ currentMessage = MessageUserStore.message();
+
+ const trashFolder = FolderUserStore.trashFolder(),
+ spamFolder = FolderUserStore.spamFolder(),
+ fromFolder = getFolderFromCacheList(fromFolderFullName),
+ toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
+ messages =
+ FolderUserStore.currentFolderFullName() === fromFolderFullName
+ ? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
+ : [];
+
+ messages.forEach(item => {
+ if (item && item.isUnseen()) {
+ ++unseenCount;
+ }
+ });
+
+ if (fromFolder && !copy) {
+ fromFolder.messageCountAll(
+ 0 <= fromFolder.messageCountAll() - uidForRemove.length ? fromFolder.messageCountAll() - uidForRemove.length : 0
+ );
+
+ if (0 < unseenCount) {
+ fromFolder.messageCountUnread(
+ 0 <= fromFolder.messageCountUnread() - unseenCount ? fromFolder.messageCountUnread() - unseenCount : 0
+ );
+ }
+ }
+
+ if (toFolder) {
+ if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
+ unseenCount = 0;
+ }
+
+ toFolder.messageCountAll(toFolder.messageCountAll() + uidForRemove.length);
+ if (0 < unseenCount) {
+ toFolder.messageCountUnread(toFolder.messageCountUnread() + unseenCount);
+ }
+
+ toFolder.actionBlink(true);
+ }
+
+ if (messages.length) {
+ if (copy) {
+ messages.forEach(item => item.checked(false));
+ } else {
+ MessagelistUserStore.isIncomplete(true);
+
+ messages.forEach(item => {
+ if (currentMessage && currentMessage.hash === item.hash) {
+ currentMessage = null;
+ MessageUserStore.message(null);
+ }
+
+ item.deleted(true);
+ });
+
+ setTimeout(() => messages.forEach(item => messageList.remove(item)), 350);
+ }
+ }
+
+ if (fromFolderFullName) {
+ setFolderHash(fromFolderFullName, '');
+ }
+
+ if (toFolderFullName) {
+ setFolderHash(toFolderFullName, '');
+ }
+
+ if (MessagelistUserStore.threadUid()) {
+ if (
+ messageList.length &&
+ !!messageList.find(item => !!(item && item.deleted() && item.uid == MessagelistUserStore.threadUid()))
+ ) {
+ const message = messageList.find(item => item && !item.deleted());
+ let setHash;
+ if (!message) {
+ if (1 < MessagelistUserStore.page()) {
+ MessagelistUserStore.page(MessagelistUserStore.page() - 1);
+ setHash = 1;
+ } else {
+ MessagelistUserStore.threadUid(0);
+ replaceHash(
+ mailBox(
+ FolderUserStore.currentFolderFullNameHash(),
+ MessagelistUserStore.pageBeforeThread(),
+ MessagelistUserStore.listSearch()
+ )
+ );
+ }
+ } else if (MessagelistUserStore.threadUid() != message.uid) {
+ MessagelistUserStore.threadUid(message.uid);
+ setHash = 1;
+ }
+ if (setHash) {
+ replaceHash(
+ mailBox(
+ FolderUserStore.currentFolderFullNameHash(),
+ MessagelistUserStore.page(),
+ MessagelistUserStore.listSearch(),
+ MessagelistUserStore.threadUid()
+ )
+ );
+ }
+ }
+ }
+},
+
+MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
+ MessagelistUserStore.forEach(message => MessageFlagsCache.initMessage(message));
+ MessageFlagsCache.initMessage(MessageUserStore.message());
+};
diff --git a/dev/Stores/User/Notification.js b/dev/Stores/User/Notification.js
index 23322ceeb..d84bf4b97 100644
--- a/dev/Stores/User/Notification.js
+++ b/dev/Stores/User/Notification.js
@@ -1,22 +1,21 @@
-import ko from 'ko';
-
import { SMAudio } from 'Common/Audio';
-import { SettingsGet } from 'Common/Globals';
import * as Links from 'Common/Links';
+import { addObservablesTo } from 'External/ko';
+import { fireEvent } from 'Common/Globals';
/**
* Might not work due to the new ServiceWorkerRegistration.showNotification
*/
-const HTML5Notification = window.Notification ? Notification : null,
+const HTML5Notification = window.Notification,
HTML5NotificationStatus = () => (HTML5Notification && HTML5Notification.permission) || 'denied',
NotificationsDenied = () => 'denied' === HTML5NotificationStatus(),
NotificationsGranted = () => 'granted' === HTML5NotificationStatus(),
dispatchMessage = data => {
focus();
if (data.Folder && data.Uid) {
- dispatchEvent(new CustomEvent('mailbox.message.show', {detail:data}));
+ fireEvent('mailbox.message.show', data);
} else if (data.Url) {
- rl.route.setHash(data.Url);
+ hasher.setHash(data.Url);
}
};
@@ -37,17 +36,19 @@ if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistratio
export const NotificationUserStore = new class {
constructor() {
- this.enableSoundNotification = ko.observable(false);
+ addObservablesTo(this, {
+ enableSoundNotification: false,
- this.enableDesktopNotification = ko.observable(false)/*.extend({ notify: 'always' })*/;
+ enableDesktopNotification: false,/*.extend({ notify: 'always' })*/
- this.isDesktopNotificationDenied = ko.observable(NotificationsDenied());
+ isDesktopNotificationAllowed: !NotificationsDenied()
+ });
this.enableDesktopNotification.subscribe(value => {
DesktopNotifications = !!value;
if (value && HTML5Notification && !NotificationsGranted()) {
HTML5Notification.requestPermission(() =>
- this.isDesktopNotificationDenied(NotificationsDenied())
+ this.isDesktopNotificationAllowed(!NotificationsDenied())
);
}
});
@@ -101,9 +102,4 @@ export const NotificationUserStore = new class {
}
}
}
-
- populate() {
- this.enableSoundNotification(!!SettingsGet('SoundNotification'));
- this.enableDesktopNotification(!!SettingsGet('DesktopNotifications'));
- }
};
diff --git a/dev/Stores/User/OpenPGP.js b/dev/Stores/User/OpenPGP.js
new file mode 100644
index 000000000..5e929ec13
--- /dev/null
+++ b/dev/Stores/User/OpenPGP.js
@@ -0,0 +1,294 @@
+/**
+ * OpenPGP.js
+ */
+
+import ko from 'ko';
+
+import { arrayLength } from 'Common/Utils';
+
+import Remote from 'Remote/User/Fetch';
+
+import { showScreenPopup } from 'Knoin/Knoin';
+import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
+import { AskPopupView } from 'View/Popup/Ask';
+
+const
+ findOpenPGPKey = (keys, query/*, sign*/) =>
+ keys.find(key =>
+ key.emails.includes(query) || query == key.id || query == key.fingerprint
+ ),
+
+ askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') =>
+ await AskPopupView.password('OpenPGP.js key ' + privateKey.id + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt),
+
+ /**
+ * OpenPGP.js v5 removed the localStorage (keyring)
+ * This should be compatible with the old OpenPGP.js v2
+ */
+ publicKeysItem = 'openpgp-public-keys',
+ privateKeysItem = 'openpgp-private-keys',
+ storage = window.localStorage,
+ loadOpenPgpKeys = async itemname => {
+ let keys = [], key,
+ armoredKeys = JSON.parse(storage.getItem(itemname)),
+ i = arrayLength(armoredKeys);
+ while (i--) {
+ key = await openpgp.readKey({armoredKey:armoredKeys[i]});
+ if (!key.err) {
+ keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
+ }
+ }
+ return keys;
+ },
+ storeOpenPgpKeys = (keys, section) => {
+ let armoredKeys = keys.map(item => item.armor);
+ if (armoredKeys.length) {
+ storage.setItem(section, JSON.stringify(armoredKeys));
+ } else {
+ storage.removeItem(section);
+ }
+ };
+
+class OpenPgpKeyModel {
+ constructor(armor, key) {
+ this.key = key;
+ const aEmails = [];
+ if (key.users) {
+ key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
+ }
+ this.id = key.getKeyID().toHex().toUpperCase();
+ this.fingerprint = key.getFingerprint();
+ this.can_encrypt = !!key.getEncryptionKey();
+ this.can_sign = !!key.getSigningKey();
+ this.emails = aEmails;
+ this.armor = armor;
+ this.askDelete = ko.observable(false);
+ this.openForDeletion = ko.observable(null).askDeleteHelper();
+// key.getUserIDs()
+// key.getPrimaryUser()
+ }
+
+ view() {
+ showScreenPopup(OpenPgpKeyPopupView, [this]);
+ }
+
+ remove() {
+ if (this.askDelete()) {
+ if (this.key.isPrivate()) {
+ OpenPGPUserStore.privateKeys.remove(this);
+ storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
+ } else {
+ OpenPGPUserStore.publicKeys.remove(this);
+ storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem);
+ }
+ }
+ }
+/*
+ toJSON() {
+ return this.armor;
+ }
+*/
+}
+
+export const OpenPGPUserStore = new class {
+ constructor() {
+ this.publicKeys = ko.observableArray();
+ this.privateKeys = ko.observableArray();
+ }
+
+ loadKeyrings() {
+ loadOpenPgpKeys(publicKeysItem).then(keys => {
+ this.publicKeys(keys || []);
+ console.log('openpgp.js public keys loaded');
+ });
+ loadOpenPgpKeys(privateKeysItem).then(keys => {
+ this.privateKeys(keys || [])
+ console.log('openpgp.js private keys loaded');
+ });
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ isSupported() {
+ return !!window.openpgp;
+ }
+
+ importKey(armoredKey) {
+ openpgp.readKey({armoredKey:armoredKey}).then(key => {
+ if (!key.err) {
+ if (key.isPrivate()) {
+ this.privateKeys.push(new OpenPgpKeyModel(armoredKey, key));
+ storeOpenPgpKeys(this.privateKeys, privateKeysItem);
+ } else {
+ this.publicKeys.push(new OpenPgpKeyModel(armoredKey, key));
+ storeOpenPgpKeys(this.publicKeys, publicKeysItem);
+ }
+ }
+ });
+ }
+
+ /**
+ keyPair.privateKey
+ keyPair.publicKey
+ keyPair.revocationCertificate
+ */
+ storeKeyPair(keyPair) {
+ openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
+ this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
+ storeOpenPgpKeys(this.publicKeys, publicKeysItem);
+ });
+ openpgp.readKey({armoredKey:keyPair.privateKey}).then(key => {
+ this.privateKeys.push(new OpenPgpKeyModel(keyPair.privateKey, key));
+ storeOpenPgpKeys(this.privateKeys, privateKeysItem);
+ });
+ }
+
+ /**
+ * Checks if verifying/encrypting a message is possible with given email addresses.
+ */
+ hasPublicKeyForEmails(recipients) {
+ const count = recipients.length,
+ length = count ? recipients.filter(email =>
+ this.publicKeys().find(key => key.emails.includes(email))
+ ).length : 0;
+ return length && length === count;
+ }
+
+ getPrivateKeyFor(query/*, sign*/) {
+ return findOpenPGPKey(this.privateKeys, query/*, sign*/);
+ }
+
+ /**
+ * https://docs.openpgpjs.org/#encrypt-and-decrypt-string-data-with-pgp-keys
+ */
+ async decrypt(armoredText, sender)
+ {
+ const message = await openpgp.readMessage({ armoredMessage: armoredText }),
+ privateKeys = this.privateKeys(),
+ msgEncryptionKeyIDs = message.getEncryptionKeyIDs().map(key => key.bytes);
+ // Find private key that can decrypt message
+ let i = privateKeys.length, privateKey;
+ while (i--) {
+ if ((await privateKeys[i].key.getDecryptionKeys()).find(
+ key => msgEncryptionKeyIDs.includes(key.getKeyID().bytes)
+ )) {
+ privateKey = privateKeys[i];
+ break;
+ }
+ }
+ if (privateKey) try {
+ const passphrase = await askPassphrase(privateKey, 'BUTTON_DECRYPT');
+
+ if (null !== passphrase) {
+ const
+ publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/),
+ decryptedKey = await openpgp.decryptKey({
+ privateKey: privateKey.key,
+ passphrase
+ });
+
+ return await openpgp.decrypt({
+ message,
+ verificationKeys: publicKey && publicKey.key,
+// expectSigned: true,
+// signature: '', // Detached signature
+ decryptionKeys: decryptedKey
+ });
+ }
+ } catch (err) {
+ alert(err);
+ console.error(err);
+ }
+ }
+
+ /**
+ * https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
+ */
+ async verify(message) {
+ const data = message.pgpSigned(), // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
+ publicKey = this.publicKeys().find(key => key.emails.includes(message.from[0].email));
+ if (data && publicKey) {
+ data.Folder = message.folder;
+ data.Uid = message.uid;
+ data.GnuPG = 0;
+ let response;
+ if (data.SigPartId) {
+ response = await Remote.post('MessagePgpVerify', null, data);
+ } else if (data.BodyPart) {
+ response = { Result: { text: data.BodyPart.raw, signature: data.SigPart.body } };
+ } else {
+ response = { Result: { text: message.plain(), signature: null } };
+ }
+ if (response) {
+ const signature = response.Result.signature
+ ? await openpgp.readSignature({ armoredSignature: response.Result.signature })
+ : null;
+ const signedMessage = signature
+ ? await openpgp.createMessage({ text: response.Result.text })
+ : await openpgp.readCleartextMessage({ cleartextMessage: response.Result.text });
+// (signature||signedMessage).getSigningKeyIDs();
+ let result = await openpgp.verify({
+ message: signedMessage,
+ verificationKeys: publicKey.key,
+// expectSigned: true, // !!detachedSignature
+ signature: signature
+ });
+ return {
+ fingerprint: publicKey.fingerprint,
+ success: result && !!result.signatures.length
+ };
+ }
+ }
+ }
+
+ /**
+ * https://docs.openpgpjs.org/global.html#sign
+ */
+ async sign(text, privateKey, detached) {
+ const passphrase = await askPassphrase(privateKey);
+ if (null !== passphrase) {
+ privateKey = await openpgp.decryptKey({
+ privateKey: privateKey.key,
+ passphrase
+ });
+ const message = detached
+ ? await openpgp.createMessage({ text: text })
+ : await openpgp.createCleartextMessage({ text: text });
+ return await openpgp.sign({
+ message: message,
+ signingKeys: privateKey,
+ detached: !!detached
+ });
+ }
+ throw 'Sign cancelled';
+ }
+
+ /**
+ * https://docs.openpgpjs.org/global.html#encrypt
+ */
+ async encrypt(text, recipients, signPrivateKey) {
+ const count = recipients.length;
+ recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
+ if (count === recipients.length) {
+ if (signPrivateKey) {
+ const passphrase = await askPassphrase(signPrivateKey);
+ if (null === passphrase) {
+ return;
+ }
+ signPrivateKey = await openpgp.decryptKey({
+ privateKey: signPrivateKey.key,
+ passphrase
+ });
+ }
+ return await openpgp.encrypt({
+ message: await openpgp.createMessage({ text: text }),
+ encryptionKeys: recipients.map(pkey => pkey.key),
+ signingKeys: signPrivateKey
+// signature
+ });
+ }
+ throw 'Encrypt failed';
+ }
+
+};
diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js
index 952295536..e7f3bcbbb 100644
--- a/dev/Stores/User/Pgp.js
+++ b/dev/Stores/User/Pgp.js
@@ -1,368 +1,235 @@
-import ko from 'ko';
+import { SettingsCapa, SettingsGet } from 'Common/Globals';
+import { staticLink } from 'Common/Links';
-import { i18n } from 'Common/Translator';
-import { isArray, isNonEmptyArray, pString } from 'Common/Utils';
-import { createElement } from 'Common/Globals';
+//import { showScreenPopup } from 'Knoin/Knoin';
-import { AccountUserStore } from 'Stores/User/Account';
+//import { EmailModel } from 'Model/Email';
+//import { OpenPgpKeyModel } from 'Model/OpenPgpKey';
-import { showScreenPopup } from 'Knoin/Knoin';
+import { GnuPGUserStore } from 'Stores/User/GnuPG';
+import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
-import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp';
+export const
+ BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
+// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
+// BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----',
-function controlsHelper(dom, verControl, success, title, text)
-{
- dom.classList.toggle('error', !success);
- dom.classList.toggle('success', success);
- verControl.classList.toggle('error', !success);
- verControl.classList.toggle('success', success);
- dom.title = verControl.title = title;
-
- if (undefined !== text) {
- dom.textContent = text.trim();
- }
-}
-
-function domControlEncryptedClickHelper(store, dom, armoredMessage, recipients) {
- return function() {
- let message = null;
-
- if (this.classList.contains('success')) {
- return false;
+ PgpUserStore = new class {
+ constructor() {
+ // https://mailvelope.github.io/mailvelope/Keyring.html
+ this.mailvelopeKeyring = null;
}
- try {
- message = store.openpgp.message.readArmored(armoredMessage);
- } catch (e) {
- console.log(e);
+ init() {
+ if (SettingsCapa('OpenPGP') && window.crypto && crypto.getRandomValues) {
+ rl.loadScript(staticLink('js/min/openpgp.min.js'))
+ .then(() => this.loadKeyrings())
+ .catch(e => {
+ this.loadKeyrings();
+ console.error(e);
+ });
+ } else {
+ this.loadKeyrings();
+ }
}
- if (message && message.getText && message.verify && message.decrypt) {
- store.decryptMessage(
- message,
- recipients,
- (validPrivateKey, decryptedMessage, validPublicKey, signingKeyIds) => {
- if (decryptedMessage) {
- if (validPublicKey) {
- controlsHelper(
- dom,
- this,
- true,
- i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
- USER: validPublicKey.user + ' (' + validPublicKey.id + ')'
- }),
- decryptedMessage.getText()
- );
- } else if (validPrivateKey) {
- const keyIds = isNonEmptyArray(signingKeyIds) ? signingKeyIds : null,
- additional = keyIds
- ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
- : '';
-
- controlsHelper(
- dom,
- this,
- false,
- i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : ''),
- decryptedMessage.getText()
- );
- } else {
- controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
- }
+ loadKeyrings(identifier) {
+ identifier = identifier || SettingsGet('Email');
+ if (window.mailvelope) {
+ const fn = keyring => {
+ this.mailvelopeKeyring = keyring;
+ console.log('mailvelope ready');
+ };
+ mailvelope.getKeyring().then(fn, err => {
+ if (identifier) {
+ // attempt to create a new keyring for this app/user
+ mailvelope.createKeyring(identifier).then(fn, err => console.error(err));
} else {
- controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
+ console.error(err);
}
+ });
+ addEventListener('mailvelope-disconnect', event => {
+ alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
+ }, false);
+ } else {
+ addEventListener('mailvelope', () => this.loadKeyrings(identifier));
+ }
+
+ if (OpenPGPUserStore.isSupported()) {
+ OpenPGPUserStore.loadKeyrings();
+ }
+
+ if (SettingsCapa('GnuPG')) {
+ GnuPGUserStore.loadKeyrings();
+ }
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ isSupported() {
+ return !!(OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported() || window.mailvelope);
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ isEncrypted(text) {
+ return 0 === text.trim().indexOf(BEGIN_PGP_MESSAGE);
+ }
+
+ async mailvelopeHasPublicKeyForEmails(recipients) {
+ const
+ keyring = this.mailvelopeKeyring,
+ mailvelope = keyring && await keyring.validKeyForAddress(recipients)
+ /*.then(LookupResult => Object.entries(LookupResult))*/,
+ entries = mailvelope && Object.entries(mailvelope);
+ return entries && entries.filter(value => value[1]).length === recipients.length;
+ }
+
+ /**
+ * Checks if verifying/encrypting a message is possible with given email addresses.
+ * Returns the first library that can.
+ */
+ async hasPublicKeyForEmails(recipients) {
+ const count = recipients.length;
+ if (count) {
+ if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
+ return 'gnupg';
}
- );
-
- return false;
- }
-
- controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
- return false;
- };
-}
-
-function domControlSignedClickHelper(store, dom, armoredMessage) {
- return function() {
- let message = null;
-
- if (this.classList.contains('success') || this.classList.contains('error')) {
- return false;
- }
-
- try {
- message = store.openpgp.cleartext.readArmored(armoredMessage);
- } catch (e) {
- console.log(e);
- }
-
- if (message && message.getText && message.verify) {
- store.verifyMessage(message, (validKey, signingKeyIds) => {
- if (validKey) {
- controlsHelper(
- dom,
- this,
- true,
- i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
- USER: validKey.user + ' (' + validKey.id + ')'
- }),
- message.getText()
- );
- } else {
- const keyIds = isNonEmptyArray(signingKeyIds) ? signingKeyIds : null,
- additional = keyIds
- ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
- : '';
-
- controlsHelper(
- dom,
- this,
- false,
- i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : '')
- );
+ if (OpenPGPUserStore.hasPublicKeyForEmails(recipients)) {
+ return 'openpgp';
}
- });
-
+ }
return false;
}
- controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
- return false;
- };
-}
-
-export const PgpUserStore = new class {
- constructor() {
- this.capaOpenPGP = ko.observable(false);
-
- this.openpgp = null;
-
- this.openpgpkeys = ko.observableArray();
- this.openpgpKeyring = null;
-
- this.openpgpkeysPublic = ko.computed(() => this.openpgpkeys.filter(item => item && !item.isPrivate));
- this.openpgpkeysPrivate = ko.computed(() => this.openpgpkeys.filter(item => item && item.isPrivate));
- }
-
- /**
- * @returns {boolean}
- */
- isSupported() {
- return !!this.openpgp;
- }
-
- findKeyByHex(keys, hash) {
- return keys.find(item => hash && item && (hash === item.id || item.ids.includes(hash)));
- }
-
- findPublicKeyByHex(hash) {
- return this.findKeyByHex(this.openpgpkeysPublic(), hash);
- }
-
- findPrivateKeyByHex(hash) {
- return this.findKeyByHex(this.openpgpkeysPrivate(), hash);
- }
-
- findPublicKeysByEmail(email) {
- return this.openpgpkeysPublic().map(item => {
- const key = item && item.emails.includes(email) ? item : null;
- return key ? key.getNativeKeys() : [null];
- }).flat().filter(v => v);
- }
-
- findPublicKeysBySigningKeyIds(signingKeyIds) {
- return signingKeyIds.map(id => {
- const key = id && id.toHex ? this.findPublicKeyByHex(id.toHex()) : null;
- return key ? key.getNativeKeys() : [null];
- }).flat().filter(v => v);
- }
-
- findPrivateKeysByEncryptionKeyIds(encryptionKeyIds, recipients, returnWrapKeys) {
- let result = isArray(encryptionKeyIds)
- ? encryptionKeyIds.map(id => {
- const key = id && id.toHex ? this.findPrivateKeyByHex(id.toHex()) : null;
- return key ? (returnWrapKeys ? [key] : key.getNativeKeys()) : [null];
- }).flat().filter(v => v)
- : [];
-
- if (!result.length && isNonEmptyArray(recipients)) {
- result = recipients.map(sEmail => {
- const keys = sEmail ? this.findAllPrivateKeysByEmailNotNative(sEmail) : null;
- return keys
- ? returnWrapKeys
- ? keys
- : keys.map(key => key.getNativeKeys()).flat()
- : [null];
- }).flat().validUnique(key => key.id);
+ async getMailvelopePrivateKeyFor(email/*, sign*/) {
+ let keyring = this.mailvelopeKeyring;
+ if (keyring && await keyring.hasPrivateKey({email:email})) {
+ return ['mailvelope', email];
+ }
+ return false;
}
- return result;
- }
+ /**
+ * Checks if signing a message is possible with given email address.
+ * Returns the first library that can.
+ */
+ async getKeyForSigning(email) {
+ let key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
+ if (key) {
+ return ['openpgp', key];
+ }
- /**
- * @param {string} email
- * @returns {?}
- */
- findPublicKeyByEmailNotNative(email) {
- return this.openpgpkeysPublic().find(item => item && item.emails.includes(email)) || null;
- }
+ key = GnuPGUserStore.getPrivateKeyFor(email, 1);
+ if (key) {
+ return ['gnupg', key];
+ }
- /**
- * @param {string} email
- * @returns {?}
- */
- findPrivateKeyByEmailNotNative(email) {
- return this.openpgpkeysPrivate().find(item => item && item.emails.includes(email)) || null;
- }
+ // return await this.getMailvelopePrivateKeyFor(email, 1);
+ }
- /**
- * @param {string} email
- * @returns {?}
- */
- findAllPublicKeysByEmailNotNative(email) {
- return this.openpgpkeysPublic().filter(item => item && item.emails.includes(email)) || null;
- }
+ async decrypt(message) {
+ const sender = message.from[0].email,
+ armoredText = message.plain();
- /**
- * @param {string} email
- * @returns {?}
- */
- findAllPrivateKeysByEmailNotNative(email) {
- return this.openpgpkeysPrivate().filter(item => item && item.emails.includes(email)) || null;
- }
+ if (!this.isEncrypted(armoredText)) {
+ return;
+ }
- /**
- * @param {string} email
- * @param {string=} password
- * @returns {?}
- */
- findPrivateKeyByEmail(email, password) {
- let privateKey = null;
- const key = this.openpgpkeysPrivate().find(item => item && item.emails.includes(email));
+ // Try OpenPGP.js
+ let result = await OpenPGPUserStore.decrypt(armoredText, sender);
+ if (result) {
+ return result;
+ }
- if (key) {
+ // Try Mailvelope (does not support inline images)
try {
- privateKey = key.getNativeKeys()[0] || null;
- if (privateKey) {
- privateKey.decrypt(pString(password));
- }
- } catch (e) {
- privateKey = null;
- }
- }
-
- return privateKey;
- }
-
- /**
- * @param {string=} password
- * @returns {?}
- */
- findSelfPrivateKey(password) {
- return this.findPrivateKeyByEmail(AccountUserStore.email(), password);
- }
-
- decryptMessage(message, recipients, fCallback) {
- if (message && message.getEncryptionKeyIds) {
- const privateKeys = this.findPrivateKeysByEncryptionKeyIds(message.getEncryptionKeyIds(), recipients, true);
- if (privateKeys && privateKeys.length) {
- showScreenPopup(MessageOpenPgpPopupView, [
- (decryptedKey) => {
- if (decryptedKey) {
- message.decrypt(decryptedKey).then(
- (decryptedMessage) => {
- let privateKey = null;
- if (decryptedMessage) {
- privateKey = this.findPrivateKeyByHex(decryptedKey.primaryKey.keyid.toHex());
- if (privateKey) {
- this.verifyMessage(decryptedMessage, (oValidKey, aSigningKeyIds) => {
- fCallback(privateKey, decryptedMessage, oValidKey || null, aSigningKeyIds || null);
- });
- } else {
- fCallback(privateKey, decryptedMessage);
- }
- } else {
- fCallback(privateKey, decryptedMessage);
- }
- },
- () => {
- fCallback(null, null);
- }
- );
- } else {
- fCallback(null, null);
+ let key = await this.getMailvelopePrivateKeyFor(message.to[0].email);
+ if (key) {
+ /**
+ * https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
+ * Creates an iframe to display an encrypted form
+ */
+ // mailvelope.createEncryptedFormContainer('#mailvelope-form');
+ /**
+ * https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
+ * Creates an iframe to display the decrypted content of the encrypted mail.
+ */
+ const body = message.body;
+ body.textContent = '';
+ result = await mailvelope.createDisplayContainer(
+ '#'+body.id,
+ armoredText,
+ this.mailvelopeKeyring,
+ {
+ senderAddress: sender
}
- },
- privateKeys
- ]);
-
- return false;
- }
- }
-
- fCallback(null, null);
-
- return false;
- }
-
- verifyMessage(message, fCallback) {
- if (message && message.getSigningKeyIds) {
- const signingKeyIds = message.getSigningKeyIds();
- if (signingKeyIds && signingKeyIds.length) {
- const publicKeys = this.findPublicKeysBySigningKeyIds(signingKeyIds);
- if (publicKeys && publicKeys.length) {
- try {
- const result = message.verify(publicKeys),
- valid = (isArray(result) ? result : []).find(item => item && item.valid && item.keyid);
-
- if (valid && valid.keyid && valid.keyid && valid.keyid.toHex) {
- fCallback(this.findPublicKeyByHex(valid.keyid.toHex()));
+ );
+ if (result) {
+ if (result.error && result.error.message) {
+ if ('PWD_DIALOG_CANCEL' !== result.error.code) {
+ alert(result.error.code + ': ' + result.error.message);
+ }
+ } else {
+ body.classList.add('mailvelope');
return true;
}
- } catch (e) {
- console.log(e);
}
}
-
- fCallback(null, signingKeyIds);
- return false;
+ } catch (err) {
+ console.error(err);
}
+
+ // Now try GnuPG
+ return GnuPGUserStore.decrypt(message);
}
- fCallback(null);
- return false;
- }
-
- /**
- * @param {*} dom
- * @param {MessageModel} rainLoopMessage
- */
- initMessageBodyControls(dom, rainLoopMessage) {
- const cl = dom && dom.classList;
- if (!cl.has('inited')) {
- cl.add('inited');
-
- const encrypted = cl.has('encrypted'),
- signed = cl.has('signed'),
- recipients = rainLoopMessage ? rainLoopMessage.getEmails(['from', 'to', 'cc']) : [];
-
- let verControl = null;
-
- if (encrypted || signed) {
- const domText = dom.textContent;
-
- verControl = Element.fromHTML('🔒
');
- if (encrypted) {
- verControl.title = i18n('MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC');
- verControl.addEventHandler('click', domControlEncryptedClickHelper(this, dom, domText, recipients));
- } else {
- verControl.title = i18n('MESSAGE/PGP_SIGNED_MESSAGE_DESC');
- verControl.addEventHandler('click', domControlSignedClickHelper(this, dom, domText));
+ async verify(message) {
+ const signed = message.pgpSigned();
+ if (signed) {
+ const sender = message.from[0].email,
+ gnupg = GnuPGUserStore.hasPublicKeyForEmails([sender]),
+ openpgp = OpenPGPUserStore.hasPublicKeyForEmails([sender]);
+ // Detached signature use GnuPG first, else we must download whole message
+ if (gnupg && signed.SigPartId) {
+ return GnuPGUserStore.verify(message);
}
-
- dom.before(verControl, createElement('div'));
+ if (openpgp) {
+ return OpenPGPUserStore.verify(message);
+ }
+ if (gnupg) {
+ return GnuPGUserStore.verify(message);
+ }
+ // Mailvelope can't
+ // https://github.com/mailvelope/mailvelope/issues/434
}
}
- }
-};
+
+ /**
+ * Returns headers that should be added to an outgoing email.
+ * So far this is only the autocrypt header.
+ */
+ /*
+ this.mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers)
+ this.mailvelopeKeyring.addSyncHandler(syncHandlerObj)
+ this.mailvelopeKeyring.createKeyBackupContainer(selector, options)
+ this.mailvelopeKeyring.createKeyGenContainer(selector, {
+ // userIds: [],
+ keySize: 4096
+ })
+
+ this.mailvelopeKeyring.exportOwnPublicKey(emailAddr).then()
+ this.mailvelopeKeyring.importPublicKey(armored)
+
+ // https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
+ this.mailvelopeKeyring.addSyncHandler({
+ uploadSync
+ downloadSync
+ backup
+ restore
+ });
+ */
+
+ };
diff --git a/dev/Stores/User/Quota.js b/dev/Stores/User/Quota.js
deleted file mode 100644
index c5ba326ce..000000000
--- a/dev/Stores/User/Quota.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import ko from 'ko';
-
-export const QuotaUserStore = new class {
- constructor() {
- this.quota = ko.observable(0);
- this.usage = ko.observable(0);
-
- this.percentage = ko.computed(() => {
- const quota = this.quota(),
- usage = this.usage();
-
- return 0 < quota ? Math.ceil((usage / quota) * 100) : 0;
- });
- }
-
- /**
- * @param {number} quota
- * @param {number} usage
- */
- populateData(quota, usage) {
- this.quota(quota * 1024);
- this.usage(usage * 1024);
- }
-};
diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js
index d3b20fd73..50597f36b 100644
--- a/dev/Stores/User/Settings.js
+++ b/dev/Stores/User/Settings.js
@@ -1,18 +1,21 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
-import { MESSAGES_PER_PAGE_VALUES } from 'Common/Consts';
import { Layout, EditorDefaultType } from 'Common/EnumsUser';
-import { pInt, addObservablesTo } from 'Common/Utils';
-import { $htmlCL, SettingsGet } from 'Common/Globals';
+import { pInt } from 'Common/Utils';
+import { addObservablesTo } from 'External/ko';
+import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
export const SettingsUserStore = new class {
constructor() {
- this.layout = ko
- .observable(pInt(SettingsGet('Layout')))
+ const self = this;
+
+ self.layout = ko
+ .observable(1)
.extend({ limitedList: Object.values(Layout) });
- this.editorDefaultType = ko.observable(SettingsGet('EditorDefaultType')).extend({
+ self.editorDefaultType = ko.observable('Html').extend({
limitedList: [
EditorDefaultType.Html,
EditorDefaultType.Plain,
@@ -21,42 +24,66 @@ export const SettingsUserStore = new class {
]
});
- this.messagesPerPage = ko.observable(SettingsGet('MPP')).extend({ limitedList: MESSAGES_PER_PAGE_VALUES });
+ self.messagesPerPage = ko.observable(25).extend({ debounce: 999 });
- addObservablesTo(this, {
- showImages: !!SettingsGet('ShowImages'),
- removeColors: !!SettingsGet('RemoveColors'),
- useCheckboxesInList: !!(ThemeStore.isMobile() || SettingsGet('UseCheckboxesInList')),
- allowDraftAutosave: !!SettingsGet('AllowDraftAutosave'),
- useThreads: !!SettingsGet('UseThreads'),
- replySameFolder: !!SettingsGet('ReplySameFolder'),
- hideUnsubscribed: !!SettingsGet('HideUnsubscribed'),
+ self.messageReadDelay = ko.observable(5).extend({ debounce: 999 });
- autoLogout: pInt(SettingsGet('AutoLogout'))
+ addObservablesTo(self, {
+ viewHTML: 1,
+ showImages: 0,
+ removeColors: 0,
+ useCheckboxesInList: 1,
+ allowDraftAutosave: 1,
+ useThreads: 0,
+ replySameFolder: 0,
+ hideUnsubscribed: 0,
+ autoLogout: 0
});
- this.usePreviewPane = ko.computed(() => Layout.NoPreview !== this.layout() && !ThemeStore.isMobile());
+ self.init();
+
+ self.usePreviewPane = koComputable(() => Layout.NoPreview !== self.layout() && !ThemeStore.isMobile());
const toggleLayout = () => {
- const value = ThemeStore.isMobile() ? Layout.NoPreview : this.layout();
+ const value = ThemeStore.isMobile() ? Layout.NoPreview : self.layout();
$htmlCL.toggle('rl-no-preview-pane', Layout.NoPreview === value);
$htmlCL.toggle('rl-side-preview-pane', Layout.SidePreview === value);
$htmlCL.toggle('rl-bottom-preview-pane', Layout.BottomPreview === value);
- dispatchEvent(new CustomEvent('rl-layout', {detail:value}));
+ fireEvent('rl-layout', value);
};
- this.layout.subscribe(toggleLayout);
+ self.layout.subscribe(toggleLayout);
ThemeStore.isMobile.subscribe(toggleLayout);
toggleLayout();
let iAutoLogoutTimer;
- this.delayLogout = (() => {
+ self.delayLogout = (() => {
clearTimeout(iAutoLogoutTimer);
- if (0 < this.autoLogout() && !SettingsGet('AccountSignMe')) {
+ if (0 < self.autoLogout() && !SettingsGet('AccountSignMe')) {
iAutoLogoutTimer = setTimeout(
rl.app.logout,
- this.autoLogout() * 60000
+ self.autoLogout() * 60000
);
}
}).throttle(5000);
}
+
+ init() {
+ const self = this;
+ self.editorDefaultType(SettingsGet('EditorDefaultType'));
+
+ self.layout(pInt(SettingsGet('Layout')));
+ self.messagesPerPage(pInt(SettingsGet('MessagesPerPage')));
+ self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
+ self.autoLogout(pInt(SettingsGet('AutoLogout')));
+
+ self.viewHTML(SettingsGet('ViewHTML'));
+ self.showImages(SettingsGet('ShowImages'));
+ self.removeColors(SettingsGet('RemoveColors'));
+ self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
+ self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
+ self.useThreads(SettingsGet('UseThreads'));
+ self.replySameFolder(SettingsGet('ReplySameFolder'));
+
+ self.hideUnsubscribed(SettingsGet('HideUnsubscribed'));
+ }
};
diff --git a/dev/Stores/User/Sieve.js b/dev/Stores/User/Sieve.js
index 0c9d50d71..9272c2ebf 100644
--- a/dev/Stores/User/Sieve.js
+++ b/dev/Stores/User/Sieve.js
@@ -1,8 +1,8 @@
-import ko from 'ko';
+import { koArrayWithDestroy } from 'External/ko';
export const SieveUserStore = {
// capabilities
capa: ko.observableArray(),
// Sieve scripts SieveScriptModel
- scripts: ko.observableArray()
+ scripts: koArrayWithDestroy()
}
diff --git a/dev/Stores/User/Template.js b/dev/Stores/User/Template.js
deleted file mode 100644
index 13204eb74..000000000
--- a/dev/Stores/User/Template.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import ko from 'ko';
-
-// import Remote from 'Remote/User/Fetch';
-
-export const TemplateUserStore = new class {
- constructor() {
- this.templates = ko.observableArray();
- this.templates.loading = ko.observable(false).extend({ debounce: 100 });
-
- this.templatesNames = ko.observableArray().extend({ debounce: 1000 });
- this.templatesNames.skipFirst = true;
-
- this.templates.subscribe((list) => {
- this.templatesNames(list.map(item => (item ? item.name : null)).filter(v => v));
- });
-
- // this.templatesNames.subscribe((aList) => {
- // if (this.templatesNames.skipFirst)
- // {
- // this.templatesNames.skipFirst = false;
- // }
- // else if (aList && aList.length)
- // {
- // Remote.templatesSortOrder(null, aList);
- // }
- // });
- }
-};
diff --git a/dev/Styles/@Admin.less b/dev/Styles/@Admin.less
index 65c42afd6..85ebb53dc 100644
--- a/dev/Styles/@Admin.less
+++ b/dev/Styles/@Admin.less
@@ -6,39 +6,31 @@
@import "../../vendors/bootstrap/less/grid.less";
@import "../../vendors/bootstrap/less/type.less";
-@import "../../vendors/bootstrap/less/code.less";
@import "../../vendors/bootstrap/less/forms.less";
@import "../../vendors/bootstrap/less/tables.less";
-@import "../../vendors/bootstrap/less/dropdowns.less";
@import "../../vendors/bootstrap/less/close.less";
@import "../../vendors/bootstrap/less/buttons.less";
@import "../../vendors/bootstrap/less/button-groups.less";
@import "../../vendors/bootstrap/less/alerts.less";
-@import "../../vendors/bootstrap/less/modals.less";
-@import "../../vendors/bootstrap/less/utilities.less";
-@import "../../vendors/bootstrap/less/wells.less";
@import "_FontasticToBoot.less";
@import "_BootstrapFix.less";
@import "Ui.less";
@import "Main.less";
-@import "LayoutAdmin.less";
@import "Components.less";
@import "Login.less";
@import "Ask.less";
@import "Languages.less";
-@import "Admin.less";
-@import "AdminGeneral.less";
-@import "AdminBranding.less";
-@import "AdminDomains.less";
-@import "AdminDomain.less";
-@import "AdminPackages.less";
-@import "AdminPlugins.less";
-@import "AdminPlugin.less";
-@import "AdminAbout.less";
-
-@import "Animations.less";
+@import "Admin/Layout.less";
+@import "Admin/General.less";
+@import "Admin/Branding.less";
+@import "Admin/Config.less";
+@import "Admin/Domains.less";
+@import "Admin/Domain.less";
+@import "Admin/Packages.less";
+@import "Admin/Plugin.less";
+@import "Admin/About.less";
@import "_End.less";
diff --git a/dev/Styles/@Boot.css b/dev/Styles/@Boot.css
index d5a9aaf1c..87cf109ef 100644
--- a/dev/Styles/@Boot.css
+++ b/dev/Styles/@Boot.css
@@ -27,34 +27,7 @@ body {
width: 100%;
}
-.e-spinner {
- margin: 5px auto 0;
- width: 100px;
- text-align: center;
-}
-
-.e-spinner .e-bounce {
- animation: bouncedelay 1.4s infinite ease-in-out;
- background-color: var(--loading-color, #000);
- border-radius: 100%;
- box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
- display: inline-block;
- height: 15px;
- margin: 0 5px;
- width: 15px;
- /* Prevent first frame from flickering when animation starts */
- animation-fill-mode: both;
-}
-
-.e-spinner .bounce1 {
- animation-delay: -0.32s;
-}
-
-.e-spinner .bounce2 {
- animation-delay: -0.16s;
-}
-
-@keyframes bouncedelay {
- 0%, 80%, 100% { transform: scale(0.0); }
- 40% { transform: scale(1.0); }
+#rl-loading .icon-spinner {
+ height: 1em;
+ width: 1em;
}
diff --git a/dev/Styles/@Main.less b/dev/Styles/@Main.less
index b46372da1..0c1b15a8b 100644
--- a/dev/Styles/@Main.less
+++ b/dev/Styles/@Main.less
@@ -6,63 +6,52 @@
@import "../../vendors/bootstrap/less/grid.less";
@import "../../vendors/bootstrap/less/type.less";
-@import "../../vendors/bootstrap/less/code.less";
@import "../../vendors/bootstrap/less/forms.less";
@import "../../vendors/bootstrap/less/tables.less";
-@import "../../vendors/bootstrap/less/dropdowns.less";
@import "../../vendors/bootstrap/less/close.less";
@import "../../vendors/bootstrap/less/buttons.less";
@import "../../vendors/bootstrap/less/button-groups.less";
@import "../../vendors/bootstrap/less/alerts.less";
-@import "../../vendors/bootstrap/less/modals.less";
-@import "../../vendors/bootstrap/less/utilities.less";
+@import "../../vendors/bootstrap/less/code.less";
+@import "../../vendors/bootstrap/less/dropdowns.less";
@import "../../vendors/bootstrap/less/navs.less";
-@import "../../vendors/bootstrap/less/labels-badges.less";
+@import "../../vendors/bootstrap/less/utilities.less";
@import "_FontasticToBoot.less";
@import "_BootstrapFix.less";
@import "Ui.less";
@import "Main.less";
-@import "Layout.less";
@import "Components.less";
@import "Login.less";
@import "Ask.less";
@import "Languages.less";
-@import "SystemDropDown.less";
-@import "Shortcuts.less";
-@import "FolderList.less";
-/*
-@import "FolderClear.less";
-@import "FolderCreate.less";
-@import "FolderSystem.less";
-@import "Account.less";
-*/
-@import "Filter.less";
-@import "Template.less";
-@import "OpenPgpKey.less";
-@import "Identity.less";
-@import "AdvancedSearch.less";
-@import "Attachmnets.less";
-@import "MessageList.less";
-@import "MessageView.less";
-@import "Contacts.less";
-@import "Compose.less";
-@import "EmailAddresses.less";
+@import "User/Layout.less";
+@import "User/SystemDropDown.less";
+@import "User/Shortcuts.less";
+@import "User/FolderList.less";
+@import "User/Filter.less";
+@import "User/OpenPgpKey.less";
+@import "User/Identity.less";
+@import "User/AdvancedSearch.less";
+@import "User/Attachmnets.less";
+@import "User/MessageList.less";
+@import "User/MessageView.less";
+@import "User/Contacts.less";
+@import "User/Compose.less";
+@import "User/EmailAddresses.less";
-@import "Settings.less";
-@import "SettingsGeneral.less";
-@import "SettingsAccounts.less";
-@import "SettingsTemplates.less";
-@import "SettingsOpenPGP.less";
-@import "SettingsFolders.less";
-@import "SettingsThemes.less";
-@import "SettingsFilters.less";
+@import "User/Settings.less";
+@import "User/SettingsGeneral.less";
+@import "User/SettingsAccounts.less";
+@import "User/SettingsOpenPGP.less";
+@import "User/SettingsFolders.less";
+@import "User/SettingsThemes.less";
+@import "User/SettingsFilters.less";
-@import "SquireUI.less";
+@import "User/SquireUI.less";
-@import "AnimationsUser.less";
-@import "Animations.less";
+@import "User/Animations.less";
@import "_End.less";
diff --git a/dev/Styles/Account.less b/dev/Styles/Account.less
deleted file mode 100644
index f233b3c39..000000000
--- a/dev/Styles/Account.less
+++ /dev/null
@@ -1,4 +0,0 @@
-.b-account-add-content {
- .modal-header {
- }
-}
diff --git a/dev/Styles/Admin.less b/dev/Styles/Admin.less
deleted file mode 100644
index 1eb54beb8..000000000
--- a/dev/Styles/Admin.less
+++ /dev/null
@@ -1,115 +0,0 @@
-
-.b-admin-left {
-
- .b-toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 34px;
- padding: 8px 0 0 @rlLowMargin;
- }
-
- .b-content {
- position: absolute;
- top: 50px + @rlLowMargin + 10px;
- bottom: @rlLowMargin;
- left: 0;
- right: 0;
- overflow: hidden;
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
- }
-}
-
-.b-admin-menu {
-
- .e-item {
- overflow: hidden;
- text-decoration: none;
- outline: 0;
- }
-
- .e-link {
- position: relative;
- display: block;
- height: 30px;
- line-height: 29px;
- cursor: pointer;
- font-size: 18px;
- z-index: 1;
- cursor: default;
-
- background-color: transparent;
- color: var(--settings-menu-disabled-color, #666);
-
- padding: 4px 10px;
-
- outline: 0;
- text-decoration: none;
- }
-
- .e-item.selectable .e-link {
- cursor: pointer;
- color: var(--settings-menu-color, #333);
- }
-
- .e-item.selectable {
- &:hover .e-link{
- background-color: var(--settings-menu-hover-bg-color, #333);
- color: var(--settings-menu-hover-color, #eee);
- }
- &.selected .e-link {
- background-color: var(--settings-menu-selected-bg-color, #333);
- color: var(--settings-menu-selected-color, #eee);
- }
- }
-}
-
-.b-admin-right {
-
- .b-toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 34px;
- padding: 8px @rlLowMargin 8px 0;
- color: #fff;
- text-shadow: 0 1px 1px #000;
-
- display: flex;
-
- h4 {
- flex-grow: 1;
- }
- }
-
- .b-content {
- position: absolute;
- top: 50px + @rlLowMargin;
- bottom: @rlLowMargin;
- left: 0;
- right: @rlLowMargin;
- overflow-y: auto;
- z-index: 2;
-
- background-color: #fff;
- border: @rlMainBorderSize solid @rlMainDarkColor;
-
- box-shadow: @rlMainShadow;
- border-radius: @rlMainBorderRadius;
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
- }
-
-}
-
-.btn.btn-logout {
- padding-left: 12px;
- padding-right: 12px;
-}
diff --git a/dev/Styles/AdminAbout.less b/dev/Styles/Admin/About.less
similarity index 87%
rename from dev/Styles/AdminAbout.less
rename to dev/Styles/Admin/About.less
index 2b2fb7a8a..73d0991cd 100644
--- a/dev/Styles/AdminAbout.less
+++ b/dev/Styles/Admin/About.less
@@ -1,5 +1,5 @@
-.b-admin-about {
+#V-Settings-About {
.rl-logo {
display: inline-block;
width: 250px;
diff --git a/dev/Styles/Admin/Branding.less b/dev/Styles/Admin/Branding.less
new file mode 100644
index 000000000..3e30f64dd
--- /dev/null
+++ b/dev/Styles/Admin/Branding.less
@@ -0,0 +1,4 @@
+
+#V-Settings-Branding {
+
+}
diff --git a/dev/Styles/Admin/Config.less b/dev/Styles/Admin/Config.less
new file mode 100644
index 000000000..68283efa6
--- /dev/null
+++ b/dev/Styles/Admin/Config.less
@@ -0,0 +1,8 @@
+
+#V-Settings-Config {
+ user-select: text;
+
+ em {
+ display: block;
+ }
+}
diff --git a/dev/Styles/Admin/Domain.less b/dev/Styles/Admin/Domain.less
new file mode 100644
index 000000000..268e90d67
--- /dev/null
+++ b/dev/Styles/Admin/Domain.less
@@ -0,0 +1,36 @@
+#V-PopupsDomainAlias {
+ max-width: 330px;
+}
+
+#V-PopupsDomain {
+
+ max-width: 810px;
+
+ .domain-desc {
+ color: #666;
+ line-height: 20px;
+ background-color: #f9f9f9;
+ padding: 8px;
+ border: 1px solid #eee;
+ border-radius: 3px;
+ margin: -5px 0;
+ i {
+ font-style: normal;
+ color: red;
+ }
+ }
+
+ .testing-done {
+ color: green !important;
+ }
+
+ .testing-error {
+ color: red !important;
+ }
+
+ .tab-content {
+ grid-column-end: 5;
+ border: 1px solid #ddd;
+ padding: 10px;
+ }
+}
diff --git a/dev/Styles/Admin/Domains.less b/dev/Styles/Admin/Domains.less
new file mode 100644
index 000000000..8c92ecb57
--- /dev/null
+++ b/dev/Styles/Admin/Domains.less
@@ -0,0 +1,32 @@
+
+#V-Settings-Domains {
+ table {
+ width: 600px;
+ }
+
+ .domain-name {
+ display: inline-block;
+ word-break: break-all;
+ box-sizing: border-box;
+ }
+
+ .domain-alias {
+ display: inline-block;
+ box-sizing: border-box;
+ color: #bbb;
+ padding-left: 5px;
+ }
+
+ .delete-domain, .disabled-domain {
+ cursor: pointer;
+ }
+ .delete-domain:not(:hover), .disabled-domain:not(:hover) {
+ opacity: 0.5;
+ }
+
+ tr.disabled {
+ .domain-name, .domain-alias {
+ color: #bbb;
+ }
+ }
+}
diff --git a/dev/Styles/Admin/General.less b/dev/Styles/Admin/General.less
new file mode 100644
index 000000000..5488992ab
--- /dev/null
+++ b/dev/Styles/Admin/General.less
@@ -0,0 +1,4 @@
+
+#V-Settings-General {
+
+}
diff --git a/dev/Styles/Admin/Layout.less b/dev/Styles/Admin/Layout.less
new file mode 100644
index 000000000..83294f1d8
--- /dev/null
+++ b/dev/Styles/Admin/Layout.less
@@ -0,0 +1,118 @@
+
+.UserBackground body {
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+#rl-content {
+ display:flex;
+ height: 100%;
+}
+
+#rl-left {
+ flex-grow: 0;
+ flex-shrink: 0;
+ overflow: auto;
+ padding-top: 50px + @rlLowMargin + 10px;
+ transition: width 0.3s ease-out;
+ width: @rlLeftWidth;
+
+ nav {
+ a {
+ color: var(--settings-menu-color, #333);
+ cursor: pointer;
+ display: block;
+ font-size: 18px;
+ line-height: 30px;
+ padding: 4px 10px;
+ text-decoration: none;
+ }
+
+ a:focus, a:hover {
+ background-color: var(--settings-menu-hover-bg-color, #333);
+ color: var(--settings-menu-hover-color, #eee);
+ }
+
+ a.selected {
+ background-color: var(--settings-menu-selected-bg-color, #333);
+ color: var(--settings-menu-selected-color, #eee);
+ }
+ }
+}
+
+#rl-right {
+ flex-grow: 1;
+ transition: left 0.3s ease-out, right 0.3s ease-out;
+ z-index: 1;
+}
+
+#V-AdminPane {
+ height: 100%;
+
+ > .b-toolbar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 34px;
+ padding: 8px @rlLowMargin 8px 0;
+ color: #fff;
+ text-shadow: 0 1px 1px #000;
+
+ display: flex;
+
+ h4 {
+ flex-grow: 1;
+ margin: 8px;
+ }
+ }
+}
+
+.btn.btn-logout {
+ padding-left: 12px;
+ padding-right: 12px;
+}
+
+#rl-settings-subscreen {
+ background-color: #fff;
+ border: 1px solid @rlMainDarkColor;
+ border-radius: @rlMainBorderRadius;
+ box-shadow: @rlMainShadow;
+ box-sizing: border-box;
+ height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
+ margin: (50px + @rlLowMargin) @rlLowMargin @rlLowMargin 0;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+/* desktop */
+@media screen and (min-width: 1000px) {
+ .toggleLeft {
+ display: none;
+ }
+}
+
+/* desktop-large */
+@media screen and (min-width: 1401px) {
+ #rl-left {
+ width: @rlLeftWidth + 20;
+ }
+}
+
+/* mobile and tablet */
+@media screen and (max-width: 999px) {
+ #rl-settings-subscreen {
+ padding: 10px;
+ }
+
+ #rl-right {
+ min-width: calc(100% - @rlLowMargin);
+ }
+
+ html.rl-left-panel-disabled {
+ #rl-left {
+ width: 0;
+ }
+ }
+}
diff --git a/dev/Styles/Admin/Packages.less b/dev/Styles/Admin/Packages.less
new file mode 100644
index 000000000..50b17d72a
--- /dev/null
+++ b/dev/Styles/Admin/Packages.less
@@ -0,0 +1,43 @@
+
+#V-Settings-Packages {
+
+ .alert {
+ width: 650px;
+ }
+
+ .disabled table,
+ table .disabled {
+ opacity: 0.5;
+ }
+
+ table {
+
+ width: 700px;
+
+ div:first-child {
+ display: flex;
+ }
+ }
+
+ .package-name {
+ flex-grow: 1;
+ }
+ .package-name.core {
+ font-weight: bold;
+ }
+ .package-desc {
+ font-size: 12px;
+ opacity: 0.7;
+ }
+
+ td + td {
+ text-align: center;
+ vertical-align: middle;
+ white-space: nowrap;
+ }
+
+ a {
+ cursor: pointer;
+ margin-right: 1em;
+ }
+}
diff --git a/dev/Styles/Admin/Plugin.less b/dev/Styles/Admin/Plugin.less
new file mode 100644
index 000000000..cb6c50b81
--- /dev/null
+++ b/dev/Styles/Admin/Plugin.less
@@ -0,0 +1,48 @@
+#V-PopupsPlugin {
+
+ max-width: 660px;
+
+ .information {
+ display: inline-block;
+ background-color: #ddd;
+ border-radius: 10px;
+ cursor: pointer;
+ height: 25px;
+ width: 30px;
+ text-align: center;
+ padding-top: 5px;
+ }
+
+ textarea {
+ width: 400px;
+ height: 70px;
+ }
+
+ .help-block {
+ cursor:help;
+ display: inline-block;
+ margin-left: 0.25em;
+ white-space: break-spaces;
+ }
+ .help-block::before {
+ content: 'ⓘ';
+ }
+ .help-block span {
+ background-color: #fff;
+ border: 1px solid #000;
+ border-radius: 3px;
+ color: #000;
+ display: none;
+ left: 2px;
+ padding: 4px 20px 4px 4px;
+ position: absolute;
+ right: 2px;
+ z-index: 1000;
+ }
+ .help-block:active span,
+ .help-block:focus span,
+ .help-block:hover span {
+ display: block;
+ }
+
+}
diff --git a/dev/Styles/AdminBranding.less b/dev/Styles/AdminBranding.less
deleted file mode 100644
index 4c5442974..000000000
--- a/dev/Styles/AdminBranding.less
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.b-admin-branding {
-
-}
diff --git a/dev/Styles/AdminDomain.less b/dev/Styles/AdminDomain.less
deleted file mode 100644
index 79ec859dc..000000000
--- a/dev/Styles/AdminDomain.less
+++ /dev/null
@@ -1,59 +0,0 @@
-.b-domain-alias-content {
- &.modal {
- width: 330px;
- }
-
- .error-desc {
- color: red;
- }
-}
-
-.b-domain-content {
-
- &.modal {
- width: 810px;
- }
-
- .modal-body {
- position: relative;
- overflow: hidden;
- width: 1600px;
- left: 0;
- transition: left 500ms ease;
- }
-
- &.domain-second-page .modal-body {
- left: -800px;
- }
-
- .domain-desc {
- color: #666;
- line-height: 20px;
- background-color: #f9f9f9;
- padding: 8px;
- border: 1px solid #eee;
- border-radius: 3px;
- margin: -5px 0;
- i {
- font-style: normal;
- color: red;
- }
- }
-
- .error-desc {
- color: red;
- }
-
- .testing-done {
- &.imap-header, &.sieve-header, &.smtp-header {
- color: green;
- font-weight: bold;
- }
- }
-
- .testing-error {
- &.imap-header, &.sieve-header, &.smtp-header {
- color: red;
- }
- }
-}
diff --git a/dev/Styles/AdminDomains.less b/dev/Styles/AdminDomains.less
deleted file mode 100644
index 626286056..000000000
--- a/dev/Styles/AdminDomains.less
+++ /dev/null
@@ -1,56 +0,0 @@
-
-.b-admin-domains-list-table {
-
- width: 600px;
-
- .e-item {
-
- .domain-name {
- display: inline-block;
- word-break: break-all;
- box-sizing: border-box;
- }
-
- .domain-alias {
- display: inline-block;
- box-sizing: border-box;
- color: #bbb;
- padding-left: 5px;
- }
-
- &.disabled {
- .domain-name, .domain-alias {
- color: #bbb;
- }
- }
-
- .button-delete {
- margin-right: 15px;
- visibility: hidden;
- opacity: 0;
- }
-
- .delete-access {
- &.button-delete {
- visibility: visible;
- margin-right: 0;
- opacity: 1;
- }
- }
-
- .delete-domain, .disabled-domain {
- cursor: pointer;
- opacity: 0.5;
- }
-
- &.disabled .disabled-domain {
- opacity: 0.5;
- }
-
- .delete-domain, .disabled-domain {
- &:hover {
- opacity: 1;
- }
- }
- }
-}
diff --git a/dev/Styles/AdminGeneral.less b/dev/Styles/AdminGeneral.less
deleted file mode 100644
index 8a90bc85e..000000000
--- a/dev/Styles/AdminGeneral.less
+++ /dev/null
@@ -1,20 +0,0 @@
-
-.b-admin-general {
-
- .flag-selector {
- padding-top: 5px;
- }
-
- .flag-name {
-
- border-bottom: 1px dashed #555;
- cursor: pointer;
- padding: 2px 0;
-
- &:focus {
- outline: 1px;
- outline-style: dotted;
- }
- }
-}
-
diff --git a/dev/Styles/AdminPackages.less b/dev/Styles/AdminPackages.less
deleted file mode 100644
index 1605c7577..000000000
--- a/dev/Styles/AdminPackages.less
+++ /dev/null
@@ -1,36 +0,0 @@
-
-.b-admin-packages {
-
- .alert {
- width: 650px;
- }
-
-}
-
-.b-admin-packages-list-table {
-
- width: 700px;
-
- .e-item {
-
- .package-img {
- font-size: 12px;
- margin-right: 2px;
- }
-
- .package-name.core {
- font-weight: bold;
- }
- .package-desc {
- color: #999;
- font-size: 12px;
- }
-
- .package-release, .package-actions {
- text-align: center;
- }
- .package-actions {
- vertical-align: middle;
- }
- }
-}
diff --git a/dev/Styles/AdminPlugin.less b/dev/Styles/AdminPlugin.less
deleted file mode 100644
index 94b45b005..000000000
--- a/dev/Styles/AdminPlugin.less
+++ /dev/null
@@ -1,26 +0,0 @@
-.b-plugin-content {
-
- &.modal {
- width: 660px;
- }
-
- .modal-body {
- overflow: auto;
- }
-
- .information {
- display: inline-block;
- background-color: #ddd;
- border-radius: 10px;
- cursor: pointer;
- height: 25px;
- width: 30px;
- text-align: center;
- padding-top: 5px;
- }
-
- textarea {
- width: 400px;
- height: 70px;
- }
-}
diff --git a/dev/Styles/AdminPlugins.less b/dev/Styles/AdminPlugins.less
deleted file mode 100644
index 21993f756..000000000
--- a/dev/Styles/AdminPlugins.less
+++ /dev/null
@@ -1,36 +0,0 @@
-
-.b-admin-plugins-list-table {
-
- &.disabled {
- opacity: 0.5;
- background-color: #eee;
- }
-
- .e-item {
- .plugin-img {
- font-size: 12px;
- margin-right: 2px;
- }
-
- &.disabled {
- .plugin-img, .plugin-name {
- color: #bbb;
- }
- .disabled-plugin {
- opacity: 0.5;
- }
- }
- }
-}
-
-.b-admin-plugin-property {
- .help-block {
- display: block; // account for any element using help-block
- font-style: italic;
- opacity: 0.5;
- margin-bottom: 5px;
- }
- .controls {
- width: 470px;
- }
-}
diff --git a/dev/Styles/AdvancedSearch.less b/dev/Styles/AdvancedSearch.less
deleted file mode 100644
index 5ffb11241..000000000
--- a/dev/Styles/AdvancedSearch.less
+++ /dev/null
@@ -1,11 +0,0 @@
-.b-advanced-search-content {
- &.modal {
- width: 780px;
- }
- .control-label {
- width: 110px;
- }
- .span4 {
- width: 360px;
- }
-}
diff --git a/dev/Styles/Animations.less b/dev/Styles/Animations.less
deleted file mode 100644
index 8fd23b9e7..000000000
--- a/dev/Styles/Animations.less
+++ /dev/null
@@ -1,74 +0,0 @@
-
-@keyframes login-form-shake {
- 0% {transform: translateX(0);}
- 12.5% {transform: translateX(-6px)}
- 37.5% {transform: translateX(5px)}
- 62.5% {transform: translateX(-3px)}
- 87.5% {transform: translateX(2px)}
- 100% {transform: translateX(0)}
-}
-
-html.rl-started-delay {
-
- #rl-left {
- transition: width 0.3s ease-out;
- }
-
- #rl-right {
- transition: left 0.3s ease-out, right 0.3s ease-out;
- }
-
- #rl-sub-right {
- transition: left 0.3s ease-out;
- }
-}
-
-@media screen and (min-width: 1000px) {
-
- html.rl-started-trigger .b-login-content .loginFormWrapper {
- /*transform: scale(1.1);*/
- transform: translateY(-20px);
- opacity: 0.5;
- }
-
- .b-login-content .errorAnimated {
- animation: login-form-shake 400ms ease-in-out;
- }
-
- .b-login-content .errorAnimated .buttonLogin {
- color: #b94a48;
- font-weight: bold;
- }
-
- .b-login-content .afterLoginHide {
- opacity: 0;
- }
-
- .b-login-content .loginFormWrapper {
- transition: all 0.3s ease-out;
- }
-
- .button-delete-transitions {
- transition: all 0.2s linear;
- }
-
- #rl-popups > .rl-view-model .modal {
- transition: all .2s ease-out;
- }
- #rl-popups > .rl-view-model:not(.show) .modal {
- top: -25%;
- }
- #rl-popups > .rl-view-model .modal.fade {
- opacity: 1;
- }
- #rl-popups > .rl-view-model:not(.show) .modal.fade {
- opacity: 0;
- }
-}
-
-#rl-popups > .rl-view-model {
- transition: background-color 0.2s linear;
-}
-#rl-popups > .rl-view-model:not(.show) {
- background-color: rgba(0,0,0,0);
-}
diff --git a/dev/Styles/AnimationsUser.less b/dev/Styles/AnimationsUser.less
deleted file mode 100644
index ce777e8c0..000000000
--- a/dev/Styles/AnimationsUser.less
+++ /dev/null
@@ -1,80 +0,0 @@
-
-@keyframes highlight-folder-row {
- 0% { transform: scale(1); }
- 50% { transform: scale(1.1); }
- 100% { transform: scale(1); }
-}
-
-@keyframes textLoadingAnimationKeyFrame {
- 0% { opacity: 1; }
- 33% { opacity: 0; }
- 100% { opacity: 1; }
-}
-
-@keyframes animate-stripes {
- 0% {background-position: 0 0;}
- 100% {background-position: 60px 0;}
-}
-
-.e-strip-animation {
- background-size: 60px 60px;
- background-image: linear-gradient(135deg, #000 25%, #fff 25%,
- #fff 50%, #000 50%, #000 75%,
- #fff 75%, #fff);
- animation: animate-stripes 2s linear infinite;
- opacity: 0.25;
-}
-
-@media screen and (min-width: 1000px) {
-
- .button-delete-transitions {
- transition: all 0.2s linear;
- }
-
- .b-folders .e-item .anim-action-class {
- animation: highlight-folder-row 0.5s linear;
- }
-
- .b-folders .btn.buttonContacts {
- transition: margin 0.3s linear;
- }
-
- .b-folders .b-content.opacity-on-panel-disabled {
- transition: opacity 0.3s linear;
- }
-
- .messageList {
- .messageListItem {
- transition: max-height 400ms ease;
- }
- .listDragOver {
- transition: all 400ms ease;
- }
- }
-
- .b-list-content {
- .e-contact-item {
- transition: max-height 400ms ease;
- }
- }
-
- .b-compose.loading .b-header-toolbar {
- background-size: 60px 60px;
- background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%,
- transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%,
- transparent 75%, transparent);
- animation: animate-stripes 2s linear infinite;
- }
-}
-
-.textLoadingAnimationD1, .textLoadingAnimationD2, .textLoadingAnimationD3 {
- animation: textLoadingAnimationKeyFrame 1s linear infinite 0s;
-}
-
-.textLoadingAnimationD2 {
- animation-delay: 0.3s;
-}
-
-.textLoadingAnimationD3 {
- animation-delay: 0.6s;
-}
diff --git a/dev/Styles/Ask.less b/dev/Styles/Ask.less
index 1f678163b..7f7d569bc 100644
--- a/dev/Styles/Ask.less
+++ b/dev/Styles/Ask.less
@@ -1,10 +1,8 @@
-.b-ask-content {
+#V-PopupsAsk {
.modal-body {
+ font-size: 18px;
+ padding: 3em 15px;
text-align: center;
}
-
- .desc-place {
- font-size: 18px;
- }
}
diff --git a/dev/Styles/Components.less b/dev/Styles/Components.less
index 3abef0d8c..0cc3edb44 100644
--- a/dev/Styles/Components.less
+++ b/dev/Styles/Components.less
@@ -14,7 +14,7 @@
left: 5px;
width: 10px;
height: 18px;
-
+ border: 2px solid #0F9D58;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
@@ -31,122 +31,76 @@
.e-component {
- &.e-select {
+ white-space: nowrap;
- select:focus {
- outline: 1px dotted;
- }
+ * {
+ display: inline-block;
+ }
+
+ select:focus {
+ outline: 1px dotted;
}
&.e-checkbox {
+ cursor: pointer;
margin-bottom: 6px;
margin-left: -2px;
padding: 2px;
- cursor: pointer;
-
&:focus {
outline: 1px dotted;
}
- .e-checkbox-icon {
- padding: 1px 0 0 1px;
- }
-
- &.disabled {
- cursor: not-allowed;
- opacity: 0.5;
- outline: 0;
- }
- }
-
- &.e-radio {
-
- cursor: pointer;
-
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
-}
-.e-component.material-design {
+ span {
+ margin-left: 0.5em;
+ white-space: normal;
+ }
-
- &.e-checkbox {
-
- margin-top: 2px;
- padding: 2px 2px 1px 2px;
-
- .sub-checkbox-container {
- display: inline-block;
- position: relative;
- transform: translateZ(0);
- width: 18px;
- height: 18px;
- vertical-align: bottom;
- margin-bottom: 3px;
- }
-
- .sub-label {
- padding-left: 12px;
- }
-
- .sub-checkbox {
- position: absolute;
- box-sizing: border-box;
- margin-top: 1px;
-
- .checkbox-box-sizes();
- }
-
- .sub-checkbox.checked {
- border-color: #0F9D58;
-
- .checkbox-mark-sizes();
- }
-
- // animation
- .sub-checkbox.checked {
- &.box {
- border: solid 2px;
- animation: box-shrink 140ms ease-out forwards;
- }
- &.checkmark {
- animation: checkmark-expand 140ms ease-out forwards;
- }
- }
-
- .sub-checkbox.unchecked {
- &.box {
- animation: box-expand 140ms ease-out forwards;
- }
- &.checkmark {
- transform: rotate(45deg);
- animation: checkmark-shrink 140ms ease-out forwards;
- }
- }
+ select + span {
+ line-height: 1.43em;
+ padding: 4px 0;
}
}
-@keyframes box-shrink {
+.e-checkbox.material-design {
+
+ line-height: 20px;
+
+ > div {
+ position: relative;
+ width: 18px;
+ height: 18px;
+ vertical-align: top;
+ }
+
+ > div > div {
+ position: absolute;
+ box-sizing: border-box;
+ margin-top: 1px;
+
+ animation: checkmark-to-box 200ms ease-out forwards;
+ }
+
+ div.checked {
+ animation: box-to-checkmark 200ms ease-out forwards;
+ }
+}
+
+@keyframes box-to-checkmark {
0% { .checkbox-box-sizes(); }
- 100% { .checkbox-result-sizes(); }
-}
-
-@keyframes checkmark-expand {
- 0% { .checkbox-result-sizes(); }
+ 50% { .checkbox-result-sizes(); }
100% { .checkbox-mark-sizes(); }
}
-@keyframes checkmark-shrink {
+@keyframes checkmark-to-box {
0% { .checkbox-mark-sizes(); }
- 100% { .checkbox-result-sizes(); }
-}
-
-@keyframes box-expand {
- 0% { .checkbox-result-sizes(); }
+ 50% { .checkbox-result-sizes(); }
100% { .checkbox-box-sizes(); }
}
diff --git a/dev/Styles/Compose.less b/dev/Styles/Compose.less
deleted file mode 100644
index 5cab01450..000000000
--- a/dev/Styles/Compose.less
+++ /dev/null
@@ -1,172 +0,0 @@
-
-.b-compose {
-
- &.modal {
- width: 98%;
- max-width: 1000px;
- min-height: calc(100% - 52px);
- }
-
- .modal-body {
- padding: 0;
- }
-
- .textAreaParent, .attachmentAreaParent {
- box-sizing: border-box;
- min-height: 200px;
- position: relative;
- }
-
- .attachmentAreaParent {
- border-top: 1px solid #ccc;
- overflow-y: auto;
- padding: 10px 10px 6px 10px;
-
- .no-attachments-desc {
- padding: 50px 10px;
- text-align: center;
- font-size: 24px;
- color: #666;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- }
-
- .attachmentList {
- margin: 0;
- padding: 10px;
- }
- }
-
- .b-header-toolbar {
-
- color: #fff;
- background-color: rgba(0,0,0,0.8);
-
- .close, .minimize-custom {
- opacity: 1;
- color: #fff;
- border-color: #eee;
-
- font-size: 24px;
- line-height: 24px;
- }
-
- .btn.disabled {
- &.button-delete {
- visibility: hidden;
- }
- }
-
- .button-save, .button-delete, .saved-text {
- margin-left: 8px;
- }
-
- .button-close, .button-skip {
- margin-left: 8px;
- }
-
- .disabled.button-delete {
- margin-left: 0;
- }
- }
-
- .b-header {
-
- padding: 10px;
- background-color: #eee;
- color: #333;
-
- .e-identity {
-
- color: #333;
- text-decoration: none;
- font-weight: bold;
-
- &:hover {
- color: #333;
- text-decoration: none;
- }
-
- &.multiply {
- cursor: pointer;
- border-bottom: 1px dashed #555;
- }
- }
-
- .e-row {
- line-height: 30px;
- }
-
- .e-label {
- text-align: right;
- padding: 6px 10px;
- }
-
- .e-value {
-
- padding: 2px 0;
-
- textarea, input[type="text"] {
- width: 100%;
- }
- textarea {
- height: 40px;
- }
- }
-
- .error-desc {
- color: red;
- }
-
- .error-to {
- color: red;
- font-weight: bold;
- }
- }
-
- .b-attachment-button {
- display: inline-block;
- }
-
- .b-attachment-place {
-
- position: absolute;
- left: 5px;
- right: 5px;
- top: 5px;
- bottom: 5px;
-
- border: 2px #777 dashed;
- z-index: 300;
-
- line-height: 119px;
- text-align: center;
- background-color: #efefef;
- font-size: 24px;
-
- border-radius: 10px;
-
- &.dragAndDropOver {
- background-color: #fff;
- }
- }
-}
-
-.minimize-custom {
- border: 0 solid #333;
- border-bottom-width: 3px;
- display: inline-block;
- float: right;
- height: 20px;
- width: 16px;
- font-size: 20px;
- font-weight: bold;
- line-height: 20px;
- margin-right: 15px;
- cursor: pointer;
-}
-
-@media screen and (max-width: 700px) {
- .b-compose .b-header .e-label {
- padding-left: 0;
- }
-}
diff --git a/dev/Styles/FolderClear.less b/dev/Styles/FolderClear.less
deleted file mode 100644
index 8089f0de6..000000000
--- a/dev/Styles/FolderClear.less
+++ /dev/null
@@ -1,4 +0,0 @@
-.b-folder-clear-content {
- .modal-header {
- }
-}
diff --git a/dev/Styles/FolderCreate.less b/dev/Styles/FolderCreate.less
deleted file mode 100644
index 2b2be8b48..000000000
--- a/dev/Styles/FolderCreate.less
+++ /dev/null
@@ -1,4 +0,0 @@
-.b-folder-create-content {
- .modal-header {
- }
-}
diff --git a/dev/Styles/FolderSystem.less b/dev/Styles/FolderSystem.less
deleted file mode 100644
index 4a5f64d25..000000000
--- a/dev/Styles/FolderSystem.less
+++ /dev/null
@@ -1,4 +0,0 @@
-.b-folder-system-content {
- .modal-header {
- }
-}
diff --git a/dev/Styles/Languages.less b/dev/Styles/Languages.less
index ca88ccadf..8111bec7e 100644
--- a/dev/Styles/Languages.less
+++ b/dev/Styles/Languages.less
@@ -1,16 +1,13 @@
-.b-languages-content {
+#V-PopupsLanguages {
- &.modal {
- width: 700px;
- }
+ max-width: 710px;
- .lang-item {
+ label {
display: inline-block;
padding: 5px 15px;
margin: 2px 5px;
width: 180px;
background-color: rgba(128, 128, 128, 0.1);
- text-align: left;
border: 1px solid transparent;
border-radius: 3px;
position: relative;
@@ -26,7 +23,6 @@
}
&.selected::after {
content: "✔";
- color: #080;
position: absolute;
right: 4px;
font-family: snappymail;
@@ -39,7 +35,7 @@
}
@media screen and (max-width: 999px) {
- .b-languages-content .lang-item {
- width: calc(~'100% - 40px');
+ #V-PopupsLanguages label {
+ width: calc(100% - 40px);
}
}
diff --git a/dev/Styles/LayoutAdmin.less b/dev/Styles/LayoutAdmin.less
deleted file mode 100644
index b92c44dbb..000000000
--- a/dev/Styles/LayoutAdmin.less
+++ /dev/null
@@ -1,105 +0,0 @@
-
-.UserBackground body {
- background-size: contain;
- background-repeat: no-repeat;
- background-position: center;
-}
-
-#rl-center, #rl-top, #rl-left, #rl-right {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- padding: 0;
- margin: 0;
- border: 0;
- z-index: 0;
-}
-
-#rl-content {
- height: 100%;
- width: 100%;
-}
-
-#rl-center {
- min-width: 600px;
- min-height: 400px;
-}
-
-#rl-top {
- bottom: auto;
- z-index: 2;
-}
-
-#rl-left {
- width: @rlLeftWidth;
-}
-
-#rl-right {
- z-index: 1;
- left: @rlLeftWidth;
-}
-
-#rl-popups > .rl-view-model {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1100;
- overflow: auto;
- background-color: rgba(0,0,0,0.3);
-// -webkit-overflow-scrolling: touch;
-}
-
-.b-settings-content {
- padding:20px 20px 20px 30px;
-}
-
-/* desktop */
-@media screen and (min-width: 1000px) {
- .toggleLeft {
- display: none;
- }
-}
-
-/* desktop-large */
-@media screen and (min-width: 1401px) {
- #rl-left {
- width: @rlLeftWidth + 20;
- }
-
- #rl-right {
- left: @rlLeftWidth + 20;
- }
-}
-
-/* mobile and tablet */
-@media screen and (max-width: 999px) {
- .b-settings-content {
- padding: 10px 10px 10px 20px;
- }
-
- #rl-center {
- min-width: 250px;
- min-height: 250px;
- }
-
- html:not(.rl-left-panel-disabled) #rl-right {
- right: 5-@rlLeftWidth;
- }
-
- html.rl-left-panel-disabled {
- #rl-left {
- width: 0px !important;
-
- .opacity-on-panel-disabled {
- opacity: 0.3;
- }
- }
- #rl-right {
- left: 5px !important;
- }
- }
-}
diff --git a/dev/Styles/Login.less b/dev/Styles/Login.less
index 89bb310fc..603d59903 100644
--- a/dev/Styles/Login.less
+++ b/dev/Styles/Login.less
@@ -1,138 +1,128 @@
-
-.rl-view-model {
- &.RL-Login, &.RL-AdminLogin {
- position: fixed;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 100%;
- max-width: 450px;
- z-index: 5;
- }
+#V-Login, #V-AdminLogin {
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 100%;
+ min-width: 250px;
+ max-width: 600px;
+ transition: all 0.3s ease-out;
+ z-index: 5;
}
-@glass-color: #fff !important;
-@glass-error-color: #f76260 !important;
-@glass-m-color: rgba(255, 255, 255, .8) !important;
+@glass-color: #fff;
+@glass-error-color: #f76260;
+@glass-m-color: rgba(255, 255, 255, .8);
-.b-login-content {
+.LoginView {
- .loginFormWrapper {
+ .descWrapper {
+ color: var(--loading-color, #000);
+ font-size: 30px;
+ margin-bottom: 10px;
+ text-align: center;
+ text-shadow: var(--loading-text-shadow);
+ }
+
+ form {
+ background-color: var(--login-bg-color, rgba(0, 0, 0, .5));
+ border: var(--login-border, 1px solid rgba(255, 255, 255, .2));
+ border-radius: var(--login-border-radius, 7px);
+ box-shadow: var(--login-box-shadow);
+ color: var(--login-color, @glass-m-color);
+ margin: 0;
+ padding: 40px 40px 20px 40px;
position: relative;
- .descWrapper {
- margin-bottom: 10px;
- text-align: center;
-
- .desc {
- font-size: 30px;
- color: var(--loading-color, #000);
- text-shadow: var(--loading-text-shadow);
- }
- }
-
- .loginForm {
- background-color: var(--login-bg-color, rgba(0, 0, 0, .5));
- border: var(--login-border, 1px solid rgba(255, 255, 255, .2));
- border-radius: var(--login-border-radius, 7px);
- box-shadow: var(--login-box-shadow);
- margin: 0;
- }
-
- .input-append .add-on *,
- .loginForm, .legend, .e-checkbox-icon, .g-ui-link, .language-button {
+ * {
color: var(--login-color, @glass-m-color);
- text-shadow: none !important;
}
- .controls, .control-group {
- margin-bottom: 25px;
+ .controls.error * {
+ color: @glass-error-color;
+ border-color: @glass-error-color;
}
- .wrapper {
- padding: 40px 40px 20px 40px;
+ &.submitting > * {
+ opacity: 0.3;
}
- .controls {
- .inputLoginForm, .inputEmail, .inputLogin, .inputPassword {
- font-size: 18px;
- height: 40px;
- line-height: 20px;
- padding-left: 12px;
- padding-right: 12px;
- }
- .inputEmail, .inputLogin, .inputPassword {
- padding-right: 35px;
- }
+ &.submitting::after{
+ content: '';
+ position: absolute;
+ width: 60px;
+ height: 60px;
+ top: 50%;
+ left: 50%;
+ margin-top: -30px;
+ margin-left: -30px;
- input, .btn {
- border: 1px solid none !important;
- background: none !important;
- outline: none !important;
- text-shadow: none !important;
- box-shadow: none !important;
+ border: 8px solid transparent;
+ border-top-color: var(--spinner-color, #fff);
+ animation: loginRotation 1s infinite ease-in-out;
- color: @glass-color;
- border-color: @glass-m-color;
- }
+ border-radius: 50%;
+ z-index: 1052;
+ }
+ }
- input {
+ .controls, .control-group {
+ margin-bottom: 25px;
+ position: relative;
+ }
- &:placeholder {
- color: @glass-color;
- text-shadow: none !important;
- }
+ input {
+ font-size: 18px;
+ height: 40px;
+ padding-right: 30px;
- &:focus, &:hover {
- border-color: @glass-color;
- }
- }
-
- .btn {
-
- text-transform: uppercase;
- font-size: 13px;
-
- &:hover, &:active {
- border-color: @glass-color;
- }
- }
-
- &.error {
- .input-append .add-on * {
- color: @glass-error-color;
- }
-
- input {
- color: @glass-error-color;
- border-color: @glass-error-color;
- }
- }
+ &:placeholder {
+ color: @glass-color;
}
- .signMeLabel .e-checkbox {
- margin-top: 5px;
+ &:focus, &:hover {
+ border-color: @glass-color;
}
+ }
- .input-append .add-on {
- position: relative;
- height: 30px;
- background: none;
- margin-left: -35px;
- z-index: 1000;
- border: 0;
- }
+ input, .btn {
+ background: none !important;
+ border: 1px solid @glass-m-color;
+ color: @glass-color;
+ }
- .input-append .add-on * {
- font-size: 17px;
- line-height: 29px;
- outline: none !important;
- box-shadow: none !important;
- }
+ .fontastic + input {
+ padding-left: 30px;
+ }
- .controls.error .add-on *,
- .control-group.error .add-on * {
- color: #b94a48;
+ .controls > .fontastic:first-child {
+ position: absolute;
+ font-size: 17px;
+ line-height: 38px;
+ left: 6px;
+ }
+
+ input + button {
+ border: none;
+ background: none;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 17px;
+ line-height: 38px;
+ margin: 0;
+ padding: 0;
+ position: absolute;
+ right: 6px;
+ top: 0;
+ }
+
+ .btn {
+
+ text-transform: uppercase;
+ font-size: 13px;
+
+ &:hover, &:active {
+ border-color: @glass-color;
}
}
@@ -153,24 +143,30 @@
opacity: 0;
}
- .flag-selector {
- margin-bottom: 0;
- }
-
.language-buttons {
+ flex-grow: 1;
margin-top: 5px;
+ text-align: right;
}
.language-button {
padding: 5px;
- outline: none;
text-decoration: none;
}
+
+ .errorAnimated {
+ animation: login-form-shake 400ms ease-in-out;
+ }
+
+ .errorAnimated .buttonLogin {
+ color: #b94a48;
+ font-weight: bold;
+ }
}
@media screen and (max-width: 480px) {
- .b-login-content .loginFormWrapper {
- .wrapper {
+ .LoginView {
+ form {
padding: 30px 4vw 10px;
}
}
@@ -180,36 +176,15 @@
to {transform: rotate(1turn);}
}
-.submitting-pane {
- position: relative;
- &.submitting > * {
- opacity: 0.3;
- }
- html &.submitting::after{
- content: '';
- position: absolute;
- width: 60px;
- height: 60px;
- top: 50%;
- left: 50%;
- margin-top: -30px;
- margin-left: -30px;
-
- border: 8px solid transparent;
- border-top-color: var(--spinner-color, #fff);
- animation: loginRotation 1s infinite ease-in-out;
-
- border-radius: 50%;
- z-index: 1052;
- }
+@keyframes login-form-shake {
+ 0% {transform: translateX(0);}
+ 12.5% {transform: translateX(-6px)}
+ 37.5% {transform: translateX(5px)}
+ 62.5% {transform: translateX(-3px)}
+ 87.5% {transform: translateX(2px)}
+ 100% {transform: translateX(0)}
}
-.btn-submit-icon-wrp {
- border: none;
- background: none;
- display: inline-block;
- margin: 0;
- padding: 0;
- outline: none;
- cursor: pointer;
+html.rl-started-trigger .LoginView {
+ opacity: 0.5;
}
diff --git a/dev/Styles/Main.less b/dev/Styles/Main.less
index 62ba75249..b23555de0 100644
--- a/dev/Styles/Main.less
+++ b/dev/Styles/Main.less
@@ -12,7 +12,7 @@ html, body {
body {
-webkit-touch-callout: none;
-
+ font-size: @baseFontSize;
position: fixed;
top: 0;
left: 0;
@@ -20,8 +20,9 @@ body {
right: 0;
}
-html:not(.rl-mobile) {
- min-width: 700px;
+html.list-loading body {
+/* cursor: wait;*/
+ cursor: progress;
}
@media screen and (min-width: 1000px) {
@@ -32,10 +33,6 @@ html:not(.rl-mobile) {
}
}
-textarea {
- resize: none;
-}
-
option:disabled {
color: #aaa;
cursor: not-allowed;
@@ -46,21 +43,88 @@ option:disabled {
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
-.button-confirm-delete {
- margin-right: 15px;
- opacity: 0;
- visibility: hidden;
+
+dialog::backdrop {
+ background: rgba(0,0,0,0.5);
}
-.button-confirm-delete.delete-access {
- margin-right: 0;
- opacity: 1;
- visibility: visible;
+.dialog-backdrop {
+ background: rgba(0,0,0,0.5);
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
}
-.drag-handle {
- color: #eee;
- cursor: grab;
+dialog {
+ border: 0;
+ background-color: @white;
+ border: 1px solid rgba(0,0,0,.3);
+ border-radius: 6px;
+ box-shadow: 0 5px 80px rgba(0,0,0,0.3);
+ display: flex;
+ flex-direction: column;
+ margin: 10px auto;
+ max-height: calc(100vh - 20px);
+ max-width: 560px;
+ overflow: auto;
+ padding: 0;
+ position: fixed;
+ transition: all .2s ease-out;
+ top: 0;
+ width: calc(100% - 20px);
}
-tr:hover .drag-handle {
- color: #aaa;
+dialog:not([open]) {
+ display: none !important;
+}
+dialog.animate {
+ opacity: 1;
+}
+dialog:not(.animate) {
+ background-color: rgba(0,0,0,0);
+ opacity: 0;
+ top: -25%;
+}
+
+dialog > header {
+ padding: 9px 15px;
+ border-bottom: 1px solid #eee;
+
+ // Close icon
+ .close {
+ margin-top: 2px;
+ }
+
+ // Heading
+ h3 {
+ margin: 0;
+ line-height: 30px;
+ }
+}
+
+// Body (where all modal content resides)
+dialog .modal-body {
+ overflow: auto;
+ margin: 0;
+ padding: 15px;
+ position: relative;
+}
+
+// Footer (for actions)
+dialog > footer {
+ padding: 14px 15px 15px;
+ margin-bottom: 0;
+ text-align: right; // right align buttons
+ border-top: 1px solid #ddd;
+ border-radius: 0 0 6px 6px;
+
+ // Properly space out buttons
+ .btn + .btn {
+ margin-left: 5px;
+ margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
+ }
+ // but override that for button groups
+ .btn-group .btn + .btn {
+ margin-left: -1px;
+ }
}
diff --git a/dev/Styles/MessageList.less b/dev/Styles/MessageList.less
deleted file mode 100644
index 8a73446de..000000000
--- a/dev/Styles/MessageList.less
+++ /dev/null
@@ -1,530 +0,0 @@
-
-html.rl-mobile,
-html.rl-no-preview-pane {
- .messageList.message-selected {
- display: none;
- }
-}
-
-#sort-list-dropdown-id {
- padding-left: 6px;
- padding-right: 6px;
-}
-
-.messageList {
-
- .toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 30px;
- padding: 10px @rlMainBorderSize;
- z-index: 102;
- white-space: nowrap;
- }
-
- .b-footer {
- position: absolute;
- bottom: 0;
- right: 0;
- left: 0;
- height: 30px;
- padding: 7px;
- z-index: 101;
-
- background-color: var(--message-list-toolbar-bg-color, #eee);
-// #gradient > .vertical(#f4f4f4, #dfdfdf);
-
- border-bottom-right-radius: @rlMainBorderRadius;
- border-bottom-left-radius: @rlMainBorderRadius;
- border-top: 1px solid #bbb;
-
- .e-quota {
- display: inline-block;
- margin-top: 5px;
- margin-left: 5px;
- font-size: 18px;
- cursor: help;
- }
- .e-quota:hover {
- border-bottom: 1px dashed #333;
- }
- }
-
- .btn.buttonMoreSearch {
- padding-left: 8px;
- padding-right: 8px;
- }
-
- .b-message-list-wrapper {
- position: absolute;
- top: 50px;
- right: 0;
- left: 0;
- bottom: @rlBottomMargin;
-
- border: @rlMainBorderSize solid @rlMainDarkColor;
-
- box-shadow: @rlMainShadow;
- border-radius: @rlMainBorderRadius;
- z-index: 101;
- }
-
- .second-toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 29px;
- padding: 10px 8px 10px 11px;
- z-index: 101;
- white-space: nowrap;
-
- background-color: var(--message-list-toolbar-bg-color, #eee);
-// #gradient > .vertical(#f4f4f4, #dfdfdf);
-
- border-top-right-radius: @rlMainBorderRadius;
- border-top-left-radius: @rlMainBorderRadius;
- border-bottom: 1px solid #bbb;
-
- .checkboxCheckAll {
- cursor: pointer;
- line-height: 30px;
- }
-
- .input-append {
- margin-left: 14px;
- text-align: right;
- width: calc(100% - 30px);
-
- .close-input-wrp {
- width: 90%;
- min-width: 200px;
- max-width: 300px;
- }
-
- .inputSearch {
- width: 100%;
- }
- }
- }
-
- .line-loading {
- position: absolute;
- left: 0;
- right: 0;
- z-index: 102;
- height: 0;
- top: 50px;
- }
-
- .b-content {
- position: absolute;
- top: 50px;
- bottom: 45px;
- left: 0;
- right: 0;
- padding: 0;
- overflow: auto;
- z-index: 101;
- scroll-behavior: smooth;
-
- box-sizing: border-box;
-
- background-color: #fff;
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
-
- .listClear {
- text-align: center;
- padding: 10px;
- font-size: 14px;
- line-height: 13px;
- }
-
- .listEmptyList, .listEmptyListLoading, .listDragOver, .listError, .listEmptySearchList {
- color: #999;
- text-align: center;
- padding: 60px 10px;
- font-size: 24px;
- line-height: 30px;
-
- .e-icon {
- font-size: 24px;
- line-height: 30px;
- }
- }
-
- .listDragOver {
- max-height: 0;
- overflow: hidden;
- padding: 0 10px;
- }
-
- .listDragOver.viewAppendArea {
- max-height: 120px;
- padding: 30px 10px;
- }
- .listDragOver.dragOverEnter {
- background-color: #e0fdda;
- color: #333;
- }
-
- .listError {
- color: #DA4F49;
- }
-
- .listSearchDesc {
- font-size: 16px;
- padding: 12px;
- border-bottom: 1px solid #eee;
- }
-
- .listThreadUidDesc {
- font-size: 16px;
- padding: 7px 20px 6px 20px;
- background-color: rgba(128,128,128,0.5);
- border-bottom: 1px solid #888;
- color: #fff;
- cursor: pointer;
- text-shadow: 0 1px 0 #000;
- text-align: center;
- }
-
- .fullThreadsParent {
- height: 25px;
- padding: 3px 5px;
- background-color: #f4f4f4;
- text-align: center;
- }
- }
-
- .messageListItem {
-
- position: relative;
- font-size: 12px;
- overflow: hidden;
- cursor: pointer;
-
- margin: 0;
- padding: 5px 0;
- border: 0;
- border-bottom: 1px solid rgba(153, 153, 153, 0.2);
- border-left: 6px solid #eee;
-
- z-index: 100;
-
- &.focused {
- background-color: rgba(128, 128, 128, 0.1);
- border-left-color: #ccc;
- }
-
- .importantMark {
- display: none;
-
- color:red;
- margin-right:5px
- }
-
- &.deleted-mark {
- opacity: .7;
- .subjectParent {
- text-decoration: line-through;
- }
- }
-
- &.important .importantMark {
- display: inline;
- }
-
- &.new {
- max-height: 0;
- }
-
- &.deleted {
- max-height: 0;
- border-color: transparent !important;
- }
-
- .checkboxMessage {
- line-height: 12px;
- padding: 0 8px 0 6px;
- font-size: 16px;
- }
-
- .flagParent {
- padding: 0 10px 0 5px;
- }
-
- time, .sizeParent {
- margin: 0 5px;
- opacity: 0.6;
- font-size: 11px;
- white-space: nowrap;
- }
-
- .threadsParent {
- position: relative;
- }
-
- .attachmentParent {
- position: relative;
- margin: 2px 10px 0 5px;
- color: #666;
- text-shadow: 0 1px 0 #eee;
- }
-
- .senderParent, .subjectParent {
- overflow: hidden;
- white-space: nowrap;
- }
-
- .threadsCountParent {
- display: inline;
- overflow: hidden;
- background-color: #eee;
- padding: 1px 5px;
- margin-right: 5px;
- border: 1px solid #ccc;
- border-radius: 5px;
- }
-
- .threadsCountParent.lastSelected {
- background-color: #999;
- border-color: #999;
- color: #fff;
- }
-
- .threadsCountParent:hover {
- border-color: #666;
- }
-
- .subjectParent .emptySubjectText {
- display: none;
- font-style: italic;
- opacity: 0.5;
- }
-
- &.emptySubject .subjectParent {
- .subject {
- display: none;
- }
- .emptySubjectText {
- display: inline;
- }
- }
-
- .sender, .subject {
- margin: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .threads-len {
- .threads-len-data {
-
- border-radius: 6px;
- padding: 2px 0 1px 4px;
- border: 1px solid #ccc;
-
- &:hover {
- background-color: rgba(127,127,127,0.3);
- border-color: #666;
- }
- }
- }
-
- .flagOff {
- opacity: 0.5;
- &:hover {
- opacity: 1;
- }
- }
-
- .flagOn, .flagOnHalf {
- color: orange;
- }
-
- .replyFlag, .forwardFlag {
- display: none;
- margin-right: 0.25em;
- }
-
- &.answered .replyFlag {
- display: inline-block;
- }
-
- &.forwarded .forwardFlag {
- display: inline-block;
- }
-
- &:not(.withAttachments) .attachmentParent {
- display: none;
- }
-
- &.hasUnseenSubMessage {
- background-color: rgba(255, 255, 64, 0.15);
- border-left-color: lighten(orange, 30%);
- &.focused {
- border-left-color: darken(orange, 10%);
- }
- }
-
- &.unseen {
- background-color: rgba(255, 255, 64, 0.15);
- border-left-color: orange;
- .sender, .subjectParent {
- font-weight: bold;
- }
-
- &.focused {
- border-left-color: darken(orange, 10%);
- }
- }
-
- &.checked {
- border-left-color: lighten(#398CF2, 10%);
-
- &.focused {
- border-left-color: darken(#398CF2, 5%);
- }
- }
-
- &.selected {
- background-color: rgba(140, 200, 255, 0.3);
- border-bottom-color: rgba(57, 140, 242, 0.2);
- border-left-color: #398CF2;
- z-index: 101;
-
- + .messageListItem {
- border-bottom-color: rgba(57, 140, 242, 0.3);
- }
- }
-
- &:not(.flagged) .flagOn,
- &:not(.hasFlaggedSubMessage) .flagOnHalf,
- &.flagged .flagOff, &.hasFlaggedSubMessage .flagOff {
- display: none;
- }
- }
-
- &.message-focused {
- .b-message-list-wrapper {
- background-color: #000;
- }
- .b-content {
- opacity: 0.97;
- }
- }
-
- &.hideMessageListCheckbox {
- .checkboxMessage, .checkboxCheckAll {
- visibility: hidden;
- }
- }
-}
-
-/* desktop-large */
-@media screen and (min-width: 1401px) {
- .messageList .messageListItem {
- font-size: 13px;
- time {
- font-size: 13px;
- }
- }
-}
-
-html .messageList .line-loading {
- height: 5px !important;
-}
-
-#messagesDragImage {
- color: #fff;
- background-color: #000;
- height: 20px;
- min-width: 30px;
- padding: 4px 10px;
- position: fixed;
- right: -100px;
- top: 0;
-}
-
-.messageListItem {
-
- display: flex;
- flex-wrap: wrap;
-
- > * {
- display: flex;
- flex: 0 0 auto;
- order: 0;
- }
-}
-
-.rl-side-preview-pane, .rl-mobile {
-
- .messageListItem {
-
- .checkboxMessage {
- margin: 10px 0 -5px;
- }
-
- .subjectParent {
- flex: 1 0 auto;
- line-height: 16px;
- margin-left: 30px;
- order: 1;
- width: calc(100% - 120px);
- }
-
- .senderParent {
- flex: 1 0 45%;
- }
-
- .flagParent {
- order: 1;
- }
-
- .sizeParent {
- order: 2;
- }
-
- .attachmentParent {
- order: 3;
- }
- }
-}
-
-html:not(.rl-mobile):not(.rl-side-preview-pane) .messageList {
-
- .messageListItem {
-
- line-height: 25px;
- flex-wrap: nowrap;
-
- .checkboxMessage {
- line-height: 25px;
- }
-
- .flagParent {
- margin-top: 6px;
- }
-
- .attachmentParent {
- margin: 6px 8px 0 0;
- }
-
- .senderParent {
- font-weight: normal;
- text-overflow: none;
- flex: 0 0 25%;
- }
-
- .subjectParent {
- flex: 1 1 auto;
- }
- }
-}
diff --git a/dev/Styles/MessageView.less b/dev/Styles/MessageView.less
deleted file mode 100644
index 60d89596a..000000000
--- a/dev/Styles/MessageView.less
+++ /dev/null
@@ -1,589 +0,0 @@
-
-.g-ui-min-height-300 {
- min-height: 300px;
-}
-
-.messageView {
-
- .toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 30px;
- padding: 10px 0;
- color: #fff;
- }
-
- .b-content {
-
- position: absolute;
- margin: 0;
- top: 50px + @rlLowMargin;
- bottom: @rlLowMargin + @rlBottomMargin;
- right: @rlLowMargin;
- left: -2px;
- /*overflow: hidden;*/
- border: @rlLowBorderSize solid @rlMainDarkColor;
-
- border-radius: @rlLowBorderRadius;
-
- background-color: #fff;
-
- .b-message-view-checked-helper {
- text-align: center;
- font-size: 70px;
- line-height: 70px;
- padding-top: 100px;
- color: #999;
-
- .icon-mail {
- font-size: 100px;
- font-size: 50px;
- line-height: 90px;
- padding-left: 10px;
- }
- }
-
- .b-message-view-desc {
- text-align: center;
- font-size: 24px;
- line-height: 30px;
- color: #999;
- padding: 120px 10px 0 10px;
- }
-
- .b-message-view-desc.error {
- color: #DA4F49;
- }
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
-
- .message-fixed-button-toolbar {
- z-index: 100;
- position: absolute;
- top: 33px;
- right: 10px;
- }
-
- .infoParent {
- cursor: pointer;
- opacity: 0.5;
-
- &:hover {
- opacity: 1;
- }
- }
-
- .flagParent {
-
- cursor: pointer;
- margin: 0 0.25em;
-
- .flagOn {
- color: orange;
- }
-
- &.flagOff:not(:hover) {
- opacity: 0.5;
- }
- }
- }
-
- .messageItemHeader {
-
- padding: 10px;
- background-color: #f8f8f8;
- border-top: 0;
- border-bottom: 1px solid #ddd;
- border-top-right-radius: 5px;
- border-top-left-radius: 5px;
- z-index: 1;
-
- .subjectParent {
- font-size: 18px;
- font-weight: bold;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- margin-bottom: 8px;
- line-height: 100%;
- height: 22px;
- vertical-align: middle;
- }
-
- .messageButtons {
- margin-top: 5px;
- }
-
- .informationShort {
- margin: 4px 50px 0 5px;
- overflow: hidden;
- text-overflow: ellipsis;
-
- a {
- .g-ui-link;
- }
- }
-
- .informationShortWrp {
- max-height: 100px;
- overflow-y: auto;
- min-height: 30px;
- margin-top: 4px;
- }
-
- .informationFull {
- margin-top: 30px;
-
-
- table {
- width: 100%;
- }
-
- table, tr, td {
- border-spacing: 0;
- }
-
- td {
- padding: 4px;
- vertical-align: top;
- min-width: 43px;
- }
-
- td:first-child {
- border-right: 1px solid #999;
- text-align: right;
- white-space: nowrap;
- width: 1%;
- }
-
- .labelTo {
- white-space: nowrap;
- }
-
- td + td {
- word-break: break-all;
- }
- }
-
- .emptySubjectText {
- display: none;
- font-style: italic;
- font-weight: normal;
- color: #999;
- }
-
- &.emptySubject .emptySubjectText {
- display: inline;
- }
- }
-
- .messageItem {
-
- color: #000;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- overflow: auto;
- -webkit-overflow-scrolling: touch;
- border-radius: @rlLowBorderRadius;
- scroll-behavior: smooth;
-
- .buttonFull {
- display: inline-block;
- position: fixed;
- right: 25px;
- bottom: 25px;
- height: 30px;
- width: 30px;
- text-align: center;
- vertical-align: middle;
- line-height: 30px;
- background-color: transparent;
- background-color: #fff;
- border: 1px solid #333;
- color: #333;
- z-index: 2;
- cursor: pointer;
- border-radius: 5px;
- opacity: 0.5;
-
- &:hover {
- opacity: 0.8;
- border-color: #666;
- background-color: #888;
- color: #fff;
- }
- }
-
- .loading {
- text-align: center;
- font-size: 24px;
- color: grey;
- padding-top: 50px;
- }
-
- .line-loading {
- height: 5px;
- }
-
- .showImages, .readReceipt, .pgpSigned, .pgpEncrypted {
- cursor: pointer;
- padding: 10px 15px;
- border-bottom: 1px solid #ddd;
- background-color: #eee;
- }
-
- .pgpInfo {
- padding: 5px 15px;
- border-bottom: 1px solid #ddd;
- background-color: #fcf8e3;
-
- &.success {
- background-color: #e9f4ff;
- }
- }
-
- .readReceipt {
- background-color: #ffffd9;
- }
-
- .attachmentsPlace {
-
- padding: 10px 10px 6px 10px;
- background: #eee;
- border-bottom: 1px solid #ddd;
- position: relative;
-
- .attachmentList {
- margin: 0;
- }
-
- &:not(.selection-mode) {
- .checkboxAttachment {
- display: none;
- }
- }
-
- &.unselectedAttachmentsError {
- .attachmentItem {
- box-shadow: 0 1px 4px red;
- box-shadow: 0 1px 5px rgba(255, 0, 0, 0.4);
- box-shadow: 0 0 0 1px rgba(255, 0, 0, 0.2), 0 1px 5px rgba(255, 0, 0, 0.3);
- }
- }
-
- .controls-handle {
- position: absolute;
- bottom: 5px;
- right: 8px;
- color: #999;
- cursor: pointer;
- }
- }
-
- .attachmentsControls {
- padding: 7px 5px 7px 14px;
- background: #e8e8e8;
- border-bottom: 1px solid #ddd;
- }
-
- .rlBlockquoteSwitcher {
-
- background-color: #eee;
- border: 1px solid #999;
- display: block;
- width: 30px;
- height: 14px;
- line-height: 14px;
- text-align: center;
- cursor: pointer;
- margin: 2em 0 10px;
-
- opacity: 0.5;
- &:hover {
- opacity: 1;
- }
- }
-
- .bodySubHeader {
- z-index: 2;
- }
-
- .b-text-part {
-
- height: 100%;
-
- div[data-x-div-type=html] {
- height: 100%;
- div[data-x-div-type=body] {
- height: 100%;
- }
- }
-
- a {
- color: blue;
- text-decoration: underline;
-
- &:visited {
- color: #609;
- }
- &:active {
- color: red;
- }
- }
-
- blockquote {
- border-left: 2px solid #000;
- padding: 0 10px;
- margin: 0;
- }
-
- .rl-bq-switcher.hidden-bq {
- display: none;
- }
-
- &.html {
-
- div[data-x-div-type=body] {
- /*padding: 15px;*/
- margin: 15px;
- }
-
- img {
- max-width: 100%;
- }
- img[data-x-src]:not([src]) {
- border: 1px solid #999;
- position: relative;
- }
- img[data-x-src]:not([src])::after {
- content: "🖼";
- font-family: snappymail;
- position: absolute;
- top: 0;
- left: 0;
- color: #000;
- background-color: #fff;
- }
- img[data-x-src]:not([src]):hover::after {
- content: attr(data-x-src);
- font-family: var(--fontSans);
- font-size: 11px;
- height: auto;
- transform: translate(-25%,0);
- width: auto;
- width: -moz-fit-content;
- width: -webkit-fit-content;
- width: fit-content;
- z-index: 1000;
- }
-
- pre, code {
- margin: 0;
- border: none;
- word-break: normal;
- word-wrap: break-word;
- }
-
- code {
- border-radius: 0;
- display: inline;
- padding: 2px 5px;
- }
-
- pre {
- border-radius: 5px;
- display: block;
- padding: 5px 10px;
- }
-
- pre > code {
- padding: 0;
- }
- }
-
- &.plain {
-
- padding: 15px;
- /*white-space: pre-wrap;*/
- font-family: var(--fontMono);
-
- pre {
- margin: 0;
- padding: 0;
- border: none;
- display: block;
- word-break: normal;
- }
-
- pre.b-plain-openpgp {
- display: inline-block;
- padding: 6px 10px;
- border: 1px dashed #666;
- word-break: break-all;
-
- &.success {
- border-color: green;
- background-color: rgba(0, 255, 0, 0.03);
- }
- &.error {
- border-color: red;
- background-color: rgba(255, 0, 0, 0.03);
- }
- }
-
- blockquote {
- border-left: 2px solid blue;
- color: blue;
- }
-
- blockquote blockquote {
- border-left: 2px solid green;
- color: green;
- }
-
- blockquote blockquote blockquote {
- border-left: 2px solid red;
- color: red;
- }
- }
-
- .b-openpgp-control {
-
- display: inline-block;
- cursor: pointer;
- color: #777;
-
- &:hover {
- color: #111;
- }
-
- &.success {
- color: green;
- cursor: help;
- }
-
- &.error {
- color: red;
- }
- }
- }
- }
-
- &.message-focused .b-content {
- z-index: 101;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- border-radius: @rlLowBorderRadius;
- border-color: darken(@rlMainDarkColor, 5%);
- }
-
- .thread-controls {
- .dropdown-toggle {
- padding-left: 10px;
- padding-right: 10px;
- }
- &.open .dropdown-toggle {
- padding-left: 10px;
- padding-right: 10px;
- }
- }
-
- .thread-list {
-
- .e-link {
- padding: 4px 8px 6px 10px;
- }
-
- &.hide-more .thread-list-message.more-that {
- display: none;
- }
-
- .thread-date {
- font-size: 13px;
- color: #999;
- }
-
- .more-threads {
- text-align: center;
- padding: 8px;
- background-color: #F5F5F5;
- color: #555;
- text-decoration: underline;
- border-top: 1px dashed #555;
- }
- }
-}
-
-html.rl-mobile .messageView,
-html.rl-no-preview-pane .messageView {
-
- &:not(.message-selected) {
- display: none;
- }
-
- .toolbar {
- padding-left: @rlMainBorderSize;
- }
-
- .b-content {
- top: 50px;
- left: 0;
- bottom: @rlBottomMargin;
- right: @rlBottomMargin;
- border: @rlMainBorderSize solid @rlMainDarkColor;
- box-shadow: @rlMainShadow;
- border-radius: @rlMainBorderRadius;
- }
-}
-
-html:not(.rl-mobile):not(.rl-no-preview-pane) .messageView {
- .top-toolbar {
- display: none;
- }
-}
-
-html.rl-bottom-preview-pane:not(.rl-mobile) .messageView {
- .b-content {
- bottom: @rlBottomMargin;
- }
-}
-
-html.rl-message-fullscreen {
-
- #rl-left {
- display: none !important;
- }
-
- #rl-right {
- .RL-MailMessageList,
- .RL-SettingsPane,
- .RL-SystemDropDown,
- .RL-MailMessageView .messageView .toolbar {
- display: none !important;
- }
- }
-
- .messageView .b-content {
- position: fixed !important;
- margin: 5px !important;
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
- z-index: 10000 !important;
- border: @rlLowBorderSize solid @rlMainDarkColor !important;
- border-radius: @rlLowBorderRadius !important;
- }
-}
-
-html:not(.rl-mobile) .messageItem {
- .buttonFull {
- display: none !important;
- }
-}
diff --git a/dev/Styles/OpenPgpKey.less b/dev/Styles/OpenPgpKey.less
deleted file mode 100644
index 0769472c4..000000000
--- a/dev/Styles/OpenPgpKey.less
+++ /dev/null
@@ -1,144 +0,0 @@
-.b-open-pgp-key-view-content, .b-open-pgp-key-generate-content {
- &.modal {
- width: 570px;
- }
-}
-
-.b-open-pgp-key-view-content {
- .key-viewer {
- max-height: 500px;
- overflow: auto;
- }
-}
-
-.b-compose-open-pgp-content {
- &.modal {
- width: 800px;
- }
-
- .key-list {
-
- background-color: #f9f9f9;
- border-radius: 5px;
- padding: 10px 15px;
-
- &-wrp {
-
- &:hover {
- overflow: auto;
- }
-
- &:hover .key-list__item-name {
- overflow: visible;
- }
-
- &.empty {
- text-align: center;
- padding-top: 10px;
- color: #aaa;
- font-size: 16px;
- }
- }
-
- &__item {
-
- color: #333;
- white-space: nowrap;
- padding-bottom: 4px;
- display: flex;
-
- &:last-child {
- padding-bottom: 0;
- }
-
- &-delete {
- cursor: pointer;
-
- &.disabled {
- cursor: not-allowed;
- }
- }
-
- &-names {
- color: #333;
- width: 80%;
-
- &.empty {
- color: red;
- }
- }
-
- &-name {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- &-error {
- color: red;
- width: 80%;
- }
-
- &-hash {
- color: #aaa;
- width: 20%;
- }
- }
- }
-
- .key-actions {
- margin-top: 10px;
- min-height: 40px;
-
- select option:nth-child(even) {
- background-color: rgba(128, 128, 128, 0.1);
- }
- }
-}
-
-.b-message-open-pgp-content {
- &.modal {
- width: 700px;
- }
-
- .key-list {
-
- margin-top: 5px;
- overflow: hidden;
-
- &__item {
-
- color: #555;
- cursor: pointer;
- text-overflow: ellipsis;
- white-space: nowrap;
-
- &__radio {
- padding: 3px 5px 0 0;
- vertical-align: top;
- }
-
- &__name {
- border-bottom: 1px solid transparent;
- }
-
- &__names {
- display: inline-block;
-
- &:hover .key-list__item__name {
- border-bottom: 1px dashed #555;
- }
- }
- }
- }
-}
-
-.b-open-pgp-key-add-content {
- &.modal {
- width: 645px;
- }
- .inputKey {
- font-family: var(--fontMono);
- width: 600px;
-
- }
-}
diff --git a/dev/Styles/Settings.less b/dev/Styles/Settings.less
deleted file mode 100644
index 06bffca4f..000000000
--- a/dev/Styles/Settings.less
+++ /dev/null
@@ -1,125 +0,0 @@
-
-.b-settins-left {
-
- .b-toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 34px;
- padding: 8px 0 0 @rlLowMargin;
- }
-
- .b-footer {
- position: absolute;
- bottom: 20px;
- right: 0;
- left: 0;
- height: 20px;
- padding: 0 10px 0 5px;
- z-index: 101;
- }
-
- .b-content {
- position: absolute;
- top: 50px + @rlLowMargin + 10px;
- bottom: @rlLowMargin;
- left: 0;
- right: 0;
- overflow: hidden;
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
- }
-}
-
-.b-settings-menu {
-
- .e-item {
- overflow: hidden;
- text-decoration: none;
- outline: 0;
- }
-
- .e-link {
- position: relative;
- display: block;
- height: 30px;
- line-height: 29px;
- font-size: 18px;
- z-index: 1;
- cursor: default;
-
- background-color: transparent;
- color: var(--settings-menu-disabled-color, #666);
-
- padding: 4px 10px;
-
- outline: 0;
- text-decoration: none;
- }
-
- .e-item.selectable .e-link {
- cursor: pointer;
- color: var(--settings-menu-color, #333);
- }
-
- .e-item.selectable {
- &:hover .e-link{
- background-color: var(--settings-menu-hover-bg-color, #333);
- color: var(--settings-menu-hover-color, #eee);
- }
- &.selected .e-link {
- background-color: var(--settings-menu-selected-bg-color, #333);
- color: var(--settings-menu-selected-color, #eee);
- }
- }
-}
-
-.b-settins-right {
-
- .b-toolbar {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- height: 34px;
- padding: 8px 5px;
- color: #fff;
- }
-
- .b-content {
- position: absolute;
- top: 50px;
- bottom: @rlLowMargin;
- left: 0;
- right: @rlLowMargin;
- overflow-y: auto;
- z-index: 2;
- scroll-behavior: smooth;
-
- background-color: #fff;
- border: @rlMainBorderSize solid @rlMainDarkColor;
- box-shadow: @rlMainShadow;
- border-radius: @rlMainBorderRadius;
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
- }
-
- td {
- padding: 4px 8px;
- line-height: 30px;
- }
-}
-
-html.rl-mobile .rl-settings-view-model {
- margin-right: 15px;
-}
-
-.firefox-drag .b-settins-right .b-content,
-.firefox-drag .modal-body {
- overflow: hidden;
-}
diff --git a/dev/Styles/SettingsAccounts.less b/dev/Styles/SettingsAccounts.less
deleted file mode 100644
index 1a4b69975..000000000
--- a/dev/Styles/SettingsAccounts.less
+++ /dev/null
@@ -1,57 +0,0 @@
-
-.b-settings-accounts {
-
- .list-table {
-
- .account-img, .identity-img {
- font-size: 12px;
- margin-right: 5px;
- }
-
- .account-name {
- display: inline-block;
- word-break: break-all;
- box-sizing: border-box;
- line-height: 22px;
- }
-
- .identity-default {
- cursor: pointer;
- color: #ccc;
- }
-
- .identity-name {
- display: inline-block;
- word-break: break-all;
- box-sizing: border-box;
- line-height: 22px;
- cursor: pointer;
- }
- }
-
- .accounts-list-top-padding, .identities-list-top-padding {
- display: inline-block;
- height: 5px;
- width: 5px;
- }
-
- .account-item {
-
- white-space: nowrap;
-
- .delete-account {
- cursor: pointer;
- opacity: 0.5;
- }
- }
-
- .identity-item {
-
- white-space: nowrap;
-
- .delete-identity {
- cursor: pointer;
- opacity: 0.5;
- }
- }
-}
diff --git a/dev/Styles/SettingsFolders.less b/dev/Styles/SettingsFolders.less
deleted file mode 100644
index 41b6b87c3..000000000
--- a/dev/Styles/SettingsFolders.less
+++ /dev/null
@@ -1,73 +0,0 @@
-
-.b-settings-folders {
-
- .folders-list-error {
- margin: 10px 0;
- }
-
- .list-table {
- margin-top: 1em;
-
- .folder-padding {
- display: inline-block;
- width: 0;
- }
-
- .folder-name {
- display: inline-block;
- word-break: break-all;
- white-space: pre-wrap;
- box-sizing: border-box;
- margin-left: 7px;
- }
-
- .folder-system-name {
- display: inline-block;
- opacity: 0.6;
- }
-
- .folder-name.can-be-edited:hover {
- text-decoration-style: dashed;
- cursor: pointer;
- }
- }
-
- .folder-item {
-
- &.system .folder-name {
- font-weight: bold;
- }
-
- input {
- border-width: 1px;
- margin-bottom: 0;
- }
-
- .button-delete {
- margin-right: 15px;
- margin-top: 5px;
- visibility: hidden;
- opacity: 0;
- }
-
- .unsubscribed-folder, .unchecked-folder {
- opacity: 0.4;
- }
- }
-
- .folder-padding.deep-1 {
- width: 25px;
- }
- .folder-padding.deep-2 {
- width: 40px;
- }
- .folder-padding.deep-3 {
- width: 55px;
- }
- .folder-padding.deep-4 {
- width: 70px;
- }
- .folder-padding.deep-5 {
- width: 85px;
- }
-}
diff --git a/dev/Styles/SettingsGeneral.less b/dev/Styles/SettingsGeneral.less
deleted file mode 100644
index a5f891dae..000000000
--- a/dev/Styles/SettingsGeneral.less
+++ /dev/null
@@ -1,19 +0,0 @@
-
-.b-settings-general {
-
- .flag-selector {
- padding-top: 5px;
- }
-
- .flag-name {
-
- border-bottom: 1px dashed #555;
- cursor: pointer;
- padding: 2px 0;
-
- &:focus {
- outline: 1px;
- outline-style: dotted;
- }
- }
-}
diff --git a/dev/Styles/SettingsOpenPGP.less b/dev/Styles/SettingsOpenPGP.less
deleted file mode 100644
index b483b56af..000000000
--- a/dev/Styles/SettingsOpenPGP.less
+++ /dev/null
@@ -1,36 +0,0 @@
-
-.b-settings-open-pgp {
-
- .list-table {
-
- .open-pgp-key-img {
- margin-right: 10px;
- vertical-align: top;
- }
-
- .open-pgp-key-id, .open-pgp-key-user {
- display: inline-block;
- word-break: break-all;
- box-sizing: border-box;
- line-height: 22px;
- cursor: default;
- }
-
- .open-pgp-key-user-address:first-child {
- line-height: 30px;
- margin-bottom: -4px;
- }
- }
-
- .open-pgp-key-item {
-
- .delete-open-pgp-key, .view-open-pgp-key {
- cursor: pointer;
- opacity: 0.7;
-
- &:hover {
- opacity: 0.9;
- }
- }
- }
-}
diff --git a/dev/Styles/SettingsTemplates.less b/dev/Styles/SettingsTemplates.less
deleted file mode 100644
index 74d02182f..000000000
--- a/dev/Styles/SettingsTemplates.less
+++ /dev/null
@@ -1,32 +0,0 @@
-
-.b-settings-templates {
-
- .list-table {
-
- .template-img {
- font-size: 12px;
- margin-right: 5px;
- }
-
- .template-name {
- display: inline-block;
- word-break: break-all;
- box-sizing: border-box;
- line-height: 22px;
- }
- }
-
- .templates-list-top-padding {
- display: inline-block;
- height: 5px;
- width: 5px;
- }
-
- .template-item {
-
- .delete-template {
- cursor: pointer;
- opacity: 0.5;
- }
- }
-}
diff --git a/dev/Styles/Shortcuts.less b/dev/Styles/Shortcuts.less
deleted file mode 100644
index ce6fca195..000000000
--- a/dev/Styles/Shortcuts.less
+++ /dev/null
@@ -1,7 +0,0 @@
-.rl-view-model {
- .b-shortcuts-content {
- &.modal {
- width: 700px;
- }
- }
-}
diff --git a/dev/Styles/SystemDropDown.less b/dev/Styles/SystemDropDown.less
deleted file mode 100644
index 3122d8040..000000000
--- a/dev/Styles/SystemDropDown.less
+++ /dev/null
@@ -1,173 +0,0 @@
-
-#top-system-dropdown-id {
- overflow: hidden;
- padding-left: 10px;
- padding-right: 10px;
-}
-
-.rl-left-panel-disabled #more-list-dropdown-id + .dropdown-menu {
- position: fixed;
- top: 40px;
-}
-
-.b-system-drop-down {
-
- .b-toolbar {
- position: absolute;
- top: 0;
- right: 0;
- height: 30px;
- padding: 10px @rlLowMargin;
- z-index: 103;
- }
-
- .email-title {
- display: inline-block;
- max-width: 200px;
- text-align: left;
- text-overflow: ellipsis;
- overflow: hidden;
- margin-right: 28px;
- vertical-align: middle;
- }
-
- .audioPlace {
- display: inline-block;
- font-size: 30px;
- height: 30px;
- margin-right: 10px;
- width: 1em;
-
- .playIcon, .stopIcon {
- cursor: pointer;
- color: orange;
- text-shadow: 0 1px 0 #555;
- }
-
- .stopIcon {
- display: none;
- }
-
- &:hover {
-
- .playIcon {
- display: none;
- }
-
- .stopIcon {
- display: inline-block;
- }
- }
- }
-
- .accountPlace {
- background-color: #000;
- background-color: rgba(0, 0, 0, 0.5);
-
- color: #fff;
- text-shadow: 0 1px 0 #000;
-
- display: inline-block;
- height: 29px;
- max-width: 250px;
- border-radius: 4px;
- font-size: 16px;
- line-height: 30px;
- padding: 1px 8px;
- overflow: hidden;
- text-overflow: ellipsis;
- border-radius: 4px;
- font-weight: bold;
-
- white-space: nowrap;
- }
-
- .account-item {
-
- .icon-ok {
- display: none;
- }
-
- &.current {
- .icon-ok {
- display: inline-block;
- }
- .icon-user {
- display: none;
- }
- }
- }
-
- .counter {
- display: inline-block;
- }
-
- .g-ui-menu .e-link.account-item {
- padding-right: 5px;
- }
-}
-
-/* mobile */
-@media screen and (max-width: 767px) {
- .accountPlace {
- max-width: 150px !important;
- }
-}
-
-@keyframes firstBar {
- 0% { height: 30%; }
- 50% { height: 100%; }
- 100% { height: 30%; }
-}
-
-@keyframes secondBar {
- 0% { height: 90%; }
- 50% { height: 30%; }
- 100% { height: 100%; }
-}
-
-@keyframes thirdBar {
- 0% { height: 20%; }
- 40% { height: 40%; }
- 60% { height: 80%; }
- 100% { height: 40%; }
-}
-
-.equaliser {
-
- margin-top: 5px;
- width: 30px;
- height: 20px;
- position: relative;
-
- .bar {
- width: 5px;
- height: 5px;
- background: orange;
- position: absolute;
- bottom:0;
- animation: none;
- }
-
- .first {
- left: calc(1em / 2 - 10px);
- }
- .second {
- left: calc(1em / 2 - 2px);
- }
- .third {
- left: calc(1em / 2 + 6px);
- }
-
- &.animated {
- .first {
- animation: firstBar 1s infinite;
- }
- .second {
- animation: secondBar 1s infinite;
- }
- .third {
- animation: thirdBar 1s infinite;
- }
- }
-}
diff --git a/dev/Styles/Template.less b/dev/Styles/Template.less
deleted file mode 100644
index 331dae1cf..000000000
--- a/dev/Styles/Template.less
+++ /dev/null
@@ -1,10 +0,0 @@
-.b-template-add-content {
-
- &.modal {
- width: 750px;
- }
-
- .e-template-place {
- height: 300px;
- }
-}
diff --git a/dev/Styles/Ui.less b/dev/Styles/Ui.less
index 86e39f5ca..6a7cb090e 100644
--- a/dev/Styles/Ui.less
+++ b/dev/Styles/Ui.less
@@ -1,14 +1,15 @@
+#rl-settings-subscreen,
.g-ui-user-select-none {
user-select: none;
-webkit-touch-callout: none;
}
.g-ui-link {
- color: #369;
- text-decoration: underline;
+ color: var(--link-color, #369);
cursor: pointer;
padding: 2px;
+ text-decoration: underline;
&:focus {
outline: 1px;
@@ -16,43 +17,17 @@
}
}
-.e-paginator {
-
- .e-page {
-
- display: inline-block;
- opacity: 0.5;
- text-decoration: none;
- font-size: 22px;
- padding: 3px;
- cursor: pointer;
-
- &:hover {
- opacity: 0.8;
- }
-
- &.current {
- opacity: 1;
- }
-
- &.current .e-page-number {
- font-size: 115%;
- text-decoration: underline;
- }
- }
-}
-
.settings-save-trigger {
display: inline-block;
height: 1em;
line-height: 1em;
+ margin-left: 0.5em;
&::after {
font-family: "snappymail";
content: " ";
display: block;
- margin-left: 1em;
opacity: 0;
transition: opacity 1s linear;
}
@@ -93,25 +68,6 @@ textarea + .settings-save-trigger {
}
}
-.e-languages {
-
- margin-top: 8px;
- color: #333;
-
- .flag-name {
-
- color: #333;
- border-bottom: 1px dashed #333;
- cursor: pointer;
- padding: 2px 0;
-
- &:focus {
- outline: 1px;
- outline-style: dotted;
- }
- }
-}
-
.e-action {
cursor: pointer;
}
@@ -119,3 +75,71 @@ textarea + .settings-save-trigger {
.list-table {
max-width: 800px;
}
+
+.button-confirm-delete {
+ transition: all 0.2s linear;
+ white-space: nowrap;
+}
+.button-confirm-delete:not(.delete-access) {
+ opacity: 0;
+ transform: translateX(-15px);
+ visibility: hidden;
+}
+
+.drag-handle {
+ color: #eee;
+ cursor: grab;
+}
+tr:hover .drag-handle {
+ color: #aaa;
+}
+
+// TABS
+// -------------
+
+.tabs {
+ display: grid;
+}
+
+.tabs input[type="radio"] {
+ position: absolute;
+ top: 0;
+ left: -9999px;
+ display: none;
+}
+
+// Actual tabs
+.tabs label {
+ border: 1px solid transparent;
+ border-radius: 4px 4px 0 0;
+ cursor: pointer;
+ grid-row-start: 1;
+ line-height: @baseLineHeight;
+ opacity: 0.6;
+ margin: 0 2px -1px 0;
+ padding: 5px;
+ text-align: center;
+ &:hover {
+ border-color: @grayLighter;
+ opacity: 0.8;
+ }
+}
+.tabs [id^="tab"]:checked + label {
+ background-color: @white;
+ border-color: #ddd #ddd transparent #ddd;
+ opacity: 1;
+ z-index: 1;
+}
+
+// TABBABLE
+// --------
+
+.tabs .tab-content {
+ grid-column-start: 1;
+ grid-row-start: 2;
+ visibility: hidden;
+}
+
+.tabs [id^="tab"]:checked + label + .tab-content {
+ visibility: visible;
+}
diff --git a/dev/Styles/User/AdvancedSearch.less b/dev/Styles/User/AdvancedSearch.less
new file mode 100644
index 000000000..e9043f598
--- /dev/null
+++ b/dev/Styles/User/AdvancedSearch.less
@@ -0,0 +1,11 @@
+#V-PopupsAdvancedSearch {
+
+ max-width: 780px;
+
+ label {
+ width: 110px;
+ }
+ .span4 {
+ width: 360px;
+ }
+}
diff --git a/dev/Styles/User/Animations.less b/dev/Styles/User/Animations.less
new file mode 100644
index 000000000..da155da61
--- /dev/null
+++ b/dev/Styles/User/Animations.less
@@ -0,0 +1,40 @@
+
+@keyframes highlight-folder-row {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
+}
+
+@keyframes animate-stripes {
+ 0% {background-position: 0 0;}
+ 100% {background-position: 60px 0;}
+}
+
+@media screen and (min-width: 1000px) {
+
+ .b-folders li .anim-action-class {
+ animation: highlight-folder-row 0.5s linear;
+ }
+
+ .b-folders .btn.buttonContacts {
+ transition: margin 0.3s linear;
+ }
+
+ #rl-left .b-content {
+ transition: opacity 0.3s linear;
+ }
+
+ .b-list-content {
+ .e-contact-item {
+ transition: max-height 400ms ease;
+ }
+ }
+
+ #V-PopupsCompose header.loading {
+ background-size: 60px 60px;
+ background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%,
+ transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%,
+ transparent 75%, transparent);
+ animation: animate-stripes 2s linear infinite;
+ }
+}
diff --git a/dev/Styles/Attachmnets.less b/dev/Styles/User/Attachmnets.less
similarity index 53%
rename from dev/Styles/Attachmnets.less
rename to dev/Styles/User/Attachmnets.less
index 85ddab889..783924f79 100644
--- a/dev/Styles/Attachmnets.less
+++ b/dev/Styles/User/Attachmnets.less
@@ -11,9 +11,6 @@
line-height: 24px;
border: 0;
background-color: rgba(128, 128, 128, 0.1);
-
- box-shadow: 0 1px 4px #ccc;
- box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04), 0 1px 5px rgba(0, 0, 0, 0.1);
border-radius: 3px;
@@ -23,8 +20,6 @@
}
&.checked {
- box-shadow: 0 1px 4px #00a;
- box-shadow: 0 1px 5px rgba(0, 0, 255, 0.3);
box-shadow: 0 0 0 1px rgba(0, 0, 255, 0.1), 0 1px 5px rgba(0, 0, 255, 0.2);
}
@@ -41,41 +36,40 @@
}
&.error {
- .attachmentIcon, .attachmentSize, .attachmentName {
+ .iconMain, .iconPreview, .attachmentSize, .attachmentName {
color: red;
}
}
.attachmentIconParent {
-
position: absolute;
height: 56px;
width: 60px;
text-align: center;
+ }
- .iconPreview, .iconBG, .iconMain, .iconProgress {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- }
+ .iconPreview, .iconBG, .iconMain, .iconProgress {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
- .iconProgress {
- background: #eee;
- width: 0%;
- }
+ .iconProgress {
+ background: #eee;
+ width: 0%;
+ }
- .iconBG {
- font-size: 18px;
- font-weight: bold;
- line-height: 55px;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8);
- }
+ .iconBG {
+ font-size: 18px;
+ font-weight: bold;
+ line-height: 55px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8);
+ }
- .iconPreview {
- display: none;
- }
+ .iconPreview {
+ display: none;
}
.attachmentNameParent {
@@ -91,11 +85,10 @@
border-left: 1px solid #ddd;
}
- .attachmentIcon {
- margin: 6px 0 0;
+ .iconMain, .iconPreview {
+ box-sizing: border-box;
+ padding: 6px 0 0;
font-size: 36px;
- width: 36px;
- height: 36px;
color: #aaa;
&.icon-none {
@@ -112,20 +105,20 @@
*/
.showPreview, .showPreplay,
- .attachmentIconParent.hasPreview:hover .iconMain,
- .attachmentIconParent.hasPreplay:hover .iconMain,
- .attachmentIconParent.hasPreview .hidePreview,
- .attachmentIconParent.hasPreplay .hidePreview {
+ .hasPreview:hover .iconMain,
+ .hasPreplay:hover .iconMain,
+ .hasPreview .hidePreview,
+ .hasPreplay .hidePreview {
display: none;
}
- .attachmentIconParent.hasPreview:hover .iconPreview,
- .attachmentIconParent.hasPreplay:hover .iconPreview {
+ .hasPreview:hover .iconPreview,
+ .hasPreplay:hover .iconPreview {
display: inline-block;
}
- .attachmentIconParent.hasPreview .showPreview,
- .attachmentIconParent.hasPreplay .showPreview {
+ .hasPreview .showPreview,
+ .hasPreplay .showPreplay {
display: inline;
cursor: pointer;
}
diff --git a/dev/Styles/User/Compose.less b/dev/Styles/User/Compose.less
new file mode 100644
index 000000000..c7294d7af
--- /dev/null
+++ b/dev/Styles/User/Compose.less
@@ -0,0 +1,173 @@
+
+.mailvelope-icon {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACjElEQVQ4y2XTXWjVdRgH8M/vf457OUVJzc7U5myWmysrFROKjFyUgqasEsNIrFFetC5KYkXMdVP0chne1MVWUBGWnlAKcogLJmiGXciUkaPtHPYSWumo6XbOv4v/afbyXP14+H6/z8vv+Qb/iNijEdFK4p1Yh1sQ4WccJe4h9Ae54t+ccJW88VpSb+NZdZWVmm+muR4xgwVODzM0dQWfEb0Q7L80KxDbmCH6irDO82uCHZvIVJMfT9Tn11Askevlnd7YdOk4xYeCg5PpBBG9R2gB9bXkjrD/GIXLSYlsBetX0NZK/cLgue41pLrxeIhtuZP4R6uyvPoEVdUcOMKH3/tf3FXDx6/x6de89W1MfH+qS2MnxdVynYyeZzDPM1sYHeH2LJMX2XAHjTfSN8zkOO1P8v6hQLQgjRZLarlhLk/vxTW0b6XjKaoqmJomjslUsrqPjn28tIOVi/lh+O4Ii61dQmEs2WnrUobHad3DR4eoruCxPXz+DQ+sSDAD51jbBDdFYKZ0dc4Hl5EvkJ/itiwjBfKXqath4tfyn8ekEmqEIf3nWJBFKRml/3QCzFzH4VPl/Dy+G0jyyxroOwMTafT6aazJxUlefpjXe7gwnbTa2c0v5ff5C2y+l/saSQdODCGcSnVpzJPaJT0ZtG+j9ySDvyeVfpvhSpwInDzL8vnJZb7yAaN/ILSl3nB2vEtTjRP5eyzKsHs7188wMZaQ5wQWVbFpFQ31bN/L6J+wL8i9W77Emd2kb9XxxSMK48HW9WzbwMhYsrDaeZRKfHkYUYzjlHb+x0ytVRTfxC51ldWaF7K0jjkpzuQZmDXTJxRfDA5e+pdAIrI5Il5OaEMLGsqYsp31EI4FB2bt/Bf/WNuibWZY5gAAAABJRU5ErkJggg==') center/contain no-repeat;
+ display: inline-block;
+ height: 1em;
+ width: 1em;
+}
+
+#V-PopupsCompose {
+ height: calc(100vh - 52px);
+ max-width: 1000px;
+ width: 98%;
+
+ .modal-body {
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .textAreaParent, .attachmentAreaParent {
+ box-sizing: border-box;
+ height: 100%;
+ min-height: 200px;
+ position: relative;
+ }
+
+ .textAreaParent {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .attachmentAreaParent {
+ border-top: 1px solid #ccc;
+ overflow-y: auto;
+ padding: 10px;
+ }
+
+ .attachmentName {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .no-attachments-desc {
+ padding-top: 50px;
+ text-align: center;
+ font-size: 24px;
+ color: #666;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ }
+
+ header {
+
+ background-color: rgba(0,0,0,0.8);
+
+ .close, .minimize-custom {
+ border-color: #eee;
+ float: none;
+ font-size: 24px;
+ opacity: 1;
+ line-height: 28px;
+ margin-left: .7em;
+ }
+
+ .btn.disabled {
+ &.button-delete {
+ visibility: hidden;
+ }
+ }
+
+ .button-save, .button-delete, .saved-text {
+ margin-left: 8px;
+ }
+
+ .disabled.button-delete {
+ margin-left: 0;
+ }
+
+ .pull-right * {
+ vertical-align: top;
+ }
+ }
+
+ .b-header {
+
+ padding: 10px;
+ background-color: rgba(128,128,128,0.2);;
+
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ td {
+ vertical-align: baseline;
+ padding: 2px 0;
+ }
+ td:first-child {
+ padding: 0 10px 0 0;
+ text-align: right;
+ white-space: nowrap;
+ width: 4em;
+ }
+
+ #identity-toggle {
+ color: var(--main-color);
+ margin-left: 6px;
+ text-decoration: none;
+ }
+ #identity-toggle::after {
+ content: '▼';
+ }
+
+ textarea, input[type="text"] {
+ width: 100%;
+ }
+ textarea {
+ height: 40px;
+ }
+
+ .error-desc {
+ color: red;
+ }
+
+ .error-to {
+ color: red;
+ font-weight: bold;
+ }
+ }
+
+ .b-attachment-button {
+ display: inline-block;
+ }
+
+ .b-attachment-place {
+
+ position: absolute;
+ left: 5px;
+ right: 5px;
+ top: 5px;
+ bottom: 5px;
+
+ border: 2px #777 dashed;
+ z-index: 300;
+
+ line-height: 119px;
+ text-align: center;
+ background-color: #efefef;
+ font-size: 24px;
+
+ border-radius: 10px;
+
+ &.dragAndDropOver {
+ background-color: #fff;
+ }
+ }
+}
+
+@media screen and (max-width: 999px) {
+ #V-PopupsCompose {
+ border: 0;
+ width: 100%;
+ }
+}
+
+.minimize-custom {
+ border: 0 solid #333;
+ border-bottom-width: 3px;
+ display: inline-block;
+ height: 20px;
+ width: 16px;
+ cursor: pointer;
+}
diff --git a/dev/Styles/Contacts.less b/dev/Styles/User/Contacts.less
similarity index 70%
rename from dev/Styles/Contacts.less
rename to dev/Styles/User/Contacts.less
index 4e5e43b17..620cf5b2f 100644
--- a/dev/Styles/Contacts.less
+++ b/dev/Styles/User/Contacts.less
@@ -1,36 +1,26 @@
-@contacts-popup-left-width: 220px;
+@contacts-popup-left-width: 25%;
-.b-contacts-content {
+#V-PopupsContacts {
+
+ bottom: 0;
+ width: auto;
+ min-width: 550px;
+ max-width: 900px;
+ min-height: 300px;
+ max-height: 700px;
.control-group {
- .control-label {
+ label {
padding-top: 0;
width: 50px;
}
- .controls {
- margin-left: 70px;
- }
- }
-
- &.modal {
-
- position: absolute;
- right: 0;
- top: 0;
- bottom: 0;
- left: 0;
- width: auto;
- min-width: 550px;
- max-width: 900px;
- min-height: 300px;
- max-height: 700px;
}
.modal-body {
- overflow: auto;
- height: calc(100% - 49px);
+ height: calc(100vh - 49px);
padding: 0;
+ position: relative;
}
.b-list-toolbar {
@@ -41,8 +31,8 @@
border-bottom: 1px solid rgba(128,128,128,0.4);
.e-search {
- margin-top: 7px;
- width: @contacts-popup-left-width - 20;
+ margin: 7px 0 0;
+ width: calc(100% - 20px);
}
}
@@ -71,14 +61,9 @@
overflow-y: auto;
scroll-behavior: smooth;
- .content {
- -webkit-overflow-scrolling: touch;
- }
-
.listClear {
text-align: center;
padding: 10px;
- font-size: 14px;
line-height: 13px;
box-shadow: inset 0 -1px 0 #ccc;
}
@@ -90,12 +75,6 @@
font-size: 24px;
line-height: 30px;
}
-
- &.hideContactListCheckbox {
- .checkboxItem {
- visibility: hidden;
- }
- }
}
.e-contact-item {
@@ -184,27 +163,13 @@
overflow: auto;
border-left: 1px solid rgba(128,128,128,0.25);
- .content {
- -webkit-overflow-scrolling: touch;
- }
-
- .contactValueStatic, .contactValueLargeStatic, .contactValueTextAreaStatic {
+ span {
height: 20px;
line-height: 20px;
- font-size: 18px;
- display: inline-block;
- padding: 5px 7px;
display: none;
- }
-
- &.read-only {
- .contactValueStatic, .contactValueLargeStatic, .contactValueTextAreaStatic {
- display: inline-block;
- }
-
- .contactValueInput, .contactValueInputLarge, .contactValueTextArea {
- display: none;
- }
+ padding: 5px 7px;
+ font-size: 18px;
+ display: none;
}
.b-contact-view-desc {
@@ -235,16 +200,12 @@
color: #aaa;
}
- .contactValueStatic, .contactValueLargeStatic, .contactValueTextAreaStatic {
- font-size: 18px;
- display: none;
- }
-
- .contactValueInput, .contactValueInputLarge, .contactValueTextArea {
+ input, textarea {
box-shadow: none;
border-color: #fff;
font-size: 18px;
- width: 300px;
+ width: 70vw;
+ max-width: 400px;
&:hover {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
@@ -257,16 +218,8 @@
}
}
- .contactValueTextArea {
- width: 300px;
- }
-
- .contactValueInputLarge {
- width: 400px;
- }
-
.hasError {
- .contactValueInput {
+ input {
color: #ee5f5b;
border-color: #ee5f5b;
}
@@ -293,28 +246,21 @@
}
&.read-only {
- .e-read-only-sign {
+ span, .e-read-only-sign {
display: inline-block;
}
- .e-share-sign {
+ input, textarea, .e-share-sign {
display: none;
}
}
}
}
-
-@media screen and (max-width: 700px) {
- .b-contacts-content {
- .b-list-content, .b-list-toolbar, .b-list-footer-toolbar {
- width: 170px;
- }
- .b-list-toolbar .e-search {
- width: 150px;
- }
- .b-view-content-toolbar, .b-view-content {
- left: 170px;
+html:not(rl-mobile) {
+ .hideContactListCheckbox {
+ .checkboxItem {
+ visibility: hidden;
}
}
}
diff --git a/dev/Styles/EmailAddresses.less b/dev/Styles/User/EmailAddresses.less
similarity index 92%
rename from dev/Styles/EmailAddresses.less
rename to dev/Styles/User/EmailAddresses.less
index 58f8da449..12015fbbc 100644
--- a/dev/Styles/EmailAddresses.less
+++ b/dev/Styles/User/EmailAddresses.less
@@ -13,8 +13,7 @@
}
.emailaddresses.emailaddresses-focused {
- background-color: #fff;
- border: @rlInputBorderSize solid darken(@inputBorder, 20%);
+ border: 1px solid darken(@inputBorder, 20%);
box-shadow: none;
}
@@ -49,7 +48,6 @@
.emailaddresses li a {
font-size: 12px;
color: #999;
- outline: 0;
padding: 1px;
position: absolute;
right: 2px;
@@ -73,8 +71,7 @@
height: auto;
line-height: inherit;
margin: 0;
- outline: 0;
- padding: 0;
+ padding: 4px;
vertical-align: baseline;
}
diff --git a/dev/Styles/Filter.less b/dev/Styles/User/Filter.less
similarity index 74%
rename from dev/Styles/Filter.less
rename to dev/Styles/User/Filter.less
index b0914440a..f36d45368 100644
--- a/dev/Styles/Filter.less
+++ b/dev/Styles/User/Filter.less
@@ -1,6 +1,6 @@
-.b-filter-content {
+#V-PopupsFilter {
- width: 750px;
+ max-width: 750px;
.button-delete {
cursor: pointer;
diff --git a/dev/Styles/FolderList.less b/dev/Styles/User/FolderList.less
similarity index 63%
rename from dev/Styles/FolderList.less
rename to dev/Styles/User/FolderList.less
index d07e361c1..97d8372bf 100644
--- a/dev/Styles/FolderList.less
+++ b/dev/Styles/User/FolderList.less
@@ -4,6 +4,16 @@
.b-folders {
+ > * {
+ position: absolute;
+ right: 0;
+ left: 0;
+ }
+
+ ul {
+ margin: 0;
+ }
+
.move-action-content-wrapper {
z-index: -1;
position: fixed;
@@ -19,17 +29,8 @@
transparent 75%, transparent);
}
- .b-folders-user {
- .e-link.print-count.system .badge {
- display: none !important;
- }
- }
-
.b-toolbar {
- position: absolute;
top: 0;
- right: 0;
- left: 0;
height: 30px;
padding: 10px 0 0 @rlLowMargin;
color: #fff;
@@ -37,10 +38,7 @@
}
.b-footer {
- position: absolute;
bottom: 10px;
- right: 0;
- left: 0;
height: 30px;
padding: 0 10px 0 5px;
z-index: 101;
@@ -50,32 +48,24 @@
}
.b-content {
- position: absolute;
- top: 50px + @rlLowMargin;
bottom: 32px + @rlLowMargin + @rlBottomMargin;
- left: 0;
- right: 0;
+ top: 50px + @rlLowMargin;
overflow: hidden;
overflow-y: auto;
min-width: 100px;
-
- .content {
- -webkit-overflow-scrolling: touch;
- }
}
hr {
margin: 10px;
- border-top: 0;
- border-bottom: 1px solid #999;
+ border-color: #999;
}
- .e-item {
-
+ li {
+ display: block;
overflow: hidden;
white-space: nowrap;
- .e-link {
+ a {
display: block;
position: relative;
z-index: 1;
@@ -84,35 +74,10 @@
background-color: transparent;
vertical-align: middle;
color: var(--folders-disabled-color, #666);
- cursor: not-allowed;
- font-size: 14px;
border-left: 3px solid transparent;
- .inbox-star-icon {
- display: none;
- margin-left: 7px;
+ padding: 0 2em 0 @folderItemPadding;
- .flagged {
- display: none;
- }
- .unflagged {
- opacity: .5;
-
- &:hover {
- opacity: 1;
- }
- }
- }
-
- &.is-inbox .inbox-star-icon {
- display: inline;
- }
-
- padding: 0;
- padding-left: @folderItemPadding;
- padding-right: @folderItemPadding;
-
- outline: 0;
text-decoration: none;
&.selectable {
@@ -144,31 +109,16 @@
color: var(--folders-selected-color, #eee);
}
}
+ &:not(.selectable) {
+ cursor: default;
+ font-style: italic;
+ }
&.focused {
background-color: #888;
border-left-color: #fff;
}
- &.system {
- cursor: default;
- color: grey;
- }
-
- .count {
- position: relative;
- display: none;
- margin-top: 5px;
- line-height: 19px;
- }
-
- &.print-count {
- font-weight: bold;
- .count {
- display: inline;
- }
- }
-
&.unread-sub {
font-weight: bold;
}
@@ -179,66 +129,90 @@
.e-collapsed-sign {
cursor: pointer;
+ font-size: 150%;
vertical-align: inherit;
}
}
- .b-sub-folders.collapsed {
+ a[data-unread] {
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ ul.collapsed {
max-height: 0;
height: 0;
display: none;
}
}
- &.inbox-is-starred {
- .flagged {
+ .b-folders-system a[data-unread]::after,
+ .b-folders-user a[data-unread]:not(.system)::after
+ {
+ content: attr(data-unread);
+ background-color: @grayLight;
+ border-radius: 9px;
+ color: @white;
+ font-size: 11px;
+ line-height: 19px;
+ margin-top: 5px;
+ min-width: 1em;
+ padding: 1px 4px;
+ text-align: center;
+ text-shadow: 0 -1px 0 rgba(0,0,0,.25);
+ position: absolute;
+ right: 3px;
+ }
+
+ .flag-icon {
+ margin-left: 7px;
+ }
+ .flag-icon::after {
+ content:'☆';
+ opacity: .5;
+ }
+ .is-flagged {
+ .flag-icon::after {
color: orange;
- display: inline-block !important;
- }
- .unflagged {
- display: none;
+ content:'★';
+ opacity: 1;
}
}
- .b-folder-system-item {
+ .b-folders-system {
font-weight: bold;
}
- .b-sub-folders .e-item .e-link {
+ li li a {
padding-left: @subPadding * 1 + @folderItemPadding;
}
- .b-sub-folders .b-sub-folders .e-item .e-link {
+ li li li a {
padding-left: @subPadding * 2 + @folderItemPadding;
}
- .b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
+ li li li li a {
padding-left: @subPadding * 3 + @folderItemPadding;
}
- .b-sub-folders .b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
+ li li li li li a {
padding-left: @subPadding * 4 + @folderItemPadding;
}
/**/
- &.single-root-inbox .i-am-inbox.e-link {
+ &.single-root-inbox .b-folders-user > li > a {
display: none !important;
}
- &.single-root-inbox .i-am-inbox-wrapper > .b-sub-folders {
- max-height: none !important;
- height: inherit !important;
- display: block !important;
- }
-
- &.single-root-inbox .i-am-inbox-wrapper {
- .b-sub-folders .e-item .e-link {
+ &.single-root-inbox {
+ li li a {
padding-left: @folderItemPadding;
}
- .b-sub-folders .b-sub-folders .e-item .e-link {
+ li li li a {
padding-left: @subPadding * 1 + @folderItemPadding;
}
- .b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
+ li li li li a {
padding-left: @subPadding * 2 + @folderItemPadding;
}
- .b-sub-folders .b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
+ li li li li li a {
padding-left: @subPadding * 3 + @folderItemPadding;
}
}
@@ -247,13 +221,12 @@
.btn {
&.buttonCompose {
- width: calc(~'100% - 85px');
+ width: calc(100% - 68px);
max-width: -moz-fit-content;
max-width: -webkit-fit-content;
max-width: fit-content;
overflow: hidden;
height: 19px;
- margin-top: 1px;
padding: 4px 8px;
}
}
@@ -273,3 +246,7 @@ html.rl-left-panel-disabled {
visibility: hidden;
}
}
+
+html.rl-mobile .b-folders .b-content {
+ bottom: 0;
+}
diff --git a/dev/Styles/Identity.less b/dev/Styles/User/Identity.less
similarity index 55%
rename from dev/Styles/Identity.less
rename to dev/Styles/User/Identity.less
index 2d1b25209..bc8d1210e 100644
--- a/dev/Styles/Identity.less
+++ b/dev/Styles/User/Identity.less
@@ -1,17 +1,10 @@
-.b-identity-content {
+#V-PopupsIdentity {
- &.modal {
- width: 770px;
- }
-
- .modal-body {
- overflow: hidden;
- }
+ max-width: 770px;
.e-signature-place {
border: 1px solid #ccc;
border-radius: 3px;
- height: 200px;
}
.textEmail {
diff --git a/dev/Styles/Layout.less b/dev/Styles/User/Layout.less
similarity index 56%
rename from dev/Styles/Layout.less
rename to dev/Styles/User/Layout.less
index 3d59c2d71..5563a3a00 100644
--- a/dev/Styles/Layout.less
+++ b/dev/Styles/User/Layout.less
@@ -5,7 +5,7 @@
background-position: center;
}
-#rl-center, #rl-top, #rl-left, #rl-right {
+#rl-left, #rl-right {
position: absolute;
top: 0;
right: 0;
@@ -17,23 +17,17 @@
z-index: 0;
}
-#rl-content {
- height: 100%;
- width: 100%;
+#rl-left {
+ width: @rlLeftWidth;
}
-#rl-center {
- min-width: 600px;
- min-height: 400px;
+#rl-right {
+ z-index: 1;
+ left: @rlLeftWidth;
}
html.rl-mobile {
- #rl-center {
- min-width: 250px;
- min-height: 250px;
- }
-
&:not(.rl-left-panel-disabled) #rl-right {
right: -150px;
}
@@ -43,15 +37,6 @@ html.rl-mobile {
}
}
-#rl-top {
- bottom: auto;
- z-index: 2;
-}
-
-#rl-left {
- width: @rlLeftWidth;
-}
-
/*
.resizable::after {
background-color: #aaa;
@@ -70,7 +55,7 @@ html.rl-mobile {
}
*/
-.resizable > .resizer {
+.resizer {
background: #aaa;
background: rgba(255,255,255,0.5);
display: none;
@@ -78,17 +63,19 @@ html.rl-mobile {
position: absolute;
z-index: 102;
}
-.resizable > .resizer:hover {
+.resizer:hover {
opacity: 1;
}
-html:not(.rl-left-panel-disabled) #rl-left.resizable {
+html:not(.rl-left-panel-disabled) #rl-left {
resize: horizontal;
overflow: hidden;
min-width: 155px;
max-width: 350px;
}
-#rl-left > .resizer {
+
+#rl-left > .resizer,
+.rl-side-preview-pane #V-MailMessageList > .resizer {
cursor: ew-resize;
height: 100%;
right: 0;
@@ -96,58 +83,17 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable {
width: 5px;
}
-.g-ui-menu {
- max-height: 60vh;
- max-width: 90vw;
- overflow: auto;
-
- .e-link {
- text-decoration: none;
- cursor: pointer;
- color: var(--dropdown-menu-color, #333);
- background-color: var(--dropdown-menu-bg-color, #fff);
- }
-
- .e-item.selected > .e-link {
- background-color: var(--dropdown-menu-selected-bg-color, #eee);
- }
-
- .e-item:not(.disabled) > .e-link:focus,
- .e-item:not(.disabled) > .e-link:hover {
- background-color: var(--dropdown-menu-hover-bg-color, #444);
- background-image: none;
- color: var(--dropdown-menu-hover-color, #eee);
- }
-
- .e-item.disabled > .e-link {
- cursor: not-allowed;
- opacity: 0.5;
- }
+.rl-side-preview-pane #V-MailMessageList {
+ resize: horizontal;
+ min-width: 320px;
+ max-width: 60%;
}
-
-.g-ui-table {
-
- display: table;
- width: 100%;
-
- .e-row {
- display: table-row;
- }
-
- .e-cell {
- display: table-cell;
- vertical-align: top;
- text-align: left;
- }
-}
-
-.b-message-list-wrapper.resizable {
+.rl-bottom-preview-pane #V-MailMessageList {
resize: vertical;
- overflow: hidden;
min-height: 200px;
- max-height: 500px;
+ max-height: 60%;
}
-.b-message-list-wrapper > .resizer {
+.rl-bottom-preview-pane #V-MailMessageList > .resizer {
cursor: ns-resize;
height: 5px;
left: 0;
@@ -155,30 +101,25 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable {
width: 100%;
}
-html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
-.b-message-list-wrapper > .resizer {
+html:not(.rl-left-panel-disabled) #rl-left > .resizer,
+#V-MailMessageList > .resizer {
display: block;
}
-#rl-right {
- z-index: 1;
- left: @rlLeftWidth;
-}
-
-#rl-sub-left {
+#V-MailMessageList {
position: absolute;
top: 0;
- bottom: 0;
+ bottom: @rlBottomMargin;
left: 0;
width: 50%;
}
-#rl-sub-right {
+#V-MailMessageView {
position: absolute;
- top: 0;
- bottom: 0;
+ top: 50px + @rlLowMargin;
+ bottom: 13px;
right: 0;
- left: 50%;
+ left: 0;
.b-message-view-backdrop {
position: absolute;
@@ -206,28 +147,10 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
}
}
-/*
- #rl-popups > dialog {
- top: 0;
- margin: 10px auto;
- padding: 0;
- border: 0;
+html.rl-side-preview-pane {
+ #V-MailMessageView {
+ left: 50%;
}
-*/
-#rl-popups > .rl-view-model {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1100;
- overflow: auto;
- background-color: rgba(0,0,0,0.3);
-// -webkit-overflow-scrolling: touch;
-}
-
-.b-settings-content {
- padding:20px 20px 20px 30px;
}
.dropdown-menu * + .dividerbar {
@@ -236,12 +159,8 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
border-top: 1px solid #e5e5e5;
}
-.dropdown.colored-toggle.open .btn.dropdown-toggle {
- color: #BD362F;
-
- .caret {
- border-top-color: #BD362F;
- }
+.dropdown.show {
+ box-shadow: 0 0 1px var(--main-color, @dropdownLinkColor);
}
.btn.btn-transparent {
@@ -264,7 +183,7 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
}
.btn-group > .btn.single {
- border-radius: @btnBorderRadius !important;
+ border-radius: 3px !important;
}
/* desktop */
@@ -281,12 +200,14 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
left: @rlLeftWidth + 20;
}
- #rl-sub-left {
+ #V-MailMessageList {
width: 40%;
}
- #rl-sub-right {
- left: 40%;
+ html.rl-side-preview-pane {
+ #V-MailMessageView {
+ left: 40%;
+ }
}
}
@@ -300,8 +221,8 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
left: 155px;
}
- .b-settings-content {
- padding: 10px 10px 10px 20px;
+ #rl-settings-subscreen {
+ padding: 10px;
}
.dropdown-menu a {
@@ -324,7 +245,7 @@ html.rl-left-panel-disabled {
display: block;
}
- .opacity-on-panel-disabled {
+ .b-content {
opacity: 0.3;
}
@@ -338,49 +259,32 @@ html.rl-left-panel-disabled {
}
}
-html.rl-mobile,
html.rl-no-preview-pane {
- #rl-sub-left {
+ #V-MailMessageList {
right: @rlBottomMargin !important;
width: inherit;
}
- #rl-sub-right {
- left: 0 !important;
- }
}
html.rl-mobile #rl-left > .resizer,
-html.rl-side-preview-pane:not(.rl-mobile) #rl-right .resizer {
+html.rl-no-preview-pane #rl-right .resizer {
display: none !important;
}
-html.rl-bottom-preview-pane:not(.rl-mobile) {
+html.rl-bottom-preview-pane {
- #rl-sub-left {
- right: @rlBottomMargin !important;
+ #V-MailMessageList {
+ bottom: inherit;
width: inherit;
-
- .b-message-list-wrapper {
- bottom: inherit;
- height: 300px;
-
- box-shadow: none;
- }
+ height: 300px;
+ right: 0;
}
- #rl-sub-right {
-
- left: 0 !important;
-
- .b-message-view-wrapper {
- top: 356px;
- left: 0;
- right: 5px;
- box-shadow: none;
- background-color: var(--message-bg-color, #fff);
- }
+ #V-MailMessageView {
+ left: 0;
+ top: 356px;
}
}
@@ -389,6 +293,23 @@ html:not(.rl-mobile) .show-mobile {
display: none !important;
}
-html.rl-mobile .width100-on-mobile {
- width: 100% !important;
+.e-paginator {
+
+ a {
+ opacity: 0.5;
+ text-decoration: none;
+ font-size: 22px;
+ padding: 3px;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &.current {
+ opacity: 1;
+ font-size: 25px;
+ cursor: default;
+ }
+ }
}
diff --git a/dev/Styles/User/MessageList.less b/dev/Styles/User/MessageList.less
new file mode 100644
index 000000000..d1e7312f2
--- /dev/null
+++ b/dev/Styles/User/MessageList.less
@@ -0,0 +1,445 @@
+
+html.rl-mobile,
+html.rl-no-preview-pane {
+ .message-selected #V-MailMessageList {
+ display: none;
+ }
+}
+
+#V-MailMessageList.focused .messageList {
+ border-color: #9d9d9d;
+}
+
+#sort-list-dropdown-id {
+ padding-left: 6px;
+ padding-right: 6px;
+}
+
+#V-MailMessageList .btn-toolbar {
+ height: 30px;
+ padding: 10px 1px;
+ white-space: nowrap;
+}
+
+.messageList {
+ height: calc(100% - 50px);
+ background-color: #fff;
+ border: 1px solid @rlMainDarkColor;
+ border-radius: @rlMainBorderRadius;
+ box-shadow: @rlMainShadow;
+ display: flex;
+ flex-direction: column;
+
+ .b-footer {
+ display: flex;
+ flex-shrink: 0;
+ padding: 7px;
+
+ background-color: var(--message-list-toolbar-bg-color, #eee);
+// #gradient > .vertical(#f4f4f4, #dfdfdf);
+
+ border-bottom-right-radius: @rlMainBorderRadius;
+ border-bottom-left-radius: @rlMainBorderRadius;
+ border-top: 1px solid #bbb;
+
+ .e-quota {
+ border-bottom: 1px dashed #333;
+ display: inline-block;
+ margin-top: 5px;
+ margin-left: 5px;
+ font-size: 18px;
+ cursor: help;
+ }
+ }
+
+ .b-footer nav {
+ flex-grow: 1;
+ text-align: right;
+ }
+
+ .btn.buttonMoreSearch {
+ font-size: 11px;
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+
+ .second-toolbar {
+ display: flex;
+ flex-shrink: 0;
+ padding: 10px 8px 10px 11px;
+ white-space: nowrap;
+
+ background-color: var(--message-list-toolbar-bg-color, #eee);
+// #gradient > .vertical(#f4f4f4, #dfdfdf);
+
+ border-top-right-radius: @rlMainBorderRadius;
+ border-top-left-radius: @rlMainBorderRadius;
+ border-bottom: 1px solid #bbb;
+ }
+
+ .checkboxCheckAll {
+ cursor: pointer;
+ margin: 0.45em 0.5em 0 0;
+ }
+
+ .search-input-wrp {
+ flex-grow: 1;
+ position: relative;
+ text-align: right;
+ }
+
+ .inputSearch {
+ max-width: 300px;
+ width: 100%;
+ }
+
+ .closeSearch {
+ position: absolute;
+ right: 0;
+ top: 5px;
+ margin: 0 7px;
+ z-index: 100;
+ vertical-align: middle;
+ opacity: .4;
+ &:hover {
+ opacity: .6;
+ }
+ }
+
+ .b-content {
+ height: 100%;
+ padding: 0;
+ overflow: auto;
+ scroll-behavior: smooth;
+
+ .listClear {
+ text-align: center;
+ padding: 10px;
+ line-height: 13px;
+ }
+
+ .listEmptyMessage, .listLoading, .listDragOver, .listError {
+ color: #999;
+ text-align: center;
+ padding: 60px 10px;
+ font-size: 24px;
+ line-height: 30px;
+ }
+
+ .listDragOver {
+ max-height: 0;
+ overflow: hidden;
+ padding: 0 10px;
+ }
+
+ .listDragOver.viewAppendArea {
+ max-height: 120px;
+ padding: 30px 10px;
+ }
+ .listDragOver.dragOverEnter {
+ background-color: #e0fdda;
+ color: #333;
+ }
+
+ .listError {
+ color: #DA4F49;
+ }
+
+ .listSearchDesc {
+ font-size: 16px;
+ padding: 12px;
+ border-bottom: 1px solid #eee;
+ }
+
+ .listThreadUidDesc {
+ font-size: 16px;
+ padding: 7px 20px 6px 20px;
+ background-color: rgba(128,128,128,0.5);
+ border-bottom: 1px solid #888;
+ color: #fff;
+ cursor: pointer;
+ text-shadow: 0 1px 0 #000;
+ text-align: center;
+ }
+
+ .fullThreadsParent {
+ height: 25px;
+ padding: 3px 5px;
+ background-color: #f4f4f4;
+ text-align: center;
+ }
+ }
+}
+
+html:not(rl-mobile) {
+ .hideMessageListCheckbox {
+ .checkboxCheckAll {
+ visibility: hidden;
+ }
+
+ .checkboxMessage {
+ display: none;
+ }
+ }
+}
+
+.messageListItem {
+ display: flex;
+ flex-wrap: wrap;
+
+ position: relative;
+ font-size: 12px;
+ overflow: hidden;
+ cursor: pointer;
+
+ margin: 0;
+ padding: 5px 0;
+ border: 0;
+ border-bottom: 1px solid rgba(153, 153, 153, 0.2);
+ border-left: 6px solid #eee;
+
+ z-index: 100;
+/*
+ > * {
+ display: flex;
+ flex: 0 0 auto;
+ order: 0;
+ }
+*/
+ &.focused {
+ background-color: rgba(128, 128, 128, 0.1);
+ border-left-color: #ccc;
+ }
+
+ .importantMark {
+ color:red;
+ margin-right:5px
+ }
+
+ &.deleted-mark {
+ opacity: .7;
+ .subjectParent {
+ text-decoration: line-through;
+ }
+ }
+
+ &.deleted {
+ opacity: .3;
+ }
+
+ .checkboxMessage {
+ padding: 0 6px;
+ font-size: 16px;
+ }
+
+ time, .sizeParent {
+ margin: 0 5px;
+ opacity: 0.6;
+ font-size: 11px;
+ white-space: nowrap;
+ }
+
+ .attachmentParent {
+ position: relative;
+ margin: 2px 10px 0 5px;
+ color: #666;
+ text-shadow: 0 1px 0 #eee;
+ }
+
+ .senderParent, .subjectParent {
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .threadsCountParent {
+ display: inline;
+ overflow: hidden;
+ background-color: #eee;
+ padding: 1px 5px;
+ margin-right: 5px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ }
+
+ .threadsCountParent.lastSelected {
+ background-color: #999;
+ border-color: #999;
+ color: #fff;
+ }
+
+ .threadsCountParent:hover {
+ border-color: #666;
+ }
+
+ &.emptySubject .subjectParent {
+ font-style: italic;
+ opacity: 0.5;
+ }
+
+ .threads-len span {
+ border-radius: 6px;
+ border: 1px solid #ccc;
+ font-size: 11px;
+ padding: 0 2px;
+ &:hover {
+ background-color: rgba(127,127,127,0.3);
+ border-color: #666;
+ }
+ }
+
+ .replyFlag, .forwardFlag {
+ margin-right: 0.25em;
+ }
+
+ &:not(.withAttachments) .attachmentParent {
+ display: none;
+ }
+
+ &.hasUnseenSubMessage {
+ background-color: rgba(255, 255, 64, 0.15);
+ border-left-color: lighten(orange, 30%);
+ &.focused {
+ border-left-color: darken(orange, 10%);
+ }
+ }
+
+ &.unseen {
+ background-color: rgba(255, 255, 64, 0.15);
+ border-left-color: orange;
+ .senderParent, .subjectParent {
+ font-weight: bold;
+ }
+
+ &.focused {
+ border-left-color: darken(orange, 10%);
+ }
+ }
+
+ &.checked {
+ border-left-color: lighten(#398CF2, 10%);
+
+ &.focused {
+ border-left-color: darken(#398CF2, 5%);
+ }
+ }
+
+ &.selected {
+ background-color: rgba(140, 200, 255, 0.3);
+ border-bottom-color: rgba(57, 140, 242, 0.2);
+ border-left-color: #398CF2;
+ z-index: 101;
+
+ + .messageListItem {
+ border-bottom-color: rgba(57, 140, 242, 0.3);
+ }
+ }
+
+ .flagParent {
+ padding: 0 10px 0 5px;
+ }
+ &.flagged .flagParent::after,
+ &.hasFlaggedSubMessage .flagParent::after {
+ color: orange;
+ content: '★'; /*⚑*/
+ }
+ &:not(.flagged):not(.hasFlaggedSubMessage) .flagParent::after {
+ content: '☆'; /*⚐*/
+ }
+ &:not(.flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
+ opacity: 0.5;
+ }
+}
+
+html.rl-ctrl-key-pressed .messageListItem {
+ cursor: copy;
+}
+
+@media screen and (min-width: 1000px) {
+ .messageList {
+ .listDragOver {
+ transition: all 400ms ease;
+ }
+ }
+}
+
+/* desktop-large */
+@media screen and (min-width: 1401px) {
+ .messageListItem {
+ font-size: 13px;
+ time {
+ font-size: 13px;
+ }
+ }
+}
+
+#messagesDragImage {
+ color: #fff;
+ background-color: #000;
+ height: 20px;
+ min-width: 30px;
+ padding: 4px 10px;
+ position: fixed;
+ right: -100px;
+ top: 0;
+}
+
+.rl-side-preview-pane, .rl-mobile {
+
+ .messageListItem {
+
+ .checkboxMessage {
+ line-height: 12px;
+ margin: 10px 0 -5px;
+ }
+
+ .subjectParent {
+ flex: 1 0 auto;
+ line-height: 16px;
+ order: 1;
+ width: calc(100% - 120px);
+ }
+
+ .senderParent {
+ flex: 1 0 45%;
+ }
+
+ .flagParent {
+ order: 1;
+ }
+
+ .sizeParent {
+ order: 2;
+ }
+
+ .attachmentParent {
+ order: 3;
+ }
+ }
+
+ .messageList:not(.hideMessageListCheckbox) .subjectParent {
+ margin-left: 28px;
+ }
+}
+
+html:not(.rl-mobile):not(.rl-side-preview-pane) {
+
+ .messageListItem {
+
+ line-height: 25px;
+ flex-wrap: nowrap;
+
+ > * {
+ line-height: 25px;
+ }
+
+ .senderParent {
+ flex: 0 0 25%;
+ }
+
+ .subjectParent {
+ flex: 1 1 auto;
+ }
+ }
+}
diff --git a/dev/Styles/User/MessageView.less b/dev/Styles/User/MessageView.less
new file mode 100644
index 000000000..4b016cb80
--- /dev/null
+++ b/dev/Styles/User/MessageView.less
@@ -0,0 +1,517 @@
+
+html.rl-no-preview-pane {
+ #rl-right:not(.message-selected) #V-MailMessageView {
+ display: none;
+ }
+}
+
+#V-MailMessageView.focused .messageView {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-color: darken(@rlMainDarkColor, 5%);
+}
+
+.messageView {
+ background-color: #fff;
+ border: 1px solid @rlMainDarkColor;
+ border-radius: @rlMainBorderRadius;
+ height: 100%;
+
+ .toolbar {
+ position: absolute;
+ top: -50px - @rlLowMargin;
+ right: 0;
+ left: 0;
+ height: 30px;
+ padding: 10px 0;
+ color: #fff;
+ }
+
+ .b-content {
+ height: 100%;
+ }
+
+ .b-message {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .b-message-view-checked-helper {
+ text-align: center;
+ font-size: 70px;
+ line-height: 70px;
+ padding-top: 100px;
+ color: #999;
+
+ .icon-mail {
+ font-size: 100px;
+ font-size: 50px;
+ line-height: 90px;
+ padding-left: 10px;
+ }
+ }
+
+ .b-message-view-desc {
+ text-align: center;
+ font-size: 24px;
+ line-height: 30px;
+ color: #999;
+ padding: 120px 10px 0 10px;
+ }
+
+ .b-message-view-desc.error {
+ color: #DA4F49;
+ }
+
+ .message-fixed-button-toolbar {
+ z-index: 100;
+ position: absolute;
+ top: 33px;
+ right: 10px;
+ }
+
+ .infoParent {
+ cursor: pointer;
+ opacity: 0.5;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .flagParent {
+
+ cursor: pointer;
+ margin: 0 0.25em;
+
+ .flagOn {
+ color: orange;
+ }
+
+ &.flagOff:not(:hover) {
+ opacity: 0.5;
+ }
+ }
+
+ .messageItemHeader {
+
+ background-color: #f8f8f8;
+ border-bottom: 1px solid #ddd;
+ border-radius: @rlMainBorderRadius @rlMainBorderRadius 0 0;
+ padding: 10px;
+ flex-shrink: 0;
+
+ .subjectParent {
+ display: flex;
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 8px;
+ }
+
+ .subject, .emptySubjectText {
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .messageButtons {
+ margin-top: 5px;
+ }
+
+ .informationShort {
+ margin: 4px 40px 0 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ a {
+ .g-ui-link;
+ }
+ }
+
+ .informationShortWrp {
+ max-height: 100px;
+ overflow-y: auto;
+ }
+
+ .informationFull {
+ margin-top: 30px;
+
+
+ table {
+ border-spacing: 0;
+ width: 100%;
+ }
+
+ td {
+ padding: 4px;
+ vertical-align: top;
+ min-width: 43px;
+ }
+
+ td:first-child {
+ border-right: 1px solid #999;
+ text-align: right;
+ white-space: nowrap;
+ width: 1%;
+ }
+
+ td + td {
+ word-break: break-all;
+ }
+ }
+
+ .emptySubjectText {
+ font-style: italic;
+ font-weight: normal;
+ color: #999;
+ }
+
+ .hasVirus {
+ background: #f00;
+ color: #fff;
+ padding: 3px;
+ }
+ }
+
+ .openpgp-control {
+ margin: 0.5em;
+ padding: 0.5em;
+
+ span {
+ margin-right: 1em;
+ }
+
+ button {
+ cursor: pointer;
+ }
+
+ &.encrypted {
+ border: 1px dashed #18F;
+ color: #18F;
+ }
+ &.signed {
+ border: 1px dashed #FA0;
+ color: #FA0;
+ }
+
+ &.success {
+ border-color: #090;
+ color: #090;
+ }
+ &.error {
+ border-color: #F00;
+ color: #F00;
+ }
+ &.error button,
+ &.success button {
+ display: none;
+ }
+ }
+ .b-text-part > iframe {
+ min-height: 50vh;
+ }
+
+ .thread-controls {
+ .dropdown-toggle {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+ &.open .dropdown-toggle {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+ }
+}
+
+#messageItem {
+
+ color: #000;
+ height: 100%;
+ overflow: auto;
+ scroll-behavior: smooth;
+
+ .buttonFull {
+ display: inline-block;
+ position: fixed;
+ right: 25px;
+ bottom: 25px;
+ height: 30px;
+ width: 30px;
+ text-align: center;
+ vertical-align: middle;
+ line-height: 30px;
+ background-color: transparent;
+ background-color: #fff;
+ border: 1px solid #333;
+ color: #333;
+ z-index: 2;
+ cursor: pointer;
+ border-radius: 5px;
+ opacity: 0.5;
+
+ &:hover {
+ opacity: 0.8;
+ border-color: #666;
+ background-color: #888;
+ color: #fff;
+ }
+ }
+
+ .loading {
+ text-align: center;
+ font-size: 24px;
+ color: grey;
+ padding: 50px 0;
+ }
+
+ .showImages, .readReceipt, .pgpSigned, .pgpEncrypted {
+ cursor: pointer;
+ padding: 10px 15px;
+ border-bottom: 1px solid #ddd;
+ background-color: #eee;
+ }
+
+ .pgpInfo {
+ padding: 5px 15px;
+ border-bottom: 1px solid #ddd;
+ background-color: #fcf8e3;
+
+ &.success {
+ background-color: #e9f4ff;
+ }
+ }
+
+ .readReceipt {
+ background-color: #ffffd9;
+ }
+
+ .attachmentsPlace {
+
+ padding: 10px 10px 6px 10px;
+ background: #eee;
+ border-bottom: 1px solid #ddd;
+ position: relative;
+
+ .attachmentList {
+ margin: 0;
+ }
+
+ &:not(.selection-mode) {
+ .checkboxAttachment {
+ display: none;
+ }
+ }
+
+ &.unselectedAttachmentsError {
+ .attachmentItem {
+ box-shadow: 0 1px 4px red;
+ box-shadow: 0 1px 5px rgba(255, 0, 0, 0.4);
+ box-shadow: 0 0 0 1px rgba(255, 0, 0, 0.2), 0 1px 5px rgba(255, 0, 0, 0.3);
+ }
+ }
+
+ .controls-handle {
+ position: absolute;
+ bottom: 5px;
+ right: 8px;
+ color: #999;
+ cursor: pointer;
+ }
+ }
+
+ .attachmentsControls {
+ padding: 7px 5px 7px 14px;
+ background: #e8e8e8;
+ border-bottom: 1px solid #ddd;
+ }
+
+ .rlBlockquoteSwitcher {
+ border: 1px solid #999;
+ display: block;
+ width: 3em;
+ line-height: 1em;
+ text-align: center;
+ cursor: pointer;
+ margin: 2em 0 10px;
+ opacity: 0.5;
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .b-text-part {
+
+ height: 100%;
+ padding: 10px;
+
+ a {
+ color: blue;
+ text-decoration: underline;
+
+ &:visited {
+ color: #609;
+ }
+ &:active {
+ color: red;
+ }
+ }
+
+ blockquote {
+ border-left: 2px solid #000;
+ opacity: 0.8;
+ padding: 0 10px;
+ margin: 0;
+ }
+
+ .rl-bq-switcher.hidden-bq {
+ display: none;
+ }
+
+ &.html {
+
+ img {
+ max-width: 90vw;
+ }
+ img[data-x-src]:not([src]) {
+ border: 1px solid #999;
+ position: relative;
+ }
+ img[data-x-src]:not([src])::after {
+ content: "🖼";
+ font-family: snappymail;
+ position: absolute;
+ top: 0;
+ left: 0;
+ color: #000;
+ background-color: #fff;
+ }
+ img[data-x-src]:not([src]):hover::after {
+ content: attr(data-x-src);
+ font-family: var(--fontSans);
+ font-size: 11px;
+ height: auto;
+ transform: translate(-25%,0);
+ width: auto;
+ width: -moz-fit-content;
+ width: -webkit-fit-content;
+ width: fit-content;
+ z-index: 1000;
+ }
+
+ pre, code {
+ margin: 0;
+ border: none;
+ word-break: normal;
+ word-wrap: break-word;
+ }
+
+ code {
+ border-radius: 0;
+ display: inline;
+ padding: 2px 5px;
+ }
+
+ pre {
+ border-radius: 5px;
+ display: block;
+ padding: 5px 10px;
+ }
+
+ pre > code {
+ padding: 0;
+ }
+ }
+
+ &.plain {
+
+ white-space: pre-wrap;
+ font-family: var(--fontMono);
+
+ pre {
+ margin: 0;
+ padding: 0;
+ border: none;
+ display: block;
+ word-break: normal;
+ }
+ }
+
+/*
+ &.openpgp-signed,
+ &.openpgp-encrypted {
+ border: 1px dashed #FA0;
+
+ &.success {
+ border-color: green;
+ background-color: rgba(0, 255, 0, 0.03);
+ }
+ &.error {
+ border-color: red;
+ background-color: rgba(255, 0, 0, 0.03);
+ }
+ }
+*/
+ }
+}
+
+html.rl-no-preview-pane .messageView {
+
+ .toolbar {
+ padding-left: 1px;
+ }
+
+ .b-content {
+ top: 50px;
+ left: 0;
+ bottom: @rlBottomMargin;
+ right: @rlBottomMargin;
+ border: 1px solid @rlMainDarkColor;
+ box-shadow: @rlMainShadow;
+ }
+}
+
+html:not(.rl-mobile):not(.rl-no-preview-pane) .messageView {
+ .top-toolbar {
+ display: none;
+ }
+}
+
+html.rl-bottom-preview-pane .messageView {
+ .b-content {
+ bottom: @rlBottomMargin;
+ }
+}
+
+html.rl-message-fullscreen {
+
+ #rl-left {
+ display: none !important;
+ }
+
+ #rl-right {
+ #V-MailMessageList,
+ #V-SettingsPane,
+ #V-SystemDropDown,
+ #V-MailMessageView .messageView .toolbar {
+ display: none !important;
+ }
+ }
+
+ .messageView .b-content {
+ position: fixed !important;
+ margin: 5px !important;
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+ z-index: 10000 !important;
+ border: 1px solid @rlMainDarkColor !important;
+ }
+}
+
+html:not(.rl-mobile) #messageItem {
+ .buttonFull {
+ display: none !important;
+ }
+}
diff --git a/dev/Styles/User/OpenPgpKey.less b/dev/Styles/User/OpenPgpKey.less
new file mode 100644
index 000000000..c318b157d
--- /dev/null
+++ b/dev/Styles/User/OpenPgpKey.less
@@ -0,0 +1,21 @@
+#V-PopupsOpenPgpKey, #V-PopupsOpenPgpGenerate {
+ max-width: 570px;
+}
+
+#V-PopupsOpenPgpKey {
+ .key-viewer {
+ margin: 0;
+ max-height: 500px;
+ overflow: auto;
+ }
+}
+
+#V-PopupsOpenPgpImport {
+ max-width: 645px;
+
+ .inputKey {
+ font-family: var(--fontMono);
+ width: 600px;
+
+ }
+}
diff --git a/dev/Styles/User/Settings.less b/dev/Styles/User/Settings.less
new file mode 100644
index 000000000..aaca64723
--- /dev/null
+++ b/dev/Styles/User/Settings.less
@@ -0,0 +1,86 @@
+
+#V-SettingsMenu {
+
+ .b-footer {
+ position: absolute;
+ bottom: 10px;
+ right: 0;
+ left: 0;
+ padding: 0 10px 0 5px;
+ z-index: 101;
+ }
+
+ .b-content {
+ position: absolute;
+ top: 50px + @rlLowMargin + 10px;
+ bottom: @rlLowMargin;
+ left: 0;
+ right: 0;
+ overflow: hidden;
+ }
+
+ nav {
+
+ a {
+ background-color: transparent;
+ color: var(--settings-menu-color, #333);
+ cursor: pointer;
+ display: block;
+ font-size: 18px;
+ height: 30px;
+ line-height: 29px;
+ overflow: hidden;
+ padding: 4px 10px;
+ text-decoration: none;
+ }
+
+ a:focus, a:hover {
+ background-color: var(--settings-menu-hover-bg-color, #333);
+ color: var(--settings-menu-hover-color, #eee);
+ }
+
+ a.selected {
+ background-color: var(--settings-menu-selected-bg-color, #333);
+ color: var(--settings-menu-selected-color, #eee);
+ }
+ }
+}
+
+#V-SettingsPane {
+ height: 100%;
+
+ .btn-toolbar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 34px;
+ padding: 8px 0;
+ color: #fff;
+ }
+
+ td {
+ padding: 4px 8px;
+ line-height: 30px;
+ }
+}
+
+#rl-settings-subscreen {
+ margin: 50px @rlLowMargin @rlLowMargin 0;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+ padding:20px;
+
+ box-sizing: border-box;
+ height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
+
+ background-color: #fff;
+ border: 1px solid @rlMainDarkColor;
+ box-shadow: @rlMainShadow;
+ border-radius: @rlMainBorderRadius;
+}
+
+.firefox-drag #rl-settings-subscreen,
+.firefox-drag .modal-body {
+ overflow: hidden;
+}
diff --git a/dev/Styles/User/SettingsAccounts.less b/dev/Styles/User/SettingsAccounts.less
new file mode 100644
index 000000000..b83ee1a3c
--- /dev/null
+++ b/dev/Styles/User/SettingsAccounts.less
@@ -0,0 +1,48 @@
+
+#V-Settings-Accounts {
+
+ .account-name {
+ display: inline-block;
+ word-break: break-all;
+ }
+
+ .identity-default {
+ cursor: pointer;
+ color: #ccc;
+ }
+
+ .identity-name {
+ display: inline-block;
+ word-break: break-all;
+ cursor: pointer;
+ }
+
+ table {
+ margin-top: 1em;
+ }
+
+ tr {
+ white-space: nowrap;
+ }
+
+ td + td + td {
+ text-align: right;
+ }
+ td:first-child,
+ td:last-child, {
+ width: 1em;
+ }
+
+ .delete {
+ cursor: pointer;
+ opacity: 0.5;
+ }
+
+ .identity-default::before {
+ content: '(';
+ }
+ .identity-default::after {
+ content: ')';
+ }
+
+}
diff --git a/dev/Styles/SettingsFilters.less b/dev/Styles/User/SettingsFilters.less
similarity index 75%
rename from dev/Styles/SettingsFilters.less
rename to dev/Styles/User/SettingsFilters.less
index afca4b2c3..ef70221b8 100644
--- a/dev/Styles/SettingsFilters.less
+++ b/dev/Styles/User/SettingsFilters.less
@@ -1,16 +1,16 @@
-.b-filter-script {
+#V-PopupsSieveScript {
width: auto;
max-width: 720px;
}
-.filter-item td.drag-wrapper {
- padding: 4px 0;
+#V-PopupsSieveScript .filter-item,
+#V-Settings-Filters .script-item {
+ white-space: nowrap;
}
-.b-filter-script .filter-item,
-.b-settings-filters .script-item {
- white-space: nowrap;
+.filter-item td.drag-wrapper {
+ padding: 4px 0;
}
.filter-item .delete-filter {
@@ -22,7 +22,6 @@
.filter-item .filter-sub-name {
display: inline-block;
word-break: break-all;
- box-sizing: border-box;
line-height: 22px;
cursor: pointer;
}
@@ -31,7 +30,7 @@
color: #aaa;
}
-.b-filter-script textarea {
+#V-PopupsSieveScript textarea {
height: 300px;
font-family: var(--fontMono);
}
diff --git a/dev/Styles/User/SettingsFolders.less b/dev/Styles/User/SettingsFolders.less
new file mode 100644
index 000000000..6cf0fd517
--- /dev/null
+++ b/dev/Styles/User/SettingsFolders.less
@@ -0,0 +1,70 @@
+
+#V-Settings-Folders {
+
+ .folders-list-error {
+ margin: 10px 0;
+ }
+
+ table {
+ margin-top: 1em;
+ }
+
+ td + td {
+ white-space: nowrap;
+ width: 1em;
+ }
+
+ .folder-name {
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
+
+ .folder-system-name {
+ opacity: 0.6;
+ }
+
+ .folder-name.can-be-edited:hover {
+ text-decoration-style: dashed;
+ cursor: pointer;
+ }
+
+ select {
+ width: auto;
+ }
+
+ tr {
+
+ &:not(.selectable) {
+ font-style: italic;
+ }
+
+ &.system .folder-name {
+ font-weight: bold;
+ }
+
+ input {
+ border-width: 1px;
+ margin-bottom: 0;
+ }
+
+ .unsubscribed-folder, .unchecked-folder {
+ opacity: 0.4;
+ }
+ }
+
+ .deep-1 {
+ text-indent: 15px;
+ }
+ .deep-2 {
+ text-indent: 30px;
+ }
+ .deep-3 {
+ text-indent: 45px;
+ }
+ .deep-4 {
+ text-indent: 60px;
+ }
+ .deep-5 {
+ text-indent: 75px;
+ }
+}
diff --git a/dev/Styles/User/SettingsGeneral.less b/dev/Styles/User/SettingsGeneral.less
new file mode 100644
index 000000000..b5832f09c
--- /dev/null
+++ b/dev/Styles/User/SettingsGeneral.less
@@ -0,0 +1,9 @@
+
+#V-Settings-General {
+ .editMainIdentity {
+ border-bottom: 1px dashed #555;
+ cursor: pointer;
+ display: inline-block;
+ padding: 5px 0;
+ }
+}
diff --git a/dev/Styles/User/SettingsOpenPGP.less b/dev/Styles/User/SettingsOpenPGP.less
new file mode 100644
index 000000000..9274d6867
--- /dev/null
+++ b/dev/Styles/User/SettingsOpenPGP.less
@@ -0,0 +1,23 @@
+
+#V-Settings-OpenPgp {
+
+ td * {
+ cursor: pointer;
+ }
+ td + td {
+ width: 1%;
+ }
+
+ .key-user {
+ margin: 0 0.5em;
+ white-space: nowrap;
+ }
+
+ .delete-key:not(:hover) {
+ opacity: 0.7;
+ }
+
+ iframe {
+ max-width: 991px;
+ }
+}
diff --git a/dev/Styles/SettingsThemes.less b/dev/Styles/User/SettingsThemes.less
similarity index 60%
rename from dev/Styles/SettingsThemes.less
rename to dev/Styles/User/SettingsThemes.less
index 8042f6686..9b26a27cd 100644
--- a/dev/Styles/SettingsThemes.less
+++ b/dev/Styles/User/SettingsThemes.less
@@ -1,13 +1,14 @@
-.b-themes-list {
+#V-Settings-Themes {
- .e-item {
+ figure {
display: inline-block;
border: 2px solid transparent;
cursor: pointer;
- padding: 16px;
+ padding: 8px;
margin: 5px;
+ width: 128px;
&:hover {
border-color: grey;
@@ -18,9 +19,13 @@
border-color: #000;
}
- .e-image {
- width: 100px;
- height: 100px;
+ figcaption {
+ padding: 0 0 8px 0;
+ text-align: center;
+ }
+
+ img {
+ height: 128px;
}
}
}
diff --git a/dev/Styles/User/Shortcuts.less b/dev/Styles/User/Shortcuts.less
new file mode 100644
index 000000000..1605d89c6
--- /dev/null
+++ b/dev/Styles/User/Shortcuts.less
@@ -0,0 +1,11 @@
+#V-PopupsKeyboardShortcutsHelp {
+ max-width: 700px;
+
+ td[class^="icon-"] {
+ display: table-cell;
+ }
+
+ .tab-content {
+ grid-column-end: 6;
+ }
+}
diff --git a/dev/Styles/SquireUI.less b/dev/Styles/User/SquireUI.less
similarity index 80%
rename from dev/Styles/SquireUI.less
rename to dev/Styles/User/SquireUI.less
index 1e79d40fd..72f290898 100644
--- a/dev/Styles/SquireUI.less
+++ b/dev/Styles/User/SquireUI.less
@@ -1,10 +1,10 @@
.squire-toolbar {
- padding: 2px;
border-bottom: 1px solid #b6b6b6;
- background: #EEE;
+ min-height: 28px;
overflow-x: auto;
overflow-y: hidden;
+ padding: 2px;
white-space: nowrap;
}
@@ -39,6 +39,7 @@
.squire-wysiwyg, .squire-plain {
box-sizing: border-box;
font-size: 13px;
+ height: 100%;
line-height: 16px;
min-height: 200px;
overflow: auto;
@@ -63,16 +64,14 @@
}
pre, code {
- margin: 0;
- padding: 0;
- background: #fff;
border: none;
border-radius: 0;
- font-family: var(--fontMono);
display: block;
+ font-family: var(--fontMono);
+ margin: 0;
+ padding: 0;
word-break: normal;
word-wrap: break-word;
- background-color: #f9f9f9;
}
code {
@@ -81,9 +80,8 @@
}
pre {
- padding: 5px 10px;
border-radius: 5px;
- background-color: #f9f9f9;
+ padding: 5px 10px;
}
pre > code {
@@ -92,32 +90,19 @@
blockquote {
border: 0;
- border-left: solid 2px #444;
+ border-left: 2px solid #444;
margin: 5px 0 5px 5px;
padding-left: 5px;
}
blockquote p {
margin: 0 0 10px;
- font-size: 14px;
line-height: 20px;
}
img {
vertical-align: bottom;
}
-
- a {
- color: blue;
- text-decoration: underline;
-
- &:visited {
- color: #609;
- }
- &:active {
- color: red;
- }
- }
}
/* This does make the block element focusable with mouse in Gecko and Webkit so we don't need a .
@@ -138,41 +123,49 @@ Secondly, we can't rely on MUA's what to do with :empty
*/
.squire-plain {
- background-color: #fff;
border: 0;
border-radius: 0;
display: none;
font-family: var(--fontMono);
- outline: none;
margin: 0;
resize: none;
white-space: pre-wrap;
width: 100%;
}
+
+.squire-mode-wysiwyg .squire-plain,
.squire-mode-plain .squire-wysiwyg,
.squire-mode-plain .btn-group:not(#squire-toolgroup-mode) {
display: none;
}
+
.squire-mode-plain .squire-plain {
display: block;
}
/* @media (hover: none) */
-.rl-mobile .textAreaParent:not(:focus-within) .squire-toolbar {
- display: none;
-}
-.rl-mobile .squire-toolbar {
- padding: 1px;
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
+@media screen and (max-height: 600px) {
+ .rl-mobile .textAreaParent:not(:focus-within) .squire-toolbar {
+ display: none;
+ }
+ .rl-mobile .squire-toolbar {
+ background: #EEE;
+ padding: 1px;
+ position: fixed;
+ top: 18px;
+ left: 0;
+ right: 0;
+ /*
+ transform: translateY(-42px);
+ z-index: 9;
+ */
+ }
}
-.rl-mobile .b-compose.modal,
-.rl-mobile .b-identity-content.modal {
- margin-bottom: 50px;
+.rl-mobile #V-PopupsCompose {
+ min-height: calc(100% - 12px);
}
-.rl-mobile .b-compose.modal {
- min-height: calc(100% - 62px);
+
+#V-PopupsCompose[data-wysiwyg*=Forced] #squire-toolgroup-mode {
+ display: none;
}
diff --git a/dev/Styles/User/SystemDropDown.less b/dev/Styles/User/SystemDropDown.less
new file mode 100644
index 000000000..5f0ff170f
--- /dev/null
+++ b/dev/Styles/User/SystemDropDown.less
@@ -0,0 +1,113 @@
+
+#top-system-dropdown-id {
+ overflow: hidden;
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+.rl-left-panel-disabled #more-list-dropdown-id + .dropdown-menu {
+ position: fixed;
+ top: 40px;
+}
+
+#V-SystemDropDown {
+
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 30px;
+ padding: 10px @rlLowMargin;
+ z-index: 103;
+
+ .email-title {
+ display: inline-block;
+ max-width: 50vw;
+ text-align: left;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin-right: 28px;
+ vertical-align: middle;
+ }
+
+ .audioPlace {
+ cursor: pointer;
+ display: inline-block;
+ font-size: 30px;
+ height: 30px;
+ margin-right: 10px;
+ width: 1em;
+
+ .stopIcon {
+ color: orange;
+ display: none;
+ }
+
+ &:hover {
+
+ .playIcon {
+ display: none;
+ }
+
+ .stopIcon {
+ display: inline-block;
+ }
+ }
+ }
+
+ .accountPlace {
+ background-color: rgba(0, 0, 0, 0.5);
+ border-radius: 4px;
+ color: #fff;
+ display: inline-block;
+ font-weight: bold;
+ font-size: 16px;
+ height: 29px;
+ line-height: 30px;
+ max-width: 25vw;
+ overflow: hidden;
+ padding: 1px 8px;
+ text-overflow: ellipsis;
+ text-shadow: 0 1px 0 #000;
+ white-space: nowrap;
+ }
+
+ .counter {
+ display: inline-block;
+ }
+
+ .dropdown-menu a.account-item {
+ padding-right: 5px;
+ }
+}
+
+@keyframes equaliserBar {
+ 0% { top: 30%; }
+ 50% { top: 100%; }
+ 100% { top: 30%; }
+}
+
+.playIcon {
+
+ margin-top: 5px;
+ height: 20px;
+ position: relative;
+
+ *, &::before, &::after {
+ animation: equaliserBar 1s infinite;
+ background: orange;
+ bottom:0;
+ left: calc(1em / 2 - 2px);
+ position: absolute;
+ width: 5px;
+ content: '';
+ }
+
+ &::before {
+ left: calc(1em / 2 - 9px);
+ animation-delay: -0.33s;
+ }
+ &::after {
+ left: calc(1em / 2 + 5px);
+ animation-delay: -0.66s;
+ }
+}
diff --git a/dev/Styles/_BootstrapFix.less b/dev/Styles/_BootstrapFix.less
index 460157a94..e2c0b51d1 100644
--- a/dev/Styles/_BootstrapFix.less
+++ b/dev/Styles/_BootstrapFix.less
@@ -1,99 +1,49 @@
-label.inline, span.inline {
- display: inline-block;
-}
-
.legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: @baseLineHeight;
font-size: @baseFontSize * 1.5;
- line-height: @baseLineHeight * 2;
+ line-height: @baseLineHeight * 1.5;
color: @grayDark;
border: 0;
- border-bottom: 1px solid #e5e5e5;
-
- small {
- font-size: @baseLineHeight * .75;
- color: @grayLight;
- }
+ border-bottom: 1px solid @dropdownDividerTop;
}
-
-.legend + .control-group {
- margin-top: @baseLineHeight;
- -webkit-margin-top-collapse: separate;
-}
-
-.btn-small.btn-small-small {
- font-size: 11px;
- line-height: 11px;
+summary.legend {
+ display: list-item;
+ cursor: pointer;
}
.btn.btn-thin {
padding: 4px 9px;
}
-.btn.btn-thin-2 {
- padding: 4px 7px;
-}
-
-.alert a {
+.alert a:not(.close) {
text-decoration: underline;
- color: #c09853;
+ color: @warningText;
}
-.alert.alert-info a {
- color: #3a87ad;
+.alert.alert-info a:not(.close) {
+ color: @infoText;
}
-.alert.alert-error a {
- color: #b94a48;
-}
-
-.form-horizontal.long-label .control-group {
- .control-label {
- width: 160px;
- }
+.alert.alert-error a:not(.close) {
+ color: @errorText;
}
/* mobile and tablet */
@media screen and (max-width: 999px) {
-
- .modal {
- width: calc(~'100% - 20px') !important;
- }
-
.form-horizontal {
.control-group {
- .control-label {
+ > label {
text-align: left;
}
- .controls {
- margin-left: 10px;
+
+ // Move over all input controls and content
+ > *:not(label) {
+ margin-left: 20px;
}
}
}
}
-
-.close-input-wrp {
- display: inline-block;
- position: relative;
-
- input {
- padding-right: 15px;
- }
-
- .close {
- position: absolute;
- right: 0;
- top: 5px;
- margin: 0 7px;
- z-index: 100;
- vertical-align: middle;
- opacity: .4;
- &:hover {
- opacity: .6;
- }
- }
-}
diff --git a/dev/Styles/_End.less b/dev/Styles/_End.less
index a3fb20f95..91f0b998e 100644
--- a/dev/Styles/_End.less
+++ b/dev/Styles/_End.less
@@ -17,7 +17,7 @@
top: 100%;
transition: opacity 0.2s ease-in-out;
white-space: nowrap;
- z-index: 2001 !important;
+ z-index: 2001;
}
@media only screen and (max-width: 600px) {
[data-rainloopErrorTip]::before {
@@ -25,18 +25,6 @@
white-space: normal;
}
}
-[data-rainloopErrorTip]::after {
- border-color: transparent transparent #999 transparent;
- border-style: solid;
- border-width: 5px;
- content: "";
- margin: -5px;
- max-width: 80vw;
- position: absolute;
- top: 100%;
- transform: rotate(-45deg);
- z-index: 2002;
-}
input:-webkit-autofill,
input:-webkit-autofill:hover,
diff --git a/dev/Styles/_FontasticToBoot.less b/dev/Styles/_FontasticToBoot.less
index 1692a00e5..92a7645b3 100644
--- a/dev/Styles/_FontasticToBoot.less
+++ b/dev/Styles/_FontasticToBoot.less
@@ -9,8 +9,6 @@
[class*=" icon-"] {
display: inline-block;
width: 1em;
- height: 1em;
- line-height: 1;
vertical-align: text-top;
.disabled &,
@@ -38,8 +36,8 @@
.icon-spinner {
- height: 10px;
- width: 10px;
+ height: 0.75em;
+ width: 0.75em;
margin: 0 -1px;
@@ -48,27 +46,18 @@
border-radius: 100%;
}
-.btn-success .icon-spinner {
- border-color: #fff;
- border-top-color: #999;
- &.animated {
- border-color: transparent;
- border-top-color: #fff;
- }
-}
-
-.btn-large .icon-spinner {
- height: 13px;
- width: 13px;
- margin: -1px -2px;
-}
-
-.icon-spinner:not(.not-animated) {
+.icon-spinner:not(.not-animated),
+.list-loading .icon-spinner.not-animated {
border-color: transparent;
border-top-color: #999;
animation: rotation .8s infinite ease-in-out;
}
+.btn.btn-success .icon-spinner {
+ border-color: transparent;
+ border-top-color: #fff;
+}
+
.fontastic.icon-spinner {
text-indent: -5em;
overflow: hidden;
diff --git a/dev/Styles/_Values.less b/dev/Styles/_Values.less
index d1c7e48c2..75f0faf08 100644
--- a/dev/Styles/_Values.less
+++ b/dev/Styles/_Values.less
@@ -1,11 +1,7 @@
@rlLeftWidth: 200px;
-@rlInputBorderSize: 1px;
-@rlMainBorderSize: 1px;
-@rlLowBorderSize: 1px;
@rlMainBorderRadius: 5px;
-@rlLowBorderRadius: 3px;
@rlMainShadow: 0 2px 8px rgba(0, 0, 0, 0.2);
@@ -13,6 +9,3 @@
@rlBottomMargin: 5px;
@rlMainDarkColor: #aaa;
-
-// bootstart
-@btnBorderRadius: 3px;
diff --git a/dev/Styles/_progressjs.css b/dev/Styles/_progressjs.css
deleted file mode 100644
index 152d1a519..000000000
--- a/dev/Styles/_progressjs.css
+++ /dev/null
@@ -1,33 +0,0 @@
-#progressjs {
- left: 0;
- position: fixed;
- top: 0;
- width: 100%;
- z-index: 2000;
-}
-
-.progressjs-inner {
- background-color: #939595;
- height: 3px;
- overflow: hidden;
- position: relative;
- transition: width .5s;
- width: 0;
- z-index: 2000;
-}
-
-.progressjs-percent {
- position: absolute;
- top: 0;
- left: 0;
- right: -32px;
- bottom: 0;
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.3) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.3) 50%, rgba(255, 255, 255, 0.3) 75%, transparent 75%, transparent);
- background-size: 32px 32px;
- animation: simple-pace-stripe-animation 500ms linear infinite;
-}
-
-@keyframes simple-pace-stripe-animation {
- 0% { transform: none; }
- 100% { transform: translate(-32px, 0); }
-}
diff --git a/dev/View/Admin/Login.js b/dev/View/Admin/Login.js
index fd8af23b4..ba8571aa6 100644
--- a/dev/View/Admin/Login.js
+++ b/dev/View/Admin/Login.js
@@ -1,34 +1,27 @@
-import ko from 'ko';
-
-import { Settings } from 'Common/Globals';
+import { fireEvent } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewCenter } from 'Knoin/AbstractViews';
+import { AbstractViewLogin } from 'Knoin/AbstractViews';
-class LoginAdminView extends AbstractViewCenter {
+export class AdminLoginView extends AbstractViewLogin {
constructor() {
- super('Admin/Login', 'AdminLogin');
-
- this.hideSubmitButton = Settings.app('hideSubmitButton');
+ super('AdminLogin');
this.addObservables({
login: '',
password: '',
+ totp: '',
loginError: false,
passwordError: false,
- formHidden: false,
-
submitRequest: false,
submitError: ''
});
- this.formError = ko.observable(false).extend({ falseTimeout: 500 });
-
this.addSubscribables({
login: () => this.loginError(false),
password: () => this.passwordError(false)
@@ -40,41 +33,34 @@ class LoginAdminView extends AbstractViewCenter {
}
submitCommand(self, event) {
- const valid = event.target.form.reportValidity(),
- name = this.login().trim(),
- pass = this.password();
+ let form = event.target.form,
+ data = new FormData(form),
+ valid = form.reportValidity() && fireEvent('sm-admin-login', data);
- this.loginError(!name);
- this.passwordError(!pass);
+ this.loginError(!this.login());
+ this.passwordError(!this.password());
this.formError(!valid);
if (valid) {
this.submitRequest(true);
- Remote.adminLogin(
- iError => {
+ Remote.request('AdminLogin',
+ (iError, oData) => {
+ fireEvent('sm-admin-login-response', {
+ error: iError,
+ data: oData
+ });
if (iError) {
this.submitRequest(false);
this.submitError(getNotification(iError));
} else {
- rl.route.reload();
+ rl.setData(oData.Result);
}
},
- name,
- pass
+ data
);
}
return valid;
}
-
- onShow() {
- rl.route.off();
- }
-
- submitForm() {
-// return false;
- }
}
-
-export { LoginAdminView };
diff --git a/dev/View/Admin/Settings/Menu.js b/dev/View/Admin/Settings/Menu.js
index 3f218ddff..6a991a573 100644
--- a/dev/View/Admin/Settings/Menu.js
+++ b/dev/View/Admin/Settings/Menu.js
@@ -1,6 +1,3 @@
-import { Scope } from 'Common/Enums';
-
-import { settingsMenuKeysHandler } from 'Knoin/Knoin';
import { AbstractViewLeft } from 'Knoin/AbstractViews';
class MenuSettingsAdminView extends AbstractViewLeft {
@@ -8,7 +5,7 @@ class MenuSettingsAdminView extends AbstractViewLeft {
* @param {?} screen
*/
constructor(screen) {
- super('Admin/Settings/Menu', 'AdminMenu');
+ super('AdminMenu');
this.menu = screen.menu;
}
@@ -16,11 +13,6 @@ class MenuSettingsAdminView extends AbstractViewLeft {
link(route) {
return '#/' + route;
}
-
- onBuild(dom) {
- shortcuts.add('arrowup,arrowdown', '', Scope.Settings,
- settingsMenuKeysHandler(dom.querySelectorAll('.b-admin-menu .e-item')));
- }
}
export { MenuSettingsAdminView };
diff --git a/dev/View/Admin/Settings/Pane.js b/dev/View/Admin/Settings/Pane.js
index 016241a81..207c90440 100644
--- a/dev/View/Admin/Settings/Pane.js
+++ b/dev/View/Admin/Settings/Pane.js
@@ -1,34 +1,16 @@
-import ko from 'ko';
-
import Remote from 'Remote/Admin/Fetch';
-import { PackageAdminStore } from 'Stores/Admin/Package';
-
import { AbstractViewRight } from 'Knoin/AbstractViews';
-import { leftPanelDisabled, Settings } from 'Common/Globals';
+import { leftPanelDisabled } from 'Common/Globals';
-class PaneSettingsAdminView extends AbstractViewRight {
+export class PaneSettingsAdminView extends AbstractViewRight {
constructor() {
- super('Admin/Settings/Pane', 'AdminPane');
-
- this.version = ko.observable(Settings.app('version'));
-
+ super('AdminPane');
this.leftPanelDisabled = leftPanelDisabled;
-
- this.adminManLoadingVisibility = ko
- .computed(() => PackageAdminStore.loading() ? 'visible' : 'hidden');
- }
-
- toggleLeft(item, event) {
- event.preventDefault();
- event.stopPropagation();
- leftPanelDisabled(!leftPanelDisabled());
}
logoutClick() {
- Remote.adminLogout(() => rl.logoutReload());
+ Remote.request('AdminLogout', () => rl.logoutReload());
}
}
-
-export { PaneSettingsAdminView };
diff --git a/dev/View/Popup/Account.js b/dev/View/Popup/Account.js
index 292fbc24b..71e8ab12e 100644
--- a/dev/View/Popup/Account.js
+++ b/dev/View/Popup/Account.js
@@ -2,10 +2,9 @@ import { getNotification } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
-import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class AccountPopupView extends AbstractViewPopup {
+export class AccountPopupView extends AbstractViewPopup {
constructor() {
super('Account');
@@ -26,45 +25,42 @@ class AccountPopupView extends AbstractViewPopup {
this.email.subscribe(() => this.emailError(false));
this.password.subscribe(() => this.passwordError(false));
-
- decorateKoCommands(this, {
- addAccountCommand: self => !self.submitRequest()
- });
}
- addAccountCommand() {
- this.emailError(!this.email().trim());
- this.passwordError(!this.password().trim());
-
- if (this.emailError() || this.passwordError()) {
- return false;
+ submitForm() {
+ if (!this.submitRequest()) {
+ const email = this.email().trim(), pass = this.password();
+ this.emailError(!email);
+ this.passwordError(!pass);
+ if (!this.emailError() && pass) {
+ this.submitRequest(true);
+ Remote.request('AccountSetup', (iError, data) => {
+ this.submitRequest(false);
+ if (iError) {
+ this.submitError(getNotification(iError));
+ this.submitErrorAdditional((data && data.ErrorMessageAdditional) || '');
+ } else {
+ rl.app.accountsAndIdentities();
+ this.close();
+ }
+ }, {
+ Email: email,
+ Password: pass,
+ New: this.isNew() ? 1 : 0
+ }
+ );
+ }
}
-
- this.submitRequest(true);
-
- Remote.accountSetup(
- (iError, data) => {
- this.submitRequest(false);
- if (iError) {
- this.submitError(getNotification(iError));
- this.submitErrorAdditional((data && data.ErrorMessageAdditional) || '');
- } else {
- rl.app.accountsAndIdentities();
- this.cancelCommand();
- }
- },
- this.email(),
- this.password(),
- this.isNew()
- );
-
- return true;
}
- clearPopup() {
- this.isNew(true);
-
- this.email('');
+ onShow(account) {
+ if (account && account.isAdditional()) {
+ this.isNew(false);
+ this.email(account.email);
+ } else {
+ this.isNew(true);
+ this.email('');
+ }
this.password('');
this.emailError(false);
@@ -74,14 +70,4 @@ class AccountPopupView extends AbstractViewPopup {
this.submitError('');
this.submitErrorAdditional('');
}
-
- onShow(account) {
- this.clearPopup();
- if (account && account.canBeEdit()) {
- this.isNew(false);
- this.email(account.email);
- }
- }
}
-
-export { AccountPopupView, AccountPopupView as default };
diff --git a/dev/View/Popup/AddOpenPgpKey.js b/dev/View/Popup/AddOpenPgpKey.js
deleted file mode 100644
index 1ed1b1238..000000000
--- a/dev/View/Popup/AddOpenPgpKey.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { PgpUserStore } from 'Stores/User/Pgp';
-
-import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-class AddOpenPgpKeyPopupView extends AbstractViewPopup {
- constructor() {
- super('AddOpenPgpKey');
-
- this.addObservables({
- key: '',
- keyError: false,
- keyErrorMessage: ''
- });
-
- this.key.subscribe(() => {
- this.keyError(false);
- this.keyErrorMessage('');
- });
-
- decorateKoCommands(this, {
- addOpenPgpKeyCommand: 1
- });
- }
-
- addOpenPgpKeyCommand() {
- // eslint-disable-next-line max-len
- const reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi,
- openpgpKeyring = PgpUserStore.openpgpKeyring;
-
- let keyTrimmed = this.key().trim();
-
- if (/[\n]/.test(keyTrimmed)) {
- keyTrimmed = keyTrimmed.replace(/[\r]+/g, '').replace(/[\n]{2,}/g, '\n\n');
- }
-
- this.keyError(!keyTrimmed);
- this.keyErrorMessage('');
-
- if (!openpgpKeyring || this.keyError()) {
- return false;
- }
-
- let match = null,
- count = 30,
- done = false;
-
- do {
- match = reg.exec(keyTrimmed);
- if (match && 0 < count) {
- if (match[0] && match[1] && match[2] && match[1] === match[2]) {
- let err = null;
- if ('PRIVATE' === match[1]) {
- err = openpgpKeyring.privateKeys.importKey(match[0]);
- } else if ('PUBLIC' === match[1]) {
- err = openpgpKeyring.publicKeys.importKey(match[0]);
- }
-
- if (err) {
- this.keyError(true);
- this.keyErrorMessage(err && err[0] ? '' + err[0] : '');
- console.log(err);
- }
- }
-
- --count;
- done = false;
- } else {
- done = true;
- }
- } while (!done);
-
- openpgpKeyring.store();
-
- rl.app.reloadOpenPgpKeys();
-
- if (this.keyError()) {
- return false;
- }
-
- this.cancelCommand && this.cancelCommand();
- return true;
- }
-
- clearPopup() {
- this.key('');
- this.keyError(false);
- this.keyErrorMessage('');
- }
-
- onShow() {
- this.clearPopup();
- }
-}
-
-export { AddOpenPgpKeyPopupView, AddOpenPgpKeyPopupView as default };
diff --git a/dev/View/Popup/AdvancedSearch.js b/dev/View/Popup/AdvancedSearch.js
index 2c8cb6fa1..b06ba0544 100644
--- a/dev/View/Popup/AdvancedSearch.js
+++ b/dev/View/Popup/AdvancedSearch.js
@@ -1,13 +1,13 @@
-import ko from 'ko';
+import { koComputable } from 'External/ko';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
-import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
-import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
+import { FolderUserStore } from 'Stores/User/Folder';
-class AdvancedSearchPopupView extends AbstractViewPopup {
+export class AdvancedSearchPopupView extends AbstractViewPopup {
constructor() {
super('AdvancedSearch');
@@ -17,15 +17,18 @@ class AdvancedSearchPopupView extends AbstractViewPopup {
subject: '',
text: '',
selectedDateValue: -1,
+ selectedTreeValue: '',
hasAttachment: false,
starred: false,
unseen: false
});
- let prefix = 'SEARCH/LABEL_ADV_DATE_';
- this.selectedDates = ko.computed(() => {
+ this.showMultisearch = koComputable(() => FolderUserStore.hasCapability('MULTISEARCH'));
+
+ this.selectedDates = koComputable(() => {
translatorTrigger();
+ let prefix = 'SEARCH/LABEL_ADV_DATE_';
return [
{ id: -1, name: i18n(prefix + 'ALL') },
{ id: 3, name: i18n(prefix + '3_DAYS') },
@@ -37,18 +40,24 @@ class AdvancedSearchPopupView extends AbstractViewPopup {
];
});
- decorateKoCommands(this, {
- searchCommand: 1
+ this.selectedTree = koComputable(() => {
+ translatorTrigger();
+ let prefix = 'SEARCH/LABEL_ADV_SUBFOLDERS_';
+ return [
+ { id: '', name: i18n(prefix + 'NONE') },
+ { id: 'subtree-one', name: i18n(prefix + 'SUBTREE_ONE') },
+ { id: 'subtree', name: i18n(prefix + 'SUBTREE') }
+ ];
});
}
- searchCommand() {
+ submitForm() {
const search = this.buildSearchString();
if (search) {
- MessageUserStore.mainMessageListSearch(search);
+ MessagelistUserStore.mainSearch(search);
}
- this.cancelCommand();
+ this.close();
}
parseSearchStringValue(search) {
@@ -69,68 +78,38 @@ class AdvancedSearchPopupView extends AbstractViewPopup {
});
}
- buildSearchStringValue(value) {
- if (value.includes(' ')) {
- value = '"' + value + '"';
- }
- return value;
- }
-
buildSearchString() {
- const result = [],
- from_ = this.from().trim(),
- to = this.to().trim(),
- subject = this.subject().trim(),
- text = this.text().trim(),
- isPart = [],
- hasPart = [];
-
- if (from_) {
- result.push('from:' + this.buildSearchStringValue(from_));
- }
-
- if (to) {
- result.push('to:' + this.buildSearchStringValue(to));
- }
-
- if (subject) {
- result.push('subject:' + this.buildSearchStringValue(subject));
- }
-
- if (this.hasAttachment()) {
- hasPart.push('attachment');
- }
-
- if (this.unseen()) {
- isPart.push('unseen');
- }
-
- if (this.starred()) {
- isPart.push('flagged');
- }
-
- if (hasPart.length) {
- result.push('has:' + hasPart.join(','));
- }
-
- if (isPart.length) {
- result.push('is:' + isPart.join(','));
- }
+ const
+ data = new FormData(),
+ append = (key, value) => value.length && data.append(key, value);
+ append('from', this.from().trim());
+ append('to', this.to().trim());
+ append('subject', this.subject().trim());
+ append('text', this.text().trim());
+ append('in', this.selectedTreeValue());
if (-1 < this.selectedDateValue()) {
let d = new Date();
d.setDate(d.getDate() - this.selectedDateValue());
- result.push('date:' + d.format('Y.m.d') + '/');
+ append('date', d.format('Y.m.d') + '/');
}
- if (text) {
- result.push('text:' + this.buildSearchStringValue(text));
+ let result = new URLSearchParams(data).toString();
+
+ if (this.hasAttachment()) {
+ result += '&attachment';
+ }
+ if (this.unseen()) {
+ result += '&unseen';
+ }
+ if (this.starred()) {
+ result += '&flagged';
}
- return result.join(' ').trim();
+ return result.replace(/^&+/, '');
}
- clearPopup() {
+ onShow(search) {
this.from('');
this.to('');
this.subject('');
@@ -140,12 +119,7 @@ class AdvancedSearchPopupView extends AbstractViewPopup {
this.hasAttachment(false);
this.starred(false);
this.unseen(false);
- }
- onShow(search) {
- this.clearPopup();
this.parseSearchStringValue(search);
}
}
-
-export { AdvancedSearchPopupView, AdvancedSearchPopupView as default };
diff --git a/dev/View/Popup/Ask.js b/dev/View/Popup/Ask.js
index 41d505697..ae1f8388e 100644
--- a/dev/View/Popup/Ask.js
+++ b/dev/View/Popup/Ask.js
@@ -1,84 +1,68 @@
-import { Scope } from 'Common/Enums';
import { i18n } from 'Common/Translator';
import { isFunction } from 'Common/Utils';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class AskPopupView extends AbstractViewPopup {
+export class AskPopupView extends AbstractViewPopup {
constructor() {
super('Ask');
this.addObservables({
askDesc: '',
yesButton: '',
- noButton: ''
+ noButton: '',
+ passphrase: '',
+ askPass: false
});
this.fYesAction = null;
this.fNoAction = null;
- this.bFocusYesOnShow = true;
- this.bDisabeCloseOnEsc = true;
- }
-
- clearPopup() {
- this.askDesc('');
- this.yesButton(i18n('POPUPS_ASK/BUTTON_YES'));
- this.noButton(i18n('POPUPS_ASK/BUTTON_NO'));
-
- this.fYesAction = null;
- this.fNoAction = null;
+ this.focusOnShow = true;
}
yesClick() {
- this.cancelCommand();
+ this.close();
- isFunction(this.fYesAction) && this.fYesAction.call(null);
+ isFunction(this.fYesAction) && this.fYesAction();
}
noClick() {
- this.cancelCommand();
+ this.close();
- isFunction(this.fNoAction) && this.fNoAction.call(null);
+ isFunction(this.fNoAction) && this.fNoAction();
}
/**
* @param {string} sAskDesc
* @param {Function=} fYesFunc
* @param {Function=} fNoFunc
- * @param {string=} sYesButton
- * @param {string=} sNoButton
- * @param {boolean=} bFocusYesOnShow = true
+ * @param {boolean=} focusOnShow = true
* @returns {void}
*/
- onShow(askDesc, fYesFunc = null, fNoFunc = null, yesButton = '', noButton = '', isFocusYesOnShow = true) {
- this.clearPopup();
-
- this.fYesAction = fYesFunc || null;
- this.fNoAction = fNoFunc || null;
-
- this.askDesc(askDesc || '');
-
- if (yesButton) {
- this.yesButton(yesButton);
- }
-
- if (noButton) {
- this.noButton(noButton);
- }
-
- this.bFocusYesOnShow = !!isFocusYesOnShow;
+ onShow(sAskDesc, fYesFunc = null, fNoFunc = null, focusOnShow = true, askPass = false, btnText = '') {
+ this.askDesc(sAskDesc || '');
+ this.askPass(askPass);
+ this.passphrase('');
+ this.yesButton(i18n(btnText || 'POPUPS_ASK/BUTTON_YES'));
+ this.noButton(i18n(askPass ? 'GLOBAL/CANCEL' : 'POPUPS_ASK/BUTTON_NO'));
+ this.fYesAction = fYesFunc;
+ this.fNoAction = fNoFunc;
+ this.focusOnShow = focusOnShow ? (askPass ? 'input[type="password"]' : '.buttonYes') : '';
}
- onShowWithDelay() {
- if (this.bFocusYesOnShow) {
- this.querySelector('.buttonYes').focus();
- }
+ afterShow() {
+ this.focusOnShow && this.querySelector(this.focusOnShow).focus();
+ }
+
+ onClose() {
+ this.noClick();
+ return false;
}
onBuild() {
-// shortcuts.add('tab', 'shift', Scope.Ask, () => {
- shortcuts.add('tab,arrowright,arrowleft', '', Scope.Ask, () => {
+// shortcuts.add('tab', 'shift', 'Ask', () => {
+ shortcuts.add('tab,arrowright,arrowleft', '', 'Ask', () => {
let btn = this.querySelector('.buttonYes');
if (btn.matches(':focus')) {
btn = this.querySelector('.buttonNo');
@@ -86,12 +70,18 @@ class AskPopupView extends AbstractViewPopup {
btn.focus();
return false;
});
-
- shortcuts.add('escape', '', Scope.Ask, () => {
- this.noClick();
- return false;
- });
}
}
-export { AskPopupView, AskPopupView as default };
+AskPopupView.password = function(sAskDesc, btnText) {
+ return new Promise(resolve => {
+ this.showModal([
+ sAskDesc,
+ () => resolve(this.__vm.passphrase()),
+ () => resolve(null),
+ true,
+ true,
+ btnText
+ ]);
+ });
+}
diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js
index 3a0fb21ea..530b4df81 100644
--- a/dev/View/Popup/Compose.js
+++ b/dev/View/Popup/Compose.js
@@ -1,7 +1,6 @@
import ko from 'ko';
import {
- Scope,
Notification,
UploadErrorCode
} from 'Common/Enums';
@@ -12,16 +11,17 @@ import {
SetSystemFoldersNotification
} from 'Common/EnumsUser';
-import { inFocus, pInt, isArray, isNonEmptyArray } from 'Common/Utils';
-import { delegateRunOnDestroy } from 'Common/UtilsUser';
-import { encodeHtml, HtmlEditor } from 'Common/Html';
+import { pInt, isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
+import { initFullscreen } from 'Common/UtilsUser';
+import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
+import { koArrayWithDestroy } from 'External/ko';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
+import { messagesDeleteHelper } from 'Common/Folders';
import { serverRequest } from 'Common/Links';
-import { i18n, getNotification, getUploadErrorDescByCode } from 'Common/Translator';
-import { timestampToString } from 'Common/Momentor';
+import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
-import { Settings, SettingsGet } from 'Common/Globals';
+import { doc, Settings, SettingsGet, getFullscreenElement, exitFullscreen, elementById, addShortcut } from 'Common/Globals';
import { AppUserStore } from 'Stores/User/App';
import { SettingsUserStore } from 'Stores/User/Settings';
@@ -29,21 +29,37 @@ import { IdentityUserStore } from 'Stores/User/Identity';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp';
+import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
+import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
+import { EmailModel } from 'Model/Email';
-import { decorateKoCommands, isPopupVisible, showScreenPopup, hideScreenPopup } from 'Knoin/Knoin';
+import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { AskPopupView } from 'View/Popup/Ask';
import { ContactsPopupView } from 'View/Popup/Contacts';
-import { ComposeOpenPgpPopupView } from 'View/Popup/ComposeOpenPgp';
+
+import { ThemeStore } from 'Stores/Theme';
const
+ ScopeCompose = 'Compose',
+
+ base64_encode = text => btoa(text).match(/.{1,76}/g).join('\r\n'),
+
+ email = new EmailModel(),
+ getEmail = value => {
+ email.clear();
+ email.parse(value.trim());
+ return email.email || false;
+ },
+
/**
* @param {string} prefix
* @param {string} subject
@@ -94,7 +110,34 @@ ko.extenders.toggleSubscribe = (target, options) => {
return target;
};
-class ComposePopupView extends AbstractViewPopup {
+class MimePart {
+ constructor() {
+ this.headers = {};
+ this.body = '';
+ this.boundary = '';
+ this.children = [];
+ }
+
+ toString() {
+ const hasSub = this.children.length,
+ boundary = this.boundary || (this.boundary = 'part' + Jua.randomId()),
+ headers = this.headers;
+ if (hasSub) {
+ headers['Content-Type'] += `; boundary="${boundary}"`;
+ }
+ let result = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n') + '\r\n';
+ if (this.body) {
+ result += '\r\n' + this.body.replace(/\r?\n/g, '\r\n');
+ }
+ if (hasSub) {
+ this.children.forEach(part => result += '\r\n--' + boundary + '\r\n' + part);
+ result += '\r\n--' + boundary + '--\r\n';
+ }
+ return result;
+ }
+}
+
+export class ComposePopupView extends AbstractViewPopup {
constructor() {
super('Compose');
@@ -128,15 +171,11 @@ class ComposePopupView extends AbstractViewPopup {
this.allowContacts = AppUserStore.allowContacts();
this.bSkipNextHide = false;
- this.editorDefaultType = SettingsUserStore.editorDefaultType;
-
- this.capaOpenPGP = PgpUserStore.capaOpenPGP;
-
- this.identities = IdentityUserStore;
this.addObservables({
identitiesDropdownTrigger: false,
+ from: '',
to: '',
cc: '',
bcc: '',
@@ -168,25 +207,29 @@ class ComposePopupView extends AbstractViewPopup {
showBcc: false,
showReplyTo: false,
- draftFolder: '',
- draftUid: '',
+ pgpSign: false,
+ canPgpSign: false,
+ pgpEncrypt: false,
+ canPgpEncrypt: false,
+ canMailvelope: false,
+
+ draftsFolder: '',
+ draftUid: 0,
sending: false,
saving: false,
- attachmentsPlace: false,
+ viewArea: 'body',
- composeUploaderButton: null,
- composeUploaderDropPlace: null,
- dragAndDropEnabled: false,
attacheMultipleAllowed: false,
addAttachmentEnabled: false,
- // div.textAreaParent
- composeEditorArea: null,
+ editorArea: null, // initDom
- currentIdentity: this.identities()[0] ? this.identities()[0] : null
+ currentIdentity: IdentityUserStore()[0]
});
+ this.from(IdentityUserStore()[0].formattedName());
+
// this.to.subscribe((v) => console.log(v));
// Used by ko.bindingHandlers.emailsTags
@@ -197,7 +240,7 @@ class ComposePopupView extends AbstractViewPopup {
this.bcc.focused = ko.observable(false);
this.bcc.focused.subscribe(value => value && (this.sLastFocusedField = 'bcc'));
- this.attachments = ko.observableArray();
+ this.attachments = koArrayWithDestroy();
this.dragAndDropOver = ko.observable(false).extend({ debounce: 1 });
this.dragAndDropVisible = ko.observable(false).extend({ debounce: 1 });
@@ -216,8 +259,6 @@ class ComposePopupView extends AbstractViewPopup {
]
});
- this.bDisabeCloseOnEsc = true;
-
this.tryToClosePopup = this.tryToClosePopup.debounce(200);
this.iTimer = 0;
@@ -250,7 +291,7 @@ class ComposePopupView extends AbstractViewPopup {
attachmentsCount: () => this.attachments.length,
attachmentsInErrorCount: () => this.attachmentsInError.length,
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
- isDraftFolderMessage: () => this.draftFolder() && this.draftUid(),
+ isDraftFolderMessage: () => this.draftsFolder() && this.draftUid(),
identitiesOptions: () =>
IdentityUserStore.map(item => ({
@@ -259,13 +300,7 @@ class ComposePopupView extends AbstractViewPopup {
optText: item.formattedName()
})),
- currentIdentityView: () => {
- const item = this.currentIdentity();
- return item ? item.formattedName() : 'unknown';
- },
-
canBeSentOrSaved: () => !this.sending() && !this.saving()
-
});
this.addSubscribables({
@@ -275,16 +310,32 @@ class ComposePopupView extends AbstractViewPopup {
sendSuccessButSaveError: value => !value && this.savedErrorDesc(''),
+ currentIdentity: value => value && this.from(value.formattedName()),
+
+ from: value => {
+ this.canPgpSign(false);
+ value = getEmail(value);
+ value && PgpUserStore.getKeyForSigning(value).then(result => {
+ console.log({
+ email: value,
+ canPgpSign:result
+ });
+ this.canPgpSign(result)
+ });
+ },
+
cc: value => {
if (false === this.showCc() && value.length) {
this.showCc(true);
}
+ this.initPgpEncrypt();
},
bcc: value => {
if (false === this.showBcc() && value.length) {
this.showBcc(true);
}
+ this.initPgpEncrypt();
},
replyTo: value => {
@@ -303,30 +354,55 @@ class ComposePopupView extends AbstractViewPopup {
if (this.emptyToError() && value.length) {
this.emptyToError(false);
}
+ this.initPgpEncrypt();
},
attachmentsInProcess: value => {
- if (this.attachmentsInProcessError() && isNonEmptyArray(value)) {
+ if (this.attachmentsInProcessError() && arrayLength(value)) {
this.attachmentsInProcessError(false);
}
}
});
- this.resizeObserver = new ResizeObserver(this.resizerTrigger.throttle(50).bind(this));
-
decorateKoCommands(this, {
sendCommand: self => self.canBeSentOrSaved(),
- saveCommand: self => self.canBeSentOrSaved(),
- deleteCommand: self => self.isDraftFolderMessage(),
- skipCommand: self => self.canBeSentOrSaved(),
- contactsCommand: self => self.allowContacts
+ saveCommand: self => self.canBeSentOrSaved(),
+ deleteCommand: self => self.isDraftFolderMessage(),
+ skipCommand: self => self.canBeSentOrSaved(),
+ contactsCommand: self => self.allowContacts
});
}
- getMessageRequestParams(sSaveFolder)
+ async getMessageRequestParams(sSaveFolder, draft)
{
- let TextIsHtml = this.oEditor.isHtml() ? 1 : 0,
- Text = this.oEditor.getData(true);
+ const
+ identity = this.currentIdentity(),
+ params = {
+ IdentityID: identity.id(),
+ MessageFolder: this.draftsFolder(),
+ MessageUid: this.draftUid(),
+ SaveFolder: sSaveFolder,
+ From: this.from(),
+ To: this.to(),
+ Cc: this.cc(),
+ Bcc: this.bcc(),
+ ReplyTo: this.replyTo(),
+ Subject: this.subject(),
+ DraftInfo: this.aDraftInfo,
+ InReplyTo: this.sInReplyTo,
+ References: this.sReferences,
+ MarkAsImportant: this.markAsImportant() ? 1 : 0,
+ Attachments: this.prepareAttachmentsForSendOrSave(),
+ // Only used at send, not at save:
+ Dsn: this.requestDsn() ? 1 : 0,
+ ReadReceiptRequest: this.requestReadReceipt() ? 1 : 0
+ },
+ recipients = draft ? [identity.email()] : this.allRecipients(),
+ sign = !draft && this.pgpSign() && this.canPgpSign(),
+ encrypt = this.pgpEncrypt() && this.canPgpEncrypt(),
+ TextIsHtml = this.oEditor.isHtml();
+
+ let Text = this.oEditor.getData();
if (TextIsHtml) {
let l;
do {
@@ -337,28 +413,73 @@ class ComposePopupView extends AbstractViewPopup {
// Remove hubspot data-hs- attributes
.replace(/(<[^>]+)\s+data-hs-[a-z-]+=("[^"]+"|'[^']+')/gi, '$1');
} while (l != Text.length)
+ params.Html = Text;
+ params.Text = htmlToPlain(Text);
+ } else {
+ params.Text = Text;
}
- return {
- IdentityID: this.currentIdentity() ? this.currentIdentity().id() : '',
- MessageFolder: this.draftFolder(),
- MessageUid: this.draftUid(),
- SaveFolder: sSaveFolder,
- To: this.to(),
- Cc: this.cc(),
- Bcc: this.bcc(),
- ReplyTo: this.replyTo(),
- Subject: this.subject(),
- TextIsHtml: TextIsHtml,
- Text: Text,
- DraftInfo: this.aDraftInfo,
- InReplyTo: this.sInReplyTo,
- References: this.sReferences,
- MarkAsImportant: this.markAsImportant() ? 1 : 0,
- Attachments: this.prepearAttachmentsForSendOrSave(),
- // Only used at send, not at save:
- Dsn: this.requestDsn() ? 1 : 0,
- ReadReceiptRequest: this.requestReadReceipt() ? 1 : 0
- };
+
+ if (this.mailvelope && 'mailvelope' === this.viewArea()) {
+ params.Encrypted = draft
+ ? await this.mailvelope.createDraft()
+ : await this.mailvelope.encrypt(recipients);
+ } else if (sign || encrypt) {
+ let data = new MimePart;
+ data.headers['Content-Type'] = 'text/'+(TextIsHtml?'html':'plain')+'; charset="utf-8"';
+ data.headers['Content-Transfer-Encoding'] = 'base64';
+ data.body = base64_encode(Text);
+ if (TextIsHtml) {
+ const alternative = new MimePart, plain = new MimePart;
+ alternative.headers['Content-Type'] = 'multipart/alternative';
+ plain.headers['Content-Type'] = 'text/plain; charset="utf-8"';
+ plain.headers['Content-Transfer-Encoding'] = 'base64';
+ plain.body = base64_encode(params.Text);
+ // First add plain
+ alternative.children.push(plain);
+ // Now add HTML
+ alternative.children.push(data);
+ data = alternative;
+ }
+ if (sign && !draft && sign[1]) {
+ if ('openpgp' == sign[0]) {
+ // Doesn't sign attachments
+ params.Html = params.Text = '';
+ let signed = new MimePart;
+ signed.headers['Content-Type'] =
+ 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
+ signed.headers['Content-Transfer-Encoding'] = '7Bit';
+ signed.children.push(data);
+ let signature = new MimePart;
+ signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"';
+ signature.headers['Content-Transfer-Encoding'] = '7Bit';
+ signature.body = await OpenPGPUserStore.sign(data.toString(), sign[1], 1);
+ signed.children.push(signature);
+ params.Signed = signed.toString();
+ params.Boundary = signed.boundary;
+ data = signed;
+ } else if ('gnupg' == sign[0]) {
+ // TODO: sign in PHP fails
+// params.SignData = data.toString();
+ params.SignFingerprint = sign[1].fingerprint;
+ params.SignPassphrase = await GnuPGUserStore.sign(sign[1]);
+ } else {
+ throw 'Signing with ' + sign[0] + ' not yet implemented';
+ }
+ }
+ if (encrypt) {
+ if ('openpgp' == encrypt) {
+ // Doesn't encrypt attachments
+ params.Encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
+ params.Signed = '';
+ } else if ('gnupg' == encrypt) {
+ // Does encrypt attachments
+ params.EncryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
+ } else {
+ throw 'Encryption with ' + encrypt + ' not yet implemented';
+ }
+ }
+ }
+ return params;
}
sendCommand() {
@@ -370,10 +491,10 @@ class ComposePopupView extends AbstractViewPopup {
if (this.attachmentsInProcess().length) {
this.attachmentsInProcessError(true);
- this.attachmentsPlace(true);
+ this.attachmentsArea();
} else if (this.attachmentsInError().length) {
this.attachmentsInErrorError(true);
- this.attachmentsPlace(true);
+ this.attachmentsArea();
}
if (!this.to().trim() && !this.cc().trim() && !this.bcc().trim()) {
@@ -383,8 +504,7 @@ class ComposePopupView extends AbstractViewPopup {
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
if (SettingsUserStore.replySameFolder()) {
if (
- isArray(this.aDraftInfo) &&
- 3 === this.aDraftInfo.length &&
+ 3 === arrayLength(this.aDraftInfo) &&
null != this.aDraftInfo[2] &&
this.aDraftInfo[2].length
) {
@@ -394,80 +514,142 @@ class ComposePopupView extends AbstractViewPopup {
if (!sSentFolder) {
showScreenPopup(FolderSystemPopupView, [SetSystemFoldersNotification.Sent]);
- } else {
+ } else try {
this.sendError(false);
this.sending(true);
- if (isArray(this.aDraftInfo) && 3 === this.aDraftInfo.length) {
+ if (3 === arrayLength(this.aDraftInfo)) {
const flagsCache = MessageFlagsCache.getFor(this.aDraftInfo[2], this.aDraftInfo[1]);
- if (flagsCache) {
- if ('forward' === this.aDraftInfo[0]) {
- flagsCache[3] = true;
- } else {
- flagsCache[2] = true;
- }
-
+ if (isArray(flagsCache)) {
+ flagsCache.push(('forward' === this.aDraftInfo[0]) ? '$forwarded' : '\\answered');
MessageFlagsCache.setFor(this.aDraftInfo[2], this.aDraftInfo[1], flagsCache);
- rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
setFolderHash(this.aDraftInfo[2], '');
}
}
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
- setFolderHash(this.draftFolder(), '');
- setFolderHash(sSentFolder, '');
-
- Remote.sendMessage(
- this.sendMessageResponse.bind(this),
- this.getMessageRequestParams(sSentFolder)
- );
+ this.getMessageRequestParams(sSentFolder).then(params => {
+ Remote.request('SendMessage',
+ (iError, data) => {
+ this.sending(false);
+ if (iError) {
+ if (Notification.CantSaveMessage === iError) {
+ this.sendSuccessButSaveError(true);
+ this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
+ } else {
+ this.sendError(true);
+ this.sendErrorDesc(getNotification(iError, data && data.ErrorMessage)
+ || getNotification(Notification.CantSendMessage));
+ }
+ } else {
+ this.close();
+ }
+ setFolderHash(this.draftsFolder(), '');
+ setFolderHash(sSentFolder, '');
+ this.reloadDraftFolder();
+ },
+ params,
+ 30000
+ );
+ }).catch(e => {
+ console.error(e);
+ this.sendError(true);
+ this.sendErrorDesc(e);
+ this.sending(false);
+ });
+ } catch (e) {
+ console.error(e);
+ this.sendError(true);
+ this.sendErrorDesc(e);
+ this.sending(false);
}
}
}
saveCommand() {
- if (FolderUserStore.draftFolderNotEnabled()) {
+ if (FolderUserStore.draftsFolderNotEnabled()) {
showScreenPopup(FolderSystemPopupView, [SetSystemFoldersNotification.Draft]);
} else {
this.savedError(false);
this.saving(true);
-
this.autosaveStart();
+ this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => {
+ Remote.request('SaveMessage',
+ (iError, oData) => {
+ let result = false;
- setFolderHash(FolderUserStore.draftFolder(), '');
+ this.saving(false);
- Remote.saveMessage(
- this.saveMessageResponse.bind(this),
- this.getMessageRequestParams(FolderUserStore.draftFolder())
- );
+ if (!iError) {
+ if (oData.Result.NewFolder && oData.Result.NewUid) {
+ result = true;
+
+ if (this.bFromDraft) {
+ const message = MessageUserStore.message();
+ if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) {
+ MessageUserStore.message(null);
+ }
+ }
+
+ this.draftsFolder(oData.Result.NewFolder);
+ this.draftUid(oData.Result.NewUid);
+
+ this.savedTime(new Date);
+
+ if (this.bFromDraft) {
+ setFolderHash(this.draftsFolder(), '');
+ }
+ setFolderHash(FolderUserStore.draftsFolder(), '');
+ }
+ }
+
+ if (!result) {
+ this.savedError(true);
+ this.savedErrorDesc(getNotification(Notification.CantSaveMessage));
+ }
+
+ this.reloadDraftFolder();
+ },
+ params,
+ 200000
+ );
+ }).catch(e => {
+ this.saving(false);
+ this.savedError(true);
+ this.savedErrorDesc(getNotification(Notification.CantSaveMessage) + ': ' + e);
+ });
}
-
- return true;
}
deleteCommand() {
- if (!isPopupVisible(AskPopupView) && this.modalVisibility()) {
- showScreenPopup(AskPopupView, [
- i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
- () => {
- if (this.modalVisibility()) {
- rl.app.deleteMessagesFromFolderWithoutCheck(this.draftFolder(), [this.draftUid()]);
- hideScreenPopup(ComposePopupView);
- }
- }
- ]);
- }
+ AskPopupView.hidden()
+ && showScreenPopup(AskPopupView, [
+ i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
+ () => {
+ const
+ sFromFolderFullName = this.draftsFolder(),
+ aUidForRemove = [this.draftUid()];
+ messagesDeleteHelper(sFromFolderFullName, aUidForRemove);
+ MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, aUidForRemove);
+ this.close();
+ }
+ ]);
+ }
+
+ onClose() {
+ this.skipCommand();
+ return false;
}
skipCommand() {
this.bSkipNextHide = true;
if (
- this.modalVisibility() &&
!this.saving() &&
!this.sending() &&
- !FolderUserStore.draftFolderNotEnabled() &&
+ !FolderUserStore.draftsFolderNotEnabled() &&
SettingsUserStore.allowDraftAutosave()
) {
this.saveCommand();
@@ -488,8 +670,8 @@ class ComposePopupView extends AbstractViewPopup {
autosaveStart() {
clearTimeout(this.iTimer);
this.iTimer = setTimeout(()=>{
- if (this.modalVisibility()
- && !FolderUserStore.draftFolderNotEnabled()
+ if (this.modalVisible()
+ && !FolderUserStore.draftsFolderNotEnabled()
&& SettingsUserStore.allowDraftAutosave()
&& !this.isEmptyForm(false)
&& !this.saving()
@@ -503,138 +685,75 @@ class ComposePopupView extends AbstractViewPopup {
}, 60000);
}
+ // getAutocomplete
emailsSource(oData, fResponse) {
- rl.app.getAutocomplete(oData.term, aData => fResponse(aData.map(oEmailItem => oEmailItem.toLine(false))));
- }
-
- openOpenPgpPopup() {
- if (PgpUserStore.capaOpenPGP() && !this.oEditor.isHtml()) {
- showScreenPopup(ComposeOpenPgpPopupView, [
- result => this.editor(editor => editor.setPlain(result)),
- this.oEditor.getData(false),
- this.currentIdentity(),
- this.to(),
- this.cc(),
- this.bcc()
- ]);
- }
+ Remote.request('Suggestions',
+ (iError, data) => {
+ if (!iError && isArray(data.Result)) {
+ fResponse(
+ data.Result.map(item => (item && item[0] ? (new EmailModel(item[0], item[1])).toLine(false) : null))
+ .filter(v => v)
+ );
+ } else if (Notification.RequestAborted !== iError) {
+ fResponse([]);
+ }
+ },
+ {
+ Query: oData.term
+// ,Page: 1
+ },
+ null,
+ '',
+ ['Suggestions']
+ );
}
reloadDraftFolder() {
- const draftFolder = FolderUserStore.draftFolder();
- if (draftFolder && UNUSED_OPTION_VALUE !== draftFolder) {
- setFolderHash(draftFolder, '');
- if (FolderUserStore.currentFolderFullNameRaw() === draftFolder) {
- rl.app.reloadMessageList(true);
+ const draftsFolder = FolderUserStore.draftsFolder();
+ if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) {
+ setFolderHash(draftsFolder, '');
+ if (FolderUserStore.currentFolderFullName() === draftsFolder) {
+ MessagelistUserStore.reload(true);
} else {
- rl.app.folderInformation(draftFolder);
+ rl.app.folderInformation(draftsFolder);
}
}
}
findIdentityByMessage(composeType, message) {
- let resultIndex = 1000,
- resultIdentity = null;
- const identities = IdentityUserStore(),
- identitiesCache = {},
- fEachHelper = (item) => {
- if (item && item.email && identitiesCache[item.email]) {
- if (!resultIdentity || resultIndex > identitiesCache[item.email][1]) {
- resultIdentity = identitiesCache[item.email][0];
- resultIndex = identitiesCache[item.email][1];
- }
- }
- };
-
- identities.forEach((item, index) => identitiesCache[item.email()] = [item, index]);
+ let resultIdentity = null;
+ const find = addresses => {
+ addresses = addresses.map(item => item.email);
+ return IdentityUserStore.find(item => addresses.includes(item.email()));
+ };
if (message) {
switch (composeType) {
- case ComposeType.Empty:
- break;
case ComposeType.Reply:
case ComposeType.ReplyAll:
case ComposeType.Forward:
case ComposeType.ForwardAsAttachment:
- message.to.concat(message.cc, message.bcc).forEach(fEachHelper);
- if (!resultIdentity) {
- message.deliveredTo.forEach(fEachHelper);
- }
+ resultIdentity = find(message.to.concat(message.cc, message.bcc))/* || find(message.deliveredTo)*/;
break;
case ComposeType.Draft:
- message.from.concat(message.replyTo).forEach(fEachHelper);
+ resultIdentity = find(message.from.concat(message.replyTo));
break;
// no default
+// case ComposeType.Empty:
}
}
- return resultIdentity || identities[0] || null;
+ return resultIdentity || IdentityUserStore()[0] || null;
}
selectIdentity(identity) {
- if (identity && identity.item) {
- this.currentIdentity(identity.item);
- this.setSignatureFromIdentity(identity.item);
+ identity = identity && identity.item;
+ if (identity) {
+ this.currentIdentity(identity);
+ this.setSignatureFromIdentity(identity);
}
}
- sendMessageResponse(iError, data) {
-
- this.sending(false);
-
- if (this.modalVisibility()) {
- if (iError) {
- if (Notification.CantSaveMessage === iError) {
- this.sendSuccessButSaveError(true);
- this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
- } else {
- this.sendError(true);
- this.sendErrorDesc(getNotification(iError, data && data.ErrorMessage)
- || getNotification(Notification.CantSendMessage));
- }
- } else {
- this.closeCommand && this.closeCommand();
- }
- }
-
- this.reloadDraftFolder();
- }
-
- saveMessageResponse(iError, oData) {
- let result = false;
-
- this.saving(false);
-
- if (!iError) {
- if (oData.Result.NewFolder && oData.Result.NewUid) {
- result = true;
-
- if (this.bFromDraft) {
- const message = MessageUserStore.message();
- if (message && this.draftFolder() === message.folder && this.draftUid() === message.uid) {
- MessageUserStore.message(null);
- }
- }
-
- this.draftFolder(oData.Result.NewFolder);
- this.draftUid(oData.Result.NewUid);
-
- this.savedTime(new Date);
-
- if (this.bFromDraft) {
- setFolderHash(this.draftFolder(), '');
- }
- }
- }
-
- if (!result) {
- this.savedError(true);
- this.savedErrorDesc(getNotification(Notification.CantSaveMessage));
- }
-
- this.reloadDraftFolder();
- }
-
onHide() {
// Stop autosave
clearTimeout(this.iTimer);
@@ -647,19 +766,24 @@ class ComposePopupView extends AbstractViewPopup {
this.to.focused(false);
- rl.route.on();
+ (getFullscreenElement() === this.oContent) && exitFullscreen();
+ }
- this.resizeObserver.disconnect();
+ dropMailvelope() {
+ if (this.mailvelope) {
+ elementById('mailvelope-editor').textContent = '';
+ this.mailvelope = null;
+ }
}
editor(fOnInit) {
- if (fOnInit && this.composeEditorArea()) {
+ if (fOnInit && this.editorArea()) {
if (this.oEditor) {
fOnInit(this.oEditor);
} else {
// setTimeout(() => {
this.oEditor = new HtmlEditor(
- this.composeEditorArea(),
+ this.editorArea(),
null,
() => fOnInit(this.oEditor),
bHtml => this.isHtml(!!bHtml)
@@ -669,36 +793,27 @@ class ComposePopupView extends AbstractViewPopup {
}
}
- convertSignature(signature) {
- let fromLine = this.oLastMessage ? this.emailArrayToStringLineHelper(this.oLastMessage.from, true) : '';
- if (fromLine) {
- signature = signature.replace(/{{FROM-FULL}}/g, fromLine);
-
- if (!fromLine.includes(' ') && 0 < fromLine.indexOf('@')) {
- fromLine = fromLine.replace(/@\S+/, '');
- }
-
- signature = signature.replace(/{{FROM}}/g, fromLine);
- }
-
- return signature
- .replace(/\r/g, '')
- .replace(/\s{1,2}?{{FROM}}/g, '')
- .replace(/\s{1,2}?{{FROM-FULL}}/g, '')
- .replace(/{{DATE}}/g, new Date().format('LLLL'))
- .replace(/{{TIME}}/g, new Date().format('LT'))
- .replace(/{{MOMENT:[^}]+}}/g, '');
- }
-
setSignatureFromIdentity(identity) {
if (identity) {
this.editor(editor => {
- let signature = identity.signature(),
- isHtml = signature && ':HTML:' === signature.substr(0, 6);
-
- editor.setSignature(
- this.convertSignature(isHtml ? signature.substr(6) : signature),
- isHtml, !!identity.signatureInsertBefore());
+ let signature = identity.signature() || '',
+ isHtml = ':HTML:' === signature.slice(0, 6),
+ fromLine = this.oLastMessage ? this.emailArrayToStringLineHelper(this.oLastMessage.from, true) : '';
+ if (fromLine) {
+ signature = signature.replace(/{{FROM-FULL}}/g, fromLine);
+ if (!fromLine.includes(' ') && 0 < fromLine.indexOf('@')) {
+ fromLine = fromLine.replace(/@\S+/, '');
+ }
+ signature = signature.replace(/{{FROM}}/g, fromLine);
+ }
+ signature = (isHtml ? signature.slice(6) : signature)
+ .replace(/\r/g, '')
+ .replace(/\s{1,2}?{{FROM}}/g, '')
+ .replace(/\s{1,2}?{{FROM-FULL}}/g, '')
+ .replace(/{{DATE}}/g, new Date().format('LLLL'))
+ .replace(/{{TIME}}/g, new Date().format('LT'))
+ .replace(/{{MOMENT:[^}]+}}/g, '');
+ editor.setSignature(signature, isHtml, !!identity.signatureInsertBefore());
});
}
}
@@ -713,14 +828,10 @@ class ComposePopupView extends AbstractViewPopup {
* @param {string=} sCustomPlainText = null
*/
onShow(type, oMessageOrArray, aToEmails, aCcEmails, aBccEmails, sCustomSubject, sCustomPlainText) {
- rl.route.off();
-
- const ro = this.resizeObserver;
- ro.observe(ro.compose);
- ro.observe(ro.header);
-
this.autosaveStart();
+ this.viewModelDom.dataset.wysiwyg = SettingsUserStore.editorDefaultType();
+
if (AppUserStore.composeInEdit()) {
type = type || ComposeType.Empty;
if (ComposeType.Empty !== type) {
@@ -730,8 +841,6 @@ class ComposePopupView extends AbstractViewPopup {
this.initOnShow(type, oMessageOrArray, aToEmails, aCcEmails, aBccEmails, sCustomSubject, sCustomPlainText);
},
null,
- null,
- null,
false
]);
} else {
@@ -746,12 +855,9 @@ class ComposePopupView extends AbstractViewPopup {
} else {
this.initOnShow(type, oMessageOrArray, aToEmails, aCcEmails, aBccEmails, sCustomSubject, sCustomPlainText);
}
- }
- onWarmUp() {
- if (this.modalVisibility && !this.modalVisibility()) {
- this.editor(editor => editor.modeWysiwyg());
- }
+// (navigator.standalone || matchMedia('(display-mode: standalone)').matches || matchMedia('(display-mode: fullscreen)').matches) &&
+ ThemeStore.isMobile() && this.oContent.requestFullscreen && this.oContent.requestFullscreen();
}
/**
@@ -759,7 +865,7 @@ class ComposePopupView extends AbstractViewPopup {
* @param {Array} emails
*/
addEmailsTo(fKoValue, emails) {
- if (isNonEmptyArray(emails)) {
+ if (arrayLength(emails)) {
const value = fKoValue().trim(),
values = emails.map(item => item ? item.toLine(false) : null)
.validUnique();
@@ -780,7 +886,7 @@ class ComposePopupView extends AbstractViewPopup {
}
isPlainEditor() {
- let type = this.editorDefaultType();
+ let type = SettingsUserStore.editorDefaultType();
return EditorDefaultType.Html !== type && EditorDefaultType.HtmlForced !== type;
}
@@ -800,7 +906,6 @@ class ComposePopupView extends AbstractViewPopup {
sDate = '',
sSubject = '',
sText = '',
- sReplyTitle = '',
identity = null,
aDraftInfo = null,
message = null;
@@ -812,11 +917,11 @@ class ComposePopupView extends AbstractViewPopup {
oMessageOrArray = oMessageOrArray || null;
if (oMessageOrArray) {
message =
- isArray(oMessageOrArray) && 1 === oMessageOrArray.length
+ 1 === arrayLength(oMessageOrArray)
? oMessageOrArray[0]
- : !isArray(oMessageOrArray)
- ? oMessageOrArray
- : null;
+ : isArray(oMessageOrArray)
+ ? null
+ : oMessageOrArray;
}
this.oLastMessage = message;
@@ -832,15 +937,15 @@ class ComposePopupView extends AbstractViewPopup {
excludeEmail[identity.email()] = true;
}
- if (isNonEmptyArray(aToEmails)) {
+ if (arrayLength(aToEmails)) {
this.to(this.emailArrayToStringLineHelper(aToEmails));
}
- if (isNonEmptyArray(aCcEmails)) {
+ if (arrayLength(aCcEmails)) {
this.cc(this.emailArrayToStringLineHelper(aCcEmails));
}
- if (isNonEmptyArray(aBccEmails)) {
+ if (arrayLength(aBccEmails)) {
this.bcc(this.emailArrayToStringLineHelper(aBccEmails));
}
@@ -848,7 +953,6 @@ class ComposePopupView extends AbstractViewPopup {
sDate = timestampToString(message.dateTimeStampInUTC(), 'FULL');
sSubject = message.subject();
aDraftInfo = message.aDraftInfo;
- sText = message.bodyAsHTML();
let resplyAllParts = null;
switch (lineComposeType) {
@@ -899,13 +1003,13 @@ class ComposePopupView extends AbstractViewPopup {
this.bFromDraft = true;
- this.draftFolder(message.folder);
+ this.draftsFolder(message.folder);
this.draftUid(message.uid);
this.subject(sSubject);
this.prepareMessageAttachments(message, lineComposeType);
- this.aDraftInfo = isNonEmptyArray(aDraftInfo) && 3 === aDraftInfo.length ? aDraftInfo : null;
+ this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null;
this.sInReplyTo = message.sInReplyTo;
this.sReferences = message.sReferences;
break;
@@ -919,72 +1023,70 @@ class ComposePopupView extends AbstractViewPopup {
this.subject(sSubject);
this.prepareMessageAttachments(message, lineComposeType);
- this.aDraftInfo = isNonEmptyArray(aDraftInfo) && 3 === aDraftInfo.length ? aDraftInfo : null;
+ this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null;
this.sInReplyTo = message.sInReplyTo;
this.sReferences = message.sReferences;
break;
// no default
}
+ sText = message.bodyAsHTML();
+ let encrypted;
+
switch (lineComposeType) {
case ComposeType.Reply:
case ComposeType.ReplyAll:
sFrom = message.fromToLine(false, true);
- sReplyTitle = i18n('COMPOSE/REPLY_MESSAGE_TITLE', {
- DATETIME: sDate,
- EMAIL: sFrom
- });
-
- sText = sText.replace(/ ]+>/g, '').replace(/]+><\/a>/g, '').trim();
- sText = ' ' + sReplyTitle + ':' + sText + ' ';
-
+ sText = '' + i18n('COMPOSE/REPLY_MESSAGE_TITLE', { DATETIME: sDate, EMAIL: sFrom })
+ + ':
'
+ + sText.replace(/ ]+>/g, '').replace(/]+><\/a>/g, '').trim()
+ + ' ';
break;
case ComposeType.Forward:
sFrom = message.fromToLine(false, true);
sTo = message.toToLine(false, true);
sCc = message.ccToLine(false, true);
- sText =
- ' ' +
- i18n('COMPOSE/FORWARD_MESSAGE_TOP_TITLE') +
- ' ' +
- i18n('GLOBAL/FROM') +
- ': ' +
- sFrom +
- ' ' +
- i18n('GLOBAL/TO') +
- ': ' +
- sTo +
- (sCc.length ? ' ' + i18n('GLOBAL/CC') + ': ' + sCc : '') +
- ' ' +
- i18n('COMPOSE/FORWARD_MESSAGE_TOP_SENT') +
- ': ' +
- encodeHtml(sDate) +
- ' ' +
- i18n('GLOBAL/SUBJECT') +
- ': ' +
- encodeHtml(sSubject) +
- ' ' +
- sText.trim() +
- ' ';
+ sText = '' + i18n('COMPOSE/FORWARD_MESSAGE_TOP_TITLE') + '
'
+ + i18n('GLOBAL/FROM') + ': ' + sFrom
+ + '
'
+ + i18n('GLOBAL/TO') + ': ' + sTo
+ + (sCc.length ? '
' + i18n('GLOBAL/CC') + ': ' + sCc : '')
+ + '
'
+ + i18n('COMPOSE/FORWARD_MESSAGE_TOP_SENT')
+ + ': '
+ + encodeHtml(sDate)
+ + '
'
+ + i18n('GLOBAL/SUBJECT')
+ + ': '
+ + encodeHtml(sSubject)
+ + '
'
+ + sText.trim()
+ + '
';
break;
case ComposeType.ForwardAsAttachment:
sText = '';
break;
- // no default
+ default:
+ encrypted = PgpUserStore.isEncrypted(sText);
+ if (encrypted) {
+ sText = message.plain();
+ }
}
this.editor(editor => {
- editor.setHtml(sText);
+ encrypted || editor.setHtml(sText);
- if (
- EditorDefaultType.PlainForced === this.editorDefaultType() ||
- (!message.isHtml() && EditorDefaultType.HtmlForced !== this.editorDefaultType())
+ if (encrypted
+ || EditorDefaultType.PlainForced === SettingsUserStore.editorDefaultType()
+ || (!message.isHtml() && EditorDefaultType.HtmlForced !== SettingsUserStore.editorDefaultType())
) {
editor.modePlain();
}
+ !encrypted || editor.setPlain(sText);
+
if (identity && ComposeType.Draft !== lineComposeType && ComposeType.EditAsNew !== lineComposeType) {
this.setSignatureFromIdentity(identity);
}
@@ -1009,7 +1111,7 @@ class ComposePopupView extends AbstractViewPopup {
this.setFocusInPopup();
});
- } else if (isNonEmptyArray(oMessageOrArray)) {
+ } else if (arrayLength(oMessageOrArray)) {
oMessageOrArray.forEach(item => this.addMessageAsAttachment(item));
this.editor(editor => {
@@ -1030,31 +1132,37 @@ class ComposePopupView extends AbstractViewPopup {
}
const downloads = this.getAttachmentsDownloadsForUpload();
- if (isNonEmptyArray(downloads)) {
- Remote.messageUploadAttachments((iError, oData) => {
- if (!iError) {
- Object.entries(oData.Result).forEach(([tempName, id]) => {
- const attachment = this.getAttachmentById(id);
- if (attachment) {
- attachment.tempName(tempName);
- attachment
- .waiting(false)
- .uploading(false)
- .complete(true);
- }
- });
- } else {
- this.attachments.forEach(attachment => {
- if (attachment && attachment.fromMessage) {
- attachment
- .waiting(false)
- .uploading(false)
- .complete(true)
- .error(getUploadErrorDescByCode(UploadErrorCode.NoFileUploaded));
- }
- });
- }
- }, downloads);
+ if (arrayLength(downloads)) {
+ Remote.request('MessageUploadAttachments',
+ (iError, oData) => {
+ if (!iError) {
+ forEachObjectEntry(oData.Result, (tempName, id) => {
+ const attachment = this.getAttachmentById(id);
+ if (attachment) {
+ attachment.tempName(tempName);
+ attachment
+ .waiting(false)
+ .uploading(false)
+ .complete(true);
+ }
+ });
+ } else {
+ this.attachments.forEach(attachment => {
+ if (attachment && attachment.fromMessage) {
+ attachment
+ .waiting(false)
+ .uploading(false)
+ .complete(true)
+ .error(getUploadErrorDescByCode(UploadErrorCode.NoFileUploaded));
+ }
+ });
+ }
+ },
+ {
+ Attachments: downloads
+ },
+ 999000
+ );
}
if (identity) {
@@ -1073,79 +1181,184 @@ class ComposePopupView extends AbstractViewPopup {
}
tryToClosePopup() {
- if (!isPopupVisible(AskPopupView) && this.modalVisibility()) {
+ if (AskPopupView.hidden()) {
if (this.bSkipNextHide || (this.isEmptyForm() && !this.draftUid())) {
- this.closeCommand && this.closeCommand();
+ this.close();
} else {
showScreenPopup(AskPopupView, [
i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
- () => {
- if (this.modalVisibility()) {
- this.closeCommand && this.closeCommand();
- }
- }
+ () => this.close()
]);
}
}
}
- popupMenu(event) {
- if (event.ctrlKey || event.metaKey || 'ContextMenu' == event.key
- || (this.oEditor && !this.oEditor.hasFocus() && !inFocus())) {
+ onBuild(dom) {
+ // initUploader
+ const oJua = new Jua({
+ action: serverRequest('Upload'),
+ clickElement: dom.querySelector('#composeUploadButton'),
+ dragAndDropElement: dom.querySelector('.b-attachment-place')
+ }),
+ uploadCache = {},
+ attachmentSizeLimit = pInt(SettingsGet('AttachmentLimit'));
+
+ oJua
+ // .on('onLimitReached', (limit) => {
+ // alert(limit);
+ // })
+ .on('onDragEnter', () => {
+ this.dragAndDropOver(true);
+ })
+ .on('onDragLeave', () => {
+ this.dragAndDropOver(false);
+ })
+ .on('onBodyDragEnter', () => {
+ this.attachmentsArea();
+ this.dragAndDropVisible(true);
+ })
+ .on('onBodyDragLeave', () => {
+ this.dragAndDropVisible(false);
+ })
+ .on('onProgress', (id, loaded, total) => {
+ let item = uploadCache[id];
+ if (!item) {
+ item = this.getAttachmentById(id);
+ if (item) {
+ uploadCache[id] = item;
+ }
+ }
+
+ if (item) {
+ item.progress(Math.floor((loaded / total) * 100));
+ }
+ })
+ .on('onSelect', (sId, oData) => {
+ this.dragAndDropOver(false);
+
+ const fileName = undefined === oData.FileName ? '' : oData.FileName.toString(),
+ size = pInt(oData.Size, null),
+ attachment = new ComposeAttachmentModel(sId, fileName, size);
+
+ attachment.cancel = this.cancelAttachmentHelper(sId, oJua);
+
+ this.attachments.push(attachment);
+
+ this.attachmentsArea();
+
+ if (0 < size && 0 < attachmentSizeLimit && attachmentSizeLimit < size) {
+ attachment
+ .waiting(false)
+ .uploading(true)
+ .complete(true)
+ .error(i18n('UPLOAD/ERROR_FILE_IS_TOO_BIG'));
+
+ return false;
+ }
+
+ return true;
+ })
+ .on('onStart', (id) => {
+ let item = uploadCache[id];
+ if (!item) {
+ item = this.getAttachmentById(id);
+ if (item) {
+ uploadCache[id] = item;
+ }
+ }
+
+ if (item) {
+ item
+ .waiting(false)
+ .uploading(true)
+ .complete(false);
+ }
+ })
+ .on('onComplete', (id, result, data) => {
+ const attachment = this.getAttachmentById(id),
+ response = (data && data.Result) || {},
+ errorCode = response.ErrorCode,
+ attachmentJson = result && response.Attachment;
+
+ let error = '';
+ if (null != errorCode) {
+ error = getUploadErrorDescByCode(errorCode);
+ } else if (!attachmentJson) {
+ error = i18n('UPLOAD/ERROR_UNKNOWN');
+ }
+
+ if (attachment) {
+ if (error) {
+ attachment
+ .waiting(false)
+ .uploading(false)
+ .complete(true)
+ .error(error + '\n' + response.ErrorMessage);
+ } else if (attachmentJson) {
+ attachment
+ .waiting(false)
+ .uploading(false)
+ .complete(true);
+
+ attachment.initByUploadJson(attachmentJson);
+ }
+
+ if (undefined === uploadCache[id]) {
+ delete uploadCache[id];
+ }
+ }
+ });
+
+ this.addAttachmentEnabled(true);
+
+ addShortcut('q', 'meta', ScopeCompose, ()=>false);
+ addShortcut('w', 'meta', ScopeCompose, ()=>false);
+
+ addShortcut('m', 'meta', ScopeCompose, () => {
this.identitiesDropdownTrigger(true);
return false;
- }
- return true;
- }
-
- onBuild(dom) {
- this.initUploader();
-
- shortcuts.add('q', 'meta', Scope.Compose, ()=>false);
- shortcuts.add('w', 'meta', Scope.Compose, ()=>false);
-
- shortcuts.add('m,contextmenu', '', Scope.Compose, e => this.popupMenu(e));
- shortcuts.add('m', 'ctrl', Scope.Compose, e => this.popupMenu(e));
-
- shortcuts.add('escape,close', '', Scope.Compose, () => {
- this.skipCommand();
- return false;
});
- shortcuts.add('arrowdown', 'meta', Scope.Compose, () => {
+
+ addShortcut('arrowdown', 'meta', ScopeCompose, () => {
this.skipCommand();
return false;
});
- shortcuts.add('s', 'meta', Scope.Compose, () => {
+ addShortcut('s', 'meta', ScopeCompose, () => {
this.saveCommand();
return false;
});
- shortcuts.add('save', Scope.Compose, () => {
+ addShortcut('save', '', ScopeCompose, () => {
this.saveCommand();
return false;
});
if (Settings.app('allowCtrlEnterOnCompose')) {
- shortcuts.add('enter', 'meta', Scope.Compose, () => {
+ addShortcut('enter', 'meta', ScopeCompose, () => {
this.sendCommand();
return false;
});
}
- shortcuts.add('mailsend', '', Scope.Compose, () => {
+ addShortcut('mailsend', '', ScopeCompose, () => {
this.sendCommand();
return false;
});
- shortcuts.add('escape,close', 'shift', Scope.Compose, () => {
- this.modalVisibility() && this.tryToClosePopup();
+ addShortcut('escape,close', 'shift', ScopeCompose, () => {
+ this.tryToClosePopup();
return false;
});
- const ro = this.resizeObserver;
- ro.compose = dom.querySelector('.b-compose');
- ro.header = dom.querySelector('.b-header');
- ro.toolbar = dom.querySelector('.b-header-toolbar');
- ro.els = [dom.querySelector('.textAreaParent'), dom.querySelector('.attachmentAreaParent')];
+ this.editor(editor => editor[this.isPlainEditor()?'modePlain':'modeWysiwyg']());
+
+ // Fullscreen must be on app, else other popups fail
+ const el = doc.getElementById('rl-app');
+ this.oContent = initFullscreen(el, () =>
+ ThemeStore.isMobile()
+ && this.modalVisible()
+ && (getFullscreenElement() !== el)
+ && this.skipCommand()
+ );
}
/**
@@ -1161,142 +1374,15 @@ class ComposePopupView extends AbstractViewPopup {
const attachment = this.getAttachmentById(id);
if (attachment) {
this.attachments.remove(attachment);
- delegateRunOnDestroy(attachment);
oJua && oJua.cancel(id);
}
};
}
- initUploader() {
- if (this.composeUploaderButton()) {
- const uploadCache = {},
- attachmentSizeLimit = pInt(SettingsGet('AttachmentLimit')),
- oJua = new Jua({
- action: serverRequest('Upload'),
- name: 'uploader',
- queueSize: 2,
- multipleSizeLimit: 50,
- clickElement: this.composeUploaderButton(),
- dragAndDropElement: this.composeUploaderDropPlace()
- });
-
- if (oJua) {
- oJua
- // .on('onLimitReached', (limit) => {
- // alert(limit);
- // })
- .on('onDragEnter', () => {
- this.dragAndDropOver(true);
- })
- .on('onDragLeave', () => {
- this.dragAndDropOver(false);
- })
- .on('onBodyDragEnter', () => {
- this.attachmentsPlace(true);
- this.dragAndDropVisible(true);
- })
- .on('onBodyDragLeave', () => {
- this.dragAndDropVisible(false);
- })
- .on('onProgress', (id, loaded, total) => {
- let item = uploadCache[id];
- if (!item) {
- item = this.getAttachmentById(id);
- if (item) {
- uploadCache[id] = item;
- }
- }
-
- if (item) {
- item.progress(Math.floor((loaded / total) * 100));
- }
- })
- .on('onSelect', (sId, oData) => {
- this.dragAndDropOver(false);
-
- const fileName = undefined === oData.FileName ? '' : oData.FileName.toString(),
- size = pInt(oData.Size, null),
- attachment = new ComposeAttachmentModel(sId, fileName, size);
-
- attachment.cancel = this.cancelAttachmentHelper(sId, oJua);
-
- this.attachments.push(attachment);
-
- this.attachmentsPlace(true);
-
- if (0 < size && 0 < attachmentSizeLimit && attachmentSizeLimit < size) {
- attachment
- .waiting(false)
- .uploading(true)
- .complete(true)
- .error(i18n('UPLOAD/ERROR_FILE_IS_TOO_BIG'));
-
- return false;
- }
-
- return true;
- })
- .on('onStart', (id) => {
- let item = uploadCache[id];
- if (!item) {
- item = this.getAttachmentById(id);
- if (item) {
- uploadCache[id] = item;
- }
- }
-
- if (item) {
- item
- .waiting(false)
- .uploading(true)
- .complete(false);
- }
- })
- .on('onComplete', (id, result, data) => {
- const attachment = this.getAttachmentById(id),
- errorCode = data && data.Result && data.Result.ErrorCode ? data.Result.ErrorCode : null,
- attachmentJson = result && data && data.Result && data.Result.Attachment ? data.Result.Attachment : null;
-
- let error = '';
- if (null !== errorCode) {
- error = getUploadErrorDescByCode(errorCode);
- } else if (!attachmentJson) {
- error = i18n('UPLOAD/ERROR_UNKNOWN');
- }
-
- if (attachment) {
- if (error && error.length) {
- attachment
- .waiting(false)
- .uploading(false)
- .complete(true)
- .error(error);
- } else if (attachmentJson) {
- attachment
- .waiting(false)
- .uploading(false)
- .complete(true);
-
- attachment.initByUploadJson(attachmentJson);
- }
-
- if (undefined === uploadCache[id]) {
- delete uploadCache[id];
- }
- }
- });
-
- this.addAttachmentEnabled(true).dragAndDropEnabled(true);
- } else {
- this.addAttachmentEnabled(false).dragAndDropEnabled(false);
- }
- }
- }
-
/**
* @returns {Object}
*/
- prepearAttachmentsForSendOrSave() {
+ prepareAttachmentsForSendOrSave() {
const result = {};
this.attachments.forEach(item => {
if (item && item.complete() && item.tempName() && item.enabled()) {
@@ -1313,7 +1399,7 @@ class ComposePopupView extends AbstractViewPopup {
addMessageAsAttachment(message) {
if (message) {
let temp = message.subject();
- temp = '.eml' === temp.substr(-4).toLowerCase() ? temp : temp + '.eml';
+ temp = '.eml' === temp.slice(-4).toLowerCase() ? temp : temp + '.eml';
const attachment = new ComposeAttachmentModel(message.requestHash, temp, message.size());
@@ -1346,7 +1432,7 @@ class ComposePopupView extends AbstractViewPopup {
this.attachments.push(attachment);
- this.attachmentsPlace(true);
+ this.attachmentsArea();
return attachment;
}
@@ -1421,7 +1507,7 @@ class ComposePopupView extends AbstractViewPopup {
this.requestReadReceipt(false);
this.markAsImportant(false);
- this.attachmentsPlace(false);
+ this.bodyArea();
this.aDraftInfo = null;
this.sInReplyTo = '';
@@ -1439,19 +1525,23 @@ class ComposePopupView extends AbstractViewPopup {
this.showBcc(false);
this.showReplyTo(false);
- delegateRunOnDestroy(this.attachments());
+ this.pgpSign(false);
+ this.pgpEncrypt(false);
+
this.attachments([]);
this.dragAndDropOver(false);
this.dragAndDropVisible(false);
- this.draftFolder('');
- this.draftUid('');
+ this.draftsFolder('');
+ this.draftUid(0);
this.sending(false);
this.saving(false);
this.oEditor && this.oEditor.clear();
+
+ this.dropMailvelope();
}
/**
@@ -1463,15 +1553,80 @@ class ComposePopupView extends AbstractViewPopup {
);
}
- resizerTrigger() {
- let ro = this.resizeObserver,
- height = Math.max(200, ro.compose.clientHeight - ro.header.offsetHeight - ro.toolbar.offsetHeight) + 'px';
- if (ro.height !== height) {
- ro.height = height;
- ro.els.forEach(element => element.style.height = height);
- this.oEditor && this.oEditor.resize();
+ mailvelopeArea() {
+ if (!this.mailvelope) {
+ /**
+ * Creates an iframe with an editor for a new encrypted mail.
+ * The iframe will be injected into the container identified by selector.
+ * https://mailvelope.github.io/mailvelope/Editor.html
+ */
+ let text = this.oEditor.getData(),
+ encrypted = PgpUserStore.isEncrypted(text),
+ size = SettingsGet('PhpUploadSizes')['post_max_size'],
+ quota = pInt(size);
+ switch (size.slice(-1)) {
+ case 'G': quota *= 1024; // fallthrough
+ case 'M': quota *= 1024; // fallthrough
+ case 'K': quota *= 1024;
+ }
+ // Issue: can't select signing key
+// this.pgpSign(this.pgpSign() || confirm('Sign this message?'));
+ mailvelope.createEditorContainer('#mailvelope-editor', PgpUserStore.mailvelopeKeyring, {
+ // https://mailvelope.github.io/mailvelope/global.html#EditorContainerOptions
+ quota: Math.max(2048, (quota / 1024)) - 48, // (text + attachments) limit in kilobytes
+ armoredDraft: encrypted ? text : '', // Ascii Armored PGP Text Block
+ predefinedText: encrypted ? '' : (this.oEditor.isHtml() ? htmlToPlain(text) : text),
+/*
+ quotedMail: '', // Ascii Armored PGP Text Block mail that should be quoted
+ quotedMailIndent: true, // if true the quoted mail will be indented (default: true)
+ quotedMailHeader: '', // header to be added before the quoted mail
+ keepAttachments: false, // add attachments of quotedMail to editor (default: false)
+ // Issue: can't select signing key
+ signMsg: this.pgpSign()
+*/
+ }).then(editor => this.mailvelope = editor);
}
+ this.viewArea('mailvelope');
+ }
+ attachmentsArea() {
+ this.viewArea('attachments');
+ }
+ bodyArea() {
+ this.viewArea('body');
+ }
+
+ allRecipients() {
+ return [
+ // From/sender is also recipient (Sent mailbox)
+// this.currentIdentity().email(),
+ this.from(),
+ this.to(),
+ this.cc(),
+ this.bcc()
+ ].join(',').split(',').map(value => getEmail(value.trim())).validUnique();
+ }
+
+ initPgpEncrypt() {
+ const recipients = this.allRecipients();
+ PgpUserStore.hasPublicKeyForEmails(recipients).then(result => {
+ console.log({canPgpEncrypt:result});
+ this.canPgpEncrypt(result);
+ });
+ PgpUserStore.mailvelopeHasPublicKeyForEmails(recipients).then(result => {
+ console.log({canMailvelope:result});
+ this.canMailvelope(result);
+ if (!result) {
+ 'mailvelope' === this.viewArea() && this.bodyArea();
+// this.dropMailvelope();
+ }
+ });
+ }
+
+ togglePgpSign() {
+ this.pgpSign(!this.pgpSign()/* && this.canPgpSign()*/);
+ }
+
+ togglePgpEncrypt() {
+ this.pgpEncrypt(!this.pgpEncrypt()/* && this.canPgpEncrypt()*/);
}
}
-
-export { ComposePopupView, ComposePopupView as default };
diff --git a/dev/View/Popup/ComposeOpenPgp.js b/dev/View/Popup/ComposeOpenPgp.js
deleted file mode 100644
index 749bdfefc..000000000
--- a/dev/View/Popup/ComposeOpenPgp.js
+++ /dev/null
@@ -1,385 +0,0 @@
-import ko from 'ko';
-
-import { pString, defaultOptionsAfterRender } from 'Common/Utils';
-
-import { Scope } from 'Common/Enums';
-import { i18n } from 'Common/Translator';
-
-import { PgpUserStore } from 'Stores/User/Pgp';
-
-import { EmailModel } from 'Model/Email';
-
-import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-const KEY_NAME_SUBSTR = -8,
- i18nPGP = (key, params) => i18n('PGP_NOTIFICATIONS/' + key, params);
-
-class ComposeOpenPgpPopupView extends AbstractViewPopup {
- constructor() {
- super('ComposeOpenPgp');
-
- this.publicKeysOptionsCaption = i18nPGP('ADD_A_PUBLICK_KEY');
- this.privateKeysOptionsCaption = i18nPGP('SELECT_A_PRIVATE_KEY');
-
- this.addObservables({
- notification: '',
-
- sign: false,
- encrypt: false,
-
- password: '',
-
- text: '',
- selectedPrivateKey: null,
- selectedPublicKey: null,
-
- signKey: null,
-
- submitRequest: false
- });
- this.encryptKeys = ko.observableArray();
-
- this.addComputables({
- encryptKeysView: () => this.encryptKeys.map(oKey => (oKey ? oKey.key : null)).filter(v => v),
-
- privateKeysOptions: () => {
- const opts = PgpUserStore.openpgpkeysPrivate().map(oKey => {
- if (this.signKey() && this.signKey().key.id === oKey.id) {
- return null;
- }
- return oKey.users.map(user => ({
- id: oKey.guid,
- name: '(' + oKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user,
- key: oKey
- }));
- });
-
- return opts.flat().filter(v => v);
- },
-
- publicKeysOptions: () => {
- const opts = PgpUserStore.openpgpkeysPublic().map(oKey => {
- if (this.encryptKeysView().includes(oKey)) {
- return null;
- }
- return oKey.users.map(user => ({
- id: oKey.guid,
- name: '(' + oKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user,
- key: oKey
- }));
- });
- return opts.flat().filter(v => v);
- }
- });
-
- this.resultCallback = null;
-
- this.selectedPrivateKey.subscribe((value) => {
- if (value) {
- this.selectCommand();
- this.updateCommand();
- }
- });
-
- this.selectedPublicKey.subscribe((value) => {
- if (value) {
- this.addCommand();
- }
- });
-
- this.defaultOptionsAfterRender = defaultOptionsAfterRender;
-
- this.deletePublickKey = this.deletePublickKey.bind(this);
-
- decorateKoCommands(this, {
- doCommand: self => !self.submitRequest() && (self.sign() || self.encrypt()),
- selectCommand: 1,
- addCommand: 1,
- updateCommand: 1,
- });
- }
-
- doCommand() {
- let result = true,
- privateKey = null,
- aPublicKeys = [];
-
- this.submitRequest(true);
-
- if (result && this.sign()) {
- if (!this.signKey()) {
- this.notification(i18nPGP('NO_PRIVATE_KEY_FOUND'));
- result = false;
- } else if (!this.signKey().key) {
- this.notification(
- i18nPGP('NO_PRIVATE_KEY_FOUND_FOR', {
- EMAIL: this.signKey().email
- })
- );
-
- result = false;
- }
-
- if (result) {
- const privateKeys = this.signKey().key.getNativeKeys();
- privateKey = privateKeys[0] || null;
-
- try {
- if (privateKey) {
- privateKey.decrypt(pString(this.password()));
- }
- } catch (e) {
- privateKey = null;
- }
-
- if (!privateKey) {
- this.notification(i18nPGP('NO_PRIVATE_KEY_FOUND'));
- result = false;
- }
- }
- }
-
- if (result && this.encrypt()) {
- if (this.encryptKeys.length) {
- aPublicKeys = [];
-
- this.encryptKeys.forEach(oKey => {
- if (oKey && oKey.key) {
- aPublicKeys = aPublicKeys.concat(oKey.key.getNativeKeys().flat(Infinity).filter(v => v));
- } else if (oKey && oKey.email) {
- this.notification(
- i18nPGP('NO_PUBLIC_KEYS_FOUND_FOR', {
- EMAIL: oKey.email
- })
- );
-
- result = false;
- }
- });
-
- if (result && (!aPublicKeys.length || this.encryptKeys.length !== aPublicKeys.length)) {
- result = false;
- }
- } else {
- this.notification(i18nPGP('NO_PUBLIC_KEYS_FOUND'));
- result = false;
- }
- }
-
- if (result && this.resultCallback) {
- setTimeout(() => {
- let pgpPromise = null;
-
- try {
- if (aPublicKeys.length) {
- if (privateKey) {
- pgpPromise = PgpUserStore.openpgp.encrypt({
- data: this.text(),
- publicKeys: aPublicKeys,
- privateKeys: [privateKey]
- });
- } else {
- pgpPromise = PgpUserStore.openpgp.encrypt({
- data: this.text(),
- publicKeys: aPublicKeys
- });
- }
- } else if (privateKey) {
- pgpPromise = PgpUserStore.openpgp.sign({
- data: this.text(),
- privateKeys: [privateKey]
- });
- }
- } catch (e) {
- console.log(e);
-
- this.notification(
- i18nPGP('PGP_ERROR', {
- ERROR: '' + e
- })
- );
- }
-
- if (pgpPromise) {
- try {
- pgpPromise
- .then((mData) => {
- this.resultCallback(mData.data);
- this.cancelCommand();
- })
- .catch((e) => {
- this.notification(
- i18nPGP('PGP_ERROR', {
- ERROR: '' + e
- })
- );
- });
- } catch (e) {
- this.notification(
- i18nPGP('PGP_ERROR', {ERROR: '' + e})
- );
- }
- }
-
- this.submitRequest(false);
- }, 20);
- } else {
- this.submitRequest(false);
- }
-
- return result;
- }
-
- selectCommand() {
- const keyId = this.selectedPrivateKey(),
- option = keyId ? this.privateKeysOptions().find(item => item && keyId === item.id) : null;
-
- if (option) {
- this.signKey({
- empty: !option.key,
- selected: ko.observable(!!option.key),
- users: option.key.users,
- hash: option.key.id.substr(KEY_NAME_SUBSTR).toUpperCase(),
- key: option.key
- });
- }
- }
-
- addCommand() {
- const keyId = this.selectedPublicKey(),
- option = keyId ? this.publicKeysOptions().find(item => item && keyId === item.id) : null;
-
- if (option) {
- this.encryptKeys.push({
- empty: !option.key,
- selected: ko.observable(!!option.key),
- removable: ko.observable(!this.sign() || !this.signKey() || this.signKey().key.id !== option.key.id),
- users: option.key.users,
- hash: option.key.id.substr(KEY_NAME_SUBSTR).toUpperCase(),
- key: option.key
- });
- }
- }
-
- updateCommand() {
- this.encryptKeys.forEach(oKey =>
- oKey.removable(!this.sign() || !this.signKey() || this.signKey().key.id !== oKey.key.id)
- );
- }
-
- deletePublickKey(publicKey) {
- this.encryptKeys.remove(publicKey);
- }
-
- clearPopup() {
- this.notification('');
-
- this.sign(false);
- this.encrypt(false);
-
- this.password('');
-
- this.signKey(null);
- this.encryptKeys([]);
- this.text('');
-
- this.resultCallback = null;
- }
-
- onBuild() {
-// shortcuts.add('tab', 'shift', Scope.ComposeOpenPgp, () => {
- shortcuts.add('tab', '', Scope.ComposeOpenPgp, () => {
- let btn = this.querySelector('.inputPassword');
- if (btn.matches(':focus')) {
- btn = this.querySelector('.buttonDo');
- }
- btn.focus();
- return false;
- });
- }
-
- onHideWithDelay() {
- this.clearPopup();
- }
-
- onShowWithDelay() {
- this.querySelector(this.sign() ? '.inputPassword' : '.buttonDo').focus();
- }
-
- onShow(fCallback, sText, identity, sTo, sCc, sBcc) {
- this.clearPopup();
-
- let rec = [],
- emailLine = '';
-
- const email = new EmailModel();
-
- this.resultCallback = fCallback;
-
- if (sTo) {
- rec.push(sTo);
- }
-
- if (sCc) {
- rec.push(sCc);
- }
-
- if (sBcc) {
- rec.push(sBcc);
- }
-
- rec = rec.join(', ').split(',');
- rec = rec.map(value => {
- email.clear();
- email.parse(value.trim());
- return email.email || false;
- }).filter(v => v);
-
- if (identity && identity.email()) {
- emailLine = identity.email();
- rec.unshift(emailLine);
-
- const keys = PgpUserStore.findAllPrivateKeysByEmailNotNative(emailLine);
- if (keys && keys[0]) {
- this.signKey({
- users: keys[0].users || [emailLine],
- hash: keys[0].id.substr(KEY_NAME_SUBSTR).toUpperCase(),
- key: keys[0]
- });
- }
- }
-
- if (this.signKey()) {
- this.sign(true);
- }
-
- if (rec.length) {
- this.encryptKeys(
- rec.map(recEmail => {
- const keys = PgpUserStore.findAllPublicKeysByEmailNotNative(recEmail);
- return keys
- ? keys.map(publicKey => ({
- empty: !publicKey,
- selected: ko.observable(!!publicKey),
- removable: ko.observable(
- !this.sign() || !this.signKey() || this.signKey().key.id !== publicKey.id
- ),
- users: publicKey ? publicKey.users || [recEmail] : [recEmail],
- hash: publicKey ? publicKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() : '',
- key: publicKey
- }))
- : [];
- }).flat().validUnique(encryptKey => encryptKey.hash)
- );
-
- if (this.encryptKeys.length) {
- this.encrypt(true);
- }
- }
-
- this.text(sText);
- }
-}
-
-export { ComposeOpenPgpPopupView, ComposeOpenPgpPopupView as default };
diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js
index 6faf64ab9..bc654e85c 100644
--- a/dev/View/Popup/Contacts.js
+++ b/dev/View/Popup/Contacts.js
@@ -1,14 +1,10 @@
-import ko from 'ko';
-
-import {
- SaveSettingsStep,
- Scope
-} from 'Common/Enums';
+import { koArrayWithDestroy } from 'External/ko';
+import { SaveSettingsStep } from 'Common/Enums';
import { ComposeType } from 'Common/EnumsUser';
-
-import { isNonEmptyArray, pInt } from 'Common/Utils';
-import { delegateRunOnDestroy, computedPaginatorHelper, showMessageComposer } from 'Common/UtilsUser';
+import { registerShortcut } from 'Common/Globals';
+import { arrayLength, pInt } from 'Common/Utils';
+import { download, computedPaginatorHelper, showMessageComposer } from 'Common/UtilsUser';
import { Selector } from 'Common/Selector';
import { serverRequestRaw, serverRequest } from 'Common/Links';
@@ -23,14 +19,18 @@ import { EmailModel } from 'Model/Email';
import { ContactModel } from 'Model/Contact';
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
-import { decorateKoCommands, hideScreenPopup } from 'Knoin/Knoin';
+import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-const CONTACTS_PER_PAGE = 50,
+import { AskPopupView } from 'View/Popup/Ask';
+
+const
+ CONTACTS_PER_PAGE = 50,
+ ScopeContacts = 'Contacts',
propertyIsMail = prop => prop.isType(ContactPropertyType.Email),
propertyIsName = prop => prop.isType(ContactPropertyType.FirstName) || prop.isType(ContactPropertyType.LastName);
-class ContactsPopupView extends AbstractViewPopup {
+export class ContactsPopupView extends AbstractViewPopup {
constructor() {
super('Contacts');
@@ -66,7 +66,7 @@ class ContactsPopupView extends AbstractViewPopup {
this.contacts = ContactUserStore;
- this.viewProperties = ko.observableArray();
+ this.viewProperties = koArrayWithDestroy();
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
@@ -79,18 +79,18 @@ class ContactsPopupView extends AbstractViewPopup {
'.e-contact-item.focused'
);
- this.selector.on('onItemSelect', contact => {
- this.populateViewContact(contact || null);
+ this.selector.on('ItemSelect', contact => {
+ this.populateViewContact(contact);
if (!contact) {
this.emptySelection(true);
}
});
- this.selector.on('onItemGetUid', contact => contact ? contact.generateUid() : '');
+ this.selector.on('ItemGetUid', contact => contact ? contact.generateUid() : '');
this.bDropPageAfterDelete = false;
- // this.saveCommandDebounce = _.debounce(this.saveCommand.bind(this), 1000);
+ // this.saveCommandDebounce = this.saveCommand.bind(this).debounce(1000);
const
// propertyFocused = property => !property.isValid() && !property.focused(),
@@ -136,17 +136,16 @@ class ContactsPopupView extends AbstractViewPopup {
});
decorateKoCommands(this, {
- newCommand: 1,
+// close: self => !self.watchDirty(),
deleteCommand: self => 0 < self.contactsCheckedOrSelected().length,
newMessageCommand: self => 0 < self.contactsCheckedOrSelected().length,
- clearCommand: 1,
saveCommand: self => !self.viewSaving() && !self.viewReadOnly()
&& (self.contactHasValidName() || self.viewProperties.find(prop => propertyIsMail(prop) && prop.isValid())),
syncCommand: self => !self.contacts.syncing() && !self.contacts.importing()
});
}
- newCommand() {
+ newContact() {
this.populateViewContact(null);
this.currentContact(null);
}
@@ -163,7 +162,7 @@ class ContactsPopupView extends AbstractViewPopup {
bccEmails = null;
const aC = this.contactsCheckedOrSelected();
- if (isNonEmptyArray(aC)) {
+ if (arrayLength(aC)) {
aE = aC.map(oItem => {
if (oItem) {
const data = oItem.getNameAndEmailHelper(),
@@ -180,10 +179,10 @@ class ContactsPopupView extends AbstractViewPopup {
aE = aE.filter(value => !!value);
}
- if (isNonEmptyArray(aE)) {
+ if (arrayLength(aE)) {
this.bBackToCompose = false;
- hideScreenPopup(ContactsPopupView);
+ this.close();
switch (this.sLastComposeFocusedField) {
case 'cc':
@@ -192,7 +191,6 @@ class ContactsPopupView extends AbstractViewPopup {
case 'bcc':
bccEmails = aE;
break;
- case 'to':
default:
toEmails = aE;
break;
@@ -208,7 +206,7 @@ class ContactsPopupView extends AbstractViewPopup {
return true;
}
- clearCommand() {
+ clearSearch() {
this.search('');
}
@@ -218,7 +216,7 @@ class ContactsPopupView extends AbstractViewPopup {
const requestUid = Jua.randomId();
- Remote.contactSave(
+ Remote.request('ContactSave',
(iError, oData) => {
let res = false;
this.viewSaving(false);
@@ -244,15 +242,16 @@ class ContactsPopupView extends AbstractViewPopup {
this.watchDirty(false);
setTimeout(() => this.viewSaveTrigger(SaveSettingsStep.Idle), 1000);
}
- },
- requestUid,
- this.viewID(),
- this.viewProperties.map(oItem => oItem.toJSON())
+ }, {
+ RequestUid: requestUid,
+ Uid: this.viewID(),
+ Properties: this.viewProperties.map(oItem => oItem.toJSON())
+ }
);
}
syncCommand() {
- rl.app.contactsSync(iError => {
+ ContactUserStore.sync(iError => {
iError && alert(getNotification(iError));
this.reloadContactList(true);
@@ -317,37 +316,11 @@ class ContactsPopupView extends AbstractViewPopup {
}
exportVcf() {
- rl.app.download(serverRequestRaw('ContactsVcf'));
+ download(serverRequestRaw('ContactsVcf'), 'contacts.vcf');
}
exportCsv() {
- rl.app.download(serverRequestRaw('ContactsCsv'));
- }
-
- initUploader() {
- if (this.importUploaderButton()) {
- const j = new Jua({
- action: serverRequest('UploadContacts'),
- name: 'uploader',
- queueSize: 1,
- multipleSizeLimit: 1,
- disableMultiple: true,
- disableDocumentDropPrevent: true,
- clickElement: this.importUploaderButton()
- });
-
- if (j) {
- j.on('onStart', () => {
- ContactUserStore.importing(true);
- }).on('onComplete', (id, result, data) => {
- ContactUserStore.importing(false);
- this.reloadContactList();
- if (!id || !result || !data || !data.Result) {
- alert(i18n('CONTACTS/ERROR_IMPORT_FILE'));
- }
- });
- }
- }
+ download(serverRequestRaw('ContactsCsv'), 'contacts.csv');
}
removeCheckedOrSelectedContactsFromList() {
@@ -372,39 +345,30 @@ class ContactsPopupView extends AbstractViewPopup {
}
setTimeout(() => {
- contacts.forEach(contact => {
- ContactUserStore.remove(contact);
- delegateRunOnDestroy(contact);
- });
+ contacts.forEach(contact => ContactUserStore.remove(contact));
}, 500);
}
}
deleteSelectedContacts() {
if (this.contactsCheckedOrSelected().length) {
- Remote.contactsDelete(this.deleteResponse.bind(this), this.contactsCheckedOrSelectedUids());
-
+ Remote.request('ContactsDelete',
+ (iError, oData) => {
+ if (500 < (!iError && oData && oData.Time ? pInt(oData.Time) : 0)) {
+ this.reloadContactList(this.bDropPageAfterDelete);
+ } else {
+ setTimeout(() => this.reloadContactList(this.bDropPageAfterDelete), 500);
+ }
+ }, {
+ Uids: this.contactsCheckedOrSelectedUids().join(',')
+ }
+ );
this.removeCheckedOrSelectedContactsFromList();
}
}
- /**
- * @param {int} iError
- * @param {FetchJsonDefaultResponse} oData
- */
- deleteResponse(iError, oData) {
- if (500 < (!iError && oData && oData.Time ? pInt(oData.Time) : 0)) {
- this.reloadContactList(this.bDropPageAfterDelete);
- } else {
- setTimeout(() => {
- this.reloadContactList(this.bDropPageAfterDelete);
- }, 500);
- }
- }
-
removeProperty(oProp) {
this.viewProperties.remove(oProp);
- delegateRunOnDestroy(oProp);
}
/**
@@ -428,7 +392,6 @@ class ContactsPopupView extends AbstractViewPopup {
this.viewID(id);
-// delegateRunOnDestroy(this.viewProperties());
// this.viewProperties([]);
this.viewProperties(contact.properties);
@@ -450,12 +413,12 @@ class ContactsPopupView extends AbstractViewPopup {
}
ContactUserStore.loading(true);
- Remote.contacts(
+ Remote.request('Contacts',
(iError, data) => {
let count = 0,
list = [];
- if (!iError && isNonEmptyArray(data.Result.List)) {
+ if (!iError && arrayLength(data.Result.List)) {
data.Result.List.forEach(item => {
item = ContactModel.reviveFromJson(item);
item && list.push(item);
@@ -467,27 +430,31 @@ class ContactsPopupView extends AbstractViewPopup {
this.contactsCount(count);
- delegateRunOnDestroy(ContactUserStore());
ContactUserStore(list);
ContactUserStore.loading(false);
this.viewClearSearch(!!this.search());
},
- offset,
- CONTACTS_PER_PAGE,
- this.search()
+ {
+ Offset: offset,
+ Limit: CONTACTS_PER_PAGE,
+ Search: this.search()
+ },
+ null,
+ '',
+ ['Contacts']
);
}
onBuild(dom) {
- this.selector.init(dom.querySelector('.b-list-content'), Scope.Contacts);
+ this.selector.init(dom.querySelector('.b-list-content'), ScopeContacts);
- shortcuts.add('delete', '', Scope.Contacts, () => {
+ registerShortcut('delete', '', ScopeContacts, () => {
this.deleteCommand();
return false;
});
- shortcuts.add('c,w', '', Scope.Contacts, () => {
+ registerShortcut('c,w', '', ScopeContacts, () => {
this.newMessageCommand();
return false;
});
@@ -495,33 +462,59 @@ class ContactsPopupView extends AbstractViewPopup {
const self = this;
dom.addEventListener('click', event => {
- let el = event.target.closestWithin('.e-paginator .e-page', dom);
+ let el = event.target.closestWithin('.e-paginator a', dom);
if (el && ko.dataFor(el)) {
self.contactsPage(pInt(ko.dataFor(el).value));
self.reloadContactList();
}
});
- this.initUploader();
+ // initUploader
+
+ if (this.importUploaderButton()) {
+ const j = new Jua({
+ action: serverRequest('UploadContacts'),
+ limit: 1,
+ disableDocumentDropPrevent: true,
+ clickElement: this.importUploaderButton()
+ });
+
+ if (j) {
+ j.on('onStart', () => {
+ ContactUserStore.importing(true);
+ }).on('onComplete', (id, result, data) => {
+ ContactUserStore.importing(false);
+ this.reloadContactList();
+ if (!id || !result || !data || !data.Result) {
+ alert(i18n('CONTACTS/ERROR_IMPORT_FILE'));
+ }
+ });
+ }
+ }
+ }
+
+ onClose() {
+ if (this.watchDirty() && AskPopupView.hidden()) {
+ showScreenPopup(AskPopupView, [
+ i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
+ () => this.close()
+ ]);
+ return false;
+ }
}
onShow(bBackToCompose, sLastComposeFocusedField) {
- this.bBackToCompose = undefined === bBackToCompose ? false : !!bBackToCompose;
- this.sLastComposeFocusedField = undefined === sLastComposeFocusedField ? '' : sLastComposeFocusedField;
-
- rl.route.off();
+ this.bBackToCompose = !!bBackToCompose;
+ this.sLastComposeFocusedField = sLastComposeFocusedField;
this.reloadContactList(true);
}
onHide() {
- rl.route.on();
-
this.currentContact(null);
this.emptySelection(true);
this.search('');
this.contactsCount(0);
- delegateRunOnDestroy(ContactUserStore());
ContactUserStore([]);
this.sLastComposeFocusedField = '';
@@ -533,5 +526,3 @@ class ContactsPopupView extends AbstractViewPopup {
}
}
}
-
-export { ContactsPopupView, ContactsPopupView as default };
diff --git a/dev/View/Popup/Domain.js b/dev/View/Popup/Domain.js
index 1d36cdaaf..dbe07bc03 100644
--- a/dev/View/Popup/Domain.js
+++ b/dev/View/Popup/Domain.js
@@ -1,4 +1,4 @@
-import { pInt, pString } from 'Common/Utils';
+import { pInt, forEachObjectEntry } from 'Common/Utils';
import { i18n, getNotification } from 'Common/Translator';
import Remote from 'Remote/Admin/Fetch';
@@ -8,16 +8,34 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { DomainAdminStore } from 'Stores/Admin/Domain';
-class DomainPopupView extends AbstractViewPopup {
+const domainToParams = oDomain => ({
+ Name: oDomain.name(),
+
+ IncHost: oDomain.imapHost(),
+ IncPort: oDomain.imapPort(),
+ IncSecure: oDomain.imapSecure(),
+
+ UseSieve: oDomain.useSieve() ? 1 : 0,
+ SieveHost: oDomain.sieveHost(),
+ SievePort: oDomain.sievePort(),
+ SieveSecure: oDomain.sieveSecure(),
+
+ OutHost: oDomain.smtpHost(),
+ OutPort: oDomain.smtpPort(),
+ OutSecure: oDomain.smtpSecure(),
+ OutAuth: oDomain.smtpAuth() ? 1 : 0,
+ OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
+ });
+
+export class DomainPopupView extends AbstractViewPopup {
constructor() {
super('Domain');
+ this.addObservables(this.getDefaults());
this.addObservables({
edit: false,
+
saving: false,
- savingError: '',
- page: 'main',
- sieveSettings: false,
testing: false,
testingDone: false,
@@ -28,31 +46,9 @@ class DomainPopupView extends AbstractViewPopup {
testingSieveErrorDesc: '',
testingSmtpErrorDesc: '',
- imapServerFocus: false,
- sieveServerFocus: false,
- smtpServerFocus: false,
-
- name: '',
-
- imapServer: '',
- imapPort: '143',
- imapSecure: 0,
- imapShortLogin: false,
- useSieve: false,
- sieveServer: '',
- sievePort: '4190',
- sieveSecure: 0,
- smtpServer: '',
- smtpPort: '25',
- smtpSecure: 0,
- smtpShortLogin: false,
- smtpAuth: true,
- smtpSetSender: false,
- smtpPhpMail: false,
- whiteList: '',
- aliasName: '',
-
- enableSmartPorts: false
+ imapHostFocus: false,
+ sieveHostFocus: false,
+ smtpHostFocus: false,
});
this.addComputables({
@@ -65,7 +61,7 @@ class DomainPopupView extends AbstractViewPopup {
if (this.edit()) {
result = i18n('POPUPS_DOMAIN/TITLE_EDIT_DOMAIN', { NAME: name });
if (aliasName) {
- result += ' ← ' + aliasName;
+ result += ' ⫘ ' + aliasName;
}
} else {
result = name
@@ -87,10 +83,10 @@ class DomainPopupView extends AbstractViewPopup {
return (
this.name() &&
- this.imapServer() &&
+ this.imapHost() &&
this.imapPort() &&
- (useSieve ? this.sieveServer() && this.sievePort() : true) &&
- ((this.smtpServer() && this.smtpPort()) || usePhpMail)
+ (useSieve ? this.sieveHost() && this.sievePort() : true) &&
+ ((this.smtpHost() && this.smtpPort()) || usePhpMail)
);
},
@@ -103,31 +99,29 @@ class DomainPopupView extends AbstractViewPopup {
testingSieveError: value => value || this.testingSieveErrorDesc(''),
testingSmtpError: value => value || this.testingSmtpErrorDesc(''),
- page: () => this.sieveSettings(false),
-
// smart form improvements
- imapServerFocus: value =>
- value && this.name() && !this.imapServer() && this.imapServer(this.name().replace(/[.]?[*][.]?/g, '')),
+ imapHostFocus: value =>
+ value && this.name() && !this.imapHost() && this.imapHost(this.name().replace(/[.]?[*][.]?/g, '')),
- sieveServerFocus: value =>
- value && this.imapServer() && !this.sieveServer() && this.sieveServer(this.imapServer()),
+ sieveHostFocus: value =>
+ value && this.imapHost() && !this.sieveHost() && this.sieveHost(this.imapHost()),
- smtpServerFocus: value => value && this.imapServer() && !this.smtpServer()
- && this.smtpServer(this.imapServer().replace(/imap/gi, 'smtp')),
+ smtpHostFocus: value => value && this.imapHost() && !this.smtpHost()
+ && this.smtpHost(this.imapHost().replace(/imap/gi, 'smtp')),
imapSecure: value => {
if (this.enableSmartPorts()) {
const port = pInt(this.imapPort());
- switch (pString(value)) {
- case '0':
- case '2':
+ switch (pInt(value)) {
+ case 0:
+ case 2:
if (993 === port) {
- this.imapPort('143');
+ this.imapPort(143);
}
break;
- case '1':
+ case 1:
if (143 === port) {
- this.imapPort('993');
+ this.imapPort(993);
}
break;
// no default
@@ -138,20 +132,20 @@ class DomainPopupView extends AbstractViewPopup {
smtpSecure: value => {
if (this.enableSmartPorts()) {
const port = pInt(this.smtpPort());
- switch (pString(value)) {
- case '0':
+ switch (pInt(value)) {
+ case 0:
if (465 === port || 587 === port) {
- this.smtpPort('25');
+ this.smtpPort(25);
}
break;
- case '1':
+ case 1:
if (25 === port || 587 === port) {
- this.smtpPort('465');
+ this.smtpPort(465);
}
break;
- case '2':
+ case 2:
if (25 === port || 465 === port) {
- this.smtpPort('587');
+ this.smtpPort(587);
}
break;
// no default
@@ -162,90 +156,62 @@ class DomainPopupView extends AbstractViewPopup {
decorateKoCommands(this, {
createOrAddCommand: self => self.canBeSaved(),
- testConnectionCommand: self => self.canBeTested(),
- whiteListCommand: 1,
- backCommand: 1,
- sieveCommand: 1
+ testConnectionCommand: self => self.canBeTested()
});
}
createOrAddCommand() {
this.saving(true);
- Remote.createOrUpdateDomain(
+ Remote.request('AdminDomainSave',
this.onDomainCreateOrSaveResponse.bind(this),
- this
+ Object.assign(domainToParams(this), {
+ Create: this.edit() ? 0 : 1,
+
+ IncShortLogin: this.imapShortLogin() ? 1 : 0,
+
+ OutShortLogin: this.smtpShortLogin() ? 1 : 0,
+ OutSetSender: this.smtpSetSender() ? 1 : 0,
+
+ WhiteList: this.whiteList()
+ })
);
}
testConnectionCommand() {
- this.page('main');
-
- this.testingDone(false);
- this.testingImapError(false);
- this.testingSieveError(false);
- this.testingSmtpError(false);
+ this.clearTesting(false);
this.testing(true);
- Remote.testConnectionForDomain(
- this.onTestConnectionResponse.bind(this),
- this
- );
- }
+ Remote.request('AdminDomainTest',
+ (iError, oData) => {
+ this.testing(false);
+ if (iError) {
+ this.testingImapError(true);
+ this.testingSieveError(true);
+ this.testingSmtpError(true);
+ } else {
+ this.testingDone(true);
+ this.testingImapError(true !== oData.Result.Imap);
+ this.testingSieveError(true !== oData.Result.Sieve);
+ this.testingSmtpError(true !== oData.Result.Smtp);
- whiteListCommand() {
- this.page('white-list');
- }
+ if (this.testingImapError() && oData.Result.Imap) {
+ this.testingImapErrorDesc('');
+ this.testingImapErrorDesc(oData.Result.Imap);
+ }
- backCommand() {
- this.page('main');
- }
+ if (this.testingSieveError() && oData.Result.Sieve) {
+ this.testingSieveErrorDesc('');
+ this.testingSieveErrorDesc(oData.Result.Sieve);
+ }
- sieveCommand() {
- this.sieveSettings(!this.sieveSettings());
- this.clearTesting();
- }
-
- onTestConnectionResponse(iError, oData) {
- this.testing(false);
- if (iError) {
- this.testingImapError(true);
- this.testingSieveError(true);
- this.testingSmtpError(true);
- this.sieveSettings(false);
- } else {
- let bImap = false,
- bSieve = false;
-
- this.testingDone(true);
- this.testingImapError(true !== oData.Result.Imap);
- this.testingSieveError(true !== oData.Result.Sieve);
- this.testingSmtpError(true !== oData.Result.Smtp);
-
- if (this.testingImapError() && oData.Result.Imap) {
- bImap = true;
- this.testingImapErrorDesc('');
- this.testingImapErrorDesc(oData.Result.Imap);
- }
-
- if (this.testingSieveError() && oData.Result.Sieve) {
- bSieve = true;
- this.testingSieveErrorDesc('');
- this.testingSieveErrorDesc(oData.Result.Sieve);
- }
-
- if (this.testingSmtpError() && oData.Result.Smtp) {
- this.testingSmtpErrorDesc('');
- this.testingSmtpErrorDesc(oData.Result.Smtp);
- }
-
- if (this.sieveSettings()) {
- if (!bSieve && bImap) {
- this.sieveSettings(false);
+ if (this.testingSmtpError() && oData.Result.Smtp) {
+ this.testingSmtpErrorDesc('');
+ this.testingSmtpErrorDesc(oData.Result.Smtp);
+ }
}
- } else if (bSieve && !bImap) {
- this.sieveSettings(true);
- }
- }
+ },
+ domainToParams(this)
+ );
}
onDomainCreateOrSaveResponse(iError) {
@@ -254,7 +220,7 @@ class DomainPopupView extends AbstractViewPopup {
this.savingError(getNotification(iError));
} else {
DomainAdminStore.fetch();
- this.closeCommand();
+ this.close();
}
}
@@ -266,82 +232,52 @@ class DomainPopupView extends AbstractViewPopup {
this.testingSmtpError(false);
}
- onHide() {
- this.page('main');
- this.sieveSettings(false);
- }
-
onShow(oDomain) {
this.saving(false);
-
- this.page('main');
- this.sieveSettings(false);
-
this.clearTesting();
-
this.clearForm();
if (oDomain) {
this.enableSmartPorts(false);
-
this.edit(true);
-
- this.name(oDomain.Name);
- this.imapServer(oDomain.IncHost);
- this.imapPort('' + pInt(oDomain.IncPort));
- this.imapSecure(oDomain.IncSecure);
- this.imapShortLogin(!!oDomain.IncShortLogin);
- this.useSieve(!!oDomain.UseSieve);
- this.sieveServer(oDomain.SieveHost);
- this.sievePort('' + pInt(oDomain.SievePort));
- this.sieveSecure(oDomain.SieveSecure);
- this.smtpServer(oDomain.OutHost);
- this.smtpPort('' + pInt(oDomain.OutPort));
- this.smtpSecure(oDomain.OutSecure);
- this.smtpShortLogin(!!oDomain.OutShortLogin);
- this.smtpAuth(!!oDomain.OutAuth);
- this.smtpSetSender(!!oDomain.OutSetSender);
- this.smtpPhpMail(!!oDomain.OutUsePhpMail);
- this.whiteList(oDomain.WhiteList);
- this.aliasName(oDomain.AliasName);
-
+ forEachObjectEntry(oDomain, (key, value) => this[key] && this[key](value));
this.enableSmartPorts(true);
}
}
+ getDefaults() {
+ return {
+ enableSmartPorts: false,
+
+ savingError: '',
+
+ name: '',
+
+ imapHost: '',
+ imapPort: 143,
+ imapSecure: 0,
+ imapShortLogin: false,
+
+ useSieve: false,
+ sieveHost: '',
+ sievePort: 4190,
+ sieveSecure: 0,
+
+ smtpHost: '',
+ smtpPort: 25,
+ smtpSecure: 0,
+ smtpShortLogin: false,
+ smtpAuth: true,
+ smtpSetSender: false,
+ smtpPhpMail: false,
+
+ whiteList: '',
+ aliasName: ''
+ };
+ }
+
clearForm() {
this.edit(false);
-
- this.page('main');
- this.sieveSettings(false);
-
- this.enableSmartPorts(false);
-
- this.savingError('');
-
- this.name('');
-
- this.imapServer('');
- this.imapPort('143');
- this.imapSecure(0);
- this.imapShortLogin(false);
-
- this.useSieve(false);
- this.sieveServer('');
- this.sievePort('4190');
- this.sieveSecure(0);
-
- this.smtpServer('');
- this.smtpPort('25');
- this.smtpSecure(0);
- this.smtpShortLogin(false);
- this.smtpAuth(true);
- this.smtpSetSender(true);
- this.smtpPhpMail(false);
-
- this.whiteList('');
- this.aliasName('');
+ forEachObjectEntry(this.getDefaults(), (key, value) => this[key](value));
this.enableSmartPorts(true);
}
}
-
-export { DomainPopupView, DomainPopupView as default };
diff --git a/dev/View/Popup/DomainAlias.js b/dev/View/Popup/DomainAlias.js
index cecd86acb..f6952d3a3 100644
--- a/dev/View/Popup/DomainAlias.js
+++ b/dev/View/Popup/DomainAlias.js
@@ -1,5 +1,3 @@
-import ko from 'ko';
-
import { getNotification } from 'Common/Translator';
import { DomainAdminStore } from 'Stores/Admin/Domain';
@@ -9,7 +7,7 @@ import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class DomainAliasPopupView extends AbstractViewPopup {
+export class DomainAliasPopupView extends AbstractViewPopup {
constructor() {
super('DomainAlias');
@@ -22,11 +20,13 @@ class DomainAliasPopupView extends AbstractViewPopup {
alias: ''
});
- this.domains = ko.computed(() => DomainAdminStore.filter(item => item && !item.alias));
+ this.addComputables({
+ domains: () => DomainAdminStore.filter(item => item && !item.alias),
- this.domainsOptions = ko.computed(() => this.domains().map(item => ({ optValue: item.name, optText: item.name })));
+ domainsOptions: () => this.domains().map(item => ({ optValue: item.name, optText: item.name })),
- this.canBeSaved = ko.computed(() => !this.saving() && this.name() && this.alias());
+ canBeSaved: () => !this.saving() && this.name() && this.alias()
+ });
decorateKoCommands(this, {
createCommand: self => self.canBeSaved()
@@ -35,15 +35,19 @@ class DomainAliasPopupView extends AbstractViewPopup {
createCommand() {
this.saving(true);
- Remote.createDomainAlias(iError => {
- this.saving(false);
- if (iError) {
- this.savingError(getNotification(iError));
- } else {
- DomainAdminStore.fetch();
- this.closeCommand();
- }
- }, this.name(), this.alias());
+ Remote.request('AdminDomainAliasSave',
+ iError => {
+ this.saving(false);
+ if (iError) {
+ this.savingError(getNotification(iError));
+ } else {
+ DomainAdminStore.fetch();
+ this.close();
+ }
+ }, {
+ Name: this.name(),
+ Alias: this.alias()
+ });
}
onShow() {
@@ -59,5 +63,3 @@ class DomainAliasPopupView extends AbstractViewPopup {
this.alias('');
}
}
-
-export { DomainAliasPopupView, DomainAliasPopupView as default };
diff --git a/dev/View/Popup/Filter.js b/dev/View/Popup/Filter.js
deleted file mode 100644
index 88da060d0..000000000
--- a/dev/View/Popup/Filter.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import ko from 'ko';
-
-import { FilterAction } from 'Model/Filter';
-import { FilterConditionField, FilterConditionType } from 'Model/FilterCondition';
-import { defaultOptionsAfterRender } from 'Common/Utils';
-import { i18n, initOnStartOrLangChange } from 'Common/Translator';
-
-import { FolderUserStore } from 'Stores/User/Folder';
-import { SieveUserStore } from 'Stores/User/Sieve';
-
-import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-class FilterPopupView extends AbstractViewPopup {
- constructor() {
- super('Filter');
-
- this.addObservables({
- isNew: true,
- filter: null,
- allowMarkAsRead: false,
- selectedFolderValue: ''
- });
-
- this.fTrueCallback = null;
-
- this.defaultOptionsAfterRender = defaultOptionsAfterRender;
- this.folderSelectList = FolderUserStore.folderMenuForFilters;
-
- this.selectedFolderValue.subscribe(() => this.filter() && this.filter().actionValueError(false));
-
- ['actionTypeOptions','fieldOptions','typeOptions','typeOptionsSize','typeOptionsBody'].forEach(
- key => this[key] = ko.observableArray()
- );
-
- initOnStartOrLangChange(this.populateOptions.bind(this));
-
- SieveUserStore.capa.subscribe(this.populateOptions, this);
-
- decorateKoCommands(this, {
- saveFilterCommand: 1
- });
- }
-
- saveFilterCommand() {
- if (this.filter()) {
- if (FilterAction.MoveTo === this.filter().actionType()) {
- this.filter().actionValue(this.selectedFolderValue());
- }
-
- if (!this.filter().verify()) {
- return false;
- }
-
- this.fTrueCallback && this.fTrueCallback(this.filter());
-
- this.modalVisibility() && this.closeCommand && this.closeCommand();
- }
-
- return true;
- }
-
- populateOptions() {
- this.actionTypeOptions([]);
-
- let i18nFilter = key => i18n('POPUPS_FILTER/SELECT_' + key);
-
- this.fieldOptions([
- { id: FilterConditionField.From, name: i18n('GLOBAL/FROM') },
- { id: FilterConditionField.Recipient, name: i18nFilter('FIELD_RECIPIENTS') },
- { id: FilterConditionField.Subject, name: i18n('GLOBAL/SUBJECT') },
- { id: FilterConditionField.Size, name: i18nFilter('FIELD_SIZE') },
- { id: FilterConditionField.Header, name: i18nFilter('FIELD_HEADER') }
- ]);
-
- this.typeOptions([
- { id: FilterConditionType.Contains, name: i18nFilter('TYPE_CONTAINS') },
- { id: FilterConditionType.NotContains, name: i18nFilter('TYPE_NOT_CONTAINS') },
- { id: FilterConditionType.EqualTo, name: i18nFilter('TYPE_EQUAL_TO') },
- { id: FilterConditionType.NotEqualTo, name: i18nFilter('TYPE_NOT_EQUAL_TO') }
- ]);
-
- // this.actionTypeOptions.push({id: FilterAction.None,
- // name: i18n('GLOBAL/NONE')});
- const modules = SieveUserStore.capa;
- if (modules) {
- if (modules.includes('imap4flags')) {
- this.allowMarkAsRead(true);
- }
-
- if (modules.includes('fileinto')) {
- this.actionTypeOptions.push({
- id: FilterAction.MoveTo,
- name: i18nFilter('ACTION_MOVE_TO')
- });
- this.actionTypeOptions.push({
- id: FilterAction.Forward,
- name: i18nFilter('ACTION_FORWARD_TO')
- });
- }
-
- if (modules.includes('reject')) {
- this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') });
- }
-
- if (modules.includes('vacation')) {
- this.actionTypeOptions.push({
- id: FilterAction.Vacation,
- name: i18nFilter('ACTION_VACATION_MESSAGE')
- });
- }
-
- if (modules.includes('body')) {
- this.fieldOptions.push({ id: FilterConditionField.Body, name: i18nFilter('FIELD_BODY') });
- }
-
- if (modules.includes('regex')) {
- this.typeOptions.push({ id: FilterConditionType.Regex, name: 'Regex' });
- }
- }
-
- this.actionTypeOptions.push({ id: FilterAction.Discard, name: i18nFilter('ACTION_DISCARD') });
-
- this.typeOptionsSize([
- { id: FilterConditionType.Over, name: i18nFilter('TYPE_OVER') },
- { id: FilterConditionType.Under, name: i18nFilter('TYPE_UNDER') }
- ]);
-
- this.typeOptionsBody([
- { id: FilterConditionType.Text, name: i18nFilter('TYPE_TEXT') },
- { id: FilterConditionType.Raw, name: i18nFilter('TYPE_RAW') }
- ]);
- }
-
- removeCondition(oConditionToDelete) {
- if (this.filter()) {
- this.filter().removeCondition(oConditionToDelete);
- }
- }
-
- clearPopup() {
- this.isNew(true);
-
- this.fTrueCallback = null;
- this.filter(null);
- }
-
- onShow(oFilter, fTrueCallback, bEdit) {
- this.clearPopup();
-
- this.fTrueCallback = fTrueCallback;
- this.filter(oFilter);
-
- if (oFilter) {
- this.selectedFolderValue(oFilter.actionValue());
- }
-
- this.isNew(!bEdit);
-
- if (!bEdit && oFilter) {
- oFilter.nameFocused(true);
- }
- }
-
- onShowWithDelay() {
- this.isNew() && this.filter() && this.filter().nameFocused(true);
- }
-}
-
-export { FilterPopupView, FilterPopupView as default };
diff --git a/dev/View/Popup/FolderClear.js b/dev/View/Popup/FolderClear.js
index ce5ccd57c..388b0a0d6 100644
--- a/dev/View/Popup/FolderClear.js
+++ b/dev/View/Popup/FolderClear.js
@@ -2,13 +2,14 @@ import { i18n, getNotification } from 'Common/Translator';
import { setFolderHash } from 'Common/Cache';
import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class FolderClearPopupView extends AbstractViewPopup {
+export class FolderClearPopupView extends AbstractViewPopup {
constructor() {
super('FolderClear');
@@ -19,17 +20,11 @@ class FolderClearPopupView extends AbstractViewPopup {
});
this.addComputables({
- folderFullNameForClear: () => {
+ dangerDescHtml: () => {
const folder = this.selectedFolder();
- return folder ? folder.printableFullName() : '';
- },
-
- folderNameForClear: () => {
- const folder = this.selectedFolder();
- return folder ? folder.localName() : '';
- },
-
- dangerDescHtml: () => i18n('POPUPS_CLEAR_FOLDER/DANGER_DESC_HTML_1', { FOLDER: this.folderNameForClear() })
+// return i18n('POPUPS_CLEAR_FOLDER/DANGER_DESC_HTML_1', { FOLDER: folder ? folder.fullName.replace(folder.delimiter, ' / ') : '' });
+ return i18n('POPUPS_CLEAR_FOLDER/DANGER_DESC_HTML_1', { FOLDER: folder ? folder.localName() : '' });
+ }
});
decorateKoCommands(this, {
@@ -44,38 +39,31 @@ class FolderClearPopupView extends AbstractViewPopup {
const folderToClear = this.selectedFolder();
if (folderToClear) {
MessageUserStore.message(null);
- MessageUserStore.list([]);
+ MessagelistUserStore([]);
this.clearingProcess(true);
folderToClear.messageCountAll(0);
folderToClear.messageCountUnread(0);
- setFolderHash(folderToClear.fullNameRaw, '');
+ setFolderHash(folderToClear.fullName, '');
- Remote.folderClear(iError => {
+ Remote.request('FolderClear', iError => {
this.clearingProcess(false);
if (iError) {
this.clearingError(getNotification(iError));
} else {
- rl.app.reloadMessageList(true);
- this.cancelCommand();
+ MessagelistUserStore.reload(true);
+ this.close();
}
- }, folderToClear.fullNameRaw);
+ }, {
+ Folder: folderToClear.fullName
+ });
}
}
- clearPopup() {
- this.clearingProcess(false);
- this.selectedFolder(null);
- }
-
onShow(folder) {
- this.clearPopup();
- if (folder) {
- this.selectedFolder(folder);
- }
+ this.clearingProcess(false);
+ this.selectedFolder(folder || null);
}
}
-
-export { FolderClearPopupView, FolderClearPopupView as default };
diff --git a/dev/View/Popup/FolderCreate.js b/dev/View/Popup/FolderCreate.js
index ee8f627a7..a7da49fbd 100644
--- a/dev/View/Popup/FolderCreate.js
+++ b/dev/View/Popup/FolderCreate.js
@@ -1,74 +1,86 @@
-import ko from 'ko';
+import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { defaultOptionsAfterRender } from 'Common/Utils';
-import { folderListOptionsBuilder } from 'Common/UtilsUser';
+import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
+import { getNotification } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
+import { SettingsUserStore } from 'Stores/User/Settings';
import Remote from 'Remote/User/Fetch';
-import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class FolderCreatePopupView extends AbstractViewPopup {
+import { setFolder, getFolderFromCacheList } from 'Common/Cache';
+import { FolderModel } from 'Model/FolderCollection';
+
+export class FolderCreatePopupView extends AbstractViewPopup {
constructor() {
super('FolderCreate');
this.addObservables({
folderName: '',
+ folderSubscribe: SettingsUserStore.hideUnsubscribed(),
selectedParentValue: UNUSED_OPTION_VALUE
});
- this.parentFolderSelectList = ko.computed(() =>
+ this.parentFolderSelectList = koComputable(() =>
folderListOptionsBuilder(
- [],
- FolderUserStore.folderList(),
[],
[['', '']],
- FolderUserStore.namespace
- ? item => FolderUserStore.namespace !== item.fullNameRaw.substr(0, FolderUserStore.namespace.length)
- : null,
oItem =>
- oItem ? (oItem.isSystemFolder() ? oItem.name() + ' ' + oItem.manageFolderSystemName() : oItem.name()) : ''
+ oItem ? (oItem.isSystemFolder() ? oItem.name() + ' ' + oItem.manageFolderSystemName() : oItem.name()) : '',
+ FolderUserStore.namespace
+ ? item => FolderUserStore.namespace !== item.fullName.slice(0, FolderUserStore.namespace.length)
+ : null,
+ true
)
);
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
-
- decorateKoCommands(this, {
- createFolderCommand: self => self.simpleFolderNameValidation(self.folderName())
- });
}
- createFolderCommand() {
- let parentFolderName = this.selectedParentValue();
- if (!parentFolderName && 1 < FolderUserStore.namespace.length) {
- parentFolderName = FolderUserStore.namespace.substr(0, FolderUserStore.namespace.length - 1);
+ submitForm() {
+ if (/^[^\\/]+$/g.test(this.folderName())) {
+ let parentFolderName = this.selectedParentValue();
+ if (!parentFolderName && 1 < FolderUserStore.namespace.length) {
+ parentFolderName = FolderUserStore.namespace.slice(0, FolderUserStore.namespace.length - 1);
+ }
+
+ Remote.abort('Folders').post('FolderCreate', FolderUserStore.foldersCreating, {
+ Folder: this.folderName(),
+ Parent: parentFolderName,
+ Subscribe: this.folderSubscribe() ? 1 : 0
+ })
+ .then(
+ data => {
+ const folder = getFolderFromCacheList(parentFolderName),
+ subFolder = FolderModel.reviveFromJson(data.Result),
+ folders = (folder ? folder.subFolders : FolderUserStore.folderList);
+ setFolder(subFolder);
+ folders.push(subFolder);
+ sortFolders(folders);
+/*
+ var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
+ console.log((folder ? folder.subFolders : FolderUserStore.folderList).sort(collator.compare));
+*/
+ },
+ error => {
+ FolderUserStore.folderListError(
+ getNotification(error.code, '', Notification.CantCreateFolder)
+ + '.\n' + error.message);
+ }
+ );
+
+ this.close();
}
-
- rl.app.foldersPromisesActionHelper(
- Remote.folderCreate(this.folderName(), parentFolderName),
- Notification.CantCreateFolder
- );
-
- this.cancelCommand();
- }
-
- simpleFolderNameValidation(sName) {
- return /^[^\\/]+$/g.test(sName);
- }
-
- clearPopup() {
- this.folderName('');
- this.selectedParentValue('');
}
onShow() {
- this.clearPopup();
+ this.folderName('');
+ this.selectedParentValue('');
}
}
-
-export { FolderCreatePopupView, FolderCreatePopupView as default };
diff --git a/dev/View/Popup/FolderSystem.js b/dev/View/Popup/FolderSystem.js
index 75f5612fa..a5186a3f2 100644
--- a/dev/View/Popup/FolderSystem.js
+++ b/dev/View/Popup/FolderSystem.js
@@ -1,83 +1,46 @@
import ko from 'ko';
+import { koComputable, addSubscribablesTo } from 'External/ko';
import { SetSystemFoldersNotification } from 'Common/EnumsUser';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
-import { Settings } from 'Common/Globals';
-import { defaultOptionsAfterRender, addSubscribablesTo } from 'Common/Utils';
-import { folderListOptionsBuilder } from 'Common/UtilsUser';
-import { initOnStartOrLangChange, i18n } from 'Common/Translator';
+import { defaultOptionsAfterRender } from 'Common/Utils';
+import { folderListOptionsBuilder } from 'Common/Folders';
+import { i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
-import Remote from 'Remote/User/Fetch';
-
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class FolderSystemPopupView extends AbstractViewPopup {
+export class FolderSystemPopupView extends AbstractViewPopup {
constructor() {
super('FolderSystem');
- this.sChooseOnText = '';
- this.sUnuseText = '';
-
- initOnStartOrLangChange(() => {
- this.sChooseOnText = i18n('POPUPS_SYSTEM_FOLDERS/SELECT_CHOOSE_ONE');
- this.sUnuseText = i18n('POPUPS_SYSTEM_FOLDERS/SELECT_UNUSE_NAME');
- });
-
this.notification = ko.observable('');
- this.folderSelectList = ko.computed(() =>
+ this.folderSelectList = koComputable(() =>
folderListOptionsBuilder(
- [],
- FolderUserStore.folderList(),
FolderUserStore.folderListSystemNames(),
[
- ['', this.sChooseOnText],
- [UNUSED_OPTION_VALUE, this.sUnuseText]
- ],
- null,
- null,
- null,
- true
+ ['', i18n('POPUPS_SYSTEM_FOLDERS/SELECT_CHOOSE_ONE')],
+ [UNUSED_OPTION_VALUE, i18n('POPUPS_SYSTEM_FOLDERS/SELECT_UNUSE_NAME')]
+ ]
)
);
this.sentFolder = FolderUserStore.sentFolder;
- this.draftFolder = FolderUserStore.draftFolder;
+ this.draftsFolder = FolderUserStore.draftsFolder;
this.spamFolder = FolderUserStore.spamFolder;
this.trashFolder = FolderUserStore.trashFolder;
this.archiveFolder = FolderUserStore.archiveFolder;
- const settingsSet = Settings.set,
- fSetSystemFolders = () => {
- settingsSet('SentFolder', FolderUserStore.sentFolder());
- settingsSet('DraftFolder', FolderUserStore.draftFolder());
- settingsSet('SpamFolder', FolderUserStore.spamFolder());
- settingsSet('TrashFolder', FolderUserStore.trashFolder());
- settingsSet('ArchiveFolder', FolderUserStore.archiveFolder());
- },
- fSaveSystemFolders = (()=>{
- fSetSystemFolders();
- Remote.saveSystemFolders(()=>{}, {
- SentFolder: FolderUserStore.sentFolder(),
- DraftFolder: FolderUserStore.draftFolder(),
- SpamFolder: FolderUserStore.spamFolder(),
- TrashFolder: FolderUserStore.trashFolder(),
- ArchiveFolder: FolderUserStore.archiveFolder()
- });
- }).debounce(1000),
- fCallback = () => {
- fSetSystemFolders();
- fSaveSystemFolders();
- };
+ const fSaveSystemFolders = (()=>FolderUserStore.saveSystemFolders()).debounce(1000);
addSubscribablesTo(FolderUserStore, {
- sentFolder: fCallback,
- draftFolder: fCallback,
- spamFolder: fCallback,
- trashFolder: fCallback,
- archiveFolder: fCallback
+ sentFolder: fSaveSystemFolders,
+ draftsFolder: fSaveSystemFolders,
+ spamFolder: fSaveSystemFolders,
+ trashFolder: fSaveSystemFolders,
+ archiveFolder: fSaveSystemFolders
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
@@ -110,5 +73,3 @@ class FolderSystemPopupView extends AbstractViewPopup {
this.notification(notification);
}
}
-
-export { FolderSystemPopupView, FolderSystemPopupView as default };
diff --git a/dev/View/Popup/Identity.js b/dev/View/Popup/Identity.js
index 65f18e5a6..dcdbde8bc 100644
--- a/dev/View/Popup/Identity.js
+++ b/dev/View/Popup/Identity.js
@@ -2,12 +2,11 @@ import { getNotification } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
-import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
const reEmail = /^[^@\s]+@[^@\s]+$/;
-class IdentityPopupView extends AbstractViewPopup {
+export class IdentityPopupView extends AbstractViewPopup {
constructor() {
super('Identity');
@@ -60,60 +59,57 @@ class IdentityPopupView extends AbstractViewPopup {
this.replyTo.valueHasMutated();
this.bcc.valueHasMutated();
*/
- decorateKoCommands(this, {
- addOrEditIdentityCommand: self => !self.submitRequest()
- });
}
- addOrEditIdentityCommand() {
- if (this.signature && this.signature.__fetchEditorValue) {
- this.signature.__fetchEditorValue();
- }
-
- if (!this.emailHasError()) {
- this.emailHasError(!this.email().trim());
- }
-
- if (this.emailHasError()) {
- if (!this.owner()) {
- this.emailFocused(true);
+ submitForm() {
+ if (!this.submitRequest()) {
+ if (this.signature && this.signature.__fetchEditorValue) {
+ this.signature.__fetchEditorValue();
}
- return false;
- }
+ if (!this.emailHasError()) {
+ this.emailHasError(!this.email().trim());
+ }
- if (this.replyToHasError()) {
- this.replyToFocused(true);
- return false;
- }
-
- if (this.bccHasError()) {
- this.bccFocused(true);
- return false;
- }
-
- this.submitRequest(true);
-
- Remote.identityUpdate(
- iError => {
- this.submitRequest(false);
- if (iError) {
- this.submitError(getNotification(iError));
- } else {
- rl.app.accountsAndIdentities();
- this.cancelCommand();
+ if (this.emailHasError()) {
+ if (!this.owner()) {
+ this.emailFocused(true);
}
- },
- this.id,
- this.email(),
- this.name(),
- this.replyTo(),
- this.bcc(),
- this.signature(),
- this.signatureInsertBefore()
- );
- return true;
+ return;
+ }
+
+ if (this.replyToHasError()) {
+ this.replyToFocused(true);
+ return;
+ }
+
+ if (this.bccHasError()) {
+ this.bccFocused(true);
+ return;
+ }
+
+ this.submitRequest(true);
+
+ Remote.request('IdentityUpdate', iError => {
+ this.submitRequest(false);
+ if (iError) {
+ this.submitError(getNotification(iError));
+ } else {
+ rl.app.accountsAndIdentities();
+ this.close();
+ }
+ }, {
+ Id: this.id,
+ Email: this.email(),
+ Name: this.name(),
+ ReplyTo: this.replyTo(),
+ Bcc: this.bcc(),
+ Signature: this.signature(),
+ SignatureInsertBefore: this.signatureInsertBefore() ? 1 : 0
+ }
+ );
+ }
}
clearPopup() {
@@ -162,13 +158,11 @@ class IdentityPopupView extends AbstractViewPopup {
}
}
- onShowWithDelay() {
+ afterShow() {
this.owner() || this.emailFocused(true);
}
- onHideWithDelay() {
+ afterHide() {
this.clearPopup();
}
}
-
-export { IdentityPopupView, IdentityPopupView as default };
diff --git a/dev/View/Popup/KeyboardShortcutsHelp.js b/dev/View/Popup/KeyboardShortcutsHelp.js
index 1ad896083..b53d6f709 100644
--- a/dev/View/Popup/KeyboardShortcutsHelp.js
+++ b/dev/View/Popup/KeyboardShortcutsHelp.js
@@ -1,38 +1,33 @@
-import { Scope } from 'Common/Enums';
-
+import { addShortcut } from 'Common/Globals';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class KeyboardShortcutsHelpPopupView extends AbstractViewPopup {
+export class KeyboardShortcutsHelpPopupView extends AbstractViewPopup {
constructor() {
super('KeyboardShortcutsHelp');
+ this.metaKey = shortcuts.getMetaKey();
}
onBuild(dom) {
- dom.querySelectorAll('a[data-toggle="tab"]').forEach(node => node.Tab || new BSN.Tab(node));
+ const tabs = dom.querySelectorAll('.tabs input'),
+ last = tabs.length - 1;
-// shortcuts.add('tab', 'shift',
- shortcuts.add('tab,arrowleft,arrowright', '',
- Scope.KeyboardShortcutsHelp,
- ((event, handler)=>{
- if (event && handler) {
- const tabs = dom.querySelectorAll('.nav.nav-tabs > li'),
- last = tabs.length - 1;
- let next = 0;
- tabs.forEach((node, index) => {
- if (node.matches('.active')) {
- if (['tab','arrowright'].includes(handler.shortcut)) {
- next = index < last ? index+1 : 0;
- } else {
- next = index ? index-1 : last;
- }
+// addShortcut('tab', 'shift',
+ addShortcut('tab,arrowleft,arrowright', '',
+ 'KeyboardShortcutsHelp',
+ event => {
+ let next = 0;
+ tabs.forEach((node, index) => {
+ if (node.matches(':checked')) {
+ if (['Tab','ArrowRight'].includes(event.key)) {
+ next = index < last ? index+1 : 0;
+ } else {
+ next = index ? index-1 : last;
}
- });
-
- tabs[next].querySelector('a[data-toggle="tab"]').Tab.show();
- }
- }).throttle(100)
+ }
+ });
+ tabs[next].checked = true;
+ return false;
+ }
);
}
}
-
-export { KeyboardShortcutsHelpPopupView, KeyboardShortcutsHelpPopupView as default };
diff --git a/dev/View/Popup/Languages.js b/dev/View/Popup/Languages.js
index 350b8e775..ff62f63e3 100644
--- a/dev/View/Popup/Languages.js
+++ b/dev/View/Popup/Languages.js
@@ -1,10 +1,11 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
import { convertLangName } from 'Common/Translator';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
-class LanguagesPopupView extends AbstractViewPopup {
+export class LanguagesPopupView extends AbstractViewPopup {
constructor() {
super('Languages');
@@ -13,7 +14,7 @@ class LanguagesPopupView extends AbstractViewPopup {
this.langs = ko.observableArray();
- this.languages = ko.computed(() => {
+ this.languages = koComputable(() => {
const userLanguage = this.userLanguage();
return this.langs.map(language => ({
key: language,
@@ -27,8 +28,7 @@ class LanguagesPopupView extends AbstractViewPopup {
}
languageTooltipName(language) {
- const result = convertLangName(language, true);
- return convertLangName(language, false) === result ? '' : result;
+ return convertLangName(language, true);
}
setLanguageSelection() {
@@ -36,7 +36,7 @@ class LanguagesPopupView extends AbstractViewPopup {
this.languages().forEach(item => item.selected(item.key === currentLang));
}
- onBeforeShow() {
+ beforeShow() {
this.fLang = null;
this.userLanguage('');
@@ -52,8 +52,6 @@ class LanguagesPopupView extends AbstractViewPopup {
changeLanguage(lang) {
this.fLang && this.fLang(lang);
- this.cancelCommand();
+ this.close();
}
}
-
-export { LanguagesPopupView, LanguagesPopupView as default };
diff --git a/dev/View/Popup/MessageOpenPgp.js b/dev/View/Popup/MessageOpenPgp.js
deleted file mode 100644
index 3218c8c18..000000000
--- a/dev/View/Popup/MessageOpenPgp.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import ko from 'ko';
-
-import { pString } from 'Common/Utils';
-import { Scope } from 'Common/Enums';
-
-import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-class MessageOpenPgpPopupView extends AbstractViewPopup {
- constructor() {
- super('MessageOpenPgp');
-
- this.addObservables({
- notification: '',
- selectedKey: null,
- password: '',
- submitRequest: false
- });
- this.privateKeys = ko.observableArray();
-
- this.resultCallback = null;
-
- decorateKoCommands(this, {
- doCommand: self => !self.submitRequest()
- });
- }
-
- doCommand() {
- this.submitRequest(true);
-
- setTimeout(() => {
- let privateKey = null;
-
- try {
- if (this.resultCallback && this.selectedKey()) {
- const privateKeys = this.selectedKey().getNativeKeys();
- privateKey = privateKeys && privateKeys[0] ? privateKeys[0] : null;
-
- if (privateKey) {
- try {
- if (!privateKey.decrypt(pString(this.password()))) {
- console.log('Error: Private key cannot be decrypted');
- privateKey = null;
- }
- } catch (e) {
- console.log(e);
- privateKey = null;
- }
- } else {
- console.log('Error: Private key cannot be found');
- }
- }
- } catch (e) {
- console.log(e);
- privateKey = null;
- }
-
- this.submitRequest(false);
-
- this.cancelCommand();
- this.resultCallback(privateKey);
- }, 100);
- }
-
- clearPopup() {
- this.notification('');
-
- this.password('');
-
- this.selectedKey(false);
- this.submitRequest(false);
-
- this.resultCallback = null;
- this.privateKeys([]);
- }
-
- onBuild(oDom) {
-// shortcuts.add('tab', 'shift', Scope.MessageOpenPgp, () => {
- shortcuts.add('tab', '', Scope.MessageOpenPgp, () => {
- let btn = this.querySelector('.inputPassword');
- if (btn.matches(':focus')) {
- btn = this.querySelector('.buttonDo');
- }
- btn.focus();
- return false;
- });
-
- const self = this;
-
- oDom.addEventListener('click', event => {
- const el = event.target.closestWithin('.key-list__item', oDom);
- if (el) {
- oDom.querySelectorAll('.key-list__item .key-list__item__radio').forEach(node =>
- node.textContent = el === node ? '⦿' : '○'
- );
-
- self.selectedKey(ko.dataFor(el));
-
-// this.querySelector('.inputPassword').focus();
- }
- });
- }
-
- onHideWithDelay() {
- this.clearPopup();
- }
-
- onShow(fCallback, privateKeys) {
- this.clearPopup();
-
- this.resultCallback = fCallback;
- this.privateKeys(privateKeys);
-
- if (this.viewModelDom) {
- const el = this.viewModelDom.querySelector('.key-list__item');
- el && el.click();
- }
- }
-}
-
-export { MessageOpenPgpPopupView, MessageOpenPgpPopupView as default };
diff --git a/dev/View/Popup/NewOpenPgpKey.js b/dev/View/Popup/NewOpenPgpKey.js
deleted file mode 100644
index efdabbb2d..000000000
--- a/dev/View/Popup/NewOpenPgpKey.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import { pInt } from 'Common/Utils';
-
-import { PgpUserStore } from 'Stores/User/Pgp';
-
-import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-class NewOpenPgpKeyPopupView extends AbstractViewPopup {
- constructor() {
- super('NewOpenPgpKey');
-
- this.addObservables({
- email: '',
- emailError: false,
-
- name: '',
- password: '',
- keyBitLength: 4096,
-
- submitRequest: false,
- submitError: ''
- });
-
- this.email.subscribe(() => this.emailError(false));
-
- decorateKoCommands(this, {
- generateOpenPgpKeyCommand: 1
- });
- }
-
- generateOpenPgpKeyCommand() {
- const userId = {},
- openpgpKeyring = PgpUserStore.openpgpKeyring;
-
- this.emailError(!this.email().trim());
- if (!openpgpKeyring || this.emailError()) {
- return false;
- }
-
- userId.email = this.email();
- if (this.name()) {
- userId.name = this.name();
- }
-
- this.submitRequest(true);
- this.submitError('');
-
- setTimeout(() => {
- try {
- PgpUserStore.openpgp
- .generateKey({
- userIds: [userId],
- numBits: pInt(this.keyBitLength()),
- passphrase: this.password().trim()
- })
- .then((keyPair) => {
- this.submitRequest(false);
-
- if (keyPair && keyPair.privateKeyArmored) {
- openpgpKeyring.privateKeys.importKey(keyPair.privateKeyArmored);
- openpgpKeyring.publicKeys.importKey(keyPair.publicKeyArmored);
-
- openpgpKeyring.store();
-
- rl.app.reloadOpenPgpKeys();
- this.cancelCommand && this.cancelCommand();
- }
- })
- .catch((e) => {
- this.submitRequest(false);
- this.showError(e);
- });
- } catch (e) {
- this.submitRequest(false);
- this.showError(e);
- }
- }, 100);
-
- return true;
- }
-
- showError(e) {
- console.log(e);
- if (e && e.message) {
- this.submitError(e.message);
- }
- }
-
- clearPopup() {
- this.name('');
- this.password('');
-
- this.email('');
- this.emailError(false);
- this.keyBitLength(4096);
-
- this.submitError('');
- }
-
- onShow() {
- this.clearPopup();
- }
-}
-
-export { NewOpenPgpKeyPopupView, NewOpenPgpKeyPopupView as default };
diff --git a/dev/View/Popup/OpenPgpGenerate.js b/dev/View/Popup/OpenPgpGenerate.js
new file mode 100644
index 000000000..6d50d8dd8
--- /dev/null
+++ b/dev/View/Popup/OpenPgpGenerate.js
@@ -0,0 +1,109 @@
+//import { pInt } from 'Common/Utils';
+
+import { GnuPGUserStore } from 'Stores/User/GnuPG';
+import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
+
+import { IdentityUserStore } from 'Stores/User/Identity';
+
+import { AbstractViewPopup } from 'Knoin/AbstractViews';
+
+import { SettingsCapa } from 'Common/Globals';
+
+export class OpenPgpGeneratePopupView extends AbstractViewPopup {
+ constructor() {
+ super('OpenPgpGenerate');
+
+ this.identities = IdentityUserStore;
+
+ this.addObservables({
+ email: '',
+ emailError: false,
+
+ name: '',
+ password: '',
+ keyType: 'ECC',
+
+ submitRequest: false,
+ submitError: '',
+
+ backupPublicKey: true,
+ backupPrivateKey: false,
+
+ saveGnuPGPublic: true,
+ saveGnuPGPrivate: false
+ });
+
+ this.canGnuPG = SettingsCapa('GnuPG');
+
+ this.email.subscribe(() => this.emailError(false));
+ }
+
+ submitForm() {
+ const type = this.keyType().toLowerCase(),
+ userId = {
+ name: this.name(),
+ email: this.email()
+ },
+ cfg = {
+ type: type,
+ userIDs: [userId],
+ passphrase: this.password().trim()
+// format: 'armored' // output key format, defaults to 'armored' (other options: 'binary' or 'object')
+ }
+/*
+ if ('ecc' === type) {
+ cfg.curve = 'curve25519';
+ } else {
+ cfg.rsaBits = pInt(this.keyBitLength());
+ }
+*/
+ this.emailError(!this.email().trim());
+ if (this.emailError()) {
+ return;
+ }
+
+ this.submitRequest(true);
+ this.submitError('');
+
+ openpgp.generateKey(cfg).then(keyPair => {
+ if (keyPair) {
+ const fn = () => {
+ this.submitRequest(false);
+ this.close();
+ };
+
+ OpenPGPUserStore.storeKeyPair(keyPair);
+
+ keyPair.onServer = (this.backupPublicKey() ? 1 : 0) + (this.backupPrivateKey() ? 2 : 0);
+ keyPair.inGnuPG = (this.saveGnuPGPublic() ? 1 : 0) + (this.saveGnuPGPrivate() ? 2 : 0);
+ if (keyPair.onServer || keyPair.inGnuPG) {
+ if (!this.backupPrivateKey() && !this.saveGnuPGPrivate()) {
+ delete keyPair.privateKey;
+ }
+ GnuPGUserStore.storeKeyPair(keyPair, fn);
+ } else {
+ fn();
+ }
+ }
+ })
+ .catch((e) => {
+ this.submitRequest(false);
+ this.showError(e);
+ });
+ }
+
+ showError(e) {
+ console.log(e);
+ if (e && e.message) {
+ this.submitError(e.message);
+ }
+ }
+
+ onShow() {
+ this.name(''/*IdentityUserStore()[0].name()*/);
+ this.password('');
+ this.email(''/*IdentityUserStore()[0].email()*/);
+ this.emailError(false);
+ this.submitError('');
+ }
+}
diff --git a/dev/View/Popup/OpenPgpImport.js b/dev/View/Popup/OpenPgpImport.js
new file mode 100644
index 000000000..e9fb4d195
--- /dev/null
+++ b/dev/View/Popup/OpenPgpImport.js
@@ -0,0 +1,74 @@
+import { GnuPGUserStore } from 'Stores/User/GnuPG';
+import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
+
+import { AbstractViewPopup } from 'Knoin/AbstractViews';
+
+export class OpenPgpImportPopupView extends AbstractViewPopup {
+ constructor() {
+ super('OpenPgpImport');
+
+ this.addObservables({
+ key: '',
+ keyError: false,
+ keyErrorMessage: '',
+
+ saveGnuPG: true,
+ saveServer: false
+ });
+
+ this.canGnuPG = GnuPGUserStore.isSupported();
+
+ this.key.subscribe(() => {
+ this.keyError(false);
+ this.keyErrorMessage('');
+ });
+ }
+
+ submitForm() {
+ let keyTrimmed = this.key().trim();
+
+ if (/\n/.test(keyTrimmed)) {
+ keyTrimmed = keyTrimmed.replace(/\r+/g, '').replace(/\n{2,}/g, '\n\n');
+ }
+
+ this.keyError(!keyTrimmed);
+ this.keyErrorMessage('');
+
+ if (!keyTrimmed) {
+ return;
+ }
+
+ let match = null,
+ count = 30,
+ done = false;
+ // eslint-disable-next-line max-len
+ const reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi;
+
+ do {
+ match = reg.exec(keyTrimmed);
+ if (match && 0 < count) {
+ if (match[0] && match[1] && match[2] && match[1] === match[2]) {
+ this.saveGnuPG() && GnuPGUserStore.isSupported() && GnuPGUserStore.importKey(this.key());
+ OpenPGPUserStore.isSupported() && OpenPGPUserStore.importKey(this.key());
+ }
+
+ --count;
+ done = false;
+ } else {
+ done = true;
+ }
+ } while (!done);
+
+ if (this.keyError()) {
+ return;
+ }
+
+ this.close();
+ }
+
+ onShow() {
+ this.key('');
+ this.keyError(false);
+ this.keyErrorMessage('');
+ }
+}
diff --git a/dev/View/Popup/OpenPgpKey.js b/dev/View/Popup/OpenPgpKey.js
new file mode 100644
index 000000000..ba8ca84a3
--- /dev/null
+++ b/dev/View/Popup/OpenPgpKey.js
@@ -0,0 +1,126 @@
+import { doc, addShortcut } from 'Common/Globals';
+import { AbstractViewPopup } from 'Knoin/AbstractViews';
+
+export class OpenPgpKeyPopupView extends AbstractViewPopup {
+ constructor() {
+ super('OpenPgpKey');
+
+ this.addObservables({
+ key: '',
+ keyDom: null
+ });
+ }
+
+ selectKey() {
+ const el = this.keyDom();
+ if (el) {
+ let sel = getSelection(),
+ range = doc.createRange();
+ sel.removeAllRanges();
+ range.selectNodeContents(el);
+ sel.addRange(range);
+ }
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(this.key()).then(
+ () => console.log('Copied to clipboard'),
+ err => console.error(err)
+ );
+ }
+ }
+
+ onShow(openPgpKey) {
+ // TODO: show more info
+ this.key(openPgpKey ? openPgpKey.armor : '');
+/*
+ this.key = key;
+ const aEmails = [];
+ if (key.users) {
+ key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
+ }
+ this.id = key.getKeyID().toHex();
+ this.fingerprint = key.getFingerprint();
+ this.can_encrypt = !!key.getEncryptionKey();
+ this.can_sign = !!key.getSigningKey();
+ this.emails = aEmails;
+ this.armor = armor;
+ this.askDelete = ko.observable(false);
+ this.openForDeletion = ko.observable(null).askDeleteHelper();
+
+ key.id = key.subkeys[0].keyid;
+ key.fingerprint = key.subkeys[0].fingerprint;
+ key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
+ key.emails = aEmails;
+ "disabled": false,
+ "expired": false,
+ "revoked": false,
+ "is_secret": true,
+ "can_sign": true,
+ "can_decrypt": true
+ "can_verify": true
+ "can_encrypt": true,
+ "uids": [
+ {
+ "name": "demo",
+ "comment": "",
+ "email": "demo@snappymail.eu",
+ "uid": "demo ",
+ "revoked": false,
+ "invalid": false
+ }
+ ],
+ "subkeys": [
+ {
+ "fingerprint": "2C223F20EA2ADB4CB68F81D95F3A5CDC09AD8AE3",
+ "keyid": "5F3A5CDC09AD8AE3",
+ "timestamp": 1643381672,
+ "expires": 0,
+ "is_secret": false,
+ "invalid": false,
+ "can_encrypt": false,
+ "can_sign": true,
+ "disabled": false,
+ "expired": false,
+ "revoked": false,
+ "can_certify": true,
+ "can_authenticate": false,
+ "is_qualified": false,
+ "is_de_vs": false,
+ "pubkey_algo": 303,
+ "length": 256,
+ "keygrip": "5A1A6C7310D0508C68E8E74F15068301E83FD1AE",
+ "is_cardkey": false,
+ "curve": "ed25519"
+ },
+ {
+ "fingerprint": "3CD720549D8833872C267D08F1230DCE2A561ADE",
+ "keyid": "F1230DCE2A561ADE",
+ "timestamp": 1643381672,
+ "expires": 0,
+ "is_secret": false,
+ "invalid": false,
+ "can_encrypt": true,
+ "can_sign": false,
+ "disabled": false,
+ "expired": false,
+ "revoked": false,
+ "can_certify": false,
+ "can_authenticate": false,
+ "is_qualified": false,
+ "is_de_vs": false,
+ "pubkey_algo": 302,
+ "length": 256,
+ "keygrip": "886921A7E06BE56F8E8C51797BB476BB26DF21BF",
+ "is_cardkey": false,
+ "curve": "cv25519"
+ }
+ ]
+*/
+ }
+
+ onBuild() {
+ addShortcut('a', 'meta', 'OpenPgpKey', () => {
+ this.selectKey();
+ return false;
+ });
+ }
+}
diff --git a/dev/View/Popup/Plugin.js b/dev/View/Popup/Plugin.js
index 242d302bb..2c899a367 100644
--- a/dev/View/Popup/Plugin.js
+++ b/dev/View/Popup/Plugin.js
@@ -1,36 +1,33 @@
import ko from 'ko';
-import { Scope } from 'Common/Enums';
import { getNotification, i18n } from 'Common/Translator';
-import { isNonEmptyArray } from 'Common/Utils';
+import { arrayLength } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
-import { decorateKoCommands, isPopupVisible, showScreenPopup } from 'Knoin/Knoin';
+import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { AskPopupView } from 'View/Popup/Ask';
-class PluginPopupView extends AbstractViewPopup {
+export class PluginPopupView extends AbstractViewPopup {
constructor() {
super('Plugin');
- this.onPluginSettingsUpdateResponse = this.onPluginSettingsUpdateResponse.bind(this);
-
this.addObservables({
saveError: '',
+ id: '',
name: '',
readme: ''
});
- this.configures = ko.observableArray();
+ this.config = ko.observableArray();
- this.hasReadme = ko.computed(() => !!this.readme());
- this.hasConfiguration = ko.computed(() => 0 < this.configures().length);
+ this.addComputables({
+ hasReadme: () => !!this.readme(),
+ hasConfiguration: () => 0 < this.config().length
+ });
- this.bDisabeCloseOnEsc = true;
- this.sDefaultScope = Scope.All;
-
- this.tryToClosePopup = this.tryToClosePopup.debounce(200);
+ this.keyScope.scope = 'all';
decorateKoCommands(this, {
saveCommand: self => self.hasConfiguration()
@@ -38,73 +35,72 @@ class PluginPopupView extends AbstractViewPopup {
}
saveCommand() {
- const list = {};
- list.Name = this.name();
-
- this.configures.forEach(oItem => {
- let value = oItem.value();
+ const oConfig = {
+ Id: this.id(),
+ Settings: {}
+ },
+ setItem = item => {
+ let value = item.value();
if (false === value || true === value) {
- value = value ? '1' : '0';
+ value = value ? 1 : 0;
+ }
+ oConfig.Settings[item.Name] = value;
+ };
+
+ this.config.forEach(oItem => {
+ if (7 == oItem.Type) {
+ // Group
+ oItem.config.forEach(oSubItem => setItem(oSubItem));
+ } else {
+ setItem(oItem);
}
- list['_' + oItem.Name] = value;
});
this.saveError('');
- Remote.pluginSettingsUpdate(this.onPluginSettingsUpdateResponse, list);
- }
-
- onPluginSettingsUpdateResponse(iError) {
- if (iError) {
- this.saveError(getNotification(iError));
- } else {
- this.cancelCommand();
- }
+ Remote.request('AdminPluginSettingsUpdate',
+ iError => iError
+ ? this.saveError(getNotification(iError))
+ : this.close(),
+ oConfig);
}
onShow(oPlugin) {
+ this.id('');
this.name('');
this.readme('');
- this.configures([]);
+ this.config([]);
if (oPlugin) {
+ this.id(oPlugin.Id);
this.name(oPlugin.Name);
this.readme(oPlugin.Readme);
const config = oPlugin.Config;
- if (isNonEmptyArray(config)) {
- this.configures(
- config.map(item => ({
- value: ko.observable(item[0]),
- placeholder: ko.observable(item[6]),
- Name: item[1],
- Type: item[2],
- Label: item[3],
- Default: item[4],
- Desc: item[5]
- }))
+ if (arrayLength(config)) {
+ this.config(
+ config.map(item => {
+ if (7 == item.Type) {
+ // Group
+ item.config.forEach(subItem => {
+ subItem.value = ko.observable(subItem.value);
+ });
+ } else {
+ item.value = ko.observable(item.value);
+ }
+ return item;
+ })
);
}
}
}
- tryToClosePopup() {
- if (!isPopupVisible(AskPopupView)) {
+ onClose() {
+ if (AskPopupView.hidden()) {
showScreenPopup(AskPopupView, [
i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
- () => this.modalVisibility() && this.cancelCommand && this.cancelCommand()
+ () => this.close()
]);
}
- }
-
- onBuild() {
- shortcuts.add('escape', '', Scope.All, () => {
- if (this.modalVisibility()) {
- this.tryToClosePopup();
- }
-
- return false;
- });
+ return false;
}
}
-
-export { PluginPopupView, PluginPopupView as default };
diff --git a/dev/View/Popup/SieveScript.js b/dev/View/Popup/SieveScript.js
deleted file mode 100644
index ab15918e5..000000000
--- a/dev/View/Popup/SieveScript.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import ko from 'ko';
-
-import { getNotification, i18nToNodes } from 'Common/Translator';
-import { addObservablesTo } from 'Common/Utils';
-import { delegateRunOnDestroy } from 'Common/UtilsUser';
-
-import Remote from 'Remote/User/Fetch';
-import { FilterModel } from 'Model/Filter';
-import { SieveUserStore } from 'Stores/User/Sieve';
-
-import { showScreenPopup/*, decorateKoCommands*/ } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-import { FilterPopupView } from 'View/Popup/Filter';
-
-class SieveScriptPopupView extends AbstractViewPopup {
- constructor() {
- super('SieveScript');
-
- addObservablesTo(this, {
- saveError: false,
- saveErrorText: '',
- rawActive: false,
- allowToggle: false,
- script: null
- });
-
- this.sieveCapabilities = SieveUserStore.capa.join(' ');
- this.saving = false;
-
- this.filterForDeletion = ko.observable(null).deleteAccessHelper();
-/*
- decorateKoCommands(this, {
- saveScriptCommand: 1
- });
-*/
- }
-
- saveScriptCommand() {
- let self = this,
- script = self.script();
- if (!self.saving/* && script.hasChanges()*/) {
- if (!script.verify()) {
- return false;
- }
-
- if (!script.exists() && SieveUserStore.scripts.find(item => item.name() === script.name())) {
- script.nameError(true);
- return false;
- }
-
- self.saving = true;
- self.saveError(false);
-
- if (self.allowToggle()) {
- script.body(script.filtersToRaw());
- }
-
- Remote.filtersScriptSave(
- (iError, data) => {
- self.saving = false;
-
- if (iError) {
- self.saveError(true);
- self.saveErrorText((data && data.ErrorMessageAdditional) || getNotification(iError));
- } else {
- script.exists() || SieveUserStore.scripts.push(script);
- script.exists(true);
- script.hasChanges(false);
- }
- },
- script
- );
- }
-
- return true;
- }
-
- deleteFilter(filter) {
- this.script().filters.remove(filter);
- delegateRunOnDestroy(filter);
- }
-
- addFilter() {
- /* this = SieveScriptModel */
- const filter = new FilterModel();
- filter.generateID();
- showScreenPopup(FilterPopupView, [
- filter,
- () => {
- this.filters.push(filter);
- },
- false
- ]);
- }
-
- editFilter(filter) {
- const clonedFilter = filter.cloneSelf();
- showScreenPopup(FilterPopupView, [
- clonedFilter,
- () => {
- const script = this.script(),
- filters = script.filters(),
- index = filters.indexOf(filter);
- if (-1 < index) {
- delegateRunOnDestroy(filters[index]);
- filters[index] = clonedFilter;
- script.filters(filters);
- }
- },
- true
- ]);
- }
-
- toggleFiltersRaw() {
- if (!this.rawActive()) {
- let script = this.script(),
- changed = script.hasChanges();
- script.body(script.filtersToRaw());
- script.hasChanges(changed);
- }
- this.rawActive(!this.rawActive());
- }
-
- onBuild(oDom) {
- oDom.addEventListener('click', event => {
- const el = event.target.closestWithin('.filter-item .e-action', oDom),
- filter = el && ko.dataFor(el);
- filter && this.editFilter(filter);
- });
- }
-
- onShow(oScript) {
- this.script(oScript);
- this.rawActive(!oScript.allowFilters());
- this.allowToggle(oScript.allowFilters());
- this.saveError(false);
- }
-
- onShowWithDelay() {
- // Sometimes not everything is translated, try again
- i18nToNodes(this.viewModelDom);
- }
-}
-
-export { SieveScriptPopupView, SieveScriptPopupView as default };
diff --git a/dev/View/Popup/Template.js b/dev/View/Popup/Template.js
deleted file mode 100644
index 60758a808..000000000
--- a/dev/View/Popup/Template.js
+++ /dev/null
@@ -1,148 +0,0 @@
-import { getNotification } from 'Common/Translator';
-import { HtmlEditor } from 'Common/Html';
-
-import Remote from 'Remote/User/Fetch';
-
-import { decorateKoCommands } from 'Knoin/Knoin';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-import { TemplateModel } from 'Model/Template';
-
-class TemplatePopupView extends AbstractViewPopup {
- constructor() {
- super('Template');
-
- this.editor = null;
-
- this.addObservables({
- signatureDom: null,
-
- id: '',
-
- name: '',
- nameError: false,
-
- body: '',
- bodyLoading: false,
- bodyError: false,
-
- submitRequest: false,
- submitError: ''
- });
-
- this.name.subscribe(() => this.nameError(false));
-
- this.body.subscribe(() => this.bodyError(false));
-
- decorateKoCommands(this, {
- addTemplateCommand: self => !self.submitRequest()
- });
- }
-
- addTemplateCommand() {
- this.populateBodyFromEditor();
-
- this.nameError(!this.name().trim());
- this.bodyError(!this.body().trim() || ':HTML:' === this.body().trim());
-
- if (this.nameError() || this.bodyError()) {
- return false;
- }
-
- this.submitRequest(true);
-
- Remote.templateSetup(
- iError => {
- this.submitRequest(false);
- if (iError) {
- this.submitError(getNotification(iError));
- } else {
- rl.app.templates();
- this.cancelCommand();
- }
- },
- this.id(),
- this.name(),
- this.body()
- );
-
- return true;
- }
-
- clearPopup() {
- this.id('');
-
- this.name('');
- this.nameError(false);
-
- this.body('');
- this.bodyLoading(false);
- this.bodyError(false);
-
- this.submitRequest(false);
- this.submitError('');
-
- if (this.editor) {
- this.editor.setPlain('');
- }
- }
-
- populateBodyFromEditor() {
- if (this.editor) {
- this.body(this.editor.getDataWithHtmlMark());
- }
- }
-
- editorSetBody(sBody) {
- if (!this.editor && this.signatureDom()) {
- this.editor = new HtmlEditor(
- this.signatureDom(),
- () => this.populateBodyFromEditor(),
- () => this.editor.setHtmlOrPlain(sBody)
- );
- } else {
- this.editor.setHtmlOrPlain(sBody);
- }
- }
-
- onShow(template) {
- this.clearPopup();
-
- if (template && template.id) {
- this.id(template.id);
- this.name(template.name);
- this.body(template.body);
-
- if (template.populated) {
- this.editorSetBody(this.body());
- } else {
- this.bodyLoading(true);
- this.bodyError(false);
-
- Remote.templateGetById((iError, data) => {
- this.bodyLoading(false);
-
- if (
- !iError &&
- TemplateModel.validJson(data.Result) &&
- null != data.Result.Body
- ) {
- template.body = data.Result.Body;
- template.populated = true;
-
- this.body(template.body);
- this.bodyError(false);
- } else {
- this.body('');
- this.bodyError(true);
- }
-
- this.editorSetBody(this.body());
- }, this.id());
- }
- } else {
- this.editorSetBody('');
- }
- }
-}
-
-export { TemplatePopupView, TemplatePopupView as default };
diff --git a/dev/View/Popup/ViewOpenPgpKey.js b/dev/View/Popup/ViewOpenPgpKey.js
deleted file mode 100644
index 5dcbbc289..000000000
--- a/dev/View/Popup/ViewOpenPgpKey.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Scope } from 'Common/Enums';
-import { doc } from 'Common/Globals';
-import { AbstractViewPopup } from 'Knoin/AbstractViews';
-
-class ViewOpenPgpKeyPopupView extends AbstractViewPopup {
- constructor() {
- super('ViewOpenPgpKey');
-
- this.addObservables({
- key: '',
- keyDom: null
- });
- }
-
- clearPopup() {
- this.key('');
- }
-
- selectKey() {
- const el = this.keyDom();
- if (el) {
- let sel = getSelection(),
- range = doc.createRange();
- sel.removeAllRanges();
- range.selectNodeContents(el);
- sel.addRange(range);
- }
- }
-
- onShow(openPgpKey) {
- this.clearPopup();
-
- if (openPgpKey) {
- this.key(openPgpKey.armor);
- }
- }
-
- onBuild() {
- shortcuts.add('a', 'meta', Scope.ViewOpenPgpKey, () => {
- this.selectKey();
- return false;
- });
- }
-}
-
-export { ViewOpenPgpKeyPopupView, ViewOpenPgpKeyPopupView as default };
diff --git a/dev/View/User/AbstractSystemDropDown.js b/dev/View/User/AbstractSystemDropDown.js
deleted file mode 100644
index 96c9fb0fe..000000000
--- a/dev/View/User/AbstractSystemDropDown.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { AppUserStore } from 'Stores/User/App';
-import { AccountUserStore } from 'Stores/User/Account';
-import { MessageUserStore } from 'Stores/User/Message';
-
-import { Capa, Scope } from 'Common/Enums';
-import { settings } from 'Common/Links';
-
-import { showScreenPopup } from 'Knoin/Knoin';
-import { AbstractViewRight } from 'Knoin/AbstractViews';
-
-import { KeyboardShortcutsHelpPopupView } from 'View/Popup/KeyboardShortcutsHelp';
-import { AccountPopupView } from 'View/Popup/Account';
-import { ContactsPopupView } from 'View/Popup/Contacts';
-
-import { doc, Settings, leftPanelDisabled } from 'Common/Globals';
-
-import { ThemeStore } from 'Stores/Theme';
-
-export class AbstractSystemDropDownUserView extends AbstractViewRight {
- constructor(name) {
- super(name, 'SystemDropDown');
-
- this.allowAccounts = Settings.capa(Capa.AdditionalAccounts);
- this.allowSettings = Settings.capa(Capa.Settings);
- this.allowHelp = Settings.capa(Capa.Help);
-
- this.accountEmail = AccountUserStore.email;
-
- this.accounts = AccountUserStore.accounts;
- this.accountsLoading = AccountUserStore.loading;
- this.accountsUnreadCount = AccountUserStore.accountsUnreadCount;
-
- this.addObservables({
- currentAudio: '',
- accountMenuDropdownTrigger: false
- });
-
- this.allowContacts = AppUserStore.allowContacts();
-
- addEventListener('audio.stop', () => this.currentAudio(''));
- addEventListener('audio.start', e => this.currentAudio(e.detail));
- }
-
- stopPlay() {
- dispatchEvent(new CustomEvent('audio.api.stop'));
- }
-
- accountClick(account, event) {
- if (account && 0 === event.button) {
- AccountUserStore.loading(true);
- setTimeout(() => AccountUserStore.loading(false), 1000);
- }
-
- return true;
- }
-
- emailTitle() {
- return AccountUserStore.email();
- }
-
- settingsClick() {
- this.allowSettings && rl.route.setHash(settings());
- }
-
- settingsHelp() {
- this.allowHelp && showScreenPopup(KeyboardShortcutsHelpPopupView);
- }
-
- addAccountClick() {
- this.allowAccounts && showScreenPopup(AccountPopupView);
- }
-
- contactsClick() {
- this.allowContacts && showScreenPopup(ContactsPopupView);
- }
-
- layoutDesktop()
- {
- doc.cookie = 'rllayout=desktop';
- ThemeStore.isMobile(false);
- leftPanelDisabled(false);
-// location.reload();
- }
-
- layoutMobile()
- {
- doc.cookie = 'rllayout=mobile';
- ThemeStore.isMobile(true);
- leftPanelDisabled(true);
-// location.reload();
- }
-
- logoutClick() {
- rl.app.logout();
- }
-
- onBuild() {
- shortcuts.add('m,contextmenu', '', [Scope.MessageList, Scope.MessageView, Scope.Settings], () => {
- if (this.viewModelVisible) {
- MessageUserStore.messageFullScreenMode(false);
- this.accountMenuDropdownTrigger(true);
- return false;
- }
- });
-
- // shortcuts help
- shortcuts.add('?,f1,help', '', [Scope.MessageList, Scope.MessageView, Scope.Settings], () => {
- if (this.viewModelVisible) {
- showScreenPopup(KeyboardShortcutsHelpPopupView);
- return false;
- }
- return true;
- });
- }
-}
diff --git a/dev/View/User/Login.js b/dev/View/User/Login.js
index 2ab453b05..6db9779fa 100644
--- a/dev/View/User/Login.js
+++ b/dev/View/User/Login.js
@@ -1,10 +1,7 @@
-import ko from 'ko';
-
import { Notification } from 'Common/Enums';
-import { ClientSideKeyName } from 'Common/EnumsUser';
-import { Settings, SettingsGet } from 'Common/Globals';
-import { getNotification, reload as translatorReload, convertLangName } from 'Common/Translator';
-import { runHook } from 'Common/Plugins';
+import { ClientSideKeyNameLastSignMe } from 'Common/EnumsUser';
+import { SettingsGet, fireEvent } from 'Common/Globals';
+import { getNotification, translatorReload, convertLangName } from 'Common/Translator';
import { LanguageStore } from 'Stores/Language';
@@ -13,29 +10,19 @@ import * as Local from 'Storage/Client';
import Remote from 'Remote/User/Fetch';
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
-import { AbstractViewCenter } from 'Knoin/AbstractViews';
+import { AbstractViewLogin } from 'Knoin/AbstractViews';
import { LanguagesPopupView } from 'View/Popup/Languages';
const
- LoginSignMeType = {
- DefaultOff: 0,
- DefaultOn: 1,
- Unused: 2
- },
-
- LoginSignMeTypeAsString = {
- DefaultOff: 'defaultoff',
- DefaultOn: 'defaulton',
- Unused: 'unused'
- };
+ SignMeOff = 0,
+ SignMeOn = 1,
+ SignMeUnused = 2;
-class LoginUserView extends AbstractViewCenter {
+export class LoginUserView extends AbstractViewLogin {
constructor() {
- super('User/Login', 'Login');
-
- this.hideSubmitButton = Settings.app('hideSubmitButton');
+ super();
this.addObservables({
loadingDesc: SettingsGet('LoadingDescription'),
@@ -47,22 +34,15 @@ class LoginUserView extends AbstractViewCenter {
emailError: false,
passwordError: false,
- formHidden: false,
-
submitRequest: false,
submitError: '',
submitErrorAddidional: '',
langRequest: false,
- signMeType: LoginSignMeType.Unused
+ signMeType: SignMeUnused
});
- this.forgotPasswordLinkUrl = Settings.app('forgotPasswordLinkUrl');
- this.registrationLinkUrl = Settings.app('registrationLinkUrl');
-
- this.formError = ko.observable(false).extend({ falseTimeout: 500 });
-
this.allowLanguagesOnLogin = !!SettingsGet('AllowLanguagesOnLogin');
this.language = LanguageStore.language;
@@ -74,19 +54,17 @@ class LoginUserView extends AbstractViewCenter {
languageFullName: () => convertLangName(this.language()),
- signMeVisibility: () => LoginSignMeType.Unused !== this.signMeType()
+ signMeVisibility: () => SignMeUnused !== this.signMeType()
});
this.addSubscribables({
- email: () => {
- this.emailError(false);
- },
+ email: () => this.emailError(false),
password: () => this.passwordError(false),
submitError: value => value || this.submitErrorAddidional(''),
- signMeType: iValue => this.signMe(LoginSignMeType.DefaultOn === iValue),
+ signMeType: iValue => this.signMe(SignMeOn === iValue),
language: value => {
this.langRequest(true);
@@ -110,96 +88,75 @@ class LoginUserView extends AbstractViewCenter {
}
submitCommand(self, event) {
- let email = this.email().trim(),
- valid = event.target.form.reportValidity() && email,
- pass = this.password();
-
- this.emailError(!email);
- this.passwordError(!pass);
-
- let pluginResultCode = 0, pluginResultMessage = '';
- runHook('user-login-submit', [(iResultCode, sResultMessage) => {
- pluginResultCode = iResultCode;
- pluginResultMessage = sResultMessage;
- }]);
- if (0 < pluginResultCode) {
- this.submitError(getNotification(pluginResultCode));
- valid = false;
- } else if (pluginResultMessage) {
- this.submitError(pluginResultMessage);
- valid = false;
- }
+ let form = event.target.form,
+ data = new FormData(form),
+ valid = form.reportValidity() && fireEvent('sm-user-login', data);
+ this.emailError(!this.email());
+ this.passwordError(!this.password());
this.formError(!valid);
if (valid) {
this.submitRequest(true);
-
- Remote.login(
+ data.set('Language', this.bSendLanguage ? this.language() : '');
+ data.set('SignMe', this.signMe() ? 1 : 0);
+ Remote.request('Login',
(iError, oData) => {
+ fireEvent('sm-user-login-response', {
+ error: iError,
+ data: oData
+ });
if (iError) {
this.submitRequest(false);
if (Notification.InvalidInputArgument == iError) {
iError = Notification.AuthError;
}
- this.submitError(getNotification(iError, oData.ErrorMessage, Notification.UnknownNotification));
+ this.submitError(getNotification(iError, (oData ? oData.ErrorMessage : ''),
+ Notification.UnknownNotification));
this.submitErrorAddidional((oData && oData.ErrorMessageAdditional) || '');
} else {
- rl.route.reload();
+ rl.setData(oData.Result);
}
},
- email,
- pass,
- !!this.signMe(),
- this.bSendLanguage ? this.language() : ''
+ data
);
- Local.set(ClientSideKeyName.LastSignMe, this.signMe() ? '-1-' : '-0-');
+ Local.set(ClientSideKeyNameLastSignMe, this.signMe() ? '-1-' : '-0-');
}
return valid;
}
- onShow() {
- rl.route.off();
- }
+ onBuild(dom) {
+ super.onBuild(dom);
- onBuild() {
- const signMeLocal = Local.get(ClientSideKeyName.LastSignMe),
- signMe = (SettingsGet('SignMe') || 'unused').toLowerCase();
+ const signMe = (SettingsGet('SignMe') || '').toLowerCase();
switch (signMe) {
- case LoginSignMeTypeAsString.DefaultOff:
- case LoginSignMeTypeAsString.DefaultOn:
+ case 'defaultoff':
+ case 'defaulton':
this.signMeType(
- LoginSignMeTypeAsString.DefaultOn === signMe ? LoginSignMeType.DefaultOn : LoginSignMeType.DefaultOff
+ 'defaulton' === signMe ? SignMeOn : SignMeOff
);
- switch (signMeLocal) {
+ switch (Local.get(ClientSideKeyNameLastSignMe)) {
case '-1-':
- this.signMeType(LoginSignMeType.DefaultOn);
+ this.signMeType(SignMeOn);
break;
case '-0-':
- this.signMeType(LoginSignMeType.DefaultOff);
+ this.signMeType(SignMeOff);
break;
// no default
}
break;
- case LoginSignMeTypeAsString.Unused:
default:
- this.signMeType(LoginSignMeType.Unused);
+ this.signMeType(SignMeUnused);
break;
}
}
- submitForm() {
-// return false;
- }
-
selectLanguage() {
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
}
}
-
-export { LoginUserView };
diff --git a/dev/View/User/MailBox/FolderList.js b/dev/View/User/MailBox/FolderList.js
index 63f286d3b..9f367cf0c 100644
--- a/dev/View/User/MailBox/FolderList.js
+++ b/dev/View/User/MailBox/FolderList.js
@@ -1,15 +1,16 @@
import ko from 'ko';
-import { Capa, Scope } from 'Common/Enums';
-import { leftPanelDisabled, moveAction, Settings } from 'Common/Globals';
+import { Scope } from 'Common/Enums';
+import { moveAction, addShortcut } from 'Common/Globals';
import { mailBox, settings } from 'Common/Links';
import { setFolderHash } from 'Common/Cache';
+import { addComputablesTo } from 'External/ko';
import { AppUserStore } from 'Stores/User/App';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
-import { ThemeStore } from 'Stores/Theme';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewLeft } from 'Knoin/AbstractViews';
@@ -18,9 +19,13 @@ import { showMessageComposer } from 'Common/UtilsUser';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { ContactsPopupView } from 'View/Popup/Contacts';
-export class FolderListMailBoxUserView extends AbstractViewLeft {
+import { moveMessagesToFolder } from 'Common/Folders';
+
+import { setExpandedFolder } from 'Model/FolderCollection';
+
+export class MailFolderList extends AbstractViewLeft {
constructor() {
- super('User/MailBox/FolderList', 'MailFolderList');
+ super();
this.oContentScrollable = null;
@@ -32,74 +37,44 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
this.moveAction = moveAction;
- this.foldersListWithSingleInboxRootFolder = FolderUserStore.foldersListWithSingleInboxRootFolder;
+ this.foldersListWithSingleInboxRootFolder = ko.observable(false);
- this.leftPanelDisabled = leftPanelDisabled;
-
- this.allowComposer = Settings.capa(Capa.Composer);
this.allowContacts = AppUserStore.allowContacts();
- this.folderListFocused = ko.computed(() => Scope.FolderList === AppUserStore.focusedState());
-
- this.isInboxStarred = ko.computed(
- () =>
- FolderUserStore.currentFolder() &&
- FolderUserStore.currentFolder().isInbox() &&
- MessageUserStore.listSearch().trim().includes('is:flagged')
- );
+ addComputablesTo(this, {
+ folderListVisible: () => {
+ let multiple = false,
+ inbox, visible,
+ result = FolderUserStore.folderList().filter(folder => {
+ if (folder.isInbox()) {
+ inbox = folder;
+ }
+ visible = folder.visible();
+ multiple |= visible && !folder.isInbox();
+ return visible;
+ });
+ if (inbox && !multiple) {
+ inbox.collapsed(false);
+ }
+ this.foldersListWithSingleInboxRootFolder(!multiple);
+ return result;
+ }
+ });
}
onBuild(dom) {
const qs = s => dom.querySelector(s),
- eqs = (ev, s) => ev.target.closestWithin(s, dom),
- fSelectFolder = (el, event, starred) => {
- const isMove = moveAction();
- ThemeStore.isMobile() && leftPanelDisabled(true);
-
- event.preventDefault();
-
- if (starred) {
- event.stopPropagation();
- }
-
- const folder = ko.dataFor(el);
- if (folder) {
- if (isMove) {
- moveAction(false);
- rl.app.moveMessagesToFolder(
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
- folder.fullNameRaw,
- event.ctrlKey
- );
- } else {
- if (!SettingsUserStore.usePreviewPane()) {
- MessageUserStore.message(null);
- }
-
- if (folder.fullNameRaw === FolderUserStore.currentFolderFullNameRaw()) {
- setFolderHash(folder.fullNameRaw, '');
- }
-
- rl.route.setHash(starred
- ? mailBox(folder.fullNameHash, 1, 'is:flagged')
- : mailBox(folder.fullNameHash)
- );
- }
-
- AppUserStore.focusedState(Scope.MessageList);
- }
- };
+ eqs = (ev, s) => ev.target.closestWithin(s, dom);
this.oContentScrollable = qs('.b-content');
dom.addEventListener('click', event => {
- let el = event.target.closest('.e-collapsed-sign');
+ let el = eqs(event, '.e-collapsed-sign');
if (el) {
const folder = ko.dataFor(el);
if (folder) {
const collapsed = folder.collapsed();
- rl.app.setExpandedFolder(folder.fullNameHash, collapsed);
+ setExpandedFolder(folder.fullName, collapsed);
folder.collapsed(!collapsed);
event.preventDefault();
@@ -108,16 +83,43 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
}
}
- el = eqs(event, '.b-folders .e-item .e-link.selectable .inbox-star-icon');
- el && fSelectFolder(el, event, !this.isInboxStarred());
+ el = eqs(event, 'a');
+ if (el && el.matches('.selectable')) {
+ event.preventDefault();
+ const folder = ko.dataFor(el);
+ if (folder) {
+ if (moveAction()) {
+ moveAction(false);
+ moveMessagesToFolder(
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ folder.fullName,
+ event.ctrlKey
+ );
+ } else {
+ if (!SettingsUserStore.usePreviewPane()) {
+ MessageUserStore.message(null);
+ }
- el = eqs(event, '.b-folders .e-item .e-link.selectable');
- el && fSelectFolder(el, event, false);
+ if (folder.fullName === FolderUserStore.currentFolderFullName()) {
+ setFolderHash(folder.fullName, '');
+ }
+
+ hasher.setHash(
+ mailBox(folder.fullNameHash, 1,
+ (event.target.matches('.flag-icon') && !folder.isFlagged()) ? 'flagged' : ''
+ )
+ );
+ }
+
+ AppUserStore.focusedState(Scope.MessageList);
+ }
+ }
});
- shortcuts.add('arrowup,arrowdown', '', Scope.FolderList, event => {
+ addShortcut('arrowup,arrowdown', '', Scope.FolderList, event => {
let items = [], index = 0;
- dom.querySelectorAll('.b-folders .e-item .e-link:not(.hidden)').forEach(node => {
+ dom.querySelectorAll('li a:not(.hidden)').forEach(node => {
if (node.offsetHeight || node.getClientRects().length) {
items.push(node);
if (node.matches('.focused')) {
@@ -139,8 +141,8 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
return false;
});
- shortcuts.add('enter,open', '', Scope.FolderList, () => {
- const item = qs('.b-folders .e-item .e-link:not(.hidden).focused');
+ addShortcut('enter,open', '', Scope.FolderList, () => {
+ const item = qs('li a:not(.hidden).focused');
if (item) {
AppUserStore.focusedState(Scope.MessageList);
item.click();
@@ -149,31 +151,31 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
return false;
});
- shortcuts.add('space', '', Scope.FolderList, () => {
- const item = qs('.b-folders .e-item .e-link:not(.hidden).focused'),
+ addShortcut('space', '', Scope.FolderList, () => {
+ const item = qs('li a:not(.hidden).focused'),
folder = item && ko.dataFor(item);
if (folder) {
const collapsed = folder.collapsed();
- rl.app.setExpandedFolder(folder.fullNameHash, collapsed);
+ setExpandedFolder(folder.fullName, collapsed);
folder.collapsed(!collapsed);
}
return false;
});
-// shortcuts.add('tab', 'shift', Scope.FolderList, () => {
- shortcuts.add('escape,tab,arrowright', '', Scope.FolderList, () => {
+// addShortcut('tab', 'shift', Scope.FolderList, () => {
+ addShortcut('escape,tab,arrowright', '', Scope.FolderList, () => {
AppUserStore.focusedState(Scope.MessageList);
moveAction(false);
return false;
});
AppUserStore.focusedState.subscribe(value => {
- let el = qs('.b-folders .e-item .e-link.focused');
- el && qs('.b-folders .e-item .e-link.focused').classList.remove('focused');
+ let el = qs('li a.focused');
+ el && el.classList.remove('focused');
if (Scope.FolderList === value) {
- el = qs('.b-folders .e-item .e-link.selected');
- el && qs('.b-folders .e-item .e-link.selected').classList.add('focused');
+ el = qs('li a.selected');
+ el && el.classList.add('focused');
}
});
}
@@ -181,7 +183,7 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
scrollToFocused() {
const scrollable = this.oContentScrollable;
if (scrollable) {
- let block, focused = scrollable.querySelector('.e-item .e-link.focused');
+ let block, focused = scrollable.querySelector('li a.focused');
if (focused) {
const fRect = focused.getBoundingClientRect(),
sRect = scrollable.getBoundingClientRect();
@@ -204,7 +206,7 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
}
configureFolders() {
- rl.route.setHash(settings('folders'));
+ hasher.setHash(settings('folders'));
}
contactsClick() {
diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js
index 27459d8bb..650731d5b 100644
--- a/dev/View/User/MailBox/MessageList.js
+++ b/dev/View/User/MailBox/MessageList.js
@@ -1,22 +1,20 @@
import ko from 'ko';
-import {
- Capa,
- Scope
-} from 'Common/Enums';
+import { Scope } from 'Common/Enums';
-import {
- ComposeType,
- FolderType,
- MessageSetAction
-} from 'Common/EnumsUser';
+import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
-import { doc, leftPanelDisabled, moveAction, Settings, SettingsGet } from 'Common/Globals';
+import { doc, leftPanelDisabled, moveAction,
+ Settings, SettingsCapa, SettingsGet,
+ addEventsListeners,
+ addShortcut, registerShortcut, formFieldFocused
+} from 'Common/Globals';
-import { computedPaginatorHelper, showMessageComposer } from 'Common/UtilsUser';
+import { computedPaginatorHelper, showMessageComposer, populateMessageBody } from 'Common/UtilsUser';
import { FileInfo } from 'Common/File';
+import { folderListOptionsBuilder, moveMessagesToFolder } from 'Common/Folders';
import { mailBox, serverRequest } from 'Common/Links';
import { Selector } from 'Common/Selector';
@@ -31,66 +29,69 @@ import {
} from 'Common/Cache';
import { AppUserStore } from 'Stores/User/App';
-import { QuotaUserStore } from 'Stores/User/Quota';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
-import { decorateKoCommands, showScreenPopup, popupVisibilityNames } from 'Knoin/Knoin';
+import { decorateKoCommands, showScreenPopup, arePopupsVisible } from 'Knoin/Knoin';
import { AbstractViewRight } from 'Knoin/AbstractViews';
import { FolderClearPopupView } from 'View/Popup/FolderClear';
import { AdvancedSearchPopupView } from 'View/Popup/AdvancedSearch';
-const
- canBeMovedHelper = () => MessageUserStore.hasCheckedOrSelected();
+import { MessageModel } from 'Model/Message';
-export class MessageListMailBoxUserView extends AbstractViewRight {
+const
+ canBeMovedHelper = () => MessagelistUserStore.hasCheckedOrSelected(),
+
+ /**
+ * @param {string} sFolderFullName
+ * @param {number} iSetAction
+ * @param {Array=} aMessages = null
+ * @returns {void}
+ */
+ listAction = (...args) => MessagelistUserStore.setAction(...args);
+
+export class MailMessageList extends AbstractViewRight {
constructor() {
- super('User/MailBox/MessageList', 'MailMessageList');
+ super();
this.bPrefetch = false;
this.emptySubjectValue = '';
- this.iGoToUpUpOrDownDownTimeout = 0;
+ this.iGoToUpOrDownTimeout = 0;
this.newMoveToFolder = !!SettingsGet('NewMoveToFolder');
- this.allowReload = Settings.capa(Capa.Reload);
- this.allowSearch = Settings.capa(Capa.Search);
- this.allowSearchAdv = Settings.capa(Capa.SearchAdv);
- this.allowComposer = Settings.capa(Capa.Composer);
- this.allowMessageListActions = Settings.capa(Capa.MessageListActions);
- this.allowDangerousActions = Settings.capa(Capa.DangerousActions);
+ this.allowSearch = SettingsCapa('Search');
+ this.allowSearchAdv = SettingsCapa('SearchAdv');
+ this.allowDangerousActions = SettingsCapa('DangerousActions');
- this.popupVisibility = ko.computed(() => 0 < popupVisibilityNames().length);
-
- this.messageList = MessageUserStore.list;
-
- this.sortSupported = FolderUserStore.sortSupported;
+ this.messageList = MessagelistUserStore;
this.composeInEdit = AppUserStore.composeInEdit;
+
+ this.isMobile = ThemeStore.isMobile;
this.leftPanelDisabled = leftPanelDisabled;
- this.isMessageSelected = MessageUserStore.isMessageSelected;
- this.messageListSearch = MessageUserStore.listSearch;
- this.messageListError = MessageUserStore.listError;
- this.folderMenuForMove = FolderUserStore.folderMenuForMove;
+ this.messageListSearch = MessagelistUserStore.listSearch;
+ this.messageListError = MessagelistUserStore.error;
+
+ this.popupVisibility = arePopupsVisible;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
- this.messageListEndThreadUid = MessageUserStore.listEndThreadUid;
+ this.messageListThreadUid = MessagelistUserStore.endThreadUid;
- this.messageListCheckedOrSelected = MessageUserStore.listCheckedOrSelected;
- this.messageListCompleteLoadingThrottle = MessageUserStore.listCompleteLoading;
- this.messageListCompleteLoadingThrottleForAnimation = MessageUserStore.listLoadingAnimation;
+ this.messageListIsLoading = MessagelistUserStore.isLoading;
initOnStartOrLangChange(() => this.emptySubjectValue = i18n('MESSAGE_LIST/EMPTY_SUBJECT_TEXT'));
- this.userUsageProc = QuotaUserStore.percentage;
+ this.userUsageProc = FolderUserStore.quotaPercentage;
this.addObservables({
moveDropdownTrigger: false,
@@ -110,45 +111,56 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
this.sLastSearchValue = '';
this.addComputables({
+
+ sortSupported: () =>
+ FolderUserStore.hasCapability('SORT') | FolderUserStore.hasCapability('ESORT'),
+
+ folderMenuForMove: () =>
+ folderListOptionsBuilder(
+ [FolderUserStore.currentFolderFullName()],
+ [],
+ item => item ? item.localName() : ''
+ ),
+
messageListSearchDesc: () => {
- const value = MessageUserStore.listEndSearch();
+ const value = MessagelistUserStore().Search;
return value ? i18n('MESSAGE_LIST/SEARCH_RESULT_FOR', { SEARCH: value }) : ''
},
- messageListPaginator: computedPaginatorHelper(MessageUserStore.listPage,
- MessageUserStore.listPageCount),
+ messageListPaginator: computedPaginatorHelper(MessagelistUserStore.page,
+ MessagelistUserStore.pageCount),
checkAll: {
- read: () => 0 < MessageUserStore.listChecked().length,
+ read: () => 0 < MessagelistUserStore.listChecked().length,
write: (value) => {
value = !!value;
- MessageUserStore.list.forEach(message => message.checked(value));
+ MessagelistUserStore.forEach(message => message.checked(value));
}
},
inputProxyMessageListSearch: {
- read: MessageUserStore.mainMessageListSearch,
+ read: MessagelistUserStore.mainSearch,
write: value => this.sLastSearchValue = value
},
isIncompleteChecked: () => {
- const c = MessageUserStore.listChecked().length;
- return c && MessageUserStore.list.length > c;
+ const c = MessagelistUserStore.listChecked().length;
+ return c && MessagelistUserStore().length > c;
},
- hasMessages: () => 0 < MessageUserStore.list.length,
+ hasMessages: () => 0 < MessagelistUserStore().length,
- isSpamFolder: () => (FolderUserStore.spamFolder() || 0) === MessageUserStore.listEndFolder(),
+ isSpamFolder: () => (FolderUserStore.spamFolder() || 0) === MessagelistUserStore().Folder,
isSpamDisabled: () => UNUSED_OPTION_VALUE === FolderUserStore.spamFolder(),
- isTrashFolder: () => (FolderUserStore.trashFolder() || 0) === MessageUserStore.listEndFolder(),
+ isTrashFolder: () => (FolderUserStore.trashFolder() || 0) === MessagelistUserStore().Folder,
- isDraftFolder: () => (FolderUserStore.draftFolder() || 0) === MessageUserStore.listEndFolder(),
+ isDraftFolder: () => (FolderUserStore.draftsFolder() || 0) === MessagelistUserStore().Folder,
- isSentFolder: () => (FolderUserStore.sentFolder() || 0) === MessageUserStore.listEndFolder(),
+ isSentFolder: () => (FolderUserStore.sentFolder() || 0) === MessagelistUserStore().Folder,
- isArchiveFolder: () => (FolderUserStore.archiveFolder() || 0) === MessageUserStore.listEndFolder(),
+ isArchiveFolder: () => (FolderUserStore.archiveFolder() || 0) === MessagelistUserStore().Folder,
isArchiveDisabled: () => UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder(),
@@ -160,11 +172,9 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
isUnSpamVisible: () =>
this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder(),
- mobileCheckedStateShow: () => ThemeStore.isMobile() ? 0 < MessageUserStore.listChecked().length : true,
+ mobileCheckedStateShow: () => ThemeStore.isMobile() ? 0 < MessagelistUserStore.listChecked().length : true,
- mobileCheckedStateHide: () => ThemeStore.isMobile() ? !MessageUserStore.listChecked().length : true,
-
- messageListFocused: () => Scope.MessageList === AppUserStore.focusedState(),
+ mobileCheckedStateHide: () => ThemeStore.isMobile() ? !MessagelistUserStore.listChecked().length : true,
sortText: () => {
let mode = FolderUserStore.sortMode(),
@@ -180,24 +190,33 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
});
- this.hasCheckedOrSelectedLines = MessageUserStore.hasCheckedOrSelected,
+ this.hasCheckedOrSelectedLines = MessagelistUserStore.hasCheckedOrSelected,
this.selector = new Selector(
- MessageUserStore.list,
- MessageUserStore.selectorMessageSelected,
- MessageUserStore.selectorMessageFocused,
+ MessagelistUserStore,
+ MessagelistUserStore.selectedMessage,
+ MessagelistUserStore.focusedMessage,
'.messageListItem .actionHandle',
'.messageListItem .checkboxMessage',
'.messageListItem.focused'
);
- this.selector.on('onItemSelect', message => MessageUserStore.selectMessage(message));
+ this.selector.on('ItemSelect', message => {
+ if (message) {
+ MessageUserStore.message(MessageModel.fromMessageListItem(message));
+ populateMessageBody(MessageUserStore.message());
+ } else {
+ MessageUserStore.message(null);
+ }
+ });
- this.selector.on('onItemGetUid', message => (message ? message.generateUid() : ''));
+ this.selector.on('MiddleClick', message => populateMessageBody(message, true));
- this.selector.on('onAutoSelect', () => this.useAutoSelect());
+ this.selector.on('ItemGetUid', message => (message ? message.generateUid() : ''));
- this.selector.on('onUpUpOrDownDown', v => this.goToUpUpOrDownDown(v));
+ this.selector.on('AutoSelect', () => this.useAutoSelect());
+
+ this.selector.on('UpOrDown', v => this.goToUpOrDown(v));
addEventListener('mailbox.message-list.selector.go-down',
e => this.selector.newSelectPosition('ArrowDown', false, e.detail)
@@ -208,34 +227,39 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
);
addEventListener('mailbox.message.show', e => {
- const sFolder = e.detail.Folder, sUid = e.detail.Uid;
+ const sFolder = e.detail.Folder, iUid = e.detail.Uid;
- const message = MessageUserStore.list.find(
- item => item && sFolder === item.folder && sUid === item.uid
+ const message = MessagelistUserStore.find(
+ item => item && sFolder === item.folder && iUid == item.uid
);
if ('INBOX' === sFolder) {
- rl.route.setHash(mailBox(sFolder, 1));
+ hasher.setHash(mailBox(sFolder));
}
if (message) {
this.selector.selectMessageItem(message);
} else {
if ('INBOX' !== sFolder) {
- rl.route.setHash(mailBox(sFolder, 1));
+ hasher.setHash(mailBox(sFolder));
}
+ if (sFolder && iUid) {
+ MessageUserStore.message(MessageModel.fromMessageListItem(null));
+ MessageUserStore.message().folder = sFolder;
+ MessageUserStore.message().uid = iUid;
- MessageUserStore.selectMessageByFolderAndUid(sFolder, sUid);
+ populateMessageBody(MessageUserStore.message());
+ } else {
+ MessageUserStore.message(null);
+ }
}
});
- MessageUserStore.listEndHash.subscribe((() =>
+ MessagelistUserStore.endHash.subscribe((() =>
this.selector.scrollToFocused()
).throttle(50));
decorateKoCommands(this, {
- clearCommand: 1,
- reloadCommand: 1,
multyForwardCommand: canBeMovedHelper,
deleteWithoutMoveCommand: canBeMovedHelper,
deleteCommand: canBeMovedHelper,
@@ -249,34 +273,34 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
changeSort(self, event) {
FolderUserStore.sortMode(event.target.closest('li').dataset.sort);
- this.reloadCommand();
+ this.reload();
}
- clearCommand() {
- if (Settings.capa(Capa.DangerousActions)) {
+ clear() {
+ if (SettingsCapa('DangerousActions')) {
showScreenPopup(FolderClearPopupView, [FolderUserStore.currentFolder()]);
}
}
- reloadCommand() {
- if (!MessageUserStore.listLoadingAnimation() && this.allowReload) {
- rl.app.reloadMessageList(false, true);
+ reload() {
+ if (!MessagelistUserStore.isLoading()) {
+ MessagelistUserStore.reload(false, true);
}
}
multyForwardCommand() {
showMessageComposer([
ComposeType.ForwardAsAttachment,
- MessageUserStore.listCheckedOrSelected()
+ MessagelistUserStore.listCheckedOrSelected()
]);
}
deleteWithoutMoveCommand() {
- if (Settings.capa(Capa.DangerousActions)) {
+ if (SettingsCapa('DangerousActions')) {
rl.app.deleteMessagesFromFolder(
FolderType.Trash,
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
false
);
}
@@ -285,8 +309,8 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
deleteCommand() {
rl.app.deleteMessagesFromFolder(
FolderType.Trash,
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
true
);
}
@@ -294,8 +318,8 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
archiveCommand() {
rl.app.deleteMessagesFromFolder(
FolderType.Archive,
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
true
);
}
@@ -303,8 +327,8 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
spamCommand() {
rl.app.deleteMessagesFromFolder(
FolderType.Spam,
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
true
);
}
@@ -312,8 +336,8 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
notSpamCommand() {
rl.app.deleteMessagesFromFolder(
FolderType.NotSpam,
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
true
);
}
@@ -333,31 +357,17 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
}
- hideLeft(item, event) {
- event.preventDefault();
- event.stopPropagation();
-
- leftPanelDisabled(true);
- }
-
- showLeft(item, event) {
- event.preventDefault();
- event.stopPropagation();
-
- leftPanelDisabled(false);
- }
-
composeClick() {
showMessageComposer();
}
- goToUpUpOrDownDown(up) {
- if (MessageUserStore.listChecked().length) {
+ goToUpOrDown(up) {
+ if (MessagelistUserStore.listChecked().length) {
return false;
}
- clearTimeout(this.iGoToUpUpOrDownDownTimeout);
- this.iGoToUpUpOrDownDownTimeout = setTimeout(() => {
+ clearTimeout(this.iGoToUpOrDownTimeout);
+ this.iGoToUpOrDownTimeout = setTimeout(() => {
let prev, next, temp, current;
this.messageListPaginator().find(item => {
@@ -397,42 +407,38 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
useAutoSelect() {
- return !MessageUserStore.listDisableAutoSelect()
- && !/is:unseen/.test(MessageUserStore.mainMessageListSearch())
+ return !MessagelistUserStore.disableAutoSelect()
+ && !/is:unseen/.test(MessagelistUserStore.mainSearch())
&& SettingsUserStore.usePreviewPane();
}
- searchEnterAction() {
- MessageUserStore.mainMessageListSearch(this.sLastSearchValue);
- this.inputMessageListSearchFocus(false);
- }
-
cancelSearch() {
- MessageUserStore.mainMessageListSearch('');
+ MessagelistUserStore.mainSearch('');
this.inputMessageListSearchFocus(false);
}
cancelThreadUid() {
- rl.route.setHash(
+ // history.go(-1) better?
+ hasher.setHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
- MessageUserStore.listPageBeforeThread(),
- MessageUserStore.listSearch()
+ MessagelistUserStore.pageBeforeThread(),
+ MessagelistUserStore.listSearch()
)
);
}
/**
- * @param {string} sToFolderFullNameRaw
+ * @param {string} sToFolderFullName
* @param {boolean} bCopy
* @returns {boolean}
*/
- moveSelectedMessagesToFolder(sToFolderFullNameRaw, bCopy) {
- if (MessageUserStore.hasCheckedOrSelected()) {
- rl.app.moveMessagesToFolder(
- FolderUserStore.currentFolderFullNameRaw(),
- MessageUserStore.listCheckedOrSelectedUidsWithSubMails(),
- sToFolderFullNameRaw,
+ moveSelectedMessagesToFolder(sToFolderFullName, bCopy) {
+ if (MessagelistUserStore.hasCheckedOrSelected()) {
+ moveMessagesToFolder(
+ FolderUserStore.currentFolderFullName(),
+ MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
+ sToFolderFullName,
bCopy
);
}
@@ -443,151 +449,97 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
getDragData(event) {
const item = ko.dataFor(doc.elementFromPoint(event.clientX, event.clientY));
item && item.checked && item.checked(true);
- const uids = MessageUserStore.listCheckedOrSelectedUidsWithSubMails();
+ const uids = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
item && !uids.includes(item.uid) && uids.push(item.uid);
return uids.length ? {
copy: event.ctrlKey,
- folder: FolderUserStore.currentFolderFullNameRaw(),
+ folder: FolderUserStore.currentFolderFullName(),
uids: uids
} : null;
}
- /**
- * @param {string} sFolderFullNameRaw
- * @param {number} iSetAction
- * @param {Array=} aMessages = null
- * @returns {void}
- */
- setAction(sFolderFullNameRaw, iSetAction, aMessages) {
- rl.app.messageListAction(sFolderFullNameRaw, iSetAction, aMessages);
- }
-
- /**
- * @param {string} sFolderFullNameRaw
- * @param {number} iSetAction
- * @param {string} sThreadUid = ''
- * @returns {void}
- */
- setActionForAll(sFolderFullNameRaw, iSetAction, sThreadUid = '') {
- if (sFolderFullNameRaw) {
- let cnt = 0;
- const uids = [];
-
- let folder = getFolderFromCacheList(sFolderFullNameRaw);
- if (folder) {
- switch (iSetAction) {
- case MessageSetAction.SetSeen:
- folder = getFolderFromCacheList(sFolderFullNameRaw);
- if (folder) {
- MessageUserStore.list.forEach(message => {
- if (message.isUnseen()) {
- ++cnt;
- }
-
- message.isUnseen(false);
- uids.push(message.uid);
- });
-
- if (sThreadUid) {
- folder.messageCountUnread(folder.messageCountUnread() - cnt);
- if (0 > folder.messageCountUnread()) {
- folder.messageCountUnread(0);
- }
- } else {
- folder.messageCountUnread(0);
- }
-
- MessageFlagsCache.clearFolder(sFolderFullNameRaw);
- }
-
- Remote.messageSetSeenToAll(()=>{}, sFolderFullNameRaw, true, sThreadUid ? uids : null);
- break;
- case MessageSetAction.UnsetSeen:
- folder = getFolderFromCacheList(sFolderFullNameRaw);
- if (folder) {
- MessageUserStore.list.forEach(message => {
- if (!message.isUnseen()) {
- ++cnt;
- }
-
- message.isUnseen(true);
- uids.push(message.uid);
- });
-
- if (sThreadUid) {
- folder.messageCountUnread(folder.messageCountUnread() + cnt);
- if (folder.messageCountAll() < folder.messageCountUnread()) {
- folder.messageCountUnread(folder.messageCountAll());
- }
- } else {
- folder.messageCountUnread(folder.messageCountAll());
- }
-
- MessageFlagsCache.clearFolder(sFolderFullNameRaw);
- }
-
- Remote.messageSetSeenToAll(()=>{}, sFolderFullNameRaw, false, sThreadUid ? uids : null);
- break;
- // no default
- }
-
- rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
- }
- }
- }
-
listSetSeen() {
- this.setAction(
- FolderUserStore.currentFolderFullNameRaw(),
+ listAction(
+ FolderUserStore.currentFolderFullName(),
MessageSetAction.SetSeen,
- MessageUserStore.listCheckedOrSelected()
+ MessagelistUserStore.listCheckedOrSelected()
);
}
listSetAllSeen() {
- this.setActionForAll(
- FolderUserStore.currentFolderFullNameRaw(),
- MessageSetAction.SetSeen,
- this.messageListEndThreadUid()
- );
+ let sFolderFullName = FolderUserStore.currentFolderFullName(),
+ iThreadUid = MessagelistUserStore.endThreadUid();
+ if (sFolderFullName) {
+ let cnt = 0;
+ const uids = [];
+
+ let folder = getFolderFromCacheList(sFolderFullName);
+ if (folder) {
+ MessagelistUserStore.forEach(message => {
+ if (message.isUnseen()) {
+ ++cnt;
+ }
+
+ message.flags.push('\\seen');
+// message.flags.valueHasMutated();
+ iThreadUid && uids.push(message.uid);
+ });
+
+ if (iThreadUid) {
+ folder.messageCountUnread(Math.max(0, folder.messageCountUnread() - cnt));
+ } else {
+ folder.messageCountUnread(0);
+ }
+
+ MessageFlagsCache.clearFolder(sFolderFullName);
+
+ Remote.request('MessageSetSeenToAll', null, {
+ Folder: sFolderFullName,
+ SetAction: 1,
+ ThreadUids: uids.join(',')
+ });
+
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
+ }
+ }
}
listUnsetSeen() {
- this.setAction(
- FolderUserStore.currentFolderFullNameRaw(),
+ listAction(
+ FolderUserStore.currentFolderFullName(),
MessageSetAction.UnsetSeen,
- MessageUserStore.listCheckedOrSelected()
+ MessagelistUserStore.listCheckedOrSelected()
);
}
listSetFlags() {
- this.setAction(
- FolderUserStore.currentFolderFullNameRaw(),
+ listAction(
+ FolderUserStore.currentFolderFullName(),
MessageSetAction.SetFlag,
- MessageUserStore.listCheckedOrSelected()
+ MessagelistUserStore.listCheckedOrSelected()
);
}
listUnsetFlags() {
- this.setAction(
- FolderUserStore.currentFolderFullNameRaw(),
+ listAction(
+ FolderUserStore.currentFolderFullName(),
MessageSetAction.UnsetFlag,
- MessageUserStore.listCheckedOrSelected()
+ MessagelistUserStore.listCheckedOrSelected()
);
}
flagMessages(currentMessage) {
- const checked = MessageUserStore.listCheckedOrSelected();
+ const checked = MessagelistUserStore.listCheckedOrSelected();
if (currentMessage) {
const checkedUids = checked.map(message => message.uid);
if (checkedUids.includes(currentMessage.uid)) {
- this.setAction(
+ listAction(
currentMessage.folder,
currentMessage.isFlagged() ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
checked
);
} else {
- this.setAction(
+ listAction(
currentMessage.folder,
currentMessage.isFlagged() ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
[currentMessage]
@@ -597,17 +549,17 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
flagMessagesFast(bFlag) {
- const checked = MessageUserStore.listCheckedOrSelected();
+ const checked = MessagelistUserStore.listCheckedOrSelected();
if (checked.length) {
if (undefined === bFlag) {
const flagged = checked.filter(message => message.isFlagged());
- this.setAction(
+ listAction(
checked[0].folder,
checked.length === flagged.length ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
checked
);
} else {
- this.setAction(
+ listAction(
checked[0].folder,
!bFlag ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
checked
@@ -617,17 +569,17 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
seenMessagesFast(seen) {
- const checked = MessageUserStore.listCheckedOrSelected();
+ const checked = MessagelistUserStore.listCheckedOrSelected();
if (checked.length) {
if (undefined === seen) {
const unseen = checked.filter(message => message.isUnseen());
- this.setAction(
+ listAction(
checked[0].folder,
unseen.length ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
checked
);
} else {
- this.setAction(
+ listAction(
checked[0].folder,
seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
checked
@@ -637,32 +589,42 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
gotoPage(page) {
- page && rl.route.setHash(
+ page && hasher.setHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
page.value,
- MessageUserStore.listSearch(),
- MessageUserStore.listThreadUid()
+ MessagelistUserStore.listSearch(),
+ MessagelistUserStore.threadUid()
)
);
}
gotoThread(message) {
if (message && 0 < message.threadsLen()) {
- MessageUserStore.listPageBeforeThread(MessageUserStore.listPage());
+ MessagelistUserStore.pageBeforeThread(MessagelistUserStore.page());
- rl.route.setHash(
- mailBox(FolderUserStore.currentFolderFullNameHash(), 1, MessageUserStore.listSearch(), message.uid)
+ hasher.setHash(
+ mailBox(FolderUserStore.currentFolderFullNameHash(), 1, MessagelistUserStore.listSearch(), message.uid)
);
}
}
+ listEmptyMessage() {
+ if (!this.dragOver()
+ && !MessagelistUserStore().length
+ && !MessagelistUserStore.isLoading()
+ && !MessagelistUserStore.error()) {
+ return i18n('MESSAGE_LIST/EMPTY_' + (MessagelistUserStore.listSearch() ? 'SEARCH_' : '') + 'LIST');
+ }
+ return '';
+ }
+
clearListIsVisible() {
return (
!this.messageListSearchDesc() &&
- !this.messageListError() &&
- !this.messageListEndThreadUid() &&
- MessageUserStore.list.length &&
+ !MessagelistUserStore.error() &&
+ !MessagelistUserStore.endThreadUid() &&
+ MessagelistUserStore().length &&
(this.isSpamFolder() || this.isTrashFolder())
);
}
@@ -672,99 +634,121 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
this.selector.init(dom.querySelector('.b-content'), Scope.MessageList);
- dom.addEventListener('click', event => {
- ThemeStore.isMobile() && leftPanelDisabled(true);
+ addEventsListeners(dom, {
+ click: event => {
+ ThemeStore.isMobile() && !eqs(event, '.toggleLeft') && leftPanelDisabled(true);
- if (eqs(event, '.messageList .b-message-list-wrapper') && Scope.MessageView === AppUserStore.focusedState()) {
- AppUserStore.focusedState(Scope.MessageList);
+ if (eqs(event, '.messageList') && Scope.MessageView === AppUserStore.focusedState()) {
+ AppUserStore.focusedState(Scope.MessageList);
+ }
+
+ let el = eqs(event, '.e-paginator a');
+ el && this.gotoPage(ko.dataFor(el));
+
+ eqs(event, '.checkboxCheckAll') && this.checkAll(!this.checkAll());
+
+ el = eqs(event, '.flagParent');
+ el && this.flagMessages(ko.dataFor(el));
+
+ el = eqs(event, '.threads-len');
+ el && this.gotoThread(ko.dataFor(el));
+ },
+ dblclick: event => {
+ let el = eqs(event, '.actionHandle');
+ el && this.gotoThread(ko.dataFor(el));
}
-
- let el = eqs(event, '.e-paginator .e-page');
- el && this.gotoPage(ko.dataFor(el));
-
- eqs(event, '.messageList .checkboxCheckAll') && this.checkAll(!this.checkAll());
-
- el = eqs(event, '.messageList .messageListItem .flagParent');
- el && this.flagMessages(ko.dataFor(el));
-
- el = eqs(event, '.messageList .messageListItem .threads-len');
- el && this.gotoThread(ko.dataFor(el));
});
- dom.addEventListener('dblclick', event => {
- let el = eqs(event, '.messageList .messageListItem .actionHandle');
- el && this.gotoThread(ko.dataFor(el));
- });
+ // initUploaderForAppend
- this.initUploaderForAppend();
- this.initShortcuts();
+ if (Settings.app('allowAppendMessage') && this.dragOverArea()) {
+ const oJua = new Jua({
+ action: serverRequest('Append'),
+ name: 'AppendFile',
+ limit: 1,
+ hidden: {
+ Folder: () => FolderUserStore.currentFolderFullName()
+ },
+ dragAndDropElement: this.dragOverArea(),
+ dragAndDropBodyElement: this.dragOverBodyArea()
+ });
- if (!ThemeStore.isMobile() && Settings.capa(Capa.Prefetch)) {
- ifvisible.idle(this.prefetchNextTick.bind(this));
+ this.dragOver.subscribe(value => value && this.selector.scrollToTop());
+
+ oJua
+ .on('onDragEnter', () => this.dragOverEnter(true))
+ .on('onDragLeave', () => this.dragOverEnter(false))
+ .on('onBodyDragEnter', () => this.dragOver(true))
+ .on('onBodyDragLeave', () => this.dragOver(false))
+ .on('onSelect', (sUid, oData) => {
+ if (sUid && oData && 'message/rfc822' === oData.Type) {
+ MessagelistUserStore.loading(true);
+ return true;
+ }
+
+ return false;
+ })
+ .on('onComplete', () => MessagelistUserStore.reload(true, true));
}
- }
- initShortcuts() {
- shortcuts.add('enter,open', '', Scope.MessageList, () => {
+ // initShortcuts
+
+ addShortcut('enter,open', '', Scope.MessageList, () => {
+ if (formFieldFocused()) {
+ MessagelistUserStore.mainSearch(this.sLastSearchValue);
+ return false;
+ }
if (MessageUserStore.message() && this.useAutoSelect()) {
- dispatchEvent(new CustomEvent('mailbox.message-view.toggle-full-screen'));
+ MessageUserStore.toggleFullScreen();
return false;
}
-
- return true;
});
- if (Settings.capa(Capa.MessageListActions)) {
- // archive (zip)
- shortcuts.add('z', '', [Scope.MessageList, Scope.MessageView], () => {
- this.archiveCommand();
- return false;
- });
+ // archive (zip)
+ registerShortcut('z', '', [Scope.MessageList, Scope.MessageView], () => {
+ this.archiveCommand();
+ return false;
+ });
- // delete
- shortcuts.add('delete', 'shift', Scope.MessageList, () => {
- MessageUserStore.listCheckedOrSelected().length && this.deleteWithoutMoveCommand();
- return false;
- });
-// shortcuts.add('3', 'shift', Scope.MessageList, () => {
- shortcuts.add('delete', '', Scope.MessageList, () => {
- MessageUserStore.listCheckedOrSelected().length && this.deleteCommand();
- return false;
- });
- }
+ // delete
+ registerShortcut('delete', 'shift', Scope.MessageList, () => {
+ MessagelistUserStore.listCheckedOrSelected().length && this.deleteWithoutMoveCommand();
+ return false;
+ });
+// registerShortcut('3', 'shift', Scope.MessageList, () => {
+ registerShortcut('delete', '', Scope.MessageList, () => {
+ MessagelistUserStore.listCheckedOrSelected().length && this.deleteCommand();
+ return false;
+ });
- if (Settings.capa(Capa.Reload)) {
- // check mail
- shortcuts.add('r', 'meta', [Scope.FolderList, Scope.MessageList, Scope.MessageView], () => {
- this.reloadCommand();
- return false;
- });
- }
+ // check mail
+ addShortcut('r', 'meta', [Scope.FolderList, Scope.MessageList, Scope.MessageView], () => {
+ this.reload();
+ return false;
+ });
// check all
- shortcuts.add('a', 'meta', Scope.MessageList, () => {
+ registerShortcut('a', 'meta', Scope.MessageList, () => {
this.checkAll(!(this.checkAll() && !this.isIncompleteChecked()));
return false;
});
// write/compose (open compose popup)
- shortcuts.add('w,c,new', '', [Scope.MessageList, Scope.MessageView], () => {
+ registerShortcut('w,c,new', '', [Scope.MessageList, Scope.MessageView], () => {
showMessageComposer();
return false;
});
- if (Settings.capa(Capa.MessageListActions)) {
- // important - star/flag messages
- shortcuts.add('i', '', [Scope.MessageList, Scope.MessageView], () => {
- this.flagMessagesFast();
- return false;
- });
- }
+ // important - star/flag messages
+ registerShortcut('i', '', [Scope.MessageList, Scope.MessageView], () => {
+ this.flagMessagesFast();
+ return false;
+ });
- shortcuts.add('t', '', [Scope.MessageList], () => {
- let message = MessageUserStore.selectorMessageSelected();
+ registerShortcut('t', '', [Scope.MessageList], () => {
+ let message = MessagelistUserStore.selectedMessage();
if (!message) {
- message = MessageUserStore.selectorMessageFocused();
+ message = MessagelistUserStore.focusedMessage();
}
if (message && 0 < message.threadsLen()) {
@@ -774,80 +758,82 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
return false;
});
- if (Settings.capa(Capa.MessageListActions)) {
- // move
- shortcuts.add('insert', '', Scope.MessageList, () => {
- if (this.newMoveToFolder) {
- this.moveNewCommand();
- } else {
- this.moveDropdownTrigger(true);
- }
+ // move
+ registerShortcut('insert', '', Scope.MessageList, () => {
+ if (this.newMoveToFolder) {
+ this.moveNewCommand();
+ } else {
+ this.moveDropdownTrigger(true);
+ }
- return false;
- });
- }
+ return false;
+ });
- if (Settings.capa(Capa.MessageListActions)) {
- // read
- shortcuts.add('q', '', [Scope.MessageList, Scope.MessageView], () => {
- this.seenMessagesFast(true);
- return false;
- });
+ // read
+ registerShortcut('q', '', [Scope.MessageList, Scope.MessageView], () => {
+ this.seenMessagesFast(true);
+ return false;
+ });
- // unread
- shortcuts.add('u', '', [Scope.MessageList, Scope.MessageView], () => {
- this.seenMessagesFast(false);
- return false;
- });
- }
+ // unread
+ registerShortcut('u', '', [Scope.MessageList, Scope.MessageView], () => {
+ this.seenMessagesFast(false);
+ return false;
+ });
- shortcuts.add('f,mailforward', 'shift', [Scope.MessageList, Scope.MessageView], () => {
+ addShortcut('f,mailforward', 'shift', [Scope.MessageList, Scope.MessageView], () => {
this.multyForwardCommand();
return false;
});
- if (Settings.capa(Capa.Search)) {
+ if (SettingsCapa('Search')) {
// search input focus
- shortcuts.add('/', '', [Scope.MessageList, Scope.MessageView], () => {
+ addShortcut('/', '', [Scope.MessageList, Scope.MessageView], () => {
this.inputMessageListSearchFocus(true);
return false;
});
}
// cancel search
- shortcuts.add('escape', '', Scope.MessageList, () => {
+ addShortcut('escape', '', Scope.MessageList, () => {
if (this.messageListSearchDesc()) {
this.cancelSearch();
return false;
- } else if (this.messageListEndThreadUid()) {
+ } else if (MessagelistUserStore.endThreadUid()) {
this.cancelThreadUid();
return false;
}
-
- return true;
});
// change focused state
- shortcuts.add('tab', 'shift', Scope.MessageList, () => {
+ addShortcut('tab', 'shift', Scope.MessageList, () => {
AppUserStore.focusedState(Scope.FolderList);
return false;
});
- shortcuts.add('arrowleft', '', Scope.MessageList, () => {
+ addShortcut('arrowleft', '', Scope.MessageList, () => {
AppUserStore.focusedState(Scope.FolderList);
return false;
});
- shortcuts.add('tab,arrowright', '', Scope.MessageList, () => {
- MessageUserStore.message() && AppUserStore.focusedState(Scope.MessageView);
- return false;
+ addShortcut('tab,arrowright', '', Scope.MessageList, () => {
+ if (MessageUserStore.message()){
+ AppUserStore.focusedState(Scope.MessageView);
+ return false;
+ }
});
- shortcuts.add('arrowleft', 'meta', Scope.MessageView, ()=>false);
- shortcuts.add('arrowright', 'meta', Scope.MessageView, ()=>false);
+ addShortcut('arrowleft', 'meta', Scope.MessageView, ()=>false);
+ addShortcut('arrowright', 'meta', Scope.MessageView, ()=>false);
+
+ addShortcut('f', 'meta', Scope.MessageList, ()=>this.advancedSearchClick());
+
+ if (!ThemeStore.isMobile() && SettingsCapa('Prefetch')) {
+ ifvisible.idle(this.prefetchNextTick.bind(this));
+ }
}
prefetchNextTick() {
- if (!this.bPrefetch && !ifvisible.now() && this.viewModelVisible) {
- const message = MessageUserStore.list.find(
+ if (!this.bPrefetch && !ifvisible.now() && !this.viewModelDom.hidden) {
+ const message = MessagelistUserStore.find(
item => item && !hasRequestedMessage(item.folder, item.uid)
);
if (message) {
@@ -871,52 +857,15 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
}
advancedSearchClick() {
- Settings.capa(Capa.SearchAdv)
- && showScreenPopup(AdvancedSearchPopupView, [MessageUserStore.mainMessageListSearch()]);
+ SettingsCapa('SearchAdv')
+ && showScreenPopup(AdvancedSearchPopupView, [MessagelistUserStore.mainSearch()]);
}
quotaTooltip() {
return i18n('MESSAGE_LIST/QUOTA_SIZE', {
- SIZE: FileInfo.friendlySize(QuotaUserStore.usage()),
- PROC: QuotaUserStore.percentage(),
- LIMIT: FileInfo.friendlySize(QuotaUserStore.quota())
+ SIZE: FileInfo.friendlySize(FolderUserStore.quotaUsage()),
+ PROC: FolderUserStore.quotaPercentage(),
+ LIMIT: FileInfo.friendlySize(FolderUserStore.quotaLimit())
}).replace(/<[^>]+>/g, '');
}
-
- initUploaderForAppend() {
- if (!Settings.app('allowAppendMessage') || !this.dragOverArea()) {
- return false;
- }
-
- const oJua = new Jua({
- action: serverRequest('Append'),
- name: 'AppendFile',
- queueSize: 1,
- multipleSizeLimit: 1,
- hidden: {
- Folder: () => FolderUserStore.currentFolderFullNameRaw()
- },
- dragAndDropElement: this.dragOverArea(),
- dragAndDropBodyElement: this.dragOverBodyArea()
- });
-
- this.dragOver.subscribe(value => value && this.selector.scrollToTop());
-
- oJua
- .on('onDragEnter', () => this.dragOverEnter(true))
- .on('onDragLeave', () => this.dragOverEnter(false))
- .on('onBodyDragEnter', () => this.dragOver(true))
- .on('onBodyDragLeave', () => this.dragOver(false))
- .on('onSelect', (sUid, oData) => {
- if (sUid && oData && 'message/rfc822' === oData.Type) {
- MessageUserStore.listLoading(true);
- return true;
- }
-
- return false;
- })
- .on('onComplete', () => rl.app.reloadMessageList(true, true));
-
- return !!oJua;
- }
}
diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js
index c3301dced..2c1e122f0 100644
--- a/dev/View/User/MailBox/MessageView.js
+++ b/dev/View/User/MailBox/MessageView.js
@@ -1,23 +1,36 @@
import ko from 'ko';
+import { koComputable } from 'External/ko';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
-import {
- Capa,
- Scope
-} from 'Common/Enums';
+import { Scope } from 'Common/Enums';
import {
ComposeType,
- ClientSideKeyName,
+ ClientSideKeyNameLastReplyAction,
+ ClientSideKeyNameMessageHeaderFullInfo,
+ ClientSideKeyNameMessageAttachmentControls,
FolderType,
MessageSetAction
} from 'Common/EnumsUser';
-import { doc, $htmlCL, leftPanelDisabled, keyScopeReal, moveAction, Settings } from 'Common/Globals';
+import {
+ elementById,
+ $htmlCL,
+ leftPanelDisabled,
+ keyScopeReal,
+ moveAction,
+ Settings,
+ SettingsCapa,
+ getFullscreenElement,
+ exitFullscreen,
+ fireEvent,
+ addShortcut,
+ registerShortcut
+} from 'Common/Globals';
-import { isNonEmptyArray, inFocus } from 'Common/Utils';
-import { mailToHelper, showMessageComposer } from 'Common/UtilsUser';
+import { arrayLength } from 'Common/Utils';
+import { download, mailToHelper, showMessageComposer, initFullscreen } from 'Common/UtilsUser';
import { SMAudio } from 'Common/Audio';
@@ -31,122 +44,101 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
+import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import * as Local from 'Storage/Client';
import Remote from 'Remote/User/Fetch';
-import { decorateKoCommands, createCommand } from 'Knoin/Knoin';
+import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewRight } from 'Knoin/AbstractViews';
-class MessageViewMailBoxUserView extends AbstractViewRight {
+import { PgpUserStore } from 'Stores/User/Pgp';
+
+import { MimeToMessage } from 'Mime/Utils';
+
+const
+ oMessageScrollerDom = () => elementById('messageItem') || {},
+
+ currentMessage = () => MessageUserStore.message();
+
+export class MailMessageView extends AbstractViewRight {
constructor() {
- super('User/MailBox/MessageView', 'MailMessageView');
+ super();
const
+ /**
+ * @param {Function} fExecute
+ * @param {Function} fCanExecute = true
+ * @returns {Function}
+ */
+ createCommand = (fExecute, fCanExecute) => {
+ let fResult = () => {
+ fResult.canExecute() && fExecute.call(null);
+ return false;
+ };
+ fResult.canExecute = koComputable(() => fCanExecute());
+ return fResult;
+ },
+
createCommandReplyHelper = type =>
- createCommand(() => {
- this.lastReplyAction(type);
- this.replyOrforward(type);
- }, this.canBeRepliedOrForwarded),
+ createCommand(() => this.replyOrforward(type), this.canBeRepliedOrForwarded),
createCommandActionHelper = (folderType, useFolder) =>
createCommand(() => {
- const message = this.message();
- if (message && this.allowMessageListActions) {
- this.message(null);
+ const message = currentMessage();
+ if (message) {
+ MessageUserStore.message(null);
rl.app.deleteMessagesFromFolder(folderType, message.folder, [message.uid], useFolder);
}
}, this.messageVisibility);
- this.oHeaderDom = null;
- this.oMessageScrollerDom = null;
-
this.addObservables({
- showAttachmnetControls: false,
+ showAttachmentControls: false,
downloadAsZipLoading: false,
lastReplyAction_: '',
- showFullInfo: '1' === Local.get(ClientSideKeyName.MessageHeaderFullInfo),
- moreDropdownTrigger: false
+ showFullInfo: '1' === Local.get(ClientSideKeyNameMessageHeaderFullInfo),
+ moreDropdownTrigger: false,
+
+ // viewer
+ viewFromShort: '',
+ viewFromDkimData: ['none', ''],
+ viewToShort: ''
});
this.moveAction = moveAction;
- this.allowComposer = Settings.capa(Capa.Composer);
- this.allowMessageActions = Settings.capa(Capa.MessageActions);
- this.allowMessageListActions = Settings.capa(Capa.MessageListActions);
+ this.allowMessageActions = SettingsCapa('MessageActions');
const attachmentsActions = Settings.app('attachmentsActions');
- this.attachmentsActions = ko.observableArray(isNonEmptyArray(attachmentsActions) ? attachmentsActions : []);
+ this.attachmentsActions = ko.observableArray(arrayLength(attachmentsActions) ? attachmentsActions : []);
this.message = MessageUserStore.message;
- this.hasCheckedMessages = MessageUserStore.hasCheckedMessages;
- this.messageLoadingThrottle = MessageUserStore.messageLoading;
- this.messagesBodiesDom = MessageUserStore.messagesBodiesDom;
- this.useThreads = SettingsUserStore.useThreads;
- this.replySameFolder = SettingsUserStore.replySameFolder;
- this.layout = SettingsUserStore.layout;
- this.isMessageSelected = MessageUserStore.isMessageSelected;
- this.messageActiveDom = MessageUserStore.messageActiveDom;
- this.messageError = MessageUserStore.messageError;
+ this.hasCheckedMessages = MessagelistUserStore.hasCheckedMessages;
+ this.messageLoadingThrottle = MessageUserStore.loading;
+ this.messagesBodiesDom = MessageUserStore.bodiesDom;
+ this.messageError = MessageUserStore.error;
- this.fullScreenMode = MessageUserStore.messageFullScreenMode;
+ this.fullScreenMode = MessageUserStore.fullScreen;
+ this.toggleFullScreen = MessageUserStore.toggleFullScreen;
this.messageListOfThreadsLoading = ko.observable(false).extend({ rateLimit: 1 });
this.highlightUnselectedAttachments = ko.observable(false).extend({ falseTimeout: 2000 });
- this.showAttachmnetControlsState = v => Local.set(ClientSideKeyName.MessageAttachmentControls, !!v);
+ this.showAttachmentControlsState = v => Local.set(ClientSideKeyNameMessageAttachmentControls, !!v);
this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
- // commands
- this.replyCommand = createCommandReplyHelper(ComposeType.Reply);
- this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll);
- this.forwardCommand = createCommandReplyHelper(ComposeType.Forward);
- this.forwardAsAttachmentCommand = createCommandReplyHelper(ComposeType.ForwardAsAttachment);
- this.editAsNewCommand = createCommandReplyHelper(ComposeType.EditAsNew);
-
- this.deleteCommand = createCommandActionHelper(FolderType.Trash, true);
- this.deleteWithoutMoveCommand = createCommandActionHelper(FolderType.Trash, false);
- this.archiveCommand = createCommandActionHelper(FolderType.Archive, true);
- this.spamCommand = createCommandActionHelper(FolderType.Spam, true);
- this.notSpamCommand = createCommandActionHelper(FolderType.NotSpam, true);
-
// viewer
-
- this.viewFolder = '';
- this.viewUid = '';
this.viewHash = '';
- this.addObservables({
- viewBodyTopValue: 0,
- viewSubject: '',
- viewFromShort: '',
- viewFromDkimData: ['none', ''],
- viewToShort: '',
- viewFrom: '',
- viewTo: '',
- viewCc: '',
- viewBcc: '',
- viewReplyTo: '',
- viewTimeStamp: 0,
- viewSize: '',
- viewSpamScore: 0,
- viewSpamStatus: '',
- viewLineAsCss: '',
- viewViewLink: '',
- viewUnsubscribeLink: '',
- viewDownloadLink: '',
- viewIsImportant: false,
- viewIsFlagged: false
- });
this.addComputables({
- allowAttachmnetControls: () => this.attachmentsActions.length && Settings.capa(Capa.AttachmentsActions),
+ allowAttachmentControls: () => this.attachmentsActions.length && SettingsCapa('AttachmentsActions'),
- downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') && this.allowAttachmnetControls(),
+ downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') && this.allowAttachmentControls(),
lastReplyAction: {
read: this.lastReplyAction_,
@@ -157,7 +149,7 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
)
},
- messageVisibility: () => !MessageUserStore.messageLoading() && !!this.message(),
+ messageVisibility: () => !MessageUserStore.loading() && !!currentMessage(),
canBeRepliedOrForwarded: () => !this.isDraftFolder() && this.messageVisibility(),
@@ -176,35 +168,33 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
viewFromDkimStatusTitle:() => {
const status = this.viewFromDkimData();
- if (isNonEmptyArray(status)) {
- if (status[0]) {
- return status[1] || 'DKIM: ' + status[0];
- }
+ if (arrayLength(status) && status[0]) {
+ return status[1] || 'DKIM: ' + status[0];
}
return '';
},
- messageFocused: () => Scope.MessageView === AppUserStore.focusedState(),
+ pgpSupported: () => currentMessage() && PgpUserStore.isSupported(),
- messageListAndMessageViewLoading:
- () => MessageUserStore.listCompleteLoading() || MessageUserStore.messageLoading()
+ messageListOrViewLoading:
+ () => MessagelistUserStore.isLoading() | MessageUserStore.loading()
});
this.addSubscribables({
- showAttachmnetControls: v => this.message()
- && this.message().attachments.forEach(item => item && item.checked(!!v)),
+ showAttachmentControls: v => currentMessage()
+ && currentMessage().attachments.forEach(item => item && item.checked(!!v)),
- lastReplyAction_: value => Local.set(ClientSideKeyName.LastReplyAction, value),
+ lastReplyAction_: value => Local.set(ClientSideKeyNameLastReplyAction, value),
message: message => {
- this.messageActiveDom(null);
+ MessageUserStore.activeDom(null);
if (message) {
- this.showAttachmnetControls(false);
- if (Local.get(ClientSideKeyName.MessageAttachmentControls)) {
+ this.showAttachmentControls(false);
+ if (Local.get(ClientSideKeyNameMessageAttachmentControls)) {
setTimeout(() => {
- this.showAttachmnetControls(true);
+ this.showAttachmentControls(true);
}, 50);
}
@@ -212,33 +202,13 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
this.scrollMessageToTop();
}
- this.viewFolder = message.folder;
- this.viewUid = message.uid;
this.viewHash = message.hash;
- this.viewSubject(message.subject());
this.viewFromShort(message.fromToLine(true, true));
this.viewFromDkimData(message.fromDkimData());
this.viewToShort(message.toToLine(true, true));
- this.viewFrom(message.fromToLine());
- this.viewTo(message.toToLine());
- this.viewCc(message.ccToLine());
- this.viewBcc(message.bccToLine());
- this.viewReplyTo(message.replyToToLine());
- this.viewTimeStamp(message.dateTimeStampInUTC());
- this.viewSize(message.friendlySize());
- this.viewSpamScore(message.spamScore());
- this.viewSpamStatus(i18n(message.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + message.spamResult());
- this.viewLineAsCss(message.lineAsCss());
- this.viewViewLink(message.viewLink());
- this.viewUnsubscribeLink(message.getFirstUnsubsribeLink());
- this.viewDownloadLink(message.downloadLink());
- this.viewIsImportant(message.isImportant());
- this.viewIsFlagged(message.isFlagged());
} else {
- MessageUserStore.selectorMessageSelected(null);
+ MessagelistUserStore.selectedMessage(null);
- this.viewFolder = '';
- this.viewUid = '';
this.viewHash = '';
this.scrollMessageToTop();
@@ -246,59 +216,59 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
},
fullScreenMode: value => {
+ value && currentMessage() && AppUserStore.focusedState(Scope.MessageView);
if (this.oContent) {
- value ? this.oContent.requestFullscreen() : doc.exitFullscreen();
+ value ? this.oContent.requestFullscreen() : exitFullscreen();
} else {
$htmlCL.toggle('rl-message-fullscreen', value);
}
- }
+ },
+
+ showFullInfo: value => Local.set(ClientSideKeyNameMessageHeaderFullInfo, value ? '1' : '0')
});
- MessageUserStore.messageViewTrigger.subscribe(() => {
- const message = this.message();
- this.viewIsFlagged(message ? message.isFlagged() : false);
- });
+ this.lastReplyAction(Local.get(ClientSideKeyNameLastReplyAction) || ComposeType.Reply);
- this.lastReplyAction(Local.get(ClientSideKeyName.LastReplyAction) || ComposeType.Reply);
+ // commands
+ this.replyCommand = createCommandReplyHelper(ComposeType.Reply);
+ this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll);
+ this.forwardCommand = createCommandReplyHelper(ComposeType.Forward);
+ this.forwardAsAttachmentCommand = createCommandReplyHelper(ComposeType.ForwardAsAttachment);
+ this.editAsNewCommand = createCommandReplyHelper(ComposeType.EditAsNew);
- addEventListener('mailbox.message-view.toggle-full-screen', () => this.toggleFullScreen());
-
- this.attachmentPreview = this.attachmentPreview.bind(this);
+ this.deleteCommand = createCommandActionHelper(FolderType.Trash, true);
+ this.deleteWithoutMoveCommand = createCommandActionHelper(FolderType.Trash, false);
+ this.archiveCommand = createCommandActionHelper(FolderType.Archive, true);
+ this.spamCommand = createCommandActionHelper(FolderType.Spam, true);
+ this.notSpamCommand = createCommandActionHelper(FolderType.NotSpam, true);
decorateKoCommands(this, {
- closeMessageCommand: 1,
messageEditCommand: self => self.messageVisibility(),
- goUpCommand: self => !self.messageListAndMessageViewLoading(),
- goDownCommand: self => !self.messageListAndMessageViewLoading()
+ goUpCommand: self => !self.messageListOrViewLoading(),
+ goDownCommand: self => !self.messageListOrViewLoading()
});
}
- closeMessageCommand() {
+ closeMessage() {
MessageUserStore.message(null);
}
messageEditCommand() {
- this.editMessage();
+ if (currentMessage()) {
+ showMessageComposer([ComposeType.Draft, currentMessage()]);
+ }
}
goUpCommand() {
- dispatchEvent(new CustomEvent('mailbox.message-list.selector.go-up',
- {detail:SettingsUserStore.usePreviewPane() || !!this.message()} // bForceSelect
- ));
+ fireEvent('mailbox.message-list.selector.go-up',
+ SettingsUserStore.usePreviewPane() || !!currentMessage() // bForceSelect
+ );
}
goDownCommand() {
- dispatchEvent(new CustomEvent('mailbox.message-list.selector.go-down',
- {detail:SettingsUserStore.usePreviewPane() || !!this.message()} // bForceSelect
- ));
- }
-
- toggleFullScreen() {
- try {
- getSelection().removeAllRanges();
- } catch (e) {} // eslint-disable-line no-empty
-
- this.fullScreenMode(!this.fullScreenMode());
+ fireEvent('mailbox.message-list.selector.go-down',
+ SettingsUserStore.usePreviewPane() || !!currentMessage() // bForceSelect
+ );
}
/**
@@ -306,95 +276,13 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
* @returns {void}
*/
replyOrforward(sType) {
- showMessageComposer([sType, MessageUserStore.message()]);
- }
-
- checkHeaderHeight() {
- this.oHeaderDom && this.viewBodyTopValue(this.message() ? this.oHeaderDom.offsetHeight : 0);
- }
-
- // displayMailToPopup(sMailToUrl) {
- // sMailToUrl = sMailToUrl.replace(/\?.+$/, '');
- //
- // var
- // sResult = '',
- // aTo = [],
- // EmailModel = require('Model/Email').default,
- // fParseEmailLine = function(sLine) {
- // return sLine ? [decodeURIComponent(sLine)].map(sItem => {
- // var oEmailModel = new EmailModel();
- // oEmailModel.parse(sItem);
- // return oEmailModel.email ? oEmailModel : null;
- // }).filter(v => v) : null;
- // }
- // ;
- //
- // aTo = fParseEmailLine(sMailToUrl);
- // sResult = aTo && aTo[0] ? aTo[0].email : '';
- //
- // return sResult;
- // }
-
- /**
- * @param {Object} oAttachment
- * @returns {boolean}
- */
- attachmentPreview(/*attachment*/) {
-/*
- if (attachment && attachment.isImage() && !attachment.isLinked && this.message() && this.message().attachments()) {
- const items = this.message().attachments.map(item => {
- if (item && !item.isLinked && item.isImage()) {
- if (item === attachment) {
- index = listIndex;
- }
- ++listIndex;
- return {
- src: item.linkPreview(),
- msrc: item.linkThumbnail(),
- title: item.fileName
- };
- }
- return null;
- }).filter(v => v);
-
- if (items.length) {
- }
- }
-*/
- return true;
+ this.lastReplyAction(sType);
+ showMessageComposer([sType, currentMessage()]);
}
onBuild(dom) {
- this.fullScreenMode.subscribe(value => value && this.message() && AppUserStore.focusedState(Scope.MessageView));
-
- this.showFullInfo.subscribe(value => Local.set(ClientSideKeyName.MessageHeaderFullInfo, value ? '1' : '0'));
-
- let el = dom.querySelector('.messageItemHeader');
- this.oHeaderDom = el;
- if (el) {
- if (!this.resizeObserver) {
- this.resizeObserver = new ResizeObserver(this.checkHeaderHeight.debounce(50).bind(this));
- }
- this.resizeObserver.observe(el);
- } else if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- }
-
- let event = 'fullscreenchange';
- el = dom.querySelector('.b-message-view-wrapper');
- if (!el.requestFullscreen && el.webkitRequestFullscreen) {
- el.requestFullscreen = el.webkitRequestFullscreen;
- event = 'webkit'+event;
- }
- if (el.requestFullscreen) {
- if (!doc.exitFullscreen && doc.webkitExitFullscreen) {
- doc.exitFullscreen = doc.webkitExitFullscreen;
- }
- this.oContent = el;
- el.addEventListener(event, () =>
- this.fullScreenMode((doc.fullscreenElement || doc.webkitFullscreenElement) === el)
- );
- }
+ const el = dom.querySelector('.b-content');
+ this.oContent = initFullscreen(el, () => MessageUserStore.fullScreen(getFullscreenElement() === el));
const eqs = (ev, s) => ev.target.closestWithin(s, dom);
dom.addEventListener('click', event => {
@@ -435,173 +323,140 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
el = eqs(event, '.attachmentsPlace .attachmentItem .attachmentNameParent');
if (el) {
const attachment = ko.dataFor(el);
- attachment && attachment.download && rl.app.download(attachment.linkDownload());
+ attachment && attachment.linkDownload() && download(attachment.linkDownload(), attachment.fileName);
}
if (eqs(event, '.messageItemHeader .subjectParent .flagParent')) {
- const message = this.message();
- message && rl.app.messageListAction(
+ const message = currentMessage();
+ message && MessagelistUserStore.setAction(
message.folder,
message.isFlagged() ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
[message]
);
}
-
- el = eqs(event, '.thread-list .flagParent');
- if (el) {
- const message = ko.dataFor(el);
- message && message.folder && message.uid && rl.app.messageListAction(
- message.folder,
- message.isFlagged() ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
- [message]
- );
-
- this.threadsDropdownTrigger(true);
-
- return false;
- }
});
- AppUserStore.focusedState.subscribe((value) => {
+ AppUserStore.focusedState.subscribe(value => {
if (Scope.MessageView !== value) {
this.scrollMessageToTop();
this.scrollMessageToLeft();
}
});
- keyScopeReal.subscribe(value => this.messageDomFocused(Scope.MessageView === value && !inFocus()));
+ keyScopeReal.subscribe(value => this.messageDomFocused(Scope.MessageView === value));
- this.oMessageScrollerDom = dom.querySelector('.messageItem');
+ // initShortcuts
- this.initShortcuts();
- }
+ // exit fullscreen, back
+ addShortcut('escape', '', Scope.MessageView, () => {
+ if (!this.viewModelDom.hidden && currentMessage()) {
+ const preview = SettingsUserStore.usePreviewPane();
+ if (MessageUserStore.fullScreen()) {
+ MessageUserStore.fullScreen(false);
- /**
- * @returns {boolean}
- */
- escShortcuts() {
- if (this.viewModelVisible && this.message()) {
- const preview = SettingsUserStore.usePreviewPane();
- if (this.fullScreenMode()) {
- this.fullScreenMode(false);
-
- if (preview) {
+ if (preview) {
+ AppUserStore.focusedState(Scope.MessageList);
+ }
+ } else if (!preview) {
+ MessageUserStore.message(null);
+ } else {
AppUserStore.focusedState(Scope.MessageList);
}
- } else if (!preview) {
- this.message(null);
- } else {
- AppUserStore.focusedState(Scope.MessageList);
+
+ return false;
}
-
- return false;
- }
-
- return true;
- }
-
- initShortcuts() {
- // exit fullscreen, back
- shortcuts.add('escape,backspace', '', Scope.MessageView, this.escShortcuts.bind(this));
+ });
// fullscreen
- shortcuts.add('enter,open', '', Scope.MessageView, () => {
- this.toggleFullScreen();
+ addShortcut('enter,open', '', Scope.MessageView, () => {
+ MessageUserStore.toggleFullScreen();
return false;
});
// reply
- shortcuts.add('r,mailreply', '', [Scope.MessageList, Scope.MessageView], () => {
- if (MessageUserStore.message()) {
+ registerShortcut('r,mailreply', '', [Scope.MessageList, Scope.MessageView], () => {
+ if (currentMessage()) {
this.replyCommand();
return false;
}
return true;
});
- // replaAll
- shortcuts.add('a', '', [Scope.MessageList, Scope.MessageView], () => {
- if (MessageUserStore.message()) {
+ // replyAll
+ registerShortcut('a', '', [Scope.MessageList, Scope.MessageView], () => {
+ if (currentMessage()) {
this.replyAllCommand();
return false;
}
- return true;
});
- shortcuts.add('mailreply', 'shift', [Scope.MessageList, Scope.MessageView], () => {
- if (MessageUserStore.message()) {
+ registerShortcut('mailreply', 'shift', [Scope.MessageList, Scope.MessageView], () => {
+ if (currentMessage()) {
this.replyAllCommand();
return false;
}
- return true;
});
// forward
- shortcuts.add('f,mailforward', '', [Scope.MessageList, Scope.MessageView], () => {
- if (MessageUserStore.message()) {
+ registerShortcut('f,mailforward', '', [Scope.MessageList, Scope.MessageView], () => {
+ if (currentMessage()) {
this.forwardCommand();
return false;
}
-
- return true;
});
// message information
- shortcuts.add('i', 'meta', [Scope.MessageList, Scope.MessageView], () => {
- if (MessageUserStore.message()) {
+ registerShortcut('i', 'meta', [Scope.MessageList, Scope.MessageView], () => {
+ if (currentMessage()) {
this.showFullInfo(!this.showFullInfo());
}
return false;
});
// toggle message blockquotes
- shortcuts.add('b', '', [Scope.MessageList, Scope.MessageView], () => {
- const message = MessageUserStore.message();
+ registerShortcut('b', '', [Scope.MessageList, Scope.MessageView], () => {
+ const message = currentMessage();
if (message && message.body) {
message.body.querySelectorAll('.rlBlockquoteSwitcher').forEach(node => node.click());
return false;
}
- return true;
});
- shortcuts.add('arrowup,arrowleft', 'meta', [Scope.MessageList, Scope.MessageView], () => {
+ addShortcut('arrowup,arrowleft', 'meta', [Scope.MessageList, Scope.MessageView], () => {
this.goUpCommand();
return false;
});
- shortcuts.add('arrowdown,arrowright', 'meta', [Scope.MessageList, Scope.MessageView], () => {
+ addShortcut('arrowdown,arrowright', 'meta', [Scope.MessageList, Scope.MessageView], () => {
this.goDownCommand();
return false;
});
// print
- shortcuts.add('p,printscreen', 'meta', [Scope.MessageView, Scope.MessageList], () => {
- this.message() && this.message().printMessage();
+ addShortcut('p,printscreen', 'meta', [Scope.MessageView, Scope.MessageList], () => {
+ currentMessage() && currentMessage().printMessage();
return false;
});
// delete
- shortcuts.add('delete', '', Scope.MessageView, () => {
+ addShortcut('delete', '', Scope.MessageView, () => {
this.deleteCommand();
return false;
});
- shortcuts.add('delete', 'shift', Scope.MessageView, () => {
+ addShortcut('delete', 'shift', Scope.MessageView, () => {
this.deleteWithoutMoveCommand();
return false;
});
// change focused state
- shortcuts.add('arrowleft', '', Scope.MessageView, () => {
- if (!this.fullScreenMode() && this.message() && SettingsUserStore.usePreviewPane()) {
- if (this.oMessageScrollerDom && 0 < this.oMessageScrollerDom.scrollLeft) {
- return true;
- }
+ addShortcut('arrowleft', '', Scope.MessageView, () => {
+ if (!MessageUserStore.fullScreen() && currentMessage() && SettingsUserStore.usePreviewPane()
+ && !oMessageScrollerDom().scrollLeft) {
AppUserStore.focusedState(Scope.MessageList);
return false;
}
});
-// shortcuts.add('tab', 'shift', Scope.MessageView, (event, handler) => {
- shortcuts.add('tab', '', Scope.MessageView, () => {
- if (!this.fullScreenMode() && this.message() && SettingsUserStore.usePreviewPane()) {
+ addShortcut('tab', 'shift', Scope.MessageView, () => {
+ if (!MessageUserStore.fullScreen() && currentMessage() && SettingsUserStore.usePreviewPane()) {
AppUserStore.focusedState(Scope.MessageList);
}
return false;
@@ -612,42 +467,42 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
* @returns {boolean}
*/
isDraftFolder() {
- return MessageUserStore.message() && FolderUserStore.draftFolder() === MessageUserStore.message().folder;
+ return currentMessage() && FolderUserStore.draftsFolder() === currentMessage().folder;
}
/**
* @returns {boolean}
*/
isSentFolder() {
- return MessageUserStore.message() && FolderUserStore.sentFolder() === MessageUserStore.message().folder;
+ return currentMessage() && FolderUserStore.sentFolder() === currentMessage().folder;
}
/**
* @returns {boolean}
*/
isSpamFolder() {
- return MessageUserStore.message() && FolderUserStore.spamFolder() === MessageUserStore.message().folder;
+ return currentMessage() && FolderUserStore.spamFolder() === currentMessage().folder;
}
/**
* @returns {boolean}
*/
isSpamDisabled() {
- return MessageUserStore.message() && FolderUserStore.spamFolder() === UNUSED_OPTION_VALUE;
+ return currentMessage() && FolderUserStore.spamFolder() === UNUSED_OPTION_VALUE;
}
/**
* @returns {boolean}
*/
isArchiveFolder() {
- return MessageUserStore.message() && FolderUserStore.archiveFolder() === MessageUserStore.message().folder;
+ return currentMessage() && FolderUserStore.archiveFolder() === currentMessage().folder;
}
/**
* @returns {boolean}
*/
isArchiveDisabled() {
- return MessageUserStore.message() && FolderUserStore.archiveFolder() === UNUSED_OPTION_VALUE;
+ return currentMessage() && FolderUserStore.archiveFolder() === UNUSED_OPTION_VALUE;
}
/**
@@ -661,44 +516,32 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
showMessageComposer();
}
- editMessage() {
- if (MessageUserStore.message()) {
- showMessageComposer([ComposeType.Draft, MessageUserStore.message()]);
- }
- }
-
scrollMessageToTop() {
- if (this.oMessageScrollerDom) {
- if (50 < this.oMessageScrollerDom.scrollTop) {
- this.oMessageScrollerDom.scrollTop = 50;
- } else {
- this.oMessageScrollerDom.scrollTop = 0;
- }
- }
+ oMessageScrollerDom().scrollTop = (50 < oMessageScrollerDom().scrollTop) ? 50 : 0;
}
scrollMessageToLeft() {
- if (this.oMessageScrollerDom) {
- this.oMessageScrollerDom.scrollLeft = 0;
- }
+ oMessageScrollerDom().scrollLeft = 0;
}
downloadAsZip() {
- const hashes = (this.message() ? this.message().attachments : [])
- .map(item => (item && !item.isLinked && item.checked() ? item.download : ''))
+ const hashes = (currentMessage() ? currentMessage().attachments : [])
+ .map(item => (item && !item.isLinked() && item.checked() ? item.download : ''))
.filter(v => v);
if (hashes.length) {
- Remote.attachmentsActions('Zip', hashes, this.downloadAsZipLoading)
- .then((result) => {
- if (result && result.Result && result.Result.FileHash) {
- rl.app.download(attachmentDownload(result.Result.FileHash));
- } else {
- this.downloadAsZipError(true);
- }
- })
- .catch(() => {
+ Remote.post('AttachmentsActions', this.downloadAsZipLoading, {
+ Do: 'Zip',
+ Hashes: hashes
+ })
+ .then(result => {
+ let hash = result && result.Result && result.Result.FileHash;
+ if (hash) {
+ download(attachmentDownload(hash), hash+'.zip');
+ } else {
this.downloadAsZipError(true);
- });
+ }
+ })
+ .catch(() => this.downloadAsZipError(true));
} else {
this.highlightUnselectedAttachments(true);
}
@@ -708,17 +551,15 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
* @param {MessageModel} oMessage
* @returns {void}
*/
- showImages(message) {
- if (message && message.showExternalImages) {
- message.showExternalImages();
- }
+ showImages() {
+ currentMessage().showExternalImages();
}
/**
* @returns {string}
*/
printableCheckedMessageCount() {
- const cnt = MessageUserStore.listCheckedOrSelectedUidsWithSubMails().length;
+ const cnt = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails().length;
return 0 < cnt ? (100 > cnt ? cnt : '99+') : '';
}
@@ -726,24 +567,70 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
* @param {MessageModel} oMessage
* @returns {void}
*/
- readReceipt(oMessage) {
- if (oMessage && oMessage.readReceipt()) {
- Remote.sendReadReceiptMessage(
- ()=>{},
- oMessage.folder,
- oMessage.uid,
- oMessage.readReceipt(),
- i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
- i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
- );
+ readReceipt() {
+ let oMessage = currentMessage()
+ if (oMessage.readReceipt()) {
+ Remote.request('SendReadReceiptMessage', null, {
+ MessageFolder: oMessage.folder,
+ MessageUid: oMessage.uid,
+ ReadReceipt: oMessage.readReceipt(),
+ Subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
+ Text: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
+ });
- oMessage.isReadReceipt(true);
+ oMessage.flags.push('$mdnsent');
+// oMessage.flags.valueHasMutated();
MessageFlagsCache.store(oMessage);
- rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
}
}
-}
-export { MessageViewMailBoxUserView };
+ pgpDecrypt() {
+ const oMessage = currentMessage();
+ PgpUserStore.decrypt(oMessage).then(result => {
+ if (result) {
+ oMessage.pgpDecrypted(true);
+ if (result.data) {
+ MimeToMessage(result.data, oMessage);
+ oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
+ if (result.signatures && result.signatures.length) {
+ oMessage.pgpSigned(true);
+ oMessage.pgpVerified({
+ signatures: result.signatures,
+ success: !!result.signatures.length
+ });
+ }
+ }
+ }
+ });
+ }
+
+ pgpVerify(/*self, event*/) {
+ const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
+ PgpUserStore.verify(oMessage).then(result => {
+ if (result) {
+ oMessage.pgpVerified(result);
+ }
+/*
+ if (result && result.success) {
+ i18n('OPENPGP/GOOD_SIGNATURE', {
+ USER: validKey.user + ' (' + validKey.id + ')'
+ });
+ message.getText()
+ } else {
+ const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
+ additional = keyIds
+ ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
+ : '';
+
+ i18n('OPENPGP/ERROR', {
+ ERROR: 'message'
+ }) + (additional ? ' (' + additional + ')' : '');
+ }
+*/
+ });
+ }
+
+}
diff --git a/dev/View/User/MailBox/SystemDropDown.js b/dev/View/User/MailBox/SystemDropDown.js
deleted file mode 100644
index d27213356..000000000
--- a/dev/View/User/MailBox/SystemDropDown.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { AbstractSystemDropDownUserView } from 'View/User/AbstractSystemDropDown';
-
-class SystemDropDownMailBoxUserView extends AbstractSystemDropDownUserView
-{
- constructor() {
- super('User/MailBox/SystemDropDown');
- }
-}
-
-export { SystemDropDownMailBoxUserView };
diff --git a/dev/View/User/Settings/Menu.js b/dev/View/User/Settings/Menu.js
index c7a8fc09e..8726f3377 100644
--- a/dev/View/User/Settings/Menu.js
+++ b/dev/View/User/Settings/Menu.js
@@ -1,41 +1,23 @@
-import { Scope } from 'Common/Enums';
-import { leftPanelDisabled } from 'Common/Globals';
import { settings, mailbox } from 'Common/Links';
import { getFolderInboxName } from 'Common/Cache';
-import { settingsMenuKeysHandler } from 'Knoin/Knoin';
import { AbstractViewLeft } from 'Knoin/AbstractViews';
-import { ThemeStore } from 'Stores/Theme';
-
-export class MenuSettingsUserView extends AbstractViewLeft {
+export class SettingsMenuUserView extends AbstractViewLeft {
/**
* @param {Object} screen
*/
constructor(screen) {
- super('User/Settings/Menu', 'SettingsMenu');
-
- this.leftPanelDisabled = leftPanelDisabled;
+ super();
this.menu = screen.menu;
}
- onBuild(dom) {
- dom.addEventListener('click', event =>
- ThemeStore.isMobile()
- && event.target.closestWithin('.b-settings-menu .e-item.selectable', dom)
- && leftPanelDisabled(true)
- );
-
- shortcuts.add('arrowup,arrowdown', '', Scope.Settings,
- settingsMenuKeysHandler(dom.querySelectorAll('.b-settings-menu .e-item')));
- }
-
link(route) {
return settings(route);
}
backToMailBoxClick() {
- rl.route.setHash(mailbox(getFolderInboxName()));
+ hasher.setHash(mailbox(getFolderInboxName()));
}
}
diff --git a/dev/View/User/Settings/Pane.js b/dev/View/User/Settings/Pane.js
index 3c815b195..13deb056f 100644
--- a/dev/View/User/Settings/Pane.js
+++ b/dev/View/User/Settings/Pane.js
@@ -7,10 +7,11 @@ import { ThemeStore } from 'Stores/Theme';
import { AbstractViewRight } from 'Knoin/AbstractViews';
-export class PaneSettingsUserView extends AbstractViewRight {
+export class SettingsPaneUserView extends AbstractViewRight {
constructor() {
- super('User/Settings/Pane', 'SettingsPane');
+ super();
+ this.isMobile = ThemeStore.isMobile;
this.leftPanelDisabled = leftPanelDisabled;
}
@@ -18,25 +19,13 @@ export class PaneSettingsUserView extends AbstractViewRight {
MessageUserStore.message(null);
}
- hideLeft(item, event) {
- event.preventDefault();
- event.stopPropagation();
-
- leftPanelDisabled(true);
- }
-
- showLeft(item, event) {
- event.preventDefault();
- event.stopPropagation();
-
- leftPanelDisabled(false);
- }
-
onBuild(dom) {
- dom.addEventListener('click', () => ThemeStore.isMobile() && leftPanelDisabled(true));
+ dom.addEventListener('click', () =>
+ ThemeStore.isMobile() && !event.target.closestWithin('.toggleLeft', dom) && leftPanelDisabled(true)
+ );
}
backToMailBoxClick() {
- rl.route.setHash(mailbox(getFolderInboxName()));
+ hasher.setHash(mailbox(getFolderInboxName()));
}
}
diff --git a/dev/View/User/Settings/SystemDropDown.js b/dev/View/User/Settings/SystemDropDown.js
deleted file mode 100644
index 4b33219a0..000000000
--- a/dev/View/User/Settings/SystemDropDown.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { AbstractSystemDropDownUserView } from 'View/User/AbstractSystemDropDown';
-
-class SystemDropDownSettingsUserView extends AbstractSystemDropDownUserView
-{
- constructor() {
- super('User/Settings/SystemDropDown');
- }
-}
-
-export { SystemDropDownSettingsUserView };
diff --git a/dev/View/User/SystemDropDown.js b/dev/View/User/SystemDropDown.js
new file mode 100644
index 000000000..51830e983
--- /dev/null
+++ b/dev/View/User/SystemDropDown.js
@@ -0,0 +1,143 @@
+import { AppUserStore } from 'Stores/User/App';
+import { AccountUserStore } from 'Stores/User/Account';
+import { MessageUserStore } from 'Stores/User/Message';
+//import { FolderUserStore } from 'Stores/User/Folder';
+
+import { Scope } from 'Common/Enums';
+import { settings } from 'Common/Links';
+
+import { showScreenPopup } from 'Knoin/Knoin';
+import { AbstractViewRight } from 'Knoin/AbstractViews';
+
+import { KeyboardShortcutsHelpPopupView } from 'View/Popup/KeyboardShortcutsHelp';
+import { AccountPopupView } from 'View/Popup/Account';
+import { ContactsPopupView } from 'View/Popup/Contacts';
+
+import { doc, leftPanelDisabled, fireEvent, SettingsCapa, registerShortcut } from 'Common/Globals';
+
+import { ThemeStore } from 'Stores/Theme';
+
+import Remote from 'Remote/User/Fetch';
+import { getNotification } from 'Common/Translator';
+//import { clearCache } from 'Common/Cache';
+//import { koComputable } from 'External/ko';
+
+export class SystemDropDownUserView extends AbstractViewRight {
+ constructor() {
+ super();
+
+ this.allowAccounts = SettingsCapa('AdditionalAccounts');
+
+ this.accountEmail = AccountUserStore.email;
+
+ this.accounts = AccountUserStore.accounts;
+ this.accountsLoading = AccountUserStore.loading;
+/*
+ this.accountsUnreadCount = : koComputable(() => 0);
+ this.accountsUnreadCount = : koComputable(() => AccountUserStore.accounts().reduce((result, item) => result + item.count(), 0));
+*/
+
+ this.addObservables({
+ currentAudio: '',
+ accountMenuDropdownTrigger: false
+ });
+
+ this.allowContacts = AppUserStore.allowContacts();
+
+ addEventListener('audio.stop', () => this.currentAudio(''));
+ addEventListener('audio.start', e => this.currentAudio(e.detail));
+ }
+
+ stopPlay() {
+ fireEvent('audio.api.stop');
+ }
+
+ accountClick(account, event) {
+ let email = account && account.email;
+ if (email && 0 === event.button && AccountUserStore.email() != email) {
+ AccountUserStore.loading(true);
+ event.preventDefault();
+ event.stopPropagation();
+ Remote.request('AccountSwitch',
+ (iError/*, oData*/) => {
+ if (iError) {
+ AccountUserStore.loading(false);
+ alert(getNotification(iError).replace('%EMAIL%', email));
+ if (account.isAdditional()) {
+ showScreenPopup(AccountPopupView, [account]);
+ }
+ } else {
+/* // Not working yet
+ forEachObjectEntry(oData.Result, (key, value) => rl.settings.set(key, value));
+ clearCache();
+// MessageUserStore.setMessage();
+// MessageUserStore.purgeMessageBodyCache();
+// MessageUserStore.hideMessageBodies();
+ MessagelistUserStore([]);
+// FolderUserStore.folderList([]);
+ loadFolders(value => {
+ if (value) {
+// 4. Change to INBOX = reload MessageList
+// MessagelistUserStore.setMessageList();
+ }
+ });
+ AccountUserStore.loading(false);
+*/
+ rl.route.reload();
+ }
+ }, {Email:email}
+ );
+ }
+ return true;
+ }
+
+ emailTitle() {
+ return AccountUserStore.email();
+ }
+
+ settingsClick() {
+ hasher.setHash(settings());
+ }
+
+ settingsHelp() {
+ showScreenPopup(KeyboardShortcutsHelpPopupView);
+ }
+
+ addAccountClick() {
+ this.allowAccounts && showScreenPopup(AccountPopupView);
+ }
+
+ contactsClick() {
+ this.allowContacts && showScreenPopup(ContactsPopupView);
+ }
+
+ toggleLayout()
+ {
+ const mobile = !ThemeStore.isMobile();
+ doc.cookie = 'rllayout=' + (mobile ? 'mobile' : 'desktop') + '; samesite=strict';
+ ThemeStore.isMobile(mobile);
+ leftPanelDisabled(mobile);
+ }
+
+ logoutClick() {
+ rl.app.logout();
+ }
+
+ onBuild() {
+ registerShortcut('m', '', [Scope.MessageList, Scope.MessageView, Scope.Settings], () => {
+ if (!this.viewModelDom.hidden) {
+ MessageUserStore.fullScreen(false);
+ this.accountMenuDropdownTrigger(true);
+ return false;
+ }
+ });
+
+ // shortcuts help
+ registerShortcut('?,f1,help', '', [Scope.MessageList, Scope.MessageView, Scope.Settings], () => {
+ if (!this.viewModelDom.hidden) {
+ showScreenPopup(KeyboardShortcutsHelpPopupView);
+ return false;
+ }
+ });
+ }
+}
diff --git a/dev/boot.js b/dev/boot.js
index 3f988daf2..3d67e868a 100644
--- a/dev/boot.js
+++ b/dev/boot.js
@@ -1,55 +1,13 @@
+(doc => {
-(win => {
+[].flat || doc.location.replace('./?/BadBrowser');
+navigator.cookieEnabled || doc.location.replace('./?/NoCookie');
const
- doc = document,
- eId = id => doc.getElementById(id),
- app = eId('rl-app'),
+ eId = id => doc.getElementById('rl-'+id),
+ app = eId('app'),
admin = app && '1' == app.dataset.admin,
-
- getCookie = name => {
- let data = doc.cookie.match('(^|;) ?'+name+'=([^;]*)(;|$)');
- return data ? decodeURIComponent(data[2]) : null;
- },
-
- Storage = type => {
- let name = type+'Storage';
- try {
- win[name].setItem(name, '');
- win[name].getItem(name);
- win[name].removeItem(name);
- } catch (e) {
- console.error(e);
- const cookieName = encodeURIComponent(name+('session' === type ? win.name || (win.name = Date.now()) : ''));
-
- // initialise if there's already data
- let data = getCookie(cookieName);
- data = data ? JSON.parse(data) : {};
-
- win[name] = {
- getItem: key => data[key] === undefined ? null : data[key],
- setItem: function (key, value) {
- data[key] = ''+value; // forces the value to a string
- doc.cookie = cookieName+'='+encodeURIComponent(JSON.stringify(data))
- +"; expires="+('local' === type ? (new Date(Date.now()+(365*24*60*60*1000))).toGMTString() : '')
- +"; path=/; samesite=strict";
- }
- };
- }
- return win[name];
- },
- STORAGE_KEY = '__rlA',
- TIME_KEY = '__rlT',
- AUTH_KEY = 'AuthAccountHash',
- storage = Storage('session'),
- timestamp = () => Math.round(Date.now() / 1000),
- setTimestamp = () => storage.setItem(TIME_KEY, timestamp()),
-
- showError = () => {
- eId('rl-loading').hidden = true;
- eId('rl-loading-error').hidden = false;
- p.end();
- },
+ layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
loadScript = src => {
if (!src) {
@@ -57,123 +15,58 @@ const
}
return new Promise((resolve, reject) => {
const script = doc.createElement('script');
- script.onload = () => {
- p.set(pStep += step);
- resolve();
- };
- script.onerror = () => reject(new Error(src));
+ script.onload = () => resolve();
+ script.onerror = () => reject(new Error('Failed loading ' + src));
script.src = src;
// script.async = true;
doc.head.append(script);
});
- },
-
- step = 100 / 7,
- p = win.progressJs = {
- set: percent => progress.style.width = Math.min(percent, 100) + '%',
- end: () => {
- if (container) {
- p.set(100);
- setTimeout(() => {
- container.remove();
- container = progress = null;
- }, 600);
- }
- }
};
-if (!navigator || !navigator.cookieEnabled) {
- doc.location.href = './?/NoCookie';
-}
+let RL_APP_DATA = {};
-const layout = getCookie('rllayout');
-doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout || (!layout && 1000 > innerWidth));
+doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout[2] || (!layout && 1000 > innerWidth));
-let pStep = 0,
- container = eId('progressjs'),
- progress = container.querySelector('.progressjs-inner'),
-
- RL_APP_DATA = {};
-
-win.rl = {
- hash: {
- // getHash
- get: () => storage.getItem(STORAGE_KEY) || null,
- // setHash
- set: () => {
- storage.setItem(STORAGE_KEY, RL_APP_DATA && RL_APP_DATA[AUTH_KEY]
- ? RL_APP_DATA[AUTH_KEY] : '');
- setTimestamp();
- },
- // clearHash
- clear: () => {
- storage.setItem(STORAGE_KEY, '');
- setTimestamp();
- },
- // checkTimestamp
- check: () => {
- if (timestamp() > (parseInt(storage.getItem(TIME_KEY) || 0, 10) || 0) + 3600000) {
- // 60m
- rl.hash.clear();
- return true;
- }
- return false;
- }
- },
- data: () => RL_APP_DATA,
+window.rl = {
adminArea: () => admin,
settings: {
- get: name => null == RL_APP_DATA[name] ? null : RL_APP_DATA[name],
+ get: name => RL_APP_DATA[name],
set: (name, value) => RL_APP_DATA[name] = value,
- app: name => {
- const APP_SETTINGS = RL_APP_DATA.System || {};
- return null == APP_SETTINGS[name] ? null : APP_SETTINGS[name];
- },
- capa: name => null != name && Array.isArray(RL_APP_DATA.Capa) && RL_APP_DATA.Capa.includes(name)
- },
- setWindowTitle: title => {
- title = null == title ? '' : '' + title;
- if (RL_APP_DATA.Title) {
- title += (title ? ' - ' : '') + RL_APP_DATA.Title;
- }
- doc.title = title;
+ app: name => RL_APP_DATA.System[name],
+ capa: name => name && !!(RL_APP_DATA.Capa || {})[name]
},
+ setWindowTitle: title =>
+ doc.title = RL_APP_DATA.Title ? (title ? title + ' - ' : '') + RL_APP_DATA.Title : (title ? '' + title : ''),
initData: appData => {
RL_APP_DATA = appData;
-
- rl.hash.set();
-
- if (appData) {
- if (appData.NewThemeLink) {
- eId('app-theme-link').href = appData.NewThemeLink;
- }
-
- loadScript(appData.StaticLibJsLink)
- .then(() => Promise.all([loadScript(appData.TemplatesLink), loadScript(appData.LangLink)]))
- .then(() => loadScript(appData.StaticAppJsLink))
+ const url = appData.StaticLibsJs,
+ cb = () => rl.app.bootstart(),
+ div = eId('loading-error'),
+ showError = msg => {
+ div.append(' ' + msg);
+ eId('loading').hidden = true;
+ div.hidden = false;
+ };
+ loadScript(url)
+ .then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
.then(() => appData.PluginsLink ? loadScript(appData.PluginsLink) : Promise.resolve())
- .then(() => win.__APP_BOOT ? win.__APP_BOOT(showError) : showError())
+ .then(() => ('loading' !== doc.readyState) ? cb() : doc.addEventListener('DOMContentLoaded', cb))
.catch(e => {
- showError();
+ showError(e.message);
throw e;
});
- } else {
- showError();
- }
- }
+ },
+
+ setData: appData => {
+ RL_APP_DATA = appData;
+ rl.app.refresh();
+ },
+
+ loadScript: loadScript
};
-p.set(1);
+loadScript(`./?/${admin ? 'Admin' : ''}AppData/0/${Math.random().toString().slice(2)}/`)
+ .then(() => 0);
-Storage('local');
-
-// init section
-setInterval(setTimestamp, 60000); // 1m
-
-[eId('app-css'),eId('app-theme-link')].forEach(css => css.href = css.dataset.href);
-
-loadScript(`./?/${admin ? 'Admin' : ''}AppData/${rl.hash.get() || '0'}/${Math.random().toString().substr(2)}/`)
- .then(() => {});
-
-})(this);
+})(document);
diff --git a/dev/bootstrap.js b/dev/bootstrap.js
index 7c56eb092..7715858f9 100644
--- a/dev/bootstrap.js
+++ b/dev/bootstrap.js
@@ -1,32 +1,9 @@
-import { doc, elementById, dropdownVisibility, Settings } from 'Common/Globals';
+import { Settings } from 'Common/Globals';
import { i18n } from 'Common/Translator';
import { root } from 'Common/Links';
-export default (App) => {
-
- addEventListener('keydown', event => {
- event = event || window.event;
- if (event && event.ctrlKey && !event.shiftKey && !event.altKey) {
- if ('S' == event.key) {
- event.preventDefault();
- } else if ('A' == event.key) {
- const sender = event.target;
- if (
- sender &&
- ('true' === '' + sender.contentEditable || (sender.matches && sender.matches('INPUT,TEXTAREA')))
- ) {
- return;
- }
-
- getSelection().removeAllRanges();
-
- event.preventDefault();
- }
- }
- });
-
- addEventListener('click', ()=>rl.Dropdowns.detectVisibility());
+export default App => {
rl.app = App;
rl.logoutReload = App.logoutReload;
@@ -41,47 +18,20 @@ export default (App) => {
}
};
- rl.Dropdowns = [];
- rl.Dropdowns.register = function(element) { this.push(element); };
- rl.Dropdowns.detectVisibility = (() =>
- dropdownVisibility(!!rl.Dropdowns.find(item => item.classList.contains('show')))
- ).debounce(50);
-
rl.route = {
root: () => {
- rl.route.setHash(root(), true);
rl.route.off();
+ hasher.setHash(root());
},
reload: () => {
rl.route.root();
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.reload(), 100);
},
- off: () => hasher.changed.active = false,
- on: () => hasher.changed.active = true,
- /**
- * @param {string} sHash
- * @param {boolean=} silence = false
- * @param {boolean=} replace = false
- * @returns {void}
- */
- setHash: (hash, silence = false, replace = false) => {
- hash = hash.replace(/^[#/]+/, '');
-
- const cmd = replace ? 'replaceHash' : 'setHash';
-
- if (silence) {
- hasher.changed.active = false;
- hasher[cmd](hash);
- hasher.changed.active = true;
- } else {
- hasher.changed.active = true;
- hasher[cmd](hash);
- hasher.setHash(hash);
- }
- }
+ off: () => hasher.active = false,
+ on: () => hasher.active = true
};
- rl.fetchJSON = (resource, init, postData) => {
+ rl.fetch = (resource, init, postData) => {
init = Object.assign({
mode: 'same-origin',
cache: 'no-cache',
@@ -90,14 +40,10 @@ export default (App) => {
credentials: 'same-origin',
headers: {}
}, init);
- init.headers.Accept = 'application/json';
if (postData) {
init.method = 'POST';
init.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
- postData.XToken = Settings.app('token');
-// init.body = JSON.stringify(postData);
- const formData = new FormData(),
- buildFormData = (formData, data, parentKey) => {
+ const buildFormData = (formData, data, parentKey) => {
if (data && typeof data === 'object' && !(data instanceof Date || data instanceof File)) {
Object.keys(data).forEach(key =>
buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key)
@@ -105,12 +51,23 @@ export default (App) => {
} else {
formData.set(parentKey, data == null ? '' : data);
}
+ return formData;
};
- buildFormData(formData, postData);
- init.body = new URLSearchParams(formData);
+ postData = (postData instanceof FormData)
+ ? postData
+ : buildFormData(new FormData(), postData);
+ postData.set('XToken', Settings.app('token'));
+// init.body = JSON.stringify(Object.fromEntries(postData));
+ init.body = new URLSearchParams(postData);
}
- return fetch(resource, init).then(response => {
+ return fetch(resource, init);
+ };
+
+ rl.fetchJSON = (resource, init, postData) => {
+ init = Object.assign({ headers: {} }, init);
+ init.headers.Accept = 'application/json';
+ return rl.fetch(resource, init, postData).then(response => {
if (!response.ok) {
return Promise.reject('Network response error: ' + response.status);
}
@@ -135,17 +92,4 @@ export default (App) => {
});
};
- window.__APP_BOOT = fErrorCallback => {
- const cb = () => setTimeout(() => {
- if (rl.TEMPLATES) {
- elementById('rl-templates').innerHTML = rl.TEMPLATES;
- setTimeout(() => App.bootstart(), 10);
- } else {
- fErrorCallback();
- }
-
- window.__APP_BOOT = null;
- }, 10);
- ('loading' !== doc.readyState) ? cb() : doc.addEventListener('DOMContentLoaded', cb);
- };
};
diff --git a/dev/dragdropgecko.js b/dev/dragdropgecko.js
index 1dff7eee1..e48ea3f8f 100644
--- a/dev/dragdropgecko.js
+++ b/dev/dragdropgecko.js
@@ -6,168 +6,162 @@
*/
(doc => {
- let ua = navigator.userAgent.toLowerCase(),
-
- dropEffect = 'move',
- effectAllowed = 'all',
- data = {},
-
- dataTransfer,
- dragSource,
- isDragging,
- allowDrop,
- lastTarget,
- lastTouch,
- holdInterval,
-
- img;
-
-/*
- class DataTransferItem
- {
- get kind() { return 'string'; }
- }
-*/
- /** https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer */
- class DataTransfer
- {
- get dropEffect() { return dropEffect; }
- set dropEffect(value) { dropEffect = value; }
-
- get effectAllowed() { return effectAllowed; }
- set effectAllowed(value) { effectAllowed = value; }
-
- get files() { return []; }
- get items() { return []; } // DataTransferItemList
- get types() { return Object.keys(data); }
-
- clearData(type) {
- if (type != null) {
- delete data[type];
- } else {
- data = {};
- }
- }
-
- getData(type) {
- return data[type] || '';
- }
-
- setData(type, value) {
- data[type] = value;
- }
-
- constructor() {
- this.setDragImage = setDragImage;
- }
- }
-
- const
- htmlDrag = b => doc.documentElement.classList.toggle('firefox-drag', b),
-
- setDragImage = (src, xOffset, yOffset) => {
- img && img.remove();
- if (src) {
- // create drag image from custom element or drag source
- img = src.cloneNode(true);
- copyStyle(src, img);
- img._x = xOffset == null ? src.clientWidth / 2 : xOffset;
- img._y = yOffset == null ? src.clientHeight / 2 : yOffset;
- }
- },
-
- // clear all members
- reset = () => {
- if (dragSource) {
- clearInterval(holdInterval);
- // dispose of drag image element
- img && img.remove();
- isDragging && dispatchEvent(lastTouch, 'dragend', dragSource);
- img = dragSource = lastTouch = lastTarget = dataTransfer = holdInterval = null;
- isDragging = allowDrop = false;
- htmlDrag(false);
- }
- },
-
- // get point for a touch event
- getPoint = e => {
- e = e.touches ? e.touches[0] : e;
- return { x: e.clientX, y: e.clientY };
- },
-
- touchend = e => {
- if (dragSource) {
- // finish dragging
- allowDrop && 'touchcancel' !== e.type && dispatchEvent(lastTouch, 'drop', lastTarget);
- reset();
- }
- },
-
- // get the element at a given touch event
- getTarget = pt => {
- let el = doc.elementFromPoint(pt.x, pt.y);
- while (el && getComputedStyle(el).pointerEvents == 'none') {
- el = el.parentElement;
- }
- return el;
- },
-
- // move the drag image element
- moveImage = pt => {
- requestAnimationFrame(() => {
- if (img) {
- img.style.left = Math.round(pt.x - img._x) + 'px';
- img.style.top = Math.round(pt.y - img._y) + 'px';
- }
- });
- },
-
- copyStyle = (src, dst) => {
- // remove potentially troublesome attributes
- ['id','class','style','draggable'].forEach(att => dst.removeAttribute(att));
- // copy canvas content
- if (src instanceof HTMLCanvasElement) {
- let cSrc = src, cDst = dst;
- cDst.width = cSrc.width;
- cDst.height = cSrc.height;
- cDst.getContext('2d').drawImage(cSrc, 0, 0);
- }
- // copy style (without transitions)
- let cs = getComputedStyle(src), i;
- for (i = 0; i < cs.length; i++) {
- let key = cs[i];
- if (key.indexOf('transition') < 0) {
- dst.style[key] = cs[key];
- }
- }
- dst.style.pointerEvents = 'none';
- // and repeat for all children
- for (i = 0; i < src.children.length; i++) {
- copyStyle(src.children[i], dst.children[i]);
- }
- },
-
- // return false when cancelled
- dispatchEvent = (e, type, target) => {
- if (e && target) {
- let evt = new Event(type, {bubbles:true,cancelable:true});
- evt.button = 0;
- evt.buttons = 1;
- // copy event properties into new event
- ['altKey','ctrlKey','metaKey','shiftKey'].forEach(k => evt[k] = e[k]);
- let src = e.touches ? e.touches[0] : e;
- ['pageX','pageY','clientX','clientY','screenX','screenY','offsetX','offsetY'].forEach(k => evt[k] = src[k]);
- if (dragSource) {
- evt.dataTransfer = dataTransfer;
- }
- return target.dispatchEvent(evt);
- }
- return false;
- };
-
+ let ua = navigator.userAgent.toLowerCase();
// Chrome on mobile supports drag & drop
if (ua.includes('mobile') && ua.includes('gecko/')) {
- let opt = { passive: false, capture: false };
+
+ let opt = { passive: false, capture: false },
+
+ dropEffect = 'move',
+ effectAllowed = 'all',
+ data = {},
+
+ dataTransfer,
+ dragSource,
+ isDragging,
+ allowDrop,
+ lastTarget,
+ lastTouch,
+ holdInterval,
+
+ img;
+
+/*
+ class DataTransferItem
+ {
+ get kind() { return 'string'; }
+ }
+*/
+ /** https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer */
+ class DataTransfer
+ {
+ get dropEffect() { return dropEffect; }
+ set dropEffect(value) { dropEffect = value; }
+
+ get effectAllowed() { return effectAllowed; }
+ set effectAllowed(value) { effectAllowed = value; }
+
+ get files() { return []; }
+ get items() { return []; } // DataTransferItemList
+ get types() { return Object.keys(data); }
+
+ clearData(type) {
+ if (type != null) {
+ delete data[type];
+ } else {
+ data = {};
+ }
+ }
+
+ getData(type) {
+ return data[type] || '';
+ }
+
+ setData(type, value) {
+ data[type] = value;
+ }
+
+ constructor() {
+ this.setDragImage = setDragImage;
+ }
+ }
+
+ const
+ htmlDrag = b => doc.documentElement.classList.toggle('firefox-drag', b),
+
+ setDragImage = (src, xOffset, yOffset) => {
+ img && img.remove();
+ if (src) {
+ // create drag image from custom element or drag source
+ img = src.cloneNode(true);
+ copyStyle(src, img);
+ img._x = xOffset == null ? src.clientWidth / 2 : xOffset;
+ img._y = yOffset == null ? src.clientHeight / 2 : yOffset;
+ }
+ },
+
+ // clear all members
+ reset = () => {
+ if (dragSource) {
+ clearInterval(holdInterval);
+ // dispose of drag image element
+ img && img.remove();
+ isDragging && dispatchEvent(lastTouch, 'dragend', dragSource);
+ img = dragSource = lastTouch = lastTarget = dataTransfer = holdInterval = null;
+ isDragging = allowDrop = false;
+ htmlDrag(false);
+ }
+ },
+
+ // get point for a touch event
+ getPoint = e => {
+ e = e.touches ? e.touches[0] : e;
+ return { x: e.clientX, y: e.clientY };
+ },
+
+ touchend = e => {
+ if (dragSource) {
+ // finish dragging
+ allowDrop && 'touchcancel' !== e.type && dispatchEvent(lastTouch, 'drop', lastTarget);
+ reset();
+ }
+ },
+
+ // get the element at a given touch event
+ getTarget = pt => {
+ let el = doc.elementFromPoint(pt.x, pt.y);
+ while (el && getComputedStyle(el).pointerEvents == 'none') {
+ el = el.parentElement;
+ }
+ return el;
+ },
+
+ // move the drag image element
+ moveImage = pt => {
+ requestAnimationFrame(() => {
+ if (img) {
+ img.style.left = Math.round(pt.x - img._x) + 'px';
+ img.style.top = Math.round(pt.y - img._y) + 'px';
+ }
+ });
+ },
+
+ copyStyle = (src, dst) => {
+ // remove potentially troublesome attributes
+ ['id','class','style','draggable'].forEach(att => dst.removeAttribute(att));
+ // copy canvas content
+ if (src instanceof HTMLCanvasElement) {
+ let cSrc = src, cDst = dst;
+ cDst.width = cSrc.width;
+ cDst.height = cSrc.height;
+ cDst.getContext('2d').drawImage(cSrc, 0, 0);
+ }
+ // copy style (without transitions)
+ let cs = getComputedStyle(src);
+ Object.entries(cs).forEach(([key, value]) => key.includes('transition') || (dst.style[key] = value));
+ dst.style.pointerEvents = 'none';
+ // and repeat for all children
+ let i = src.children.length;
+ while (i--) copyStyle(src.children[i], dst.children[i]);
+ },
+
+ // return false when cancelled
+ dispatchEvent = (e, type, target) => {
+ if (e && target) {
+ let evt = new Event(type, {bubbles:true,cancelable:true});
+ evt.button = 0;
+ evt.buttons = 1;
+ // copy event properties into new event
+ ['altKey','ctrlKey','metaKey','shiftKey'].forEach(k => evt[k] = e[k]);
+ let src = e.touches ? e.touches[0] : e;
+ ['pageX','pageY','clientX','clientY','screenX','screenY','offsetX','offsetY'].forEach(k => evt[k] = src[k]);
+ if (dragSource) {
+ evt.dataTransfer = dataTransfer;
+ }
+ return target.dispatchEvent(evt);
+ }
+ return false;
+ };
doc.addEventListener('touchstart', e => {
// clear all variables
diff --git a/dev/polyfill.js b/dev/polyfill.js
deleted file mode 100644
index c266cac9c..000000000
--- a/dev/polyfill.js
+++ /dev/null
@@ -1,35 +0,0 @@
-'use strict';
-
-(w=>{
-
-Array.prototype.flat || Object.defineProperty(Array.prototype, 'flat', {
- configurable: true,
- value: function flat(depth) {
- depth = isNaN(depth) ? 1 : Number(depth);
- return depth ? Array.prototype.reduce.call(this, (acc, cur) => {
- if (Array.isArray(cur)) {
- acc.push.apply(acc, flat.call(cur, depth - 1));
- } else {
- acc.push(cur);
- }
- return acc;
- }, []) : this.slice();
- },
- writable: true
-});
-
-w.ResizeObserver || (w.ResizeObserver = class {
- constructor(callback) {
- this.observer = new MutationObserver(callback.debounce(250));
- }
-
- disconnect() {
- this.observer.disconnect();
- }
-
- observe(target) {
- this.observer.observe(target, { attributes: true, subtree: true, attributeFilter: ['style'] });
- }
-});
-
-})(this);
diff --git a/dev/shortcuts.js b/dev/shortcuts.js
index cc42f0f21..794cfd2bf 100644
--- a/dev/shortcuts.js
+++ b/dev/shortcuts.js
@@ -1,86 +1,76 @@
(win => {
+let
+ scope = {},
+ _scope = 'all';
+
const
doc = document,
+ // On Mac we use ⌘ else the Ctrl key
meta = /Mac OS X/.test(navigator.userAgent) ? 'meta' : 'ctrl',
_scopes = {
all: {}
},
toArray = v => Array.isArray(v) ? v : v.split(/\s*,\s*/),
- keydown = event => {
- let key = (event.key || event.code || '').toLowerCase().replace(' ','space'),
- scopes = [];
- scope[key] && scopes.push(scope[key]);
- _scope !== 'all' && _scopes.all[key] && scopes.push(_scopes.all[key]);
- if (scopes.length && win.shortcuts.filter(event.target)) {
- let modifiers = [];
- event.altKey && modifiers.push('alt');
- event.ctrlKey && modifiers.push('ctrl');
- event.metaKey && modifiers.push('meta');
- event.shiftKey && modifiers.push('shift');
- modifiers = modifiers.join('+');
- scopes.forEach(actions => {
- if (actions[modifiers]) {
- // for each potential shortcut
- actions[modifiers].forEach(cmd => {
- try {
- // call the handler and stop the event if neccessary
- if (!event.defaultPrevented && cmd(event) === false) {
- event.preventDefault();
- event.stopPropagation();
- }
- } catch (e) {
- console.error(e);
- }
- });
- }
- });
+ exec = (event, cmd) => {
+ try {
+ // call the handler and stop the event if neccessary
+ if (!event.defaultPrevented && cmd(event) === false) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ } catch (e) {
+ console.error(e);
}
+ },
+
+ shortcuts = {
+ on: () => doc.addEventListener('keydown', keydown),
+ off: () => doc.removeEventListener('keydown', keydown),
+ add: (keys, modifiers, scopes, method) => {
+ if (null == method) {
+ method = scopes;
+ scopes = 'all';
+ }
+ toArray(scopes).forEach(scope => {
+ if (!_scopes[scope]) {
+ _scopes[scope] = {};
+ }
+ toArray(keys).forEach(key => {
+ key = key.toLowerCase();
+ if (!_scopes[scope][key]) {
+ _scopes[scope][key] = {};
+ }
+ modifiers = toArray(modifiers)
+ .map(key => 'meta' == key ? meta : key)
+ .unique().sort().join('+');
+ if (!_scopes[scope][key][modifiers]) {
+ _scopes[scope][key][modifiers] = [];
+ }
+ _scopes[scope][key][modifiers].push(method);
+ });
+ });
+ },
+ setScope: value => {
+ _scope = value || 'all';
+ scope = _scopes[_scope] || {};
+ },
+ getScope: () => _scope,
+ getMetaKey: () => 'meta' === meta ? '⌘' : 'Ctrl'
+ },
+
+ keydown = event => {
+ let key = (event.key || '').toLowerCase().replace(' ','space'),
+ modifiers = ['alt','ctrl','meta','shift'].filter(v => event[v+'Key']).join('+');
+ scope[key] && scope[key][modifiers] && scope[key][modifiers].forEach(cmd => exec(event, cmd));
+ !event.defaultPrevented && _scope !== 'all' && _scopes.all[key] && _scopes.all[key][modifiers]
+ && _scopes.all[key][modifiers].forEach(cmd => exec(event, cmd));
};
-let
- scope = {},
- _scope = 'all';
+win.shortcuts = shortcuts;
-win.shortcuts = {
- on: () => doc.addEventListener('keydown', keydown),
- off: () => doc.removeEventListener('keydown', keydown),
- add: (keys, modifiers, scopes, method) => {
- if (method === undefined) {
- method = scopes;
- scopes = 'all';
- }
- toArray(scopes).forEach(scope => {
- if (!_scopes[scope]) {
- _scopes[scope] = {};
- }
- toArray(keys).forEach(key => {
- key = key.toLowerCase();
- if (!_scopes[scope][key]) {
- _scopes[scope][key] = {};
- }
- modifiers = toArray(modifiers)
- .map(key => 'meta' == key ? meta : key)
- .unique().sort().join('+');
- if (!_scopes[scope][key][modifiers]) {
- _scopes[scope][key][modifiers] = [];
- }
- _scopes[scope][key][modifiers].push(method);
- });
- });
- },
- setScope: value => {
- _scope = value || 'all';
- scope = _scopes[_scope] || {};
- },
- getScope: () => _scope,
- getMetaKey: () => meta,
- // ignore keydown in any element that supports keyboard input
- filter: node => !(!node.closest || node.closest('input,select,textarea,[contenteditable]'))
-};
-
-win.shortcuts.on();
+shortcuts.on();
})(this);
diff --git a/dev/sieve.js b/dev/sieve.js
new file mode 100644
index 000000000..238463aa0
--- /dev/null
+++ b/dev/sieve.js
@@ -0,0 +1,84 @@
+import {
+ capa,
+ forEachObjectValue,
+ getNotification,
+ loading,
+ Remote,
+ scripts,
+ serverError,
+ serverErrorDesc,
+ setError
+} from 'Sieve/Utils';
+
+import { SieveScriptModel } from 'Sieve/Model/Script';
+import { SieveScriptPopupView } from 'Sieve/View/Script';
+
+// SieveUserStore
+window.Sieve = {
+ capa: capa,
+ scripts: scripts,
+ setError: setError,
+ loading: loading,
+ serverError: serverError,
+ serverErrorDesc: serverErrorDesc,
+ ScriptView: SieveScriptPopupView,
+
+ folderList: null,
+
+ updateList: () => {
+ if (!loading()) {
+ loading(true);
+ serverError(false);
+
+ Remote.request('Filters', (iError, data) => {
+ loading(false);
+ scripts([]);
+
+ if (iError) {
+ capa([]);
+ setError(getNotification(iError));
+ } else {
+ capa(data.Result.Capa);
+/*
+ scripts(
+ data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
+ );
+*/
+ forEachObjectValue(data.Result.Scripts, value => {
+ value = SieveScriptModel.reviveFromJson(value);
+ value && scripts.push(value)
+ });
+ }
+ });
+ }
+ },
+
+ deleteScript: script => {
+ serverError(false);
+ Remote.request('FiltersScriptDelete',
+ (iError, data) => {
+ if (iError) {
+ setError((data && data.ErrorMessageAdditional) || getNotification(iError));
+ } else {
+ scripts.remove(script);
+ }
+ },
+ {name:script.name()}
+ );
+ },
+
+ toggleScript(script) {
+ let name = script.active() ? '' : script.name();
+ serverError(false);
+ Remote.request('FiltersScriptActivate',
+ (iError, data) => {
+ if (iError) {
+ setError((data && data.ErrorMessageAdditional) || iError)
+ } else {
+ scripts.forEach(script => script.active(script.name() === name));
+ }
+ },
+ {name:name}
+ );
+ }
+};
diff --git a/index.php b/index.php
index d9b810f64..efcc8acd1 100644
--- a/index.php
+++ b/index.php
@@ -3,13 +3,12 @@
if (!defined('APP_VERSION'))
{
define('APP_VERSION', '0.0.0');
- define('APP_INDEX_ROOT_FILE', __FILE__);
- define('APP_INDEX_ROOT_PATH', strtr(__DIR__, DIRECTORY_SEPARATOR, '/') . '/');
+ define('APP_INDEX_ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR);
}
-if (file_exists(APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php'))
+if (file_exists('snappymail/v/'.APP_VERSION.'/include.php'))
{
- include APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php';
+ include 'snappymail/v/'.APP_VERSION.'/include.php';
}
else
{
diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md
new file mode 100644
index 000000000..00587691c
--- /dev/null
+++ b/integrations/cloudron/DESCRIPTION.md
@@ -0,0 +1,22 @@
+This app packages SnappyMail 2.13.4 .
+
+SnappyMail is a simple, modern, lightweight & fast web-based email client.
+
+### Features
+ * Privacy/GDPR friendly
+ * Modern user (dark mode) interface.
+ * Mobile booting with only ~144 KB download
+ * Complete support of IMAP and SMTP protocols including SSL and STARTTLS.
+ * Sieve scripts (Filters and vacation message).
+ * Minimalistic resources requirements.
+ * Direct access to mail server is used (mails are not stored locally on web server).
+ * Allows for adding multiple accounts to primary one.
+ * Additional identities.
+ * Administrative panel for configuring main options.
+ * Really simple installation and update (the product is updated from admin panel).
+ * Managing folders list.
+ * Simple look'n'feel customization.
+ * Extending functionality with plugins installed through admin panel.
+ * Perfect rendering of complex HTML mails.
+ * Drag'n'drop for mails and attachments.
+ * Keyboard shortcuts support.
diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile
new file mode 100644
index 000000000..a43e855be
--- /dev/null
+++ b/integrations/cloudron/Dockerfile
@@ -0,0 +1,38 @@
+FROM cloudron/base:3.0.0@sha256:455c70428723e3a823198c57472785437eb6eab082e79b3ff04ea584faf46e92
+
+RUN mkdir -p /app/code
+WORKDIR /app/code
+
+# If you change the extraction below, be sure to test on scaleway
+VERSION=2.13.4
+RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
+ unzip /tmp/snappymail.zip -d /app/code && \
+ rm /tmp/snappymail.zip && \
+ find /app/code/snappymail -type d -exec chmod 755 {} \; && \
+ find /app/code/snappymail -type f -exec chmod 644 {} \; && \
+ rm -rf /app/code/data && ln -s /app/data /app/code/data && \
+ chown -R www-data:www-data /app/code/snappymail
+
+# configure apache
+RUN rm /etc/apache2/sites-enabled/*
+RUN sed -e 's,^ErrorLog.*,ErrorLog "|/bin/cat",' -i /etc/apache2/apache2.conf
+COPY apache/mpm_prefork.conf /etc/apache2/mods-available/mpm_prefork.conf
+
+RUN a2disconf other-vhosts-access-log
+ADD apache/snappymail.conf /etc/apache2/sites-enabled/snappymail.conf
+RUN echo "Listen 8000" > /etc/apache2/ports.conf
+
+# mod_php config
+RUN a2enmod rewrite
+RUN crudini --set /etc/php/7.4/apache2/php.ini PHP upload_max_filesize 25M && \
+ crudini --set /etc/php/7.4/apache2/php.ini PHP post_max_size 25M && \
+ crudini --set /etc/php/7.4/apache2/php.ini Session session.save_path /run/snappymail/sessions && \
+ crudini --set /etc/php/7.4/apache2/php.ini Session session.gc_probability 1 && \
+ crudini --set /etc/php/7.4/apache2/php.ini Session session.gc_divisor 100
+
+RUN ln -s /app/data/php.ini /etc/php/7.4/apache2/conf.d/99-cloudron.ini && \
+ ln -s /app/data/php.ini /etc/php/7.4/cli/conf.d/99-cloudron.ini
+
+ADD start.sh /app/code/start.sh
+
+CMD [ "/app/code/start.sh" ]
diff --git a/integrations/nextcloud/snappymail/INSTALL b/integrations/nextcloud/snappymail/INSTALL
new file mode 100755
index 000000000..d9c57d60b
--- /dev/null
+++ b/integrations/nextcloud/snappymail/INSTALL
@@ -0,0 +1,17 @@
+************************************************************************
+*
+* Nextcloud - SnappyMail Webmail package
+*
+* @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
+*
+************************************************************************
+
+REQUIREMENTS:
+- Nextcloud version 14 and above
+
+INSTALL & CONFIGURATION:
+Start within Nextcloud, and click on the "+ Apps" button in the upper-left corner dropdown menu:
+Then, enable the SnappyMail plugin that is in the "Social & communication" section.
+After a quick wait, SnappyMail is installed. Even if it is really attractive, it is too soon to click on the newly appeared "Email" icon in the apps list. You should configure SnappyMail before using it (which makes some sense, doesn'it): go to Nextcloud admin panel (upper-right corner dropdown menu) and go to "Additionnal settings". There click on the "Go to SnappyMail Webmail admin panel".
+In the SnappyMail admin prompt, the default login is "admin" and the default password is "12345". No need to advise you to change it once in the admin panel!
+This is it, you are now free to configure SnappyMail as you wish. One important point is the Domains section when you will set up the IMAP/SMTP parameters that shall be associated with the email adresses of your users.
diff --git a/integrations/nextcloud/snappymail/README.md b/integrations/nextcloud/snappymail/README.md
new file mode 100644
index 000000000..12f2e2cb5
--- /dev/null
+++ b/integrations/nextcloud/snappymail/README.md
@@ -0,0 +1,76 @@
+# snappymail-nextcloud
+
+snappymail-nextcloud is a plugin for Nextcloud to use the excellent SnappyMail webmail (https://snappymail.eu/).
+
+## Which branch for which version of Nextcloud?
+
+- The master branch corresponds to the latest stable version of the plugin that is suitable for Nextcloud 19 and upwards.
+- The 'up-to-nc18' corresponds to the working version of the plugin for Nextcloud 14 to 18.
+- The 'nc14' branch corresponds to a beta version of the plugin for Nextcloud 14. All the changes of 'nc14' branch were merged into master when it was deemed suitable for production.
+- The 'nc13' branch corresponds to the working version of the plugin for Nextcloud 10 to Nextcloud 13.
+
+
+Thank you to all contributors to SnappyMail for nextcloud:
+- SnappyMail Team, who initiated it
+- Tab Fitts (@tabp0le)
+- Nextgen Networks (@nextgen-networks)
+
+## How to Install
+
+Start within Nextcloud, and click on the "+ Apps" button in the upper-left corner dropdown menu:
+
+
+
+Then, enable the SnappyMail plugin that you will find in the "Social & communication" section:
+
+
+
+After a quick wait, SnappyMail is installed. Now you should configure it before use: open the Nextcloud admin panel (upper-right corner dropdown menu) and go to "Additionnal settings". There, click on the "Go to SnappyMail Webmail admin panel" link.
+
+
+
+To enter SnappyMail admin area, the default login is "admin" and the default password is "12345". Don't forget to change it once in the admin panel!
+
+From that point, all instance-wide SnappyMail settings can be tweaked as you wish. One important point is the "Domains" section where you should set up the IMAP/SMTP parameters that will be associated with the email adresses of your users. Basically, if a user of the Nextcloud instance starts SnappyMail and puts "firstname@domain.tld" as an email address, then SnappyMail should know how to connect to the IMAP & SMTP of domain.tld. You can fill in this information in the "Domains" section of the SnappyMail admin settings.
+
+
+
+## SnappyMail Settings, Where Are They?
+
+SnappyMail for Nextcloud is highly configurable. But settings are available in multiple places and this can be misleading for first-time users.
+
+### SnappyMail admin settings
+SnappyMail admin settings can be reached only by the Nextcloud administrator. Open the Nextcloud admin panel ("Admin" in the upper-right corner dropdown menu) and go to "Additionnal settings". There, click on the "Go to SnappyMail Webmail admin panel" link. Alternatively, you may use the following link: https://path.to.nextcloud/index.php/apps/snappymail/app/?admin.
+
+SnappyMail admin settings include all settings that will apply to all SnappyMail users (default login rules, branding, management of plugins, security rules and domains).
+
+### SnappyMail user settings
+Each user of SnappyMail can also change user-specific behaviors in the SnappyMail user settings. SnappyMail user settings are found within SnappyMail by clicking on the user button (in the upper-right corner of SnappyMail) and then choosing "Settings" in the dropdown menu.
+
+SnappyMail user settings include management of contacts, of email accounts, of folders, appearance and OpenPGP.
+
+### The specificity of SnappyMail user accounts
+The plugin passes the login information of the user to the SnappyMail app which then creates and manages the user accounts. Accounts in SnappyMail are based soley on the authenticated email accounts, and do not take into account the nextcloud user which created them in the first place. If two or more Nextcloud users have the same email account in additional settings, they will in fact share the same 'email account' in SnappyMail including any additional email accounts that they may have added subsequently to their main account.
+This is to be kept in mind for the use case where multiple users shall have the same email account but may be also tempted to add additionnal acounts to their SnappyMail.
+
+## How to auto-connect to SnappyMail?
+
+### Auto-connection for all Nextcloud users
+If your Nextcloud users base is synchronized with an email system, then it is possible that Nextcloud credentials could be used right away to access the centralized email system. In the SnappyMail admin settings, the Nextcloud administrator can then tick the "Automatically login with Nextcloud/Nextcloud user credentials" checkbox.
+
+Beware, if you tick this box, all Nextcloud users will *not* be able to use the override it with the setting below.
+
+### Auto-connection for one user at a time
+Except if the above setting is activated, any Nextcloud user can have Nextcloud and SnappyMail keep in mind the default email/password to connect to SnappyMail. There, logging in Nextcloud is sufficient to then access SnappyMail within Nextcloud.
+
+To fill in the default email address and password to use, each Nextcloud user should go in the personal settings: choose "Personal" in the upper-right corner dropdown menu. Then select the "Rainlopp webmail" section. You can also use this direct link: https://path.to.nextcloud/index.php/settings/personal#snappymail-webmail.
+
+
+## How to Activate SnappyMail Logging and then Find Logs
+
+You can activate SnappyMail logging here: `/path/to/nextcloud/data/snappymail-storage/_data_/_default_/configs/application.ini`
+```
+[logs]
+enable = On
+```
+Logs are then available in `/path/to/nextcloud/data/snappymail-storage/_data_/_default_/logs/`
diff --git a/integrations/nextcloud/snappymail/VERSION b/integrations/nextcloud/snappymail/VERSION
new file mode 100755
index 000000000..cb9778b41
--- /dev/null
+++ b/integrations/nextcloud/snappymail/VERSION
@@ -0,0 +1 @@
+2.13.4
\ No newline at end of file
diff --git a/integrations/nextcloud/snappymail/app/data/_data_/_default_/plugins/nextcloud/NextCloudSuggestions.php b/integrations/nextcloud/snappymail/app/data/_data_/_default_/plugins/nextcloud/NextCloudSuggestions.php
new file mode 100644
index 000000000..a99e9052e
--- /dev/null
+++ b/integrations/nextcloud/snappymail/app/data/_data_/_default_/plugins/nextcloud/NextCloudSuggestions.php
@@ -0,0 +1,107 @@
+getContactsManager();
+ if (!$cm && !$cm->isEnabled())
+ {
+ return $aResult;
+ }
+
+ $aSearchResult = $cm->search($sQuery, $aParams);
+ }
+ else if (\class_exists('OCP\Contacts') && \OCP\Contacts::isEnabled())
+ {
+ $aSearchResult = \OCP\Contacts::search($sQuery, $aParams);
+ }
+ else
+ {
+ return $aResult;
+ }
+
+ //$this->oLogger->WriteDump($aSearchResult);
+
+ $aHashes = array();
+ if (\is_array($aSearchResult) && 0 < \count($aSearchResult))
+ {
+ foreach ($aSearchResult as $aContact)
+ {
+ if (0 >= $iLimit)
+ {
+ break;
+ }
+
+ $sUid = empty($aContact['UID']) ? '' : $aContact['UID'];
+ if (!empty($sUid))
+ {
+ $mEmails = isset($aContact['EMAIL']) ? $aContact['EMAIL'] : '';
+
+ $sFullName = isset($aContact['FN']) ? \trim($aContact['FN']) : '';
+ if (empty($sFullName))
+ {
+ $sFullName = isset($aContact['NICKNAME']) ? \trim($aContact['NICKNAME']) : '';
+ }
+
+ if (!\is_array($mEmails))
+ {
+ $mEmails = array($mEmails);
+ }
+
+ foreach ($mEmails as $sEmail)
+ {
+ $sHash = '"'.$sFullName.'" <'.$sEmail.'>';
+ if (!isset($aHashes[$sHash]))
+ {
+ $aHashes[$sHash] = true;
+ $aResult[] = array($sEmail, $sFullName);
+ $iLimit--;
+ }
+ }
+ }
+ }
+
+ $aResult = \array_slice($aResult, 0, $iInputLimit);
+ }
+
+ unset($aSearchResult, $aHashes);
+ }
+ catch (\Throwable $oException)
+ {
+ if ($this->oLogger)
+ {
+ $this->oLogger->WriteException($oException);
+ }
+ }
+
+ return $aResult;
+ }
+
+ /**
+ * @param \MailSo\Log\Logger $oLogger
+ */
+ public function SetLogger($oLogger)
+ {
+ $this->oLogger = $oLogger instanceof \MailSo\Log\Logger ? $oLogger : null;
+ }
+}
diff --git a/integrations/nextcloud/snappymail/app/data/_data_/_default_/plugins/nextcloud/index.php b/integrations/nextcloud/snappymail/app/data/_data_/_default_/plugins/nextcloud/index.php
new file mode 100644
index 000000000..099d239a3
--- /dev/null
+++ b/integrations/nextcloud/snappymail/app/data/_data_/_default_/plugins/nextcloud/index.php
@@ -0,0 +1,180 @@
+addHook('main.fabrica', 'MainFabrica');
+ $this->addHook('filter.app-data', 'FilterAppData');
+ $this->addHook('json.attachments', 'DoAttachmentsActions');
+
+ $sAppPath = '';
+ if (\class_exists('OC_App')) {
+ $sAppPath = \rtrim(\trim(\OC_App::getAppWebPath('snappymail')), '\\/').'/app/';
+ }
+ if (!$sAppPath) {
+ $sUrl = \MailSo\Base\Http::SingletonInstance()->GetUrl();
+ if ($sUrl && \preg_match('/\/index\.php\/apps\/snappymail/', $sUrl)) {
+ $sAppPath = \preg_replace('/\/index\.php\/apps\/snappymail.+$/',
+ '/apps/snappymail/app/', $sUrl);
+ }
+ }
+ $_SERVER['SCRIPT_NAME'] = $sAppPath;
+ }
+ }
+
+ public function Supported() : string
+ {
+ if (!static::IsNextCloud()) {
+ return 'NextCloud not found to use this plugin';
+ }
+ return '';
+ }
+
+ // DoAttachmentsActions
+ public function DoAttachmentsActions(\SnappyMail\AttachmentsAction $data)
+ {
+ if ('nextcloud' === $data->action) {
+ if (static::IsNextCloudLoggedIn() && \class_exists('OCP\Files')) {
+ $oFiles = \OCP\Files::getStorage('files');
+ if ($oFiles && $data->filesProvider->IsActive() && \method_exists($oFiles, 'file_put_contents')) {
+ $sSaveFolder = $this->Config()->Get('plugin', 'save_folder', '') ?: 'Attachments';
+ $oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder);
+ $data->result = true;
+ foreach ($data->items as $aItem) {
+ $sSavedFileName = isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat';
+ $sSavedFileHash = !empty($aItem['FileHash']) ? $aItem['FileHash'] : '';
+ if (!empty($sSavedFileHash)) {
+ $fFile = $data->filesProvider->GetFile($data->account, $sSavedFileHash, 'rb');
+ if (\is_resource($fFile)) {
+ $sSavedFileNameFull = \MailSo\Base\Utils::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, function ($sPath) use ($oFiles) {
+ return $oFiles->file_exists($sPath);
+ });
+
+ if (!$oFiles->file_put_contents($sSavedFileNameFull, $fFile)) {
+ $data->result = false;
+ }
+
+ if (\is_resource($fFile)) {
+ \fclose($fFile);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($data->items as $aItem) {
+ $sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : '');
+ if (!empty($sFileHash)) {
+ $data->filesProvider->Clear($data->account, $sFileHash);
+ }
+ }
+ }
+ }
+
+ /**
+ * TODO: create pre-login auth hook
+ */
+ public function ServiceNextCloudAuth()
+ {
+/*
+ $this->oHttp->ServerNoCache();
+
+ if (!static::IsNextCloud() ||
+ !isset($_ENV['___snappymail_nextcloud_email']) ||
+ !isset($_ENV['___snappymail_nextcloud_password']) ||
+ empty($_ENV['___snappymail_nextcloud_email'])
+ )
+ {
+ $this->oActions->SetAuthLogoutToken();
+ $this->oActions->Location('./');
+ return '';
+ }
+
+ $bLogout = true;
+
+ $sEmail = $_ENV['___snappymail_nextcloud_email'];
+ $sPassword = $_ENV['___snappymail_nextcloud_password'];
+
+ try
+ {
+ $oAccount = $this->oActions->LoginProcess($sEmail, $sPassword);
+ $this->oActions->AuthToken($oAccount);
+
+ $bLogout = !($oAccount instanceof \snappymail\Model\Account);
+ }
+ catch (\Exception $oException)
+ {
+ $this->oActions->Logger()->WriteException($oException);
+ }
+
+ if ($bLogout)
+ {
+ $this->oActions->SetAuthLogoutToken();
+ }
+
+ $this->oActions->Location('./');
+ return '';
+*/
+ }
+
+ /**
+ * @return void
+ */
+ public function FilterAppData($bAdmin, &$aResult)
+ {
+ if (!$bAdmin && \is_array($aResult) && static::IsNextCloud()) {
+ $key = \array_search(\RainLoop\Enumerations\Capa::AUTOLOGOUT, $aResult['Capa']);
+ if (false !== $key) {
+ unset($aResult['Capa'][$key]);
+ }
+ if (static::IsNextCloudLoggedIn() && \class_exists('OCP\Files')) {
+ $aResult['System']['attachmentsActions'][] = 'nextcloud';
+ }
+ }
+ }
+
+ /**
+ * @param string $sName
+ * @param mixed $mResult
+ */
+ public function MainFabrica($sName, &$mResult)
+ {
+ if ('suggestions' === $sName && static::IsNextCloud() && $this->Config()->Get('plugin', 'suggestions', true)) {
+ include_once __DIR__.'/NextCloudSuggestions.php';
+ if (!\is_array($mResult)) {
+ $mResult = array();
+ }
+ $mResult[] = new NextCloudSuggestions();
+ }
+ }
+
+ protected function configMapping() : array
+ {
+ return array(
+ \RainLoop\Plugins\Property::NewInstance('save_folder')->SetLabel('Save Folder')
+ ->SetDefaultValue('Attachments'),
+ \RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(true)
+ );
+ }
+}
diff --git a/integrations/nextcloud/snappymail/appinfo/app.php b/integrations/nextcloud/snappymail/appinfo/app.php
new file mode 100644
index 000000000..7b86a17b8
--- /dev/null
+++ b/integrations/nextcloud/snappymail/appinfo/app.php
@@ -0,0 +1,15 @@
+registerNavigation();
+$app->registerPersonalSettings();
+$app->getContainer()->query('SnappyMailHelper')->registerHooks();
+
diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml
new file mode 100644
index 000000000..284164cd7
--- /dev/null
+++ b/integrations/nextcloud/snappymail/appinfo/info.xml
@@ -0,0 +1,29 @@
+
+
+ snappymail
+ SnappyMail
+ SnappyMail Webmail
+ Simple, modern and fast web-based email client. After enabling in Nextcloud, go to Nextcloud admin panel, "Additionnal settings" and you will see a "SnappyMail webmail" section. There, click on the link to go to the SnappyMail admin panel. The default user/password is admin/12345. This version is based on SnappyMail 2.6.0 (2021-07).
+ 2.13.4
+ agpl
+ SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli
+ SnappyMail
+
+ https://github.com/pierre-alain-b/snappymail-nextcloud/blob/master/README.md
+ https://snappymail.eu/
+
+ social
+ tools
+ integration
+ https://raw.githubusercontent.com/pierre-alain-b/snappymail-nextcloud/master/screenshots/2016.10.20-screenshot.jpg
+ https://github.com/pierre-alain-b/snappymail-nextcloud
+ https://github.com/pierre-alain-b/snappymail-nextcloud/issues
+
+
+
+
+
+
+ OCA\SnappyMail\Settings\AdminSettings
+
+
diff --git a/integrations/nextcloud/snappymail/appinfo/routes.php b/integrations/nextcloud/snappymail/appinfo/routes.php
new file mode 100755
index 000000000..a19b5a4f3
--- /dev/null
+++ b/integrations/nextcloud/snappymail/appinfo/routes.php
@@ -0,0 +1,31 @@
+ [
+ [
+ 'name' => 'page#index',
+ 'url' => '/',
+ 'verb' => 'GET'
+ ],
+ [
+ 'name' => 'page#appGet',
+ 'url' => '/app/',
+ 'verb' => 'GET'
+ ],
+ [
+ 'name' => 'page#appPost',
+ 'url' => '/app/',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'ajax#setPersonal',
+ 'url' => '/ajax/personal.php',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'ajax#setAdmin',
+ 'url' => '/ajax/admin.php',
+ 'verb' => 'POST'
+ ]
+ ]
+];
diff --git a/integrations/nextcloud/snappymail/appinfo/version b/integrations/nextcloud/snappymail/appinfo/version
new file mode 100755
index 000000000..a8a188756
--- /dev/null
+++ b/integrations/nextcloud/snappymail/appinfo/version
@@ -0,0 +1 @@
+7.1.2
diff --git a/integrations/nextcloud/snappymail/css/style.css b/integrations/nextcloud/snappymail/css/style.css
new file mode 100755
index 000000000..88f73b2f7
--- /dev/null
+++ b/integrations/nextcloud/snappymail/css/style.css
@@ -0,0 +1,6 @@
+/*
+Empty style sheet!
+Only needed to give you the opportunity to theme the owncloud part
+of the snappymail app with the theming system integrated in ownCoud
+if the snappymail app is activated.
+*/
diff --git a/integrations/nextcloud/snappymail/img/favicon.png b/integrations/nextcloud/snappymail/img/favicon.png
new file mode 100644
index 000000000..78ab30b38
Binary files /dev/null and b/integrations/nextcloud/snappymail/img/favicon.png differ
diff --git a/integrations/nextcloud/snappymail/img/logo-64x64.png b/integrations/nextcloud/snappymail/img/logo-64x64.png
new file mode 100644
index 000000000..78ab30b38
Binary files /dev/null and b/integrations/nextcloud/snappymail/img/logo-64x64.png differ
diff --git a/integrations/nextcloud/snappymail/js/admin.js b/integrations/nextcloud/snappymail/js/admin.js
new file mode 100755
index 000000000..e84b4a0c0
--- /dev/null
+++ b/integrations/nextcloud/snappymail/js/admin.js
@@ -0,0 +1,12 @@
+
+/**
+ * Nextcloud - SnappyMail mail plugin
+ *
+ * @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
+ *
+ * Based initially on https://github.com/SnappyMail/snappymail-webmail/tree/master/build/owncloud
+ */
+
+$(function() {
+ SnappyMailFormHelper('#mail-snappymail-admin-form', 'admin.php');
+});
diff --git a/integrations/nextcloud/snappymail/js/personal.js b/integrations/nextcloud/snappymail/js/personal.js
new file mode 100755
index 000000000..b2a7d7c3d
--- /dev/null
+++ b/integrations/nextcloud/snappymail/js/personal.js
@@ -0,0 +1,12 @@
+
+/**
+ * Nextcloud - SnappyMail mail plugin
+ *
+ * @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
+ *
+ * Based initially on https://github.com/SnappyMail/snappymail-webmail/tree/master/build/owncloud
+ */
+
+$(function() {
+ SnappyMailFormHelper('#mail-snappymail-personal-form', 'personal.php');
+});
diff --git a/integrations/nextcloud/snappymail/js/rainloop.js b/integrations/nextcloud/snappymail/js/rainloop.js
new file mode 100755
index 000000000..d8687f21b
--- /dev/null
+++ b/integrations/nextcloud/snappymail/js/rainloop.js
@@ -0,0 +1,114 @@
+// Do the following things once the document is fully loaded.
+document.onreadystatechange = function () {
+ if (document.readyState === 'complete') {
+ watchIFrameTitle();
+ }
+}
+
+// The SnappyMail application is already configured to modify the element
+// of its root document with the number of unread messages in the inbox.
+// However, its document is the SnappyMail iframe. This function sets up a
+// Mutation Observer to watch the element of the iframe for changes in
+// the unread message count and propagates that to the parent element,
+// allowing the unread message count to be displayed in the NC tab's text when
+// the SnappyMail app is selected.
+function watchIFrameTitle() {
+ iframe = document.getElementById('rliframe');
+ if (!iframe) {
+ return;
+ }
+ target = iframe.contentDocument.getElementsByTagName('title')[0];
+ config = {
+ characterData: true,
+ childList: true,
+ subtree: true
+ }
+ observer = new MutationObserver(function(mutations) {
+ title = mutations[0].target.innerText;
+ if (title) {
+ matches = title.match(/\(([0-9]+)\)/);
+ if (matches) {
+ document.title = '('+ matches[1] + ') ' + t('snappymail', 'Email') + ' - Nextcloud';
+ } else {
+ document.title = t('snappymail', 'Email') + ' - Nextcloud';
+ }
+ }
+ });
+ observer.observe(target, config);
+}
+
+function SnappyMailFormHelper(sID, sAjaxFile, fCallback)
+{
+ try
+ {
+ var
+ oForm = $(sID),
+ oSubmit = $('#snappymail-save-button', oForm),
+ sSubmitValue = oSubmit.val(),
+ oDesc = oForm.find('.snappymail-result-desc')
+ ;
+
+ oSubmit.click(function (oEvent) {
+
+ var oDefAjax = null;
+
+ oEvent.preventDefault();
+
+ oForm
+ .addClass('snappymail-ajax')
+ .removeClass('snappymail-error')
+ .removeClass('snappymail-success')
+ ;
+
+ oDesc.text('');
+ oSubmit.val('...');
+
+ oDefAjax = $.ajax({
+ 'type': 'POST',
+ 'async': true,
+ 'url': OC.filePath('snappymail', 'ajax', sAjaxFile),
+ 'data': oForm.serialize(),
+ 'dataType': 'json',
+ 'global': true
+ });
+
+ oDefAjax.always(function (oData) {
+
+ var bResult = false;
+
+ oForm.removeClass('snappymail-ajax');
+ oSubmit.val(sSubmitValue);
+
+ if (oData)
+ {
+ bResult = 'success' === oData['status'];
+ if (oData['Message'])
+ {
+ oDesc.text(t('snappymail', oData['Message']));
+ }
+ }
+
+ if (bResult)
+ {
+ oForm.addClass('snappymail-success');
+ }
+ else
+ {
+ oForm.addClass('snappymail-error');
+ if ('' === oDesc.text())
+ {
+ oDesc.text(t('snappymail', 'Error'));
+ }
+ }
+
+ if (fCallback)
+ {
+ fCallback(bResult, oData);
+ }
+ });
+
+ return false;
+ });
+ }
+ catch(e) {}
+}
diff --git a/integrations/nextcloud/snappymail/js/resize.js b/integrations/nextcloud/snappymail/js/resize.js
new file mode 100755
index 000000000..5c16f0022
--- /dev/null
+++ b/integrations/nextcloud/snappymail/js/resize.js
@@ -0,0 +1,26 @@
+$(function (window, document) {
+
+ var
+ buffer = 0, //was 5 but this was creating a white space of 5px at the bottom of the page
+ ifr = document.getElementById('rliframe')
+ ;
+
+
+ function pageY(elem) {
+ return elem.offsetParent ? (elem.offsetTop + pageY(elem.offsetParent)) : elem.offsetTop;
+ }
+
+ function resizeIframe() {
+ var height = document.documentElement.clientHeight;
+ height -= pageY(ifr) + buffer;
+ height = (height < 0) ? 0 : height;
+ ifr.style.height = height + 'px';
+ }
+
+ if (ifr)
+ {
+ ifr.onload = resizeIframe;
+ window.onresize = resizeIframe;
+ }
+
+}(window, document));
diff --git a/integrations/nextcloud/snappymail/l10n/en_GB.js b/integrations/nextcloud/snappymail/l10n/en_GB.js
new file mode 100644
index 000000000..dc856193f
--- /dev/null
+++ b/integrations/nextcloud/snappymail/l10n/en_GB.js
@@ -0,0 +1,9 @@
+OC.L10N.register(
+ "snappymail",
+ {
+ "Email" : "Email",
+ "Error" : "Error",
+ "Save" : "Save",
+ "Password" : "Password"
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/integrations/nextcloud/snappymail/l10n/en_GB.json b/integrations/nextcloud/snappymail/l10n/en_GB.json
new file mode 100644
index 000000000..98a2adb74
--- /dev/null
+++ b/integrations/nextcloud/snappymail/l10n/en_GB.json
@@ -0,0 +1,7 @@
+{ "translations": {
+ "Email" : "Email",
+ "Error" : "Error",
+ "Save" : "Save",
+ "Password" : "Password"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+}
\ No newline at end of file
diff --git a/integrations/nextcloud/snappymail/lib/AppInfo/Application.php b/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
new file mode 100644
index 000000000..d1d81e6bd
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
@@ -0,0 +1,87 @@
+getContainer();
+ $server = $container->getServer();
+ $config = $server->getConfig();
+
+ /**
+ * Controllers
+ */
+ $container->registerService(
+ 'PageController', function($c) {
+ return new PageController(
+ $c->query('AppName'),
+ $c->query('Request'),
+ $c->getServer()->getAppManager(),
+ $c->query('ServerContainer')->getConfig(),
+ $c->getServer()->getSession()
+ );
+ }
+ );
+
+ $container->registerService(
+ 'AjaxController', function($c) {
+ return new AjaxController(
+ $c->query('AppName'),
+ $c->query('Request'),
+ $c->getServer()->getAppManager(),
+ $c->query('ServerContainer')->getConfig(),
+ $c->query(IL10N::class)
+ );
+ }
+ );
+
+ /**
+ * Utils
+ */
+ $container->registerService(
+ 'SnappyMailHelper', function($c) {
+ return new SnappyMailHelper(
+ $c->getServer()->getConfig(),
+ $c->getServer()->getUserSession(),
+ $c->getServer()->getAppManager(),
+ $c->getServer()->getSession()
+ );
+ }
+ );
+
+ // Add script js/snappymail.js
+ \OCP\Util::addScript('snappymail', 'snappymail');
+ }
+
+ public function registerNavigation() {
+ $container = $this->getContainer();
+
+ $container->query('OCP\INavigationManager')->add(function () use ($container) {
+ $urlGenerator = $container->query('OCP\IURLGenerator');
+ return [
+ 'id' => 'snappymail',
+ 'order' => 10,
+ 'href' => $urlGenerator->linkToRoute('snappymail.page.index'),
+ 'icon' => $urlGenerator->imagePath('snappymail', 'logo-64x64.png'),
+ 'name' => \OCP\Util::getL10N('snappymail')->t('Email')
+ ];
+ });
+ }
+
+ public function registerPersonalSettings() {
+ \OCP\App::registerPersonal('snappymail', 'templates/personal');
+ }
+
+}
+
diff --git a/integrations/nextcloud/snappymail/lib/Controller/AjaxController.php b/integrations/nextcloud/snappymail/lib/Controller/AjaxController.php
new file mode 100644
index 000000000..e0a561138
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Controller/AjaxController.php
@@ -0,0 +1,98 @@
+config = $config;
+ $this->appManager = $appManager;
+ $this->l = $l;
+ }
+
+ public function setAdmin(): JSONResponse {
+ try {
+ $sUrl = '';
+ $sPath = '';
+ $bAutologin = false;
+
+ if (isset($_POST['appname']) && 'snappymail' === $_POST['appname']) {
+ $this->config->setAppValue('snappymail', 'snappymail-autologin',
+ isset($_POST['snappymail-autologin']) ? '1' === $_POST['snappymail-autologin'] : false);
+ $this->config->setAppValue('snappymail', 'snappymail-autologin-with-email',
+ isset($_POST['snappymail-autologin']) ? '2' === $_POST['snappymail-autologin'] : false);
+ $bAutologin = $this->config->getAppValue('snappymail', 'snappymail-autologin', false);
+ } else {
+ return new JSONResponse([
+ 'status' => 'error',
+ 'Message' => $this->l->t('Invalid argument(s)')
+ ]);
+ }
+
+ return new JSONResponse([
+ 'status' => 'success',
+ 'Message' => $this->l->t('Saved successfully')
+ ]);
+ } catch (Exception $e) {
+ return new JSONResponse([
+ 'status' => 'error',
+ 'Message' => $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function setPersonal(): JSONResponse {
+ try {
+
+ if (isset($_POST['appname'], $_POST['snappymail-password'], $_POST['snappymail-email']) && 'snappymail' === $_POST['appname']) {
+ $sUser = \OC::$server->getUserSession()->getUser()->getUID();
+
+ $sPostEmail = $_POST['snappymail-email'];
+ $this->config->setUserValue($sUser, 'snappymail', 'snappymail-email', $sPostEmail);
+
+ $sPass = $_POST['snappymail-password'];
+ if ('******' !== $sPass && '' !== $sPass) {
+ include_once $this->appManager->getAppPath('snappymail').'/lib/Util/SnappyMailHelper.php';
+
+ $this->config->setUserValue($sUser, 'snappymail', 'snappymail-password',
+ SnappyMailHelper::encodePassword($sPass, md5($sPostEmail)));
+ }
+
+ $sEmail = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-email', '');
+ } else {
+ return new JSONResponse([
+ 'status' => 'error',
+ 'Message' => $this->l->t('Invalid argument(s)'),
+ 'Email' => $sEmail
+ ]);
+ }
+
+ return new JSONResponse([
+ 'status' => 'success',
+ 'Message' => $this->l->t('Saved successfully'),
+ 'Email' => $sEmail
+ ]);
+ } catch (Exception $e) {
+ return new JSONResponse([
+ 'status' => 'error',
+ 'Message' => $e->getMessage()
+ ]);
+ }
+ }
+}
+
diff --git a/integrations/nextcloud/snappymail/lib/Controller/PageController.php b/integrations/nextcloud/snappymail/lib/Controller/PageController.php
new file mode 100644
index 000000000..2279430e3
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Controller/PageController.php
@@ -0,0 +1,110 @@
+appPath = $appManager->getAppPath('snappymail');
+ $this->config = $config;
+ $this->appManager = $appManager;
+ $this->session = $session;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function index() {
+ \OC::$server->getNavigationManager()->setActiveEntry('snappymail');
+
+ \OCP\Util::addStyle('snappymail', 'style');
+
+ $sUrl = SnappyMailHelper::normalizeUrl(SnappyMailHelper::getAppUrl());
+
+ $params = [
+ 'snappymail-iframe-url' => SnappyMailHelper::normalizeUrl($sUrl).'?OwnCloudAuth'
+ ];
+
+ $response = new TemplateResponse('snappymail', 'index', $params);
+
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedFrameDomain("'self'");
+ $response->setContentSecurityPolicy($csp);
+
+ return $response;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function appGet() {
+ $this->app();
+ }
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function appPost() {
+ $this->app();
+ }
+
+ public function app() {
+
+ SnappyMailHelper::regSnappyMailDataFunction();
+
+ if (isset($_GET['OwnCloudAuth'])) {
+ $sEmail = '';
+ $sEncodedPassword = '';
+
+ $sUser = \OC::$server->getUserSession()->getUser()->getUID();
+
+ if ($this->config->getAppValue('snappymail', 'snappymail-autologin', false)) {
+ $sEmail = $sUser;
+ $sPasswordSalt = $sUser;
+ $sEncodedPassword = $this->session['snappymail-autologin-password'];
+ } else if ($this->config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
+ $sEmail = $this->config->getUserValue($sUser, 'settings', 'email', '');
+ $sPasswordSalt = $sUser;
+ $sEncodedPassword = $this->session['snappymail-autologin-password'];
+ }
+
+ // If the user has set credentials for SnappyMail in their personal
+ // settings, override everything before and use those instead.
+ $sIndividualEmail = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-email', '');
+ if ($sIndividualEmail) {
+ $sEmail = $sIndividualEmail;
+ $sPasswordSalt = $sEmail;
+ $sEncodedPassword = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-password', '');
+ }
+
+ $sDecodedPassword = SnappyMailHelper::decodePassword($sEncodedPassword, md5($sPasswordSalt));
+
+ $_ENV['___snappymail_owncloud_email'] = $sEmail;
+ $_ENV['___snappymail_owncloud_password'] = $sDecodedPassword;
+ }
+
+ include $this->appPath . '/app/index.php';
+
+ }
+
+}
+
diff --git a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
new file mode 100644
index 000000000..df6107e47
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
@@ -0,0 +1,45 @@
+config = $config;
+ }
+
+ public function getForm() {
+ $keys = [
+ 'snappymail-autologin',
+ 'snappymail-autologin-with-email'
+ ];
+
+ $parameters = [];
+ foreach ($keys as $k) {
+ $v = $this->config->getAppValue('snappymail', $k);
+ $parameters[$k] = $v;
+ }
+
+ $uid = \OC::$server->getUserSession()->getUser()->getUID();
+ if (\OC_User::isAdminUser($uid)) {
+ $parameters['snappymail-admin-panel-link'] = SnappyMailHelper::getAppUrl().'?admin';
+ }
+
+ return new TemplateResponse('snappymail', 'admin-local', $parameters);
+ }
+
+ public function getSection() {
+ return 'additional';
+ }
+
+ public function getPriority() {
+ return 50;
+ }
+
+}
diff --git a/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php b/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
new file mode 100644
index 000000000..e931868a5
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
@@ -0,0 +1,32 @@
+config = $config;
+ }
+
+ public function getForm() {
+ $uid = \OC::$server->getUserSession()->getUser()->getUID();
+
+ $keys = [
+ 'snappymail-email',
+ 'snappymail-password'
+ ];
+
+ $parameters = [];
+ foreach ($keys as $k) {
+ $v = $this->config->getUserValue($uid, 'snappymail', $k);
+ $parameters[$k] = $v;
+ }
+
+ return new TemplateResponse('snappymail', 'personal_settings', $parameters, '');
+ }
+
+}
+
diff --git a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
new file mode 100644
index 000000000..9191b52f8
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
@@ -0,0 +1,341 @@
+appManager = $appManager;
+ $this->config = $config;
+ $this->userSession = $userSession;
+ $this->session = $session;
+ }
+
+ public function registerHooks() {
+ $this->userSession->listen('\OC\User', 'postLogin', function($user, $loginName, $password, $isTokenLogin) {
+ $this->login($user, $loginName, $password, $isTokenLogin, $this->config, $this->session);
+ });
+
+ $this->userSession->listen('\OC\User', 'logout', function() {
+ $this->logout($this->appManager, $this->config, $this->session);
+ });
+ }
+
+ /**
+ * @return string
+ */
+ public static function getAppUrl()
+ {
+ $sRequestUri = \OC::$server->getURLGenerator()->linkToRoute('snappymail.page.appGet');
+ if ($sRequestUri)
+ {
+ return $sRequestUri;
+ }
+
+ $sRequestUri = empty($_SERVER['REQUEST_URI']) ? '': trim($_SERVER['REQUEST_URI']);
+ $sRequestUri = preg_replace('/index.php\/.+$/', 'index.php/', $sRequestUri);
+ $sRequestUri = $sRequestUri.'apps/snappymail/app/';
+
+ return '/'.ltrim($sRequestUri, '/\\');
+ }
+
+ /**
+ * @param string $sPath
+ * @param string $sEmail
+ * @param string $sPassword
+ *
+ * @return string
+ */
+ public static function getSsoHash($sPath, $sEmail, $sPassword)
+ {
+ $SsoHash = '';
+
+ $sPath = rtrim(trim($sPath), '\\/').'/index.php';
+ if (file_exists($sPath))
+ {
+ self::regSnappyMailDataFunction();
+
+ $_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true;
+ include $sPath;
+
+ if (class_exists('\\RainLoop\\Api'))
+ {
+ $SsoHash = \RainLoop\Api::GetUserSsoHash($sEmail, $sPassword);
+ }
+ }
+
+ return $SsoHash;
+ }
+
+ /**
+ * @param string $sUrl
+ *
+ * @return string
+ */
+ public static function normalizeUrl($sUrl)
+ {
+ $sUrl = rtrim(trim($sUrl), '/\\');
+ if ('.php' !== strtolower(substr($sUrl, -4)))
+ {
+ $sUrl .= '/';
+ }
+
+ return $sUrl;
+ }
+
+ /**
+ * @return boolean
+ */
+ public static function mcryptSupported()
+ {
+ return function_exists('mcrypt_encrypt') &&
+ function_exists('mcrypt_decrypt') &&
+ defined('MCRYPT_RIJNDAEL_256') &&
+ defined('MCRYPT_MODE_ECB');
+ }
+
+ /**
+ * @return string
+ */
+ public static function openSslSupportedMethod()
+ {
+ $method = 'AES-256-CBC';
+ return function_exists('openssl_encrypt') &&
+ function_exists('openssl_decrypt') &&
+ function_exists('openssl_random_pseudo_bytes') &&
+ function_exists('openssl_cipher_iv_length') &&
+ function_exists('openssl_get_cipher_methods') &&
+ defined('OPENSSL_RAW_DATA') && defined('OPENSSL_ZERO_PADDING') &&
+ in_array($method, openssl_get_cipher_methods()) ? $method : '';
+ }
+
+ /**
+ * @param string $sMethod
+ * @param string $sPassword
+ * @param string $sSalt
+ *
+ * @return string
+ */
+ public static function encodePasswordSsl($sMethod, $sPassword, $sSalt)
+ {
+ $sData = base64_encode($sPassword);
+
+ $iv = @openssl_random_pseudo_bytes(openssl_cipher_iv_length($sMethod));
+ $r = @openssl_encrypt($sData, $sMethod, md5($sSalt), OPENSSL_RAW_DATA, $iv);
+
+ return @base64_encode(base64_encode($r).'|'.base64_encode($iv));
+ }
+
+ /**
+ * @param string $sMethod
+ * @param string $sPassword
+ * @param string $sSalt
+ *
+ * @return string
+ */
+ public static function decodePasswordSsl($sMethod, $sPassword, $sSalt)
+ {
+ $sLine = base64_decode(trim($sPassword));
+ $aParts = explode('|', $sLine, 2);
+
+ if (is_array($aParts) && !empty($aParts[0]) && !empty($aParts[1])) {
+
+ $sData = @base64_decode($aParts[0]);
+ $iv = @base64_decode($aParts[1]);
+
+ return @base64_decode(trim(
+ @openssl_decrypt($sData, $sMethod, md5($sSalt), OPENSSL_RAW_DATA, $iv)
+ ));
+ }
+
+ return '';
+ }
+
+ /**
+ * @param string $sPassword
+ * @param string $sSalt
+ *
+ * @return string
+ */
+ public static function encodePassword($sPassword, $sSalt)
+ {
+ $method = self::openSslSupportedMethod();
+ if ($method)
+ {
+ return self::encodePasswordSsl($method, $sPassword, $sSalt);
+ }
+ else if (self::mcryptSupported())
+ {
+ return @trim(base64_encode(
+ @mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5($sSalt), base64_encode($sPassword), MCRYPT_MODE_ECB)
+ ));
+ }
+
+ return @trim(base64_encode($sPassword));
+ }
+
+ /**
+ * @param string $sPassword
+ * @param string $sSalt
+ *
+ * @return string
+ */
+ public static function decodePassword($sPassword, $sSalt)
+ {
+ $method = self::openSslSupportedMethod();
+ if ($method)
+ {
+ return self::decodePasswordSsl($method, $sPassword, $sSalt);
+ }
+ else if (self::mcryptSupported())
+ {
+ return @base64_decode(trim(
+ @mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($sSalt), base64_decode(trim($sPassword)), MCRYPT_MODE_ECB)
+ ));
+ }
+
+ return @base64_decode(trim($sPassword));
+ }
+
+ /**
+ * @param array $aParams
+ * @return boolean
+ * @UseSession
+ */
+ public static function login($user, $loginName, $password, $isTokenLogin, $config, &$session)
+ {
+ $sUser = $user->getUID();
+
+ $sEmail = $sUser;
+ $sPassword = $password;
+ $sEncodedPassword = self::encodePassword($sPassword, md5($sEmail));
+
+ // Only store the user's password in the current session if they have
+ // enabled auto-login using Nextcloud username or email address.
+ if ($config->getAppValue('snappymail', 'snappymail-autologin') || $config->getAppValue('snappymail', 'snappymail-autologin-with-email')) {
+ $session['snappymail-autologin-password'] = $sEncodedPassword;
+ }
+ }
+
+ /**
+ * @UseSession
+ */
+ public static function logout($appManager, $config, &$session)
+ {
+ $session['snappymail-autologin-password'] = '';
+
+ $sApiPath = $appManager->getAppPath('snappymail') . '/app/index.php';
+
+ self::regSnappyMailDataFunction();
+
+ $_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true;
+ include $sApiPath;
+
+ \RainLoop\Api::LogoutCurrentLogginedUser();
+
+ return true;
+ }
+
+ public static function regSnappyMailDataFunction()
+ {
+ if (!@function_exists('__get_custom_data_full_path'))
+ {
+ $_ENV['SNAPPYMAIL_NEXTCLOUD'] = true;
+
+ function __get_custom_data_full_path()
+ {
+ $sData = rtrim(trim(OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/').'/';
+ return @is_dir($sData) ? $sData.'snappymail-storage' : '';
+ }
+ }
+ }
+
+ public static function mimeContentType($filename) {
+
+ $mime_types = array(
+
+ 'woff' => 'application/font-woff',
+
+ 'txt' => 'text/plain',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ 'php' => 'text/html',
+ 'css' => 'text/css',
+ 'js' => 'application/javascript',
+ 'json' => 'application/json',
+ 'xml' => 'application/xml',
+ 'swf' => 'application/x-shockwave-flash',
+ 'flv' => 'video/x-flv',
+
+ // images
+ 'png' => 'image/png',
+ 'jpe' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'bmp' => 'image/bmp',
+ 'ico' => 'image/vnd.microsoft.icon',
+ 'tiff' => 'image/tiff',
+ 'tif' => 'image/tiff',
+ 'svg' => 'image/svg+xml',
+ 'svgz' => 'image/svg+xml',
+
+ // archives
+ 'zip' => 'application/zip',
+ 'rar' => 'application/x-rar-compressed',
+ 'exe' => 'application/x-msdownload',
+ 'msi' => 'application/x-msdownload',
+ 'cab' => 'application/vnd.ms-cab-compressed',
+
+ // audio/video
+ 'mp3' => 'audio/mpeg',
+ 'qt' => 'video/quicktime',
+ 'mov' => 'video/quicktime',
+
+ // adobe
+ 'pdf' => 'application/pdf',
+ 'psd' => 'image/vnd.adobe.photoshop',
+ 'ai' => 'application/postscript',
+ 'eps' => 'application/postscript',
+ 'ps' => 'application/postscript',
+
+ // ms office
+ 'doc' => 'application/msword',
+ 'rtf' => 'application/rtf',
+ 'xls' => 'application/vnd.ms-excel',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+
+ // open office
+ 'odt' => 'application/vnd.oasis.opendocument.text',
+ 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+ );
+
+ if (0 < strpos($filename, '.'))
+ {
+ $ext = strtolower(array_pop(explode('.',$filename)));
+ if (array_key_exists($ext, $mime_types))
+ {
+ return $mime_types[$ext];
+ }
+ else if (function_exists('finfo_open'))
+ {
+ $finfo = finfo_open(FILEINFO_MIME);
+ $mimetype = finfo_file($finfo, $filename);
+ finfo_close($finfo);
+ return $mimetype;
+ }
+ }
+
+ return 'application/octet-stream';
+ }
+}
diff --git a/integrations/nextcloud/snappymail/screenshots/help_a1.png b/integrations/nextcloud/snappymail/screenshots/help_a1.png
new file mode 100644
index 000000000..bec6cbed3
Binary files /dev/null and b/integrations/nextcloud/snappymail/screenshots/help_a1.png differ
diff --git a/integrations/nextcloud/snappymail/screenshots/help_a2.png b/integrations/nextcloud/snappymail/screenshots/help_a2.png
new file mode 100644
index 000000000..0d7174f5d
Binary files /dev/null and b/integrations/nextcloud/snappymail/screenshots/help_a2.png differ
diff --git a/integrations/nextcloud/snappymail/screenshots/help_a3.png b/integrations/nextcloud/snappymail/screenshots/help_a3.png
new file mode 100644
index 000000000..77415d815
Binary files /dev/null and b/integrations/nextcloud/snappymail/screenshots/help_a3.png differ
diff --git a/integrations/nextcloud/snappymail/screenshots/help_a4.png b/integrations/nextcloud/snappymail/screenshots/help_a4.png
new file mode 100644
index 000000000..ff2e62d66
Binary files /dev/null and b/integrations/nextcloud/snappymail/screenshots/help_a4.png differ
diff --git a/integrations/nextcloud/snappymail/templates/admin-local.php b/integrations/nextcloud/snappymail/templates/admin-local.php
new file mode 100755
index 000000000..c7ee3c9a8
--- /dev/null
+++ b/integrations/nextcloud/snappymail/templates/admin-local.php
@@ -0,0 +1,45 @@
+
+
+
diff --git a/integrations/nextcloud/snappymail/templates/empty.php b/integrations/nextcloud/snappymail/templates/empty.php
new file mode 100755
index 000000000..fa0c45f25
--- /dev/null
+++ b/integrations/nextcloud/snappymail/templates/empty.php
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/integrations/nextcloud/snappymail/templates/index-empty.php b/integrations/nextcloud/snappymail/templates/index-empty.php
new file mode 100755
index 000000000..e95b0cd88
--- /dev/null
+++ b/integrations/nextcloud/snappymail/templates/index-empty.php
@@ -0,0 +1,3 @@
+
+ t('SnappyMail Webmail is not configured yet.')); ?>
+
diff --git a/integrations/nextcloud/snappymail/templates/index.php b/integrations/nextcloud/snappymail/templates/index.php
new file mode 100755
index 000000000..fcdd98db2
--- /dev/null
+++ b/integrations/nextcloud/snappymail/templates/index.php
@@ -0,0 +1 @@
+getContainer()->query(PersonalSettings::class);
+return $controller->getForm()->render();
+
diff --git a/integrations/nextcloud/snappymail/templates/personal_settings.php b/integrations/nextcloud/snappymail/templates/personal_settings.php
new file mode 100755
index 000000000..ec5ff5026
--- /dev/null
+++ b/integrations/nextcloud/snappymail/templates/personal_settings.php
@@ -0,0 +1,32 @@
+t('may have some');
+$textLink2 = $l->t('security considerations');
+$links = [
+ "{$textLink1} ",
+ "{$textLink2} "
+ ]; ?>
+
+
diff --git a/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/OwnCloudSuggestions.php b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/OwnCloudSuggestions.php
new file mode 100644
index 000000000..a99e9052e
--- /dev/null
+++ b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/OwnCloudSuggestions.php
@@ -0,0 +1,107 @@
+getContactsManager();
+ if (!$cm && !$cm->isEnabled())
+ {
+ return $aResult;
+ }
+
+ $aSearchResult = $cm->search($sQuery, $aParams);
+ }
+ else if (\class_exists('OCP\Contacts') && \OCP\Contacts::isEnabled())
+ {
+ $aSearchResult = \OCP\Contacts::search($sQuery, $aParams);
+ }
+ else
+ {
+ return $aResult;
+ }
+
+ //$this->oLogger->WriteDump($aSearchResult);
+
+ $aHashes = array();
+ if (\is_array($aSearchResult) && 0 < \count($aSearchResult))
+ {
+ foreach ($aSearchResult as $aContact)
+ {
+ if (0 >= $iLimit)
+ {
+ break;
+ }
+
+ $sUid = empty($aContact['UID']) ? '' : $aContact['UID'];
+ if (!empty($sUid))
+ {
+ $mEmails = isset($aContact['EMAIL']) ? $aContact['EMAIL'] : '';
+
+ $sFullName = isset($aContact['FN']) ? \trim($aContact['FN']) : '';
+ if (empty($sFullName))
+ {
+ $sFullName = isset($aContact['NICKNAME']) ? \trim($aContact['NICKNAME']) : '';
+ }
+
+ if (!\is_array($mEmails))
+ {
+ $mEmails = array($mEmails);
+ }
+
+ foreach ($mEmails as $sEmail)
+ {
+ $sHash = '"'.$sFullName.'" <'.$sEmail.'>';
+ if (!isset($aHashes[$sHash]))
+ {
+ $aHashes[$sHash] = true;
+ $aResult[] = array($sEmail, $sFullName);
+ $iLimit--;
+ }
+ }
+ }
+ }
+
+ $aResult = \array_slice($aResult, 0, $iInputLimit);
+ }
+
+ unset($aSearchResult, $aHashes);
+ }
+ catch (\Throwable $oException)
+ {
+ if ($this->oLogger)
+ {
+ $this->oLogger->WriteException($oException);
+ }
+ }
+
+ return $aResult;
+ }
+
+ /**
+ * @param \MailSo\Log\Logger $oLogger
+ */
+ public function SetLogger($oLogger)
+ {
+ $this->oLogger = $oLogger instanceof \MailSo\Log\Logger ? $oLogger : null;
+ }
+}
diff --git a/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/index.php b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/index.php
new file mode 100644
index 000000000..f50e99e43
--- /dev/null
+++ b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/index.php
@@ -0,0 +1,180 @@
+addHook('main.fabrica', 'MainFabrica');
+ $this->addHook('filter.app-data', 'FilterAppData');
+ $this->addHook('json.attachments', 'DoAttachmentsActions');
+
+ $sAppPath = '';
+ if (\class_exists('OC_App')) {
+ $sAppPath = \rtrim(\trim(\OC_App::getAppWebPath('snappymail')), '\\/').'/app/';
+ }
+ if (!$sAppPath) {
+ $sUrl = \MailSo\Base\Http::SingletonInstance()->GetUrl();
+ if ($sUrl && \preg_match('/\/index\.php\/apps\/snappymail/', $sUrl)) {
+ $sAppPath = \preg_replace('/\/index\.php\/apps\/snappymail.+$/',
+ '/apps/snappymail/app/', $sUrl);
+ }
+ }
+ $_SERVER['SCRIPT_NAME'] = $sAppPath;
+ }
+ }
+
+ public function Supported() : string
+ {
+ if (!static::IsOwnCloud()) {
+ return 'OwnCloud not found to use this plugin';
+ }
+ return '';
+ }
+
+ // DoAttachmentsActions
+ public function DoAttachmentsActions(\SnappyMail\AttachmentsAction $data)
+ {
+ if ('owncloud' === $data->action) {
+ if (static::IsOwnCloudLoggedIn() && \class_exists('OCP\Files')) {
+ $oFiles = \OCP\Files::getStorage('files');
+ if ($oFiles && $data->filesProvider->IsActive() && \method_exists($oFiles, 'file_put_contents')) {
+ $sSaveFolder = $this->Config()->Get('plugin', 'save_folder', '') ?: 'Attachments';
+ $oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder);
+ $data->result = true;
+ foreach ($data->items as $aItem) {
+ $sSavedFileName = isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat';
+ $sSavedFileHash = !empty($aItem['FileHash']) ? $aItem['FileHash'] : '';
+ if (!empty($sSavedFileHash)) {
+ $fFile = $data->filesProvider->GetFile($data->account, $sSavedFileHash, 'rb');
+ if (\is_resource($fFile)) {
+ $sSavedFileNameFull = \MailSo\Base\Utils::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, function ($sPath) use ($oFiles) {
+ return $oFiles->file_exists($sPath);
+ });
+
+ if (!$oFiles->file_put_contents($sSavedFileNameFull, $fFile)) {
+ $data->result = false;
+ }
+
+ if (\is_resource($fFile)) {
+ \fclose($fFile);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($data->items as $aItem) {
+ $sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : '');
+ if (!empty($sFileHash)) {
+ $data->filesProvider->Clear($data->account, $sFileHash);
+ }
+ }
+ }
+ }
+
+ /**
+ * TODO: create pre-login auth hook
+ */
+ public function ServiceOwnCloudAuth()
+ {
+/*
+ $this->oHttp->ServerNoCache();
+
+ if (!static::IsOwnCloud() ||
+ !isset($_ENV['___snappymail_owncloud_email']) ||
+ !isset($_ENV['___snappymail_owncloud_password']) ||
+ empty($_ENV['___snappymail_owncloud_email'])
+ )
+ {
+ $this->oActions->SetAuthLogoutToken();
+ $this->oActions->Location('./');
+ return '';
+ }
+
+ $bLogout = true;
+
+ $sEmail = $_ENV['___snappymail_owncloud_email'];
+ $sPassword = $_ENV['___snappymail_owncloud_password'];
+
+ try
+ {
+ $oAccount = $this->oActions->LoginProcess($sEmail, $sPassword);
+ $this->oActions->AuthToken($oAccount);
+
+ $bLogout = !($oAccount instanceof \snappymail\Model\Account);
+ }
+ catch (\Exception $oException)
+ {
+ $this->oActions->Logger()->WriteException($oException);
+ }
+
+ if ($bLogout)
+ {
+ $this->oActions->SetAuthLogoutToken();
+ }
+
+ $this->oActions->Location('./');
+ return '';
+*/
+ }
+
+ /**
+ * @return void
+ */
+ public function FilterAppData($bAdmin, &$aResult)
+ {
+ if (!$bAdmin && \is_array($aResult) && static::IsOwnCloud()) {
+ $key = \array_search(\RainLoop\Enumerations\Capa::AUTOLOGOUT, $aResult['Capa']);
+ if (false !== $key) {
+ unset($aResult['Capa'][$key]);
+ }
+ if (static::IsOwnCloudLoggedIn() && \class_exists('OCP\Files')) {
+ $aResult['System']['attachmentsActions'][] = 'owncloud';
+ }
+ }
+ }
+
+ /**
+ * @param string $sName
+ * @param mixed $mResult
+ */
+ public function MainFabrica($sName, &$mResult)
+ {
+ if ('suggestions' === $sName && static::IsOwnCloud() && $this->Config()->Get('plugin', 'suggestions', true)) {
+ include_once __DIR__.'/OwnCloudSuggestions.php';
+ if (!\is_array($mResult)) {
+ $mResult = array();
+ }
+ $mResult[] = new OwnCloudSuggestions();
+ }
+ }
+
+ protected function configMapping() : array
+ {
+ return array(
+ \RainLoop\Plugins\Property::NewInstance('save_folder')->SetLabel('Save Folder')
+ ->SetDefaultValue('Attachments'),
+ \RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(true)
+ );
+ }
+}
diff --git a/integrations/virtualmin/snappymail.pl b/integrations/virtualmin/snappymail.pl
new file mode 100644
index 000000000..30c0bad82
--- /dev/null
+++ b/integrations/virtualmin/snappymail.pl
@@ -0,0 +1,405 @@
+# This script is probably broken and not tested!
+# https://forum.virtualmin.com/t/add-snappymail-to-install-scripts/112560
+
+# script_snappymail_desc()
+sub script_snappymail_desc
+{
+return "SnappyMail";
+}
+
+sub script_snappymail_uses
+{
+return ( "php" );
+}
+
+sub script_snappymail_longdesc
+{
+return "SnappyMail Webmail is a browser-based multilingual IMAP client with an application-like user interface";
+}
+
+# script_snappymail_versions()
+sub script_snappymail_versions
+{
+return ( "2.13.4" );
+}
+
+sub script_snappymail_version_desc
+{
+local ($ver) = @_;
+return &compare_versions($ver, "2.7") >= 0 ? "$ver" : "$ver (Un-supported)";
+}
+
+sub script_snappymail_category
+{
+return "Email";
+}
+
+sub script_snappymail_php_modules
+{
+local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
+local @modules = ( "zlib", "mbstring" );
+push(@modules, $dbtype eq "mysql" ? "pdo_mysql" : $dbtype eq "sqlite" ? "pdo_sqlite" : "pdo_pgsql");
+return @modules;
+}
+
+sub script_snappymail_php_optional_modules
+{
+return ( "gd", "openssl", "sockets", "xxtea", "curl", "intl", "ldap", "zip", "gmagick", "imagick" );
+}
+
+sub script_snappymail_dbs
+{
+return ("mysql", "postgres", "sqlite");
+}
+
+# script_snappymail_php_vars(&domain)
+# Returns an array of extra PHP variables needed for this script
+sub script_snappymail_php_vars
+{
+return ([ 'memory_limit', '64M', '+' ],
+ [ 'max_execution_time', 300, '+' ],
+ [ 'file_uploads', 'On' ],
+ [ 'upload_max_filesize', '25M', '+' ],
+ [ 'post_max_size', '25M', '+' ],
+ [ 'session.auto_start', 'Off' ],
+ [ 'mbstring.func_overload', 'Off' ]);
+}
+
+
+sub script_snappymail_php_vers
+{
+return ( 7 );
+}
+
+sub script_snappymail_release
+{
+return 3; # For folders path fix
+}
+
+sub script_snappymail_php_fullver
+{
+return "7.4";
+}
+
+# script_snappymail_params(&domain, version, &upgrade-info)
+# Returns HTML for table rows for options for installing PHP-NUKE
+sub script_snappymail_params
+{
+local ($d, $ver, $upgrade) = @_;
+local $rv;
+local $hdir = &public_html_dir($d, 1);
+if ($upgrade) {
+ # Options are fixed when upgrading
+ local ($dbtype, $dbname) = split(/_/, $upgrade->{'opts'}->{'db'}, 2);
+ $rv .= &ui_table_row("Database for SnappyMail preferences", $dbname);
+ local $dir = $upgrade->{'opts'}->{'dir'};
+ $dir =~ s/^$d->{'home'}\///;
+ $rv .= &ui_table_row("Install directory", $dir);
+ }
+else {
+ # Show editable install options
+ local @dbs = &domain_databases($d, [ "mysql", "postgres", "sqlite" ]);
+ $rv .= &ui_table_row("Database for SnappyMail preferences",
+ &ui_database_select("db", undef, \@dbs, $d, "snappymail"));
+ $rv .= &ui_table_row("Install sub-directory under $hdir ",
+ &ui_opt_textbox("dir", &substitute_scriptname_template("snappymail", $d), 30, "At top level"));
+ }
+return $rv;
+}
+
+# script_snappymail_parse(&domain, version, &in, &upgrade-info)
+# Returns either a hash ref of parsed options, or an error string
+sub script_snappymail_parse
+{
+local ($d, $ver, $in, $upgrade) = @_;
+if ($upgrade) {
+ # Options are always the same
+ return $upgrade->{'opts'};
+ }
+else {
+ local $hdir = &public_html_dir($d, 0);
+ $in{'dir_def'} || $in{'dir'} =~ /\S/ && $in{'dir'} !~ /\.\./ ||
+ return "Missing or invalid installation directory";
+ local $dir = $in{'dir_def'} ? $hdir : "$hdir/$in{'dir'}";
+ local ($newdb) = ($in->{'db'} =~ s/^\*//);
+ return { 'db' => $in->{'db'},
+ 'newdb' => $newdb,
+ 'dir' => $dir,
+ 'path' => $in{'dir_def'} ? "/" : "/$in{'dir'}", };
+ }
+}
+
+# script_snappymail_check(&domain, version, &opts, &upgrade-info)
+# Returns an error message if a required option is missing or invalid
+sub script_snappymail_check
+{
+local ($d, $ver, $opts, $upgrade) = @_;
+$opts->{'dir'} =~ /^\// || return "Missing or invalid install directory";
+$opts->{'db'} || return "Missing database";
+if (-r "$opts->{'dir'}/config/db.inc.php") {
+ return "SnappyMail appears to be already installed in the selected directory";
+ }
+local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
+local $clash = &find_database_table($dbtype, $dbname, "system|filestore|contacts|users");
+$clash && return "SnappyMail appears to be already using the selected database (table $clash)";
+return undef;
+}
+
+# script_snappymail_files(&domain, version, &opts, &upgrade-info)
+# Returns a list of files needed by SnappyMail, each of which is a hash ref
+# containing a name, filename and URL
+sub script_snappymail_files
+{
+local ($d, $ver, $opts, $upgrade) = @_;
+local @files = ( { 'name' => "source",
+ 'file' => "snappymail-$ver.tar.gz",
+ 'url' => "https://github.com/the-djmaze/snappymail/releases/download/v${ver}/snappymail-${ver}.tar.gz" },
+ );
+return @files;
+}
+
+sub script_snappymail_commands
+{
+return ("tar", "gunzip");
+}
+
+# script_snappymail_install(&domain, version, &opts, &files, &upgrade-info)
+# Actually installs SnappyMail, and returns either 1 and an informational
+# message, or 0 and an error
+sub script_snappymail_install
+{
+local ($d, $version, $opts, $files, $upgrade) = @_;
+local ($out, $ex);
+
+# Create and get DB
+if ($opts->{'newdb'} && !$upgrade) {
+ local $err = &create_script_database($d, $opts->{'db'});
+ return (0, "Database creation failed : $err") if ($err);
+ }
+local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
+local $dbuser = $dbtype eq "mysql" ? &mysql_user($d) : &postgres_user($d);
+local $dbpass = $dbtype eq "mysql" ? &mysql_pass($d) : &postgres_pass($d, 1);
+local $dbphptype = $dbtype eq "mysql" ? "mysql" : "psql";
+local $dbhost = &get_database_host($dbtype, $d);
+local $dberr = &check_script_db_connection($dbtype, $dbname, $dbuser, $dbpass);
+return (0, "Database connection failed : $dberr") if ($dberr);
+
+# Extract tar file to temp dir and copy to target
+local $temp = &transname();
+local $verdir = $ver;
+$verdir =~ s/-complete$//;
+local $err = &extract_script_archive($files->{'source'}, $temp, $d,
+ $opts->{'dir'}, "snappymail-$verdir");
+$err && return (0, "Failed to extract source : $err");
+
+if (!$upgrade) {
+ # Fix up the DB config file
+ local $dbcfileorig = "$opts->{'dir'}/config/db.inc.php.dist";
+ local $dbcfile = "$opts->{'dir'}/config/db.inc.php";
+ if (-r $dbcfileorig) {
+ ©_source_dest_as_domain_user($d, $dbcfileorig, $dbcfile);
+ local $lref = &read_file_lines_as_domain_user($d, $dbcfile);
+ foreach my $l (@$lref) {
+ if ($l =~ /^\$rcmail_config\['db_dsnw'\]\s+=/) {
+ $l = "\$rcmail_config['db_dsnw'] = 'mysql://$dbuser:".
+ &php_quotemeta($dbpass, 1).
+ "\@$dbhost/$dbname';";
+ }
+ elsif ($l =~ /^\$rcmail_config\['db_backend'\]\s+=/) {
+ $l = "\$rcmail_config['db_backend'] = 'db';";
+ }
+ }
+ &flush_file_lines_as_domain_user($d, $dbcfile);
+ }
+
+ # Figure out folder names
+ local %fmap;
+ $fmap{'drafts'} = $config{'drafts_folder'} || 'drafts';
+ $fmap{'sent'} = $config{'sent_folder'} || 'sent';
+ $fmap{'trash'} = $config{'trash_folder'} || 'sent';
+ local ($sdmode, $sdpath) = &get_domain_spam_delivery($d);
+ if (($sdmode == 6 || $sdmode == 4) && $sdpath) {
+ $fmap{'junk'} = $sdpath;
+ }
+ elsif ($sdmode == 1 && $sdpath =~ /^Maildir\/\.?(\S+)\/$/) {
+ $fmap{'junk'} = $1;
+ }
+
+ # Fix up the main config file
+ local $mcfileorig = "$opts->{'dir'}/config/main.inc.php.dist";
+ local $mcfile = "$opts->{'dir'}/config/main.inc.php";
+ if (!-r $mcfileorig) {
+ $mcfileorig = "$opts->{'dir'}/config/config.inc.php.sample";
+ $mcfile = "$opts->{'dir'}/config/config.inc.php";
+ }
+ ©_source_dest_as_domain_user($d, $mcfileorig, $mcfile);
+ local $lref = &read_file_lines_as_domain_user($d, $mcfile);
+ local $vuf = &get_mail_virtusertable();
+ local $added_vuf = 0;
+ foreach my $l (@$lref) {
+ if ($l =~ /^\$(rcmail_config|config)\['enable_caching'\]\s+=/) {
+ $l = "\$${1}['enable_caching'] = FALSE;";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['default_host'\]\s+=/) {
+ $l = "\$${1}['default_host'] = 'localhost';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['default_port'\]\s+=/) {
+ $l = "\$${1}['default_port'] = 143;";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['smtp_server'\]\s+=/) {
+ $l = "\$${1}['smtp_server'] = 'localhost';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['smtp_port'\]\s+=/) {
+ $l = "\$${1}['smtp_port'] = 25;";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['smtp_user'\]\s+=/) {
+ $l = "\$${1}['smtp_user'] = '%u';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['smtp_pass'\]\s+=/) {
+ $l = "\$${1}['smtp_pass'] = '%p';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['mail_domain'\]\s+=/) {
+ $l = "\$${1}['mail_domain'] = '$d->{'dom'}';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['virtuser_file'\]\s+=/ && $vuf) {
+ $added_vuf = 1;
+ $l = "\$${1}['virtuser_file'] = '$vuf';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['plugins'\]\s+=\s+array\(\s*$/) {
+ $l = "\$${1}['plugins'] = array('virtuser_file',";
+ }
+ elsif ($l =~ /^\$(rcmail_config|config)\['plugins'\]\s+=/) {
+ $l = "\$${1}['plugins'] = array('virtuser_file');";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['db_dsnw'\]\s+=/) {
+ $l = "\$${1}['db_dsnw'] = 'mysql://$dbuser:".
+ &php_quotemeta($dbpass, 1)."\@$dbhost/$dbname';";
+ }
+ if ($l =~ /^\$(rcmail_config|config)\['(\S+)_mbox'\]\s+=/ &&
+ $fmap{$2} && $fmap{$2} ne "*") {
+ $l = "\$${1}['${2}_mbox'] = '$fmap{$2}';";
+ }
+ }
+ if (!$added_vuf && $vuf) {
+ # Need to add virtuser_file directive, as no default exists
+ push(@$lref, "\$rcmail_config['virtuser_file'] = '$vuf';");
+ push(@$lref, "\$config['virtuser_file'] = '$vuf';");
+ }
+ &flush_file_lines_as_domain_user($d, $mcfile);
+
+ # Run SQL setup script
+ &require_mysql();
+ local $sqlfile;
+ if ($dbtype eq "mysql") {
+ $sqlfile = "$opts->{'dir'}/SQL/mysql.initial.sql";
+ }
+ else {
+ $sqlfile = "$opts->{'dir'}/SQL/postgres.initial.sql";
+ }
+ local ($ex, $out) = &mysql::execute_sql_file($dbname, $sqlfile,
+ $dbuser, $dbpass);
+ $ex && return (-1, "Failed to run database setup script : ".
+ "$out .");
+ }
+else {
+ # Create script of upgrade SQL to run, by extracting SQL from the old
+ # version onwards from mysql.update.sql
+ &require_mysql();
+ local $sqltemp = &transname();
+ &open_tempfile(SQLTEMP, ">$sqltemp", 0, 1);
+ open(SQLIN, "<$opts->{'dir'}/SQL/mysql.update.sql");
+ local $foundver = 0;
+ while() {
+ if (/Updates\s+from\s+version\s+(\S+)/ &&
+ &compare_versions("$1", $upgrade->{'version'}) >= 0) {
+ $foundver = 1;
+ }
+ if ($foundver) {
+ &print_tempfile(SQLTEMP, $_);
+ }
+ }
+ close(SQLIN);
+ &close_tempfile(SQLTEMP);
+ if ($foundver) {
+ local ($ex, $out) = &mysql::execute_sql_file($dbname, $sqltemp,
+ $dbuser, $dbpass);
+ $ex && return (-1, "Failed to run database date script : ".
+ "$out .");
+ }
+ &unlink_file($sqltemp);
+ }
+
+# Return a URL for the user
+local $url = &script_path_url($d, $opts);
+local $rp = $opts->{'dir'};
+$rp =~ s/^$d->{'home'}\///;
+return (1, "SnappyMail installation complete. It can be accessed at $url .", "Under $rp using $dbphptype database $dbname", $url);
+}
+
+# script_wordpress_db_conn_desc()
+# Returns a list of options for config file to update
+sub script_snappymail_db_conn_desc
+{
+my $conn_desc =
+ {
+ 'replace' => [ '\$(rcmail_config|config)\[[\'"]db_dsnw[\'"]\]\s*=\s*' =>
+ '\'$$sdbtype://$$sdbuser:$$sdbpass@$$sdbhost/$$sdbname\';' ],
+ 'func' => 'php_quotemeta',
+ 'func_params' => 1,
+ 'multi' => 1,
+ };
+my $db_conn_desc =
+ { 'config/config.inc.php' =>
+ {
+ 'dbtype' => $conn_desc,
+ 'dbuser' => $conn_desc,
+ 'dbpass' => $conn_desc,
+ 'dbhost' => $conn_desc,
+ 'dbname' => $conn_desc,
+ }
+ };
+return $db_conn_desc;
+}
+
+# script_snappymail_uninstall(&domain, version, &opts)
+# Un-installs a SnappyMail installation, by deleting the directory and database.
+# Returns 1 on success and a message, or 0 on failure and an error
+sub script_snappymail_uninstall
+{
+local ($d, $version, $opts) = @_;
+
+# Remove snappymail tables from the database
+&cleanup_script_database($d, $opts->{'db'}, "(.*)");
+
+# Take out the DB
+if ($opts->{'newdb'}) {
+ &delete_script_database($d, $opts->{'db'});
+ }
+
+# Remove the contents of the target directory
+local $derr = &delete_script_install_directory($d, $opts);
+return (0, $derr) if ($derr);
+
+return (1, "SnappyMail directory and tables deleted.");
+}
+
+# script_snappymail_latest(version)
+# Returns a URL and regular expression or callback func to get the version
+sub script_snappymail_latest
+{
+local ($ver) = @_;
+return ( "https://snappymail.eu/download/",
+ "snappymail-([0-9\\.]+).tar.gz" );
+}
+
+sub script_snappymail_site
+{
+return 'https://snappymail.eu/';
+}
+
+sub script_snappymail_gpl
+{
+return 1;
+}
+
+1;
diff --git a/makedeb.sh b/makedeb.sh
deleted file mode 100755
index 23b39dcad..000000000
--- a/makedeb.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/bash
-#Build SnappyMail Webmail Debian Package
-PACKAGEVERSION="0";
-
-#Build snappymail
-gulp build;
-cd build/dist/releases/webmail;
-cd $(ls -t); #Most recent build folder
-
-#Working directory
-mkdir snappymail-deb-build;
-cd snappymail-deb-build;
-
-#Prepare zip file
-unzip ../snappymail-community-latest.zip;
-find . -type d -exec chmod 755 {} \;
-find . -type f -exec chmod 644 {} \;
-
-#Set up package directory
-VERSION=$(cat data/VERSION);
-DIR="snappymail_$VERSION-$PACKAGEVERSION";
-mkdir -m 0755 -p $DIR;
-mkdir -m 0755 -p $DIR/usr;
-mkdir -m 0755 -p $DIR/usr/share;
-mkdir -m 0755 -p $DIR/usr/share/snappymail;
-mkdir -m 0755 -p $DIR/var;
-mkdir -m 0755 -p $DIR/var/lib;
-
-#Move files into package directory
-mv snappymail $DIR/usr/share/snappymail/snappymail;
-mv index.php $DIR/usr/share/snappymail/index.php;
-mv data $DIR/var/lib/snappymail;
-
-#Update settings for Debian package
-sed -i "s/\$sCustomDataPath = '';/\$sCustomDataPath = '\/var\/lib\/snappymail';/" $DIR/usr/share/snappymail/snappymail/v/$VERSION/include.php
-
-#Set up Debian packaging tools
-cd $DIR;
-mkdir -m 0755 DEBIAN;
-
-#Create Debian packging control file
-cat >> DEBIAN/control <<-EOF
- Package: snappymail
- Version: $VERSION
- Section: web
- Priority: optional
- Architecture: all
- Depends: php7-fpm (>= 7.3), php7-curl, php7-json
- Maintainer: SnappyMail
- Installed-Size: 20330
- Description: SnappyMail Webmail
- A modern PHP webmail client.
-EOF
-
-#Create Debian packaging post-install script
-cat >> DEBIAN/postinst <<-EOF
- #!/bin/sh
- chown -R www-data:www-data /var/lib/snappymail
-EOF
-chmod +x DEBIAN/postinst;
-
-#Build Debian package
-cd ..;
-fakeroot dpkg-deb -b $DIR;
-
-#Clean up
-mv $DIR.deb ..;
-cd ..;
-rm -rf snappymail-deb-build;
diff --git a/package.json b/package.json
index 6e9a3f43e..bf64d40fe 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
- "version": "2.5.0-rc.4",
+ "version": "2.13.4",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",
@@ -43,33 +43,33 @@
"readmeFilename": "README.md",
"devDependencies": {
"babel-eslint": "10.1.0",
- "core-js": "^3.8.3",
- "eslint": "6.8.0",
+ "core-js": "3.16.3",
+ "eslint": "7.32.0",
"gulp": "4.0.2",
"gulp-cached": "1.1.1",
"gulp-clean-css": "4.3.0",
- "gulp-concat-util": "0.5.5",
"gulp-eol": "0.2.0",
"gulp-eslint": "6.0.0",
- "gulp-expect-file": "1.0.2",
- "gulp-filter": "6.0.0",
+ "gulp-expect-file": "2.0.0",
+ "gulp-filter": "7.0.0",
"gulp-header": "2.0.9",
- "gulp-less": "4.0.1",
- "gulp-livereload": "4.0.2",
+ "gulp-less": "5.0.0",
"gulp-rename": "2.0.0",
- "gulp-replace": "1.0.0",
- "gulp-rimraf": "1.0.0",
- "gulp-size": "3.0.0",
+ "gulp-replace": "1.1.3",
+ "gulp-size": "4.0.1",
"gulp-stripbom": "1.0.5",
- "gulp-terser": "^1.4.0",
- "gulp-util": "3.0.8",
- "node-fs": "0.1.7",
- "openpgp": "2.6.2",
- "rimraf": "3.0.2"
+ "gulp-terser": "2.0.1",
+ "gulp-util": "3.0.8"
},
"dependencies": {
+ "@rollup/plugin-node-resolve": "^13.1.3",
+ "@rollup/plugin-replace": "^3.0.1",
+ "del": "^6.0.0",
+ "fs": "^0.0.1-security",
+ "gulp-concat": "^2.6.1",
+ "gulp-group-css-media-queries": "^1.2.2",
"gulp-rollup-2": "^1.2.1",
- "rollup": "^2.38.0",
+ "rollup": "2.56.3",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-html": "^0.2.1",
"rollup-plugin-includepaths": "^0.2.4",
diff --git a/plugins/README.md b/plugins/README.md
index 177139cba..d430a1caa 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -1,9 +1,49 @@
-
+PHP
+```php
class Plugin extends \RainLoop\Plugins\AbstractPlugin
+{
+ public function __construct();
+ public function Name() : string;
+ public function Description() : string;
+ public function UseLangs(?bool $bLangs = null) : bool;
+ public function Supported() : string;
+ public function Init() : void;
+ public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void;
+ protected function configMapping() : array;
+}
+```
+
+JavaScript
+```javascript
+class PluginPopupView extends rl.pluginPopupView
+{
+ // Happens when DOM is created
+ onBuild(dom) {}
+
+ // Happens before showModal()
+ beforeShow(...params) {}
+ // Happens after showModal()
+ onShow(...params) {}
+ // Happens after showModal() animation transitionend
+ afterShow() {}
+
+ // Happens when user hits Escape or Close key
+ // return false to prevent closing, use close() manually
+ onClose() {}
+ // Happens before animation transitionend
+ onHide() {}
+ // Happens after animation transitionend
+ afterHide() {}
+}
+
+PluginPopupView.showModal();
+```
# Hooks
+```php
$Plugin->addHook('hook.name', 'functionName');
+```
## Login
@@ -28,48 +68,38 @@ $Plugin->addHook('hook.name', 'functionName');
## IMAP
-### imap.credentials
- params:
- \RainLoop\Model\Account $oAccount
- array &$aCredentials
-
### imap.before-connect
params:
\RainLoop\Model\Account $oAccount
- \MailSo\Mail\MailClient $oMailClient
- array $aCredentials
+ \MailSo\Imap\ImapClient $oImapClient
+ array &$aCredentials
### imap.after-connect
params:
\RainLoop\Model\Account $oAccount
- \MailSo\Mail\MailClient $oMailClient
+ \MailSo\Imap\ImapClient $oImapClient
array $aCredentials
### imap.before-login
params:
\RainLoop\Model\Account $oAccount
- \MailSo\Mail\MailClient $oMailClient
- array $aCredentials
+ \MailSo\Imap\ImapClient $oImapClient
+ array &$aCredentials
### imap.after-login
params:
\RainLoop\Model\Account $oAccount
- \MailSo\Mail\MailClient $oMailClient
+ \MailSo\Imap\ImapClient $oImapClient
bool $bSuccess
array $aCredentials
## Sieve
-### sieve.credentials
- params:
- \RainLoop\Model\Account $oAccount
- array &$aCredentials
-
### sieve.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
- array $aCredentials
+ array &$aCredentials
### sieve.after-connect
params:
@@ -81,27 +111,22 @@ $Plugin->addHook('hook.name', 'functionName');
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
- bool $bSuccess
- array $aCredentials
+ array &$aCredentials
### sieve.after-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
+ bool $bSuccess
array $aCredentials
## SMTP
-### smtp.credentials
- params:
- \RainLoop\Model\Account $oAccount
- array &$aCredentials
-
### smtp.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
- array $aCredentials
+ array &$aCredentials
### smtp.after-connect
params:
@@ -113,7 +138,7 @@ $Plugin->addHook('hook.name', 'functionName');
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
- array $aCredentials
+ array &$aCredentials
### smtp.after-login
params:
@@ -187,16 +212,17 @@ $Plugin->addHook('hook.name', 'functionName');
params:
array &$aPaths
-### filter.http-query
- params:
- string &$sQuery
-
### filter.json-response
params:
string $sAction
array &$aResponseItem
-### filter.message-html'
+### filter.message-html
+ params:
+ \RainLoop\Model\Account $oAccount
+ \MailSo\Mime\Message $oMessage
+ string &$sTextConverted
+
### filter.message-plain
params:
\RainLoop\Model\Account $oAccount
@@ -264,18 +290,13 @@ $Plugin->addHook('hook.name', 'functionName');
string $sAction
array &$aResponseItem
-### json.action-post-call
- params:
- string $sAction
- array &$aResponseItem
-
### json.action-pre-call
params:
string $sAction
-### json.action-pre-call
+### json.attachments
params:
- string $sAction
+ \SnappyMail\AttachmentsAction $oData
### json.suggestions-input-parameters
params:
@@ -297,31 +318,15 @@ $Plugin->addHook('hook.name', 'functionName');
\RainLoop\Model\Account $oAccount
int $iLimit
-### main.default-response
+### main.content-security-policy
params:
- string $sActionName
- array &$aResponseItem
+ \SnappyMail\HTTP\CSP $oCSP
### main.default-response
params:
string $sActionName
array &$aResponseItem
-### main.default-response
- params:
- string $sActionName
- array &$aResponseItem
-
-### main.default-response-data
- params:
- string $sActionName
- mixed &$mResult
-
-### main.default-response-data
- params:
- string $sActionName
- mixed &$mResult
-
### main.default-response-data
params:
string $sActionName
@@ -343,3 +348,21 @@ $Plugin->addHook('hook.name', 'functionName');
### service.app-delay-start-end
no params
+
+# JavaScript Events
+
+## mailbox
+### mailbox.message-list.selector.go-up
+### mailbox.message-list.selector.go-down
+### mailbox.message-view.toggle-full-screen
+### mailbox.inbox-unread-count
+### mailbox.message.show
+## audio
+### audio.start
+### audio.stop
+### audio.api.stop
+## Misc
+### idle
+### rl-layout
+### rl-view-model
+ event.detail = the ViewModel class
diff --git a/plugins/auto-domain-grab/LICENSE b/plugins/auto-domain-grab/LICENSE
index 325ab989b..8a36e2657 100644
--- a/plugins/auto-domain-grab/LICENSE
+++ b/plugins/auto-domain-grab/LICENSE
@@ -1,4 +1,4 @@
-This plugin, written by https://github.com/jas8522
+This extension, written by https://github.com/jas8522
and optimised by
https://github.com/rhyswilliamsza
https://github.com/rhysit
diff --git a/plugins/auto-domain-grab/README b/plugins/auto-domain-grab/README
index ad56f159e..4d7e0f495 100644
--- a/plugins/auto-domain-grab/README
+++ b/plugins/auto-domain-grab/README
@@ -1,14 +1,21 @@
What is it?
-In essense, this plugin allows for multiple users across multiple servers to use one RainLoop installation,
-all without the administrator needing to create each domain separately. This plugin detects the required
-hostname from the inputted email address (for example, it would detect "example.com" as the hostname if
-"info@example.com" is inputted). This hostname is then used for IMAP and SMTP communication.
+In essense, this extension allows multiple users across multiple servers to
+use one Snappymail installation without the administrator having to create
+and configure individual domans.
+
+This extension detects the required hostname from a given email address (for
+example, it would use "example.com" as the hostname if a user would
+log in as "info@example.com").
+
+This hostname is then used for IMAP and SMTP communication.
How to Use:
-1) Activate plugin
-2) Visit Settings, and add a new wildcard domain "*"
-3) Set your ports/ssl requirements as needed. These will not be changed by the plugin.
-4) In the SMTP and/or IMAP host fields, place the single word "auto". This will be replaced when the plugin is active.
-5) That's it! The plugin should now work!
\ No newline at end of file
+1) Activate extension
+2) Go to settings, and add a new wildcard domain "*"
+3) Set your ports/TLS/SSL configuration as needed. These settings will not be
+ altered by the extension.
+4) Set the SMTP and/or IMAP host fields to "auto", this field will then be
+ auto-configured.
+5) That's it!
diff --git a/plugins/auto-domain-grab/index.php b/plugins/auto-domain-grab/index.php
index 64a5f0ec5..58999f66b 100644
--- a/plugins/auto-domain-grab/index.php
+++ b/plugins/auto-domain-grab/index.php
@@ -1,9 +1,10 @@
addHook('smtp.credentials', 'FilterSmtpCredentials');
- $this->addHook('imap.credentials', 'FilterImapCredentials');
+ $this->addHook('smtp.before-connect', 'FilterSmtpCredentials');
+ $this->addHook('imap.before-connect', 'FilterImapCredentials');
}
/**
- * This function detects the IMAP Host, and if it is set to "auto", replaces it with the MX or email domain.
- *
- * @param \RainLoop\Model\Account $oAccount
- * @param array $aImapCredentials
+ * This function detects the IMAP Host, and if it is set to 'auto', replaces it with the MX or email domain.
*/
- public function FilterImapCredentials($oAccount, &$aImapCredentials)
+ public function FilterImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oImapClient, array &$aImapCredentials)
{
- if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aImapCredentials))
+ // Check for mail.$DOMAIN as entered value in RL settings
+ if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host'])
{
- // Check for mail.$DOMAIN as entered value in RL settings
- if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host'])
+ $domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
+ $mxhosts = array();
+ if (\getmxrr($domain, $mxhosts) && $mxhosts)
{
- $domain = substr(strrchr($oAccount->Email(), "@"), 1);
- $mxhosts = array();
- if(getmxrr($domain, $mxhosts) && sizeof($mxhosts) > 0)
- {
- $aImapCredentials['Host'] = $mxhosts[0];
- }
- else
- {
- $aImapCredentials['Host'] = $this->imap_prefix.$domain;
- }
+ $aImapCredentials['Host'] = $mxhosts[0];
+ }
+ else
+ {
+ $aImapCredentials['Host'] = $this->imap_prefix.$domain;
}
}
}
/**
- * This function detects the SMTP Host, and if it is set to "auto", replaces it with the MX or email domain.
- *
- * @param \RainLoop\Model\Account $oAccount
- * @param array $aSmtpCredentials
+ * This function detects the SMTP Host, and if it is set to 'auto', replaces it with the MX or email domain.
*/
- public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials)
+ public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, array &$aSmtpCredentials)
{
- if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials))
+ // Check for mail.$DOMAIN as entered value in RL settings
+ if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host'])
{
- // Check for mail.$DOMAIN as entered value in RL settings
- if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host'])
+ $domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
+ $mxhosts = array();
+ if (\getmxrr($domain, $mxhosts) && $mxhosts)
{
- $domain = substr(strrchr($oAccount->Email(), "@"), 1);
- $mxhosts = array();
- if(getmxrr($domain, $mxhosts) && sizeof($mxhosts) > 0)
- {
- $aSmtpCredentials['Host'] = $mxhosts[0];
- }
- else
- {
- $aSmtpCredentials['Host'] = $this->smtp_prefix.$domain;
- }
+ $aSmtpCredentials['Host'] = $mxhosts[0];
+ }
+ else
+ {
+ $aSmtpCredentials['Host'] = $this->smtp_prefix . $domain;
}
}
}
diff --git a/plugins/black-list/index.php b/plugins/black-list/index.php
index 3b6aefd63..7f692f0e4 100644
--- a/plugins/black-list/index.php
+++ b/plugins/black-list/index.php
@@ -3,10 +3,12 @@
class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
- NAME = 'Black list',
- VERSION = '2.0',
+ NAME = 'Blacklist',
+ VERSION = '2.1',
+ RELEASE = '2021-04-21',
+ REQUIRED = '2.5.0',
CATEGORY = 'Login',
- DESCRIPTION = 'Simple black list plugin (with wildcard and exceptions functionality).';
+ DESCRIPTION = 'Simple blacklist extension (with wildcard and exceptions functionality).';
public function Init() : void
{
diff --git a/plugins/change-password/drivers/ispconfig.php b/plugins/change-password/drivers/ispconfig.php
new file mode 100644
index 000000000..1cfae2681
--- /dev/null
+++ b/plugins/change-password/drivers/ispconfig.php
@@ -0,0 +1,104 @@
+oConfig = $oConfig;
+ $this->oLogger = $oLogger;
+ }
+
+ public static function isSupported() : bool
+ {
+ return \class_exists('PDO', false)
+ // The PHP extension PDO (mysql) must be installed to use this plugin
+ && \in_array('mysql', \PDO::getAvailableDrivers());
+ }
+
+ public static function configMapping() : array
+ {
+ return array(
+ \RainLoop\Plugins\Property::NewInstance('ispconfig_dsn')->SetLabel('ISPConfig PDO dsn')
+ ->SetDefaultValue('mysql:host=localhost;dbname=dbispconfig;charset=utf8'),
+ \RainLoop\Plugins\Property::NewInstance('ispconfig_user')->SetLabel('User'),
+ \RainLoop\Plugins\Property::NewInstance('ispconfig_password')->SetLabel('Password')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD),
+ \RainLoop\Plugins\Property::NewInstance('ispconfig_allowed_emails')->SetLabel('Allowed emails')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
+ ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net')
+ ->SetDefaultValue('*')
+ );
+ }
+
+ public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
+ {
+ if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'ispconfig_allowed_emails', ''))) {
+ return false;
+ }
+
+ try
+ {
+ if ($this->oLogger) {
+ $this->oLogger->Write('ISPConfig: Try to change password for '.$oAccount->Email());
+ }
+
+ $oPdo = new \PDO(
+ $this->oConfig->Get('plugin', 'ispconfig_dsn', ''),
+ $this->oConfig->Get('plugin', 'ispconfig_user', ''),
+ $this->oConfig->Get('plugin', 'ispconfig_password', ''),
+ array(
+ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
+ )
+ );
+
+ $oStmt = $oPdo->prepare('SELECT password, mailuser_id FROM mail_user WHERE login = ? LIMIT 1');
+ if ($oStmt->execute(array($oAccount->IncLogin()))) {
+ $aFetchResult = $oStmt->fetch(\PDO::FETCH_ASSOC);
+ if (!empty($aFetchResult['mailuser_id'])) {
+ $sDbPassword = $aFetchResult['password'];
+ $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
+ if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
+ $oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?');
+ return !!$oStmt->execute(array(
+ $this->cryptPassword($sNewPassword),
+ $aFetchResult['mailuser_id']
+ ));
+ }
+ }
+ }
+ }
+ catch (\Exception $oException)
+ {
+ if ($this->oLogger) {
+ $this->oLogger->WriteException($oException);
+ }
+ }
+ return false;
+ }
+
+ private function cryptPassword(string $sPassword) : string
+ {
+ if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
+ $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
+ } elseif (\defined('CRYPT_SHA256') && CRYPT_SHA256) {
+ $sSalt = '$5$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
+ } else {
+ $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
+ }
+ return \crypt($sPassword, $sSalt);
+ }
+}
diff --git a/plugins/change-password/drivers/ldap.php b/plugins/change-password/drivers/ldap.php
index f021c27be..e1fc347c4 100644
--- a/plugins/change-password/drivers/ldap.php
+++ b/plugins/change-password/drivers/ldap.php
@@ -7,8 +7,8 @@ class ChangePasswordDriverLDAP
DESCRIPTION = 'Change passwords in LDAP.';
private
- $sHostName = 'localhost',
- $iHostPort = 389,
+ $sLdapUri = 'ldap://localhost:389',
+ $bUseStartTLS = True,
$sUserDnFormat = '',
$sPasswordField = 'userPassword',
$sPasswordEncType = 'SHA';
@@ -21,8 +21,8 @@ class ChangePasswordDriverLDAP
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
{
$this->oLogger = $oLogger;
- $this->sHostName = \trim($oConfig->Get('plugin', 'ldap_hostname', ''));
- $this->iHostPort = (int) $oConfig->Get('plugin', 'ldap_port', 389);
+ $this->sLdapUri = \trim($oConfig->Get('plugin', 'ldap_uri', ''));
+ $this->bUseStartTLS = (bool) \trim($oConfig->Get('plugin', 'ldap_use_start_tls', ''));
$this->sUserDnFormat = \trim($oConfig->Get('plugin', 'ldap_user_dn_format', ''));
$this->sPasswordField = \trim($oConfig->Get('plugin', 'ldap_password_field', ''));
$this->sPasswordEncType = \trim($oConfig->Get('plugin', 'ldap_password_enc_type', ''));
@@ -37,11 +37,12 @@ class ChangePasswordDriverLDAP
public static function configMapping() : array
{
return array(
- \RainLoop\Plugins\Property::NewInstance('ldap_hostname')->SetLabel('Hostname')
- ->SetDefaultValue('localhost'),
- \RainLoop\Plugins\Property::NewInstance('ldap_port')->SetLabel('Port')
- ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT)
- ->SetDefaultValue(389),
+ \RainLoop\Plugins\Property::NewInstance('ldap_uri')->SetLabel('LDAP URI')
+ ->SetDefaultValue('ldap://localhost:389')
+ ->SetDescription('LDAP server URI(s), space separated'),
+ \RainLoop\Plugins\Property::NewInstance('ldap_use_start_tls')->SetLabel('Use StartTLS')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(True),
\RainLoop\Plugins\Property::NewInstance('ldap_user_dn_format')->SetLabel('User DN format')
->SetDescription('LDAP user dn format. Supported tokens: {email}, {email:user}, {email:domain}, {login}, {domain}, {domain:dc}, {imap:login}, {imap:host}, {imap:port}, {gecos}')
->SetDefaultValue('uid={imap:login},ou=Users,{domain:dc}'),
@@ -65,12 +66,12 @@ class ChangePasswordDriverLDAP
'{email:domain}' => $sDomain,
'{login}' => $oAccount->Login(),
'{imap:login}' => $oAccount->Login(),
- '{imap:host}' => $oAccount->DomainIncHost(),
- '{imap:port}' => $oAccount->DomainIncPort(),
+ '{imap:host}' => $oAccount->Domain()->IncHost(),
+ '{imap:port}' => $oAccount->Domain()->IncPort(),
'{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->Login()) : ''
));
- $oCon = \ldap_connect($this->sHostName, $this->iHostPort);
+ $oCon = \ldap_connect($this->sLdapUri);
if (!$oCon) {
return false;
}
@@ -82,8 +83,10 @@ class ChangePasswordDriverLDAP
'LDAP'
);
}
- else if (!\ldap_start_tls($oCon)) {
- $this->oLogger->Write("ldap_start_tls failed: ".$oCon, \MailSo\Log\Enumerations\Type::WARNING, 'LDAP');
+
+ if ($this->bUseStartTLS && !@\ldap_start_tls($oCon))
+ {
+ throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
}
if (!\ldap_bind($oCon, $sUserDn, $sPrevPassword)) {
diff --git a/plugins/change-password/drivers/pdo.php b/plugins/change-password/drivers/pdo.php
index f4b6f07e7..9b7078d87 100644
--- a/plugins/change-password/drivers/pdo.php
+++ b/plugins/change-password/drivers/pdo.php
@@ -6,13 +6,10 @@ class ChangePasswordDriverPDO
NAME = 'PDO',
DESCRIPTION = 'Use your own SQL (PDO) statement (with wildcards).';
- private
- $dsn = '',
- $user = '',
- $pass = '',
- $sql = '',
- $encrypt = '',
- $encrypt_prefix = ''; // Like: {ARGON2I} {BLF-CRYPT} {SHA512-CRYPT}
+ /**
+ * @var \RainLoop\Config\Plugin
+ */
+ private $oConfig = null;
/**
* @var \MailSo\Log\Logger
@@ -21,13 +18,8 @@ class ChangePasswordDriverPDO
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
{
+ $this->oConfig = $oConfig;
$this->oLogger = $oLogger;
- $this->dsn = $oConfig->Get('plugin', 'pdo_dsn', '');
- $this->user = $oConfig->Get('plugin', 'pdo_user', '');
- $this->pass = $oConfig->Get('plugin', 'pdo_password', '');
- $this->sql = $oConfig->Get('plugin', 'pdo_sql', '');
- $this->encrypt = $oConfig->Get('plugin', 'pdo_encrypt', '');
- $this->encrypt_prefix = $oConfig->Get('plugin', 'pdo_encryptprefix', '');
}
public static function isSupported() : bool
@@ -52,43 +44,73 @@ class ChangePasswordDriverPDO
->SetDefaultValue(array('none', 'bcrypt', 'Argon2i', 'Argon2id', 'SHA256-CRYPT', 'SHA512-CRYPT'))
->SetDescription('In what way do you want the passwords to be encrypted?'),
\RainLoop\Plugins\Property::NewInstance('pdo_encryptprefix')->SetLabel('Encrypt prefix')
- ->SetDescription('Optional encrypted password prefix, like: {BLF-CRYPT}'),
+ ->SetDescription('Optional encrypted password prefix, like {ARGON2I} or {BLF-CRYPT} or {SHA512-CRYPT}'),
+ \RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl')->SetLabel('MySQL SSL connection')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(false),
+ \RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl_verify')->SetLabel('MySQL SSL verify server cert')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDescription('Verify that certificate\'s Common Name of SAN matches the database server\'s hostname.')
+ ->SetDefaultValue(false),
+ \RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl_ca')->SetLabel('MySQL SSL CA certificate file')
+ ->SetDescription('Path to a file containing the CA certificate used to sign the server certificate, or a CA bundle. Required for SSL/TLS connections to work.')
+ ->SetDefaultValue('/etc/pki/tls/certs/ca-bundle.crt')
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
{
- $options = array(
- \PDO::ATTR_EMULATE_PREPARES => true,
- \PDO::ATTR_PERSISTENT => true,
- \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
- );
+ try
+ {
+ $pdo_attr = array(
+ \PDO::ATTR_EMULATE_PREPARES => true,
+ \PDO::ATTR_PERSISTENT => true,
+ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
+ );
+ if ($this->oConfig->Get('plugin', 'pdo_mysql_ssl', false)) {
+ $pdo_attr[\PDO::MYSQL_ATTR_SSL_CA] = $this->oConfig->Get('plugin', 'pdo_mysql_ssl_ca', '');
+ $pdo_attr[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->oConfig->Get('plugin', 'pdo_mysql_ssl_verify', true);
+ }
- $conn = new \PDO($this->dsn, $this->user, $this->pass, $options);
+ $conn = new \PDO(
+ $this->oConfig->Get('plugin', 'pdo_dsn', ''),
+ $this->oConfig->Get('plugin', 'pdo_user', ''),
+ $this->oConfig->Get('plugin', 'pdo_password', ''),
+ $pdo_attr
+ );
- //prepare SQL varaibles
- $sEmail = $oAccount->Email();
- $sEmailUser = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
- $sEmailDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail);
+ $sEmail = $oAccount->Email();
+ $encrypt = $this->oConfig->Get('plugin', 'pdo_encrypt', '');
+ $encrypt_prefix = $this->oConfig->Get('plugin', 'pdo_encryptprefix', '');
- $placeholders = array(
- ':email' => $sEmail,
- ':oldpass' => $this->encrypt_prefix . \ChangePasswordPlugin::encrypt($this->encrypt, $sPrevPassword),
- ':newpass' => $this->encrypt_prefix . \ChangePasswordPlugin::encrypt($this->encrypt, $sNewPassword),
- ':domain' => $sEmailDomain,
- ':username' => $sEmailUser
- );
+ $placeholders = array(
+ ':email' => $sEmail,
+ ':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sPrevPassword),
+ ':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sNewPassword),
+ ':domain' => \MailSo\Base\Utils::GetDomainFromEmail($sEmail),
+ ':username' => \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail)
+ );
- $statement = $conn->prepare($this->sql);
+ $sql = $this->oConfig->Get('plugin', 'pdo_sql', '');
- // we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error
- foreach ($placeholders as $placeholder => $value) {
- if (\preg_match_all("/{$placeholder}(?![a-zA-Z0-9\-])/", $this->sql)) {
- $statement->bindValue($placeholder, $value);
+ $statement = $conn->prepare($sql);
+
+ // we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error
+ foreach ($placeholders as $placeholder => $value) {
+ if (\preg_match_all("/{$placeholder}(?![a-zA-Z0-9\-])/", $sql)) {
+ $statement->bindValue($placeholder, $value);
+ }
+ }
+
+ // and execute
+ return !!$statement->execute();
+ }
+ catch (\Exception $oException)
+ {
+ if ($this->oLogger) {
+ $this->oLogger->WriteException($oException);
}
}
-
- // and execute
- return !!$statement->execute();
+ return false;
}
}
diff --git a/plugins/change-password/index.php b/plugins/change-password/index.php
index eebd684a1..4fc0eb724 100644
--- a/plugins/change-password/index.php
+++ b/plugins/change-password/index.php
@@ -6,11 +6,11 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password',
- VERSION = '2.1',
- RELEASE = '2021-03-18',
- REQUIRED = '2.4.0',
+ VERSION = '2.13.1',
+ RELEASE = '2022-03-10',
+ REQUIRED = '2.12.0',
CATEGORY = 'Security',
- DESCRIPTION = 'This plugin allows you to change passwords of email accounts';
+ DESCRIPTION = 'Extension to allow users to change their passwords';
// \RainLoop\Notifications\
const
@@ -23,6 +23,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
{
$this->UseLangs(true); // start use langs folder
+// $this->addCss('style.css');
$this->addJs('js/ChangePasswordUserSettings.js'); // add js file
$this->addJsonHook('ChangePassword', 'ChangePassword');
$this->addTemplate('templates/SettingsChangePassword.html');
@@ -95,16 +96,21 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetDefaultValue(70),
];
foreach ($this->getSupportedDrivers(true) as $name => $class) {
- $result[] = \RainLoop\Plugins\Property::NewInstance("driver_{$name}_enabled")
- ->SetLabel('Enable ' . $class::NAME)
- ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
- ->SetDescription($class::DESCRIPTION);
- $result[] = \RainLoop\Plugins\Property::NewInstance("driver_{$name}_allowed_emails")
- ->SetLabel('Allowed emails')
- ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
- ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@example.net user2@example1.net *@example2.net')
- ->SetDefaultValue('*');
- $result = \array_merge($result, \call_user_func("{$class}::configMapping"));
+ $group = new \RainLoop\Plugins\PropertyCollection($name);
+ $props = [
+ \RainLoop\Plugins\Property::NewInstance("driver_{$name}_enabled")
+ ->SetLabel('Enable ' . $class::NAME)
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDescription($class::DESCRIPTION)
+ ->SetDefaultValue(false),
+ \RainLoop\Plugins\Property::NewInstance("driver_{$name}_allowed_emails")
+ ->SetLabel('Allowed emails')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
+ ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@example.net user2@example1.net *@example2.net')
+ ->SetDefaultValue('*')
+ ];
+ $group->exchangeArray(\array_merge($props, \call_user_func("{$class}::configMapping")));
+ $result[] = $group;
}
return $result;
}
@@ -174,7 +180,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
$oAccount->SetPassword($sNewPassword);
$oActions->SetAuthToken($oAccount);
- return $this->jsonResponse(__FUNCTION__, $oActions->GetSpecAuthToken());
+ return $this->jsonResponse(__FUNCTION__, $oActions->AppData(false));
}
public static function encrypt(string $algo, string $password)
@@ -200,7 +206,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
break;
}
- return $sPassword;
+ return $password;
}
private static function PasswordStrength(string $sPassword) : int
diff --git a/plugins/change-password/js/ChangePasswordUserSettings.js b/plugins/change-password/js/ChangePasswordUserSettings.js
index 738bd2741..156e4655a 100644
--- a/plugins/change-password/js/ChangePasswordUserSettings.js
+++ b/plugins/change-password/js/ChangePasswordUserSettings.js
@@ -110,15 +110,14 @@
// Notification.CurrentPasswordIncorrect
this.currentPasswordError(true);
}
- this.errorDescription((data && data.ErrorMessageAdditional)
+ this.errorDescription((data && rl.i18n(data.ErrorMessageAdditional))
|| rl.i18n('NOTIFICATIONS/COULD_NOT_SAVE_NEW_PASSWORD'));
} else {
this.currentPassword('');
this.newPassword('');
this.newPassword2('');
this.passwordUpdateSuccess(true);
- rl.hash.set();
- rl.settings.set('AuthAccountHash', data.Result);
+ rl.setData(data.Result);
}
}
}
diff --git a/plugins/change-password/langs/it_IT.ini b/plugins/change-password/langs/it_IT.ini
new file mode 100644
index 000000000..e3495e465
--- /dev/null
+++ b/plugins/change-password/langs/it_IT.ini
@@ -0,0 +1,12 @@
+[SETTINGS_CHANGE_PASSWORD]
+LEGEND_CHANGE_PASSWORD = "Cambia password"
+LABEL_CURRENT_PASSWORD = "Password attuale"
+LABEL_NEW_PASSWORD = "Nuova password"
+LABEL_REPEAT_PASSWORD = "Conferma nuova password"
+BUTTON_UPDATE_PASSWORD = "Imposta nuova password"
+ERROR_PASSWORD_MISMATCH = "Le password non corrispondono, riprova"
+[NOTIFICATIONS]
+COULD_NOT_SAVE_NEW_PASSWORD = "Non è stato possibile salvare la nuova password"
+CURRENT_PASSWORD_INCORRECT = "La password attuale non è corretta"
+NEW_PASSWORD_SHORT = "La password scelta è troppo breve"
+NEW_PASSWORD_WEAK = "La password scelta non è abbastanza complessa"
diff --git a/plugins/change-password/style.css b/plugins/change-password/style.css
new file mode 100644
index 000000000..09788f981
--- /dev/null
+++ b/plugins/change-password/style.css
@@ -0,0 +1,5 @@
+.form-horizontal.change-password .control-group {
+ .control-label {
+ width: 160px;
+ }
+}
diff --git a/plugins/change-password/templates/SettingsChangePassword.html b/plugins/change-password/templates/SettingsChangePassword.html
index f16b1b7fe..ac0e62d20 100644
--- a/plugins/change-password/templates/SettingsChangePassword.html
+++ b/plugins/change-password/templates/SettingsChangePassword.html
@@ -1,5 +1,5 @@