diff --git a/dev/App/User.js b/dev/App/User.js index 2d0802b08..ed016196d 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -1,6 +1,6 @@ import 'External/User/ko'; -import { isArray, isNonEmptyArray, pInt, pString } from 'Common/Utils'; +import { isArray, arrayLength, pInt, pString } from 'Common/Utils'; import { isPosNumeric, delegateRunOnDestroy, mailToHelper } from 'Common/UtilsUser'; import { @@ -261,7 +261,7 @@ class AppUser extends AbstractApp { setFolderHash(FolderUserStore.currentFolderFullNameRaw(), ''); alert(getNotification(iError)); } else if (FolderUserStore.currentFolder()) { - if (isArray(oData.Result) && 2 === oData.Result.length) { + if (2 === arrayLength(oData.Result)) { setFolderHash(oData.Result[0], oData.Result[1]); } else { setFolderHash(FolderUserStore.currentFolderFullNameRaw(), ''); @@ -348,7 +348,7 @@ class AppUser extends AbstractApp { * @param {boolean=} bCopy = false */ moveMessagesToFolder(sFromFolderFullNameRaw, aUidForMove, sToFolderFullNameRaw, bCopy) { - if (sFromFolderFullNameRaw !== sToFolderFullNameRaw && isArray(aUidForMove) && aUidForMove.length) { + if (sFromFolderFullNameRaw !== sToFolderFullNameRaw && arrayLength(aUidForMove)) { const oFromFolder = getFolderFromCacheList(sFromFolderFullNameRaw), oToFolder = getFolderFromCacheList(sToFolderFullNameRaw); @@ -519,10 +519,9 @@ class AppUser extends AbstractApp { Remote.quota((iError, data) => { if ( !iError && - isArray(data.Result) && - 1 < data.Result.length && - isPosNumeric(data.Result[0], true) && - isPosNumeric(data.Result[1], true) + 1 < arrayLength(data.Result) && + isPosNumeric(data.Result[0]) && + isPosNumeric(data.Result[1]) ) { QuotaUserStore.populateData(pInt(data.Result[1]), pInt(data.Result[0])); } @@ -614,9 +613,9 @@ class AppUser extends AbstractApp { */ folderInformationMultiply(boot = false) { const folders = FolderUserStore.getNextFolderNames(refreshFolders); - if (isNonEmptyArray(folders)) { + if (arrayLength(folders)) { Remote.folderInformationMultiply((iError, oData) => { - if (!iError && isNonEmptyArray(oData.Result.List)) { + if (!iError && arrayLength(oData.Result.List)) { const utc = Date.now(); oData.Result.List.forEach(item => { const hash = getFolderHash(item.Folder), diff --git a/dev/Common/Cache.js b/dev/Common/Cache.js index 2a65a11cc..bde23bd7d 100644 --- a/dev/Common/Cache.js +++ b/dev/Common/Cache.js @@ -1,5 +1,5 @@ import { MessageSetAction } from 'Common/EnumsUser'; -import { isNonEmptyArray, pInt } from 'Common/Utils'; +import { arrayLength, pInt } from 'Common/Utils'; let FOLDERS_CACHE = {}, FOLDERS_NAME_CACHE = {}, @@ -255,7 +255,7 @@ export class MessageFlagsCache * @param {Array} flags */ static storeByFolderAndUid(folder, uid, flags) { - if (isNonEmptyArray(flags)) { + if (arrayLength(flags)) { this.setFor(folder, uid, flags); } } @@ -269,7 +269,7 @@ export class MessageFlagsCache let unread = 0; const flags = this.getFor(folder, uid); - if (isNonEmptyArray(flags)) { + if (arrayLength(flags)) { if (flags[0]) { unread = 1; } diff --git a/dev/Common/File.js b/dev/Common/File.js index 28bc2943f..19d194b05 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 = {}, @@ -250,7 +250,7 @@ export const FileInfo = { * @returns {string} */ getCombinedIconClass: data => { - if (isNonEmptyArray(data)) { + if (arrayLength(data)) { let icons = data .map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '') .validUnique(); diff --git a/dev/Common/Globals.js b/dev/Common/Globals.js index a2653cfc3..00e6eee77 100644 --- a/dev/Common/Globals.js +++ b/dev/Common/Globals.js @@ -40,19 +40,18 @@ export const keyScope = (()=>{ keyScope(keyScopeFake); } }); - return ko.computed({ - read: () => keyScopeFake, - write: value => { + return value => { + if (value) { if (Scope.Menu !== value) { keyScopeFake = value; if (dropdownVisibility()) { value = Scope.Menu; } } - keyScopeReal(value); + shortcuts.setScope(value); + } else { + return keyScopeFake; } - }); + }; })(); - -keyScopeReal.subscribe(value => shortcuts.setScope(value)); diff --git a/dev/Common/Utils.js b/dev/Common/Utils.js index 359d92707..6c7aa0e31 100644 --- a/dev/Common/Utils.js +++ b/dev/Common/Utils.js @@ -3,7 +3,7 @@ import { doc, elementById } from 'Common/Globals'; export const isArray = Array.isArray, - isNonEmptyArray = array => isArray(array) && array.length, + arrayLength = array => isArray(array) && array.length, isFunction = v => typeof v === 'function'; /** @@ -116,7 +116,7 @@ export function changeTheme(value, themeTrigger = ()=>{}) { } rl.fetchJSON(url, init) .then(data => { - if (data && isArray(data) && 2 === data.length) { + if (2 === arrayLength(data)) { themeStyle.textContent = data[1]; themeStyle.dataset.href = url; themeStyle.dataset.theme = data[0]; diff --git a/dev/Common/UtilsUser.js b/dev/Common/UtilsUser.js index 93fed0663..151e73939 100644 --- a/dev/Common/UtilsUser.js +++ b/dev/Common/UtilsUser.js @@ -9,8 +9,8 @@ import { createElement } from 'Common/Globals'; * @param {boolean=} includeZero = true * @returns {boolean} */ -export function isPosNumeric(value, includeZero = true) { - return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString()); +export function isPosNumeric(value) { + return null != value && /^[0-9]*$/.test(value.toString()); } /** @@ -395,13 +395,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 @@ -434,22 +434,20 @@ export function mailToHelper(mailToUrl) { mailToUrl = mailToUrl.toString().substr(7); let to = [], - cc = null, - bcc = null, params = {}; const email = mailToUrl.replace(/\?.+$/, ''), - query = mailToUrl.replace(/^[^?]*\?/, ''); + query = mailToUrl.replace(/^[^?]*\?/, ''), + toEmailModel = value => null != value ? EmailModel.parseEmailLine(decodeURIComponent(value)) : null; 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)); + if (null != params.to) { to = Object.values( - to.reduce((result, value) => { + toEmailModel(email + ',' + params.to).reduce((result, value) => { if (value) { if (result[value.email]) { if (!result[value.email].name) { @@ -466,20 +464,12 @@ export function mailToHelper(mailToUrl) { to = EmailModel.parseEmailLine(email); } - if (undefined !== params.cc) { - cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc)); - } - - if (undefined !== params.bcc) { - bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc)); - } - showMessageComposer([ ComposeType.Empty, null, to, - cc, - bcc, + toEmailModel(params.cc), + toEmailModel(params.bcc), null == params.subject ? null : decodeURIComponent(params.subject), null == params.body ? null : plainToHtml(decodeURIComponent(params.body)) ]); diff --git a/dev/External/ko.js b/dev/External/ko.js index b776995b1..b2d37afed 100644 --- a/dev/External/ko.js +++ b/dev/External/ko.js @@ -1,7 +1,7 @@ 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 } from 'Common/Utils'; const koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value); @@ -136,7 +136,7 @@ ko.extenders.limitedList = (target, limitedList) => { const currentValue = ko.unwrap(target), list = ko.unwrap(limitedList); - if (isNonEmptyArray(list)) { + if (arrayLength(list)) { if (list.includes(newValue)) { target(newValue); } else if (list.includes(currentValue, list)) { diff --git a/dev/Knoin/AbstractScreen.js b/dev/Knoin/AbstractScreen.js index 77a781447..a5210bbf0 100644 --- a/dev/Knoin/AbstractScreen.js +++ b/dev/Knoin/AbstractScreen.js @@ -1,4 +1,4 @@ -import { isArray, isNonEmptyArray } from 'Common/Utils'; +import { isArray, arrayLength } from 'Common/Utils'; export class AbstractScreen { constructor(screenName, viewModels = []) { @@ -42,7 +42,7 @@ export class AbstractScreen { if (!this.__started) { this.__started = true; const routes = this.routes(); - if (isNonEmptyArray(routes)) { + if (arrayLength(routes)) { let route = new Crossroads(), fMatcher = (this.onRoute || (()=>{})).bind(this); diff --git a/dev/Knoin/Knoin.js b/dev/Knoin/Knoin.js index 72f3015e4..8b75f6fd8 100644 --- a/dev/Knoin/Knoin.js +++ b/dev/Knoin/Knoin.js @@ -1,7 +1,7 @@ import ko from 'ko'; import { doc, $htmlCL } from 'Common/Globals'; -import { isNonEmptyArray, isFunction } from 'Common/Utils'; +import { arrayLength, isFunction } from 'Common/Utils'; let currentScreen = null, defaultScreenName = ''; @@ -227,7 +227,7 @@ function screenOnRoute(screenName, subPart) { currentScreen.onHide && currentScreen.onHide(); currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500); - if (isNonEmptyArray(currentScreen.viewModels)) { + if (arrayLength(currentScreen.viewModels)) { currentScreen.viewModels.forEach(ViewModelClass => { if ( ViewModelClass.__vm && @@ -251,7 +251,7 @@ function screenOnRoute(screenName, subPart) { if (currentScreen && !isSameScreen) { currentScreen.onShow && currentScreen.onShow(); - if (isNonEmptyArray(currentScreen.viewModels)) { + if (arrayLength(currentScreen.viewModels)) { currentScreen.viewModels.forEach(ViewModelClass => { if ( ViewModelClass.__vm && 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/Filter.js b/dev/Model/Filter.js index b53656c55..691e7b142 100644 --- a/dev/Model/Filter.js +++ b/dev/Model/Filter.js @@ -1,6 +1,6 @@ import ko from 'ko'; -import { isNonEmptyArray, pString } from 'Common/Utils'; +import { arrayLength, pString } from 'Common/Utils'; import { delegateRunOnDestroy } from 'Common/UtilsUser'; import { i18n } from 'Common/Translator'; import { getFolderFromCacheList } from 'Common/Cache'; @@ -230,7 +230,7 @@ export class FilterModel extends AbstractModel { filter.conditions([]); - if (isNonEmptyArray(json.Conditions)) { + if (arrayLength(json.Conditions)) { filter.conditions( json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) ); diff --git a/dev/Model/FolderCollection.js b/dev/Model/FolderCollection.js index b781bbc0f..b04ce4f45 100644 --- a/dev/Model/FolderCollection.js +++ b/dev/Model/FolderCollection.js @@ -226,7 +226,7 @@ export class FolderModel extends AbstractModel { folder.messageCountAll = ko.computed({ read: folder.privateMessageCountAll, write: (iValue) => { - if (isPosNumeric(iValue, true)) { + if (isPosNumeric(iValue)) { folder.privateMessageCountAll(iValue); } else { folder.privateMessageCountAll.valueHasMutated(); @@ -238,7 +238,7 @@ export class FolderModel extends AbstractModel { folder.messageCountUnread = ko.computed({ read: folder.privateMessageCountUnread, write: (value) => { - if (isPosNumeric(value, true)) { + if (isPosNumeric(value)) { folder.privateMessageCountUnread(value); } else { folder.privateMessageCountUnread.valueHasMutated(); @@ -371,6 +371,6 @@ export class FolderModel extends AbstractModel { * @returns {string} */ printableFullName() { - return this.fullName.split(this.delimiter).join(' / '); + return this.fullName.replace(this.delimiter, ' / '); } } diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 9e31a2515..2f6996dca 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -4,7 +4,7 @@ import { MessagePriority } from 'Common/EnumsUser'; import { i18n } from 'Common/Translator'; import { encodeHtml } from 'Common/Html'; -import { isArray, isNonEmptyArray } from 'Common/Utils'; +import { isArray, arrayLength } from 'Common/Utils'; import { serverRequestRaw } from 'Common/Links'; @@ -229,7 +229,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 || '']; } diff --git a/dev/Model/OpenPgpKey.js b/dev/Model/OpenPgpKey.js index 1ae0a623f..df5aa7449 100644 --- a/dev/Model/OpenPgpKey.js +++ b/dev/Model/OpenPgpKey.js @@ -1,6 +1,6 @@ import ko from 'ko'; -import { isNonEmptyArray } from 'Common/Utils'; +import { arrayLength } from 'Common/Utils'; import { AbstractModel } from 'Knoin/AbstractModel'; import { PgpUserStore } from 'Stores/User/Pgp'; @@ -21,7 +21,7 @@ export class OpenPgpKeyModel extends AbstractModel { this.index = index; this.id = ID; - this.ids = isNonEmptyArray(IDs) ? IDs : [ID]; + this.ids = arrayLength(IDs) ? IDs : [ID]; this.guid = guID; this.user = ''; this.users = userIDs; diff --git a/dev/Model/SieveScript.js b/dev/Model/SieveScript.js index b37bee73f..18b4eb02c 100644 --- a/dev/Model/SieveScript.js +++ b/dev/Model/SieveScript.js @@ -2,7 +2,7 @@ import ko from 'ko'; import { AbstractModel } from 'Knoin/AbstractModel'; import { FilterModel } from 'Model/Filter'; -import { isNonEmptyArray, pString } from 'Common/Utils'; +import { arrayLength, pString } from 'Common/Utils'; const SIEVE_FILE_NAME = 'rainloop.user'; @@ -327,7 +327,7 @@ export class SieveScriptModel extends AbstractModel if (script) { if (script.allowFilters()) { script.filters( - isNonEmptyArray(json.filters) + arrayLength(json.filters) ? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v) : sieveScriptToFilters(script.body()) ); diff --git a/dev/Remote/Admin/Fetch.js b/dev/Remote/Admin/Fetch.js index dc0ded270..7fd451842 100644 --- a/dev/Remote/Admin/Fetch.js +++ b/dev/Remote/Admin/Fetch.js @@ -6,10 +6,11 @@ class RemoteAdminFetch extends AbstractFetchRemote { * @param {string} sLogin * @param {string} sPassword */ - adminLogin(fCallback, sLogin, sPassword) { + adminLogin(fCallback, sLogin, sPassword, sCode) { this.defaultRequest(fCallback, 'AdminLogin', { Login: sLogin, - Password: sPassword + Password: sPassword, + TOTP: sCode }); } diff --git a/dev/Remote/User/Fetch.js b/dev/Remote/User/Fetch.js index 9f272e60f..fdc205072 100644 --- a/dev/Remote/User/Fetch.js +++ b/dev/Remote/User/Fetch.js @@ -1,4 +1,4 @@ -import { isArray, isNonEmptyArray, pString, pInt } from 'Common/Utils'; +import { isArray, arrayLength, pString, pInt } from 'Common/Utils'; import { getFolderHash, @@ -371,7 +371,7 @@ class RemoteUserFetch extends AbstractFetchRemote { let request = true; const uids = []; - if (isNonEmptyArray(list)) { + if (arrayLength(list)) { request = false; list.forEach(messageListItem => { if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) { diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js index 9df238ca7..bbf225b17 100644 --- a/dev/Settings/User/General.js +++ b/dev/Settings/User/General.js @@ -3,7 +3,7 @@ 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 { Settings, SettingsGet } from 'Common/Globals'; import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils'; import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator'; @@ -119,7 +119,7 @@ export class GeneralUserSettings { enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0), notificationSound: value => { Remote.saveSetting('NotificationSound', value); - rl.settings.set('NotificationSound', value); + Settings.set('NotificationSound', value); }, replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0), @@ -138,9 +138,7 @@ export class GeneralUserSettings { editMainIdentity() { const identity = this.identityMain(); - if (identity) { - showScreenPopup(IdentityPopupView, [identity]); - } + identity && showScreenPopup(IdentityPopupView, [identity]); } testSoundNotification() { diff --git a/dev/Stores/User/Message.js b/dev/Stores/User/Message.js index f36fe51d6..8e1b931fa 100644 --- a/dev/Stores/User/Message.js +++ b/dev/Stores/User/Message.js @@ -3,7 +3,7 @@ import ko from 'ko'; import { Scope, Notification } from 'Common/Enums'; import { MessageSetAction } from 'Common/EnumsUser'; import { doc, $htmlCL, createElement, elementById } from 'Common/Globals'; -import { isNonEmptyArray, pInt, pString, addObservablesTo, addSubscribablesTo } from 'Common/Utils'; +import { arrayLength, pInt, pString, addObservablesTo, addSubscribablesTo } from 'Common/Utils'; import { plainToHtml } from 'Common/UtilsUser'; import { @@ -237,7 +237,7 @@ export const MessageUserStore = new class { initUidNextAndNewMessages(folder, uidNext, newMessages) { if (getFolderInboxName() === folder && uidNext) { - if (isNonEmptyArray(newMessages)) { + if (arrayLength(newMessages)) { newMessages.forEach(item => addNewMessageCache(folder, item.Uid)); NotificationUserStore.playSoundNotification(); diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index 85dc32633..9db75d106 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -1,7 +1,7 @@ import ko from 'ko'; import { i18n } from 'Common/Translator'; -import { isArray, isNonEmptyArray, pString } from 'Common/Utils'; +import { isArray, arrayLength, pString } from 'Common/Utils'; import { createElement } from 'Common/Globals'; import { AccountUserStore } from 'Stores/User/Account'; @@ -54,7 +54,7 @@ function domControlEncryptedClickHelper(store, dom, armoredMessage, recipients) decryptedMessage.getText() ); } else if (validPrivateKey) { - const keyIds = isNonEmptyArray(signingKeyIds) ? signingKeyIds : null, + const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, additional = keyIds ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ') : ''; @@ -110,7 +110,7 @@ function domControlSignedClickHelper(store, dom, armoredMessage) { message.getText() ); } else { - const keyIds = isNonEmptyArray(signingKeyIds) ? signingKeyIds : null, + const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, additional = keyIds ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ') : ''; @@ -186,7 +186,7 @@ export const PgpUserStore = new class { }).flat().filter(v => v) : []; - if (!result.length && isNonEmptyArray(recipients)) { + if (!result.length && arrayLength(recipients)) { result = recipients.map(sEmail => { const keys = sEmail ? this.findAllPrivateKeysByEmailNotNative(sEmail) : null; return keys diff --git a/dev/Styles/Login.less b/dev/Styles/Login.less index c0a5ff70d..f1fbe591f 100644 --- a/dev/Styles/Login.less +++ b/dev/Styles/Login.less @@ -79,14 +79,13 @@ } .controls { - .inputLoginForm, .inputLogin, .inputPassword { + .input-block-level { font-size: 18px; height: 40px; line-height: 20px; padding-left: 12px; - padding-right: 12px; } - .inputLogin, .inputPassword { + .inputIcon { padding-right: 35px; } diff --git a/dev/Styles/User/MessageList.less b/dev/Styles/User/MessageList.less index af2c0c547..f4aa3b739 100644 --- a/dev/Styles/User/MessageList.less +++ b/dev/Styles/User/MessageList.less @@ -198,13 +198,9 @@ html.rl-no-preview-pane { } } - &.message-focused { - .b-message-list-wrapper { - background-color: #000; - } - .b-content { - opacity: 0.97; - } + &.focused .b-message-list-wrapper { + background-color: #000; + border-color: #9d9d9d; } } diff --git a/dev/Styles/User/MessageView.less b/dev/Styles/User/MessageView.less index 7971ce0f9..18460603c 100644 --- a/dev/Styles/User/MessageView.less +++ b/dev/Styles/User/MessageView.less @@ -458,7 +458,7 @@ } } - &.message-focused .b-content { + &.focused .b-content { z-index: 101; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); border-color: darken(@rlMainDarkColor, 5%); diff --git a/dev/View/Admin/Login.js b/dev/View/Admin/Login.js index 6cdbb11d7..639998a89 100644 --- a/dev/View/Admin/Login.js +++ b/dev/View/Admin/Login.js @@ -17,6 +17,7 @@ class LoginAdminView extends AbstractViewCenter { this.addObservables({ login: '', password: '', + totp: '', loginError: false, passwordError: false, @@ -59,7 +60,8 @@ class LoginAdminView extends AbstractViewCenter { } }, name, - pass + pass, + this.totp() ); } diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 3886fe3e8..872a75c62 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -12,7 +12,7 @@ import { SetSystemFoldersNotification } from 'Common/EnumsUser'; -import { inFocus, pInt, isArray, isNonEmptyArray } from 'Common/Utils'; +import { inFocus, pInt, isArray, arrayLength } from 'Common/Utils'; import { delegateRunOnDestroy } from 'Common/UtilsUser'; import { encodeHtml, HtmlEditor } from 'Common/Html'; @@ -308,7 +308,7 @@ class ComposePopupView extends AbstractViewPopup { }, attachmentsInProcess: value => { - if (this.attachmentsInProcessError() && isNonEmptyArray(value)) { + if (this.attachmentsInProcessError() && arrayLength(value)) { this.attachmentsInProcessError(false); } } @@ -385,8 +385,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 ) { @@ -400,7 +399,7 @@ class ComposePopupView extends AbstractViewPopup { 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]) { @@ -753,7 +752,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(); @@ -806,11 +805,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; @@ -826,15 +825,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)); } @@ -899,7 +898,7 @@ 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; @@ -913,7 +912,7 @@ 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; @@ -1003,7 +1002,7 @@ class ComposePopupView extends AbstractViewPopup { this.setFocusInPopup(); }); - } else if (isNonEmptyArray(oMessageOrArray)) { + } else if (arrayLength(oMessageOrArray)) { oMessageOrArray.forEach(item => this.addMessageAsAttachment(item)); this.editor(editor => { @@ -1024,7 +1023,7 @@ class ComposePopupView extends AbstractViewPopup { } const downloads = this.getAttachmentsDownloadsForUpload(); - if (isNonEmptyArray(downloads)) { + if (arrayLength(downloads)) { Remote.messageUploadAttachments((iError, oData) => { if (!iError) { Object.entries(oData.Result).forEach(([tempName, id]) => { diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js index e52d6cb8f..d5977108a 100644 --- a/dev/View/Popup/Contacts.js +++ b/dev/View/Popup/Contacts.js @@ -7,7 +7,7 @@ import { import { ComposeType } from 'Common/EnumsUser'; -import { isNonEmptyArray, pInt } from 'Common/Utils'; +import { arrayLength, pInt } from 'Common/Utils'; import { delegateRunOnDestroy, computedPaginatorHelper, showMessageComposer } from 'Common/UtilsUser'; import { Selector } from 'Common/Selector'; @@ -163,7 +163,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,7 +180,7 @@ class ContactsPopupView extends AbstractViewPopup { aE = aE.filter(value => !!value); } - if (isNonEmptyArray(aE)) { + if (arrayLength(aE)) { this.bBackToCompose = false; hideScreenPopup(ContactsPopupView); @@ -447,7 +447,7 @@ class ContactsPopupView extends AbstractViewPopup { 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); diff --git a/dev/View/Popup/Plugin.js b/dev/View/Popup/Plugin.js index 12ce2e6f8..4f2a61d72 100644 --- a/dev/View/Popup/Plugin.js +++ b/dev/View/Popup/Plugin.js @@ -2,7 +2,7 @@ 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'; @@ -65,7 +65,7 @@ class PluginPopupView extends AbstractViewPopup { this.readme(oPlugin.Readme); const config = oPlugin.Config; - if (isNonEmptyArray(config)) { + if (arrayLength(config)) { this.configures( config.map(item => ({ value: ko.observable(item[0]), diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js index df76e305e..93cf540cd 100644 --- a/dev/View/User/MailBox/MessageList.js +++ b/dev/View/User/MailBox/MessageList.js @@ -838,8 +838,10 @@ export class MessageListMailBoxUserView extends AbstractViewRight { return false; }); shortcuts.add('tab,arrowright', '', Scope.MessageList, () => { - MessageUserStore.message() && AppUserStore.focusedState(Scope.MessageView); - return false; + if (MessageUserStore.message()){ + AppUserStore.focusedState(Scope.MessageView); + return false; + } }); shortcuts.add('arrowleft', 'meta', Scope.MessageView, ()=>false); diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index 8717669a7..3fc9e55d0 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -16,7 +16,7 @@ import { import { doc, $htmlCL, leftPanelDisabled, keyScopeReal, moveAction, Settings } from 'Common/Globals'; -import { isNonEmptyArray, inFocus } from 'Common/Utils'; +import { arrayLength, inFocus } from 'Common/Utils'; import { mailToHelper, showMessageComposer } from 'Common/UtilsUser'; import { SMAudio } from 'Common/Audio'; @@ -78,7 +78,7 @@ class MessageViewMailBoxUserView extends AbstractViewRight { this.allowMessageListActions = Settings.capa(Capa.MessageListActions); 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; @@ -176,10 +176,8 @@ 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 ''; @@ -541,8 +539,7 @@ class MessageViewMailBoxUserView extends AbstractViewRight { return false; } }); -// shortcuts.add('tab', 'shift', Scope.MessageView, (event, handler) => { - shortcuts.add('tab', '', Scope.MessageView, () => { + shortcuts.add('tab', 'shift', Scope.MessageView, () => { if (!this.fullScreenMode() && this.message() && SettingsUserStore.usePreviewPane()) { AppUserStore.focusedState(Scope.MessageList); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php index 61dcf16aa..5bf43c4cf 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php @@ -179,10 +179,13 @@ trait Admin $this->Logger()->AddSecret($sPassword); + $totp = $this->Config()->Get('security', 'admin_totp', ''); + if (0 === strlen($sLogin) || 0 === strlen($sPassword) || !$this->Config()->Get('security', 'allow_admin_panel', true) || $sLogin !== $this->Config()->Get('security', 'admin_login', '') || - !$this->Config()->ValidatePassword($sPassword)) + !$this->Config()->ValidatePassword($sPassword) + || ($totp && !\SnappyMail\TOTP::Verify($totp, $this->GetActionParam('TOTP', '')))) { $this->loginErrorDelay(); $this->LoggerAuthHelper(null, $this->getAdditionalLogParamsByUserLogin($sLogin, true)); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php index a8a635fca..727a55c38 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php @@ -259,4 +259,34 @@ trait Contacts return $mResult; } + public function RawContactsVcf() : bool + { + $oAccount = $this->getAccountFromToken(); + + \header('Content-Type: text/x-vcard; charset=UTF-8'); + \header('Content-Disposition: attachment; filename="contacts.vcf"', true); + \header('Accept-Ranges: none', true); + \header('Content-Transfer-Encoding: binary'); + + $this->oHttp->ServerNoCache(); + + return $this->AddressBookProvider($oAccount)->IsActive() ? + $this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'vcf') : false; + } + + public function RawContactsCsv() : bool + { + $oAccount = $this->getAccountFromToken(); + + \header('Content-Type: text/csv; charset=UTF-8'); + \header('Content-Disposition: attachment; filename="contacts.csv"', true); + \header('Accept-Ranges: none', true); + \header('Content-Transfer-Encoding: binary'); + + $this->oHttp->ServerNoCache(); + + return $this->AddressBookProvider($oAccount)->IsActive() ? + $this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'csv') : false; + } + } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php index 1bd539b97..6a2b7ccdb 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php @@ -112,36 +112,6 @@ trait Raw return false; } - public function RawContactsVcf() : bool - { - $oAccount = $this->getAccountFromToken(); - - \header('Content-Type: text/x-vcard; charset=UTF-8'); - \header('Content-Disposition: attachment; filename="contacts.vcf"', true); - \header('Accept-Ranges: none', true); - \header('Content-Transfer-Encoding: binary'); - - $this->oHttp->ServerNoCache(); - - return $this->AddressBookProvider($oAccount)->IsActive() ? - $this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'vcf') : false; - } - - public function RawContactsCsv() : bool - { - $oAccount = $this->getAccountFromToken(); - - \header('Content-Type: text/csv; charset=UTF-8'); - \header('Content-Disposition: attachment; filename="contacts.csv"', true); - \header('Accept-Ranges: none', true); - \header('Content-Transfer-Encoding: binary'); - - $this->oHttp->ServerNoCache(); - - return $this->AddressBookProvider($oAccount)->IsActive() ? - $this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'csv') : false; - } - private function rawSmart(bool $bDownload, bool $bThumbnail = false) : bool { $sRawKey = (string) $this->GetActionParam('RawKey', ''); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php index 8afc3ec3a..65261b35e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php @@ -168,6 +168,7 @@ class Application extends \RainLoop\Config\AbstractConfig 'admin_login' => array('admin', 'Login and password for web admin panel'), 'admin_password' => array(''), + 'admin_totp' => array(''), 'allow_admin_panel' => array(true, 'Access settings'), 'hide_x_mailer_header' => array(true), 'admin_panel_host' => array(''), diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/totp.php b/snappymail/v/0.0.0/app/libraries/snappymail/totp.php new file mode 100644 index 000000000..133be37f1 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/totp.php @@ -0,0 +1,98 @@ + 0, // ord 65 + 'B' => 1, + 'C' => 2, + 'D' => 3, + 'E' => 4, + 'F' => 5, + 'G' => 6, + 'H' => 7, + 'I' => 8, + 'J' => 9, + 'K' => 10, + 'L' => 11, + 'M' => 12, + 'N' => 13, + 'O' => 14, + 'P' => 15, + 'Q' => 16, + 'R' => 17, + 'S' => 18, + 'T' => 19, + 'U' => 20, + 'V' => 21, + 'W' => 22, + 'X' => 23, + 'Y' => 24, + 'Z' => 25, // ord 90 + '2' => 26, // ord 50 + '3' => 27, + '4' => 28, + '5' => 29, + '6' => 30, + '7' => 31 // ord 55 + ); + + protected static function Base32Decode(string $data) + { + $data = \strtoupper(\rtrim($data, "=\x20\t\n\r\0\x0B")); + $dataSize = \strlen($data); + $buf = 0; + $bufSize = 0; + $res = ''; + for ($i = 0; $i < $dataSize; ++$i) { + $c = $data[$i]; + if (isset(static::$map[$c])) { + $buf = ($buf << 5) | static::$map[$c]; + $bufSize += 5; + if ($bufSize > 7) { + $bufSize -= 8; + $res .= \chr(($buf & (0xff << $bufSize)) >> $bufSize); + } + } + } + return $res; + } +} diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminLogin.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminLogin.html index 5af3d0b35..c47208831 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminLogin.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminLogin.html @@ -6,7 +6,7 @@
- @@ -15,15 +15,20 @@
- - - 🔑 - - + 🔑 +
+
+
+
+ +
diff --git a/snappymail/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html b/snappymail/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html index e426ef15f..d348150dc 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html +++ b/snappymail/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html @@ -63,7 +63,7 @@ Ctrl + P, ⌘ + P Esc, Close Esc, Close - Tab, Shift + Tab, Esc + Shift + Tab, Esc diff --git a/snappymail/v/0.0.0/app/templates/Views/User/Login.html b/snappymail/v/0.0.0/app/templates/Views/User/Login.html index a24f0a7ae..c677637f4 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/Login.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/Login.html @@ -9,7 +9,7 @@ data-bind="submit: submitForm, css: {'errorAnimated': formError, 'submitting': submitRequest()}">
- @@ -18,7 +18,7 @@
- diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html index 343edcb7b..ff05cfe41 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html @@ -1,5 +1,5 @@
+ data-bind="css: {'message-selected': isMessageSelected, 'focused': messageListFocused() }">
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html index 5a0967f8f..792355a41 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html @@ -1,5 +1,5 @@
-
+
diff --git a/snappymail/v/0.0.0/themes/SquaresDark/styles.less b/snappymail/v/0.0.0/themes/SquaresDark/styles.less index b24e1e61e..a0b5da470 100644 --- a/snappymail/v/0.0.0/themes/SquaresDark/styles.less +++ b/snappymail/v/0.0.0/themes/SquaresDark/styles.less @@ -144,3 +144,7 @@ .b-admin-about .rl-logo { filter: drop-shadow(0 0 1px #FFF); } + +.b-folders .e-item a.focused { + background-color: #111; +} diff --git a/vendors/bootstrap/less/navs.less b/vendors/bootstrap/less/navs.less index b52176c6a..3b0f7bcd0 100644 --- a/vendors/bootstrap/less/navs.less +++ b/vendors/bootstrap/less/navs.less @@ -29,6 +29,7 @@ // Give the tabs something to sit on .nav-tabs { border-bottom: 1px solid #ddd; + white-space: nowrap; } // Make the list-items overlay the bottom border .nav-tabs > li {