HEX
Server: LiteSpeed
System: Linux php-prod-1.spaceapp.ru 5.15.0-157-generic #167-Ubuntu SMP Wed Sep 17 21:35:53 UTC 2025 x86_64
User: sport3497 (1034)
PHP: 8.1.33
Disabled: NONE
Upload Files
File: //proc/self/root/usr/local/CyberCP/public/snappymail/snappymail/v/2.38.2/static/js/admin.js
/* SnappyMail Webmail (c) SnappyMail | Licensed under AGPL v3 */
(function () {
	'use strict';

	/* eslint quote-props: 0 */

	const /**
	 * @enum {number}
	 */
	SaveSettingStatus = {
		Saving: -2,
		Idle: -1,
		Success: 1,
		Failed: 0
	},

	/**
	 * @enum {number}
	 */
	Notifications = {
		RequestError: 1,
		RequestAborted: 2,
		RequestTimeout: 3,

		// Global
		InvalidToken: 101,
		AuthError: 102,

		// User
		ConnectionError: 104,
		DomainNotAllowed: 109,
		AccountNotAllowed: 110,
		CryptKeyError: 111,

		ContactsSyncError: 140,

		CantGetMessageList: 201,
		CantGetMessage: 202,
		CantDeleteMessage: 203,
		CantMoveMessage: 204,
		CantCopyMessage: 205,

		CantSaveMessage: 301,
		CantSendMessage: 302,
		InvalidRecipients: 303,

		CantSaveFilters: 351,
		CantGetFilters: 352,
		CantActivateFiltersScript: 353,
		CantDeleteFiltersScript: 354,
	//	FiltersAreNotCorrect: 355,

		CantCreateFolder: 400,
		CantRenameFolder: 401,
		CantDeleteFolder: 402,
		CantSubscribeFolder: 403,
		CantUnsubscribeFolder: 404,
		CantDeleteNonEmptyFolder: 405,

	//	CantSaveSettings: 501,

		DomainAlreadyExists: 601,

		DemoSendMessageError: 750,
		DemoAccountError: 751,

		AccountAlreadyExists: 801,
		AccountDoesNotExist: 802,
		AccountSwitchFailed: 803,

		MailServerError: 901,
		ClientViewError: 902,
		InvalidInputArgument: 903,

		JsonFalse: 950,
		JsonParse: 952,
	//	JsonTimeout: 953,

		UnknownError: 999,

		// Admin
		CantInstallPackage: 701,
		CantDeletePackage: 702,
		InvalidPluginPackage: 703,
		UnsupportedPluginPackage: 704,
		CantSavePluginSettings: 705
	};

	const isArray = Array.isArray,
		arrayLength = array => isArray(array) && array.length,
		isFunction = v => typeof v === 'function',
		pString = value => null != value ? '' + value : '',

		forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),

		pInt = (value, defaultValue = 0) => {
			value = parseInt(value, 10);
			return isFinite(value) ? value : defaultValue;
		},

		defaultOptionsAfterRender = (domItem, item) =>
			item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),

		getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

	let keyScopeFake = 'all';

	const ScopeMenu = 'Menu',

		doc = document,

		$htmlCL = doc.documentElement.classList,

		elementById = id => doc.getElementById(id),

		appEl = elementById('rl-app'),

		Settings = rl.settings,
		SettingsGet = Settings.get,
		SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
		SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],

		dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),

		leftPanelDisabled = ko.observable(false),
		toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),

		createElement = (name, attr) => {
			let el = doc.createElement(name);
			attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
			return el;
		},

		fireEvent = (name, detail, cancelable) => dispatchEvent(
			new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
		),

		addShortcut = (...args) => shortcuts.add(...args),

		// 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.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));

	const
		BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
		HASH_PREFIX = '#/',

		adminPath = () => rl.adminArea() && !SettingsAdmin('host'),

		prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');

	const SUB_QUERY_PREFIX = '&q[]=',

		/**
		 * @param {string=} startupUrl
		 * @returns {string}
		 */
		root = () => HASH_PREFIX,

		/**
		 * @returns {string}
		 */
		logoutLink = () => adminPath() ? prefix() : BASE,

		/**
		 * @param {string} type
		 * @param {string} hash
		 * @param {string=} customSpecSuffix
		 * @returns {string}
		 */
		serverRequestRaw = (type, hash) =>
			BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
			+ '0/' // Settings.get('accountHash') ?
			+ (type
				? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
				: ''),

		//			+ b64EncodeJSONSafe(url.replace(/ /g, '%20')),

		/**
		 * @param {string} type
		 * @returns {string}
		 */
		serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',

		// Is '?/Css/0/Admin' needed?
		cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/',

		/**
		 * @param {string} lang
		 * @param {boolean} isAdmin
		 * @returns {string}
		 */
		langLink = (lang, isAdmin) =>
			BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
				+ '/' + encodeURI(lang)
				+ '/' + Settings.app('version') + '/',

		/**
		 * @param {string=} screenName = ''
		 * @returns {string}
		 */
		settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');

	const LanguageStore = {
		language: ko.observable(''),
		languages: ko.observableArray(),
		userLanguage: ko.observable(''),
		hourCycle: ko.observable(''),

		populate: function() {
			const aLanguages = Settings.app('languages');
			this.languages(isArray(aLanguages) ? aLanguages : []);
			this.language(SettingsGet('language'));
			this.userLanguage(SettingsGet('clientLanguage'));
			this.hourCycle(SettingsGet('hourCycle'));
		}
	};

	let I18N_DATA = {};

	const init = () => {
			if (rl.I18N) {
				I18N_DATA = rl.I18N;
				rl.I18N = null;
				doc.documentElement.dir = I18N_DATA.LANG_DIR;
				return 1;
			}
		},

		getNotificationMessage = code => {
			let key = getKeyByValue(Notifications, code);
			return key ? I18N_DATA.NOTIFICATIONS[key] : '';
		};

	const translateTrigger = ko.observable(false),

		/**
		 * @param {string} key
		 * @param {Object=} valueList
		 * @param {string=} defaulValue
		 * @returns {string}
		 */
		i18n = (key, valueList, defaulValue) => {
			let result = defaulValue ?? key;
			let path = key.split('/');
			if (I18N_DATA[path[0]] && path[1]) {
				result = I18N_DATA[path[0]][path[1]] || result;
			}
			valueList && forEachObjectEntry(valueList, (key, value) => {
				result = result.replace('%' + key + '%', value);
			});
			return result;
		},

		/**
		 * @param {Object} elements
		 * @param {boolean=} animate = false
		 */
		i18nToNodes = element =>
			setTimeout(() =>
				element.querySelectorAll('[data-i18n]').forEach(element => {
					const key = element.dataset.i18n;
					if ('[' === key[0]) {
						switch (key.slice(1, 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);
					}
				})
			, 1),

		/**
		 * @param {Function} startCallback
		 * @param {Function=} langCallback = null
		 */
		initOnStartOrLangChange = (startCallback, langCallback) => {
			startCallback?.();
			startCallback && translateTrigger.subscribe(startCallback);
			langCallback && translateTrigger.subscribe(langCallback);
		},

		/**
		 * @param {number} code
		 * @param {*=} message = ''
		 * @param {*=} defCode = null
		 * @returns {string}
		 */
		getNotification = (code, message = '', defCode = 0) => {
			code = pInt(code);
			if (Notifications.ClientViewError === code && message) {
				return message;
			}

			return getNotificationMessage(code)
				|| getNotificationMessage(pInt(defCode))
				|| '';
		},

		/**
		 * @param {boolean} admin
		 * @param {string} language
		 */
		translatorReload = (language, admin) =>
			new Promise((resolve, reject) => {
				const script = createElement('script');
				script.onload = () => {
					// reload the data
					if (init()) {
						i18nToNodes(doc);
						translateTrigger(!translateTrigger());
	//					admin || reloadTime();
					}
					script.remove();
					resolve();
				};
				script.onerror = () => reject(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();

	var bootstrap = App => {

		rl.app = App;
		rl.logoutReload = App.logoutReload;

		rl.i18n = i18n;

		rl.Enums = {
			StorageResultType: {
				Success: 0,
				Error: 1,
				Abort: 2
			}
		};

		rl.route = {
			root: () => {
				rl.route.off();
				hasher.setHash(root());
			},
			reload: () => {
				rl.route.root();
				setTimeout(() => location.reload(), 100);
			},
			off: () => hasher.active = false,
			on: () => hasher.active = true
		};

	};

	const errorTip = (element, value) => value
				? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
				: 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 => isFunction(disposable?.dispose) && disposable.dispose(),

		onEvent = (element, event, fn) => {
			element.addEventListener(event, fn);
			ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
		},

		onKey = (key, element, fValueAccessor, fAllBindings, model) => {
			let fn = event => {
				if (key == event.key) {
	//				stopEvent(event);
	//				element.dispatchEvent(new Event('change'));
					fValueAccessor().call(model);
				}
			};
			onEvent(element, 'keydown', fn);
		};

	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());
				errorTip(element, isFunction(value) ? value() : value);
			}
		},

		onEnter: {
			init: (element, fValueAccessor, fAllBindings, model) =>
				onKey('Enter', element, fValueAccessor, fAllBindings, model)
		},

		onEsc: {
			init: (element, fValueAccessor, fAllBindings, model) =>
				onKey('Escape', element, fValueAccessor, fAllBindings, model)
		},

		onSpace: {
			init: (element, fValueAccessor, fAllBindings, model) =>
				onKey(' ', element, fValueAccessor, fAllBindings, model)
		},

		toggle: {
			init: (element, fValueAccessor) => {
				let observable = fValueAccessor(),
					fn = () => observable(!observable());
				onEvent(element, 'click', fn);
				onEvent(element, 'keydown', event => ' ' == event.key && fn());
			}
		},

		i18nUpdate: {
			update: (element, fValueAccessor) => {
				ko.unwrap(fValueAccessor());
				i18nToNodes(element);
			}
		},

		command: {
			init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
				const command = fValueAccessor();

				if (!command || !command.canExecute) {
					throw Error('Value should be a command');
				}

				ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
					element,
					fValueAccessor,
					fAllBindings,
					viewModel,
					bindingContext
				);
			},
			update: (element, fValueAccessor) => {
				let disabled = !fValueAccessor().canExecute();
				element.classList.toggle('disabled', disabled);

				if (element.matches('INPUT,TEXTAREA,BUTTON')) {
					element.disabled = disabled;
				}
			}
		},

		saveTrigger: {
			init: (element) => {
				let icon = element;
				if (element.matches('input,select,textarea')) {
					element.classList.add('settings-save-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 === SaveSettingStatus.Saving);
					cl.toggle('success', value === SaveSettingStatus.Success);
					cl.toggle('error', value === SaveSettingStatus.Failed);
				}
				cl = element.classList;
				cl.toggle('success', value === SaveSettingStatus.Success);
				cl.toggle('error', value === SaveSettingStatus.Failed);
			}
		}
	});

	// extenders

	ko.extenders.toggleSubscribeProperty = (target, options) => {
		const prop = options[1];
		if (prop) {
			target.subscribe(
				prev => prev?.[prop]?.(false),
				options[0],
				'beforeChange'
			);

			target.subscribe(next => next?.[prop]?.(true), options[0]);
		}

		return target;
	};

	ko.extenders.falseTimeout = (target, option) => {
		target.subscribe((() => target(false)).debounce(parseInt(option, 10) || 0));
		return target;
	};

	// functions

	ko.observable.fn.askDeleteHelper = function() {
		return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
	};

	let __themeTimer = 0;

	const
		// Also see Styles/_Values.less @maxMobileWidth
		isMobile = matchMedia('(max-width: 799px)'),
		// https://github.com/the-djmaze/snappymail/issues/1150
	//	isSmall = matchMedia('(max-width: 1400px)'),

		ThemeStore = {
			theme: ko.observable(''),
			themes: ko.observableArray(),
			userBackgroundName: ko.observable(''),
			userBackgroundHash: ko.observable(''),
			fontSansSerif: ko.observable(''),
			fontSerif: ko.observable(''),
			fontMono: ko.observable(''),
			isMobile: ko.observable(false)
		},

		initThemes = () => {
			const theme = SettingsGet('Theme'),
				themes = Settings.app('themes');

			ThemeStore.themes(isArray(themes) ? themes : []);
			ThemeStore.theme(theme);
			changeTheme(theme);
			if (!ThemeStore.isMobile()) {
				ThemeStore.userBackgroundName(SettingsGet('userBackgroundName'));
				ThemeStore.userBackgroundHash(SettingsGet('userBackgroundHash'));
			}
			ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
			ThemeStore.fontSerif(SettingsGet('fontSerif'));
			ThemeStore.fontMono(SettingsGet('fontMono'));

			leftPanelDisabled(ThemeStore.isMobile());
		},

		changeTheme = (value, themeTrigger = ()=>0) => {
			const themeStyle = elementById('app-theme-style'),
				clearTimer = () => {
					__themeTimer = setTimeout(() => themeTrigger(SaveSettingStatus.Idle), 1000);
				},
				url = cssLink(value);

			if (themeStyle.dataset.name != value) {
				clearTimeout(__themeTimer);

				themeTrigger(SaveSettingStatus.Saving);

				rl.app.Remote.abort('theme').get('theme', url)
					.then(data => {
						if (2 === arrayLength(data)) {
							themeStyle.textContent = data[1];
							themeStyle.dataset.name = value;
							themeTrigger(SaveSettingStatus.Success);
						}
						clearTimer();
					}, clearTimer);
			}
		},

		convertThemeName = theme => theme.replace(/@[a-z]+$/, '').replace(/([A-Z])/g, ' $1').trim();

	addSubscribablesTo(ThemeStore, {
		fontSansSerif: value => {
			if (null != value) {
				let cl = appEl.classList;
				cl.forEach(name => {
					if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
						cl.remove(name);
					}
				});
				value && cl.add('font'+value);
			}
		},

		fontSerif: value => {
			if (null != value) {
				let cl = appEl.classList;
				cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
				value && cl.add('fontSerif'+value);
			}
		},

		fontMono: value => {
			if (null != value) {
				let cl = appEl.classList;
				cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
				value && cl.add('fontMono'+value);
			}
		},

		userBackgroundHash: value => {
			appEl.classList.toggle('UserBackground', !!value);
			appEl.style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
		}
	});

	isMobile.onchange = e => {
		ThemeStore.isMobile(e.matches);
		$htmlCL.toggle('rl-mobile', e.matches);
		/*$htmlCL.contains('sm-msgView-side') || */leftPanelDisabled(e.matches);
	};
	isMobile.onchange(isMobile);

	//isSmall.onchange = e => $htmlCL.contains('sm-msgView-side') && leftPanelDisabled(e.matches);
	//isSmall.onchange(isSmall);

	let iJsonErrorCount = 0;

	const getURL = (add = '') => serverRequest('Json') + pString(add),

	checkResponseError = data => {
		const err = data ? data.code : null;
		if (Notifications.InvalidToken === err) {
			console.error(getNotification(err) + ` (${data.messageAdditional})`);
	//		alert(getNotification(err));
			setTimeout(rl.logoutReload, 5000);
		} else if ([
				Notifications.AuthError,
				Notifications.ConnectionError,
				Notifications.DomainNotAllowed,
				Notifications.AccountNotAllowed,
				Notifications.MailServerError,
				Notifications.UnknownError
			].includes(err)
		) {
			if (7 < ++iJsonErrorCount) {
				rl.logoutReload();
			}
		}
	},

	oRequests = {},

	abort = (sAction, sReason, bClearOnly) => {
		let controller = oRequests[sAction];
		oRequests[sAction] = null;
		if (controller) {
			clearTimeout(controller.timeoutId);
			bClearOnly || controller.abort(new DOMException(sAction, sReason || 'AbortError'));
		}
	},

	fetchJSON = (action, sUrl, params, timeout, jsonCallback) => {
		if (params) {
			if (params instanceof FormData) {
				params.set('Action', action);
			} else {
				params.Action = action;
			}
		}
		// Don't abort, read https://github.com/the-djmaze/snappymail/issues/487
	//	abort(action, 0, 1);
		const controller = new AbortController(),
			signal = controller.signal;
		oRequests[action] = controller;
		// Currently there is no way to combine multiple signals, so AbortSignal.timeout() not possible
		controller.timeoutId = timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
		return rl.fetchJSON(sUrl, {signal: signal}, params).then(data => {
			abort(action, 0, 1);
			return jsonCallback ? jsonCallback(data) : Promise.resolve(data);
		}).catch(err => {
			clearTimeout(controller.timeoutId);
			err.aborted = signal.aborted;
			return Promise.reject(err);
		});
	};

	class FetchError extends Error
	{
		constructor(code, message) {
			super(message);
			this.code = code || Notifications.JsonFalse;
		}
	}

	class AbstractFetchRemote
	{
		abort(sAction, sReason) {
			abort(sAction, sReason);
			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 <Proxy "fcgi://...." flushpackets=on></Proxy>
		 */
		streamPerLine(fCallback, sGetAdd, postData) {
			rl.fetch(getURL(sGetAdd), {}, postData)
			.then(response => response.body)
			.then(body => {
				let buffer = '';
				const
					// Firefox TextDecoderStream is not defined
	//				reader = body.pipeThrough(new TextDecoderStream()).getReader();
					reader = body.getReader(),
					re = /\r\n|\n|\r/gm,
					utf8decoder = new TextDecoder(),
					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;
						}
						// last line didn't end in a newline char
						buffer.length && fCallback(buffer);
					};
				reader.read().then(processText);
			});
		}

		/**
		 * @param {?Function} fCallback
		 * @param {string} sAction
		 * @param {Object=} oParameters
		 * @param {?number=} iTimeout
		 * @param {string=} sGetAdd = ''
		 */
		request(sAction, fCallback, params, iTimeout, sGetAdd) {
			params = params || {};

			const start = Date.now();

			fetchJSON(sAction, getURL(sGetAdd),
				sGetAdd ? null : (params || {}),
				pInt(iTimeout ?? 30000),
				async data => {
					let iError = 0;
					if (data) {
	/*
						if (sAction !== data.Action) {
							console.log(sAction + ' !== ' + data.Action);
						}
	*/
						if (data.Result) {
							iJsonErrorCount = 0;
						} else {
							checkResponseError(data);
							iError = data.code || Notifications.UnknownError;
						}
					}

					if (111 === iError && rl.app.ask && await rl.app.ask.cryptkey()) {
						return this.request(sAction, fCallback, params, iTimeout, sGetAdd);
					}

					fCallback && fCallback(
						iError,
						data,
						/**
						 * Responses like "304 Not Modified" are returned as "200 OK"
						 * This is an attempt to detect if the request comes from cache.
						 * But when client has wrong date/time, it will fail.
						 */
						data?.epoch && data.epoch < Math.floor(start / 1000) - 60
					);
				}
			)
			.catch(err => {
				console.error({fetchError:err});
				fCallback && fCallback(
					'TimeoutError' == err.name ? 3 : (err.name == 'AbortError' ? 2 : 1),
					err
				);
			});
		}

		setTrigger(trigger, value) {
			if (trigger) {
				value = !!value;
				(isArray(trigger) ? trigger : [trigger]).forEach(fTrigger => {
					fTrigger?.(value);
				});
			}
		}

		get(action, url) {
			return fetchJSON(action, url);
		}

		post(action, fTrigger, params, timeOut) {
			this.setTrigger(fTrigger, true);
			return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
				async data => {
					abort(action, 0, 1);

					if (!data) {
						return Promise.reject(new FetchError(Notifications.JsonParse));
					}

					if (111 === data?.code && rl.app.ask && await rl.app.ask.cryptkey()) {
						return this.post(action, fTrigger, params, timeOut);
					}
	/*
					let isCached = false, type = '';
					if (data?.epoch) {
						isCached = data.epoch > microtime() - start;
					}
					// backward capability
					switch (true) {
						case 'success' === textStatus && data?.Result && action === data.Action:
							type = AbstractFetchRemote.SUCCESS;
							break;
						case 'abort' === textStatus && (!data || !data.__aborted__):
							type = AbstractFetchRemote.ABORT;
							break;
						default:
							type = AbstractFetchRemote.ERROR;
							break;
					}
	*/
					this.setTrigger(fTrigger, false);

					if (!data.Result || action !== data.Action) {
						checkResponseError(data);
						return Promise.reject(new FetchError(
							data ? data.code : 0,
							data ? (data.messageAdditional || data.message) : ''
						));
					}

					return data;
				}
			);
		}
	}

	Object.assign(AbstractFetchRemote.prototype, {
		SUCCESS : 0,
		ERROR : 1,
		ABORT : 2
	});

	class RemoteAdminFetch extends AbstractFetchRemote {

		/**
		 * @param {string} key
		 * @param {?scalar} value
		 * @param {?Function} fCallback
		 */
		saveSetting(key, value, fCallback) {
			this.request('AdminSettingsUpdate', fCallback, {[key]: value});
		}

	}

	var Remote = new RemoteAdminFetch();

	class AbstractScreen {
		constructor(screenName, viewModels = []) {
			this.__cross = null;
			this.screenName = screenName;
			this.viewModels = isArray(viewModels) ? viewModels : [];
		}

		/**
		 * @returns {?Array)}
		 */
		routes() {
			return null;
		}

	/*
		onBuild(viewModelDom) {}
		onShow() {}
		onHide() {}
		__started
		__builded
	*/

		/**
		 * @returns {void}
		 */
		onStart() {
			const routes = this.routes();
			if (arrayLength(routes)) {
				let route = new Crossroads(),
					fMatcher = (this.onRoute || (()=>0)).bind(this);

				routes.forEach(item => item && (route.addRoute(item[0], fMatcher).rules = item[1]));

				this.__cross = route;
			}
		}
	}

	const VIEW_MODELS = [];

	class AbstractSettingsScreen extends AbstractScreen {
		/**
		 * @param {Array} viewModels
		 */
		constructor(viewModels) {
			super('settings', viewModels);

			this.menu = ko.observableArray();

			this.oCurrentSubScreen = null;
		}

		onRoute(subName) {
			let settingsScreen = null,
				viewModelDom = null,
				RoutedSettingsViewModel = VIEW_MODELS.find(
					SettingsViewModel => subName === SettingsViewModel.route
				);

			if (RoutedSettingsViewModel) {
	//			const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
				const vmPlace = this.viewModels[1].__vm.viewModelDom,
					SettingsViewModelClass = RoutedSettingsViewModel.vmc;
				if (SettingsViewModelClass.__vm) {
					settingsScreen = SettingsViewModelClass.__vm;
					viewModelDom = settingsScreen.viewModelDom;
				} else if (vmPlace) {
					viewModelDom = createElement('div',{
						id: 'V-Settings-' + SettingsViewModelClass.name.replace(/(User|Admin)Settings/,''),
						hidden: ''
					});
					vmPlace.append(viewModelDom);

					settingsScreen = new SettingsViewModelClass();
					settingsScreen.viewModelDom = viewModelDom;
					settingsScreen.viewModelTemplateID = RoutedSettingsViewModel.template;

					SettingsViewModelClass.__vm = settingsScreen;

					fireEvent('rl-view-model.create', settingsScreen);

					ko.applyBindingAccessorsToNode(
						viewModelDom,
						{
							template: () => ({ name: RoutedSettingsViewModel.template })
						},
						settingsScreen
					);

					settingsScreen.onBuild?.(viewModelDom);

					fireEvent('rl-view-model', settingsScreen);
				} else {
					console.log('Cannot find sub settings view model position: SettingsSubScreen');
				}

				if (settingsScreen) {
					setTimeout(() => {
						// hide
						this.onHide();
						// --

						this.oCurrentSubScreen = settingsScreen;

						// show
						settingsScreen.beforeShow?.();
						i18nToNodes(viewModelDom);
						viewModelDom.hidden = false;
						settingsScreen.onShow?.();

						this.menu.forEach(item => {
							item.selected(
								item.route === RoutedSettingsViewModel.route
							);
						});

						(vmPlace || {}).scrollTop = 0;
						// --
					}, 1);
				}
			} else {
				hasher.replaceHash(settings());
			}
		}

		onHide() {
			let subScreen = this.oCurrentSubScreen;
			if (subScreen) {
				subScreen.onHide?.();
				subScreen.viewModelDom.hidden = true;
			}
		}

		onBuild() {
			// TODO: issue on account switch
			// When current domain has sieve but the new has not, or current has not and the new has
			// SettingsViewModel.disabled() || this.menu.push()
			VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel));
		}

		routes() {
			const DefaultViewModel = VIEW_MODELS.find(
					SettingsViewModel => SettingsViewModel.isDefault
				),
				defaultRoute = DefaultViewModel?.route || 'general',
				rules = {
					subname: /^(.*)$/,
					normalize_: (rquest, vals) => {
						vals.subname = pString(vals.subname ?? defaultRoute);
						return [vals.subname];
					}
				};

			return [
				['{subname}/', rules],
				['{subname}', rules],
				['', rules]
			];
		}
	}

	/**
	 * @param {Function} SettingsViewModelClass
	 * @param {string} template
	 * @param {string} labelName
	 * @param {string} route
	 * @param {boolean=} isDefault = false
	 * @returns {void}
	 */
	function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
		let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
		VIEW_MODELS.push({
			vmc: SettingsViewModelClass,
			label: labelName || 'SETTINGS_LABELS/' + name.toUpperCase(),
			route: route || name.toLowerCase(),
			selected: ko.observable(false),
			template: template || SettingsViewModelClass.name,
			isDefault: !!isDefault
		});
	}

	let
		currentScreen = null,
		defaultScreenName = '';

	const
		SCREENS = new Map,

		autofocus = dom => dom.querySelector('[autofocus]')?.focus(),

		visiblePopups = new Set,

		/**
		 * @param {string} screenName
		 * @returns {?Object}
		 */
		screen = screenName => (screenName && SCREENS.get(screenName)) || null,

		/**
		 * Creates the extended AbstractView model
		 * @param {Function} ViewModelClass
		 * @param {Object=} vmScreen
		 * @returns {*}
		 */
		buildViewModel = (ViewModelClass, vmScreen) => {
			if (ViewModelClass && !ViewModelClass.__vm) {
				const
					vm = new ViewModelClass(vmScreen),
					id = vm.viewModelTemplateID,
					position = 'rl-' + vm.viewType,
					dialog = ViewTypePopup === vm.viewType,
					vmPlace = doc.getElementById(position);

				if (vmPlace) {
					ViewModelClass.__vm = vm;

					let vmDom = dialog
						? createElement('dialog',{id:'V-'+id})
						: createElement('div',{id:'V-'+id,hidden:''});
					vmPlace.append(vmDom);

					vm.viewModelDom = vmDom;

					if (dialog) {
						// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
						if (!vmDom.showModal) {
							vmDom.className = 'polyfill';
							vmDom.showModal = () => {
								vmDom.backdrop ||
									vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
								vmDom.setAttribute('open','');
								vmDom.open = true;
								vmDom.backdrop.hidden = false;
							};
							vmDom.close = () => {
	//							if (vmDom.dispatchEvent(new CustomEvent('cancel', {cancelable:true}))) {
									vmDom.backdrop.hidden = true;
									vmDom.removeAttribute('open', null);
									vmDom.open = false;
	//								vmDom.dispatchEvent(new CustomEvent('close'));
	//							}
							};
						}
						// https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/cancel_event
	//					vmDom.addEventListener('cancel', event => (false === vm.onClose() && event.preventDefault()));
	//					vmDom.addEventListener('close', () => vm.modalVisible(false));

						// show/hide popup/modal
						// transitionend is called for each property, so we only listen to `opacity`
						// as defined in CSS by `dialog:not(.animate)`
						const endShowHide = e => {
							if (e.target === vmDom && 'opacity' === e.propertyName) {
								if (vmDom.classList.contains('animate')) {
									vm.afterShow?.();
									fireEvent('rl-vm-visible', vm);
								} else {
									vmDom.close();
									vm.afterHide?.();
	//								fireEvent('rl-vm-hidden', vm);
								}
							}
						};

						vm.modalVisible.subscribe(value => {
							if (value) {
								i18nToNodes(vmDom);
								visiblePopups.add(vm);
								vmDom.style.zIndex = 3001 + (visiblePopups.size * 2);
								vmDom.showModal();
								if (vmDom.backdrop) {
									vmDom.backdrop.style.zIndex = 3000 + (visiblePopups.size * 2);
								}
								vm.keyScope.set();
								setTimeout(()=>autofocus(vmDom),1);
								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.keyScope.unset();
								vmDom.classList.remove('animate'); // trigger the transitions
							}
							arePopupsVisible(0 < visiblePopups.size);
						});
						vmDom.addEventListener('transitionend', endShowHide);
					}

					fireEvent('rl-view-model.create', vm);

					ko.applyBindingAccessorsToNode(
						vmDom,
						{
							template: () => ({ name: id })
						},
						vm
					);

					vm.onBuild?.(vmDom);

					fireEvent('rl-view-model', vm);
				} else {
					console.log('Cannot find view model position: ' + position);
				}
			}

			return ViewModelClass?.__vm;
		},

		forEachViewModel = (screen, fn) => {
			screen.viewModels.forEach(ViewModelClass => {
				if (
					ViewModelClass.__vm &&
					ViewTypePopup !== ViewModelClass.__vm.viewType
				) {
					fn(ViewModelClass.__vm, ViewModelClass.__vm.viewModelDom);
				}
			});
		},

		hideScreen = (screenToHide, destroy) => {
			screenToHide.onHide?.();
			forEachViewModel(screenToHide, (vm, dom) => {
				dom.hidden = true;
				vm.onHide?.();
				destroy && dom.remove();
			});
			ThemeStore.isMobile() && leftPanelDisabled(true);
		},

		/**
		 * @param {string} screenName
		 * @param {string} subPart
		 * @returns {void}
		 */
		screenOnRoute = (screenName, subPart) => {
			screenName = screenName || defaultScreenName;
			if (screenName && fireEvent('sm-show-screen', screenName + (subPart ?  '/' + subPart : ''), 1)) {
				// Close all popups
				for (let vm of visiblePopups) {
					vm.tryToClose();
				}

				let vmScreen = screen(screenName);
				if (!vmScreen) {
					vmScreen = screen(defaultScreenName);
					if (vmScreen) {
						subPart = screenName + '/' + subPart;
						screenName = defaultScreenName;
					}
				}

				if (vmScreen?.__started) {
					let isSameScreen = currentScreen && vmScreen === currentScreen;

					if (!vmScreen.__builded) {
						vmScreen.__builded = true;

						vmScreen.viewModels.forEach(ViewModelClass =>
							buildViewModel(ViewModelClass, vmScreen)
						);

						vmScreen.onBuild?.();
					}

					setTimeout(() => {
						// hide screen
						currentScreen && !isSameScreen && hideScreen(currentScreen);
						// --

						currentScreen = vmScreen;

						// show screen
						if (!isSameScreen) {
							vmScreen.onShow?.();

							forEachViewModel(vmScreen, (vm, dom) => {
								vm.beforeShow?.();
								i18nToNodes(dom);
								dom.hidden = false;
								vm.onShow?.();
								autofocus(dom);
							});
						}
						// --

						vmScreen.__cross?.parse(subPart);
					}, 1);
				}
			}
		};


	const
		ViewTypePopup = 'popups',

		/**
		 * @param {Function} ViewModelClassToShow
		 * @param {Array=} params
		 * @returns {void}
		 */
		showScreenPopup = (ViewModelClassToShow, params = []) => {
			const vm = buildViewModel(ViewModelClassToShow);
			if (vm) {
				params = params || [];
				vm.beforeShow?.(...params);
				vm.modalVisible(true);
				vm.onShow?.(...params);
			}
		},

		arePopupsVisible = ko.observable(false),

		/**
		 * @param {Array} screensClasses
		 * @returns {void}
		 */
		startScreens = screensClasses => {
			hasher.clear();
			SCREENS.forEach(screen => hideScreen(screen, 1));
			SCREENS.clear();
			currentScreen = null,
			defaultScreenName = '';

			screensClasses.forEach(CScreen => {
				const vmScreen = new CScreen(),
					screenName = vmScreen.screenName;
				defaultScreenName || (defaultScreenName = screenName);
				SCREENS.set(screenName, vmScreen);
			});

			SCREENS.forEach(vmScreen => {
				if (!vmScreen.__started) {
					vmScreen.onStart();
					vmScreen.__started = true;
				}
			});

			const cross = new Crossroads();
			cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);

			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?.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;

	class AbstractView {
		constructor(templateID, type)
		{
	//		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);
				}
			};
		}

	/*
		onBuild() {}
		beforeShow() {} // Happens before: hidden = false
		onShow() {}       // Happens after: hidden = false
		onHide() {}
	*/

		querySelector(selectors) {
			return this.viewModelDom.querySelector(selectors);
		}

		addObservables(observables) {
			addObservablesTo(this, observables);
		}

		addComputables(computables) {
			addComputablesTo(this, computables);
		}

		addSubscribables(subscribables) {
			addSubscribablesTo(this, subscribables);
		}

	}

	class AbstractViewPopup extends AbstractView
	{
		constructor(name)
		{
			super('Popups' + name, ViewTypePopup);
			this.keyScope.scope = name;
			this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
			this.close = () => this.modalVisible(false);
			this.tryToClose = () => (false === this.onClose()) || this.close();
			addShortcut('escape,close', '', name, () => {
				this.modalVisible() && this.tryToClose();
				return false;
	//			return true; Issue with supported modal close
			});
		}

		// 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
	*/
	}

	AbstractViewPopup.showModal = function(params = []) {
		showScreenPopup(this, params);
	};

	AbstractViewPopup.hidden = function() {
		return !this.__vm || !this.__vm.modalVisible();
	};

	class AbstractViewLeft extends AbstractView
	{
		constructor(templateID)
		{
			super(templateID, 'left');
			this.toggleLeftPanel = toggleLeftPanel;
		}
	}

	class AbstractViewRight extends AbstractView
	{
		constructor(templateID)
		{
			super(templateID, 'right');
		}
	}

	class AbstractViewSettings
	{
	/*
		onBuild(viewModelDom) {}
		beforeShow() {}
		onShow() {}
		onHide() {}
		viewModelDom
	*/
		/**
		 * When this[name] does not exists, create as observable with value of SettingsGet(name)
		 * When this[name+'Trigger'] does not exists, create as observable
		 * Subscribe to this[name], and handle saving the setting
		 */
		addSetting(name, valueCb)
		{
			let prop = name[0].toLowerCase() + name.slice(1),
				trigger = prop + 'Trigger';
			addObservablesTo(this, {
				[prop]: SettingsGet(name),
				[trigger]: SaveSettingStatus.Idle,
			});
			addSubscribablesTo(this, {
				[prop]: (value => {
					this[trigger](SaveSettingStatus.Saving);
					valueCb?.(value);
					rl.app.Remote.saveSetting(name, value,
						iError => {
							this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
	//						iError || Settings.set(name, value);
							setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
						}
					);
				}).debounce(999),
			});
		}

		/**
		 * Foreach name if this[name] does not exists, create as observable with value of SettingsGet(name)
		 * Subscribe to this[name], for saving the setting
		 */
		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));
			});
		}
	}

	class AbstractViewLogin extends AbstractView {
		constructor(templateID) {
			super(templateID, 'content');
			this.formError = ko.observable(false).extend({ falseTimeout: 500 });
		}

		onBuild(dom) {
			dom.classList.add('LoginView');
		}

		onShow() {
			elementById('rl-left').hidden = true;
			elementById('rl-right').hidden = true;
			rl.route.off();
		}

		onHide() {
			elementById('rl-left').hidden = false;
			elementById('rl-right').hidden = false;
		}

		submitForm() {
	//		return false;
		}
	}

	const USER_VIEW_MODELS_HOOKS = [],
		ADMIN_VIEW_MODELS_HOOKS = [];

	/**
	 * @param {Function} callback
	 * @param {string} action
	 * @param {Object=} parameters
	 * @param {?number=} timeout
	 */
	rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
		rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
	};

	/**
	 * @param {Function} SettingsViewModelClass
	 * @param {string} labelName
	 * @param {string} template
	 * @param {string} route
	 */
	rl.addSettingsViewModel = (SettingsViewModelClass, template, labelName, route) => {
		USER_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
	};

	/**
	 * @param {Function} SettingsViewModelClass
	 * @param {string} labelName
	 * @param {string} template
	 * @param {string} route
	 */
	rl.addSettingsViewModelForAdmin = (SettingsViewModelClass, template, labelName, route) => {
		ADMIN_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
	};

	/**
	 * @param {boolean} admin
	 */
	function runSettingsViewModelHooks(admin) {
		(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view =>
			settingsAddViewModel(...view)
		);
	}

	/**
	 * @param {string} pluginSection
	 * @param {string} name
	 * @returns {?}
	 */
	rl.pluginSettingsGet = (pluginSection, name) =>
		SettingsGet('Plugins')?.[pluginSection]?.[name];

	rl.pluginPopupView = AbstractViewPopup;

	class LanguagesPopupView extends AbstractViewPopup {
		constructor() {
			super('Languages');
			this.fLang = null;
			this.languages = ko.observableArray();
		}

		onShow(fLanguage, langs, userLanguage) {
			this.fLang = fLanguage;
			this.languages(langs.map(language => ({
				key: language,
				user: userLanguage === language,
				selected: fLanguage?.() === language,
				fullName: convertLangName(language),
				title: convertLangName(language, true)
			})));
		}

		changeLanguage(lang) {
			this.fLang?.(lang);
			this.close();
		}
	}

	class AdminSettingsGeneral extends AbstractViewSettings {
		constructor() {
			super();

			this.language = LanguageStore.language;
			this.languageAdmin = ko.observable(SettingsAdmin('language'));

			this.theme = ThemeStore.theme;
			this.themes = ThemeStore.themes;

			this.addSettings(['allowLanguagesOnSettings']);

			addObservablesTo(this, {
				capaThemes: SettingsCapa('Themes'),
				capaUserBackground: SettingsCapa('UserBackground'),
				capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
				capaIdentities: SettingsCapa('Identities'),
				capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
				dataFolderAccess: false
			});

			this.weakPassword = rl.app.weakPassword;

			/** https://github.com/RainLoop/rainloop-webmail/issues/1924
			if (this.weakPassword) {
				fetch('./data/VERSION?' + Math.random()).then(response => this.dataFolderAccess(response.ok));
			}
			*/

			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?.upload_max_filesize || this.uploadData?.post_max_size)
					? [
							this.uploadData.upload_max_filesize
								? 'upload_max_filesize = ' + this.uploadData.upload_max_filesize + '; '
								: '',
							this.uploadData.post_max_size ? 'post_max_size = ' + this.uploadData.post_max_size : ''
					  ].join('')
					: '';

			addComputablesTo(this, {
				themesOptions: () => this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })),

				languageFullName: () => convertLangName(this.language()),
				languageAdminFullName: () => convertLangName(this.languageAdmin())
			});

			this.languageAdminTrigger = ko.observable(SaveSettingStatus.Idle).extend({ debounce: 100 });

			const fReloadLanguageHelper = (saveSettingsStep) => () => {
					this.languageAdminTrigger(saveSettingsStep);
					setTimeout(() => this.languageAdminTrigger(SaveSettingStatus.Idle), 1000);
				},
				fSaveHelper = key => value => Remote.saveSetting(key, value);

			addSubscribablesTo(this, {
				languageAdmin: value => {
					this.languageAdminTrigger(SaveSettingStatus.Saving);
					translatorReload(value, 1)
						.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
						.then(() => Remote.saveSetting('languageAdmin', value));
				},

				capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),

				capaIdentities: fSaveHelper('CapaIdentities'),

				capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),

				capaThemes: fSaveHelper('CapaThemes'),

				capaUserBackground: fSaveHelper('CapaUserBackground')
			});
		}

		selectLanguage() {
			showScreenPopup(LanguagesPopupView, [
				this.language,
				LanguageStore.languages,
				LanguageStore.userLanguage()
			]);
		}

		selectLanguageAdmin() {
			showScreenPopup(LanguagesPopupView, [
				this.languageAdmin,
				SettingsAdmin('languages'),
				SettingsAdmin('clientLanguage')
			]);
		}
	}

	const DomainAdminStore = ko.observableArray();

	DomainAdminStore.loading = ko.observable(false);

	DomainAdminStore.fetch = () => {
		DomainAdminStore.loading(true);
		Remote.request('AdminDomainList',
			(iError, data) => {
				DomainAdminStore.loading(false);
				if (!iError) {
					DomainAdminStore(
						data.Result.map(item => {
							item.name = IDN.toUnicode(item.name);
							item.disabled = ko.observable(item.disabled);
							item.askDelete = ko.observable(false);
							return item;
						})
					);
				}
			}, {
				includeAliases: 1
			});
	};

	class AskPopupView extends AbstractViewPopup {
		constructor() {
			super('Ask');

			addObservablesTo(this, {
				askDesc: '',
				yesButton: '',
				noButton: '',
				username: '',
				askUsername: false,
				passphrase: '',
				askPass: false,
				remember: true,
				askRemeber: false
			});

			this.fYesAction = null;
			this.fNoAction = null;

			this.focusOnShow = true;
		}

		yesClick() {
			this.close();

			isFunction(this.fYesAction) && this.fYesAction(this);
		}

		noClick() {
			this.close();

			isFunction(this.fNoAction) && this.fNoAction(this);
		}

		/**
		 * @param {string} sAskDesc
		 * @param {Function=} fYesFunc
		 * @param {Function=} fNoFunc
		 * @param {boolean=} focusOnShow = true
		 * @returns {void}
		 */
		onShow(sAskDesc, fYesFunc = null, fNoFunc = null, focusOnShow = true, ask = 0, btnText = '') {
			this.askDesc(sAskDesc || '');
			this.askUsername(ask & 2);
			this.askPass(ask & 1);
			this.askRemeber(ask & 4);
			this.username('');
			this.passphrase('');
			this.remember(true);
			this.yesButton(i18n(btnText || 'GLOBAL/YES'));
			this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
			this.fYesAction = fYesFunc;
			this.fNoAction = fNoFunc;
			this.focusOnShow = focusOnShow
				? (ask ? 'input[type="'+(ask&2?'text':'password')+'"]' : '.buttonYes')
				: '';
		}

		afterShow() {
			this.focusOnShow && this.querySelector(this.focusOnShow).focus();
		}

		onClose() {
			this.noClick();
			return false;
		}

		onBuild() {
	//		shortcuts.add('tab', 'shift', 'Ask', () => {
			shortcuts.add('tab,arrowright,arrowleft', '', 'Ask', () => {
				let yes = this.querySelector('.buttonYes'),
					no = this.querySelector('.buttonNo');
				if (yes.matches(':focus')) {
					no.focus();
					return false;
				} else if (no.matches(':focus')) {
					yes.focus();
					return false;
				}
			});
		}
	}

	const
		capitalize = string => string.charAt(0).toUpperCase() + string.slice(1),
		domainDefaults = {
			enableSmartPorts: false,

			savingError: '',

			name: '',

			imapHost: '',
			imapPort: 143,
			imapType: 0,
			imapTimeout: 300,
			imapShortLogin: false,
			imapLowerLogin: true,
			// SSL
			imapSslVerify_peer: false,
			imapSslAllow_self_signed: false,
			// Options
			imapExpunge_all_on_delete: false,
			imapFast_simple_search: true,
			imapFetch_new_messages: true,
			imapForce_select: false,
			imapFolder_list_limit: 200,
			imapMessage_all_headers: false,
			imapMessage_list_limit: 10000,
			imapSearch_filter: '',
			imapSpam_headers: '',
			imapVirus_headers: '',

			sieveEnabled: false,
			sieveHost: '',
			sievePort: 4190,
			sieveType: 0,
			sieveTimeout: 10,
			sieveAuthLiteral: true,

			smtpHost: '',
			smtpPort: 25,
			smtpType: 0,
			smtpTimeout: 60,
			smtpShortLogin: false,
			smtpLowerLogin: true,
			smtpUseAuth: true,
			smtpSetSender: false,
			smtpAuthPlainLine: false,
			smtpUsePhpMail: false,
			// SSL
			smtpSslVerify_peer: false,
			smtpSslAllow_self_signed: false,

			whiteList: '',
			aliasName: ''
		},
		domainToParams = oDomain => ({
			name: oDomain.name,
			IMAP: {
				host: oDomain.imapHost,
				port: oDomain.imapPort,
				secure: pInt(oDomain.imapType()),
				timeout: oDomain.imapTimeout,
				shortLogin: !!oDomain.imapShortLogin(),
				lowerLogin: !!oDomain.imapLowerLogin(),
				ssl: {
					verify_peer: !!oDomain.imapSslVerify_peer(),
					verify_peer_name: !!oDomain.imapSslVerify_peer(),
					allow_self_signed: !!oDomain.imapSslAllow_self_signed()
				},
				disabled_capabilities:  oDomain.imapDisabled_capabilities(),
				folder_list_limit: pInt(oDomain.imapFolder_list_limit()),
				message_list_limit: pInt(oDomain.imapMessage_list_limit()),
	/*
				expunge_all_on_delete: ,
				fast_simple_search: ,
				fetch_new_messages: ,
				force_select: ,
				message_all_headers: ,
	*/
				search_filter: oDomain.imapSearch_filter(),
				spam_headers: oDomain.imapSpam_headers(),
				virus_headers: oDomain.imapVirus_headers()
			},
			SMTP: {
				host: oDomain.smtpHost,
				port: oDomain.smtpPort,
				secure: pInt(oDomain.smtpType()),
				timeout: oDomain.smtpTimeout,
				shortLogin: !!oDomain.smtpShortLogin(),
				lowerLogin: !!oDomain.smtpLowerLogin(),
				ssl: {
					verify_peer: !!oDomain.smtpSslVerify_peer(),
					verify_peer_name: !!oDomain.smtpSslVerify_peer(),
					allow_self_signed: !!oDomain.smtpSslAllow_self_signed()
				},
				setSender: !!oDomain.smtpSetSender(),
				authPlainLine: !!oDomain.smtpAuthPlainLine(),
				useAuth: !!oDomain.smtpUseAuth(),
				usePhpMail: !!oDomain.smtpUsePhpMail()
			},
			Sieve: {
				enabled: !!oDomain.sieveEnabled(),
				authLiteral: !!oDomain.sieveAuthLiteral(),
				host: oDomain.sieveHost,
				port: oDomain.sievePort,
				secure: pInt(oDomain.sieveType()),
				timeout: oDomain.sieveTimeout,
				shortLogin: !!oDomain.imapShortLogin(),
				lowerLogin: !!oDomain.imapLowerLogin(),
				ssl: {
					verify_peer: !!oDomain.imapSslVerify_peer(),
					verify_peer_name: !!oDomain.imapSslVerify_peer(),
					allow_self_signed: !!oDomain.imapSslAllow_self_signed()
				}
			},
			whiteList: oDomain.whiteList
		});

	class DomainPopupView extends AbstractViewPopup {
		constructor() {
			super('Domain');

			addObservablesTo(this, domainDefaults);
			addObservablesTo(this, {
				edit: false,

				saving: false,

				testing: false,
				testingDone: false,
				testingImapError: false,
				testingSieveError: false,
				testingSmtpError: false,

				imapHostFocus: false,
				sieveHostFocus: false,
				smtpHostFocus: false,

				detectingConfig: false
			});
			this.imapDisabled_capabilities = ko.observableArray();
			this.imapCapabilities = ko.observableArray();

			addComputablesTo(this, {
				headerText: () => {
					const name = this.name(),
						aliasName = this.aliasName();
					return this.edit()
						? i18n('POPUPS_DOMAIN/TITLE_EDIT_DOMAIN', { NAME: name }) + (aliasName ? ' ⫘ ' + aliasName : '')
						: (name
							? i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN_WITH_NAME', { NAME: name })
							: i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN'));
				},

				domainDesc: () => {
					const name = this.name();
					return !this.edit() && name ? i18n('POPUPS_DOMAIN/NEW_DOMAIN_DESC', { NAME: '*@' + name }) : '';
				},

				domainIsComputed: () => {
					const usePhpMail = this.smtpUsePhpMail(),
						sieveEnabled = this.sieveEnabled();

					return (
						this.name() &&
						this.imapHost() &&
						this.imapPort() &&
						(sieveEnabled ? this.sieveHost() && this.sievePort() : true) &&
						((this.smtpHost() && this.smtpPort()) || usePhpMail)
					);
				},

				canBeTested: () => !this.testing() && this.domainIsComputed(),
				canBeSaved: () => !this.saving() && this.domainIsComputed()
			});

			addSubscribablesTo(this, {
				// smart form improvements
				imapHostFocus: value =>
					value && this.name() && !this.imapHost() && this.imapHost(this.name().replace(/[.]?[*][.]?/g, '')),

				sieveHostFocus: value =>
					value && this.imapHost() && !this.sieveHost() && this.sieveHost(this.imapHost()),

				smtpHostFocus: value => value && this.imapHost() && !this.smtpHost()
					&& this.smtpHost(this.imapHost().replace(/imap/gi, 'smtp')),

				imapType: value => {
					if (this.enableSmartPorts()) {
						const port = pInt(this.imapPort());
						switch (pInt(value)) {
							case 0:
							case 2:
								if (993 === port) {
									this.imapPort(143);
								}
								break;
							case 1:
								if (143 === port) {
									this.imapPort(993);
								}
								break;
							// no default
						}
					}
				},

				smtpType: value => {
					if (this.enableSmartPorts()) {
						const port = pInt(this.smtpPort());
						switch (pInt(value)) {
							case 0:
								if (465 === port || 587 === port) {
									this.smtpPort(25);
								}
								break;
							case 1:
								if (25 === port || 587 === port) {
									this.smtpPort(465);
								}
								break;
							case 2:
								if (25 === port || 465 === port) {
									this.smtpPort(587);
								}
								break;
							// no default
						}
					}
				}
			});

			decorateKoCommands(this, {
				createOrAddCommand: self => self.canBeSaved(),
				testConnectionCommand: self => self.canBeTested()
			});
		}

		createOrAddCommand() {
			this.saving(true);
			Remote.request('AdminDomainSave',
				iError => {
					this.saving(false);
					if (iError) {
						this.savingError(getNotification(iError));
					} else {
						DomainAdminStore.fetch();
						this.close();
					}
				},
				Object.assign(domainToParams(this), {
					create: this.edit() ? 0 : 1
				})
			);
		}

		testConnectionCommand() {
			this.clearTesting();
			// https://github.com/the-djmaze/snappymail/issues/477
			AskPopupView.credentials('IMAP', 'GLOBAL/TEST').then(credentials => {
				if (credentials) {
					this.testing(true);
					const params = domainToParams(this);
					params.auth = {
						user: credentials.username,
						pass: credentials.password
					};
					Remote.request('AdminDomainTest',
						(iError, oData) => {
							this.testing(false);
							if (iError) {
								this.testingImapError(getNotification(iError));
								this.testingSieveError(getNotification(iError));
								this.testingSmtpError(getNotification(iError));
							} else {
								const result = oData.Result;
								this.testingDone(true);
								this.testingImapError(true !== result.Imap ? result.Imap : false);
								this.testingSieveError(true !== result.Sieve ? result.Sieve : false);
								this.testingSmtpError(true !== result.Smtp ? result.Smtp : false);
								// result.ImapResult.connectCapa
								if (true === result.Imap) {
									let capa = result.ImapResult.authCapa
										|| ['LIST-STATUS','METADATA','MOVE','SORT','THREAD','BINARY','STATUS=SIZE','PREVIEW'];
									capa = capa.concat(result.ImapResult.connectCapa).unique();
									capa.sort();
									this.imapCapabilities(capa);
								}
								// result.SmtpResult.connectCapa
								// result.SmtpResult.authCapa
								// result.SieveResult.connectCapa
								// result.SieveResult.authCapa
							}
						},
						params
					);
				}
			});
		}

		clearTesting() {
			this.testing(false);
			this.testingDone(false);
			this.testingImapError(false);
			this.testingSieveError(false);
			this.testingSmtpError(false);
		}

		autoconfig() {
			this.detectingConfig(true);
			let domain = this.name();
			Remote.request('AdminDomainAutoconfig', (iError, oData) => {
				if (oData?.Result?.config) {
					let server = oData.Result.config.incomingServer[0];
					this.imapHost(server.hostname);
					this.imapPort(server.port);
					this.imapType('STARTTLS' === server.socketType ? 2 : ('SSL' === server.socketType ? 1 : 0));
					this.imapShortLogin('%EMAILADDRESS%' !== server.username);

					server = oData.Result.config.outgoingServer[0];
					this.smtpHost(server.hostname);
					this.smtpPort(server.port);
					this.smtpType('STARTTLS' === server.socketType ? 2 : ('SSL' === server.socketType ? 1 : 0));
					this.smtpShortLogin('%EMAILADDRESS%' !== server.username);
					this.smtpUseAuth(!!server.authentication);
					this.smtpUsePhpMail(false);
				}
				this.detectingConfig(false);
			}, {domain});
		}

		onShow(oDomain) {
			this.saving(false);
			this.clearTesting();
			this.edit(false);
			this.imapCapabilities([
				'BINARY',
				'LIST-STATUS',
				'METADATA',
				'MOVE',
				'NAMESPACE',
				'PREVIEW',
				'SORT',
				'STATUS=SIZE',
				'THREAD'
			]);
			this.imapDisabled_capabilities(['METADATA','OBJECTID','PREVIEW','STATUS=SIZE']);
			forEachObjectEntry(domainDefaults, (key, value) => this[key](value));
			this.enableSmartPorts(true);
			if (oDomain) {
				this.enableSmartPorts(false);
				this.edit(true);
				forEachObjectEntry(oDomain, (key, value) => {
					if ('IMAP' === key || 'SMTP' === key || 'Sieve' === key) {
						key = key.toLowerCase();
						forEachObjectEntry(value, (skey, value) => {
							skey = capitalize(skey);
							if ('Ssl' == skey) {
								forEachObjectEntry(value, (sslkey, value) => {
									this[key + skey + capitalize(sslkey)]?.(value);
								});
							} else {
								this[key + skey]?.(value);
							}
						});
					} else {
						this[key]?.(value);
					}
				});
				this.name(IDN.toUnicode(this.name()));
				this.aliasName(IDN.toUnicode(this.aliasName()));
				this.imapCapabilities(this.imapCapabilities.concat(this.imapDisabled_capabilities()).unique());
				this.enableSmartPorts(true);
			}
		}
	}

	class DomainAliasPopupView extends AbstractViewPopup {
		constructor() {
			super('DomainAlias');

			addObservablesTo(this, {
				saving: false,
				savingError: '',

				name: '',

				alias: ''
			});

			addComputablesTo(this, {
				domains: () => DomainAdminStore.filter(item => item && !item.alias),

				domainsOptions: () => this.domains().map(item => ({ optValue: item.name, optText: item.name })),

				canBeSaved: () => !this.saving() && this.name() && this.alias()
			});

			decorateKoCommands(this, {
				createCommand: self => self.canBeSaved()
			});
		}

		createCommand() {
			this.saving(true);
			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() {
			this.saving(false);
			this.savingError('');
			this.name('');
			this.alias('');
		}
	}

	class AdminSettingsDomains /*extends AbstractViewSettings*/ {
		constructor() {
			this.domains = DomainAdminStore;
			this.username = ko.observable('');
			this.domainForDeletion = ko.observable(null).askDeleteHelper();
		}

		testUsername() {
			Remote.request('AdminDomainMatch',
				(iError, oData) => {
					if (oData?.Result?.domain) {
						alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`);
					} else {
						alert('No domain match');
					}
				},
				{
					username: this.username
				}
			);
		}

		createDomain() {
			showScreenPopup(DomainPopupView);
		}

		createDomainAlias() {
			showScreenPopup(DomainAliasPopupView);
		}

		deleteDomain(domain) {
			DomainAdminStore.remove(domain);
			Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
				name: domain.name
			});
		}

		disableDomain(domain) {
			domain.disabled(!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-action', oDom);
				el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
					(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
					{
						name: ko.dataFor(el).name
					}
				);

			});

			DomainAdminStore.fetch();
		}
	}

	class AdminSettingsLogin extends AbstractViewSettings {
		constructor() {
			super();
			this.addSetting('loginDefaultDomain');
			this.addSettings(['determineUserLanguage','determineUserDomain','allowLanguagesOnLogin']);
		}
	}

	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']);

			this.addSetting('contactsMySQLSSLCA');
			this.addSetting('contactsMySQLSSLVerify');
			this.addSetting('contactsMySQLSSLCiphers');

			this.addSetting('contactsSQLiteGlobal');

			addObservablesTo(this, {
				testing: false,
				testContactsSuccess: false,
				testContactsError: false,
				testContactsErrorMessage: ''
			});

			this.addSetting('contactsSuggestionsLimit');

			const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
				types = [{
					id:'sqlite',
					name:'SQLite'
				},{
					id:'mysql',
					name:'MySQL'
				},{
					id:'pgsql',
					name:'PostgreSQL'
				}].filter(type => supportedTypes.includes(type.id));

			this.contactsSupported = 0 < types.length;

			this.contactsTypesOptions = types;

			this.mainContactsType = ko
				.computed({
					read: this.contactsPdoType,
					write: value => {
						if (value !== this.contactsPdoType()) {
							if (supportedTypes.includes(value)) {
								this.contactsPdoType(value);
							} else if (types.length) {
								this.contactsPdoType('');
							}
						} else {
							this.contactsPdoType.valueHasMutated();
						}
					}
				})
				.extend({ notify: 'always' });

			decorateKoCommands(this, {
				testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
			});
		}

		testContactsCommand() {
			this.testContactsSuccess(false);
			this.testContactsError(false);
			this.testContactsErrorMessage('');
			this.testing(true);

			Remote.request('AdminContactsTest',
				(iError, data) => {
					this.testContactsSuccess(false);
					this.testContactsError(false);
					this.testContactsErrorMessage('');

					if (!iError && data.Result.Result) {
						this.testContactsSuccess(true);
					} else {
						this.testContactsError(true);
						this.testContactsErrorMessage(data?.Result?.Message || '');
					}

					this.testing(false);
				}, {
					PdoType: this.contactsPdoType(),
					PdoDsn: this.contactsPdoDsn(),
					PdoUser: this.contactsPdoUser(),
					PdoPassword: this.contactsPdoPassword(),
					MySQLSSLCA: this.contactsMySQLSSLCA(),
					MySQLSSLVerify: this.contactsMySQLSSLVerify(),
					MySQLSSLCiphers: this.contactsMySQLSSLCiphers(),
					SQLiteGlobal: this.contactsSQLiteGlobal()
				}
			);
		}

		onShow() {
			this.testContactsSuccess(false);
			this.testContactsError(false);
			this.testContactsErrorMessage('');
		}
	}

	class AdminSettingsSecurity extends AbstractViewSettings {
		constructor() {
			super();

			this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);

			this.weakPassword = rl.app.weakPassword;

			addObservablesTo(this, {
				adminLogin: SettingsGet('adminLogin'),
				adminLoginError: false,
				adminPassword: '',
				adminPasswordNew: '',
				adminPasswordNew2: '',
				adminPasswordNewError: false,
				adminTOTP: '',

				saveError: false,
				saveSuccess: false,

				viewQRCode: '',

				capaGnuPG: SettingsCapa('GnuPG'),
				capaOpenPGP: SettingsCapa('OpenPGP')
			});

			this.gnuPGversion = 'GnuPG v' + SettingsGet('gnupg');

			const reset = () => {
				this.saveError(false);
				this.saveSuccess(false);
				this.adminPasswordNewError(false);
			};

			addSubscribablesTo(this, {
				adminPassword: () => {
					this.saveError(false);
					this.saveSuccess(false);
				},

				adminLogin: () => this.adminLoginError(false),

				adminTOTP: value => {
					if (/[A-Z2-7]{16,}/.test(value) && 0 == value.length * 5 % 8) {
						Remote.request('AdminQRCode', (iError, data) => {
							if (!iError) {
								console.dir({data:data});
								this.viewQRCode(data.Result);
							}
						}, {
							'username': this.adminLogin(),
							'TOTP': this.adminTOTP()
						});
					} else {
						this.viewQRCode('');
					}
				},

				adminPasswordNew: reset,

				adminPasswordNew2: reset,

				capaGnuPG: value => Remote.saveSetting('capaGnuPG', value),
				capaOpenPGP: value => Remote.saveSetting('capaOpenPGP', value)
			});

			this.adminTOTP(SettingsGet('adminTOTP'));

			decorateKoCommands(this, {
				saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()
			});
		}

		generateTOTP() {
			let CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
				length = 16,
				secret = '';
			while (0 < length--) {
				secret += CHARS[Math.floor(Math.random() * 32)];
			}
			this.adminTOTP(secret);
		}

		saveAdminUserCommand() {
			if (!this.adminLogin().trim()) {
				this.adminLoginError(true);
				return false;
			}

			if (this.adminPasswordNew() !== this.adminPasswordNew2()) {
				this.adminPasswordNewError(true);
				return false;
			}

			this.saveError(false);
			this.saveSuccess(false);

			Remote.request('AdminPasswordUpdate', (iError, data) => {
				if (iError) {
					this.saveError(true);
				} else {
					this.adminPassword('');
					this.adminPasswordNew('');
					this.adminPasswordNew2('');

					this.saveSuccess(true);

					this.weakPassword(!!data.Result.Weak);
				}
			}, {
				Login: this.adminLogin(),
				Password: this.adminPassword(),
				newPassword: this.adminPasswordNew(),
				TOTP: this.adminTOTP()
			});

			return true;
		}

		onHide() {
			this.adminPassword('');
			this.adminPasswordNew('');
			this.adminPasswordNew2('');
		}
	}

	const PackageAdminStore = ko.observableArray();

	PackageAdminStore.real = ko.observable(true);

	PackageAdminStore.loading = ko.observable(false);

	PackageAdminStore.error = ko.observable('');

	PackageAdminStore.fetch = () => {
		PackageAdminStore.loading(true);
		Remote.request('AdminPackagesList', (iError, data) => {
			PackageAdminStore.loading(false);
			if (iError) {
				PackageAdminStore.real(false);
				PackageAdminStore.error(getNotification(iError));
	//			let error = getNotification(iError);
	//			if (data.message) { error = data.message + error; }
	//			if (data.reason) { error = data.reason + " " + error; }
	//			PackageAdminStore.error(error);
			} else {
				PackageAdminStore.real(!!data.Result.Real);
				PackageAdminStore.error(data.Result.Error);

				const loading = {};
				PackageAdminStore.forEach(item => {
					if (item?.loading()) {
						loading[item.file] = item;
					}
				});

				let list = [];
				if (isArray(data.Result.List)) {
					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);
			}
		});
	};

	class PluginPopupView extends AbstractViewPopup {
		constructor() {
			super('Plugin');

			addObservablesTo(this, {
				saveError: '',
				id: '',
				name: '',
				readme: '',
				author: '',
				url: '',
				version: '',
				released: ''
			});

			this.config = ko.observableArray();

			addComputablesTo(this, {
				hasReadme: () => !!this.readme(),
				hasConfiguration: () => 0 < this.config().length
			});

			this.keyScope.scope = 'all';

			decorateKoCommands(this, {
				saveCommand: self => self.hasConfiguration()
			});
		}

		hideError() {
			this.saveError('');
		}

		saveCommand() {
			const oConfig = {
				id: this.id,
				settings: {}
			},
			setItem = item => {
				let value = item.value();
				if (false === value || true === value) {
					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);
				}
			});

			this.saveError('');
			Remote.request('AdminPluginSettingsUpdate',
				iError => iError
					? this.saveError(getNotification(iError))
					: this.close(),
				oConfig);
		}

		onShow(oPlugin) {
			this.id('');
			this.name('');
			this.readme('');
			this.author('');
			this.url('');
			this.version('');
			this.released('');
			this.config([]);

			if (oPlugin) {
				this.id(oPlugin.id);
				this.name(oPlugin.name);
				this.readme(oPlugin.readme);
				this.author(oPlugin.author);
				this.url(oPlugin.url);
				this.version(oPlugin.version);
				this.released(oPlugin.released);

				const config = oPlugin.config;
				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;
						})
					);
				}
			}
		}

		onClose() {
			if (AskPopupView.hidden()) {
				showScreenPopup(AskPopupView, [
					i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
					() => this.close()
				]);
			}
			return false;
		}
	}

	class AdminSettingsPackages extends AbstractViewSettings {
		constructor() {
			super();

			this.addSettings(['pluginsEnable']);

			addObservablesTo(this, {
				packagesError: '',
				search: ''
			});

			this.packages = PackageAdminStore;

			addComputablesTo(this, {
				packagesCurrent: () => PackageAdminStore().filter(item => item?.installed && !item.canBeUpdated),
				packagesUpdate: () => PackageAdminStore().filter(item => item?.installed && item.canBeUpdated),
				packagesAvailable: () => PackageAdminStore().filter(item => !item?.installed),

				visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
			});

			this.search.subscribe(value => {
				const v = value.toLowerCase(),
					qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
					match = node => node.textContent.toLowerCase().includes(v);
				if (v.length) {
					qsa(this.viewModelDom, 'td:first-child', td => {
						td.parentNode.hidden = !match(td);
					});
				} else {
					qsa(this.viewModelDom, 'tr[hidden]', n => n.hidden = false);
				}
			});
		}

		onShow() {
			this.packagesError('');
		}

		onBuild(oDom) {
			PackageAdminStore.fetch();

			oDom.addEventListener('click', event => {
				// configurePlugin
				let el = event.target.closestWithin('.package-configure', oDom),
					data = el && ko.dataFor(el);
				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);
				data && this.disablePlugin(data);
			});
		}

		requestHelper(packageToRequest, install) {
			return (iError, data) => {
				PackageAdminStore.forEach(item => {
					if (packageToRequest && item?.loading?.() && packageToRequest.file === item.file) {
						packageToRequest.loading(false);
						item.loading(false);
					}
				});

				if (iError) {
					this.packagesError(
						getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
						+ (data.message ? ':\n' + data.message : '')
					);
				} else if (data.Result.Reload) {
					location.reload();
				} else {
					PackageAdminStore.fetch();
				}
			};
		}

		deletePackage(packageToDelete) {
			if (packageToDelete) {
				packageToDelete.loading(true);
				Remote.request('AdminPackageDelete',
					this.requestHelper(packageToDelete, false),
					{
						id: packageToDelete.id
					}
				);
			}
		}

		installPackage(packageToInstall) {
			if (packageToInstall) {
				packageToInstall.loading(true);
				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(
							(Notifications.UnsupportedPluginPackage === iError && data?.message)
							? data.message
							: getNotification(iError)
						);
					}
	//				PackageAdminStore.fetch();
				}, {
					id: plugin.id,
					disabled: disable ? 1 : 0
				}
			);
		}

	}

	class AdminSettingsAbout /*extends AbstractViewSettings*/ {
		constructor() {
			this.version = Settings.app('version');
			this.phpextensions = ko.observableArray();

			addObservablesTo(this, {
				coreReal: true,
				coreUpdatable: true,
				coreWarning: false,
				coreVersion: '',
				coreVersionCompare: -2,
				php64: true,
				load1: 0,
				load5: 0,
				load15: 0,
				errorDesc: ''
			});
			this.coreChecking = ko.observable(false).extend({ throttle: 100 });
			this.coreUpdating = ko.observable(false).extend({ throttle: 100 });

			this.coreVersionHtmlDesc = ko.computed(() => {
				translateTrigger();
				return i18n('TAB_ABOUT/HTML_NEW_VERSION', { 'VERSION': this.coreVersion() });
			});

			this.statusType = ko.computed(() => {
				let type = '';
				const versionToCompare = this.coreVersionCompare(),
					isChecking = this.coreChecking(),
					isUpdating = this.coreUpdating(),
					isReal = this.coreReal();

				if (isChecking) {
					type = 'checking';
				} else if (isUpdating) {
					type = 'updating';
				} else if (!isReal) {
					type = 'error';
					this.errorDesc('Cannot access the repository at the moment.');
				} else if (0 === versionToCompare) {
					type = 'up-to-date';
				} else if (-1 === versionToCompare) {
					type = 'available';
				}

				return type;
			});
		}

		onBuild() {
	//	beforeShow() {
			this.coreChecking(true);
			Remote.request('AdminInfo', (iError, data) => {
				this.coreChecking(false);
				data = data?.Result;
				if (!iError && data) {
					this.load1(data.system.load?.[0]);
					this.load5(data.system.load?.[1]);
					this.load15(data.system.load?.[2]);
					this.phpextensions(data.php);
					this.coreReal(true);
					this.coreUpdatable(!!data.core.updatable);
					this.coreWarning(!!data.core.warning);
					this.coreVersion(data.core.version || '');
					this.coreVersionCompare(data.core.versionCompare);
					this.php64(data.php[1].loaded);
				} else {
					this.coreReal(false);
					this.coreWarning(false);
					this.coreVersion('');
					this.coreVersionCompare(-2);
				}
			});
		}

		clearCache() {
			Remote.request('AdminClearCache');
		}

		updateCoreData() {
			if (!this.coreUpdating()) {
				this.coreUpdating(true);
				Remote.request('AdminUpgradeCore', (iError, data) => {
					this.coreUpdating(false);
					this.coreVersion('');
					this.coreVersionCompare(-2);
					if (!iError && data?.Result) {
						this.coreReal(true);
						window.location.reload();
					} else {
						this.coreReal(false);
					}
				}, {}, 90000);
			}
		}
	}

	class AdminSettingsBranding extends AbstractViewSettings {
		constructor() {
			super();
			this.addSetting('title');
			this.addSetting('loadingDescription');
			this.addSetting('faviconUrl');
		}
	}

	class AdminSettingsConfig /*extends AbstractViewSettings*/ {

		constructor() {
			this.config = ko.observableArray();
			this.search = ko.observable('');
			this.saved = ko.observable(false).extend({ falseTimeout: 5000 });

			this.search.subscribe(value => {
				const v = value.toLowerCase(),
					qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
					match = node => node.textContent.toLowerCase().includes(v);
				if (v.length) {
					qsa(this.viewModelDom, 'tbody', tbody => {
						let show = match(tbody.querySelector('th'));
						if (show) {
							qsa(tbody, '[hidden]', n => n.hidden = false);
						} else {
							qsa(tbody, 'tbody td:first-child', td => {
								let hide = !match(td);
								show = show || !hide;
	//							td.closest('tr').hidden = hide;
								td.parentNode.hidden = hide;
							});
						}
						tbody.hidden = !show;
					});
				} else {
					qsa(this.viewModelDom, 'table [hidden]', n => n.hidden = false);
				}
			});
		}

		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) => {
							if ('language' === skey) {
								item[2] = ('webmail' === key) ? LanguageStore.languages : SettingsAdmin('languages');
							} else if ('theme' === skey) {
								item[2] = ThemeStore.themes;
							}
							'admin_password' === skey ||
							section.items.push({
								key: `config[${key}][${skey}]`,
								name: skey,
								value: item[0],
								type: getInputType(item[0], skey.includes('password')),
								comment: item[1],
								options: item[2]
							});
						});
						cfg.push(section);
					});
					this.config(cfg);
				}
			});
		}

		saveConfig(form) {
			const data = new FormData(form),
				config = {};
			this.config.forEach(section => {
				if (!config[section.name]) {
					config[section.name] = {};
				}
				section.items.forEach(item => {
					let value = data.get(item.key);
					switch (typeof item.value) {
						case 'boolean':
							value = 'on' == value;
							break;
						case 'number':
							value = parseInt(value, 10);
							break;
					}
					config[section.name][item.name] = value;
				});
			});
			Remote.post('AdminSettingsSet', null, {config:config}).then(result => {
				result.Result && this.saved(true);
			});
		}
	}

	class MenuSettingsAdminView extends AbstractViewLeft {
		/**
		 * @param {?} screen
		 */
		constructor(screen) {
			super('AdminMenu');

			this.menu = screen.menu;
		}

		link(route) {
			return '#/' + route;
		}
	}

	class PaneSettingsAdminView extends AbstractViewRight {
		constructor() {
			super('AdminPane');
			this.toggleLeftPanel = toggleLeftPanel;
		}

		logoutClick() {
			Remote.request('AdminLogout', () => rl.logoutReload());
		}
	}

	class SettingsAdminScreen extends AbstractSettingsScreen {
		constructor() {
			super([MenuSettingsAdminView, PaneSettingsAdminView]);

			[
				AdminSettingsGeneral,
				AdminSettingsDomains,
				AdminSettingsLogin,
				AdminSettingsBranding,
				AdminSettingsContacts,
				AdminSettingsSecurity,
				AdminSettingsPackages,
				AdminSettingsConfig,
				AdminSettingsAbout
			].forEach((item, index) =>
				settingsAddViewModel(item, 0, 0, 0, 0 === index)
			);

			runSettingsViewModelHooks(true);
		}

		onShow() {
			rl.setTitle();
		}
	}

	class AdminLoginView extends AbstractViewLogin {
		constructor() {
			super('AdminLogin');

			addObservablesTo(this, {
				login: '',
				password: '',
				totp: '',

				loginError: false,
				passwordError: false,

				submitRequest: false,
				submitError: ''
			});

			addSubscribablesTo(this, {
				login: () => this.loginError(false),
				password: () => this.passwordError(false)
			});

			decorateKoCommands(this, {
				submitCommand: self => !self.submitRequest()
			});
		}

		hideError() {
			this.submitError('');
		}

		submitCommand(self, event) {
			let form = event.target.form,
				data = new FormData(form),
				valid = form.reportValidity() && fireEvent('sm-admin-login', data, 1);

			this.loginError(!this.login());
			this.passwordError(!this.password());
			this.formError(!valid);

			if (valid) {
				this.submitRequest(true);

				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.setData(oData.Result);
						}
					},
					data
				);
			}

			return valid;
		}
	}

	class LoginAdminScreen extends AbstractScreen {
		constructor() {
			super('login', [AdminLoginView]);
		}

		onShow() {
			rl.setTitle();
		}
	}

	class SelectComponent {
		/**
		 * @param {Object} params
		 */
		constructor(params) {
			this.value = params.value;
			this.label = params.label;
			this.trigger = params.trigger?.subscribe ? params.trigger : null;
			this.placeholder = params.placeholder;
			this.options = params.options;
			this.optionsText = params.optionsText;
			this.optionsValue = params.optionsValue;

			let size = 0 < params.size ? 'span' + params.size : '';
			if (this.trigger) {
				const
					classForTrigger = ko.observable(''),
					setTriggerState = value => {
						switch (value) {
							case SaveSettingStatus.Success:
								classForTrigger('success');
								break;
							case SaveSettingStatus.Failed:
								classForTrigger('error');
								break;
							default:
								classForTrigger('');
								break;
						}
					};

				setTriggerState(this.trigger());

				this.className = koComputable(() =>
					(size + ' settings-save-trigger-input ' + classForTrigger()).trim()
				);

				this.disposables = [
					this.trigger.subscribe(setTriggerState, this),
					this.className
				];
			} else {
				this.className = size;
			}

			this.defaultOptionsAfterRender = defaultOptionsAfterRender;
		}

		dispose() {
			this.disposables?.forEach(dispose);
		}
	}

	class CheckboxComponent {
		constructor(params = {}) {
			this.name = params.name;

			this.value = ko.isObservable(params.value) ? params.value
				: ko.observable(!!params.value);

			this.enable = ko.isObservable(params.enable) ? params.enable
				: ko.observable(params.enable ?? 1);

			this.label = params.label;
		}

		click() {
			this.enable() && this.value(!this.value());
		}
	}

	class AbstractApp {
		/**
		 * @param {RemoteStorage|AdminRemoteStorage} Remote
		 */
		constructor(Remote) {
			this.Remote = Remote;
		}

		logoutReload(url) {
			arePopupsVisible(false);
			url = url || logoutLink();
			if (location.href !== url) {
				setTimeout(() => location.href = url, 100);
			} else {
				rl.route.reload();
			}
			// this does not work due to ViewModelClass.__builded = true;
	//		rl.settings.set('Auth', false);
	//		rl.app.start();
		}

		bootstart() {
			const register = (name, ClassObject) => ko.components.register(name, {
					template: { element: ClassObject.name },
					viewModel: {
						createViewModel: (params, componentInfo) => {
							params = params || {};
							i18nToNodes(componentInfo.element);
							return new ClassObject(params);
						}
					}
				});
			register('Select', SelectComponent);
			register('Checkbox', CheckboxComponent);

			initOnStartOrLangChange();

			LanguageStore.populate();
			initThemes();

			this.start();
		}
	}

	class AdminApp extends AbstractApp {
		constructor() {
			super(Remote);
			this.weakPassword = ko.observable(false);
		}

		refresh() {
			initThemes();
			this.start();
		}

		start() {
	//		if (!Settings.app('adminAllowed')) {
			if (!SettingsAdmin('allowed')) {
				rl.route.root();
				setTimeout(() => location.href = '/', 1);
			} else if (SettingsGet('Auth')) {
				this.weakPassword(SettingsGet('weakPassword'));
				startScreens([SettingsAdminScreen]);
			} else {
				startScreens([LoginAdminScreen]);
			}
		}
	}

	AskPopupView.credentials = function(sAskDesc, btnText) {
		return new Promise(resolve => {
			this.showModal([
				sAskDesc,
				view => resolve({username:view.username(), password:view.passphrase()}),
				() => resolve(null),
				true,
				3,
				btnText
			]);
		});
	};

	bootstrap(new AdminApp);

})();