Merge branch 'master' into plugin-hooks

# Conflicts:
#	dev/Common/Plugins.js
#	dev/Knoin/Knoin.js
#	dev/Remote/AbstractFetch.js
#	dev/View/User/Login.js
This commit is contained in:
djmaze 2022-03-15 14:27:54 +01:00
commit 827cfa5051
892 changed files with 110819 additions and 48675 deletions

View file

@ -1,9 +1,7 @@
import ko from 'ko';
import {
elementById,
Settings
} from 'Common/Globals';
import { Settings, SettingsGet } from 'Common/Globals';
import { changeTheme } from 'Common/Utils';
import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
@ -25,12 +23,9 @@ export class AbstractApp {
this.Remote = Remote;
}
logoutReload(close = false) {
logoutReload() {
const url = logoutLink();
rl.hash.clear();
close && window.close && window.close();
if (location.href !== url) {
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
} else {
@ -38,25 +33,27 @@ export class AbstractApp {
}
}
refresh() {
// rl.adminArea() || !translatorReload(false, );
rl.adminArea() || (
LanguageStore.language(SettingsGet('Language'))
& ThemeStore.populate()
& changeTheme(SettingsGet('Theme'))
);
this.start();
}
bootstart() {
const register = (key, ClassObject, templateID) => ko.components.register(key, {
template: { element: templateID || (key + 'Component') },
viewModel: {
createViewModel: (params, componentInfo) => {
params = params || {};
params.element = null;
if (componentInfo && componentInfo.element) {
params.component = componentInfo;
params.element = componentInfo.element;
i18nToNodes(componentInfo.element);
if (params.inline && ko.unwrap(params.inline)) {
params.element.style.display = 'inline-block';
}
i18nToNodes(componentInfo.element);
if (params.inline) {
componentInfo.element.style.display = 'inline-block';
}
return new ClassObject(params);
}
}
@ -72,14 +69,7 @@ export class AbstractApp {
LanguageStore.populate();
ThemeStore.populate();
}
/**
* @returns {void}
*/
hideLoading() {
elementById('rl-content').hidden = false;
elementById('rl-loading').remove();
this.start();
}
}

View file

@ -1,4 +1,4 @@
import 'External/Admin/ko';
import 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals';
@ -16,12 +16,8 @@ class AdminApp extends AbstractApp {
this.weakPassword = ko.observable(false);
}
bootstart() {
super.bootstart();
this.hideLoading();
if (!Settings.app('allowAdminPanel')) {
start() {
if (!Settings.app('adminAllowed')) {
rl.route.root();
setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) {
@ -30,8 +26,6 @@ class AdminApp extends AbstractApp {
} else {
startScreens([LoginAdminScreen]);
}
progressJs.end();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import * as Links from 'Common/Links';
import { doc } from 'Common/Globals';
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
let notificator = null,
player = null,
@ -12,7 +12,7 @@ let notificator = null,
player.src = url;
player.play();
name = name.trim();
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
}
},
@ -73,8 +73,7 @@ export const SMAudio = new class {
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
if (player) {
const stopFn = () => this.pause();
player.addEventListener('ended', stopFn);
player.addEventListener('error', stopFn);
addEventsListener(player, ['ended','error'], stopFn);
addEventListener('audio.api.stop', stopFn);
}
}
@ -89,7 +88,7 @@ export const SMAudio = new class {
pause() {
player && player.pause();
dispatchEvent(new CustomEvent('audio.stop'));
fireEvent('audio.stop');
}
playMp3(url, name) {
@ -106,11 +105,11 @@ export const SMAudio = new class {
playNotification(silent) {
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
if (!notificator) {
notificator = createNewObject();
notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
}
notificator = notificator || createNewObject();
if (notificator) {
notificator.src = Links.staticLink('sounds/'
+ SettingsGet('NotificationSound')
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
notificator.volume = silent ? 0.01 : 1;
notificator.play();
}

View file

@ -1,174 +1,157 @@
import { MessageSetAction } from 'Common/EnumsUser';
import { isNonEmptyArray, pInt } from 'Common/Utils';
import { isArray } from 'Common/Utils';
let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {},
FOLDERS_HASH_CACHE = {},
FOLDERS_UID_NEXT_CACHE = {},
MESSAGE_FLAGS_CACHE = {},
NEW_MESSAGE_CACHE = {},
REQUESTED_MESSAGE_CACHE = {},
inboxFolderName = 'INBOX';
const REQUESTED_MESSAGE_CACHE = {};
export const
/**
* @returns {void}
*/
clearCache = () => {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
NEW_MESSAGE_CACHE = {};
REQUESTED_MESSAGE_CACHE = {};
},
/**
* @returns {void}
*/
export function clear() {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
FOLDERS_HASH_CACHE = {};
FOLDERS_UID_NEXT_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
}
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {string}
*/
getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
/**
* @param {string} folderFullNameRaw
* @param {string} uid
* @returns {string}
*/
export function getMessageKey(folderFullNameRaw, uid) {
return `${folderFullNameRaw}#${uid}`;
}
/**
* @param {string} folder
* @param {string} uid
*/
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
/**
* @param {string} folder
* @param {string} uid
*/
export function addRequestedMessage(folder, uid) {
REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
}
/**
* @param {string} folder
* @param {string} uid
* @returns {boolean}
*/
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
/**
* @param {string} folder
* @param {string} uid
* @returns {boolean}
*/
export function hasRequestedMessage(folder, uid) {
return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
}
/**
* @param {string} folderFullName
* @param {string} uid
*/
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
export function addNewMessageCache(folderFullNameRaw, uid) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
}
/**
* @param {string} folderFullName
* @param {string} uid
*/
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
return true;
}
return false;
},
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
return true;
}
return false;
}
/**
* @returns {void}
*/
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
/**
* @returns {void}
*/
export function clearNewMessageCache() {
NEW_MESSAGE_CACHE = {};
}
/**
* @returns {string}
*/
getFolderInboxName = () => inboxFolderName,
/**
* @returns {string}
*/
export function getFolderInboxName() {
return inboxFolderName;
}
/**
* @returns {string}
*/
setFolderInboxName = name => inboxFolderName = name,
/**
* @returns {string}
*/
export function setFolderInboxName(name) {
inboxFolderName = name;
}
/**
* @param {string} folderHash
* @returns {string}
*/
getFolderFullName = folderHash =>
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
/**
* @param {string} folderHash
* @returns {string}
*/
export function getFolderFullNameRaw(folderHash) {
return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
}
/**
* @param {string} folderHash
* @param {string} folderFullName
* @param {?FolderModel} folder
*/
setFolder = folder => {
folder.hash = '';
FOLDERS_CACHE[folder.fullName] = folder;
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
},
/**
* @param {string} folderHash
* @param {string} folderFullNameRaw
* @param {?FolderModel} folder
*/
export function setFolder(folderHash, folderFullNameRaw, folder) {
FOLDERS_CACHE[folderFullNameRaw] = folder;
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
}
/**
* @param {string} folderFullName
* @returns {string}
*/
getFolderHash = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
export function getFolderHash(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
}
/**
* @param {string} folderFullName
* @param {string} folderHash
*/
setFolderHash = (folderFullName, folderHash) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
/**
* @param {string} folderFullNameRaw
* @param {string} folderHash
*/
export function setFolderHash(folderFullNameRaw, folderHash) {
if (folderFullNameRaw) {
FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
}
}
/**
* @param {string} folderFullName
* @returns {string}
*/
getFolderUidNext = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
export function getFolderUidNext(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
: '';
}
/**
* @param {string} folderFullName
* @param {string} uidNext
*/
setFolderUidNext = (folderFullName, uidNext) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
/**
* @param {string} folderFullNameRaw
* @param {string} uidNext
*/
export function setFolderUidNext(folderFullNameRaw, uidNext) {
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
}
/**
* @param {string} folderFullName
* @returns {?FolderModel}
*/
getFolderFromCacheList = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
/**
* @param {string} folderFullNameRaw
* @returns {?FolderModel}
*/
export function getFolderFromCacheList(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
}
/**
* @param {string} folderFullNameRaw
*/
export function removeFolderFromCacheList(folderFullNameRaw) {
delete FOLDERS_CACHE[folderFullNameRaw];
}
/**
* @param {string} folderFullName
*/
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
export class MessageFlagsCache
{
/**
* @param {string} folderFullName
* @param {string} uid
* @param {string} flag
* @returns {bool}
*/
static hasFlag(folderFullName, uid, flag) {
return MESSAGE_FLAGS_CACHE[folderFullName]
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
}
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {?Array}
*/
static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
: null;
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
}
/**
@ -176,11 +159,13 @@ export class MessageFlagsCache
* @param {string} uid
* @param {Array} flagsCache
*/
static setFor(folderFullName, uid, flagsCache) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
static setFor(folderFullName, uid, flags) {
if (isArray(flags)) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
}
/**
@ -196,39 +181,24 @@ export class MessageFlagsCache
static initMessage(message) {
if (message) {
const uid = message.uid,
flags = this.getFor(message.folder, uid);
flags = this.getFor(message.folder, uid),
thread = message.threads;
if (flags && flags.length) {
message.isFlagged(!!flags[1]);
if (!message.isSimpleMessage) {
message.isUnseen(!!flags[0]);
message.isAnswered(!!flags[2]);
message.isForwarded(!!flags[3]);
message.isReadReceipt(!!flags[4]);
message.isDeleted(!!flags[5]);
}
if (isArray(flags)) {
message.flags(flags);
}
if (message.threads.length) {
const unseenSubUid = message.threads.find(sSubUid => {
if (uid !== sSubUid) {
const subFlags = this.getFor(message.folder, sSubUid);
return subFlags && subFlags.length && !!subFlags[0];
}
return false;
});
if (thread.length) {
const unseenSubUid = thread.find(iSubUid =>
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
);
const flaggedSubUid = message.threads.find(sSubUid => {
if (uid !== sSubUid) {
const subFlags = this.getFor(message.folder, sSubUid);
return subFlags && subFlags.length && !!subFlags[1];
}
return false;
});
const flaggedSubUid = thread.find(iSubUid =>
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
);
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
message.hasUnseenSubMessage(!!unseenSubUid);
message.hasFlaggedSubMessage(!!flaggedSubUid);
}
}
}
@ -238,25 +208,7 @@ export class MessageFlagsCache
*/
static store(message) {
if (message) {
this.setFor(message.folder, message.uid, [
message.isUnseen(),
message.isFlagged(),
message.isAnswered(),
message.isForwarded(),
message.isReadReceipt(),
message.isDeleted()
]);
}
}
/**
* @param {string} folder
* @param {string} uid
* @param {Array} flags
*/
static storeByFolderAndUid(folder, uid, flags) {
if (isNonEmptyArray(flags)) {
this.setFor(folder, uid, flags);
this.setFor(message.folder, message.uid, message.flags());
}
}
@ -266,33 +218,30 @@ export class MessageFlagsCache
* @param {number} setAction
*/
static storeBySetAction(folder, uid, setAction) {
let unread = 0;
const flags = this.getFor(folder, uid);
let flags = this.getFor(folder, uid) || [];
const
unread = flags.includes('\\seen') ? 0 : 1,
add = item => flags.includes(item) || flags.push(item),
remove = item => flags = flags.filter(flag => flag != item);
if (isNonEmptyArray(flags)) {
if (flags[0]) {
unread = 1;
}
switch (setAction) {
case MessageSetAction.SetSeen:
flags[0] = false;
break;
case MessageSetAction.UnsetSeen:
flags[0] = true;
break;
case MessageSetAction.SetFlag:
flags[1] = true;
break;
case MessageSetAction.UnsetFlag:
flags[1] = false;
break;
// no default
}
this.setFor(folder, uid, flags);
switch (setAction) {
case MessageSetAction.SetSeen:
add('\\seen');
break;
case MessageSetAction.UnsetSeen:
remove('\\seen');
break;
case MessageSetAction.SetFlag:
add('\\flagged');
break;
case MessageSetAction.UnsetFlag:
remove('\\flagged');
break;
// no default
}
this.setFor(folder, uid, flags);
return unread;
}

View file

@ -1,3 +1 @@
export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
export const UNUSED_OPTION_VALUE = '__UNUSE__';

View file

@ -1,85 +1,53 @@
/* eslint quote-props: 0 */
/**
* @enum {string}
*/
export const Capa = {
OpenPGP: 'OPEN_PGP',
Prefetch: 'PREFETCH',
Composer: 'COMPOSER',
Contacts: 'CONTACTS',
Reload: 'RELOAD',
Search: 'SEARCH',
SearchAdv: 'SEARCH_ADV',
MessageActions: 'MESSAGE_ACTIONS',
MessageListActions: 'MESSAGELIST_ACTIONS',
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
DangerousActions: 'DANGEROUS_ACTIONS',
Settings: 'SETTINGS',
Help: 'HELP',
Themes: 'THEMES',
UserBackground: 'USER_BACKGROUND',
Sieve: 'SIEVE',
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
Templates: 'TEMPLATES',
AutoLogout: 'AUTOLOGOUT',
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
Identities: 'IDENTITIES'
};
export const
/**
* @enum {string}
*/
export const Scope = {
All: 'all',
None: 'none',
Contacts: 'Contacts',
Scope = {
MessageList: 'MessageList',
FolderList: 'FolderList',
MessageView: 'MessageView',
Compose: 'Compose',
Settings: 'Settings',
Menu: 'Menu',
ComposeOpenPgp: 'ComposeOpenPgp',
MessageOpenPgp: 'MessageOpenPgp',
ViewOpenPgpKey: 'ViewOpenPgpKey',
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
Ask: 'Ask'
};
Settings: 'Settings'
},
/**
* @enum {number}
*/
export const UploadErrorCode = {
UploadErrorCode = {
Normal: 0,
FileIsTooBig: 1,
FilePartiallyUploaded: 2,
NoFileUploaded: 3,
MissingTempFolder: 4,
OnSavingFile: 5,
FilePartiallyUploaded: 3,
NoFileUploaded: 4,
MissingTempFolder: 6,
OnSavingFile: 7,
FileType: 98,
Unknown: 99
};
},
/**
* @enum {number}
*/
export const SaveSettingsStep = {
SaveSettingsStep = {
Animate: -2,
Idle: -1,
TrueResult: 1,
FalseResult: 0
};
},
/**
* @enum {number}
*/
export const Notification = {
Notification = {
RequestError: 1,
RequestAborted: 2,
// Global
InvalidToken: 101,
AuthError: 102,
// User
ConnectionError: 104,
DomainNotAllowed: 109,
AccountNotAllowed: 110,
@ -110,20 +78,15 @@ export const Notification = {
CantDeleteNonEmptyFolder: 405,
// CantSaveSettings: 501,
CantSavePluginSettings: 502,
DomainAlreadyExists: 601,
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
DemoSendMessageError: 750,
DemoAccountError: 751,
AccountAlreadyExists: 801,
AccountDoesNotExist: 802,
AccountSwitchFailed: 803,
MailServerError: 901,
ClientViewError: 902,
@ -134,5 +97,12 @@ export const Notification = {
// JsonTimeout: 953,
UnknownNotification: 998,
UnknownError: 999
UnknownError: 999,
// Admin
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
CantSavePluginSettings: 705
};

View file

@ -4,14 +4,28 @@
* @enum {number}
*/
export const FolderType = {
Inbox: 10,
SentItems: 11,
Draft: 12,
Trash: 13,
Spam: 14,
Archive: 15,
NotSpam: 80,
User: 99
User: 0,
Inbox: 1,
Sent: 2,
Drafts: 3,
Spam: 4, // JUNK
Trash: 5,
Archive: 6,
NotSpam: 80
};
/**
* @enum {string}
*/
export const FolderMetadataKeys = {
// RFC 5464
Comment: '/private/comment',
CommentShared: '/shared/comment',
// RFC 6154
SpecialUse: '/private/specialuse',
// Kolab
KolabFolderType: '/private/vendor/kolab/folder-type',
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
};
/**
@ -34,13 +48,13 @@ export const FolderSortMode = {
* @enum {string}
*/
export const ComposeType = {
Empty: 'empty',
Reply: 'reply',
ReplyAll: 'replyall',
Forward: 'forward',
ForwardAsAttachment: 'forward-as-attachment',
Draft: 'draft',
EditAsNew: 'editasnew'
Empty: 0,
Reply: 1,
ReplyAll: 2,
Forward: 3,
ForwardAsAttachment: 4,
Draft: 5,
EditAsNew: 6
};
/**
@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
/**
* @enum {number}
*/
export const ClientSideKeyName = {
FoldersLashHash: 0,
MessagesInboxLastHash: 1,
MailBoxListSize: 2,
ExpandedFolders: 3,
FolderListSize: 4,
MessageListSize: 5,
LastReplyAction: 6,
LastSignMe: 7,
ComposeLastIdentityID: 8,
MessageHeaderFullInfo: 9,
MessageAttachmentControls: 10
};
export const
ClientSideKeyNameExpandedFolders = 3,
ClientSideKeyNameFolderListSize = 4,
ClientSideKeyNameMessageListSize = 5,
ClientSideKeyNameLastReplyAction = 6,
ClientSideKeyNameLastSignMe = 7,
ClientSideKeyNameMessageHeaderFullInfo = 9,
ClientSideKeyNameMessageAttachmentControls = 10;
/**
* @enum {number}

View file

@ -1,7 +1,7 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
import { isNonEmptyArray } from 'Common/Utils';
import { arrayLength } from 'Common/Utils';
const
cache = {},
@ -9,6 +9,7 @@ const
msOffice = app+'vnd.openxmlformats-officedocument.',
openDoc = app+'vnd.oasis.opendocument.',
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
lowerCase = text => text.toLowerCase().trim(),
exts = {
eml: 'message/rfc822',
@ -123,13 +124,13 @@ export const FileInfo = {
* @returns {string}
*/
getExtension: fileName => {
fileName = fileName.toLowerCase().trim();
fileName = lowerCase(fileName);
const result = fileName.split('.').pop();
return result === fileName ? '' : result;
},
getContentType: fileName => {
fileName = fileName.toLowerCase().trim();
fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) {
return app + 'ms-tnef';
}
@ -158,8 +159,8 @@ export const FileInfo = {
* @returns {string}
*/
getType: (ext, mimeType) => {
ext = ext.toLowerCase().trim();
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
ext = lowerCase(ext);
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
let key = ext + mimeType;
if (cache[key]) {
@ -249,10 +250,10 @@ export const FileInfo = {
* @param {string} sFileType
* @returns {string}
*/
getCombinedIconClass: data => {
if (isNonEmptyArray(data)) {
getAttachmentsIconClass: data => {
if (arrayLength(data)) {
let icons = data
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
.validUnique();
return (icons && 1 === icons.length && 'icon-file' !== icons[0])

259
dev/Common/Folders.js Normal file
View file

@ -0,0 +1,259 @@
import { isArray, arrayLength } from 'Common/Utils';
import {
MessageFlagsCache,
setFolderHash,
getFolderHash,
getFolderInboxName,
getFolderFromCacheList,
getFolderUidNext
} from 'Common/Cache';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { getNotification } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
export const
sortFolders = folders => {
try {
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
folders.sort((a, b) =>
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
);
} catch (e) {
console.error(e);
}
},
/**
* @param {?Function} fCallback
* @param {string} folder
* @param {Array=} list = []
*/
fetchFolderInformation = (fCallback, folder, list = []) => {
let fetch = !arrayLength(list);
const uids = [];
if (!fetch) {
list.forEach(messageListItem => {
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
uids.push(messageListItem.uid);
}
if (messageListItem.threads.length) {
messageListItem.threads.forEach(uid => {
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
uids.push(uid);
}
});
}
});
fetch = uids.length;
}
if (fetch) {
Remote.request('FolderInformation', fCallback, {
Folder: folder,
FlagsUids: uids,
UidNext: getFolderUidNext(folder) // Used to check for new messages
});
} else if (SettingsUserStore.useThreads()) {
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
},
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fRenameCallback
* @param {Function=} fDisableCallback
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
* @returns {Array}
*/
folderListOptionsBuilder = (
aDisabled,
aHeaderLines,
fRenameCallback,
fDisableCallback,
bNoSelectSelectable,
aList = FolderUserStore.folderList()
) => {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
// FolderSystemPopupView should always be true
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
aResult.push({
id: oItem.fullName,
name:
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: !bNoSelectSelectable && (
!oItem.selectable() ||
aDisabled.includes(oItem.fullName) ||
fDisableCallback(oItem))
});
}
if (oItem.subFolders.length) {
foldersWalk(oItem.subFolders());
}
});
};
fDisableCallback = fDisableCallback || (() => false);
fRenameCallback = fRenameCallback || (oItem => oItem.name());
isArray(aDisabled) || (aDisabled = []);
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
aResult.push({
id: line[0],
name: line[1],
system: false,
disabled: false
})
);
foldersWalk(aList);
return aResult;
},
// Every 5 minutes
refreshFoldersInterval = 300000,
/**
* @param {boolean=} boot = false
*/
folderInformationMultiply = (boot = false) => {
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
if (arrayLength(folders)) {
Remote.request('FolderInformationMultiply', (iError, oData) => {
if (!iError && arrayLength(oData.Result)) {
const utc = Date.now();
oData.Result.forEach(item => {
const hash = getFolderHash(item.Folder),
folder = getFolderFromCacheList(item.Folder);
if (folder) {
folder.expires = utc;
setFolderHash(item.Folder, item.Hash);
folder.messageCountAll(item.MessageCount);
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
folder.messageCountUnread(item.MessageUnseenCount);
if (unreadCountChange) {
MessageFlagsCache.clearFolder(folder.fullName);
}
if (!hash || item.Hash !== hash) {
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
}
} else if (unreadCountChange
&& folder.fullName === FolderUserStore.currentFolderFullName()
&& MessagelistUserStore.length) {
rl.app.folderInformation(folder.fullName, MessagelistUserStore());
}
}
});
if (boot) {
setTimeout(() => folderInformationMultiply(true), 2000);
}
}
}, {
Folders: folders
});
}
},
moveOrDeleteResponseHelper = (iError, oData) => {
if (iError) {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
alert(getNotification(iError));
} else if (FolderUserStore.currentFolder()) {
if (2 === arrayLength(oData.Result)) {
setFolderHash(oData.Result[0], oData.Result[1]);
} else {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
}
MessagelistUserStore.reload(!MessagelistUserStore.length);
}
},
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
const
sSpamFolder = FolderUserStore.spamFolder(),
isSpam = sSpamFolder === toFolderFullName,
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
Remote.request('MessageMove',
moveOrDeleteResponseHelper,
{
FromFolder: fromFolderFullName,
ToFolder: toFolderFullName,
Uids: uidsForMove.join(','),
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
},
null,
'',
['MessageList']
);
},
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
Remote.request('MessageDelete',
moveOrDeleteResponseHelper,
{
Folder: sFromFolderFullName,
Uids: aUidForRemove.join(',')
},
null,
'',
['MessageList']
);
},
/**
* @param {string} sFromFolderFullName
* @param {Array} aUidForMove
* @param {string} sToFolderFullName
* @param {boolean=} bCopy = false
*/
moveMessagesToFolder = (sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) => {
if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
oToFolder = getFolderFromCacheList(sToFolderFullName);
if (oFromFolder && oToFolder) {
if (bCopy) {
Remote.request('MessageCopy', null, {
FromFolder: oFromFolder.fullName,
ToFolder: oToFolder.fullName,
Uids: aUidForMove.join(',')
});
} else {
messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
}
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
return true;
}
}
return false;
};

View file

@ -1,58 +1,78 @@
import ko from 'ko';
import { Scope } from 'Common/Enums';
export const doc = document;
let keyScopeFake = 'all';
export const $htmlCL = doc.documentElement.classList;
export const
ScopeMenu = 'Menu',
export const elementById = id => doc.getElementById(id);
doc = document,
export const Settings = rl.settings;
export const SettingsGet = rl.settings.get;
$htmlCL = doc.documentElement.classList,
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
elementById = id => doc.getElementById(id),
export const moveAction = ko.observable(false);
export const leftPanelDisabled = ko.observable(false);
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
export const createElement = (name, attr) => {
let el = doc.createElement(name);
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
return el;
};
Settings = rl.settings,
SettingsGet = Settings.get,
SettingsCapa = Settings.capa,
dropdowns = [],
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
moveAction = ko.observable(false),
leftPanelDisabled = ko.observable(false),
createElement = (name, attr) => {
let el = doc.createElement(name);
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
return el;
},
fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'),
addShortcut = (...args) => shortcuts.add(...args),
registerShortcut = (keys, modifiers, scopes, method) =>
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
addEventsListener = (element, events, fn, options) =>
events.forEach(event => element.addEventListener(event, fn, options)),
addEventsListeners = (element, events) =>
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
// keys / shortcuts
keyScopeReal = ko.observable('all'),
keyScope = value => {
if (!value) {
return keyScopeFake;
}
if (ScopeMenu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = ScopeMenu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
};
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(ScopeMenu);
} else if (ScopeMenu === shortcuts.getScope()) {
keyScope(keyScopeFake);
}
});
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false);
$htmlCL.toggle('rl-left-panel-disabled', value);
});
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
// keys
export const keyScopeReal = ko.observable(Scope.All);
export const keyScope = (()=>{
let keyScopeFake = Scope.All;
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(Scope.Menu);
} else if (Scope.Menu === shortcuts.getScope()) {
keyScope(keyScopeFake);
}
});
return ko.computed({
read: () => keyScopeFake,
write: value => {
if (Scope.Menu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = Scope.Menu;
}
}
keyScopeReal(value);
}
});
})();
keyScopeReal.subscribe(value => shortcuts.setScope(value));

View file

@ -1,4 +1,9 @@
import { createElement, SettingsGet } from 'Common/Globals';
import { forEachObjectEntry, pInt } from 'Common/Utils';
import { proxy } from 'Common/Links';
const
tpl = createElement('template'),
htmlre = /[&<>"']/g,
htmlmap = {
'&': '&amp;',
@ -6,17 +11,489 @@ const
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;'
},
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
// Strip utm_* tracking
stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
export const
/**
* @param {string} text
* @returns {string}
*/
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
/**
* Clears the Message Html for viewing
* @param {string} text
* @returns {string}
*/
cleanHtml = (html, contentLocationUrls, removeColors) => {
const
debug = false, // Config()->Get('debug', 'enable', false);
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
result = {
hasExternals: false,
foundCIDs: [],
foundContentLocationUrls: []
},
// convert body attributes to CSS
tasks = {
link: value => {
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
}
},
text: (value, node) => node.style.color = value,
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
},
allowedAttributes = [
// defaults
'name',
'dir', 'lang', 'style', 'title',
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
'border', 'bordercolor', 'charset', 'direction',
// a
'download', 'hreflang',
// body
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
// col
'align', 'valign',
// font
'color', 'face', 'size',
// hr
'noshade',
// img
'hspace', 'sizes', 'srcset', 'vspace',
// meter
'low', 'high', 'optimum', 'value',
// ol
'reversed', 'start',
// table
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
// th
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
],
disallowedTags = [
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
],
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
];
tpl.innerHTML = html
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
.replace(/<!doctype[^>]*>/gi, '')
.replace(/<\?xml[^>]*\?>/gi, '')
// Not supported by <template> element
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
.replace(/<\/?(html|head)[^>]*>/gi, '')
.trim();
html = '';
// \MailSo\Base\HtmlUtils::ClearComments()
// https://github.com/the-djmaze/snappymail/issues/187
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
while (nodeIterator.nextNode()) {
nodeIterator.referenceNode.remove();
}
tpl.content.querySelectorAll('*').forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
// \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name)
|| 'none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
// || ('0' === oStyle.opacity
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
) {
oElement.remove();
return;
}
// if (['CENTER','FORM'].includes(name)) {
if ('FORM' === name || 'O:P' === name) {
replaceWithChildren(oElement);
return;
}
/*
// Idea to allow CSS
if ('STYLE' === name) {
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
oElement.textContent = oElement.textContent
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
.replace(/(background-)color:[^};]+/g, '');
return;
}
*/
const aAttrsForRemove = [],
hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => oElement.removeAttribute(name);
if ('mail-body' === oElement.className) {
forEachObjectEntry(tasks, (name, cb) => {
if (hasAttribute(name)) {
cb(getAttribute(name), oElement);
delAttribute(name);
}
});
}
if (oElement.hasAttributes()) {
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName)) {
delAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
}
}
let value;
if ('TABLE' === name) {
if (hasAttribute('width')) {
oStyle.width = getAttribute('width');
delAttribute('width');
}
value = oStyle.width;
if (value && !value.includes('%')) {
oStyle.maxWidth = value;
oStyle.width = '100%';
}
}
else if ('A' === name) {
value = oElement.href;
if (!/^([a-z]+):/i.test(value)) {
setAttribute('data-x-broken-href', value);
delAttribute('href');
} else {
oElement.href = stripTracking(value);
setAttribute('target', '_blank');
setAttribute('rel', 'external nofollow noopener noreferrer');
}
setAttribute('tabindex', '-1');
}
// SVG xlink:href
/*
if (hasAttribute('xlink:href')) {
delAttribute('xlink:href');
}
*/
let skipStyle = false;
if (hasAttribute('src')) {
value = getAttribute('src');
delAttribute('src');
if (detectHiddenImages
&& 'IMG' === name
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'mandrillapp.com/track/open',
'list-manage.com/track/open'
].filter(uri => value.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
setAttribute('style', 'display:none');
setAttribute('data-x-hidden-src', value);
}
else if (contentLocationUrls[value])
{
setAttribute('data-x-src-location', value);
result.foundContentLocationUrls.push(value);
}
else if ('cid:' === value.slice(0, 4))
{
setAttribute('data-x-src-cid', value.slice(4));
result.foundCIDs.push(value.slice(4));
}
else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
{
setAttribute('data-x-src', useProxy ? proxy(value) : value);
result.hasExternals = true;
}
else if ('data:image/' === value.slice(0, 11))
{
setAttribute('src', value);
}
else
{
setAttribute('data-x-broken-src', value);
}
}
if (hasAttribute('background')) {
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
delAttribute('background');
}
if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = getAttribute('bgcolor');
delAttribute('bgcolor');
}
if (hasAttribute('color')) {
oStyle.color = getAttribute('color');
delAttribute('color');
}
if (!skipStyle) {
/*
if ('fixed' === oStyle.position) {
oStyle.position = 'absolute';
}
*/
oStyle.removeProperty('behavior');
oStyle.removeProperty('cursor');
oStyle.removeProperty('min-width');
const urls = {
cid: [], // 'data-x-style-cid'
remote: [], // 'data-x-style-url'
broken: [] // 'data-x-broken-style-src'
};
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
if (oStyle[property]) {
let value = oStyle[property],
found = value.match(/url\s*\(([^)]+)\)/gi);
if (found) {
oStyle[property] = null;
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
let lowerUrl = found.toLowerCase();
if ('cid:' === lowerUrl.slice(0, 4)) {
found = found.slice(4);
urls.cid[property] = found
result.foundCIDs.push(found);
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
result.hasExternals = true;
urls.remote[property] = useProxy ? proxy(found) : found;
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
oStyle[property] = value;
} else {
urls.broken[property] = found;
}
}
}
});
// oStyle.removeProperty('background-image');
// oStyle.removeProperty('list-style-image');
if (urls.cid.length) {
setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
}
if (urls.remote.length) {
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
}
if (urls.broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
}
if (11 > pInt(oStyle.fontSize)) {
oStyle.removeProperty('font-size');
}
// Removes background and color
// Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability
if (removeColors) {
oStyle.removeProperty('background-color');
oStyle.removeProperty('background-image');
oStyle.removeProperty('color');
}
// Drop Microsoft Office style properties
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
}
if (debug && aAttrsForRemove.length) {
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
}
});
// return tpl.content.firstChild;
result.html = tpl.innerHTML.trim();
return result;
},
/**
* @param {string} html
* @returns {string}
*/
htmlToPlain = html => {
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => {
let bq;
while ((bq = node.querySelector('blockquote'))) {
// Convert child blockquote first
blockquotes(bq);
// Convert blockquote
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '&gt; ');
// replaceWithChildren(bq);
bq.replaceWith(
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
);
}
};
html = html
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
.replace(/\r?\n/g, '')
.replace(/\s+/gm, ' ');
while (/<(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
while (/<\/(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
tpl.innerHTML = html
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
// Convert line-breaks
forEach('br', br => br.replaceWith('\n'));
// lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
// headings
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
// paragraphs
forEach('p', node => {
node.prepend('\n\n');
if ('' == node.textContent.trim()) {
node.remove();
} else {
node.after('\n\n');
}
});
// proper indenting and numbering of (un)ordered lists
forEach('ol,ul', node => {
let prefix = '',
parent = node,
ordered = 'OL' == node.tagName,
i = 0;
while (parent && parent.parentNode && parent.parentNode.closest) {
parent = parent.parentNode.closest('ol,ul');
parent && (prefix = ' ' + prefix);
}
node.querySelectorAll(':scope > li').forEach(li => {
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
});
node.prepend('\n\n');
node.after('\n\n');
});
// Convert anchors
forEach('a', a => {
let txt = a.textContent, href = a.href;
return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
});
// Bold
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
// Italic
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Blockquotes must be last
blockquotes(tpl.content);
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
},
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
plainToHtml = plain => {
plain = stripTracking(plain)
.toString()
.replace(/\r/g, '')
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.slice(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.slice(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.slice(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
return aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>');
};
/**
* @param {string} text
* @returns {string}
*/
export function encodeHtml(text) {
return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]);
}
class HtmlEditor {
export class HtmlEditor {
/**
* @param {Object} element
* @param {Function=} onBlur
@ -24,25 +501,37 @@ class HtmlEditor {
* @param {Function=} onModeChange
*/
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
this.editor;
this.blurTimer = 0;
this.__resizable = false;
this.__inited = false;
this.onBlur = onBlur;
this.onReady = onReady;
this.onModeChange = onModeChange;
this.element = element;
if (element) {
let editor;
this.resize = (() => {
try {
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
} catch (e) {} // eslint-disable-line no-empty
}).throttle(100);
onReady = onReady ? [onReady] : [];
this.onReady = fn => onReady.push(fn);
const readyCallback = () => {
this.editor = editor;
this.onReady = fn => fn();
onReady.forEach(fn => fn());
};
this.init();
if (rl.createWYSIWYG) {
editor = rl.createWYSIWYG(element, readyCallback);
}
if (!editor) {
editor = new SquireUI(element);
setTimeout(readyCallback, 1);
}
editor.on('blur', () => this.blurTrigger());
editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
editor.on('mode', () => {
this.blurTrigger();
this.onModeChange && this.onModeChange(!this.isPlain());
});
}
}
blurTrigger() {
@ -70,9 +559,9 @@ class HtmlEditor {
* @returns {void}
*/
clearCachedSignature() {
this.editor && this.editor.execCommand('insertSignature', {
this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true
});
}));
}
/**
@ -82,75 +571,66 @@ class HtmlEditor {
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
this.editor && this.editor.execCommand('insertSignature', {
this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
});
}));
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getData(wrapIsHtml = false) {
getData() {
let result = '';
if (this.editor) {
try {
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData();
} else {
result = wrapIsHtml
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
this.editor.getData() +
'</div>'
: this.editor.getData();
result = this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
return result;
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getDataWithHtmlMark(wrapIsHtml = false) {
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData();
}
modeWysiwyg() {
try {
this.editor && this.editor.setMode('wysiwyg');
} catch (e) { console.error(e); }
this.onReady(() => this.editor.setMode('wysiwyg'));
}
modePlain() {
try {
this.editor && this.editor.setMode('plain');
} catch (e) { console.error(e); }
this.onReady(() => this.editor.setMode('plain'));
}
setHtmlOrPlain(text) {
if (':HTML:' === text.substr(0, 6)) {
this.setHtml(text.substr(6));
if (':HTML:' === text.slice(0, 6)) {
this.setHtml(text.slice(6));
} else {
this.setPlain(text);
}
}
setData(mode, data) {
if (this.editor && this.__inited) {
this.onReady(() => {
const editor = this.editor;
this.clearCachedSignature();
try {
this.editor.setMode(mode);
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
this.editor.__plain.setRawData(data);
editor.setMode(mode);
if (this.isPlain() && editor.plugins.plain && editor.__plain) {
editor.__plain.setRawData(data);
} else {
this.editor.setData(data);
editor.setData(data);
}
} catch (e) { console.error(e); }
}
});
}
setHtml(html) {
@ -161,46 +641,8 @@ class HtmlEditor {
this.setData('plain', txt);
}
init() {
if (this.element && !this.editor) {
const onReady = () => {
if (this.editor.removeMenuItem) {
this.editor.removeMenuItem('cut');
this.editor.removeMenuItem('copy');
this.editor.removeMenuItem('paste');
}
this.__resizable = true;
this.__inited = true;
this.resize();
this.onReady && this.onReady();
};
if (rl.createWYSIWYG) {
this.editor = rl.createWYSIWYG(this.element, onReady);
}
if (!this.editor) {
this.editor = new SquireUI(this.element);
setTimeout(onReady,1);
}
if (this.editor) {
this.editor.on('blur', () => this.blurTrigger());
this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
this.editor.on('mode', () => {
this.blurTrigger();
this.onModeChange && this.onModeChange(!this.isPlain());
});
}
}
}
focus() {
try {
this.editor && this.editor.focus();
} catch (e) {} // eslint-disable-line no-empty
this.onReady(() => this.editor.focus());
}
hasFocus() {
@ -212,14 +654,15 @@ class HtmlEditor {
}
blur() {
try {
this.editor && this.editor.focusManager.blur(true);
} catch (e) {} // eslint-disable-line no-empty
this.onReady(() => this.editor.focusManager.blur(true));
}
clear() {
this.setHtml('');
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
}
}
export { HtmlEditor, HtmlEditor as default };
rl.Utils = {
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
};

View file

@ -1,191 +1,140 @@
import { pString, pInt } from 'Common/Utils';
import { Settings, SettingsGet } from 'Common/Globals';
import { Settings } from 'Common/Globals';
const
ROOT = './',
HASH_PREFIX = '#/',
SERVER_PREFIX = './?',
VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
getHash = () => SettingsGet('AuthAccountHash') || '0';
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
/**
* @returns {string}
*/
export const SUB_QUERY_PREFIX = '&q[]=';
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
/**
* @param {string=} startupUrl
* @returns {string}
*/
export function root(startupUrl = '') {
return HASH_PREFIX + pString(startupUrl);
}
export const
SUB_QUERY_PREFIX = '&q[]=',
/**
* @returns {string}
*/
export function logoutLink() {
return (rl.adminArea() && !Settings.app('adminHostUse'))
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
: ROOT;
}
/**
* @param {string=} startupUrl
* @returns {string}
*/
root = () => HASH_PREFIX,
/**
* @param {string} type
* @param {string} hash
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function serverRequestRaw(type, hash, customSpecSuffix) {
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
/**
* @returns {string}
*/
logoutLink = () => adminPath() ? prefix() : './',
/**
* @param {string} type
* @param {string} hash
* @param {string=} customSpecSuffix
* @returns {string}
*/
serverRequestRaw = (type, hash) =>
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // AuthAccountHash ?
+ (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: '')
;
}
: ''),
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentDownload(download, customSpecSuffix) {
return serverRequestRaw('Download', download, customSpecSuffix);
}
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download, customSpecSuffix),
/**
* @param {string} type
* @returns {string}
*/
export function serverRequest(type) {
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
}
proxy = url =>
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
/*
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
'Rnd' => \md5(\microtime(true)),
'Token' => Utils::GetConnectionToken(),
'Url' => $sUrl
)).'/';
*/
/**
* @param {string} email
* @returns {string}
*/
export function change(email) {
return serverRequest('Change') + encodeURIComponent(email) + '/';
}
/**
* @param {string} type
* @returns {string}
*/
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
/**
* @param {string} hash
* @returns {string}
*/
export function userBackground(hash) {
return serverRequestRaw('UserBackground', hash);
}
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
langLink = (lang, isAdmin) =>
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
export function langLink(lang, isAdmin) {
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
}
/**
* @param {string} path
* @returns {string}
*/
staticLink = path => VERSION_PREFIX() + 'static/' + path,
/**
* @param {string} path
* @returns {string}
*/
export function staticLink(path) {
return VERSION_PREFIX + 'static/' + path;
}
/**
* @param {string} theme
* @returns {string}
*/
themePreviewLink = theme => {
let prefix = VERSION_PREFIX();
if ('@custom' === theme.slice(-7)) {
theme = theme.slice(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || '';
}
/**
* @returns {string}
*/
export function openPgpJs() {
return staticLink('js/min/openpgp.min.js');
}
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
},
/**
* @returns {string}
*/
export function openPgpWorkerJs() {
return staticLink('js/min/openpgp.worker.min.js');
}
/**
* @param {string} inboxFolderName = 'INBOX'
* @returns {string}
*/
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
/**
* @param {string} theme
* @returns {string}
*/
export function themePreviewLink(theme) {
let prefix = VERSION_PREFIX;
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || '';
}
/**
* @param {string=} screenName = ''
* @returns {string}
*/
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
}
/**
* @param {string=} screenName
* @returns {string}
*/
admin = screenName => HASH_PREFIX + (
'AdminDomains' == screenName ? 'domains'
: 'AdminSecurity' == screenName ? 'security'
: ''
),
/**
* @param {string} inboxFolderName = 'INBOX'
* @returns {string}
*/
export function mailbox(inboxFolderName = 'INBOX') {
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
}
/**
* @param {string} folder
* @param {number=} page = 1
* @param {string=} search = ''
* @param {number=} threadUid = 0
* @returns {string}
*/
mailBox = (folder, page, search, threadUid) => {
let result = [HASH_PREFIX + 'mailbox'];
/**
* @param {string=} screenName = ''
* @returns {string}
*/
export function settings(screenName = '') {
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
}
if (folder) {
result.push(folder + (threadUid ? '~' + threadUid : ''));
}
/**
* @param {string} screenName
* @returns {string}
*/
export function admin(screenName) {
let result = HASH_PREFIX;
switch (screenName) {
case 'AdminDomains':
result += 'domains';
break;
case 'AdminSecurity':
result += 'security';
break;
// no default
}
page = pInt(page, 1);
if (1 < page) {
result.push('p' + page);
}
return result;
}
/**
* @param {string} folder
* @param {number=} page = 1
* @param {string=} search = ''
* @param {string=} threadUid = ''
* @returns {string}
*/
export function mailBox(folder, page = 1, search = '', threadUid = '') {
page = pInt(page, 1);
search = pString(search);
let result = HASH_PREFIX + 'mailbox/';
if (folder) {
const resultThreadUid = pInt(threadUid);
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
}
if (1 < page) {
result = result.replace(/\/+$/, '') + '/p' + page;
}
if (search) {
result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
}
return result;
}
search = pString(search);
if (search) {
result.push(encodeURI(search));
}
return result.join('/');
};

View file

@ -1,60 +0,0 @@
import { doc } from 'Common/Globals';
import { i18n } from 'Common/Translator';
export function timestampToString(timeStampInUTC, formatStr) {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const mt = m.getTime(), date = new Date,
dt = date.setHours(0,0,0,0);
if (mt > dt)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
if (mt > dt - 86400000)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
if (date.getFullYear() === m.getFullYear())
return m.format('d M');
return m.format('LL');
}
case 'FULL':
return m.format('LLL');
default:
return m.format(formatStr);
}
}
return '';
}
export function timeToNode(element, time) {
try {
time = time || (Date.parse(element.dateTime) / 1000);
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
let key = element.dataset.momentFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
}
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
}
addEventListener('reload-time', () => setTimeout(() =>
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
, 1)
);

View file

@ -1,5 +1,6 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { isArray, isFunction } from 'Common/Utils';
const SIMPLE_HOOKS = {},
@ -36,7 +37,7 @@ export function runHook(name, args = []) {
* @param {?number=} timeout
*/
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
rl.app && rl.app.Remote.defaultRequest(callback, 'Plugin' + action, parameters, timeout);
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
};
/**
@ -78,3 +79,5 @@ rl.pluginSettingsGet = (pluginSection, name) => {
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
};
rl.pluginPopupView = AbstractViewPopup;

View file

@ -1,5 +1,16 @@
import ko from 'ko';
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
/*
oCallbacks:
ItemSelect
MiddleClick
AutoSelect
ItemGetUid
UpOrDown
*/
export class Selector {
/**
@ -19,8 +30,8 @@ export class Selector {
sItemFocusedSelector
) {
this.list = koList;
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = koComputable(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null);
@ -209,11 +220,9 @@ export class Selector {
itemSelected(item) {
if (this.isListChecked()) {
if (!item) {
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
}
item || (this.oCallbacks.ItemSelect || (()=>0))(null);
} else if (item) {
(this.oCallbacks.onItemSelect || (()=>{}))(item);
(this.oCallbacks.ItemSelect || (()=>0))(item);
}
}
@ -226,13 +235,17 @@ export class Selector {
this.oContentScrollable = contentScrollable;
if (contentScrollable) {
contentScrollable.addEventListener('click', event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
let getItem = selector => {
let el = event.target.closestWithin(selector, contentScrollable);
return el ? ko.dataFor(el) : null;
};
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
if (el) {
const item = ko.dataFor(el);
addEventsListeners(contentScrollable, {
click: event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
const item = getItem(this.sItemCheckedSelector);
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
@ -241,26 +254,33 @@ export class Selector {
item.checked(!item.checked());
}
}
},
auxclick: event => {
if (1 == event.button) {
const item = getItem(this.sItemSelector);
if (item) {
this.focusedItem(item);
(this.oCallbacks.MiddleClick || (()=>0))(item);
}
}
}
});
shortcuts.add('enter,open', '', keyScope, () => {
registerShortcut('enter,open', '', keyScope, () => {
const focused = this.focusedItem();
if (focused && !focused.selected()) {
this.actionClick(focused);
return false;
}
return true;
});
shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false);
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
this.newSelectPosition(event.key, true);
return false;
});
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
this.newSelectPosition(event.key, false);
return false;
});
@ -271,7 +291,7 @@ export class Selector {
* @returns {boolean}
*/
autoSelect() {
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
return !!(this.oCallbacks.AutoSelect || (()=>1))();
}
/**
@ -281,7 +301,7 @@ export class Selector {
getItemUid(item) {
let uid = '';
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
if (getItemUidCallback && item) {
uid = getItemUidCallback(item);
}
@ -312,7 +332,7 @@ export class Selector {
} else if (++i < listLen) {
result = list[i];
}
result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
} else if ('Home' === sEventKey) {
result = list[0];
} else if ('End' === sEventKey) {

View file

@ -2,158 +2,211 @@ import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
let I18N_DATA = window.snappymailI18N || {};
let I18N_DATA = {};
export const trigger = ko.observable(false);
/**
* @param {string} key
* @param {Object=} valueList
* @param {string=} defaulValue
* @returns {string}
*/
export function i18n(key, valueList, defaulValue) {
let path = key.split('/');
if (!I18N_DATA[path[0]] || !path[1]) {
return defaulValue || key;
}
let result = I18N_DATA[path[0]][path[1]] || defaulValue || key;
if (valueList) {
Object.entries(valueList).forEach(([key, value]) => {
result = result.replace('%' + key + '%', value);
});
}
return result;
}
const i18nToNode = element => {
const key = element.dataset.i18n;
if (key) {
if ('[' === key.substr(0, 1)) {
switch (key.substr(0, 6)) {
case '[html]':
element.innerHTML = i18n(key.substr(6));
break;
case '[place':
element.placeholder = i18n(key.substr(13));
break;
case '[title':
element.title = i18n(key.substr(7));
break;
// no default
const
i18nToNode = element => {
const key = element.dataset.i18n;
if (key) {
if ('[' === key.slice(0, 1)) {
switch (key.slice(0, 6)) {
case '[html]':
element.innerHTML = i18n(key.slice(6));
break;
case '[place':
element.placeholder = i18n(key.slice(13));
break;
case '[title':
element.title = i18n(key.slice(7));
break;
// no default
}
} else {
element.textContent = i18n(key);
}
} else {
element.textContent = i18n(key);
}
}
},
},
init = () => {
if (rl.I18N) {
I18N_DATA = rl.I18N;
Date.defineRelativeTimeFormat(rl.relativeTime || {});
rl.I18N = null;
return 1;
}
},
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
getNotificationMessage = code => {
let key = getKeyByValue(Notification, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
};
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
export function i18nToNodes(element) {
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1);
}
export const
trigger = ko.observable(false),
/**
* @param {Function} startCallback
* @param {Function=} langCallback = null
*/
export function initOnStartOrLangChange(startCallback, langCallback = null) {
startCallback && startCallback();
startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback);
}
function getNotificationMessage(code) {
let key = getKeyByValue(Notification, code);
if (key) {
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
return I18N_DATA.NOTIFICATIONS[key];
}
}
/**
* @param {number} code
* @param {*=} message = ''
* @param {*=} defCode = null
* @returns {string}
*/
export function getNotification(code, message = '', defCode = 0) {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10))
|| '';
}
/**
* @param {*} code
* @returns {string}
*/
export function getUploadErrorDescByCode(code) {
let result = 'UNKNOWN';
code = parseInt(code, 10) || 0;
switch (code) {
case UploadErrorCode.FileIsTooBig:
case UploadErrorCode.FilePartiallyUploaded:
case UploadErrorCode.NoFileUploaded:
case UploadErrorCode.MissingTempFolder:
case UploadErrorCode.OnSavingFile:
case UploadErrorCode.FileType:
result = i18nKey(getKeyByValue(UploadErrorCode, code));
break;
}
return i18n('UPLOAD/ERROR_' + result);
}
/**
* @param {boolean} admin
* @param {string} language
*/
export function reload(admin, language) {
return new Promise((resolve, reject) => {
const script = createElement('script');
script.onload = () => {
// reload the data
if (window.snappymailI18N) {
I18N_DATA = window.snappymailI18N;
i18nToNodes(doc);
dispatchEvent(new CustomEvent('reload-time'));
trigger(!trigger());
/**
* @param {string} key
* @param {Object=} valueList
* @param {string=} defaulValue
* @returns {string}
*/
i18n = (key, valueList, defaulValue) => {
let result = defaulValue || key;
if (key) {
let path = key.split('/');
if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result;
}
window.snappymailI18N = null;
script.remove();
resolve();
};
script.onerror = () => reject(new Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);
});
}
}
if (valueList) {
forEachObjectEntry(valueList, (key, value) => {
result = result.replace('%' + key + '%', value);
});
}
return result;
},
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
*/
export function convertLangName(language, isEng = false) {
return i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
);
}
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
i18nToNodes = element =>
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1),
timestampToString = (timeStampInUTC, formatStr) => {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const mt = m.getTime(), date = new Date,
dt = date.setHours(0,0,0,0);
if (mt > dt)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
if (mt > dt - 86400000)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
if (date.getFullYear() === m.getFullYear())
return m.format('d M');
return m.format('LL');
}
case 'FULL':
return m.format('LLL');
default:
return m.format(formatStr);
}
}
return '';
},
timeToNode = (element, time) => {
try {
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
} else {
time = Date.parse(element.dateTime) / 1000;
}
let key = element.dataset.momentFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
},
reloadTime = () => setTimeout(() =>
doc.querySelectorAll('time').forEach(element => timeToNode(element))
, 1),
/**
* @param {Function} startCallback
* @param {Function=} langCallback = null
*/
initOnStartOrLangChange = (startCallback, langCallback = null) => {
startCallback && startCallback();
startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback);
},
/**
* @param {number} code
* @param {*=} message = ''
* @param {*=} defCode = null
* @returns {string}
*/
getNotification = (code, message = '', defCode = 0) => {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10))
|| '';
},
/**
* @param {*} code
* @returns {string}
*/
getUploadErrorDescByCode = code => {
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
},
/**
* @param {boolean} admin
* @param {string} language
*/
translatorReload = (admin, language) =>
new Promise((resolve, reject) => {
const script = createElement('script');
script.onload = () => {
// reload the data
if (init()) {
i18nToNodes(doc);
admin || reloadTime();
trigger(!trigger());
}
script.remove();
resolve();
};
script.onerror = () => reject(new Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);
}),
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
*/
convertLangName = (language, isEng = false) =>
i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
);
init();

View file

@ -1,107 +1,50 @@
import { SaveSettingsStep } from 'Common/Enums';
import { doc, createElement, elementById } from 'Common/Globals';
export const
isArray = Array.isArray,
isNonEmptyArray = array => isArray(array) && array.length,
isFunction = v => typeof v === 'function';
/**
* @param {*} value
* @param {number=} defaultValue = 0
* @returns {number}
*/
export function pInt(value, defaultValue = 0) {
value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value;
}
/**
* @param {*} value
* @returns {string}
*/
export function pString(value) {
return null != value ? '' + value : '';
}
/**
* @returns {boolean}
*/
export function inFocus() {
try {
return doc.activeElement && doc.activeElement.matches(
'input,textarea,.cke_editable'
);
} catch (e) {
return false;
}
}
/**
* @param {string} theme
* @returns {string}
*/
export const convertThemeName = theme => {
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
}
return theme
.replace(/([A-Z])/g, ' $1')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.trim();
};
/**
* @param {object} domOption
* @param {object} item
* @returns {void}
*/
export function defaultOptionsAfterRender(domItem, item) {
if (item && undefined !== item.disabled && domItem) {
domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
}
}
/**
* @param {object} koTrigger
* @param {mixed} context
* @returns {mixed}
*/
export function settingsSaveHelperSimpleFunction(koTrigger, context) {
return iError => {
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
};
}
import { elementById } from 'Common/Globals';
let __themeTimer = 0,
__themeJson = null;
/**
* @param {string} value
* @param {function=} themeTrigger = noop
* @returns {void}
*/
export function changeTheme(value, themeTrigger = ()=>{}) {
const themeLink = elementById('app-theme-link'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null;
};
export const
isArray = Array.isArray,
arrayLength = array => isArray(array) && array.length,
isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '',
let themeStyle = elementById('app-theme-style'),
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
if (url) {
url = url.toString()
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
if ('Json/' !== url.substr(-5)) {
url += 'Json/';
}
pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value;
},
convertThemeName = theme => theme
.replace(/@custom$/, '')
.replace(/([A-Z])/g, ' $1')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.trim(),
defaultOptionsAfterRender = (domItem, item) =>
domItem && item && undefined !== item.disabled
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
/* // Without deprecated 'unescape':
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)),
*/
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
changeTheme = (value, themeTrigger = ()=>0) => {
const themeStyle = elementById('app-theme-style'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null;
},
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
clearTimeout(__themeTimer);
@ -117,36 +60,12 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
}
rl.fetchJSON(url, init)
.then(data => {
if (data && isArray(data) && 2 === data.length) {
if (themeLink && !themeStyle) {
themeStyle = createElement('style');
themeStyle.id = 'app-theme-style';
themeLink.after(themeStyle);
themeLink.remove();
}
if (themeStyle) {
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
}
if (2 === arrayLength(data)) {
themeStyle.textContent = data[1];
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
}
}
},
export function addObservablesTo(target, observables) {
Object.entries(observables).forEach(([key, value]) =>
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) );
}
export function addComputablesTo(target, computables) {
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn));
}
export function addSubscribablesTo(target, subscribables) {
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn));
}
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

View file

@ -1,341 +1,46 @@
import { ComposeType, FolderType } from 'Common/EnumsUser';
import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
import { Notification } from 'Common/Enums';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator';
import { EmailModel } from 'Model/Email';
import { encodeHtml } from 'Common/Html';
import { isArray } from 'Common/Utils';
import { createElement } from 'Common/Globals';
import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import * as Local from 'Storage/Client';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
export const
dropdownsDetectVisibility = (() =>
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
).debounce(50),
/**
* @param {(string|number)} value
* @param {boolean=} includeZero = true
* @param {string} link
* @returns {boolean}
*/
export function isPosNumeric(value, includeZero = true) {
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
}
/**
* @param {string} text
* @param {number=} len = 100
* @returns {string}
*/
function splitPlainText(text, len = 100) {
let prefix = '',
subText = '',
result = text,
spacePos = 0,
newLinePos = 0;
while (result.length > len) {
subText = result.substr(0, len);
spacePos = subText.lastIndexOf(' ');
newLinePos = subText.lastIndexOf('\n');
if (-1 !== newLinePos) {
spacePos = newLinePos;
}
if (-1 === spacePos) {
spacePos = len;
}
prefix += subText.substr(0, spacePos) + '\n';
result = result.substr(spacePos + 1);
download = (link, name = "") => {
if (ThemeStore.isMobile()) {
open(link, '_self');
focus();
} else {
const oLink = createElement('a');
oLink.href = link;
oLink.target = '_blank';
oLink.download = name;
doc.body.appendChild(oLink).click();
oLink.remove();
}
return prefix + result;
}
/**
* @param {string} html
* @returns {string}
*/
export function htmlToPlain(html) {
let pos = 0,
limit = 0,
iP1 = 0,
iP2 = 0,
iP3 = 0,
text = '';
const convertBlockquote = (blockquoteText) => {
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
);
};
const convertDivs = (...args) => {
if (args && 1 < args.length) {
let divText = args[1].trim();
if (divText.length) {
divText = divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
divText = '\n' + divText.trim() + '\n';
}
return divText;
}
return '';
};
const
tpl = createElement('template'),
convertPre = (...args) =>
args && 1 < args.length
? args[1]
.toString()
.replace(/[\n]/gm, '<br/>')
.replace(/[\r]/gm, '')
: '',
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + encodeHtml(args[2]) : ''),
convertLinks = (...args) => (args && 1 < args.length ? args[1].trim() : '');
tpl.innerHTML = html
.replace(/<p[^>]*><\/p>/gi, '')
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
.replace(/[\s]+/gm, ' ')
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
.replace(/<br[^>]*>/gim, '\n')
.replace(/<\/h[\d]>/gi, '\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<ul[^>]*>/gim, '\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<li[^>]*>/gim, ' * ')
.replace(/<\/li>/gi, '\n')
.replace(/<\/td>/gi, '\n')
.replace(/<\/tr>/gi, '\n')
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
.replace(/<\/div>/gi, '\n')
.replace(/&nbsp;/gi, ' ')
.replace(/&quot;/gi, '"')
.replace(/<[^>]*>/gm, '');
text = splitPlainText(tpl.content.textContent
.replace(/\n[ \t]+/gm, '\n')
.replace(/[\n]{3,}/gm, '\n\n')
.replace(/&gt;/gi, '>')
.replace(/&lt;/gi, '<')
.replace(/&amp;/gi, '&')
);
pos = 0;
limit = 800;
while (0 < limit) {
--limit;
iP1 = text.indexOf('__bq__start__', pos);
if (-1 < iP1) {
iP2 = text.indexOf('__bq__start__', iP1 + 5);
iP3 = text.indexOf('__bq__end__', iP1 + 5);
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
pos = 0;
} else if (-1 < iP2 && iP2 < iP3) {
pos = iP2 - 1;
} else {
pos = 0;
}
} else {
break;
}
}
return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
}
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
export function plainToHtml(plain) {
plain = plain.toString().replace(/\r/g, '');
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.substr(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.substr(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.substr(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
return aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br/>');
}
rl.Utils = {
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
};
/**
* @param {Array} aSystem
* @param {Array} aList
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fDisableCallback
* @param {Function=} fRenameCallback
* @param {boolean=} bSystem
* @param {boolean=} bBuildUnvisible
* @returns {Array}
*/
export function folderListOptionsBuilder(
aSystem,
aList,
aDisabled,
aHeaderLines,
fDisableCallback,
fRenameCallback,
bSystem,
bBuildUnvisible
) {
let /**
* @type {?FolderModel}
*/
bSep = false,
aResult = [];
const sDeepPrefix = '\u00A0\u00A0\u00A0';
bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
if (!isArray(aDisabled)) {
aDisabled = [];
}
if (!isArray(aHeaderLines)) {
aHeaderLines = [];
}
aHeaderLines.forEach(line => {
aResult.push({
id: line[0],
name: line[1],
system: false,
dividerbar: false,
disabled: false
});
});
bSep = true;
aSystem.forEach(oItem => {
aResult.push({
id: oItem.fullNameRaw,
name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
system: true,
dividerbar: bSep,
disabled:
!oItem.selectable ||
aDisabled.includes(oItem.fullNameRaw) ||
(fDisableCallback ? fDisableCallback(oItem) : false)
});
bSep = false;
});
bSep = true;
aList.forEach(oItem => {
// if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
if (
(oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
(oItem.selectable || oItem.hasSubscribedSubfolders())
) {
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
aResult.push({
id: oItem.fullNameRaw,
name:
sDeepPrefix.repeat(oItem.deep + 1) +
(fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
system: false,
dividerbar: bSep,
disabled:
!oItem.selectable ||
aDisabled.includes(oItem.fullNameRaw) ||
(fDisableCallback ? fDisableCallback(oItem) : false)
});
bSep = false;
}
}
if (oItem.subscribed() && oItem.subFolders.length) {
aResult = aResult.concat(
folderListOptionsBuilder(
[],
oItem.subFolders(),
aDisabled,
[],
fDisableCallback,
fRenameCallback,
bSystem,
bBuildUnvisible
)
);
}
});
return aResult;
}
/**
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
* @param {Object|Array} objectOrObjects
* @returns {void}
*/
export function delegateRunOnDestroy(objectOrObjects) {
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
obj => obj.onDestroy && obj.onDestroy()
);
}
},
/**
* @returns {function}
*/
export function computedPaginatorHelper(koCurrentPage, koPageCount) {
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return () => {
const currentPage = koCurrentPage(),
pageCount = koPageCount(),
@ -395,13 +100,13 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
if (3 === prev) {
fAdd(2, false);
} else if (3 < prev) {
fAdd(Math.round((prev - 1) / 2), false, '...');
fAdd(Math.round((prev - 1) / 2), false, '');
}
if (pageCount - 2 === next) {
fAdd(pageCount - 1, true);
} else if (pageCount - 2 > next) {
fAdd(Math.round((pageCount + next) / 2), true, '...');
fAdd(Math.round((pageCount + next) / 2), true, '');
}
// first and last
@ -416,81 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
return result;
};
}
},
/**
* @param {string} mailToUrl
* @returns {boolean}
*/
export function mailToHelper(mailToUrl) {
if (
mailToUrl &&
'mailto:' ===
mailToUrl
.toString()
.substr(0, 7)
.toLowerCase()
) {
mailToUrl = mailToUrl.toString().substr(7);
mailToHelper = mailToUrl => {
if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
mailToUrl = mailToUrl.slice(7).split('?');
let to = [],
cc = null,
bcc = null,
params = {};
const email = mailToUrl.replace(/\?.+$/, ''),
query = mailToUrl.replace(/^[^?]*\?/, '');
query.split('&').forEach(temp => {
temp = temp.split('=');
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
});
if (undefined !== params.to) {
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
to = Object.values(
to.reduce((result, value) => {
if (value) {
if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
);
} else {
to = EmailModel.parseEmailLine(email);
}
if (undefined !== params.cc) {
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
}
if (undefined !== params.bcc) {
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
}
const
email = mailToUrl[0],
params = new URLSearchParams(mailToUrl[1]),
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
showMessageComposer([
ComposeType.Empty,
null,
to,
cc,
bcc,
null == params.subject ? null : decodeURIComponent(params.subject),
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
params.get('to')
? Object.values(
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
if (value) {
if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
)
: EmailModel.parseEmailLine(email),
toEmailModel(params.get('cc')),
toEmailModel(params.get('bcc')),
params.get('subject'),
plainToHtml(params.get('body') || '')
]);
return true;
}
return false;
}
},
export function showMessageComposer(params = [])
showMessageComposer = (params = []) =>
{
rl.app.showMessageComposer(params);
}
},
initFullscreen = (el, fn) =>
{
let event = 'fullscreenchange';
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
el.requestFullscreen = el.webkitRequestFullscreen;
event = 'webkit'+event;
}
if (el.requestFullscreen) {
el.addEventListener(event, fn);
return el;
}
},
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
{
if (source.layoutResizer && source.layoutResizer.mode != mode) {
target.removeAttribute('style');
source.removeAttribute('style');
}
// source.classList.toggle('resizable', mode);
if (mode) {
const length = Local.get(sClientSideKeyName+mode);
if (!source.layoutResizer) {
const resizer = createElement('div', {'class':'resizer'}),
size = {},
store = () => {
if ('Width' == resizer.mode) {
target.style.left = source.offsetWidth + 'px';
Local.set(resizer.key+resizer.mode, source.offsetWidth);
} else {
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
Local.set(resizer.key+resizer.mode, source.offsetHeight);
}
},
cssint = s => {
let value = getComputedStyle(source, null)[s].replace('px', '');
if (value.includes('%')) {
value = source.parentElement['offset'+resizer.mode]
* value.replace('%', '') / 100;
}
return parseFloat(value);
};
source.layoutResizer = resizer;
source.append(resizer);
resizer.addEventListener('mousedown', {
handleEvent: function(e) {
if ('mousedown' == e.type) {
const lmode = resizer.mode.toLowerCase();
e.preventDefault();
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
size.min = cssint('min-'+lmode);
size.max = cssint('max-'+lmode);
size.org = cssint(lmode);
addEventListener('mousemove', this);
addEventListener('mouseup', this);
} else if ('mousemove' == e.type) {
const lmode = resizer.mode.toLowerCase(),
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
if (length >= size.min && length <= size.max ) {
source.style[lmode] = length + 'px';
source.observer || store();
}
} else if ('mouseup' == e.type) {
removeEventListener('mousemove', this);
removeEventListener('mouseup', this);
}
}
});
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
}
source.layoutResizer.mode = mode;
source.layoutResizer.key = sClientSideKeyName;
source.observer && source.observer.observe(source, { box: 'border-box' });
if (length) {
source.style[mode] = length + 'px';
}
} else {
source.observer && source.observer.disconnect();
}
},
populateMessageBody = (oMessage, preload) => {
if (oMessage) {
preload || MessageUserStore.hideMessageBodies();
preload || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notification.RequestAborted !== iError && !preload) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
oMessage = preload ? oMessage : null;
let
isNew = false,
json = oData && oData.Result,
message = oMessage || MessageUserStore.message();
if (
json &&
MessageModel.validJson(json) &&
message &&
message.folder === json.Folder
) {
const threads = message.threads(),
messagesDom = MessageUserStore.bodiesDom();
if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
message = MessageModel.reviveFromJson(json);
if (message) {
message.threads(threads);
MessageFlagsCache.initMessage(message);
// Set clone
MessageUserStore.message(MessageModel.fromMessageListItem(message));
message = MessageUserStore.message();
isNew = true;
}
}
if (message && message.uid == json.Uid) {
oMessage || MessageUserStore.error('');
/*
if (bCached) {
delete json.Flags;
}
*/
isNew || message.revivePropertiesFromJson(json);
addRequestedMessage(message.folder, message.uid);
if (messagesDom) {
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
body = elementById(id);
if (body) {
message.body = body;
message.isHtml(body.classList.contains('html'));
message.hasImages(body.rlHasImages);
} else {
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
+ (message.pgpSigned() ? ' openpgp-signed' : '')
+ (message.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">'
+ '</div>');
message.body = body;
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
message.viewPlain();
}
MessageUserStore.purgeMessageBodyCache();
}
messagesDom.append(body);
if (!oMessage) {
MessageUserStore.activeDom(message.body);
MessageUserStore.hideMessageBodies();
message.body.hidden = false;
}
oMessage && message.viewPopupMessage();
}
MessageFlagsCache.initMessage(message);
if (message.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
if (message && isNew) {
let selectedMessage = MessagelistUserStore.selectedMessage();
if (
selectedMessage &&
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
) {
MessagelistUserStore.selectedMessage(null);
if (1 === MessagelistUserStore.length) {
MessagelistUserStore.focusedMessage(null);
}
} else if (!selectedMessage) {
selectedMessage = MessagelistUserStore.find(
subMessage =>
subMessage &&
subMessage.folder === message.folder &&
subMessage.uid == message.uid
);
if (selectedMessage) {
MessagelistUserStore.selectedMessage(selectedMessage);
MessagelistUserStore.focusedMessage(selectedMessage);
}
}
}
}
}
}
preload || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
}
};

View file

@ -1,14 +0,0 @@
export class AbstractComponent {
constructor() {
this.disposable = [];
}
dispose() {
this.disposable.forEach((funcToDispose) => {
if (funcToDispose && funcToDispose.dispose) {
funcToDispose.dispose();
}
});
}
}

View file

@ -1,27 +1,15 @@
import ko from 'ko';
import { AbstractComponent } from 'Component/Abstract';
class AbstractCheckbox extends AbstractComponent {
export class AbstractCheckbox {
/**
* @param {Object} params = {}
*/
constructor(params = {}) {
super();
this.value = ko.isObservable(params.value) ? params.value
: ko.observable(!!params.value);
this.value = params.value;
if (undefined === this.value || !this.value.subscribe) {
this.value = ko.observable(!!this.value);
}
this.enable = params.enable;
if (undefined === this.enable || !this.enable.subscribe) {
this.enable = ko.observable(undefined === this.enable || !!this.enable);
}
this.disable = params.disable;
if (undefined === this.disable || !this.disable.subscribe) {
this.disable = ko.observable(!!this.disable);
}
this.enable = ko.isObservable(params.enable) ? params.enable
: ko.observable(undefined === params.enable || !!params.enable);
this.label = params.label || '';
this.inline = !!params.inline;
@ -30,10 +18,6 @@ class AbstractCheckbox extends AbstractComponent {
}
click() {
if (this.enable() && !this.disable()) {
this.value(!this.value());
}
this.enable() && this.value(!this.value());
}
}
export { AbstractCheckbox };

View file

@ -1,61 +1,57 @@
import ko from 'ko';
import { pInt } from 'Common/Utils';
import { SaveSettingsStep } from 'Common/Enums';
import { AbstractComponent } from 'Component/Abstract';
import { koComputable } from 'External/ko';
import { dispose } from 'External/ko';
class AbstractInput extends AbstractComponent {
export class AbstractInput {
/**
* @param {Object} params
*/
constructor(params) {
super();
this.value = params.value || '';
this.size = params.size || 0;
this.label = params.label || '';
this.preLabel = params.preLabel || '';
this.enable = undefined === params.enable ? true : params.enable;
this.enable = null == params.enable ? true : params.enable;
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
this.placeholder = params.placeholder || '';
this.labeled = undefined !== params.label;
this.preLabeled = undefined !== params.preLabel;
this.triggered = undefined !== params.trigger && !!this.trigger;
this.classForTrigger = ko.observable('');
this.className = ko.computed(() => {
const size = ko.unwrap(this.size),
suffixValue = this.trigger ? ' ' + ('settings-saved-trigger-input ' + this.classForTrigger()).trim() : '';
return (0 < size ? 'span' + size : '') + suffixValue;
});
if (undefined !== params.width && params.element) {
params.element.querySelectorAll('input,select,textarea').forEach(node => node.style.width = params.width);
}
this.disposable.push(this.className);
this.labeled = null != params.label;
let size = 0 < params.size ? 'span' + params.size : '';
if (this.trigger) {
this.setTriggerState(this.trigger());
const
classForTrigger = ko.observable(''),
setTriggerState = value => {
switch (pInt(value)) {
case SaveSettingsStep.TrueResult:
classForTrigger('success');
break;
case SaveSettingsStep.FalseResult:
classForTrigger('error');
break;
default:
classForTrigger('');
break;
}
};
this.disposable.push(this.trigger.subscribe(this.setTriggerState, this));
setTriggerState(this.trigger());
this.className = koComputable(() =>
(size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
);
this.disposables = [
this.trigger.subscribe(setTriggerState, this),
this.className
];
} else {
this.className = size;
this.disposables = [];
}
}
setTriggerState(value) {
switch (pInt(value)) {
case SaveSettingsStep.TrueResult:
this.classForTrigger('success');
break;
case SaveSettingsStep.FalseResult:
this.classForTrigger('error');
break;
default:
this.classForTrigger('');
break;
}
dispose() {
this.disposables.forEach(dispose);
}
}
export { AbstractInput };

View file

@ -1,5 +1,4 @@
import { doc, createElement } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email';
const contentType = 'snappymail/emailaddress',
@ -17,7 +16,7 @@ export class EmailAddressesComponent {
doc.body.append(datalist);
}
var self = this,
const self = this,
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
@ -42,49 +41,55 @@ export class EmailAddressesComponent {
// Create the elements
self.ul = createElement('ul',{class:"emailaddresses"});
self.ul.addEventListener('click', e => self._focus(e));
self.ul.addEventListener('dblclick', e => self._editTag(e));
self.ul.addEventListener("dragenter", fnDrag);
self.ul.addEventListener("dragover", fnDrag);
self.ul.addEventListener("drop", e => {
if (validDropzone() && dragAddress.value) {
e.preventDefault();
dragAddress.source._removeDraggedTag(dragAddress.li);
self._parseValue(dragAddress.value);
addEventsListeners(self.ul, {
click: e => self._focus(e),
dblclick: e => self._editTag(e),
dragenter: fnDrag,
dragover: fnDrag,
drop: e => {
if (validDropzone() && dragAddress.value) {
e.preventDefault();
dragAddress.source._removeDraggedTag(dragAddress.li);
self._parseValue(dragAddress.value);
}
}
});
self.input = createElement('input',{type:"text", list:datalist.id,
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
self.input.addEventListener('focus', () => self._focusTrigger(true));
self.input.addEventListener('blur', () => {
// prevent autoComplete menu click from causing a false 'blur'
self._parseInput(true);
self._focusTrigger(false);
});
self.input.addEventListener('keydown', e => {
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
// if our input contains no value and backspace has been pressed, select the last tag
var lastTag = self.inputCont.previousElementSibling,
input = self.input;
if (lastTag && (!input.value
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
) {
e.preventDefault();
lastTag.querySelector('a').focus();
}
self._updateDatalist();
} else if (e.key == 'Enter') {
e.preventDefault();
addEventsListeners(self.input, {
focus: () => {
self._focusTrigger(true);
self.input.value || self._resetDatalist();
},
blur: () => {
// prevent autoComplete menu click from causing a false 'blur'
self._parseInput(true);
self._focusTrigger(false);
},
keydown: e => {
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
// if our input contains no value and backspace has been pressed, select the last tag
var lastTag = self.inputCont.previousElementSibling,
input = self.input;
if (lastTag && (!input.value
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
) {
e.preventDefault();
lastTag.querySelector('a').focus();
}
self._updateDatalist();
} else if (e.key == 'Enter') {
e.preventDefault();
self._parseInput(true);
}
},
input: () => {
self._parseInput();
self._updateDatalist();
}
});
self.input.addEventListener('input', () => {
self._parseInput();
self._updateDatalist();
});
self.input.addEventListener('focus', () => self.input.value || self._resetDatalist());
// define starting placeholder
if (element.placeholder) {
@ -116,7 +121,7 @@ export class EmailAddressesComponent {
)
}
}).throttle(500)
: () => {};
: () => 0;
}
_focusTrigger(bValue) {
@ -138,20 +143,56 @@ export class EmailAddressesComponent {
_parseValue(val) {
if (val) {
var self = this,
values = [];
const v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.substr(-1)))
? EmailModel.splitEmailLine(val)
: null;
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
const self = this,
v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
if (values.length) {
self._setChosen(values);
values.forEach(a => {
var v = a[0].trim(),
exists = false,
lastIndex = -1,
obj = {
key : '',
obj : null,
value : ''
};
self._chosenValues.forEach((vv, kk) => {
if (vv.value === self._lastEdit) {
lastIndex = kk;
}
vv.value === v && (exists = true);
});
if (v !== '' && a[1] && !exists) {
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
obj.value = v;
obj.obj = a[1];
if (-1 < lastIndex) {
self._chosenValues.splice(lastIndex, 0, obj);
} else {
self._chosenValues.push(obj);
}
self._lastEdit = '';
self._renderTags();
}
});
if (values.length === 1 && values[0] === '' && self._lastEdit !== '') {
self._lastEdit = '';
self._renderTags();
}
self._setValue(self._buildValue());
return true;
}
}
@ -202,56 +243,6 @@ export class EmailAddressesComponent {
self._resizeInput(ev);
}
_setChosen(valArr) {
var self = this;
if (!isArray(valArr)){
return false;
}
valArr.forEach(a => {
var v = a[0].trim(),
exists = false,
lastIndex = -1,
obj = {
key : '',
obj : null,
value : ''
};
self._chosenValues.forEach((vv, kk) => {
if (vv.value === self._lastEdit) {
lastIndex = kk;
}
vv.value === v && (exists = true);
});
if (v !== '' && a && a[1] && !exists) {
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
obj.value = v;
obj.obj = a[1];
if (-1 < lastIndex) {
self._chosenValues.splice(lastIndex, 0, obj);
} else {
self._chosenValues.push(obj);
}
self._lastEdit = '';
self._renderTags();
}
});
if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') {
self._lastEdit = '';
self._renderTags();
}
self._setValue(self._buildValue());
}
_buildValue() {
return this._chosenValues.map(v => v.value).join(',');
}
@ -276,66 +267,70 @@ export class EmailAddressesComponent {
el = createElement('a',{href:'#', class:'ficon'});
el.append('✖');
el.addEventListener('click', e => self._removeTag(e, li));
el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
el.addEventListener('blur', () => li.className = null);
el.addEventListener('keydown', e => {
switch (e.key) {
case 'Delete':
case 'Backspace':
self._removeTag(e, li);
break;
addEventsListeners(el, {
click: e => self._removeTag(e, li),
focus: () => li.className = 'emailaddresses-selected',
blur: () => li.className = null,
keydown: e => {
switch (e.key) {
case 'Delete':
case 'Backspace':
self._removeTag(e, li);
break;
// 'e' - edit tag (removes tag and places value into visible input
case 'e':
case 'Enter':
self._editTag(e);
break;
// 'e' - edit tag (removes tag and places value into visible input
case 'e':
case 'Enter':
self._editTag(e);
break;
case 'ArrowLeft':
// select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) {
previous.querySelector('a').focus();
} else {
self.focus();
}
break;
case 'ArrowLeft':
// select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) {
previous.querySelector('a').focus();
} else {
self.focus();
}
break;
case 'ArrowRight':
// select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) {
next.querySelector('a').focus();
} else {
this.focus();
}
break;
case 'ArrowRight':
// select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) {
next.querySelector('a').focus();
} else {
this.focus();
}
break;
case 'ArrowDown':
self._focus(e);
break;
case 'ArrowDown':
self._focus(e);
break;
}
}
});
li.append(el);
li.emailaddress = v;
li.addEventListener("dragstart", e => {
dragAddress = {
source: self,
li: li,
value: li.emailaddress.obj.toLine()
};
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
e.dataTransfer.setData('text/plain', contentType);
// e.dataTransfer.setDragImage(li, 0, 0);
e.dataTransfer.effectAllowed = 'move';
li.style.opacity = 0.25;
});
li.addEventListener("dragend", () => {
dragAddress = null;
li.style.cssText = '';
addEventsListeners(li, {
dragstart: e => {
dragAddress = {
source: self,
li: li,
value: li.emailaddress.obj.toLine()
};
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
e.dataTransfer.setData('text/plain', contentType);
// e.dataTransfer.setDragImage(li, 0, 0);
e.dataTransfer.effectAllowed = 'move';
li.style.opacity = 0.25;
},
dragend: () => {
dragAddress = null;
li.style.cssText = '';
}
});
self.inputCont.before(li);

View file

@ -1,41 +1,3 @@
import ko from 'ko';
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {
/**
* @param {Object} params
*/
constructor(params) {
super(params);
this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
this.animationBoxSetTrue = this.animationBoxSetTrue.bind(this);
this.animationCheckmarkSetTrue = this.animationCheckmarkSetTrue.bind(this);
this.disposable.push(
this.value.subscribe((value) => {
this.triggerAnimation(value);
}, this)
);
}
animationBoxSetTrue() {
this.animationBox(true);
}
animationCheckmarkSetTrue() {
this.animationCheckmark(true);
}
triggerAnimation(box) {
if (box) {
this.animationBoxSetTrue();
setTimeout(this.animationCheckmarkSetTrue, 200);
} else {
this.animationCheckmarkSetTrue();
setTimeout(this.animationBoxSetTrue, 200);
}
}
}
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}

View file

@ -13,11 +13,7 @@ export class SelectComponent extends AbstractInput {
this.optionsText = params.optionsText || null;
this.optionsValue = params.optionsValue || null;
this.optionsCaption = params.optionsCaption || null;
if (this.optionsCaption) {
this.optionsCaption = i18n(this.optionsCaption);
}
this.optionsCaption = i18n(params.optionsCaption || null);
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
}

View file

@ -1,10 +0,0 @@
import 'External/ko';
import ko from 'ko';
import { SaveSettingsStep } from 'Common/Enums';
// functions
ko.observable.fn.idleTrigger = function() {
this.trigger = ko.observable(SaveSettingsStep.Idle);
return this;
};

View file

@ -9,13 +9,16 @@ const
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey().replace('meta','⌘') + ' + ',
ctrlKey = shortcuts.getMetaKey() + ' + ',
tpl = doc.createElement('template'),
clr = doc.createElement('input'),
createElement = name => doc.createElement(name),
tpl = createElement('template'),
clr = createElement('input'),
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
clearHtmlLine = html => rl.Utils.htmlToPlain(html).trim(),
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
plainToHtml = text => rl.Utils.plainToHtml(text),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
@ -33,13 +36,12 @@ const
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
tpl.innerHTML = html
tpl.innerHTML = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(/<!--[^>]+-->/g,'')
.replace(/<span[^>]*>\s*<\/span>/gi,'')
.trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
if (isPaste) {
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
tpl.querySelectorAll('*').forEach(el => {
@ -57,66 +59,6 @@ const
}
return tpl.content;
}
},
rl_signature_replacer = (editor, text, signature, isHtml, insertBefore) => {
let
prevSignature = editor.__previous_signature,
skipInsert = false,
isEmptyText = false;
isEmptyText = !text.trim();
if (!isEmptyText && isHtml) {
isEmptyText = !clearHtmlLine(text);
}
if (prevSignature && !isEmptyText) {
if (isHtml && !prevSignature.isHtml) {
prevSignature = {
body: rl.Utils.plainToHtml(prevSignature.body),
isHtml: true
};
} else if (!isHtml && prevSignature.isHtml) {
prevSignature = {
body: rl.Utils.htmlToPlain(prevSignature.body),
isHtml: true
};
}
if (isHtml) {
var clearSig = clearHtmlLine(prevSignature.body);
text = text.replace(/<signature>([\s\S]*)<\/signature>/igm, all => {
var c = clearSig === clearHtmlLine(all);
if (!c) {
skipInsert = true;
}
return c ? '' : all;
});
} else {
var textLen = text.length;
text = text
.replace(prevSignature.body, '')
.replace(prevSignature.body, '');
skipInsert = textLen === text.length;
}
}
if (!skipInsert) {
signature = (isHtml ? '<br/><br/><signature>' : "\n\n") + signature + (isHtml ? '</signature>' : '');
text = insertBefore ? signature + text : text + signature;
if (10 < signature.length) {
prevSignature = {
body: signature,
isHtml: isHtml
};
}
}
editor.__previous_signature = prevSignature;
return text;
};
clr.type = "color";
@ -127,9 +69,9 @@ class SquireUI
{
constructor(container) {
const
doClr = fn => () => {
doClr = name => () => {
clr.value = '';
clr.onchange = () => squire[fn](clr.value);
clr.onchange = () => squire.setStyle({[name]:clr.value});
clr.click();
},
@ -171,29 +113,15 @@ class SquireUI
colors: {
textColor: {
html: 'A<sub>▾</sub>',
cmd: doClr('setTextColor'),
cmd: doClr('color'),
hint: 'Text color'
},
backgroundColor: {
html: '🎨', /* ▧ */
cmd: doClr('setBackgroundColor'),
cmd: doClr('backgroundColor'),
hint: 'Background color'
},
},
/*
bidi: {
bdoLtr: {
html: '&lrm;𝐁',
cmd: () => this.doAction('bold','B'),
hint: 'Bold'
},
bdoRtl: {
html: '&rlm;𝐁',
cmd: () => this.doAction('bold','B'),
hint: 'Bold'
}
},
*/
inline: {
bold: {
html: 'B',
@ -318,10 +246,10 @@ class SquireUI
}
},
plain = doc.createElement('textarea'),
wysiwyg = doc.createElement('div'),
toolbar = doc.createElement('div'),
browseImage = doc.createElement('input'),
plain = createElement('textarea'),
wysiwyg = createElement('div'),
toolbar = createElement('div'),
browseImage = createElement('input'),
squire = new Squire(wysiwyg, SquireDefaultConfig);
browseImage.type = 'file';
@ -336,7 +264,7 @@ class SquireUI
}
plain.className = 'squire-plain';
wysiwyg.className = 'squire-wysiwyg cke_editable';
wysiwyg.className = 'squire-wysiwyg';
this.mode = ''; // 'plain' | 'wysiwyg'
this.__plain = {
getRawData: () => this.plain.value,
@ -349,25 +277,24 @@ class SquireUI
this.wysiwyg = wysiwyg;
toolbar.className = 'squire-toolbar btn-toolbar';
let touchTap;
for (let group in actions) {
let group, action, touchTap;
for (group in actions) {
/*
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
continue;
}
let toolgroup = doc.createElement('div');
*/
let toolgroup = createElement('div');
toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group;
for (let action in actions[group]) {
if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) {
continue;
}
for (action in actions[group]) {
let cfg = actions[group][action], input, ev = 'click';
if (cfg.input) {
input = doc.createElement('input');
input = createElement('input');
input.type = cfg.input;
ev = 'change';
} else if (cfg.select) {
input = doc.createElement('select');
input = createElement('select');
input.className = 'btn';
if (Array.isArray(cfg.select)) {
cfg.select.forEach(value => {
@ -377,7 +304,7 @@ class SquireUI
});
} else {
Object.entries(cfg.select).forEach(([label, options]) => {
let group = doc.createElement('optgroup');
let group = createElement('optgroup');
group.label = label;
Object.entries(options).forEach(([text, value]) => {
var option = new Option(text, value);
@ -389,7 +316,7 @@ class SquireUI
}
ev = 'input';
} else {
input = doc.createElement('button');
input = createElement('button');
input.type = 'button';
input.className = 'btn';
input.innerHTML = cfg.html;
@ -412,6 +339,7 @@ class SquireUI
input.title = ctrlKey + cfg.key;
}
input.dataset.action = action;
input.tabIndex = -1;
cfg.input = input;
toolgroup.append(input);
}
@ -425,8 +353,8 @@ class SquireUI
changes.redo.input.disabled = !state.canRedo;
});
squire.addEventListener('focus', () => shortcuts.off());
squire.addEventListener('blur', () => shortcuts.on());
// squire.addEventListener('focus', () => shortcuts.off());
// squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain);
@ -477,9 +405,9 @@ class SquireUI
let cl = this.container.classList;
cl.remove('squire-mode-'+this.mode);
if ('plain' == mode) {
this.plain.value = rl.Utils.htmlToPlain(this.squire.getHTML(), true).trim();
this.plain.value = htmlToPlain(this.squire.getHTML(), true);
} else {
this.setData(rl.Utils.plainToHtml(this.plain.value, true));
this.setData(plainToHtml(this.plain.value, true));
mode = 'wysiwyg';
}
this.mode = mode; // 'wysiwyg' or 'plain'
@ -509,19 +437,32 @@ class SquireUI
}, cfg);
if (cfg.clearCache) {
this.__previous_signature = null;
this._prev_txt_sig = null;
} else try {
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
if ('plain' === this.mode) {
if (cfg.isHtml) {
cfg.signature = rl.Utils.htmlToPlain(cfg.signature);
let
text = this.plain.value,
prevSignature = this._prev_txt_sig;
if (prevSignature) {
text = text.replace(prevSignature, '').trim();
}
this.plain.value = rl_signature_replacer(this, this.plain.value, cfg.signature, false, cfg.insertBefore);
this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
} else {
if (!cfg.isHtml) {
cfg.signature = rl.Utils.plainToHtml(cfg.signature);
}
this.setData(rl_signature_replacer(this, this.getData(), cfg.signature, true, cfg.insertBefore));
const squire = this.squire,
root = squire.getRoot(),
range = squire.getSelection(),
div = createElement('div');
div.className = 'rl-signature';
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
cfg.insertBefore ? root.prepend(div) : root.append(div);
// Move cursor above signature
range.setStart(div, 0);
range.setEnd(div, 0);
squire.setSelection( range );
}
this._prev_txt_sig = signature;
} catch (e) {
console.error(e);
}
@ -540,12 +481,6 @@ class SquireUI
focus() {
('plain' == this.mode ? this.plain : this.squire).focus();
}
resize(width, height) {
height = Math.max(200, (height - this.wysiwyg.offsetTop)) + 'px';
this.wysiwyg.style.height = height;
this.plain.style.height = height;
}
}
this.SquireUI = SquireUI;

View file

@ -1,11 +1,14 @@
import 'External/ko';
import ko from 'ko';
import { HtmlEditor } from 'Common/Html';
import { timeToNode } from 'Common/Momentor';
import { elementById } from 'Common/Globals';
import { timeToNode } from 'Common/Translator';
import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme';
import { moveMessagesToFolder } from 'Common/Folders';
import { setExpandedFolder } from 'Model/FolderCollection';
const rlContentType = 'snappymail/action',
@ -27,233 +30,223 @@ const rlContentType = 'snappymail/action',
id: 0,
stop: () => clearTimeout(dragTimer.id),
start: fn => dragTimer.id = setTimeout(fn, 500)
};
},
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
let dragImage,
dragData;
ko.bindingHandlers.editor = {
init: (element, fValueAccessor) => {
let editor = null;
Object.assign(ko.bindingHandlers, {
const fValue = fValueAccessor(),
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
fOnReady = () => {
fValue.__editor = editor;
fUpdateEditorValue();
};
editor: {
init: (element, fValueAccessor) => {
let editor = null;
if (ko.isObservable(fValue) && HtmlEditor) {
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
const fValue = fValueAccessor(),
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
fOnReady = () => {
fValue.__editor = editor;
fUpdateEditorValue();
};
fValue.__fetchEditorValue = fUpdateKoValue;
if (ko.isObservable(fValue) && HtmlEditor) {
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
fValue.subscribe(fUpdateEditorValue);
fValue.__fetchEditorValue = fUpdateKoValue;
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
// });
}
}
};
fValue.subscribe(fUpdateEditorValue);
let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
ko.bindingHandlers.moment = {
init: ttn,
update: ttn
};
ko.bindingHandlers.emailsTags = {
init: (element, fValueAccessor, fAllBindingsAccessor) => {
const fValue = fValueAccessor(),
fAllBindings = fAllBindingsAccessor();
element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => fValue.focused && fValue.focused(!!value),
autoCompleteSource: fAllBindings.autoCompleteSource || null,
onChange: value => fValue(value)
});
if (fValue.focused && fValue.focused.subscribe) {
fValue.focused.subscribe(value =>
element.addresses[value ? 'focus' : 'blur']()
);
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
// });
}
}
},
update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor());
}
};
// Start dragging selected messages
ko.bindingHandlers.dragmessages = {
init: (element, fValueAccessor) => {
element.addEventListener("dragstart", e => {
let data = fValueAccessor()(e);
dragImage || (dragImage = elementById('messagesDragImage'));
if (data && dragImage && !ThemeStore.isMobile()) {
dragImage.querySelector('.text').textContent = data.uids.length;
let img = dragImage.querySelector('i');
img.classList.toggle('icon-copy', e.ctrlKey);
img.classList.toggle('icon-mail', !e.ctrlKey);
moment: {
init: ttn,
update: ttn
},
// Else Chrome doesn't show it
dragImage.style.left = e.clientX + 'px';
dragImage.style.top = e.clientY + 'px';
dragImage.style.right = 'auto';
emailsTags: {
init: (element, fValueAccessor, fAllBindings) => {
const fValue = fValueAccessor();
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => fValue.focused && fValue.focused(!!value),
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
onChange: value => fValue(value)
});
// Remove the Chrome visibility
dragImage.style.cssText = '';
} else {
e.preventDefault();
if (fValue.focused && fValue.focused.subscribe) {
fValue.focused.subscribe(value =>
element.addresses[value ? 'focus' : 'blur']()
);
}
},
update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor());
}
},
}, false);
element.addEventListener("dragend", () => dragData = null);
element.setAttribute('draggable', true);
}
};
// Start dragging selected messages
dragmessages: {
init: (element, fValueAccessor) => {
element.addEventListener("dragstart", e => {
let data = fValueAccessor()(e);
dragImage || (dragImage = elementById('messagesDragImage'));
if (data && dragImage && !ThemeStore.isMobile()) {
dragImage.querySelector('.text').textContent = data.uids.length;
let img = dragImage.querySelector('i');
img.classList.toggle('icon-copy', e.ctrlKey);
img.classList.toggle('icon-mail', !e.ctrlKey);
// Drop selected messages on folder
ko.bindingHandlers.dropmessages = {
init: (element, fValueAccessor) => {
const folder = fValueAccessor(),
// folder = ko.dataFor(element),
fnStop = e => {
e.preventDefault();
element.classList.remove('droppableHover');
dragTimer.stop();
},
fnHover = e => {
if ('messages' === getDragAction(e)) {
fnStop(e);
element.classList.add('droppableHover');
if (folder && folder.collapsed()) {
dragTimer.start(() => {
folder.collapsed(false);
rl.app.setExpandedFolder(folder.fullNameHash, true);
}, 500);
}
}
};
element.addEventListener("dragenter", fnHover);
element.addEventListener("dragover", fnHover);
element.addEventListener("dragleave", fnStop);
element.addEventListener("drop", e => {
fnStop(e);
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
let data = dragData.data;
if (folder && data && data.folder && isArray(data.uids)) {
rl.app.moveMessagesToFolder(data.folder, data.uids, folder.fullNameRaw, data.copy && e.ctrlKey);
}
}
});
}
};
// Else Chrome doesn't show it
dragImage.style.left = e.clientX + 'px';
dragImage.style.top = e.clientY + 'px';
dragImage.style.right = 'auto';
ko.bindingHandlers.sortableItem = {
init: (element, fValueAccessor) => {
let options = ko.unwrap(fValueAccessor()) || {},
parent = element.parentNode,
fnHover = e => {
if ('sortable' === getDragAction(e)) {
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
// Remove the Chrome visibility
dragImage.style.cssText = '';
} else {
e.preventDefault();
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
if (node && node !== dragData.data && parent.contains(node)) {
let rect = node.getBoundingClientRect();
if (rect.top + (rect.height / 2) <= e.clientY) {
if (node.nextElementSibling !== dragData.data) {
node.after(dragData.data);
}
} else if (node.previousElementSibling !== dragData.data) {
node.before(dragData.data);
}
}, false);
element.addEventListener("dragend", () => dragData = null);
element.setAttribute('draggable', true);
}
},
// Drop selected messages on folder
dropmessages: {
init: (element, fValueAccessor) => {
const folder = fValueAccessor(),
// folder = ko.dataFor(element),
fnStop = e => {
e.preventDefault();
element.classList.remove('droppableHover');
dragTimer.stop();
},
fnHover = e => {
if ('messages' === getDragAction(e)) {
fnStop(e);
element.classList.add('droppableHover');
if (folder && folder.collapsed()) {
dragTimer.start(() => {
folder.collapsed(false);
setExpandedFolder(folder.fullName, true);
}, 500);
}
}
};
addEventsListeners(element, {
dragenter: fnHover,
dragover: fnHover,
dragleave: fnStop,
drop: e => {
fnStop(e);
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
let data = dragData.data;
if (folder && data && data.folder && isArray(data.uids)) {
moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
}
}
}
};
element.addEventListener("dragstart", e => {
dragData = {
action: 'sortable',
element: element
};
setDragAction(e, 'sortable', 'move', element, element);
element.style.opacity = 0.25;
});
element.addEventListener("dragend", e => {
element.style.opacity = null;
if ('sortable' === getDragAction(e)) {
dragData.data.style.cssText = '';
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
if (row != dragData.data) {
row.before(dragData.data);
}
dragData = null;
}
});
if (!parent.sortable) {
parent.sortable = true;
parent.addEventListener("dragenter", fnHover);
parent.addEventListener("dragover", fnHover);
parent.addEventListener("drop", e => {
if ('sortable' === getDragAction(e)) {
e.preventDefault();
let data = ko.dataFor(dragData.data),
from = options.list.indexOf(data),
to = [...parent.children].indexOf(dragData.data);
if (from != to) {
let arr = options.list();
arr.splice(to, 0, ...arr.splice(from, 1));
options.list(arr);
}
dragData = null;
options.afterMove && options.afterMove();
}
});
}
}
};
},
ko.bindingHandlers.initDom = {
init: (element, fValueAccessor) => fValueAccessor()(element)
};
ko.bindingHandlers.onEsc = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if ('Escape' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
sortableItem: {
init: (element, fValueAccessor) => {
let options = ko.unwrap(fValueAccessor()) || {},
parent = element.parentNode,
fnHover = e => {
if ('sortable' === getDragAction(e)) {
e.preventDefault();
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
if (node && node !== dragData.data && parent.contains(node)) {
let rect = node.getBoundingClientRect();
if (rect.top + (rect.height / 2) <= e.clientY) {
if (node.nextElementSibling !== dragData.data) {
node.after(dragData.data);
}
} else if (node.previousElementSibling !== dragData.data) {
node.before(dragData.data);
}
}
}
};
addEventsListeners(element, {
dragstart: e => {
dragData = {
action: 'sortable',
element: element
};
setDragAction(e, 'sortable', 'move', element, element);
element.style.opacity = 0.25;
},
dragend: e => {
element.style.opacity = null;
if ('sortable' === getDragAction(e)) {
dragData.data.style.cssText = '';
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
if (row != dragData.data) {
row.before(dragData.data);
}
dragData = null;
}
}
});
if (!parent.sortable) {
parent.sortable = true;
addEventsListeners(parent, {
dragenter: fnHover,
dragover: fnHover,
drop: e => {
if ('sortable' === getDragAction(e)) {
e.preventDefault();
let data = ko.dataFor(dragData.data),
from = options.list.indexOf(data),
to = [...parent.children].indexOf(dragData.data);
if (from != to) {
let arr = options.list();
arr.splice(to, 0, ...arr.splice(from, 1));
options.list(arr);
}
dragData = null;
options.afterMove && options.afterMove();
}
}
});
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
};
}
},
ko.bindingHandlers.registerBootstrapDropdown = {
init: element => {
rl.Dropdowns.register(element);
element.ddBtn = new BSN.Dropdown(element.querySelector('[data-toggle="dropdown"]'));
}
};
initDom: {
init: (element, fValueAccessor) => fValueAccessor()(element)
},
ko.bindingHandlers.openDropdownTrigger = {
update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn;
el.open || el.toggle();
// el.focus();
registerBootstrapDropdown: {
init: element => {
dropdowns.push(element);
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
}
},
rl.Dropdowns.detectVisibility();
fValueAccessor()(false);
openDropdownTrigger: {
update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn;
el.open || el.toggle();
// el.focus();
dropdownsDetectVisibility();
fValueAccessor()(false);
}
}
}
};
ko.bindingHandlers.dropdownCloser = {
init: element => element.closest('.dropdown').addEventListener('click', event =>
event.target.closestWithin('.e-item', element) && element.ddBtn.toggle()
)
};
});

View file

@ -1,37 +1,36 @@
(doc => {
let visible = "visible",
let idle = 'idle',
visible = 'visible',
status = visible,
timer = 0,
wakeUp = () => {
clearTimeout(timer);
if (status !== visible) {
status = visible;
}
status = visible;
timer = setTimeout(() => {
if (status === visible) {
status = "idle";
dispatchEvent(new CustomEvent("idle"));
status = idle;
dispatchEvent(new CustomEvent(idle));
}
}, 10000);
},
init = () => {
init = ()=>{};
init = () => 0;
// Safari
addEventListener('pagehide', () => status = "hidden");
addEventListener('pagehide', () => status = 'hidden');
// Else
doc.addEventListener("visibilitychange", () => {
doc.addEventListener('visibilitychange', () => {
status = doc.visibilityState;
doc.hidden || wakeUp();
});
wakeUp();
["mousemove","keyup","touchstart"].forEach(t => doc.addEventListener(t, wakeUp));
["scroll","pageshow"].forEach(t => addEventListener(t, wakeUp));
['mousemove','keyup','touchstart'].forEach(t => doc.addEventListener(t, wakeUp));
['scroll','pageshow'].forEach(t => addEventListener(t, wakeUp));
};
this.ifvisible = {
idle: callback => {
init();
addEventListener("idle", callback);
addEventListener(idle, callback);
},
now: () => {
init();

280
dev/External/ko.js vendored
View file

@ -1,157 +1,171 @@
import ko from 'ko';
import { i18nToNodes } from 'Common/Translator';
import { doc, createElement } from 'Common/Globals';
import { SaveSettingsStep } from 'Common/Enums';
import { isNonEmptyArray, isFunction } from 'Common/Utils';
import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils';
const
koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value);
export const
errorTip = (element, value) => value
? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
: element.removeAttribute('data-rainloopErrorTip'),
ko.bindingHandlers.tooltipErrorTip = {
init: element => {
doc.addEventListener('click', () => element.removeAttribute('data-rainloopErrorTip'));
},
update: (element, fValueAccessor) => {
const value = koValue(fValueAccessor());
if (value) {
setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100);
} else {
element.removeAttribute('data-rainloopErrorTip');
/**
* The value of the pureComputed observable shouldnt vary based on the
* number of evaluations or other hidden information. Its value should be
* based solely on the values of other observables in the application
*/
koComputable = fn => ko.computed(fn, {'pure':true}),
addObservablesTo = (target, observables) =>
forEachObjectEntry(observables, (key, value) =>
target[key] || (target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value)) ),
addComputablesTo = (target, computables) =>
forEachObjectEntry(computables, (key, fn) => target[key] = koComputable(fn)),
addSubscribablesTo = (target, subscribables) =>
forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
dispose = disposable => disposable && isFunction(disposable.dispose) && disposable.dispose(),
// With this we don't need delegateRunOnDestroy
koArrayWithDestroy = data => {
data = ko.observableArray(data);
data.subscribe(changes =>
changes.forEach(item =>
'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
)
, data, 'arrayChange');
return data;
};
Object.assign(ko.bindingHandlers, {
tooltipErrorTip: {
init: (element, fValueAccessor) => {
doc.addEventListener('click', () => {
let value = fValueAccessor();
ko.isObservable(value) && !ko.isComputed(value) && value('');
errorTip(element);
});
},
update: (element, fValueAccessor) => {
let value = ko.unwrap(fValueAccessor());
value = isFunction(value) ? value() : value;
errorTip(element, value);
}
}
};
},
ko.bindingHandlers.onEnter = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if ('Enter' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
onEnter: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if ('Enter' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
}
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
}
},
onSpace: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
},
i18nInit: {
init: element => i18nToNodes(element)
},
i18nUpdate: {
update: (element, fValueAccessor) => {
ko.unwrap(fValueAccessor());
i18nToNodes(element);
}
},
title: {
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
},
command: {
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
const command = fValueAccessor();
if (!command || !command.canExecute) {
throw new Error('Value should be a command');
}
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
}
};
ko.bindingHandlers.onSpace = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
element,
fValueAccessor,
fAllBindings,
viewModel,
bindingContext
);
},
update: (element, fValueAccessor) => {
const cl = element.classList,
command = fValueAccessor();
let disabled = !command.canExecute();
cl.toggle('disabled', disabled);
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
element.disabled = disabled;
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
};
ko.bindingHandlers.modal = {
init: (element, fValueAccessor) => {
const close = element.querySelector('.close'),
click = () => fValueAccessor()(false);
close && close.addEventListener('click.koModal', click);
ko.utils.domNodeDisposal.addDisposeCallback(element, () =>
close.removeEventListener('click.koModal', click)
);
}
};
ko.bindingHandlers.i18nInit = {
init: element => i18nToNodes(element)
};
ko.bindingHandlers.i18nUpdate = {
update: (element, fValueAccessor) => {
ko.unwrap(fValueAccessor());
i18nToNodes(element);
}
};
ko.bindingHandlers.title = {
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
};
ko.bindingHandlers.command = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel, bindingContext) => {
const command = fValueAccessor();
if (!command || !command.enabled || !command.canExecute) {
throw new Error('Value should be a command');
}
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
element,
fValueAccessor,
fAllBindingsAccessor,
viewModel,
bindingContext
);
},
update: (element, fValueAccessor) => {
const cl = element.classList,
command = fValueAccessor();
let disabled = !command.enabled();
disabled = disabled || !command.canExecute();
cl.toggle('disabled', disabled);
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
element.disabled = disabled;
}
}
};
ko.bindingHandlers.saveTrigger = {
init: (element) => {
let icon = element;
if (element.matches('input,select,textarea')) {
element.classList.add('settings-saved-trigger-input');
element.after(element.saveTriggerIcon = icon = createElement('span'));
}
icon.classList.add('settings-save-trigger');
},
update: (element, fValueAccessor) => {
const value = parseInt(ko.unwrap(fValueAccessor()),10);
let cl = (element.saveTriggerIcon || element).classList;
if (element.saveTriggerIcon) {
cl.toggle('saving', value === SaveSettingsStep.Animate);
saveTrigger: {
init: (element) => {
let icon = element;
if (element.matches('input,select,textarea')) {
element.classList.add('settings-saved-trigger-input');
element.after(element.saveTriggerIcon = icon = createElement('span'));
}
icon.classList.add('settings-save-trigger');
},
update: (element, fValueAccessor) => {
const value = parseInt(ko.unwrap(fValueAccessor()),10);
let cl = (element.saveTriggerIcon || element).classList;
if (element.saveTriggerIcon) {
cl.toggle('saving', value === SaveSettingsStep.Animate);
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
cl = element.classList;
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
cl = element.classList;
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
};
});
// extenders
ko.extenders.limitedList = (target, limitedList) => {
const result = ko
.computed({
read: target,
write: (newValue) => {
const currentValue = ko.unwrap(target),
list = ko.unwrap(limitedList);
if (isNonEmptyArray(list)) {
if (list.includes(newValue)) {
target(newValue);
} else if (list.includes(currentValue, list)) {
target(currentValue + ' ');
target(currentValue);
} else {
target(list[0] + ' ');
target(list[0]);
}
} else {
target('');
}
.computed({
read: target,
write: newValue => {
let currentValue = target(),
list = ko.unwrap(limitedList);
list = arrayLength(list) ? list : [''];
if (!list.includes(newValue)) {
newValue = list.includes(currentValue, list) ? currentValue : list[0];
target(newValue + ' ');
}
})
.extend({ notify: 'always' });
target(newValue);
}
})
.extend({ notify: 'always' });
result(target());
@ -184,6 +198,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions
ko.observable.fn.deleteAccessHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'deleteAccess'] });
ko.observable.fn.askDeleteHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
};

View file

@ -1,31 +1,24 @@
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="user-scalable=no"/>
<title>{{subject}}</title>
<title></title>
<style>
html, body {
background-color: #fff;
color: #000;
font-size: 13px;
margin: 0;
padding: 0;
}
header {
background: #eee;
border-bottom: 1px solid #ccc;
padding: 0 15px;
background: rgba(125,128,128,0.3);
border-bottom: 1px solid #888;
}
header h1 {
font-size: 16px;
margin: 0;
font-size: 120%;
}
header * {
font-size: 12px;
padding: 5px 0;
margin: 5px 0;
}
header time {
@ -33,30 +26,21 @@ header time {
}
blockquote {
border-left: 2px solid black;
border-left: 2px solid rgba(125,128,128,0.5);
margin: 0;
padding: 0 10px;
padding: 0 0 0 10px;
}
pre, .body-wrp.plain {
pre {
white-space: pre-wrap;
word-wrap: break-word;
word-break: normal;
}
.body-wrp {
padding: 15px;
body > * {
padding: 0.5em 1em;
}
</style>
</head>
<body>
<header>
<h1>{{subject}}</h1>
<time>{{date}}</time>
<div>{{fromCreds}}</div>
<div>{{toLabel}}: {{toCreds}}</div>
<div {{ccHide}}>{{ccLabel}}: {{ccCreds}}</div>
</header>
<div class="body-wrp {{bodyClass}}">{{html}}</div>
</body>
<body></body>
</html>

View file

@ -1,23 +1,20 @@
import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils';
function dispose(disposable) {
if (disposable && isFunction(disposable.dispose)) {
disposable.dispose();
}
}
import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
function typeCast(curValue, newValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
if (null != curValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
}
if (isArray(curValue) && !isArray(newValue))
return [];
}
if (isArray(curValue) && !isArray(newValue))
return [];
}
return newValue;
}
@ -29,7 +26,7 @@ export class AbstractModel {
throw new Error("Can't instantiate AbstractModel!");
}
*/
this.subscribables = [];
this.disposables = [];
}
addObservables(observables) {
@ -41,25 +38,26 @@ export class AbstractModel {
}
addSubscribables(subscribables) {
Object.entries(subscribables).forEach(([key, fn]) => this.subscribables.push( this[key].subscribe(fn) ) );
// addSubscribablesTo(this, subscribables);
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
}
/** Called by delegateRunOnDestroy */
onDestroy() {
/** dispose ko subscribables */
this.subscribables.forEach(dispose);
this.disposables.forEach(dispose);
/** clear object entries */
// Object.entries(this).forEach(([key, value]) => {
Object.values(this).forEach(value => {
// forEachObjectEntry(this, (key, value) => {
forEachObjectValue(this, value => {
/** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && value.onDestroy();
arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */
dispose(value);
// dispose(value);
/** clear object value */
// this[key] = null; // TODO: issue with Contacts view
});
// this.subscribables = [];
// this.disposables = [];
}
/**
@ -84,39 +82,37 @@ export class AbstractModel {
revivePropertiesFromJson(json) {
let model = this.constructor;
try {
if (!model.validJson(json)) {
return false;
}
Object.entries(json).forEach(([key, value]) => {
if ('@' !== key[0]) {
key = key[0].toLowerCase() + key.substr(1);
switch (typeof this[key])
{
case 'function':
if (ko.isObservable(this[key])) {
this[key](typeCast(this[key](), value));
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
}
// else console.log(model.name + '.' + key + ' is a function');
break;
case 'boolean':
case 'number':
case 'object':
case 'string':
this[key] = typeCast(this[key], value);
break;
// fall through
case 'undefined':
default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
}
});
} catch (e) {
console.log(model.name);
console.error(e);
if (!model.validJson(json)) {
return false;
}
forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key])
{
case 'function':
if (ko.isObservable(this[key])) {
this[key](typeCast(this[key](), value));
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
}
// else console.log(model.name + '.' + key + ' is a function');
break;
case 'boolean':
case 'number':
case 'object':
case 'string':
this[key] = typeCast(this[key], value);
break;
// fall through
case 'undefined':
default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
} catch (e) {
console.log(model.name + '.' + key);
console.error(e);
}
});
return true;
}

View file

@ -1,24 +1,10 @@
import { isArray, isNonEmptyArray } from 'Common/Utils';
import { isArray, arrayLength } from 'Common/Utils';
export class AbstractScreen {
constructor(screenName, viewModels = []) {
this.oCross = null;
this.sScreenName = screenName;
this.aViewModels = isArray(viewModels) ? viewModels : [];
}
/**
* @returns {Array}
*/
get viewModels() {
return this.aViewModels;
}
/**
* @returns {string}
*/
screenName() {
return this.sScreenName;
this.__cross = null;
this.screenName = screenName;
this.viewModels = isArray(viewModels) ? viewModels : [];
}
/**
@ -28,28 +14,26 @@ export class AbstractScreen {
return null;
}
/**
* @returns {?Object}
*/
get __cross() {
return this.oCross;
}
/*
onBuild(viewModelDom) {}
onShow() {}
onHide() {}
__started
__builded
*/
/**
* @returns {void}
*/
onStart() {
if (!this.__started) {
this.__started = true;
const routes = this.routes();
if (isNonEmptyArray(routes)) {
let route = new Crossroads(),
fMatcher = (this.onRoute || (()=>{})).bind(this);
const routes = this.routes();
if (arrayLength(routes)) {
let route = new Crossroads(),
fMatcher = (this.onRoute || (()=>0)).bind(this);
routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
this.oCross = route;
}
this.__cross = route;
}
}
}

View file

@ -1,45 +1,38 @@
import ko from 'ko';
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
import { Scope } from 'Common/Enums';
import { keyScope } from 'Common/Globals';
import { ViewType } from 'Knoin/Knoin';
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
import { SaveSettingsStep } from 'Common/Enums';
class AbstractView {
constructor(name, templateID, type)
constructor(templateID, type)
{
this.viewModelName = 'View/' + name;
this.viewModelTemplateID = templateID;
this.viewModelPosition = type;
this.bDisabeCloseOnEsc = false;
this.sDefaultScope = Scope.None;
this.sCurrentScope = Scope.None;
this.viewModelVisible = false;
this.modalVisibility = ko.observable(false).extend({ rateLimit: 0 });
this.viewModelName = '';
// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
this.viewType = type;
this.viewModelDom = null;
this.keyScope = {
scope: 'none',
previous: 'none',
set: function() {
this.previous = keyScope();
keyScope(this.scope);
},
unset: function() {
keyScope(this.previous);
}
};
}
/**
* @returns {void}
*/
storeAndSetScope() {
this.sCurrentScope = keyScope();
keyScope(this.sDefaultScope);
}
/**
* @returns {void}
*/
restoreScope() {
keyScope(this.sCurrentScope);
}
cancelCommand() {}
closeCommand() {}
/*
onBuild() {}
beforeShow() {} // Happens before: hidden = false
onShow() {} // Happens after: hidden = false
onHide() {}
*/
querySelector(selectors) {
return this.viewModelDom.querySelector(selectors);
@ -63,51 +56,115 @@ export class AbstractViewPopup extends AbstractView
{
constructor(name)
{
super('Popup/' + name, 'Popups' + name, ViewType.Popup);
if (name in Scope) {
this.sDefaultScope = Scope[name];
}
}
/**
* @returns {void}
*/
registerPopupKeyDown() {
addEventListener('keydown', event => {
if (event && this.modalVisibility && this.modalVisibility()) {
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
this.cancelCommand && this.cancelCommand();
return false;
} else if ('Backspace' == event.key && !inFocus()) {
return false;
}
super('Popups' + name, ViewTypePopup);
this.keyScope.scope = name;
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
shortcuts.add('escape,close', '', name, () => {
if (this.modalVisible() && false !== this.onClose()) {
this.close();
return false;
}
return true;
});
}
// Happens when user hits Escape or Close key
// return false to prevent closing
onClose() {}
/*
beforeShow() {} // Happens before showModal()
onShow() {} // Happens after showModal()
afterShow() {} // Happens after showModal() animation transitionend
onHide() {} // Happens before animation transitionend
afterHide() {} // Happens after animation transitionend
close() {}
*/
}
export class AbstractViewCenter extends AbstractView
{
constructor(name, templateID)
{
super(name, templateID, ViewType.Center);
}
AbstractViewPopup.showModal = function(params = []) {
showScreenPopup(this, params);
}
AbstractViewPopup.hidden = function() {
return !this.__vm || !this.__vm.modalVisible();
}
export class AbstractViewLeft extends AbstractView
{
constructor(name, templateID)
constructor(templateID)
{
super(name, templateID, ViewType.Left);
super(templateID, 'Left');
this.leftPanelDisabled = leftPanelDisabled;
}
}
export class AbstractViewRight extends AbstractView
{
constructor(name, templateID)
constructor(templateID)
{
super(name, templateID, ViewType.Right);
super(templateID, 'Right');
}
}
export class AbstractViewSettings
{
/*
onBuild(viewModelDom) {}
beforeShow() {}
onShow() {}
onHide() {}
viewModelDom
*/
addSetting(name, valueCb)
{
let prop = name[0].toLowerCase() + name.slice(1),
trigger = prop + 'Trigger';
addObservablesTo(this, {
[prop]: SettingsGet(name),
[trigger]: SaveSettingsStep.Idle,
});
addSubscribablesTo(this, {
[prop]: (value => {
this[trigger](SaveSettingsStep.Animate);
valueCb && valueCb(value);
rl.app.Remote.saveSetting(name, value,
iError => {
this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => this[trigger](SaveSettingsStep.Idle), 1000);
}
);
}).debounce(999),
});
}
addSettings(names)
{
names.forEach(name => {
let prop = name[0].toLowerCase() + name.slice(1);
this[prop] || (this[prop] = ko.observable(SettingsGet(name)));
this[prop].subscribe(value => rl.app.Remote.saveSetting(name, value));
});
}
}
export class AbstractViewLogin extends AbstractView {
constructor(templateID) {
super(templateID, 'Content');
this.hideSubmitButton = SettingsGet('hideSubmitButton');
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
}
onBuild(dom) {
dom.classList.add('LoginView');
}
onShow() {
rl.route.off();
}
submitForm() {
// return false;
}
}

View file

@ -1,370 +1,310 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
import { doc, $htmlCL } from 'Common/Globals';
import { runHook } from 'Common/Plugins';
import { isNonEmptyArray, isFunction } from 'Common/Utils';
let currentScreen = null,
let
SCREENS = {},
currentScreen = null,
defaultScreenName = '';
const SCREENS = {},
const
autofocus = dom => {
const af = dom.querySelector('[autofocus]');
af && af.focus();
};
},
export const popupVisibilityNames = ko.observableArray([]);
visiblePopups = new Set,
export const ViewType = {
Popup: 'Popups',
Left: 'Left',
Right: 'Right',
Center: 'Center'
};
/**
* @param {string} screenName
* @returns {?Object}
*/
screen = screenName => screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null,
/**
* @param {Function} fExecute
* @param {(Function|boolean|null)=} fCanExecute = true
* @returns {Function}
*/
export function createCommand(fExecute, fCanExecute = true) {
let fResult = null;
/**
* @param {Function} ViewModelClass
* @param {Object=} vmScreen
* @returns {*}
*/
buildViewModel = (ViewModelClass, vmScreen) => {
if (ViewModelClass && !ViewModelClass.__builded) {
let vmDom = null;
const
vm = new ViewModelClass(vmScreen),
id = vm.viewModelTemplateID,
position = vm.viewType || '',
dialog = ViewTypePopup === position,
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
fResult = fExecute
? (...args) => {
if (fResult && fResult.canExecute && fResult.canExecute()) {
fExecute.apply(null, args);
}
return false;
} : ()=>{};
fResult.enabled = ko.observable(true);
fResult.isCommand = true;
ViewModelClass.__builded = true;
ViewModelClass.__vm = vm;
if (isFunction(fCanExecute)) {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null));
} else {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute);
}
if (vmPlace) {
vmDom = Element.fromHTML(dialog
? '<dialog id="V-'+ id + '"></dialog>'
: '<div id="V-'+ id + '" hidden=""></div>');
vmPlace.append(vmDom);
return fResult;
}
vm.viewModelDom = vmDom;
ViewModelClass.__dom = vmDom;
/**
* @param {string} screenName
* @returns {?Object}
*/
function screen(screenName) {
return screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null;
}
if (ViewTypePopup === position) {
vm.close = () => hideScreenPopup(ViewModelClass);
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
export function hideScreenPopup(ViewModelClassToHide) {
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
ViewModelClassToHide.__vm.modalVisibility(false);
}
}
// Firefox / Safari HTMLDialogElement not defined
if (!vmDom.showModal) {
vmDom.classList.add('polyfill');
vmDom.showModal = () => {
vmDom.backdrop ||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
vmDom.setAttribute('open','');
vmDom.open = true;
vmDom.returnValue = null;
vmDom.backdrop.hidden = false;
};
vmDom.close = v => {
vmDom.backdrop.hidden = true;
vmDom.returnValue = v;
vmDom.removeAttribute('open', null);
vmDom.open = false;
};
}
/**
* @param {Function} ViewModelClass
* @param {Object=} vmScreen
* @returns {*}
*/
function buildViewModel(ViewModelClass, vmScreen) {
if (ViewModelClass && !ViewModelClass.__builded) {
let vmDom = null;
const vm = new ViewModelClass(vmScreen),
position = vm.viewModelPosition || '',
vmPlace = position ? doc.querySelector('#rl-content #rl-' + position.toLowerCase()) : null;
ViewModelClass.__builded = true;
ViewModelClass.__vm = vm;
if (vmPlace) {
vmDom = Element.fromHTML('<div class="rl-view-model RL-' + vm.viewModelTemplateID + '" hidden=""></div>');
vmPlace.append(vmDom);
vm.viewModelDom = vmDom;
ViewModelClass.__dom = vmDom;
if (ViewType.Popup === position) {
vm.cancelCommand = vm.closeCommand = createCommand(() => {
hideScreenPopup(ViewModelClass);
});
// show/hide popup/modal
const endShowHide = e => {
if (e.target === vmDom) {
if (vmDom.classList.contains('show')) {
autofocus(vmDom);
vm.onShowWithDelay && vm.onShowWithDelay();
} else {
vmDom.hidden = true;
vm.onHideWithDelay && vm.onHideWithDelay();
// show/hide popup/modal
const endShowHide = e => {
if (e.target === vmDom) {
if (vmDom.classList.contains('animate')) {
autofocus(vmDom);
vm.afterShow && vm.afterShow();
} else {
vmDom.close();
vm.afterHide && vm.afterHide();
}
}
}
};
};
vm.modalVisibility.subscribe(value => {
if (value) {
vmDom.style.zIndex = 3000 + popupVisibilityNames().length + 10;
vmDom.hidden = false;
vm.storeAndSetScope();
popupVisibilityNames.push(vm.viewModelName);
requestAnimationFrame(() => { // wait just before the next paint
vmDom.offsetHeight; // force a reflow
vmDom.classList.add('show'); // trigger the transitions
});
} else {
vm.onHide && vm.onHide();
vmDom.classList.remove('show');
vm.restoreScope();
popupVisibilityNames(popupVisibilityNames.filter(v=>v!==vm.viewModelName));
}
vmDom.setAttribute('aria-hidden', !value);
});
if ('ontransitionend' in vmDom) {
vm.modalVisible.subscribe(value => {
if (value) {
visiblePopups.add(vm);
vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
vmDom.showModal();
if (vmDom.backdrop) {
vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
}
vm.keyScope.set();
requestAnimationFrame(() => { // wait just before the next paint
vmDom.offsetHeight; // force a reflow
vmDom.classList.add('animate'); // trigger the transitions
});
} else {
visiblePopups.delete(vm);
vm.onHide && vm.onHide();
vm.keyScope.unset();
vmDom.classList.remove('animate'); // trigger the transitions
}
arePopupsVisible(0 < visiblePopups.size);
});
vmDom.addEventListener('transitionend', endShowHide);
} else {
// For Edge < 79 and mobile browsers
vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
}
}
ko.applyBindingAccessorsToNode(
vmDom,
{
i18nInit: true,
template: () => ({ name: vm.viewModelTemplateID })
},
vm
);
vm.onBuild && vm.onBuild(vmDom);
if (vm && ViewType.Popup === position) {
vm.registerPopupKeyDown();
}
} else {
console.log('Cannot find view model position: ' + position);
}
}
return ViewModelClass && ViewModelClass.__vm;
}
function getScreenPopupViewModel(ViewModelClassToShow) {
return (buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm;
}
/**
* @param {Function} ViewModelClassToShow
* @param {Array=} params
* @returns {void}
*/
export function showScreenPopup(ViewModelClassToShow, params = []) {
const vm = getScreenPopupViewModel(ViewModelClassToShow);
if (vm) {
params = params || [];
vm.onBeforeShow && vm.onBeforeShow(...params);
vm.modalVisibility(true);
vm.onShow && vm.onShow(...params);
runHook('view-model-on-show', [vm.name, vm.__vm, params]);
}
}
/**
* @param {Function} ViewModelClassToShow
* @returns {void}
*/
export function warmUpScreenPopup(ViewModelClassToShow) {
const vm = getScreenPopupViewModel(ViewModelClassToShow);
vm && vm.onWarmUp && vm.onWarmUp();
}
/**
* @param {Function} ViewModelClassToShow
* @returns {boolean}
*/
export function isPopupVisible(ViewModelClassToShow) {
return ViewModelClassToShow && ViewModelClassToShow.__vm && ViewModelClassToShow.__vm.modalVisibility();
}
/**
* @param {string} screenName
* @param {string} subPart
* @returns {void}
*/
function screenOnRoute(screenName, subPart) {
let vmScreen = null,
isSameScreen = false;
if (null == screenName || '' == screenName) {
screenName = defaultScreenName;
}
if (screenName) {
vmScreen = screen(screenName);
if (!vmScreen) {
vmScreen = screen(defaultScreenName);
if (vmScreen) {
subPart = screenName + '/' + subPart;
screenName = defaultScreenName;
}
}
if (vmScreen && vmScreen.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen;
if (!vmScreen.__builded) {
vmScreen.__builded = true;
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
ko.applyBindingAccessorsToNode(
vmDom,
{
i18nInit: true,
template: () => ({ name: id })
},
vm
);
vmScreen.onBuild && vmScreen.onBuild();
vm.onBuild && vm.onBuild(vmDom);
fireEvent('rl-view-model', vm);
} else {
console.log('Cannot find view model position: ' + position);
}
}
return ViewModelClass && ViewModelClass.__vm;
},
forEachViewModel = (screen, fn) => {
screen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewTypePopup !== ViewModelClass.__vm.viewType
) {
fn(ViewModelClass.__vm, ViewModelClass.__dom);
}
});
},
hideScreen = (screenToHide, destroy) => {
screenToHide.onHide && screenToHide.onHide();
forEachViewModel(screenToHide, (vm, dom) => {
dom.hidden = true;
vm.onHide && vm.onHide();
destroy && vm.viewModelDom.remove();
});
},
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
hideScreenPopup = ViewModelClassToHide => {
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
ViewModelClassToHide.__vm.modalVisible(false);
}
},
/**
* @param {string} screenName
* @param {string} subPart
* @returns {void}
*/
screenOnRoute = (screenName, subPart) => {
let vmScreen = null,
isSameScreen = false;
if (null == screenName || '' == screenName) {
screenName = defaultScreenName;
}
// Close all popups
for (let vm of visiblePopups) {
false === vm.onClose() || vm.close();
}
if (screenName) {
vmScreen = screen(screenName);
if (!vmScreen) {
vmScreen = screen(defaultScreenName);
if (vmScreen) {
subPart = screenName + '/' + subPart;
screenName = defaultScreenName;
}
}
setTimeout(() => {
// hide screen
if (currentScreen && !isSameScreen) {
currentScreen.onHide && currentScreen.onHide();
currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500);
if (vmScreen && vmScreen.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen;
if (isNonEmptyArray(currentScreen.viewModels)) {
currentScreen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
) {
ViewModelClass.__dom.hidden = true;
ViewModelClass.__vm.viewModelVisible = false;
if (!vmScreen.__builded) {
vmScreen.__builded = true;
ViewModelClass.__vm.onHide && ViewModelClass.__vm.onHide();
ViewModelClass.__vm.onHideWithDelay && setTimeout(()=>ViewModelClass.__vm.onHideWithDelay(), 500);
}
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
);
vmScreen.onBuild && vmScreen.onBuild();
}
setTimeout(() => {
// hide screen
if (currentScreen && !isSameScreen) {
hideScreen(currentScreen);
}
// --
currentScreen = vmScreen;
// show screen
if (!isSameScreen) {
vmScreen.onShow && vmScreen.onShow();
forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow && vm.beforeShow();
dom.hidden = false;
vm.onShow && vm.onShow();
autofocus(dom);
});
}
}
// --
// --
currentScreen = vmScreen;
// show screen
if (currentScreen && !isSameScreen) {
currentScreen.onShow && currentScreen.onShow();
if (isNonEmptyArray(currentScreen.viewModels)) {
currentScreen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
) {
ViewModelClass.__vm.onBeforeShow && ViewModelClass.__vm.onBeforeShow();
ViewModelClass.__dom.hidden = false;
ViewModelClass.__vm.viewModelVisible = true;
ViewModelClass.__vm.onShow && ViewModelClass.__vm.onShow();
autofocus(ViewModelClass.__dom);
ViewModelClass.__vm.onShowWithDelay && setTimeout(()=>ViewModelClass.__vm.onShowWithDelay, 200);
runHook('view-model-on-show', [ViewModelClass.name, ViewModelClass.__vm, params]);
}
});
}
}
// --
vmScreen && vmScreen.__cross && vmScreen.__cross.parse(subPart);
}, 1);
vmScreen.__cross && vmScreen.__cross.parse(subPart);
}, 1);
}
}
}
}
};
/**
* @param {Array} screensClasses
* @returns {void}
*/
export function startScreens(screensClasses) {
screensClasses.forEach(CScreen => {
if (CScreen) {
const vmScreen = new CScreen(),
screenName = vmScreen && vmScreen.screenName();
if (screenName) {
export const
ViewTypePopup = 'Popups',
/**
* @param {Function} ViewModelClassToShow
* @param {Array=} params
* @returns {void}
*/
showScreenPopup = (ViewModelClassToShow, params = []) => {
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
if (vm) {
params = params || [];
vm.beforeShow && vm.beforeShow(...params);
vm.modalVisible(true);
vm.onShow && vm.onShow(...params);
}
},
arePopupsVisible = ko.observable(false),
/**
* @param {Array} screensClasses
* @returns {void}
*/
startScreens = screensClasses => {
hasher.clear();
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
SCREENS = {};
currentScreen = null,
defaultScreenName = '';
screensClasses.forEach(CScreen => {
if (CScreen) {
const vmScreen = new CScreen(),
screenName = vmScreen.screenName;
defaultScreenName || (defaultScreenName = screenName);
SCREENS[screenName] = vmScreen;
}
}
});
});
Object.values(SCREENS).forEach(vmScreen =>
vmScreen && vmScreen.onStart && vmScreen.onStart()
);
const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
hasher.changed.add(cross.parse.bind(cross));
hasher.init();
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
setTimeout(() => $htmlCL.add('rl-started-delay'), 200);
}
function decorateKoCommands(thisArg, commands) {
Object.entries(commands).forEach(([key, canExecute]) => {
let command = thisArg[key],
fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args);
// fn.__realCanExecute = canExecute;
// fn.isCommand = true;
fn.enabled = ko.observable(true);
fn.canExecute = (typeof canExecute === 'function')
? ko.computed(() => fn.enabled() && canExecute.call(thisArg, thisArg))
: ko.computed(() => fn.enabled());
thisArg[key] = fn;
});
}
ko.decorateCommands = decorateKoCommands;
/**
* @param {miced} $items
* @returns {Function}
*/
function settingsMenuKeysHandler(items) {
return ((event, handler)=>{
let index = items.length;
if (event && index) {
while (index-- && !items[index].matches('.selected'));
if (handler && 'arrowup' === handler.shortcut) {
index && --index;
} else if (index < items.length - 1) {
++index;
forEachObjectValue(SCREENS, vmScreen => {
if (!vmScreen.__started) {
vmScreen.onStart();
vmScreen.__started = true;
}
});
const resultHash = items[index].href;
resultHash && rl.route.setHash(resultHash, false, true);
}
}).throttle(200);
}
const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
export {
decorateKoCommands,
settingsMenuKeysHandler
};
hasher.add(cross.parse.bind(cross));
hasher.init();
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
const c = elementById('rl-content'), l = elementById('rl-loading');
c && (c.hidden = false);
l && l.remove();
},
/**
* Used by ko.bindingHandlers.command (template data-bind="command: ")
* to enable/disable click/submit action.
*/
decorateKoCommands = (thisArg, commands) =>
forEachObjectEntry(commands, (key, canExecute) => {
let command = thisArg[key],
fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
thisArg[key] = fn;
});
ko.decorateCommands = decorateKoCommands;

132
dev/Mime/Parser.js Normal file
View file

@ -0,0 +1,132 @@
const
// RFC2045
QPDecodeIn = /=([0-9A-F]{2})/g,
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
export function ParseMime(text)
{
class MimePart
{
header(name) {
return this.headers && this.headers[name];
}
headerValue(name) {
return (this.header(name) || {value:null}).value;
}
get raw() {
return text.slice(this.start, this.end);
}
get bodyRaw() {
return text.slice(this.bodyStart, this.bodyEnd);
}
get body() {
let body = this.bodyRaw,
encoding = this.headerValue('content-transfer-encoding');
if ('quoted-printable' == encoding) {
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
} else if ('base64' == encoding) {
body = atob(body.replace(/\r?\n/g, ''));
}
return body;
}
get dataUrl() {
let body = this.bodyRaw,
encoding = this.headerValue('content-transfer-encoding');
if ('base64' == encoding) {
body = body.replace(/\r?\n/g, '');
} else {
if ('quoted-printable' == encoding) {
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
}
body = btoa(body);
}
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
}
forEach(fn) {
fn(this);
if (this.parts) {
this.parts.forEach(part => part.forEach(fn));
}
}
getByContentType(type) {
if (type == this.headerValue('content-type')) {
return this;
}
if (this.parts) {
let i = 0, p = this.parts, part;
for (i; i < p.length; ++i) {
if ((part = p[i].getByContentType(type))) {
return part;
}
}
}
}
}
const ParsePart = (mimePart, start_pos = 0, id = '') =>
{
let part = new MimePart;
if (id) {
part.id = id;
part.start = start_pos;
part.end = start_pos + mimePart.length;
}
// get headers
let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/);
if (head) {
head = head[0];
let headers = {};
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
let match = header.match(/^([^:]+):\s*([^;]+)/),
params = {};
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
params[param[1].trim().toLowerCase()] = param[2].trim()
);
headers[match[1].trim().toLowerCase()] = {
value: match[2].trim(),
params: params
};
});
part.headers = headers;
// get body
part.bodyStart = start_pos + head.length;
part.bodyEnd = start_pos + mimePart.length;
// get child parts
let boundary = headers['content-type'].params.boundary;
if (boundary) {
part.boundary = boundary;
part.parts = [];
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
body = mimePart.slice(head.length),
bodies = body.split(regex),
pos = part.bodyStart;
[...body.matchAll(regex)].forEach(([boundary], index) => {
if (!index) {
// Mostly something like: "This is a multi-part message in MIME format."
part.bodyText = bodies[0];
}
// Not the end?
if ('--' != boundary.trim().slice(-2)) {
pos += bodies[index].length + boundary.length;
part.parts.push(ParsePart(bodies[1+index], pos, ((id ? id + '.' : '') + (1+index))));
}
});
}
}
return part;
};
return ParsePart(text);
}

76
dev/Mime/Utils.js Normal file
View file

@ -0,0 +1,76 @@
import { ParseMime } from 'Mime/Parser';
import { AttachmentModel } from 'Model/Attachment';
import { FileInfo } from 'Common/File';
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
/**
* @param string data
* @param MessageModel message
*/
export function MimeToMessage(data, message)
{
let signed;
const struct = ParseMime(data);
if (struct.headers) {
let html = struct.getByContentType('text/html');
html = html ? html.body : '';
struct.forEach(part => {
let cd = part.header('content-disposition'),
cid = part.header('content-id'),
type = part.header('content-type');
if (cid || cd) {
// if (cd && 'attachment' === cd.value) {
let attachment = new AttachmentModel;
attachment.mimeType = type.value;
attachment.fileName = type.name || (cd && cd.params.filename) || '';
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
attachment.fileType = FileInfo.getType('', type.value);
attachment.url = part.dataUrl;
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
/*
attachment.isThumbnail = false;
attachment.contentLocation = '';
attachment.download = '';
attachment.folder = '';
attachment.uid = '';
attachment.mimeIndex = part.id;
attachment.framed = false;
*/
attachment.cid = cid ? cid.value : '';
if (cid && html) {
let cid = 'cid:' + attachment.contentId(),
found = html.includes(cid);
attachment.isInline(found);
attachment.isLinked(found);
found && (html = html
.replace('src="' + cid + '"', 'src="' + attachment.url + '"')
.replace("src='" + cid + "'", "src='" + attachment.url + "'")
);
} else {
message.attachments.push(attachment);
}
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
signed = {
MicAlg: type.micalg,
BodyPart: part.parts[0],
SigPart: part.parts[1]
};
}
});
const text = struct.getByContentType('text/plain');
message.plain(text ? text.body : '');
message.html(html);
} else {
message.plain(data);
}
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
signed = true;
}
message.pgpSigned(signed);
// TODO: Verify instantly?
}

View file

@ -1,4 +1,4 @@
import { isArray } from 'Common/Utils';
import { isArray, forEachObjectEntry } from 'Common/Utils';
export class AbstractCollectionModel extends Array
{
@ -24,8 +24,7 @@ export class AbstractCollectionModel extends Array
const result = new this();
if (json) {
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
Object.entries(json).forEach(([key, value]) => '@' !== key[0] && (result[key] = value));
// json[@Count]
forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
json = json['@Collection'];
}
if (isArray(json)) {

View file

@ -1,5 +1,3 @@
import { change } from 'Common/Links';
import { AbstractModel } from 'Knoin/AbstractModel';
export class AccountModel extends AbstractModel {
@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
* @param {boolean=} canBeDelete = true
* @param {number=} count = 0
*/
constructor(email, canBeDelete = true, count = 0) {
constructor(email/*, count = 0*/, isAdditional = true) {
super();
this.email = email;
this.addObservables({
count: count,
deleteAccess: false,
canBeDeleted: !!canBeDelete
// count: count || 0,
askDelete: false,
isAdditional: isAdditional
});
this.canBeEdit = this.canBeDeleted;
}
/**
* @returns {string}
*/
changeAccountLink() {
return change(this.email);
}
}

View file

@ -21,17 +21,20 @@ export class AttachmentModel extends AbstractModel {
this.fileNameExt = '';
this.fileType = FileType.Unknown;
this.friendlySize = '';
this.isInline = false;
this.isLinked = false;
this.isThumbnail = false;
this.cid = '';
this.cidWithoutTags = '';
this.contentLocation = '';
this.download = '';
this.folder = '';
this.uid = '';
this.url = '';
this.mimeIndex = '';
this.framed = false;
this.addObservables({
isInline: false,
isLinked: false
});
}
/**
@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
const attachment = super.reviveFromJson(json);
if (attachment) {
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
return attachment;
}
contentId() {
return this.cid.replace(/^<+|>+$/g, '');
}
/**
* @returns {boolean}
*/
@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
* @returns {string}
*/
linkDownload() {
return attachmentDownload(this.download);
return this.url || attachmentDownload(this.download);
}
/**
* @returns {string}
*/
linkPreview() {
return serverRequestRaw('View', this.download);
}
/**
* @returns {string}
*/
linkThumbnail() {
return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
return this.url || serverRequestRaw('View', this.download);
}
/**
* @returns {string}
*/
linkThumbnailPreviewStyle() {
const link = this.linkThumbnail();
return link ? 'background:url(' + link + ')' : '';
return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
}
/**
@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
const localEvent = event.originalEvent || event;
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
let link = this.linkDownload();
if ('http' !== link.substr(0, 4)) {
if ('http' !== link.slice(0, 4)) {
link = location.protocol + '//' + location.host + location.pathname + link;
}
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);

View file

@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/
static reviveFromJson(items) {
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
/*
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
if (attachments) {
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
}
return attachments;
*/
}
/**
* @returns {boolean}
*/
hasVisible() {
return !!this.find(item => !item.isLinked);
return !!this.filter(item => !item.isLinked()).length;
}
/**
@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
* @returns {*}
*/
findByCid(cid) {
cid = cid.replace(/^<+|>+$/, '');
return this.find(item => cid === item.cidWithoutTags);
cid = cid.replace(/^<+|>+$/g, '');
return this.find(item => cid === item.contentId());
}
}

View file

@ -65,8 +65,8 @@ export class ComposeAttachmentModel extends AbstractModel {
item.download,
item.fileName,
item.estimatedSize,
item.isInline,
item.isLinked,
item.isInline(),
item.isLinked(),
item.cid,
item.contentLocation
);

View file

@ -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);

View file

@ -260,7 +260,7 @@ class Tokenizer
}
}
class EmailModel extends AbstractModel {
export class EmailModel extends AbstractModel {
/**
* @param {string=} email = ''
* @param {string=} name = ''
@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
return false;
}
}
export { EmailModel, EmailModel as default };

View file

@ -1,42 +1,112 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, pInt } from 'Common/Utils';
import { ClientSideKeyName, FolderType } from 'Common/EnumsUser';
import * as Cache from 'Common/Cache';
import { Settings, SettingsGet } from 'Common/Globals';
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import * as Local from 'Storage/Client';
import { AppUserStore } from 'Stores/User/App';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import ko from 'ko';
import { isPosNumeric } from 'Common/UtilsUser';
import { sortFolders } from 'Common/Folders';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel';
import { koComputable } from 'External/ko';
//import { mailBox } from 'Common/Links';
import Remote from 'Remote/User/Fetch';
const
ServerFolderType = {
USER: 0,
INBOX: 1,
SENT: 2,
DRAFTS: 3,
JUNK: 4,
TRASH: 5,
IMPORTANT: 10,
FLAGGED: 11,
ALL: 12
isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|| UNUSED_OPTION_VALUE === sFolderFullName
|| null !== getFolderFromCacheList(sFolderFullName))
? sFolderFullName
: '',
SystemFolders = {
Inbox: 0,
Sent: 0,
Drafts: 0,
Spam: 0,
Trash: 0,
Archive: 0
},
normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
|| UNUSED_OPTION_VALUE === sFolderFullNameRaw
|| null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
? sFolderFullNameRaw
: '';
kolabTypes = {
configuration: 'CONFIGURATION',
event: 'CALENDAR',
contact: 'CONTACTS',
task: 'TASKS',
note: 'NOTES',
file: 'FILES',
journal: 'JOURNAL'
},
getKolabFolderName = type => kolabTypes[type] ? 'Kolab ' + i18n('SETTINGS_FOLDERS/TYPE_' + kolabTypes[type]) : '',
getSystemFolderName = (type, def) => {
switch (type) {
case FolderType.Inbox:
case FolderType.Sent:
case FolderType.Drafts:
case FolderType.Trash:
case FolderType.Archive:
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
case FolderType.Spam:
return i18n('GLOBAL/SPAM');
// no default
}
return def;
};
export const
/**
* @param {string} sFullName
* @param {boolean} bExpanded
*/
setExpandedFolder = (sFullName, bExpanded) => {
let aExpandedList = Local.get(ClientSideKeyNameExpandedFolders);
if (!isArray(aExpandedList)) {
aExpandedList = [];
}
if (bExpanded) {
aExpandedList.includes(sFullName) || aExpandedList.push(sFullName);
} else {
aExpandedList = aExpandedList.filter(value => value !== sFullName);
}
Local.set(ClientSideKeyNameExpandedFolders, aExpandedList);
},
/**
* @param {?Function} fCallback
*/
loadFolders = fCallback => {
// clearTimeout(this.foldersTimeout);
Remote.abort('Folders')
.post('Folders', FolderUserStore.foldersLoading)
.then(data => {
data = FolderCollectionModel.reviveFromJson(data.Result);
data && data.storeIt();
fCallback && fCallback(true);
// Repeat every 15 minutes?
// this.foldersTimeout = setTimeout(loadFolders, 900000);
})
.catch(() => fCallback && setTimeout(fCallback, 1, false));
};
export class FolderCollectionModel extends AbstractCollectionModel
{
@ -44,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
constructor() {
super();
this.CountRec
this.FoldersHash
this.IsThreadsSupported
this.Namespace;
this.Optimized
this.SystemFolders
this.Capabilities
}
*/
@ -57,164 +127,162 @@ export class FolderCollectionModel extends AbstractCollectionModel
* @returns {FolderCollectionModel}
*/
static reviveFromJson(object) {
const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders);
return super.reviveFromJson(object, (oFolder, self) => {
let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw);
/*
if (oCacheFolder) {
oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
}
*/
if (!oCacheFolder && (oCacheFolder = FolderModel.reviveFromJson(oFolder))) {
if (oFolder.FullNameRaw == self.SystemFolders[ServerFolderType.INBOX]) {
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
if (object && object.SystemFolders) {
forEachObjectEntry(SystemFolders, key =>
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
);
}
const result = super.reviveFromJson(object, oFolder => {
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
if (!oCacheFolder) {
oCacheFolder = FolderModel.reviveFromJson(oFolder);
if (!oCacheFolder)
return null;
if (1 == type) {
oCacheFolder.type(FolderType.Inbox);
Cache.setFolderInboxName(oFolder.FullNameRaw);
setFolderInboxName(oFolder.FullName);
}
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder);
setFolder(oCacheFolder);
}
if (oCacheFolder) {
oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders)
|| !expandedFolders.includes(oCacheFolder.fullNameHash));
if (1 < type) {
oCacheFolder.type(type);
}
if (oFolder.Extended) {
if (oFolder.Extended.Hash) {
Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash);
}
oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders)
|| !expandedFolders.includes(oCacheFolder.fullName));
if (null != oFolder.Extended.MessageCount) {
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
}
if (oFolder.Extended) {
if (oFolder.Extended.Hash) {
setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
}
if (null != oFolder.Extended.MessageUnseenCount) {
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
}
if (null != oFolder.Extended.MessageCount) {
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
}
if (null != oFolder.Extended.MessageUnseenCount) {
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
}
}
return oCacheFolder;
});
let i = result.length;
if (i) {
sortFolders(result);
try {
while (i--) {
let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
if (parent) {
parent.subFolders.unshift(folder);
result.splice(i,1);
}
}
} catch (e) {
console.error(e);
}
}
return result;
}
storeIt() {
const cnt = pInt(this.CountRec);
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
let limit = pInt(Settings.app('folderSpecLimit'));
limit = 100 < limit ? 100 : 10 > limit ? 10 : limit;
FolderUserStore.displaySpecSetting(0 >= cnt || limit < cnt);
if (!(
SettingsGet('SentFolder') +
SettingsGet('DraftsFolder') +
SettingsGet('SpamFolder') +
SettingsGet('TrashFolder') +
SettingsGet('ArchiveFolder')
)
) {
FolderUserStore.saveSystemFolders(SystemFolders);
}
FolderUserStore.folderList(this);
if (undefined !== this.Namespace) {
FolderUserStore.namespace = this.Namespace;
}
FolderUserStore.namespace = this.Namespace;
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
FolderUserStore.folderListOptimized(!!this.Optimized);
FolderUserStore.sortSupported(!!this.IsSortSupported);
FolderUserStore.quotaUsage(this.quotaUsage);
FolderUserStore.quotaLimit(this.quotaLimit);
FolderUserStore.capabilities(this.Capabilities);
let update = false;
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
if (
this.SystemFolders &&
!('' +
SettingsGet('SentFolder') +
SettingsGet('DraftFolder') +
SettingsGet('SpamFolder') +
SettingsGet('TrashFolder') +
SettingsGet('ArchiveFolder'))
) {
Settings.set('SentFolder', this.SystemFolders[ServerFolderType.SENT] || null);
Settings.set('DraftFolder', this.SystemFolders[ServerFolderType.DRAFTS] || null);
Settings.set('SpamFolder', this.SystemFolders[ServerFolderType.JUNK] || null);
Settings.set('TrashFolder', this.SystemFolders[ServerFolderType.TRASH] || null);
Settings.set('ArchiveFolder', this.SystemFolders[ServerFolderType.ALL] || null);
update = true;
}
FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder')));
FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder')));
FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder')));
FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder')));
FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder')));
if (update) {
rl.app.Remote.saveSystemFolders(()=>{}, {
SentFolder: FolderUserStore.sentFolder(),
DraftFolder: FolderUserStore.draftFolder(),
SpamFolder: FolderUserStore.spamFolder(),
TrashFolder: FolderUserStore.trashFolder(),
ArchiveFolder: FolderUserStore.archiveFolder()
});
}
Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
// FolderUserStore.folderList.valueHasMutated();
}
}
function getSystemFolderName(type, def)
{
switch (type) {
case FolderType.Inbox:
return i18n('FOLDER_LIST/INBOX_NAME');
case FolderType.SentItems:
return i18n('FOLDER_LIST/SENT_NAME');
case FolderType.Draft:
return i18n('FOLDER_LIST/DRAFTS_NAME');
case FolderType.Spam:
return i18n('GLOBAL/SPAM');
case FolderType.Trash:
return i18n('FOLDER_LIST/TRASH_NAME');
case FolderType.Archive:
return i18n('FOLDER_LIST/ARCHIVE_NAME');
// no default
}
return def;
}
export class FolderModel extends AbstractModel {
constructor() {
super();
this.fullName = '';
this.fullNameRaw = '';
this.fullNameHash = '';
this.delimiter = '';
this.namespace = '';
this.deep = 0;
this.expires = 0;
this.metadata = {};
this.selectable = false;
this.exists = true;
// this.hash = '';
// this.uidNext = 0;
this.addObservables({
name: '',
type: FolderType.User,
selectable: false,
focused: false,
selected: false,
edited: false,
subscribed: true,
checkable: false,
deleteAccess: false,
checkable: false, // Check for new messages
askDelete: false,
nameForEdit: '',
errorMsg: '',
privateMessageCountAll: 0,
privateMessageCountUnread: 0,
collapsedPrivate: true
kolabType: null,
collapsed: true
});
this.addSubscribables({
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue
});
this.subFolders = ko.observableArray(new FolderCollectionModel);
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
}
/**
* For url safe '/#/mailbox/...' path
*/
get fullNameHash() {
return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe);
// return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName);
}
/**
* @static
* @param {FetchJsonFolder} json
@ -223,12 +291,22 @@ export class FolderModel extends AbstractModel {
static reviveFromJson(json) {
const folder = super.reviveFromJson(json);
if (folder) {
folder.deep = json.FullNameRaw.split(folder.delimiter).length - 1;
const path = folder.fullName.split(folder.delimiter),
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|| ''
).split('.')[0];
folder.messageCountAll = ko.computed({
folder.deep = path.length - 1;
path.pop();
folder.parentName = path.join(folder.delimiter);
type && 'mail' != type && folder.kolabType(type);
folder.messageCountAll = koComputable({
read: folder.privateMessageCountAll,
write: (iValue) => {
if (isPosNumeric(iValue, true)) {
if (isPosNumeric(iValue)) {
folder.privateMessageCountAll(iValue);
} else {
folder.privateMessageCountAll.valueHasMutated();
@ -237,10 +315,10 @@ export class FolderModel extends AbstractModel {
})
.extend({ notify: 'always' });
folder.messageCountUnread = ko.computed({
folder.messageCountUnread = koComputable({
read: folder.privateMessageCountUnread,
write: (value) => {
if (isPosNumeric(value, true)) {
if (isPosNumeric(value)) {
folder.privateMessageCountUnread(value);
} else {
folder.privateMessageCountUnread.valueHasMutated();
@ -253,61 +331,68 @@ export class FolderModel extends AbstractModel {
isInbox: () => FolderType.Inbox === folder.type(),
hasSubscribedSubfolders:
() =>
!!folder.subFolders.find(
oFolder => (oFolder.subscribed() || oFolder.hasSubscribedSubfolders()) && !oFolder.isSystemFolder()
),
isFlagged: () => FolderUserStore.currentFolder() === folder
&& MessagelistUserStore.listSearch().includes('flagged'),
canBeEdited: () => FolderType.User === folder.type() && folder.exists && folder.selectable,
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
oFolder => {
const subscribed = oFolder.hasSubscriptions();
return !oFolder.isSystemFolder() && subscribed;
}
),
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
canBeDeleted: () => folder.canBeSelected() && folder.exists,
canBeSubscribed: () => folder.selectable()
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
/**
* Folder is visible when:
* - hasVisibleSubfolders()
* Or when all below conditions are true:
* - selectable()
* - subscribed() OR hideUnsubscribed = false
* - FolderType.User
* - not kolabType()
*/
visible: () => {
const isSubscribed = folder.subscribed(),
isSubFolders = folder.hasSubscribedSubfolders();
return isSubscribed || (isSubFolders && (!folder.exists || !folder.selectable));
const selectable = folder.canBeSelected(),
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
return folder.hasVisibleSubfolders() | visible;
},
isSystemFolder: () => FolderType.User !== folder.type(),
hidden: () => {
const isSystem = folder.isSystemFolder(),
isSubFolders = folder.hasSubscribedSubfolders();
return (isSystem && !isSubFolders) || (!folder.selectable && !isSubFolders);
},
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
printableUnreadCount: () => {
const count = folder.messageCountAll(),
unread = folder.messageCountUnread(),
type = folder.type();
if (0 < count) {
if (FolderType.Draft === type) {
return '' + count;
if (count) {
if (FolderType.Drafts === type) {
return count;
}
if (
0 < unread &&
unread &&
FolderType.Trash !== type &&
FolderType.Archive !== type &&
FolderType.SentItems !== type
FolderType.Sent !== type
) {
return '' + unread;
return unread;
}
}
return '';
return null;
},
canBeDeleted: () => !folder.isSystemFolder() && !folder.subFolders.length,
canBeSubscribed: () => !folder.isSystemFolder()
&& SettingsUserStore.hideUnsubscribed()
&& folder.selectable
&& Settings.app('useImapSubscribe'),
canBeSelected: () => !folder.isSystemFolder() && folder.selectable,
localName: () => {
let name = folder.name();
if (folder.isSystemFolder()) {
@ -320,28 +405,22 @@ export class FolderModel extends AbstractModel {
manageFolderSystemName: () => {
if (folder.isSystemFolder()) {
translatorTrigger();
let suffix = getSystemFolderName(folder.type(), '');
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
return '(' + suffix + ')';
}
}
return '';
},
collapsed: {
read: () => !folder.hidden() && folder.collapsedPrivate(),
write: (value) => {
folder.collapsedPrivate(value);
}
},
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
hasSubscribedUnreadMessagesSubfolders: () =>
!!folder.subFolders.find(
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
)
!!folder.subFolders().find(
folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
)
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
});
folder.addSubscribables({
@ -351,7 +430,7 @@ export class FolderModel extends AbstractModel {
messageCountUnread: unread => {
if (FolderType.Inbox === folder.type()) {
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
fireEvent('mailbox.inbox-unread-count', unread);
}
}
});
@ -363,16 +442,9 @@ export class FolderModel extends AbstractModel {
* @returns {string}
*/
collapsedCss() {
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
: 'icon-none'
);
}
/**
* @returns {string}
*/
printableFullName() {
return this.fullName.split(this.delimiter).join(' / ');
}
}

View file

@ -1,4 +1,4 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { AbstractModel } from 'Knoin/AbstractModel';
@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
signature: '',
signatureInsertBefore: false,
deleteAccess: false
askDelete: false
});
this.canBeDeleted = ko.computed(() => !!this.id());
this.canBeDeleted = koComputable(() => !!this.id());
}
/**
@ -34,6 +34,6 @@ export class IdentityModel extends AbstractModel {
const name = this.name(),
email = this.email();
return name ? name + ' (' + email + ')' : email;
return name ? name + ' <' + email + '>' : email;
}
}

View file

@ -3,12 +3,13 @@ import ko from 'ko';
import { MessagePriority } from 'Common/EnumsUser';
import { i18n } from 'Common/Translator';
import { encodeHtml } from 'Common/Html';
import { isArray, isNonEmptyArray } from 'Common/Utils';
import { doc } from 'Common/Globals';
import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw } from 'Common/Links';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo } from 'Common/File';
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html';
const
SignedVerifyStatus = {
UnknownPublicKeys: -4,
UnknownPrivateKey: -3,
Unverified: -2,
Error: -1,
None: 0,
Success: 1
// eslint-disable-next-line max-len
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
// eslint-disable-next-line max-len
email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
getRealHeight = el => {
hcont.innerHTML = el.outerHTML;
const result = hcont.clientHeight;
hcont.innerHTML = '';
return result;
},
replyHelper = (emails, unic, localEmails) => {
@ -36,6 +41,8 @@ const
});
};
doc.body.append(hcont);
export class MessageModel extends AbstractModel {
constructor() {
super();
@ -44,38 +51,34 @@ export class MessageModel extends AbstractModel {
this.addObservables({
subject: '',
plain: '',
html: '',
size: 0,
spamScore: 0,
spamResult: '',
isSpam: false,
hasVirus: null, // or boolean when scanned
dateTimeStampInUTC: 0,
priority: MessagePriority.Normal,
senderEmailsString: '',
senderClearEmailsString: '',
newForAnimation: false,
deleted: false,
isDeleted: false,
isUnseen: false,
isFlagged: false,
isAnswered: false,
isForwarded: false,
isReadReceipt: false,
focused: false,
selected: false,
checked: false,
hasAttachments: false,
isHtml: false,
hasImages: false,
hasExternals: false,
isPgpSigned: false,
isPgpEncrypted: false,
pgpSignedVerifyStatus: SignedVerifyStatus.None,
pgpSignedVerifyUser: '',
pgpSigned: null,
pgpVerified: null,
pgpEncrypted: null,
pgpDecrypted: false,
readReceipt: '',
@ -84,22 +87,32 @@ export class MessageModel extends AbstractModel {
});
this.attachments = ko.observableArray(new AttachmentCollectionModel);
this.attachmentsSpecData = ko.observableArray();
this.threads = ko.observableArray();
this.unsubsribeLinks = ko.observableArray();
this.flags = ko.observableArray();
this.addComputables({
attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []),
attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
threadsLen: () => this.threads().length,
isImportant: () => MessagePriority.High === this.priority(),
hasAttachments: () => this.attachments().hasVisible(),
isDeleted: () => this.flags().includes('\\deleted'),
isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
isFlagged: () => this.flags().includes('\\flagged'),
isAnswered: () => this.flags().includes('\\answered'),
isForwarded: () => this.flags().includes('$forwarded'),
isReadReceipt: () => this.flags().includes('$mdnsent')
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing')
});
}
_reset() {
this.folder = '';
this.uid = '';
this.uid = 0;
this.hash = '';
this.requestHash = '';
this.externalProxy = false;
this.emails = [];
this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel;
@ -107,7 +120,6 @@ export class MessageModel extends AbstractModel {
this.bcc = new EmailCollectionModel;
this.replyTo = new EmailCollectionModel;
this.deliveredTo = new EmailCollectionModel;
this.unsubsribeLinks = [];
this.body = null;
this.draftInfo = [];
this.messageId = '';
@ -118,49 +130,50 @@ export class MessageModel extends AbstractModel {
clear() {
this._reset();
this.subject('');
this.html('');
this.plain('');
this.size(0);
this.spamScore(0);
this.spamResult('');
this.isSpam(false);
this.hasVirus(null);
this.dateTimeStampInUTC(0);
this.priority(MessagePriority.Normal);
this.senderEmailsString('');
this.senderClearEmailsString('');
this.newForAnimation(false);
this.deleted(false);
this.isDeleted(false);
this.isUnseen(false);
this.isFlagged(false);
this.isAnswered(false);
this.isForwarded(false);
this.isReadReceipt(false);
this.selected(false);
this.checked(false);
this.hasAttachments(false);
this.attachmentsSpecData([]);
this.isHtml(false);
this.hasImages(false);
this.hasExternals(false);
this.attachments(new AttachmentCollectionModel);
this.isPgpSigned(false);
this.isPgpEncrypted(false);
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
this.pgpSignedVerifyUser('');
this.pgpSigned(null);
this.pgpVerified(null);
this.pgpEncrypted(null);
this.pgpDecrypted(false);
this.priority(MessagePriority.Normal);
this.readReceipt('');
this.threads([]);
this.unsubsribeLinks([]);
this.hasUnseenSubMessage(false);
this.hasFlaggedSubMessage(false);
}
spamStatus() {
let spam = this.spamResult();
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
}
/**
* @param {Array} properties
* @returns {Array}
@ -179,7 +192,7 @@ export class MessageModel extends AbstractModel {
}
computeSenderEmail() {
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftFolder()].includes(this.folder) ? 'to' : 'from';
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from';
this.senderEmailsString(this[list].toString(true));
this.senderClearEmailsString(this[list].toStringClear());
}
@ -189,13 +202,12 @@ export class MessageModel extends AbstractModel {
* @returns {boolean}
*/
revivePropertiesFromJson(json) {
if ('Priority' in json) {
let p = parseInt(json.Priority, 10);
json.Priority = MessagePriority.High == p || MessagePriority.Low == p ? p : MessagePriority.Normal;
if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
json.Priority = MessagePriority.Normal;
}
if (super.revivePropertiesFromJson(json)) {
// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : [];
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs));
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
this.computeSenderEmail();
}
@ -205,14 +217,14 @@ export class MessageModel extends AbstractModel {
* @returns {boolean}
*/
hasUnsubsribeLinks() {
return this.unsubsribeLinks && this.unsubsribeLinks.length;
return this.unsubsribeLinks().length;
}
/**
* @returns {string}
*/
getFirstUnsubsribeLink() {
return this.unsubsribeLinks && this.unsubsribeLinks.length ? this.unsubsribeLinks[0] || '' : '';
return this.unsubsribeLinks()[0] || '';
}
/**
@ -229,7 +241,7 @@ export class MessageModel extends AbstractModel {
*/
fromDkimData() {
let result = ['none', ''];
if (isNonEmptyArray(this.from) && 1 === this.from.length && this.from[0] && this.from[0].dkimStatus) {
if (1 === arrayLength(this.from) && this.from[0] && this.from[0].dkimStatus) {
result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
}
@ -277,7 +289,7 @@ export class MessageModel extends AbstractModel {
*/
lineAsCss() {
let classes = [];
Object.entries({
forEachObjectEntry({
deleted: this.deleted(),
'deleted-mark': this.isDeleted(),
selected: this.selected(),
@ -288,13 +300,12 @@ export class MessageModel extends AbstractModel {
forwarded: this.isForwarded(),
focused: this.focused(),
important: this.isImportant(),
withAttachments: this.hasAttachments(),
new: this.newForAnimation(),
withAttachments: !!this.attachments().length,
emptySubject: !this.subject(),
// hasChildrenMessage: 1 < this.threadsLen(),
hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
}).forEach(([key, value]) => value && classes.push(key));
}, (key, value) => value && classes.push(key));
return classes.join(' ');
}
@ -367,35 +378,138 @@ export class MessageModel extends AbstractModel {
return [toResult, ccResult];
}
/**
* @param {boolean=} print = false
*/
viewHtml() {
const body = this.body;
if (body && this.html()) {
const contentLocationUrls = {},
oAttachments = this.attachments();
// Get contentLocationUrls
oAttachments.forEach(oAttachment => {
if (oAttachment.cid && oAttachment.contentLocation) {
contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation;
}
});
let result = cleanHtml(this.html(), contentLocationUrls, SettingsUserStore.removeColors());
this.hasExternals(result.hasExternals);
// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length;
this.hasImages(body.rlHasImages = !!result.hasExternals);
// Hide valid inline attachments in message view 'attachments' section
oAttachments.forEach(oAttachment => {
let cid = oAttachment.contentId(),
found = result.foundCIDs.includes(cid);
oAttachment.isInline(found);
oAttachment.isLinked(found || result.foundContentLocationUrls.includes(oAttachment.contentLocation));
});
body.innerHTML = result.html;
body.classList.toggle('html', 1);
body.classList.toggle('plain', 0);
// showInternalImages
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
const data = el.dataset;
if (data.xSrcCid) {
const attachment = findAttachmentByCid(data.xSrcCid);
if (attachment && attachment.download) {
el.src = attachment.linkPreview();
}
} else if (data.xSrcLocation) {
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|| findAttachmentByCid(data.xSrcLocation);
if (attachment && attachment.download) {
el.loading = 'lazy';
el.src = attachment.linkPreview();
}
} else if (data.xStyleCid) {
forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => {
const attachment = findAttachmentByCid(cid);
if (attachment && attachment.linkPreview && name) {
el.style[name] = "url('" + attachment.linkPreview() + "')";
}
});
}
});
if (SettingsUserStore.showImages()) {
this.showExternalImages();
}
this.isHtml(true);
this.initView();
return true;
}
}
viewPlain() {
const body = this.body;
if (body && this.plain()) {
body.classList.toggle('html', 0);
body.classList.toggle('plain', 1);
body.innerHTML = plainToHtml(
this.plain()
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
.trim()
)
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
.replace(email, '$1<a href="mailto:$2">$2</a>');
this.isHtml(false);
this.hasImages(false);
this.initView();
return true;
}
}
initView() {
// init BlockquoteSwitcher
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
node.removeAttribute('style')
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
let h = node.clientHeight || getRealHeight(node);
if (0 === h || 100 < h) {
const el = Element.fromHTML('<span class="rlBlockquoteSwitcher">•••</span>');
node.classList.add('rl-bq-switcher','hidden-bq');
node.before(el);
el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
}
}
});
}
viewPopupMessage(print) {
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.ccToLine(false),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
win = open(''),
doc = win.document;
doc.write(PreviewHTML
.replace(/{{subject}}/g, encodeHtml(this.subject()))
.replace('{{date}}', encodeHtml(m ? m.format('LLL') : ''))
.replace('{{fromCreds}}', encodeHtml(this.fromToLine(false)))
.replace('{{toCreds}}', encodeHtml(this.toToLine(false)))
.replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO')))
.replace('{{ccHide}}', ccLine ? '' : 'hidden=""')
.replace('{{ccCreds}}', encodeHtml(ccLine))
.replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC')))
.replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
.replace('{{html}}', this.bodyAsHTML())
sdoc = win.document;
let subject = encodeHtml(this.subject()),
mode = this.isHtml() ? 'div' : 'pre',
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
style = getComputedStyle(doc.querySelector('.messageView')),
prop = property => style.getPropertyValue(property);
sdoc.write(PreviewHTML
.replace('<title>', '<title>'+subject)
// eslint-disable-next-line max-len
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine(false))}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine(false))}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
);
doc.close();
sdoc.close();
if (print) {
setTimeout(() => win.print(), 100);
}
}
/**
* @param {boolean=} print = false
*/
popupMessage() {
this.viewPopupMessage(false);
}
printMessage() {
this.viewPopupMessage(true);
}
@ -411,70 +525,61 @@ export class MessageModel extends AbstractModel {
* @param {MessageModel} message
* @returns {MessageModel}
*/
populateByMessageListItem(message) {
if (message) {
this.folder = message.folder;
this.uid = message.uid;
this.hash = message.hash;
this.requestHash = message.requestHash;
this.subject(message.subject());
this.size(message.size());
this.spamScore(message.spamScore());
this.spamResult(message.spamResult());
this.isSpam(message.isSpam());
this.dateTimeStampInUTC(message.dateTimeStampInUTC());
this.priority(message.priority());
this.externalProxy = message.externalProxy;
this.emails = message.emails;
this.from = message.from;
this.to = message.to;
this.cc = message.cc;
this.bcc = message.bcc;
this.replyTo = message.replyTo;
this.deliveredTo = message.deliveredTo;
this.unsubsribeLinks = message.unsubsribeLinks;
this.isUnseen(message.isUnseen());
this.isFlagged(message.isFlagged());
this.isAnswered(message.isAnswered());
this.isForwarded(message.isForwarded());
this.isReadReceipt(message.isReadReceipt());
this.isDeleted(message.isDeleted());
this.priority(message.priority());
this.selected(message.selected());
this.checked(message.checked());
this.hasAttachments(message.hasAttachments());
this.attachmentsSpecData(message.attachmentsSpecData());
}
this.body = null;
this.draftInfo = [];
this.messageId = '';
this.inReplyTo = '';
this.references = '';
static fromMessageListItem(message) {
let self = new MessageModel();
if (message) {
this.threads(message.threads());
self.folder = message.folder;
self.uid = message.uid;
self.hash = message.hash;
self.requestHash = message.requestHash;
self.subject(message.subject());
self.plain(message.plain());
self.html(message.html());
self.size(message.size());
self.spamScore(message.spamScore());
self.spamResult(message.spamResult());
self.isSpam(message.isSpam());
self.hasVirus(message.hasVirus());
self.dateTimeStampInUTC(message.dateTimeStampInUTC());
self.priority(message.priority());
self.hasExternals(message.hasExternals());
self.emails = message.emails;
self.from = message.from;
self.to = message.to;
self.cc = message.cc;
self.bcc = message.bcc;
self.replyTo = message.replyTo;
self.deliveredTo = message.deliveredTo;
self.unsubsribeLinks(message.unsubsribeLinks);
self.flags(message.flags());
self.priority(message.priority());
self.selected(message.selected());
self.checked(message.checked());
self.attachments(message.attachments());
self.threads(message.threads());
}
this.computeSenderEmail();
self.computeSenderEmail();
return this;
return self;
}
showExternalImages() {
if (this.body && this.body.rlHasImages) {
const body = this.body;
if (body && this.hasImages()) {
this.hasImages(false);
this.body.rlHasImages = false;
body.rlHasImages = false;
let body = this.body, attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src';
let attr = 'data-x-src';
body.querySelectorAll('[' + attr + ']').forEach(node => {
if (node.matches('img')) {
node.loading = 'lazy';
@ -482,81 +587,28 @@ export class MessageModel extends AbstractModel {
node.src = node.getAttribute(attr);
});
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
body.querySelectorAll('[' + attr + ']').forEach(node => {
node.setAttribute('style', ((node.getAttribute('style')||'')
+ ';' + node.getAttribute(attr))
.replace(/^[;\s]+/,''));
body.querySelectorAll('[data-x-style-url]').forEach(node => {
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
});
}
}
showInternalImages() {
const body = this.body;
if (body && !body.rlInitInternalImages) {
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
body.rlInitInternalImages = true;
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
const data = el.dataset;
if (data.xSrcCid) {
const attachment = findAttachmentByCid(data.xSrcCid);
if (attachment && attachment.download) {
el.src = attachment.linkPreview();
}
} else if (data.xSrcLocation) {
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|| findAttachmentByCid(data.xSrcLocation);
if (attachment && attachment.download) {
el.loading = 'lazy';
el.src = attachment.linkPreview();
}
} else if (data.xStyleCid) {
const name = data.xStyleCidName,
attachment = findAttachmentByCid(data.xStyleCid);
if (attachment && attachment.linkPreview && name) {
el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');"
+ (el.getAttribute('style') || ''));
}
}
});
}
}
storeDataInDom() {
if (this.body) {
this.body.rlIsHtml = !!this.isHtml();
this.body.rlHasImages = !!this.hasImages();
}
}
fetchDataFromDom() {
if (this.body) {
this.isHtml(!!this.body.rlIsHtml);
this.hasImages(!!this.body.rlHasImages);
}
}
/**
* @returns {string}
*/
bodyAsHTML() {
if (this.body) {
let clone = this.body.cloneNode(true),
attr = 'data-html-editor-font-wrapper';
// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
if (this.body && !this.body.querySelector('iframe')) {
let clone = this.body.cloneNode(true);
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
node => node.classList.remove('rl-bq-switcher','hidden-bq')
);
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
node => node.remove()
);
clone.querySelectorAll('['+attr+']').forEach(
node => node.removeAttribute(attr)
);
return clone.innerHTML;
}
return '';
return this.html() || plainToHtml(this.plain());
}
/**
@ -573,4 +625,5 @@ export class MessageModel extends AbstractModel {
this.isReadReceipt()
].join(',');
}
}

View file

@ -39,7 +39,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
if (message) {
if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) {
++newCount;
message.newForAnimation(true);
}
message.deleted(false);

View file

@ -1,74 +0,0 @@
import ko from 'ko';
import { isNonEmptyArray } from 'Common/Utils';
import { AbstractModel } from 'Knoin/AbstractModel';
import { PgpUserStore } from 'Stores/User/Pgp';
export class OpenPgpKeyModel extends AbstractModel {
/**
* @param {string} index
* @param {string} guID
* @param {string} ID
* @param {array} IDs
* @param {array} userIDs
* @param {array} emails
* @param {boolean} isPrivate
* @param {string} armor
* @param {string} userID
*/
constructor(index, guID, ID, IDs, userIDs, emails, isPrivate, armor, userID) {
super();
this.index = index;
this.id = ID;
this.ids = isNonEmptyArray(IDs) ? IDs : [ID];
this.guid = guID;
this.user = '';
this.users = userIDs;
this.email = '';
this.emails = emails;
this.armor = armor;
this.isPrivate = !!isPrivate;
this.selectUser(userID);
this.deleteAccess = ko.observable(false);
}
getNativeKey() {
let key = null;
try {
key = PgpUserStore.openpgp.key.readArmored(this.armor);
if (key && !key.err && key.keys && key.keys[0]) {
return key;
}
} catch (e) {
console.log(e);
}
return null;
}
getNativeKeys() {
const key = this.getNativeKey();
return key && key.keys ? key.keys : null;
}
select(pattern, property) {
if (this[property]) {
const index = this[property].indexOf(pattern);
if (-1 !== index) {
this.user = this.users[index];
this.email = this.emails[index];
}
}
}
selectUser(user) {
this.select(user, 'users');
}
selectEmail(email) {
this.select(email, 'emails');
}
}

View file

@ -1,23 +0,0 @@
import ko from 'ko';
import { AbstractModel } from 'Knoin/AbstractModel';
export class TemplateModel extends AbstractModel {
/**
* @param {string} id
* @param {string} name
* @param {string} body
*/
constructor(id = '', name = '', body = '') {
super();
this.id = id;
this.name = name;
this.body = body;
this.populated = true;
this.deleteAccess = ko.observable(false);
}
// static reviveFromJson(json) {}
}

View file

@ -1,41 +1,30 @@
import { Notification } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links';
import { runHook } from 'Common/Plugins';
import { getNotification } from 'Common/Translator';
let iJsonErrorCount = 0,
iTokenErrorCount = 0;
let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + add,
updateToken = data => {
if (data.UpdateToken) {
rl.hash.set();
Settings.set('AuthAccountHash', data.UpdateToken);
}
},
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) {
if (Notification.InvalidToken === err) {
alert(getNotification(err));
rl.logoutReload();
} else {
if ([
Notification.AuthError,
Notification.ConnectionError,
Notification.DomainNotAllowed,
Notification.AccountNotAllowed,
Notification.MailServerError,
Notification.UnknownNotification,
Notification.UnknownError
].includes(err)
) {
++iJsonErrorCount;
}
if (data.ClearAuth || data.Logout || 7 < iJsonErrorCount) {
rl.hash.clear();
data.ClearAuth || rl.logoutReload();
} else if ([
Notification.AuthError,
Notification.ConnectionError,
Notification.DomainNotAllowed,
Notification.AccountNotAllowed,
Notification.MailServerError,
Notification.UnknownNotification,
Notification.UnknownError
].includes(err)
) {
if (7 < ++iJsonErrorCount) {
rl.logoutReload();
}
}
},
@ -57,7 +46,11 @@ abort = (sAction, bClearOnly) => {
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
sGetAdd = pString(sGetAdd);
params = params || {};
params.Action = action;
if (params instanceof FormData) {
params.set('Action', action);
} else {
params.Action = action;
}
let init = {};
if (window.AbortController) {
abort(action);
@ -69,6 +62,14 @@ fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
return rl.fetchJSON(getURL(sGetAdd), init, sGetAdd ? null : params).then(jsonCallback);
};
class FetchError extends Error
{
constructor(code, message) {
super(message);
this.code = code || Notification.JsonFalse;
}
}
export class AbstractFetchRemote
{
abort(sAction, bClearOnly) {
@ -76,6 +77,45 @@ export class AbstractFetchRemote
return this;
}
/**
* Allows quicker visual responses to the user.
* Can be used to stream lines of json encoded data, but does not work on all servers.
* Apache needs 'flushpackets' like in <Proxy "fcgi://...." flushpackets=on></Proxy>
*/
streamPerLine(fCallback, sGetAdd) {
rl.fetch(getURL(sGetAdd))
.then(response => response.body)
.then(body => {
// Firefox TextDecoderStream is not defined
// const reader = body.pipeThrough(new TextDecoderStream()).getReader();
const reader = body.getReader(),
re = /\r\n|\n|\r/gm,
utf8decoder = new TextDecoder();
let buffer = '';
function processText({ done, value }) {
buffer += value ? utf8decoder.decode(value, {stream: true}) : '';
for (;;) {
let result = re.exec(buffer);
if (!result) {
if (done) {
break;
}
reader.read().then(processText);
return;
}
fCallback(buffer.slice(0, result.index));
buffer = buffer.slice(result.index + 1);
re.lastIndex = 0;
}
if (buffer.length) {
// last line didn't end in a newline char
fCallback(buffer);
}
}
reader.read().then(processText);
})
}
/**
* @param {?Function} fCallback
* @param {string} sAction
@ -84,7 +124,7 @@ export class AbstractFetchRemote
* @param {string=} sGetAdd = ''
* @param {Array=} aAbortActions = []
*/
defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) {
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
params = params || {};
const start = Date.now();
@ -100,12 +140,8 @@ export class AbstractFetchRemote
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
let cached = false;
if (data) {
if (data.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
updateToken(data);
if (data && data.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
let iError = 0;
@ -123,7 +159,7 @@ export class AbstractFetchRemote
}
*/
if (data.Result) {
iJsonErrorCount = iTokenErrorCount = 0;
iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError
@ -153,32 +189,11 @@ export class AbstractFetchRemote
});
}
/**
* @param {?Function} fCallback
*/
noop(fCallback) {
this.defaultRequest(fCallback, 'Noop');
}
/**
* @param {?Function} fCallback
*/
getPublicKey(fCallback) {
this.defaultRequest(fCallback, 'GetPublicKey');
}
/**
* @param {?Function} fCallback
* @param {string} sVersion
*/
jsVersion(fCallback, sVersion) {
this.defaultRequest(fCallback, 'Version', {
Version: sVersion
});
}
fastResolve(mData) {
return Promise.resolve(mData);
this.request('GetPublicKey', fCallback);
}
setTrigger(trigger, value) {
@ -190,17 +205,15 @@ export class AbstractFetchRemote
}
}
postRequest(action, fTrigger, params, timeOut) {
post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true);
return fetchJSON(action, '', params, pInt(timeOut, 30000),
data => {
abort(action, true);
if (!data) {
return Promise.reject(Notification.JsonParse);
return Promise.reject(new FetchError(Notification.JsonParse));
}
updateToken(data);
/*
let isCached = false, type = '';
if (data && data.Time) {
@ -223,8 +236,10 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) {
checkResponseError(data);
const err = data ? data.ErrorCode : 0;
return Promise.reject(err || Notification.JsonFalse);
return Promise.reject(new FetchError(
data ? data.ErrorCode : 0,
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
));
}
return data;

View file

@ -1,212 +1,14 @@
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
class RemoteAdminFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
* @param {string} sLogin
* @param {string} sPassword
*/
adminLogin(fCallback, sLogin, sPassword) {
this.defaultRequest(fCallback, 'AdminLogin', {
Login: sLogin,
Password: sPassword
});
}
/**
* @param {string} key
* @param {?scalar} value
* @param {?Function} fCallback
*/
adminLogout(fCallback) {
this.defaultRequest(fCallback, 'AdminLogout');
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
saveAdminConfig(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminSettingsUpdate', oData);
}
/**
* @param {?Function} fCallback
* @param {boolean=} bIncludeAliases = true
*/
domainList(fCallback, bIncludeAliases = true) {
this.defaultRequest(fCallback, 'AdminDomainList', {
IncludeAliases: bIncludeAliases ? 1 : 0
});
}
/**
* @param {?Function} fCallback
*/
pluginList(fCallback) {
this.defaultRequest(fCallback, 'AdminPluginList');
}
/**
* @param {?Function} fCallback
*/
packagesList(fCallback) {
this.defaultRequest(fCallback, 'AdminPackagesList');
}
/**
* @param {?Function} fCallback
* @param {Object} oPackage
*/
packageInstall(fCallback, oPackage) {
this.defaultRequest(
fCallback,
'AdminPackageInstall',
{
Id: oPackage.id,
Type: oPackage.type,
File: oPackage.file
},
60000
);
}
/**
* @param {?Function} fCallback
* @param {Object} oPackage
*/
packageDelete(fCallback, oPackage) {
this.defaultRequest(fCallback, 'AdminPackageDelete', {
Id: oPackage.id
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
*/
domain(fCallback, sName) {
this.defaultRequest(fCallback, 'AdminDomainLoad', {
Name: sName
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
*/
plugin(fCallback, sName) {
this.defaultRequest(fCallback, 'AdminPluginLoad', {
Name: sName
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
*/
domainDelete(fCallback, sName) {
this.defaultRequest(fCallback, 'AdminDomainDelete', {
Name: sName
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
* @param {boolean} bDisabled
*/
domainDisable(fCallback, sName, bDisabled) {
this.defaultRequest(fCallback, 'AdminDomainDisable', {
Name: sName,
Disabled: bDisabled ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {Object} oConfig
*/
pluginSettingsUpdate(fCallback, oConfig) {
this.defaultRequest(fCallback, 'AdminPluginSettingsUpdate', oConfig);
}
/**
* @param {?Function} fCallback
* @param {string} sName
* @param {boolean} bDisabled
*/
pluginDisable(fCallback, sName, bDisabled) {
this.defaultRequest(fCallback, 'AdminPluginDisable', {
Name: sName,
Disabled: bDisabled ? 1 : 0
});
}
createDomainAlias(fCallback, sName, sAlias) {
this.defaultRequest(fCallback, 'AdminDomainAliasSave', {
Name: sName,
Alias: sAlias
});
}
createOrUpdateDomain(fCallback, oDomain) {
this.defaultRequest(fCallback, 'AdminDomainSave', {
Create: oDomain.edit() ? 0 : 1,
Name: oDomain.name(),
IncHost: oDomain.imapServer(),
IncPort: oDomain.imapPort(),
IncSecure: oDomain.imapSecure(),
IncShortLogin: oDomain.imapShortLogin() ? 1 : 0,
UseSieve: oDomain.useSieve() ? 1 : 0,
SieveHost: oDomain.sieveServer(),
SievePort: oDomain.sievePort(),
SieveSecure: oDomain.sieveSecure(),
OutHost: oDomain.smtpServer(),
OutPort: oDomain.smtpPort(),
OutSecure: oDomain.smtpSecure(),
OutShortLogin: oDomain.smtpShortLogin() ? 1 : 0,
OutAuth: oDomain.smtpAuth() ? 1 : 0,
OutSetSender: oDomain.smtpSetSender() ? 1 : 0,
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0,
WhiteList: oDomain.whiteList()
});
}
testConnectionForDomain(fCallback, oDomain) {
this.defaultRequest(fCallback, 'AdminDomainTest', {
Name: oDomain.name(),
IncHost: oDomain.imapServer(),
IncPort: oDomain.imapPort(),
IncSecure: oDomain.imapSecure(),
UseSieve: oDomain.useSieve() ? 1 : 0,
SieveHost: oDomain.sieveServer(),
SievePort: oDomain.sievePort(),
SieveSecure: oDomain.sieveSecure(),
OutHost: oDomain.smtpServer(),
OutPort: oDomain.smtpPort(),
OutSecure: oDomain.smtpSecure(),
OutAuth: oDomain.smtpAuth() ? 1 : 0,
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
testContacts(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminContactsTest', oData);
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
saveNewAdminPassword(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminPasswordUpdate', oData);
saveSetting(key, value, fCallback) {
this.request('AdminSettingsUpdate', fCallback, {[key]: value});
}
}

View file

@ -1,11 +1,9 @@
import { isArray, isNonEmptyArray, pString, pInt } from 'Common/Utils';
import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
import {
getFolderHash,
getFolderInboxName,
getFolderUidNext,
getFolderFromCacheList,
MessageFlagsCache
getFolderFromCacheList
} from 'Common/Cache';
import { SettingsGet } from 'Common/Globals';
@ -17,224 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
import { FolderCollectionModel } from 'Model/FolderCollection';
//const toUTF8 = window.TextEncoder
// ? text => String.fromCharCode(...new TextEncoder().encode(text))
// : text => unescape(encodeURIComponent(text)),
const urlsafeArray = array => btoa(unescape(encodeURIComponent(array.join('\x00').replace(/\r\n/g, '\n'))))
.replace('+', '-')
.replace('/', '_')
.replace('=', '');
class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
*/
folders(fCallback) {
this.defaultRequest(
fCallback,
'Folders',
{
SentFolder: SettingsGet('SentFolder'),
DraftFolder: SettingsGet('DraftFolder'),
SpamFolder: SettingsGet('SpamFolder'),
TrashFolder: SettingsGet('TrashFolder'),
ArchiveFolder: SettingsGet('ArchiveFolder')
},
null,
'',
['Folders']
);
}
/**
* @param {?Function} fCallback
* @param {string} sEmail
* @param {string} sLogin
* @param {string} sPassword
* @param {boolean} bSignMe
* @param {string=} sLanguage
*/
login(fCallback, sEmail, sPassword, bSignMe, sLanguage) {
this.defaultRequest(fCallback, 'Login', {
Email: sEmail,
Login: '',
Password: sPassword,
Language: sLanguage || '',
SignMe: bSignMe ? 1 : 0
});
}
/**
* @param {?Function} fCallback
*/
contactsSync(fCallback) {
this.defaultRequest(fCallback, 'ContactsSync', null, 200000);
}
/**
* @param {?Function} fCallback
* @param {boolean} bEnable
* @param {string} sUrl
* @param {string} sUser
* @param {string} sPassword
*/
saveContactsSyncData(fCallback, bEnable, sUrl, sUser, sPassword) {
this.defaultRequest(fCallback, 'SaveContactsSyncData', {
Enable: bEnable ? 1 : 0,
Url: sUrl,
User: sUser,
Password: sPassword
});
}
/**
* @param {?Function} fCallback
* @param {string} sEmail
* @param {string} sPassword
* @param {boolean=} bNew
*/
accountSetup(fCallback, sEmail, sPassword, bNew = true) {
this.defaultRequest(fCallback, 'AccountSetup', {
Email: sEmail,
Password: sPassword,
New: bNew ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sEmailToDelete
*/
accountDelete(fCallback, sEmailToDelete) {
this.defaultRequest(fCallback, 'AccountDelete', {
EmailToDelete: sEmailToDelete
});
}
/**
* @param {?Function} fCallback
* @param {Array} aAccounts
* @param {Array} aIdentities
*/
accountsAndIdentitiesSortOrder(fCallback, aAccounts, aIdentities) {
this.defaultRequest(fCallback, 'AccountsAndIdentitiesSortOrder', {
Accounts: aAccounts,
Identities: aIdentities
});
}
/**
* @param {?Function} fCallback
* @param {string} sId
* @param {string} sEmail
* @param {string} sName
* @param {string} sReplyTo
* @param {string} sBcc
* @param {string} sSignature
* @param {boolean} bSignatureInsertBefore
*/
identityUpdate(fCallback, sId, sEmail, sName, sReplyTo, sBcc, sSignature, bSignatureInsertBefore) {
this.defaultRequest(fCallback, 'IdentityUpdate', {
Id: sId,
Email: sEmail,
Name: sName,
ReplyTo: sReplyTo,
Bcc: sBcc,
Signature: sSignature,
SignatureInsertBefore: bSignatureInsertBefore ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sIdToDelete
*/
identityDelete(fCallback, sIdToDelete) {
this.defaultRequest(fCallback, 'IdentityDelete', {
IdToDelete: sIdToDelete
});
}
/**
* @param {?Function} fCallback
*/
accountsAndIdentities(fCallback) {
this.defaultRequest(fCallback, 'AccountsAndIdentities');
}
/**
* @param {?Function} fCallback
* @param {SieveScriptModel} script
*/
filtersScriptSave(fCallback, script) {
this.defaultRequest(fCallback, 'FiltersScriptSave', script.toJson());
}
/**
* @param {?Function} fCallback
* @param {string} name
*/
filtersScriptActivate(fCallback, name) {
this.defaultRequest(fCallback, 'FiltersScriptActivate', {name:name});
}
/**
* @param {?Function} fCallback
* @param {string} name
*/
filtersScriptDelete(fCallback, name) {
this.defaultRequest(fCallback, 'FiltersScriptDelete', {name:name});
}
/**
* @param {?Function} fCallback
*/
filtersGet(fCallback) {
this.defaultRequest(fCallback, 'Filters', {});
}
/**
* @param {?Function} fCallback
*/
templates(fCallback) {
this.defaultRequest(fCallback, 'Templates', {});
}
/**
* @param {Function} fCallback
* @param {string} sID
*/
templateGetById(fCallback, sID) {
this.defaultRequest(fCallback, 'TemplateGetByID', {
ID: sID
});
}
/**
* @param {Function} fCallback
* @param {string} sID
*/
templateDelete(fCallback, sID) {
this.defaultRequest(fCallback, 'TemplateDelete', {
IdToDelete: sID
});
}
/**
* @param {Function} fCallback
* @param {string} sID
* @param {string} sName
* @param {string} sBody
*/
templateSetup(fCallback, sID, sName, sBody) {
this.defaultRequest(fCallback, 'TemplateSetup', {
ID: sID,
Name: sName,
Body: sBody
});
}
/**
* @param {Function} fCallback
@ -243,23 +24,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
*/
messageList(fCallback, params, bSilent = false) {
const
sFolderFullNameRaw = pString(params.Folder),
folderHash = getFolderHash(sFolderFullNameRaw),
useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
sFolderFullName = pString(params.Folder),
folderHash = getFolderHash(sFolderFullName);
params.Folder = sFolderFullNameRaw;
params.ThreadUid = useThreads ? params.ThreadUid : '';
params = Object.assign({
Folder: '',
Offset: 0,
Limit: SettingsUserStore.messagesPerPage(),
Search: '',
UidNext: inboxUidNext,
UseThreads: useThreads,
ThreadUid: '',
Sort: FolderUserStore.sortMode()
UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
Sort: FolderUserStore.sortMode(),
Hash: folderHash + SettingsGet('AccountHash')
}, params);
params.Folder = sFolderFullName;
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.UseThreads = 1;
} else {
params.ThreadUid = 0;
}
let sGetAdd = '';
@ -267,13 +48,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
sGetAdd = 'MessageList/' +
SUB_QUERY_PREFIX +
'/' +
urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params)));
b64EncodeJSONSafe(params);
params = {};
}
this.defaultRequest(
this.request('MessageList',
fCallback,
'MessageList',
params,
30000,
sGetAdd,
@ -283,43 +63,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
* @param {Array} aDownloads
*/
messageUploadAttachments(fCallback, aDownloads) {
this.defaultRequest(
fCallback,
'MessageUploadAttachments',
{
Attachments: aDownloads
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {string} sFolderFullName
* @param {number} iUid
* @returns {boolean}
*/
message(fCallback, sFolderFullNameRaw, iUid) {
sFolderFullNameRaw = pString(sFolderFullNameRaw);
message(fCallback, sFolderFullName, iUid) {
sFolderFullName = pString(sFolderFullName);
iUid = pInt(iUid);
if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) {
this.defaultRequest(
if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
this.request('Message',
fCallback,
'Message',
{},
null,
'Message/' +
SUB_QUERY_PREFIX +
'/' +
urlsafeArray([
sFolderFullNameRaw,
b64EncodeJSONSafe([
sFolderFullName,
iUid,
SettingsGet('ProjectHash'),
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
SettingsGet('AccountHash')
]),
['Message']
);
@ -330,186 +94,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
return false;
}
/**
* @param {?Function} fCallback
* @param {Array} aExternals
*/
composeUploadExternals(fCallback, aExternals) {
this.defaultRequest(
fCallback,
'ComposeUploadExternals',
{
Externals: aExternals
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sUrl
* @param {string} sAccessToken
*/
composeUploadDrive(fCallback, sUrl, sAccessToken) {
this.defaultRequest(
fCallback,
'ComposeUploadDrive',
{
AccessToken: sAccessToken,
Url: sUrl
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation(fCallback, folder, list = []) {
let request = true;
const uids = [];
if (isNonEmptyArray(list)) {
request = false;
list.forEach(messageListItem => {
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
uids.push(messageListItem.uid);
}
if (messageListItem.threads.length) {
messageListItem.threads.forEach(uid => {
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
uids.push(uid);
}
});
}
});
if (uids.length) {
request = true;
}
}
if (request) {
this.defaultRequest(fCallback, 'FolderInformation', {
Folder: folder,
FlagsUids: isArray(uids) ? uids.join(',') : '',
UidNext: getFolderInboxName() === folder ? getFolderUidNext(folder) : ''
});
} else if (SettingsUserStore.useThreads()) {
rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
}
}
/**
* @param {?Function} fCallback
* @param {Array} aFolders
*/
folderInformationMultiply(fCallback, aFolders) {
this.defaultRequest(fCallback, 'FolderInformationMultiply', {
Folders: aFolders
});
}
/**
* @param {?Function} fCallback
*/
logout(fCallback) {
this.defaultRequest(fCallback, 'Logout');
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {Array} aUids
* @param {boolean} bSetFlagged
*/
messageSetFlagged(fCallback, sFolderFullNameRaw, aUids, bSetFlagged) {
this.defaultRequest(fCallback, 'MessageSetFlagged', {
Folder: sFolderFullNameRaw,
Uids: aUids.join(','),
SetAction: bSetFlagged ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {Array} aUids
* @param {boolean} bSetSeen
*/
messageSetSeen(fCallback, sFolderFullNameRaw, aUids, bSetSeen) {
this.defaultRequest(fCallback, 'MessageSetSeen', {
Folder: sFolderFullNameRaw,
Uids: aUids.join(','),
SetAction: bSetSeen ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bSetSeen
* @param {Array} aThreadUids = null
*/
messageSetSeenToAll(fCallback, sFolderFullNameRaw, bSetSeen, aThreadUids = null) {
this.defaultRequest(fCallback, 'MessageSetSeenToAll', {
Folder: sFolderFullNameRaw,
SetAction: bSetSeen ? 1 : 0,
ThreadUids: aThreadUids ? aThreadUids.join(',') : ''
});
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
saveMessage(fCallback, oData) {
this.defaultRequest(fCallback, 'SaveMessage', oData, 200000);
}
/**
* @param {?Function} fCallback
* @param {string} sMessageFolder
* @param {string} sMessageUid
* @param {string} sReadReceipt
* @param {string} sSubject
* @param {string} sText
*/
sendReadReceiptMessage(fCallback, sMessageFolder, sMessageUid, sReadReceipt, sSubject, sText) {
this.defaultRequest(fCallback, 'SendReadReceiptMessage', {
MessageFolder: sMessageFolder,
MessageUid: sMessageUid,
ReadReceipt: sReadReceipt,
Subject: sSubject,
Text: sText
});
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
sendMessage(fCallback, oData) {
this.defaultRequest(fCallback, 'SendMessage', oData, 30000);
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
saveSystemFolders(fCallback, oData) {
this.defaultRequest(fCallback, 'SystemFoldersUpdate', oData);
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
saveSettings(fCallback, oData) {
this.defaultRequest(fCallback, 'SettingsUpdate', oData);
this.request('SettingsUpdate', fCallback, oData);
}
/**
@ -523,234 +113,15 @@ class RemoteUserFetch extends AbstractFetchRemote {
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
*/
folderClear(fCallback, sFolderFullNameRaw) {
this.defaultRequest(fCallback, 'FolderClear', {
Folder: sFolderFullNameRaw
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bSubscribe
*/
folderSetSubscribe(fCallback, sFolderFullNameRaw, bSubscribe) {
this.defaultRequest(fCallback, 'FolderSubscribe', {
Folder: sFolderFullNameRaw,
/*
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullName,
NewFolder: sNewFolderFullName,
Subscribe: bSubscribe ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bCheckable
*/
folderSetCheckable(fCallback, sFolderFullNameRaw, bCheckable) {
this.defaultRequest(fCallback, 'FolderCheckable', {
Folder: sFolderFullNameRaw,
Checkable: bCheckable ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {string} sToFolder
* @param {Array} aUids
* @param {string=} sLearning
* @param {boolean=} bMarkAsRead
*/
messagesMove(fCallback, sFolder, sToFolder, aUids, sLearning, bMarkAsRead) {
this.defaultRequest(
fCallback,
'MessageMove',
{
FromFolder: sFolder,
ToFolder: sToFolder,
Uids: aUids.join(','),
MarkAsRead: bMarkAsRead ? 1 : 0,
Learning: sLearning || ''
},
null,
'',
['MessageList']
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {string} sToFolder
* @param {Array} aUids
*/
messagesCopy(fCallback, sFolder, sToFolder, aUids) {
this.defaultRequest(fCallback, 'MessageCopy', {
FromFolder: sFolder,
ToFolder: sToFolder,
Uids: aUids.join(',')
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {Array} aUids
*/
messagesDelete(fCallback, sFolder, aUids) {
this.defaultRequest(
fCallback,
'MessageDelete',
{
Folder: sFolder,
Uids: aUids.join(',')
},
null,
'',
['MessageList']
);
}
/**
* @param {?Function} fCallback
*/
appDelayStart(fCallback) {
this.defaultRequest(fCallback, 'AppDelayStart');
}
/**
* @param {?Function} fCallback
*/
quota(fCallback) {
this.defaultRequest(fCallback, 'Quota');
}
/**
* @param {?Function} fCallback
* @param {number} iOffset
* @param {number} iLimit
* @param {string} sSearch
*/
contacts(fCallback, iOffset, iLimit, sSearch) {
this.defaultRequest(
fCallback,
'Contacts',
{
Offset: iOffset,
Limit: iLimit,
Search: sSearch
},
null,
'',
['Contacts']
);
}
/**
* @param {?Function} fCallback
* @param {string} sRequestUid
* @param {string} sUid
* @param {Array} aProperties
*/
contactSave(fCallback, sRequestUid, sUid, aProperties) {
this.defaultRequest(fCallback, 'ContactSave', {
RequestUid: sRequestUid,
Uid: sUid,
Properties: aProperties
});
}
/**
* @param {?Function} fCallback
* @param {Array} aUids
*/
contactsDelete(fCallback, aUids) {
this.defaultRequest(fCallback, 'ContactsDelete', {
Uids: aUids.join(',')
});
}
/**
* @param {?Function} fCallback
* @param {string} sQuery
* @param {number} iPage
*/
suggestions(fCallback, sQuery, iPage) {
this.defaultRequest(
fCallback,
'Suggestions',
{
Query: sQuery,
Page: iPage
},
null,
'',
['Suggestions']
);
}
/**
* @param {?Function} fCallback
*/
clearUserBackground(fCallback) {
this.defaultRequest(fCallback, 'ClearUserBackground');
}
foldersReload(fCallback) {
this.abort('Folders')
.postRequest('Folders', FolderUserStore.foldersLoading)
.then(data => {
data = FolderCollectionModel.reviveFromJson(data.Result);
data && data.storeIt();
fCallback && fCallback(true);
})
.catch(() => fCallback && setTimeout(() => fCallback(false), 1));
}
foldersReloadWithTimeout() {
this.setTrigger(FolderUserStore.foldersLoading, true);
clearTimeout(this.foldersTimeout);
this.foldersTimeout = setTimeout(() => this.foldersReload(), 500);
}
folderDelete(sFolderFullNameRaw) {
return this.postRequest('FolderDelete', FolderUserStore.foldersDeleting, {
Folder: sFolderFullNameRaw
});
}
folderCreate(sNewFolderName, sParentName) {
return this.postRequest('FolderCreate', FolderUserStore.foldersCreating, {
Folder: sNewFolderName,
Parent: sParentName
});
}
folderMove(sPrevFolderFullNameRaw, sNewFolderFullName) {
return this.postRequest('FolderMove', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullNameRaw,
NewFolder: sNewFolderFullName
});
}
folderRename(sPrevFolderFullNameRaw, sNewFolderName) {
return this.postRequest('FolderRename', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullNameRaw,
NewFolderName: sNewFolderName
});
}
attachmentsActions(sAction, aHashes, fTrigger) {
return this.postRequest('AttachmentsActions', fTrigger, {
Do: sAction,
Hashes: aHashes
});
}
*/
}
export default new RemoteUserFetch();

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { pString } from 'Common/Utils';
import { settings } from 'Common/Links';
import { doc, elementById } from 'Common/Globals';
import { createElement, elementById } from 'Common/Globals';
import { AbstractScreen } from 'Knoin/AbstractScreen';
@ -18,52 +18,38 @@ export class AbstractSettingsScreen extends AbstractScreen {
this.menu = ko.observableArray();
this.oCurrentSubScreen = null;
this.setupSettings();
}
/**
* @param {Function=} fCallback
*/
setupSettings(fCallback = null) {
fCallback && fCallback();
}
onRoute(subName) {
let settingsScreen = null,
RoutedSettingsViewModel = null,
viewModelDom = null;
RoutedSettingsViewModel = VIEW_MODELS.find(
SettingsViewModel =>
SettingsViewModel && SettingsViewModel.__rlSettingsData && subName === SettingsViewModel.__rlSettingsData.Route
);
viewModelDom = null,
RoutedSettingsViewModel = VIEW_MODELS.find(
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
);
if (RoutedSettingsViewModel) {
if (RoutedSettingsViewModel.__builded && RoutedSettingsViewModel.__vm) {
if (RoutedSettingsViewModel.__vm) {
settingsScreen = RoutedSettingsViewModel.__vm;
} else {
const vmPlace = elementById('rl-settings-subscreen');
if (vmPlace) {
settingsScreen = new RoutedSettingsViewModel();
viewModelDom = Element.fromHTML('<div class="rl-settings-view-model" hidden=""></div>');
viewModelDom = createElement('div',{
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
hidden: ''
})
vmPlace.append(viewModelDom);
settingsScreen = new RoutedSettingsViewModel();
settingsScreen.viewModelDom = viewModelDom;
settingsScreen.__rlSettingsData = RoutedSettingsViewModel.__rlSettingsData;
RoutedSettingsViewModel.__dom = viewModelDom;
RoutedSettingsViewModel.__builded = true;
RoutedSettingsViewModel.__vm = settingsScreen;
const tmpl = { name: RoutedSettingsViewModel.__rlSettingsData.Template };
ko.applyBindingAccessorsToNode(
viewModelDom,
{
i18nInit: true,
template: () => tmpl
template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
},
settingsScreen
);
@ -75,75 +61,55 @@ export class AbstractSettingsScreen extends AbstractScreen {
}
if (settingsScreen) {
const o = this;
setTimeout(() => {
// hide
if (o.oCurrentSubScreen) {
o.oCurrentSubScreen.onHide && o.oCurrentSubScreen.onHide();
o.oCurrentSubScreen.viewModelDom.hidden = true;
}
this.onHide();
// --
o.oCurrentSubScreen = settingsScreen;
this.oCurrentSubScreen = settingsScreen;
// show
if (o.oCurrentSubScreen) {
o.oCurrentSubScreen.onBeforeShow && o.oCurrentSubScreen.onBeforeShow();
o.oCurrentSubScreen.viewModelDom.hidden = false;
o.oCurrentSubScreen.onShow && o.oCurrentSubScreen.onShow();
settingsScreen.beforeShow && settingsScreen.beforeShow();
settingsScreen.viewModelDom.hidden = false;
settingsScreen.onShow && settingsScreen.onShow();
o.menu.forEach(item => {
item.selected(
settingsScreen &&
settingsScreen.__rlSettingsData &&
item.route === settingsScreen.__rlSettingsData.Route
);
});
this.menu.forEach(item => {
item.selected(
item.route === RoutedSettingsViewModel.__rlSettingsData.route
);
});
doc.querySelector('#rl-content .b-settings .b-content').scrollTop = 0;
}
(elementById('rl-settings-subscreen') || {}).scrollTop = 0;
// --
}, 1);
}
} else {
rl.route.setHash(settings(), false, true);
hasher.replaceHash(settings());
}
}
onHide() {
if (this.oCurrentSubScreen && this.oCurrentSubScreen.viewModelDom) {
this.oCurrentSubScreen.onHide && this.oCurrentSubScreen.onHide();
this.oCurrentSubScreen.viewModelDom.hidden = true;
let subScreen = this.oCurrentSubScreen;
if (subScreen) {
subScreen.onHide && subScreen.onHide();
subScreen.viewModelDom.hidden = true;
}
}
onBuild() {
VIEW_MODELS.forEach(SettingsViewModel => {
if (
SettingsViewModel &&
SettingsViewModel.__rlSettingsData
) {
this.menu.push({
route: SettingsViewModel.__rlSettingsData.Route,
label: SettingsViewModel.__rlSettingsData.Label,
selected: ko.observable(false),
disabled: false
});
}
});
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
}
routes() {
const DefaultViewModel = VIEW_MODELS.find(
SettingsViewModel =>
SettingsViewModel && SettingsViewModel.__rlSettingsData && SettingsViewModel.__rlSettingsData.IsDefault
SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
),
defaultRoute =
DefaultViewModel && DefaultViewModel.__rlSettingsData ? DefaultViewModel.__rlSettingsData.Route : 'general',
DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
rules = {
subname: /^(.*)$/,
normalize_: (rquest, vals) => {
vals.subname = undefined === vals.subname ? defaultRoute : pString(vals.subname);
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
return [vals.subname];
}
};
@ -165,11 +131,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
* @returns {void}
*/
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
SettingsViewModelClass.__rlSettingsData = {
Label: labelName,
Template: template,
Route: route,
IsDefault: !!isDefault
label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
route: route || name.toLowerCase(),
selected: ko.observable(false),
template: template || SettingsViewModelClass.name,
isDefault: !!isDefault
};
VIEW_MODELS.push(SettingsViewModelClass);

View file

@ -1,10 +1,10 @@
import { AbstractScreen } from 'Knoin/AbstractScreen';
import { LoginAdminView } from 'View/Admin/Login';
import { AdminLoginView } from 'View/Admin/Login';
export class LoginAdminScreen extends AbstractScreen {
constructor() {
super('login', [LoginAdminView]);
super('login', [AdminLoginView]);
}
onShow() {

View file

@ -2,15 +2,15 @@ import { runSettingsViewModelHooks } from 'Common/Plugins';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
import { GeneralAdminSettings } from 'Settings/Admin/General';
import { DomainsAdminSettings } from 'Settings/Admin/Domains';
import { LoginAdminSettings } from 'Settings/Admin/Login';
import { ContactsAdminSettings } from 'Settings/Admin/Contacts';
import { SecurityAdminSettings } from 'Settings/Admin/Security';
import { PluginsAdminSettings } from 'Settings/Admin/Plugins';
import { PackagesAdminSettings } from 'Settings/Admin/Packages';
import { AboutAdminSettings } from 'Settings/Admin/About';
import { BrandingAdminSettings } from 'Settings/Admin/Branding';
import { AdminSettingsGeneral } from 'Settings/Admin/General';
import { AdminSettingsDomains } from 'Settings/Admin/Domains';
import { AdminSettingsLogin } from 'Settings/Admin/Login';
import { AdminSettingsContacts } from 'Settings/Admin/Contacts';
import { AdminSettingsSecurity } from 'Settings/Admin/Security';
import { AdminSettingsPackages } from 'Settings/Admin/Packages';
import { AdminSettingsAbout } from 'Settings/Admin/About';
import { AdminSettingsBranding } from 'Settings/Admin/Branding';
import { AdminSettingsConfig } from 'Settings/Admin/Config';
import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu';
import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
@ -18,41 +18,22 @@ import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
export class SettingsAdminScreen extends AbstractSettingsScreen {
constructor() {
super([MenuSettingsAdminView, PaneSettingsAdminView]);
}
/**
* @param {Function=} fCallback = null
*/
setupSettings(fCallback = null) {
settingsAddViewModel(
GeneralAdminSettings,
'AdminSettingsGeneral',
'TABS_LABELS/LABEL_GENERAL_NAME',
'general',
true
);
[
[DomainsAdminSettings, 'Domains'],
[LoginAdminSettings, 'Login'],
[BrandingAdminSettings, 'Branding'],
[ContactsAdminSettings, 'Contacts'],
[SecurityAdminSettings, 'Security'],
[PluginsAdminSettings, 'Plugins'],
[PackagesAdminSettings, 'Packages'],
[AboutAdminSettings, 'About'],
].forEach(item =>
settingsAddViewModel(
item[0],
'AdminSettings'+item[1],
'TABS_LABELS/LABEL_'+item[1].toUpperCase()+'_NAME',
item[1].toLowerCase()
)
AdminSettingsGeneral,
AdminSettingsDomains,
AdminSettingsLogin,
AdminSettingsBranding,
AdminSettingsContacts,
AdminSettingsSecurity,
AdminSettingsPackages,
AdminSettingsConfig,
AdminSettingsAbout
].forEach((item, index) =>
settingsAddViewModel(item, 0, 0, 0, 0 === index)
);
runSettingsViewModelHooks(true);
fCallback && fCallback();
}
onShow() {

View file

@ -1,33 +1,32 @@
import { Scope } from 'Common/Enums';
import { doc, leftPanelDisabled, moveAction, Settings } from 'Common/Globals';
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
import { doc, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils';
import { getFolderFromCacheList, getFolderFullNameRaw, getFolderInboxName } from 'Common/Cache';
import { setLayoutResizer } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { i18n } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import { SystemDropDownMailBoxUserView } from 'View/User/MailBox/SystemDropDown';
import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList';
import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList';
import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView';
import { warmUpScreenPopup } from 'Knoin/Knoin';
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { MailFolderList } from 'View/User/MailBox/FolderList';
import { MailMessageList } from 'View/User/MailBox/MessageList';
import { MailMessageView } from 'View/User/MailBox/MessageView';
import { AbstractScreen } from 'Knoin/AbstractScreen';
import { ComposePopupView } from 'View/Popup/Compose';
export class MailBoxUserScreen extends AbstractScreen {
constructor() {
super('mailbox', [
SystemDropDownMailBoxUserView,
FolderListMailBoxUserView,
MessageListMailBoxUserView,
MessageViewMailBoxUserView
SystemDropDownUserView,
MailFolderList,
MailMessageList,
MailMessageView
]);
}
@ -52,7 +51,7 @@ export class MailBoxUserScreen extends AbstractScreen {
onShow() {
this.updateWindowTitle();
AppUserStore.focusedState(Scope.None);
AppUserStore.focusedState('none');
AppUserStore.focusedState(Scope.MessageList);
ThemeStore.isMobile() && leftPanelDisabled(true);
@ -65,20 +64,17 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onRoute(folderHash, page, search) {
const folder = getFolderFromCacheList(getFolderFullNameRaw(folderHash.replace(/~([\d]+)$/, '')));
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
if (folder) {
let threadUid = folderHash.replace(/^.+~([\d]+)$/, '$1');
if (folderHash === threadUid) {
threadUid = '';
}
let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
FolderUserStore.currentFolder(folder);
MessageUserStore.listPage(1 > page ? 1 : page);
MessageUserStore.listSearch(search);
MessageUserStore.listThreadUid(threadUid);
MessagelistUserStore.page(1 > page ? 1 : page);
MessagelistUserStore.listSearch(search);
MessagelistUserStore.threadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
rl.app.reloadMessageList();
MessagelistUserStore.reload();
}
}
@ -86,28 +82,41 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onStart() {
if (!this.__started) {
super.onStart();
setTimeout(() => warmUpScreenPopup(ComposePopupView), 500);
super.onStart();
addEventListener('mailbox.inbox-unread-count', e => {
FolderUserStore.foldersInboxUnreadCount(e.detail);
const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item =>
item && email === item.email && item.count(e.detail)
);
this.updateWindowTitle();
});
}
addEventListener('mailbox.inbox-unread-count', e => {
FolderUserStore.foldersInboxUnreadCount(e.detail);
/* // Disabled in SystemDropDown.html
const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item =>
item && email === item.email && item.count(e.detail)
);
*/
this.updateWindowTitle();
});
}
/**
* @returns {void}
*/
onBuild() {
setTimeout(() => rl.app.initHorizontalLayoutResizer(), 1);
setTimeout(() => {
// initMailboxLayoutResizer
const top = elementById('V-MailMessageList'),
bottom = elementById('V-MailMessageView'),
fToggle = () => {
let layout = SettingsUserStore.layout();
setLayoutResizer(top, bottom, ClientSideKeyNameMessageListSize,
(ThemeStore.isMobile() || Layout.NoPreview === layout)
? 0
: (Layout.SidePreview === layout ? 'Width' : 'Height')
);
};
if (top && bottom) {
fToggle();
addEventListener('rl-layout', fToggle);
}
}, 1);
doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false)
@ -115,19 +124,23 @@ export class MailBoxUserScreen extends AbstractScreen {
}
/**
* Parse link as generated by mailBox()
* @returns {Array}
*/
routes() {
const
folder = (request, vals) => request ? decodeURI(pString(vals[0])) : getFolderInboxName(),
folder = (request, vals) => request ? pString(vals[0]) : getFolderInboxName(),
fNormS = (request, vals) => [folder(request, vals), request ? pInt(vals[1]) : 1, decodeURI(pString(vals[2]))];
return [
// Folder: INBOX | INBOX.sub | Sent | fullNameHash
[/^([^/]*)$/, { normalize_: fNormS }],
[/^([a-zA-Z0-9~]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
// Search: {folder}/{string}
[/^([a-zA-Z0-9.~_-]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
[folder(request, vals), 1, decodeURI(pString(vals[1]))]
}],
[/^([a-zA-Z0-9~]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }]
// Page: {folder}/p{int}(/{search})?
[/^([a-zA-Z0-9.~_-]+)\/p([1-9][0-9]*)(?:\/(.+))?$/, { normalize_: fNormS }]
];
}
}

View file

@ -1,5 +1,5 @@
import { Capa, Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, Settings } from 'Common/Globals';
import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
@ -9,23 +9,53 @@ import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
import { GeneralUserSettings } from 'Settings/User/General';
import { ContactsUserSettings } from 'Settings/User/Contacts';
import { AccountsUserSettings } from 'Settings/User/Accounts';
import { FiltersUserSettings } from 'Settings/User/Filters';
import { SecurityUserSettings } from 'Settings/User/Security';
import { TemplatesUserSettings } from 'Settings/User/Templates';
import { FoldersUserSettings } from 'Settings/User/Folders';
import { ThemesUserSettings } from 'Settings/User/Themes';
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
import { UserSettingsGeneral } from 'Settings/User/General';
import { UserSettingsContacts } from 'Settings/User/Contacts';
import { UserSettingsAccounts } from 'Settings/User/Accounts';
import { UserSettingsFilters } from 'Settings/User/Filters';
import { UserSettingsSecurity } from 'Settings/User/Security';
import { UserSettingsFolders } from 'Settings/User/Folders';
import { UserSettingsThemes } from 'Settings/User/Themes';
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown';
import { MenuSettingsUserView } from 'View/User/Settings/Menu';
import { PaneSettingsUserView } from 'View/User/Settings/Pane';
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { SettingsMenuUserView } from 'View/User/Settings/Menu';
import { SettingsPaneUserView } from 'View/User/Settings/Pane';
export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() {
super([SystemDropDownSettingsUserView, MenuSettingsUserView, PaneSettingsUserView]);
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
const views = [
UserSettingsGeneral
];
if (AppUserStore.allowContacts()) {
views.push(UserSettingsContacts);
}
if (SettingsCapa('AdditionalAccounts') || SettingsCapa('Identities')) {
views.push(UserSettingsAccounts);
}
if (SettingsCapa('Sieve')) {
views.push(UserSettingsFilters);
}
if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) {
views.push(UserSettingsSecurity);
}
views.push(UserSettingsFolders);
if (SettingsCapa('Themes')) {
views.push(UserSettingsThemes);
}
views.forEach((item, index) =>
settingsAddViewModel(item, item.name.replace('User', ''), 0, 0, 0 === index)
);
runSettingsViewModelHooks(false);
initOnStartOrLangChange(
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
@ -33,67 +63,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
);
}
/**
* @param {Function=} fCallback
*/
setupSettings(fCallback = null) {
if (!Settings.capa(Capa.Settings)) {
fCallback && fCallback();
return false;
}
settingsAddViewModel(GeneralUserSettings, 'SettingsGeneral', 'SETTINGS_LABELS/LABEL_GENERAL_NAME', 'general', true);
if (AppUserStore.allowContacts()) {
settingsAddViewModel(ContactsUserSettings, 'SettingsContacts', 'SETTINGS_LABELS/LABEL_CONTACTS_NAME', 'contacts');
}
if (Settings.capa(Capa.AdditionalAccounts) || Settings.capa(Capa.Identities)) {
settingsAddViewModel(
AccountsUserSettings,
'SettingsAccounts',
Settings.capa(Capa.AdditionalAccounts)
? 'SETTINGS_LABELS/LABEL_ACCOUNTS_NAME'
: 'SETTINGS_LABELS/LABEL_IDENTITIES_NAME',
'accounts'
);
}
if (Settings.capa(Capa.Sieve)) {
settingsAddViewModel(FiltersUserSettings, 'SettingsFilters', 'SETTINGS_LABELS/LABEL_FILTERS_NAME', 'filters');
}
if (Settings.capa(Capa.AutoLogout)) {
settingsAddViewModel(SecurityUserSettings, 'SettingsSecurity', 'SETTINGS_LABELS/LABEL_SECURITY_NAME', 'security');
}
if (Settings.capa(Capa.Templates)) {
settingsAddViewModel(
TemplatesUserSettings,
'SettingsTemplates',
'SETTINGS_LABELS/LABEL_TEMPLATES_NAME',
'templates'
);
}
settingsAddViewModel(FoldersUserSettings, 'SettingsFolders', 'SETTINGS_LABELS/LABEL_FOLDERS_NAME', 'folders');
if (Settings.capa(Capa.Themes)) {
settingsAddViewModel(ThemesUserSettings, 'SettingsThemes', 'SETTINGS_LABELS/LABEL_THEMES_NAME', 'themes');
}
if (Settings.capa(Capa.OpenPGP)) {
settingsAddViewModel(OpenPgpUserSettings, 'SettingsOpenPGP', 'OpenPGP', 'openpgp');
}
runSettingsViewModelHooks(false);
fCallback && fCallback();
return true;
}
onShow() {
this.setSettingsTitle();
keyScope(Scope.Settings);

View file

@ -1,8 +1,15 @@
import ko from 'ko';
import { Settings } from 'Common/Globals';
import Remote from 'Remote/Admin/Fetch';
export class AboutAdminSettings {
export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
constructor() {
this.version = ko.observable(Settings.app('version'));
this.version = Settings.app('version');
this.phpextensions = ko.observableArray();
}
onBuild() {
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
}
}

View file

@ -1,32 +1,10 @@
import ko from 'ko';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { SettingsGet } from 'Common/Globals';
import { settingsSaveHelperSimpleFunction } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
export class BrandingAdminSettings {
export class AdminSettingsBranding extends AbstractViewSettings {
constructor() {
this.title = ko.observable(SettingsGet('Title')).idleTrigger();
this.loadingDesc = ko.observable(SettingsGet('LoadingDescription')).idleTrigger();
this.faviconUrl = ko.observable(SettingsGet('FaviconUrl')).idleTrigger();
this.title.subscribe(value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.title.trigger, this), {
Title: value.trim()
})
);
this.loadingDesc.subscribe(value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.loadingDesc.trigger, this), {
LoadingDescription: value.trim()
})
);
this.faviconUrl.subscribe(value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.faviconUrl.trigger, this), {
FaviconUrl: value.trim()
})
);
super();
this.addSetting('Title');
this.addSetting('LoadingDescription');
this.addSetting('FaviconUrl');
}
}

View file

@ -0,0 +1,48 @@
import ko from 'ko';
import Remote from 'Remote/Admin/Fetch';
import { forEachObjectEntry } from 'Common/Utils';
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
constructor() {
this.config = ko.observableArray();
}
beforeShow() {
Remote.request('AdminSettingsGet', (iError, data) => {
if (!iError) {
const cfg = [],
getInputType = (value, pass) => {
switch (typeof value)
{
case 'boolean': return 'checkbox';
case 'number': return 'number';
}
return pass ? 'password' : 'text';
};
forEachObjectEntry(data.Result, (key, items) => {
const section = {
name: key,
items: []
};
forEachObjectEntry(items, (skey, item) => {
section.items.push({
key: `config[${key}][${skey}]`,
name: skey,
value: item[0],
type: getInputType(item[0], skey.includes('password')),
comment: item[1]
});
});
cfg.push(section);
});
this.config(cfg);
}
});
}
saveConfig(form) {
Remote.post('AdminSettingsSet', null, new FormData(form));
}
}

View file

@ -1,35 +1,30 @@
import ko from 'ko';
import { SaveSettingsStep } from 'Common/Enums';
import { SettingsGet } from 'Common/Globals';
import {
settingsSaveHelperSimpleFunction,
defaultOptionsAfterRender,
addObservablesTo,
addSubscribablesTo
} from 'Common/Utils';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { addObservablesTo } from 'External/ko';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class ContactsAdminSettings {
export class AdminSettingsContacts extends AbstractViewSettings {
constructor() {
super();
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.addSetting('ContactsPdoDsn');
this.addSetting('ContactsPdoUser');
this.addSetting('ContactsPdoPassword');
this.addSetting('ContactsPdoType', () => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
});
this.addSettings(['ContactsEnable','ContactsSync']);
addObservablesTo(this, {
enableContacts: !!SettingsGet('ContactsEnable'),
contactsSync: !!SettingsGet('ContactsSync'),
contactsType: SettingsGet('ContactsPdoType'),
pdoDsn: SettingsGet('ContactsPdoDsn'),
pdoUser: SettingsGet('ContactsPdoUser'),
pdoPassword: SettingsGet('ContactsPdoPassword'),
pdoDsnTrigger: SaveSettingsStep.Idle,
pdoUserTrigger: SaveSettingsStep.Idle,
pdoPasswordTrigger: SaveSettingsStep.Idle,
contactsTypeTrigger: SaveSettingsStep.Idle,
testing: false,
testContactsSuccess: false,
testContactsError: false,
@ -54,61 +49,23 @@ export class ContactsAdminSettings {
this.mainContactsType = ko
.computed({
read: this.contactsType,
read: this.contactsPdoType,
write: value => {
if (value !== this.contactsType()) {
if (value !== this.contactsPdoType()) {
if (supportedTypes.includes(value)) {
this.contactsType(value);
this.contactsPdoType(value);
} else if (types.length) {
this.contactsType('');
this.contactsPdoType('');
}
} else {
this.contactsType.valueHasMutated();
this.contactsPdoType.valueHasMutated();
}
}
})
.extend({ notify: 'always' });
addSubscribablesTo(this, {
enableContacts: value =>
Remote.saveAdminConfig(null, {
ContactsEnable: value ? 1 : 0
}),
contactsSync: value =>
Remote.saveAdminConfig(null, {
ContactsSync: value ? 1 : 0
}),
contactsType: value => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.contactsTypeTrigger, this), {
ContactsPdoType: value.trim()
})
},
pdoDsn: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoDsnTrigger, this), {
ContactsPdoDsn: value.trim()
}),
pdoUser: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoUserTrigger, this), {
ContactsPdoUser: value.trim()
}),
pdoPassword: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoPasswordTrigger, this), {
ContactsPdoPassword: value.trim()
})
})
this.onTestContactsResponse = this.onTestContactsResponse.bind(this);
decorateKoCommands(this, {
testContactsCommand: self => self.pdoDsn() && self.pdoUser()
testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
});
}
@ -118,31 +75,31 @@ export class ContactsAdminSettings {
this.testContactsErrorMessage('');
this.testing(true);
Remote.testContacts(this.onTestContactsResponse, {
ContactsPdoType: this.contactsType(),
ContactsPdoDsn: this.pdoDsn(),
ContactsPdoUser: this.pdoUser(),
ContactsPdoPassword: this.pdoPassword()
});
}
onTestContactsResponse(iError, data) {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
if (!iError && data.Result.Result) {
this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
Remote.request('AdminContactsTest',
(iError, data) => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
}
}
this.testing(false);
if (!iError && data.Result.Result) {
this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
this.testContactsErrorMessage('');
}
}
this.testing(false);
}, {
ContactsPdoType: this.contactsPdoType(),
ContactsPdoDsn: this.contactsPdoDsn(),
ContactsPdoUser: this.contactsPdoUser(),
ContactsPdoPassword: this.contactsPdoPassword()
}
);
}
onShow() {

View file

@ -8,14 +8,11 @@ import Remote from 'Remote/Admin/Fetch';
import { DomainPopupView } from 'View/Popup/Domain';
import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
export class DomainsAdminSettings {
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
constructor() {
this.domains = DomainAdminStore;
this.domainForDeletion = ko.observable(null).deleteAccessHelper();
this.onDomainListChangeRequest = this.onDomainListChangeRequest.bind(this);
this.onDomainLoadRequest = this.onDomainLoadRequest.bind(this);
this.domainForDeletion = ko.observable(null).askDeleteHelper();
}
createDomain() {
@ -28,30 +25,32 @@ export class DomainsAdminSettings {
deleteDomain(domain) {
DomainAdminStore.remove(domain);
Remote.domainDelete(this.onDomainListChangeRequest, domain.name);
Remote.domainDelete(DomainAdminStore.fetch, domain.name);
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
Name: domain.name
});
}
disableDomain(domain) {
domain.disabled(!domain.disabled());
Remote.domainDisable(this.onDomainListChangeRequest, domain.name, domain.disabled());
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
Name: domain.name,
Disabled: domain.disabled() ? 1 : 0
});
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.b-admin-domains-list-table .e-item .e-action', oDom);
el && ko.dataFor(el) && Remote.domain(this.onDomainLoadRequest, ko.dataFor(el).name);
let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
{
Name: ko.dataFor(el).name
}
);
});
DomainAdminStore.fetch();
}
onDomainLoadRequest(iError, oData) {
if (!iError) {
showScreenPopup(DomainPopupView, [oData.Result]);
}
}
onDomainListChangeRequest() {
DomainAdminStore.fetch();
}
}

View file

@ -2,28 +2,29 @@ import ko from 'ko';
import {
isArray,
pInt,
settingsSaveHelperSimpleFunction,
changeTheme,
convertThemeName,
addObservablesTo,
addSubscribablesTo
convertThemeName
} from 'Common/Utils';
import { Capa, SaveSettingsStep } from 'Common/Enums';
import { Settings, SettingsGet } from 'Common/Globals';
import { reload as translatorReload, convertLangName } from 'Common/Translator';
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { SaveSettingsStep } from 'Common/Enums';
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
import { translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin';
import Remote from 'Remote/Admin/Fetch';
import { ThemeStore } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language';
import LanguagesPopupView from 'View/Popup/Languages';
import { LanguagesPopupView } from 'View/Popup/Languages';
export class GeneralAdminSettings {
export class AdminSettingsGeneral extends AbstractViewSettings {
constructor() {
super();
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
@ -36,18 +37,16 @@ export class GeneralAdminSettings {
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
addObservablesTo(this, {
allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
attachmentLimitTrigger: SaveSettingsStep.Idle,
languageTrigger: SaveSettingsStep.Idle,
themeTrigger: SaveSettingsStep.Idle,
capaThemes: Settings.capa(Capa.Themes),
capaUserBackground: Settings.capa(Capa.UserBackground),
capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts),
capaIdentities: Settings.capa(Capa.Identities),
capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails),
capaTemplates: Settings.capa(Capa.Templates),
capaThemes: SettingsCapa('Themes'),
capaUserBackground: SettingsCapa('UserBackground'),
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
capaIdentities: SettingsCapa('Identities'),
capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
dataFolderAccess: false
});
@ -59,10 +58,14 @@ export class GeneralAdminSettings {
}
*/
this.mainAttachmentLimit = ko
.observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024))
this.attachmentLimit = ko
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 });
this.addSetting('Language');
this.addSetting('AttachmentLimit');
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadDataDesc =
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
@ -74,12 +77,12 @@ export class GeneralAdminSettings {
].join('')
: '';
this.themesOptions = ko.computed(() =>
this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) }))
);
addComputablesTo(this, {
themesOptions: () => this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })),
this.languageFullName = ko.computed(() => convertLangName(this.language()));
this.languageAdminFullName = ko.computed(() => convertLangName(this.languageAdmin()));
languageFullName: () => convertLangName(this.language()),
languageAdminFullName: () => convertLangName(this.languageAdmin())
});
this.languageAdminTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
@ -87,55 +90,25 @@ export class GeneralAdminSettings {
this.languageAdminTrigger(saveSettingsStep);
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
},
fSaveBoolHelper = (key, fn) =>
value => {
const data = {};
data[key] = value ? 1 : 0;
Remote.saveAdminConfig(fn, data);
};
fSaveHelper = key => value => Remote.saveSetting(key, value);
addSubscribablesTo(this, {
mainAttachmentLimit: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.attachmentLimitTrigger, this), {
AttachmentLimit: pInt(value)
}),
language: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.languageTrigger, this), {
Language: value.trim()
}),
languageAdmin: value => {
this.languageAdminTrigger(SaveSettingsStep.Animate);
translatorReload(true, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveAdminConfig(null, {
LanguageAdmin: value.trim()
}));
.then(() => Remote.saveSetting('LanguageAdmin', value));
},
theme: value => {
changeTheme(value, this.themeTrigger);
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
Theme: value.trim()
});
},
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'),
capaIdentities: fSaveHelper('CapaIdentities'),
capaIdentities: fSaveBoolHelper('CapaIdentities'),
capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
capaTemplates: fSaveBoolHelper('CapaTemplates'),
capaThemes: fSaveHelper('CapaThemes'),
capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'),
capaThemes: fSaveBoolHelper('CapaThemes'),
capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
capaUserBackground: fSaveHelper('CapaUserBackground')
});
}

View file

@ -1,46 +1,9 @@
import ko from 'ko';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { Settings, SettingsGet } from 'Common/Globals';
import { settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
export class LoginAdminSettings {
export class AdminSettingsLogin extends AbstractViewSettings {
constructor() {
addObservablesTo(this, {
determineUserLanguage: !!SettingsGet('DetermineUserLanguage'),
determineUserDomain: !!SettingsGet('DetermineUserDomain'),
allowLanguagesOnLogin: !!SettingsGet('AllowLanguagesOnLogin'),
hideSubmitButton: !!Settings.app('hideSubmitButton')
});
this.defaultDomain = ko.observable(SettingsGet('LoginDefaultDomain')).idleTrigger();
addSubscribablesTo(this, {
determineUserLanguage: value =>
Remote.saveAdminConfig(null, {
DetermineUserLanguage: value ? 1 : 0
}),
determineUserDomain: value =>
Remote.saveAdminConfig(null, {
DetermineUserDomain: value ? 1 : 0
}),
allowLanguagesOnLogin: value =>
Remote.saveAdminConfig(null, {
AllowLanguagesOnLogin: value ? 1 : 0
}),
hideSubmitButton: value =>
Remote.saveAdminConfig(null, {
hideSubmitButton: value ? 1 : 0
}),
defaultDomain: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.defaultDomain.trigger, this), {
LoginDefaultDomain: value.trim()
})
});
super();
this.addSetting('LoginDefaultDomain');
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin','hideSubmitButton']);
}
}

View file

@ -6,42 +6,58 @@ import { getNotification } from 'Common/Translator';
import { PackageAdminStore } from 'Stores/Admin/Package';
import Remote from 'Remote/Admin/Fetch';
export class PackagesAdminSettings {
import { showScreenPopup } from 'Knoin/Knoin';
import { PluginPopupView } from 'View/Popup/Plugin';
import { addObservablesTo, addComputablesTo } from 'External/ko';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class AdminSettingsPackages extends AbstractViewSettings {
constructor() {
this.packagesError = ko.observable('');
super();
this.addSettings(['EnabledPlugins']);
addObservablesTo(this, {
packagesError: ''
});
this.packages = PackageAdminStore;
this.packagesCurrent = ko.computed(() =>
PackageAdminStore.filter(item => item && item.installed && !item.compare)
);
this.packagesAvailableForUpdate = ko.computed(() =>
PackageAdminStore.filter(item => item && item.installed && !!item.compare)
);
this.packagesAvailableForInstallation = ko.computed(() =>
PackageAdminStore.filter(item => item && !item.installed)
);
addComputablesTo(this, {
packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden'));
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
});
}
onShow() {
this.packagesError('');
}
onBuild() {
onBuild(oDom) {
PackageAdminStore.fetch();
oDom.addEventListener('click', event => {
// configurePlugin
let el = event.target.closestWithin('.package-configure', oDom),
data = el ? ko.dataFor(el) : 0;
data && Remote.request('AdminPluginLoad',
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
{
Id: data.id
}
);
// disablePlugin
el = event.target.closestWithin('.package-active', oDom);
data = el ? ko.dataFor(el) : 0;
data && this.disablePlugin(data);
});
}
requestHelper(packageToRequest, install) {
return (iError, data) => {
if (iError) {
this.packagesError(
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
// ':\n' + getNotification(iError);
);
}
PackageAdminStore.forEach(item => {
if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) {
packageToRequest.loading(false);
@ -49,7 +65,12 @@ export class PackagesAdminSettings {
}
});
if (!iError && data.Result.Reload) {
if (iError) {
this.packagesError(
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
);
} else if (data.Result.Reload) {
location.reload();
} else {
PackageAdminStore.fetch();
@ -60,14 +81,49 @@ export class PackagesAdminSettings {
deletePackage(packageToDelete) {
if (packageToDelete) {
packageToDelete.loading(true);
Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete);
Remote.request('AdminPackageDelete',
this.requestHelper(packageToDelete, false),
{
Id: packageToDelete.id
}
);
}
}
installPackage(packageToInstall) {
if (packageToInstall) {
packageToInstall.loading(true);
Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall);
Remote.request('AdminPackageInstall',
this.requestHelper(packageToInstall, true),
{
Id: packageToInstall.id,
Type: packageToInstall.type,
File: packageToInstall.file
},
60000
);
}
}
disablePlugin(plugin) {
let disable = plugin.enabled();
plugin.enabled(!disable);
Remote.request('AdminPluginDisable',
(iError, data) => {
if (iError) {
plugin.enabled(disable);
this.packagesError(
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
? data.ErrorMessage
: getNotification(iError)
);
}
// PackageAdminStore.fetch();
}, {
Id: plugin.id,
Disabled: disable ? 1 : 0
}
);
}
}

View file

@ -1,73 +0,0 @@
import ko from 'ko';
import { Notification } from 'Common/Enums';
import { SettingsGet } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
import { showScreenPopup } from 'Knoin/Knoin';
import { PluginAdminStore } from 'Stores/Admin/Plugin';
import Remote from 'Remote/Admin/Fetch';
import { PluginPopupView } from 'View/Popup/Plugin';
export class PluginsAdminSettings {
constructor() {
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
this.plugins = PluginAdminStore;
this.pluginsError = PluginAdminStore.error;
this.onPluginLoadRequest = this.onPluginLoadRequest.bind(this);
this.onPluginDisableRequest = this.onPluginDisableRequest.bind(this);
this.enabledPlugins.subscribe(value =>
Remote.saveAdminConfig(null, {
EnabledPlugins: value ? 1 : 0
})
);
}
disablePlugin(plugin) {
plugin.disabled(!plugin.disabled());
Remote.pluginDisable(this.onPluginDisableRequest, plugin.name, plugin.disabled());
}
configurePlugin(plugin) {
Remote.plugin(this.onPluginLoadRequest, plugin.name);
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.e-item .configure-plugin-action', oDom);
el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el));
el = event.target.closestWithin('.e-item .disabled-plugin', oDom);
el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el));
});
}
onShow() {
PluginAdminStore.error('');
PluginAdminStore.fetch();
}
onPluginLoadRequest(iError, data) {
if (!iError) {
showScreenPopup(PluginPopupView, [data.Result]);
}
}
onPluginDisableRequest(iError, data) {
if (iError) {
if (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) {
PluginAdminStore.error(data.ErrorMessage);
} else {
PluginAdminStore.error(getNotification(iError));
}
}
PluginAdminStore.fetch();
}
}

View file

@ -1,34 +1,40 @@
import { Capa } from 'Common/Enums';
import { Settings, SettingsGet } from 'Common/Globals';
import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import { SettingsGet, SettingsCapa } from 'Common/Globals';
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class SecurityAdminSettings {
export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
this.weakPassword = rl.app.weakPassword;
addObservablesTo(this, {
useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
adminLogin: SettingsGet('AdminLogin'),
adminLoginError: false,
adminPassword: '',
adminPasswordNew: '',
adminPasswordNew2: '',
adminPasswordNewError: false,
adminTOTP: SettingsGet('AdminTOTP'),
adminPasswordUpdateError: false,
adminPasswordUpdateSuccess: false,
capaOpenPGP: Settings.capa(Capa.OpenPGP)
capaOpenPGP: SettingsCapa('OpenPGP')
});
const reset = () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
};
addSubscribablesTo(this, {
adminPassword: () => {
this.adminPasswordUpdateError(false);
@ -37,43 +43,13 @@ export class SecurityAdminSettings {
adminLogin: () => this.adminLoginError(false),
adminPasswordNew: () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
adminPasswordNew: reset,
adminPasswordNew2: () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
adminPasswordNew2: reset,
capaOpenPGP: value =>
Remote.saveAdminConfig(null, {
CapaOpenPGP: value ? 1 : 0
}),
useLocalProxyForExternalImages: value =>
Remote.saveAdminConfig(null, {
UseLocalProxyForExternalImages: value ? 1 : 0
}),
verifySslCertificate: value => {
value => value || this.allowSelfSigned(true);
Remote.saveAdminConfig(null, {
VerifySslCertificate: value ? 1 : 0
});
},
allowSelfSigned: value =>
Remote.saveAdminConfig(null, {
AllowSelfSigned: value ? 1 : 0
})
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
});
this.onNewAdminPasswordResponse = this.onNewAdminPasswordResponse.bind(this);
decorateKoCommands(this, {
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword()
});
@ -93,29 +69,28 @@ export class SecurityAdminSettings {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
Remote.saveNewAdminPassword(this.onNewAdminPasswordResponse, {
Remote.request('AdminPasswordUpdate', (iError, data) => {
if (iError) {
this.adminPasswordUpdateError(true);
} else {
this.adminPassword('');
this.adminPasswordNew('');
this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true);
this.weakPassword(!!data.Result.Weak);
}
}, {
'Login': this.adminLogin(),
'Password': this.adminPassword(),
'NewPassword': this.adminPasswordNew()
'NewPassword': this.adminPasswordNew(),
'TOTP': this.adminTOTP()
});
return true;
}
onNewAdminPasswordResponse(iError, data) {
if (iError) {
this.adminPasswordUpdateError(true);
} else {
this.adminPassword('');
this.adminPasswordNew('');
this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true);
this.weakPassword(!!data.Result.Weak);
}
}
onHide() {
this.adminPassword('');
this.adminPasswordNew('');

View file

@ -1,7 +1,6 @@
import ko from 'ko';
import { Capa } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
@ -12,17 +11,18 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account';
import { IdentityPopupView } from 'View/Popup/Identity';
export class AccountsUserSettings {
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() {
this.allowAdditionalAccount = Settings.capa(Capa.AdditionalAccounts);
this.allowIdentities = Settings.capa(Capa.Identities);
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
this.allowIdentities = SettingsCapa('Identities');
this.accounts = AccountUserStore.accounts;
this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore;
this.mainEmail = SettingsGet('MainEmail');
this.accountForDeletion = ko.observable(null).deleteAccessHelper();
this.identityForDeletion = ko.observable(null).deleteAccessHelper();
this.accountForDeletion = ko.observable(null).askDeleteHelper();
this.identityForDeletion = ko.observable(null).askDeleteHelper();
}
addNewAccount() {
@ -30,7 +30,7 @@ export class AccountsUserSettings {
}
editAccount(account) {
if (account && account.canBeEdit()) {
if (account && account.isAdditional()) {
showScreenPopup(AccountPopupView, [account]);
}
}
@ -48,19 +48,21 @@ export class AccountsUserSettings {
* @returns {void}
*/
deleteAccount(accountToRemove) {
if (accountToRemove && accountToRemove.deleteAccess()) {
if (accountToRemove && accountToRemove.askDelete()) {
this.accountForDeletion(null);
if (accountToRemove) {
this.accounts.remove((account) => accountToRemove === account);
Remote.accountDelete((iError, data) => {
Remote.request('AccountDelete', (iError, data) => {
if (!iError && data.Reload) {
rl.route.root();
setTimeout(() => location.reload(), 1);
} else {
rl.app.accountsAndIdentities();
}
}, accountToRemove.email);
}, {
EmailToDelete: accountToRemove.email
});
}
}
}
@ -70,26 +72,31 @@ export class AccountsUserSettings {
* @returns {void}
*/
deleteIdentity(identityToRemove) {
if (identityToRemove && identityToRemove.deleteAccess()) {
if (identityToRemove && identityToRemove.askDelete()) {
this.identityForDeletion(null);
if (identityToRemove) {
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
Remote.identityDelete(() => rl.app.accountsAndIdentities(), identityToRemove.id());
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
IdToDelete: identityToRemove.id()
});
}
}
}
accountsAndIdentitiesAfterMove() {
Remote.accountsAndIdentitiesSortOrder(null, AccountUserStore.getEmailAddresses(), IdentityUserStore.getIDS());
Remote.request('AccountsAndIdentitiesSortOrder', null, {
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')),
Identities: IdentityUserStore.getIDS()
});
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.accounts-list .account-item .e-action', oDom);
let el = event.target.closestWithin('.accounts-list .e-action', oDom);
el && ko.dataFor(el) && this.editAccount(ko.dataFor(el));
el = event.target.closestWithin('.identities-list .identity-item .e-action', oDom);
el = event.target.closestWithin('.identities-list .e-action', oDom);
el && ko.dataFor(el) && this.editIdentity(ko.dataFor(el));
});
}

View file

@ -1,10 +1,11 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals';
import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch';
export class ContactsUserSettings {
export class UserSettingsContacts /*extends AbstractViewSettings*/ {
constructor() {
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
@ -14,8 +15,7 @@ export class ContactsUserSettings {
this.contactsSyncUser = ContactUserStore.syncUser;
this.contactsSyncPass = ContactUserStore.syncPass;
this.saveTrigger = ko
.computed(() =>
this.saveTrigger = koComputable(() =>
[
ContactUserStore.enableSync() ? '1' : '0',
ContactUserStore.syncUrl(),
@ -26,19 +26,16 @@ export class ContactsUserSettings {
.extend({ debounce: 500 });
this.contactsAutosave.subscribe(value =>
Remote.saveSettings(null, {
ContactsAutosave: value ? 1 : 0
})
Remote.saveSettings(null, { ContactsAutosave: value })
);
this.saveTrigger.subscribe(() =>
Remote.saveContactsSyncData(
null,
ContactUserStore.enableSync(),
ContactUserStore.syncUrl(),
ContactUserStore.syncUser(),
ContactUserStore.syncPass()
)
Remote.request('SaveContactsSyncData', null, {
Enable: ContactUserStore.enableSync() ? 1 : 0,
Url: ContactUserStore.syncUrl(),
User: ContactUserStore.syncUser(),
Password: ContactUserStore.syncPass()
})
);
}
}

View file

@ -1,100 +1,44 @@
import ko from 'ko';
import { addObservablesTo } from 'External/ko';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsGet } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
import { addObservablesTo } from 'Common/Utils';
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { SieveUserStore } from 'Stores/User/Sieve';
import Remote from 'Remote/User/Fetch';
import { SieveScriptModel } from 'Model/SieveScript';
import { showScreenPopup } from 'Knoin/Knoin';
import { SieveScriptPopupView } from 'View/Popup/SieveScript';
export class FiltersUserSettings {
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
constructor() {
this.scripts = SieveUserStore.scripts;
this.loading = ko.observable(false).extend({ debounce: 200 });
this.scripts = ko.observableArray();
this.loading = ko.observable(true).extend({ debounce: 200 });
addObservablesTo(this, {
serverError: false,
serverErrorDesc: ''
});
this.scriptForDeletion = ko.observable(null).deleteAccessHelper();
}
rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/sieve.')).then(() => {
const Sieve = window.Sieve;
Sieve.folderList = FolderUserStore.folderList;
Sieve.serverError.subscribe(value => this.serverError(value));
Sieve.serverErrorDesc.subscribe(value => this.serverErrorDesc(value));
Sieve.loading.subscribe(value => this.loading(value));
Sieve.scripts.subscribe(value => this.scripts(value));
Sieve.updateList();
}).catch(e => console.error(e));
setError(text) {
this.serverError(true);
this.serverErrorDesc(text);
}
updateList() {
if (!this.loading()) {
this.loading(true);
this.serverError(false);
Remote.filtersGet((iError, data) => {
this.loading(false);
this.scripts([]);
if (iError) {
SieveUserStore.capa([]);
this.setError(getNotification(iError));
} else {
SieveUserStore.capa(data.Result.Capa);
/*
this.scripts(
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
);
*/
Object.values(data.Result.Scripts).forEach(value => {
value = SieveScriptModel.reviveFromJson(value);
value && this.scripts.push(value)
});
}
});
}
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
}
addScript() {
showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]);
this.editScript();
}
editScript(script) {
showScreenPopup(SieveScriptPopupView, [script]);
window.Sieve.ScriptView.showModal(script ? [script] : null);
}
deleteScript(script) {
this.serverError(false);
Remote.filtersScriptDelete(
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
this.scripts.remove(script);
delegateRunOnDestroy(script);
}
},
script.name()
);
window.Sieve.deleteScript(script);
}
toggleScript(script) {
let name = script.active() ? '' : script.name();
this.serverError(false);
Remote.filtersScriptActivate(
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || iError)
} else {
this.scripts.forEach(script => script.active(script.name() === name));
}
},
name
);
window.Sieve.toggleScript(script);
}
onBuild(oDom) {
@ -106,6 +50,6 @@ export class FiltersUserSettings {
}
onShow() {
this.updateList();
window.Sieve && window.Sieve.updateList();
}
}

View file

@ -1,13 +1,15 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums';
import { ClientSideKeyName } from 'Common/EnumsUser';
import { Settings } from 'Common/Globals';
import { FolderMetadataKeys } from 'Common/EnumsUser';
import { SettingsCapa } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
import { removeFolderFromCacheList } from 'Common/Cache';
import * as Local from 'Storage/Client';
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
@ -18,16 +20,36 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { loadFolders } from 'Model/FolderCollection';
export class FoldersUserSettings {
const folderForDeletion = ko.observable(null).askDeleteHelper();
export class UserSettingsFolders /*extends AbstractViewSettings*/ {
constructor() {
this.showKolab = koComputable(() => FolderUserStore.hasCapability('METADATA') && SettingsCapa('Kolab'));
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.kolabTypeOptions = ko.observableArray();
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
initOnStartOrLangChange(()=>{
this.kolabTypeOptions([
{ id: '', name: '' },
{ id: 'event', name: i18nFilter('CALENDAR') },
{ id: 'contact', name: i18nFilter('CONTACTS') },
{ id: 'task', name: i18nFilter('TASKS') },
{ id: 'note', name: i18nFilter('NOTES') },
{ id: 'file', name: i18nFilter('FILES') },
{ id: 'journal', name: i18nFilter('JOURNAL') },
{ id: 'configuration', name: i18nFilter('CONFIGURATION') }
]);
});
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
this.folderList = FolderUserStore.folderList;
this.folderListOptimized = FolderUserStore.folderListOptimized;
this.folderListError = FolderUserStore.folderListError;
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
this.loading = ko.computed(() => {
this.loading = koComputable(() => {
const loading = FolderUserStore.foldersLoading(),
creating = FolderUserStore.foldersCreating(),
deleting = FolderUserStore.foldersDeleting(),
@ -36,28 +58,43 @@ export class FoldersUserSettings {
return loading || creating || deleting || renaming;
});
this.folderForDeletion = ko.observable(null).deleteAccessHelper();
this.folderForDeletion = folderForDeletion;
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
this.useImapSubscribe = Settings.app('useImapSubscribe');
this.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value ? 1 : 0));
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
}
folderEditOnEnter(folder) {
const nameToEdit = folder ? folder.nameForEdit().trim() : '';
if (nameToEdit && folder.name() !== nameToEdit) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
rl.app.foldersPromisesActionHelper(
Remote.folderRename(folder.fullNameRaw, nameToEdit),
Notification.CantRenameFolder
);
removeFolderFromCacheList(folder.fullNameRaw);
folder.name(nameToEdit);
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
Folder: folder.fullName,
NewFolderName: nameToEdit,
Subscribe: folder.subscribed() ? 1 : 0
})
.then(data => {
folder.name(nameToEdit/*data.Name*/);
if (folder.subFolders.length) {
Remote.setTrigger(FolderUserStore.foldersLoading, true);
// clearTimeout(Remote.foldersTimeout);
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
setTimeout(loadFolders, 500);
// TODO: rename all subfolders with folder.delimiter to prevent reload?
} else {
removeFolderFromCacheList(folder.fullName);
folder.fullName = data.Result.FullName;
setFolder(folder);
const parent = getFolderFromCacheList(folder.parentName);
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
}
})
.catch(error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantRenameFolder)
+ '.\n' + error.message);
});
}
folder.edited(false);
@ -85,58 +122,69 @@ export class FoldersUserSettings {
}
deleteFolder(folderToRemove) {
if (
folderToRemove &&
folderToRemove.canBeDeleted() &&
folderToRemove.deleteAccess() &&
0 === folderToRemove.privateMessageCountAll()
if (folderToRemove
&& folderToRemove.canBeDeleted()
&& folderToRemove.askDelete()
) {
this.folderForDeletion(null);
if (0 < folderToRemove.privateMessageCountAll()) {
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
} else {
folderForDeletion(null);
if (folderToRemove) {
const fRemoveFolder = function(folder) {
if (folderToRemove === folder) {
return true;
}
folder.subFolders.remove(fRemoveFolder);
return false;
};
Local.set(ClientSideKeyName.FoldersLashHash, '');
FolderUserStore.folderList.remove(fRemoveFolder);
rl.app.foldersPromisesActionHelper(
Remote.folderDelete(folderToRemove.fullNameRaw),
Notification.CantDeleteFolder
);
removeFolderFromCacheList(folderToRemove.fullNameRaw);
if (folderToRemove) {
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
Folder: folderToRemove.fullName
}).then(
() => {
// folderToRemove.flags.push('\\nonexistent');
folderToRemove.selectable(false);
// folderToRemove.subscribed(false);
// folderToRemove.checkable(false);
if (!folderToRemove.subFolders.length) {
removeFolderFromCacheList(folderToRemove.fullName);
const folder = getFolderFromCacheList(folderToRemove.parentName);
(folder ? folder.subFolders : FolderUserStore.folderList).remove(folderToRemove);
}
},
error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantDeleteFolder)
+ '.\n' + error.message
);
}
);
}
}
} else if (0 < folderToRemove.privateMessageCountAll()) {
FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
}
}
subscribeFolder(folder) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, true);
folder.subscribed(true);
toggleFolderKolabType(folder, event) {
let type = event.target.value;
// TODO: append '.default' ?
Remote.request('FolderSetMetadata', null, {
Folder: folder.fullName,
Key: FolderMetadataKeys.KolabFolderType,
Value: type
});
folder.kolabType(type);
}
unSubscribeFolder(folder) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, false);
folder.subscribed(false);
toggleFolderSubscription(folder) {
let subscribe = !folder.subscribed();
Remote.request('FolderSubscribe', null, {
Folder: folder.fullName,
Subscribe: subscribe ? 1 : 0
});
folder.subscribed(subscribe);
}
checkableTrueFolder(folder) {
Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, true);
folder.checkable(true);
}
checkableFalseFolder(folder) {
Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, false);
folder.checkable(false);
toggleFolderCheckable(folder) {
let checkable = !folder.checkable();
Remote.request('FolderCheckable', null, {
Folder: folder.fullName,
Checkable: checkable ? 1 : 0
});
folder.checkable(checkable);
}
}

View file

@ -1,12 +1,13 @@
import ko from 'ko';
import { MESSAGES_PER_PAGE_VALUES } from 'Common/Consts';
import { SaveSettingsStep } from 'Common/Enums';
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
import { SettingsGet } from 'Common/Globals';
import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator';
import { Settings, SettingsGet } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
import { i18n, trigger as translatorTrigger, translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin';
import { AppUserStore } from 'Stores/User/App';
@ -15,27 +16,33 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages';
export class GeneralUserSettings {
export class UserSettingsGeneral extends AbstractViewSettings {
constructor() {
super();
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
this.messageReadDelay = SettingsUserStore.messageReadDelay;
this.messagesPerPage = SettingsUserStore.messagesPerPage;
this.messagesPerPageArray = MESSAGES_PER_PAGE_VALUES;
this.editorDefaultType = SettingsUserStore.editorDefaultType;
this.layout = SettingsUserStore.layout;
this.enableSoundNotification = NotificationUserStore.enableSoundNotification;
this.soundNotification = NotificationUserStore.enableSoundNotification;
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification;
this.isDesktopNotificationDenied = NotificationUserStore.isDesktopNotificationDenied;
this.desktopNotification = NotificationUserStore.enableDesktopNotification;
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
this.viewHTML = SettingsUserStore.viewHTML;
this.showImages = SettingsUserStore.showImages;
this.removeColors = SettingsUserStore.removeColors;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
@ -44,97 +51,87 @@ export class GeneralUserSettings {
this.replySameFolder = SettingsUserStore.replySameFolder;
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
this.languageFullName = ko.computed(() => convertLangName(this.language()));
this.languageTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
addObservablesTo(this, {
mppTrigger: SaveSettingsStep.Idle,
editorDefaultTypeTrigger: SaveSettingsStep.Idle,
layoutTrigger: SaveSettingsStep.Idle
});
this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
this.identities = IdentityUserStore;
this.identityMain = ko.computed(() => {
const list = this.identities();
return isArray(list) ? list.find(item => item && !item.id()) : null;
addComputablesTo(this, {
languageFullName: () => convertLangName(this.language()),
identityMain: () => {
const list = this.identities();
return isArray(list) ? list.find(item => item && !item.id()) : null;
},
identityMainDesc: () => {
const identity = this.identityMain();
return identity ? identity.formattedName() : '---';
},
editorDefaultTypes: () => {
translatorTrigger();
return [
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
{ id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
{ id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
];
},
layoutTypes: () => {
translatorTrigger();
return [
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
];
}
});
this.identityMainDesc = ko.computed(() => {
const identity = this.identityMain();
return identity ? identity.formattedName() : '---';
});
this.addSetting('EditorDefaultType');
this.addSetting('MessageReadDelay');
this.addSetting('MessagesPerPage');
this.addSetting('Layout', () => MessagelistUserStore([]));
this.editorDefaultTypes = ko.computed(() => {
translatorTrigger();
return [
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
{ id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
{ id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
];
});
this.layoutTypes = ko.computed(() => {
translatorTrigger();
return [
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
];
});
this.addSettings(['ViewHTML', 'ShowImages', 'UseCheckboxesInList', 'ReplySameFolder',
'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => {
this.languageTrigger(saveSettingsStep);
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
};
addSubscribablesTo(this, {
language: value => {
this.languageTrigger(SaveSettingsStep.Animate);
translatorReload(false, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult),
fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveSetting('Language', value));
},
editorDefaultType: value => Remote.saveSetting('EditorDefaultType', value,
settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this)),
messagesPerPage: value => Remote.saveSetting('MPP', value,
settingsSaveHelperSimpleFunction(this.mppTrigger, this)),
showImages: value => Remote.saveSetting('ShowImages', value ? 1 : 0),
removeColors: value => {
MessageUserStore.messagesBodiesDom().innerHTML = '';
Remote.saveSetting('RemoveColors', value ? 1 : 0);
let dom = MessageUserStore.bodiesDom();
if (dom) {
dom.innerHTML = '';
}
Remote.saveSetting('RemoveColors', value);
},
useCheckboxesInList: value => Remote.saveSetting('UseCheckboxesInList', value ? 1 : 0),
enableDesktopNotification: value => Remote.saveSetting('DesktopNotifications', value ? 1 : 0),
enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0),
replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
notificationSound: value => {
Remote.saveSetting('NotificationSound', value);
Settings.set('NotificationSound', value);
},
useThreads: value => {
MessageUserStore.list([]);
Remote.saveSetting('UseThreads', value ? 1 : 0);
},
layout: value => {
MessageUserStore.list([]);
Remote.saveSetting('Layout', value, settingsSaveHelperSimpleFunction(this.layoutTrigger, this));
MessagelistUserStore([]);
Remote.saveSetting('UseThreads', value);
}
});
}
editMainIdentity() {
const identity = this.identityMain();
if (identity) {
showScreenPopup(IdentityPopupView, [identity]);
}
identity && showScreenPopup(IdentityPopupView, [identity]);
}
testSoundNotification() {

View file

@ -1,66 +0,0 @@
import ko from 'ko';
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { PgpUserStore } from 'Stores/User/Pgp';
import { SettingsUserStore } from 'Stores/User/Settings';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { AddOpenPgpKeyPopupView } from 'View/Popup/AddOpenPgpKey';
import { NewOpenPgpKeyPopupView } from 'View/Popup/NewOpenPgpKey';
import { ViewOpenPgpKeyPopupView } from 'View/Popup/ViewOpenPgpKey';
export class OpenPgpUserSettings {
constructor() {
this.openpgpkeys = PgpUserStore.openpgpkeys;
this.openpgpkeysPublic = PgpUserStore.openpgpkeysPublic;
this.openpgpkeysPrivate = PgpUserStore.openpgpkeysPrivate;
this.openPgpKeyForDeletion = ko.observable(null).deleteAccessHelper();
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value ? 1 : 0))
}
addOpenPgpKey() {
showScreenPopup(AddOpenPgpKeyPopupView);
}
generateOpenPgpKey() {
showScreenPopup(NewOpenPgpKeyPopupView);
}
viewOpenPgpKey(openPgpKey) {
if (openPgpKey) {
showScreenPopup(ViewOpenPgpKeyPopupView, [openPgpKey]);
}
}
/**
* @param {OpenPgpKeyModel} openPgpKeyToRemove
* @returns {void}
*/
deleteOpenPgpKey(openPgpKeyToRemove) {
if (openPgpKeyToRemove && openPgpKeyToRemove.deleteAccess()) {
this.openPgpKeyForDeletion(null);
if (openPgpKeyToRemove && PgpUserStore.openpgpKeyring) {
const findedItem = PgpUserStore.openpgpkeys.find(key => openPgpKeyToRemove === key);
if (findedItem) {
PgpUserStore.openpgpkeys.remove(findedItem);
delegateRunOnDestroy(findedItem);
PgpUserStore.openpgpKeyring[findedItem.isPrivate ? 'privateKeys' : 'publicKeys'].removeForId(findedItem.guid);
PgpUserStore.openpgpKeyring.store();
}
rl.app.reloadOpenPgpKeys();
}
}
}
}

View file

@ -1,23 +1,32 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { pInt, settingsSaveHelperSimpleFunction } from 'Common/Utils';
import { Capa, SaveSettingsStep } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { SettingsCapa } from 'Common/Globals';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { SettingsUserStore } from 'Stores/User/Settings';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import Remote from 'Remote/User/Fetch';
export class SecurityUserSettings {
import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
export class UserSettingsSecurity extends AbstractViewSettings {
constructor() {
this.capaAutoLogout = Settings.capa(Capa.AutoLogout);
super();
this.capaAutoLogout = SettingsCapa('AutoLogout');
this.autoLogout = SettingsUserStore.autoLogout;
this.autoLogoutTrigger = ko.observable(SaveSettingsStep.Idle);
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
this.autoLogoutOptions = ko.computed(() => {
this.autoLogoutOptions = koComputable(() => {
translatorTrigger();
return [
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
@ -32,10 +41,37 @@ export class SecurityUserSettings {
});
if (this.capaAutoLogout) {
this.autoLogout.subscribe(value => Remote.saveSetting(
'AutoLogout', pInt(value),
settingsSaveHelperSimpleFunction(this.autoLogoutTrigger, this)
));
this.addSetting('AutoLogout');
}
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
this.canOpenPGP = SettingsCapa('OpenPGP');
this.canGnuPG = GnuPGUserStore.isSupported();
this.canMailvelope = !!window.mailvelope;
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
}
addOpenPgpKey() {
showScreenPopup(OpenPgpImportPopupView);
}
generateOpenPgpKey() {
showScreenPopup(OpenPgpGeneratePopupView);
}
onBuild() {
/**
* Create an iframe to display the Mailvelope keyring settings.
* The iframe will be injected into the container identified by selector.
*/
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
}
}

View file

@ -1,60 +0,0 @@
import ko from 'ko';
import { i18n } from 'Common/Translator';
import { TemplateUserStore } from 'Stores/User/Template';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { TemplatePopupView } from 'View/Popup/Template';
export class TemplatesUserSettings {
constructor() {
this.templates = TemplateUserStore.templates;
this.processText = ko.computed(() =>
TemplateUserStore.templates.loading() ? i18n('SETTINGS_TEMPLETS/LOADING_PROCESS') : ''
);
this.visibility = ko.computed(() => this.processText() ? 'visible' : 'hidden');
this.templateForDeletion = ko.observable(null).deleteAccessHelper();
}
addNewTemplate() {
showScreenPopup(TemplatePopupView);
}
editTemplate(oTemplateItem) {
if (oTemplateItem) {
showScreenPopup(TemplatePopupView, [oTemplateItem]);
}
}
deleteTemplate(templateToRemove) {
if (templateToRemove && templateToRemove.deleteAccess()) {
this.templateForDeletion(null);
if (templateToRemove) {
this.templates.remove((template) => templateToRemove === template);
Remote.templateDelete(() => {
this.reloadTemplates();
}, templateToRemove.id);
}
}
}
reloadTemplates() {
rl.app.templates();
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
const el = event.target.closestWithin('.templates-list .template-item .e-action', oDom);
el && ko.dataFor(el) && this.editTemplate(ko.dataFor(el));
});
this.reloadTemplates();
}
}

View file

@ -1,33 +1,37 @@
import ko from 'ko';
import { addObservablesTo } from 'External/ko';
import { SaveSettingsStep, UploadErrorCode, Capa } from 'Common/Enums';
import { SaveSettingsStep, UploadErrorCode } from 'Common/Enums';
import { changeTheme, convertThemeName } from 'Common/Utils';
import { userBackground, themePreviewLink, serverRequest } from 'Common/Links';
import { themePreviewLink, serverRequest } from 'Common/Links';
import { i18n } from 'Common/Translator';
import { doc, $htmlCL, Settings } from 'Common/Globals';
import { SettingsCapa } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
export class ThemesUserSettings {
const themeBackground = {
name: ThemeStore.userBackgroundName,
hash: ThemeStore.userBackgroundHash
};
addObservablesTo(themeBackground, {
uploaderButton: null,
loading: false,
error: ''
});
export class UserSettingsThemes /*extends AbstractViewSettings*/ {
constructor() {
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
this.themesObjects = ko.observableArray();
this.background = {};
this.background.name = ThemeStore.userBackgroundName;
this.background.hash = ThemeStore.userBackgroundHash;
this.background.uploaderButton = ko.observable(null);
this.background.loading = ko.observable(false);
this.background.error = ko.observable('');
this.capaUserBackground = ko.observable(Settings.capa(Capa.UserBackground));
themeBackground.enabled = SettingsCapa('UserBackground');
this.background = themeBackground;
this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
this.theme.subscribe((value) => {
ThemeStore.theme.subscribe(value => {
this.themesObjects.forEach(theme => {
theme.selected(value === theme.name);
});
@ -38,23 +42,13 @@ export class ThemesUserSettings {
Theme: value
});
});
this.background.hash.subscribe((value) => {
if (!value) {
$htmlCL.remove('UserBackground');
doc.body.removeAttribute('style');
} else {
$htmlCL.add('UserBackground');
doc.body.style.backgroundImage = "url("+userBackground(value)+")";
}
});
}
onBuild() {
const currentTheme = this.theme();
const currentTheme = ThemeStore.theme();
this.themesObjects(
this.themes.map(theme => ({
ThemeStore.themes.map(theme => ({
name: theme,
nameDisplay: convertThemeName(theme),
selected: ko.observable(theme === currentTheme),
@ -62,48 +56,30 @@ export class ThemesUserSettings {
}))
);
this.initUploader();
}
// initUploader
onShow() {
this.background.error('');
}
clearBackground() {
if (this.capaUserBackground()) {
Remote.clearUserBackground(() => {
this.background.name('');
this.background.hash('');
});
}
}
initUploader() {
if (this.background.uploaderButton() && this.capaUserBackground()) {
if (themeBackground.uploaderButton() && themeBackground.enabled) {
const oJua = new Jua({
action: serverRequest('UploadBackground'),
name: 'uploader',
queueSize: 1,
multipleSizeLimit: 1,
disableMultiple: true,
clickElement: this.background.uploaderButton()
limit: 1,
clickElement: themeBackground.uploaderButton()
});
oJua
.on('onStart', () => {
this.background.loading(true);
this.background.error('');
themeBackground.loading(true);
themeBackground.error('');
return true;
})
.on('onComplete', (id, result, data) => {
this.background.loading(false);
themeBackground.loading(false);
if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) {
this.background.name(data.Result.Name);
this.background.hash(data.Result.Hash);
themeBackground.name(data.Result.Name);
themeBackground.hash(data.Result.Hash);
} else {
this.background.name('');
this.background.hash('');
themeBackground.name('');
themeBackground.hash('');
let errorMsg = '';
if (data.ErrorCode) {
@ -122,11 +98,24 @@ export class ThemesUserSettings {
errorMsg = data.ErrorMessage;
}
this.background.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
themeBackground.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
}
return true;
});
}
}
onShow() {
themeBackground.error('');
}
clearBackground() {
if (themeBackground.enabled) {
Remote.request('ClearUserBackground', () => {
themeBackground.name('');
themeBackground.hash('');
});
}
}
}

185
dev/Sieve/Commands.js Normal file
View file

@ -0,0 +1,185 @@
/**
* https://tools.ietf.org/html/rfc5228#section-2.9
*/
import { capa } from 'Sieve/Utils';
import {
GrammarCommand,
GrammarString,
GrammarStringList,
GrammarQuotedString
} from 'Sieve/Grammar';
/**
* https://tools.ietf.org/html/rfc5228#section-3.1
* Usage:
* if <test1: test> <block1: block>
* elsif <test2: test> <block2: block>
* else <block3: block>
*/
export class ConditionalCommand extends GrammarCommand
{
constructor()
{
super();
this.test = null;
}
toString()
{
return this.identifier + ' ' + this.test + ' ' + this.commands;
}
/*
public function pushArguments(array $args): void
{
print_r($args);
exit;
}
*/
}
export class IfCommand extends ConditionalCommand
{
}
export class ElsIfCommand extends ConditionalCommand
{
}
export class ElseCommand extends ConditionalCommand
{
toString()
{
return this.identifier + ' ' + this.commands;
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-3.2
*/
export class RequireCommand extends GrammarCommand
{
constructor()
{
super();
this.capabilities = new GrammarStringList();
}
toString()
{
return 'require ' + this.capabilities.toString() + ';';
}
pushArguments(args)
{
if (args[0] instanceof GrammarStringList) {
this.capabilities = args[0];
} else if (args[0] instanceof GrammarQuotedString) {
this.capabilities.push(args[0]);
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-3.3
*/
export class StopCommand extends GrammarCommand
{
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.1
*/
export class FileIntoCommand extends GrammarCommand
{
constructor()
{
super();
// QuotedString / MultiLine
this._mailbox = new GrammarQuotedString();
}
get require() { return 'fileinto'; }
toString()
{
return 'fileinto '
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ':copy ' : '')
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
+ ((this.create && capa.includes('mailbox')) ? ':create ' : '')
+ this._mailbox
+ ';';
}
get mailbox()
{
return this._mailbox.value;
}
set mailbox(value)
{
this._mailbox.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._mailbox = args[0];
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.2
*/
export class RedirectCommand extends GrammarCommand
{
constructor()
{
super();
// QuotedString / MultiLine
this._address = new GrammarQuotedString();
}
toString()
{
return 'redirect '
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ':copy ' : '')
+ this._address
+ ';';
}
get address()
{
return this._address.value;
}
set address(value)
{
this._address.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._address = args[0];
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.3
*/
export class KeepCommand extends GrammarCommand
{
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.4
*/
export class DiscardCommand extends GrammarCommand
{
}

View file

@ -0,0 +1,45 @@
/**
* https://tools.ietf.org/html/rfc5173
*/
import {
GrammarString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class BodyTest extends GrammarTest
{
constructor()
{
super();
this.body_transform = ''; // :raw, :content <string-list>, :text
this.key_list = new GrammarStringList;
}
get require() { return 'body'; }
toString()
{
return 'body'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.body_transform
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (':raw' === arg || ':text' === arg) {
this.body_transform = arg;
} else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
if (':content' === args[i-1]) {
this.body_transform = ':content ' + arg;
} else {
this[args[i+1] ? 'content_list' : 'key_list'] = arg;
}
}
});
}
}

View file

@ -0,0 +1,36 @@
/**
* https://tools.ietf.org/html/rfc5183
*/
import {
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class EnvironmentTest extends GrammarTest
{
constructor()
{
super();
this.name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'environment'; }
toString()
{
return 'environment'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.name
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.name = args.pop();
}
}

View file

@ -0,0 +1,71 @@
/**
* https://tools.ietf.org/html/rfc5229
*/
import {
GrammarCommand,
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class SetCommand extends GrammarCommand
{
constructor()
{
super();
this.modifiers = [];
this._name = new GrammarQuotedString;
this._value = new GrammarQuotedString;
}
get require() { return 'variables'; }
toString()
{
return 'set'
+ ' ' + this.modifiers.join(' ')
+ ' ' + this._name
+ ' ' + this._value;
}
get name() { return this._name.value; }
set name(str) { this._name.value = str; }
get value() { return this._value.value; }
set value(str) { this._value.value = str; }
pushArguments(args)
{
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
args.includes(modifier) && this.modifiers.push(modifier);
});
this._value = args.pop();
this._name = args.pop();
}
}
export class StringTest extends GrammarTest
{
constructor()
{
super();
this.source = new GrammarStringList;
this.key_list = new GrammarStringList;
}
toString()
{
return 'string'
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.source.toString()
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.source = args.pop();
}
}

View file

@ -0,0 +1,87 @@
/**
* https://tools.ietf.org/html/rfc5230
* https://tools.ietf.org/html/rfc6131
*/
import { capa } from 'Sieve/Utils';
import {
GrammarCommand,
GrammarNumber,
GrammarQuotedString,
GrammarStringList
} from 'Sieve/Grammar';
export class VacationCommand extends GrammarCommand
{
constructor()
{
super();
this._days = new GrammarNumber;
this._seconds = new GrammarNumber;
this._subject = new GrammarQuotedString;
this._from = new GrammarQuotedString;
this.addresses = new GrammarStringList;
this.mime = false;
this._handle = new GrammarQuotedString;
this._reason = new GrammarQuotedString; // QuotedString / MultiLine
}
// get require() { return ['vacation','vacation-seconds']; }
get require() { return 'vacation'; }
toString()
{
let result = 'vacation';
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
result += ' :seconds ' + this._seconds;
} else if (0 < this._days.value) {
result += ' :days ' + this._days;
}
if (this._subject.length) {
result += ' :subject ' + this._subject;
}
if (this._from.length) {
result += ' :from ' + this.arguments[':from'];
}
if (this.addresses.length) {
result += ' :addresses ' + this.addresses.toString();
}
if (this.mime) {
result += ' :mime';
}
if (this._handle.length) {
result += ' :handle ' + this._handle;
}
return result + ' ' + this._reason;
}
get days() { return this._days.value; }
get seconds() { return this._seconds.value; }
get subject() { return this._subject.value; }
get from() { return this._from.value; }
get handle() { return this._handle.value; }
get reason() { return this._reason.value; }
set days(int) { this._days.value = int; }
set seconds(int) { this._seconds.value = int; }
set subject(str) { this._subject.value = str; }
set from(str) { this._from.value = str; }
set handle(str) { this._handle.value = str; }
set reason(str) { this._reason.value = str; }
pushArguments(args)
{
this._reason.value = args.pop().value; // GrammarQuotedString
args.forEach((arg, i) => {
if (':mime' === arg) {
this.mime = true;
} else if (':addresses' === args[i-1]) {
this.addresses = arg; // GrammarStringList
} else if (':' === args[i-1][0]) {
// :days, :seconds, :subject, :from, :handle
this[args[i-1].replace(':','_')].value = arg.value;
}
});
}
}

View file

@ -0,0 +1,94 @@
/**
* https://tools.ietf.org/html/rfc5232
*/
import {
GrammarCommand,
GrammarQuotedString,
GrammarString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
class FlagCommand extends GrammarCommand
{
constructor()
{
super();
this._variablename = new GrammarQuotedString;
this.list_of_flags = new GrammarStringList;
}
get require() { return 'imap4flags'; }
toString()
{
return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags.toString() + ';';
}
get variablename()
{
return this._variablename.value;
}
set variablename(value)
{
this._variablename.value = value;
}
pushArguments(args)
{
if (args[1]) {
if (args[0] instanceof GrammarQuotedString) {
this._variablename = args[0];
}
if (args[1] instanceof GrammarString) {
this.list_of_flags = args[1];
}
} else if (args[0] instanceof GrammarString) {
this.list_of_flags = args[0];
}
}
}
export class SetFlagCommand extends FlagCommand
{
}
export class AddFlagCommand extends FlagCommand
{
}
export class RemoveFlagCommand extends FlagCommand
{
}
export class HasFlagTest extends GrammarTest
{
constructor()
{
super();
this.variable_list = new GrammarStringList;
this.list_of_flags = new GrammarStringList;
}
get require() { return 'imap4flags'; }
toString()
{
return 'hasflag'
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.variable_list.toString()
+ ' ' + this.list_of_flags.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg;
}
});
}
}

View file

@ -0,0 +1,70 @@
/**
* https://tools.ietf.org/html/rfc5235
*/
import {
GrammarQuotedString,
GrammarString,
GrammarTest
} from 'Sieve/Grammar';
export class SpamTestTest extends GrammarTest
{
constructor()
{
super();
this.percent = false, // 0 - 100 else 0 - 10
this.value = new GrammarQuotedString;
}
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
toString()
{
return 'spamtest'
+ (this.percent ? ' :percent' : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.value;
}
pushArguments(args)
{
args.forEach(arg => {
if (':percent' === arg) {
this.percent = true;
} else if (arg instanceof GrammarString) {
this.value = arg;
}
});
}
}
export class VirusTestTest extends GrammarTest
{
constructor()
{
super();
this.value = new GrammarQuotedString; // 1 - 5
}
get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
toString()
{
return 'virustest'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.value;
}
pushArguments(args)
{
args.forEach(arg => {
if (arg instanceof GrammarString) {
this.value = arg;
}
});
}
}

View file

@ -0,0 +1,93 @@
/**
* https://tools.ietf.org/html/rfc5260
*/
import {
GrammarNumber,
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class DateTest extends GrammarTest
{
constructor()
{
super();
this.zone = new GrammarQuotedString;
this.originalzone = false;
this.header_name = new GrammarQuotedString;
this.date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
// rfc5260#section-6
this.index = new GrammarNumber;
this.last = false;
}
// get require() { return ['date','index']; }
get require() { return 'date'; }
toString()
{
return 'date'
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.header_name
+ ' ' + this.date_part
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.date_part = args.pop();
this.header_name = args.pop();
args.forEach((arg, i) => {
if (':originalzone' === arg) {
this.originalzone = true;
} else if (':last' === arg) {
this.last = true;
} else if (':zone' === args[i-1]) {
this.zone.value = arg.value;
} else if (':index' === args[i-1]) {
this.index.value = arg.value;
}
});
}
}
export class CurrentDateTest extends GrammarTest
{
constructor()
{
super();
this.zone = new GrammarQuotedString;
this.date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'date'; }
toString()
{
return 'currentdate'
+ (this.zone.length ? ' :zone ' + this.zone : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.date_part
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.date_part = args.pop();
args.forEach((arg, i) => {
if (':zone' === args[i-1]) {
this.zone.value = arg.value;
}
});
}
}

View file

@ -0,0 +1,85 @@
/**
* https://tools.ietf.org/html/rfc5293
*/
import {
GrammarCommand,
GrammarNumber,
GrammarQuotedString,
GrammarString,
GrammarStringList
} from 'Sieve/Grammar';
export class AddHeaderCommand extends GrammarCommand
{
constructor()
{
super();
this.last = false;
this.field_name = new GrammarQuotedString;
this.value = new GrammarQuotedString;
}
get require() { return 'editheader'; }
toString()
{
return this.identifier
+ (this.last ? ' :last' : '')
+ ' ' + this.field_name
+ ' ' + this.value + ';';
}
pushArguments(args)
{
this.value = args.pop();
this.field_name = args.pop();
this.last = args.includes(':last');
}
}
export class DeleteHeaderCommand extends GrammarCommand
{
constructor()
{
super();
this.index = new GrammarNumber;
this.last = false;
this.comparator = '',
this.match_type = ':is',
this.field_name = new GrammarQuotedString;
this.value_patterns = new GrammarStringList;
}
get require() { return 'editheader'; }
toString()
{
return this.identifier
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.field_name
+ ' ' + this.value_patterns + ';';
}
pushArguments(args)
{
let l = args.length - 1;
args.forEach((arg, i) => {
if (':last' === arg) {
this.last = true;
} else if (':index' === args[i-1]) {
this.index.value = arg.value;
args[i] = null;
}
});
if (args[l-1] instanceof GrammarString) {
this.field_name = args[l-1];
this.value_patterns = args[l];
} else {
this.field_name = args[l];
}
}
}

View file

@ -0,0 +1,81 @@
/**
* https://tools.ietf.org/html/rfc5429
*/
import {
GrammarCommand,
GrammarQuotedString,
GrammarString
} from 'Sieve/Grammar';
/**
* https://tools.ietf.org/html/rfc5429#section-2.1
*/
export class ErejectCommand extends GrammarCommand
{
constructor()
{
super();
this._reason = new GrammarQuotedString;
}
get require() { return 'ereject'; }
toString()
{
return 'ereject ' + this._reason + ';';
}
get reason()
{
return this._reason.value;
}
set reason(value)
{
this._reason.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._reason = args[0];
}
}
}
/**
* https://tools.ietf.org/html/rfc5429#section-2.2
*/
export class RejectCommand extends GrammarCommand
{
constructor()
{
super();
this._reason = new GrammarQuotedString;
}
get require() { return 'reject'; }
toString()
{
return 'reject ' + this._reason + ';';
}
get reason()
{
return this._reason.value;
}
set reason(value)
{
this._reason.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._reason = args[0];
}
}
}

View file

@ -0,0 +1,123 @@
/**
* https://tools.ietf.org/html/rfc5435
*/
import {
GrammarCommand,
GrammarNumber,
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
/**
* https://datatracker.ietf.org/doc/html/rfc5435#section-3
*/
export class NotifyCommand extends GrammarCommand
{
constructor()
{
super();
this._method = new GrammarQuotedString;
this._from = new GrammarQuotedString;
this._importance = new GrammarNumber;
this.options = new GrammarStringList;
this._message = new GrammarQuotedString;
}
get method() { return this._method.value; }
get from() { return this._from.value; }
get importance() { return this._importance.value; }
get message() { return this._message.value; }
set method(str) { this._method.value = str; }
set from(str) { this._from.value = str; }
set importance(int) { this._importance.value = int; }
set message(str) { this._message.value = str; }
get require() { return 'enotify'; }
toString()
{
let result = 'notify';
if (this._from.value) {
result += ' :from ' + this._from;
}
if (0 < this._importance.value) {
result += ' :importance ' + this._importance;
}
if (this.options.length) {
result += ' :options ' + this.options;
}
if (this._message.value) {
result += ' :message ' + this._message;
}
return result + ' ' + this._method;
}
pushArguments(args)
{
this._method.value = args.pop().value; // GrammarQuotedString
args.forEach((arg, i) => {
if (':options' === args[i-1]) {
this.options = arg; // GrammarStringList
} else if (':' === args[i-1][0]) {
// :from, :importance, :message
this[args[i-1].replace(':','_')].value = arg.value;
}
});
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5435#section-4
*/
export class ValidNotifyMethodTest extends GrammarTest
{
constructor()
{
super();
this.notification_uris = new GrammarStringList;
}
toString()
{
return 'valid_notify_method ' + this.notification_uris;
}
pushArguments(args)
{
this.notification_uris = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5435#section-5
*/
export class NotifyMethodCapabilityTest extends GrammarTest
{
constructor()
{
super();
this.notification_uri = new GrammarQuotedString;
this.notification_capability = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
toString()
{
return 'valid_notify_method '
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this.match_type ? ' ' + this.match_type : '')
+ this.notification_uri
+ this.notification_capability
+ this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.notification_capability = args.pop();
this.notification_uri = args.pop();
}
}

View file

@ -0,0 +1,58 @@
/**
* https://tools.ietf.org/html/rfc5463
*/
import {
GrammarCommand,
GrammarTest,
GrammarQuotedString,
GrammarStringList
} from 'Sieve/Grammar';
/**
* https://datatracker.ietf.org/doc/html/rfc5463#section-4
*/
export class IHaveTest extends GrammarTest
{
constructor()
{
super();
this.capabilities = new GrammarStringList;
}
get require() { return 'ihave'; }
toString()
{
return 'ihave ' + this.capabilities;
}
pushArguments(args)
{
this.capabilities = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5463#section-5
*/
export class ErrorCommand extends GrammarCommand
{
constructor()
{
super();
this.message = new GrammarQuotedString;
}
get require() { return 'ihave'; }
toString()
{
return 'error ' + this.message + ';';
}
pushArguments(args)
{
this.message = args.pop();
}
}

View file

@ -0,0 +1,151 @@
/**
* https://tools.ietf.org/html/rfc5490
*/
import {
GrammarTest,
GrammarQuotedString,
GrammarStringList
} from 'Sieve/Grammar';
/**
* https://datatracker.ietf.org/doc/html/rfc5490#section-3.1
*/
export class MailboxExistsTest extends GrammarTest
{
constructor()
{
super();
this.mailbox_names = new GrammarStringList;
}
get require() { return 'mailbox'; }
toString()
{
return 'mailboxexists ' + this.mailbox_names + ';';
}
pushArguments(args)
{
if (args[0] instanceof GrammarStringList) {
this.mailbox_names = args[0];
}
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5490#section-3.3
*/
export class MetadataTest extends GrammarTest
{
constructor()
{
super();
this.mailbox = new GrammarQuotedString;
this.annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'mboxmetadata'; }
toString()
{
return 'metadata '
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.mailbox
+ ' ' + this.annotation_name
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.annotation_name = args.pop();
this.mailbox = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5490#section-3.4
*/
export class MetadataExistsTest extends GrammarTest
{
constructor()
{
super();
this.mailbox = new GrammarQuotedString;
this.annotation_names = new GrammarStringList;
}
get require() { return 'mboxmetadata'; }
toString()
{
return 'metadataexists '
+ ' ' + this.mailbox
+ ' ' + this.annotation_names;
}
pushArguments(args)
{
this.annotation_names = args.pop();
this.mailbox = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5490#section-3.3
*/
export class ServerMetadataTest extends GrammarTest
{
constructor()
{
super();
this.annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'servermetadata'; }
toString()
{
return 'servermetadata '
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.annotation_name
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.annotation_name = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5490#section-3.4
*/
export class ServerMetadataExistsTest extends GrammarTest
{
constructor()
{
super();
this.annotation_names = new GrammarStringList;
}
get require() { return 'servermetadata'; }
toString()
{
return 'servermetadataexists '
+ ' ' + this.annotation_names;
}
pushArguments(args)
{
this.annotation_names = args.pop();
}
}

View file

@ -0,0 +1,47 @@
/**
* https://tools.ietf.org/html/rfc6609
*/
import {
GrammarCommand,
GrammarQuotedString
} from 'Sieve/Grammar';
export class IncludeCommand extends GrammarCommand
{
constructor()
{
super();
this.global = false; // ':personal' / ':global';
this.once = false;
this.optional = false;
this.value = new GrammarQuotedString;
}
get require() { return 'include'; }
toString()
{
return this.identifier
+ (this.global ? ' :global' : '')
+ (this.once ? ' :once' : '')
+ (this.optional ? ' :optional' : '')
+ ' ' + this.value + ';';
}
pushArguments(args)
{
args.forEach(arg => {
if (':global' === arg || ':once' === arg || ':optional' === arg) {
this[arg.substr(1)] = true;
} else if (arg instanceof GrammarQuotedString) {
this.value = arg;
}
});
}
}
export class ReturnCommand extends GrammarCommand
{
get require() { return 'include'; }
}

277
dev/Sieve/Grammar.js Normal file
View file

@ -0,0 +1,277 @@
/**
* https://tools.ietf.org/html/rfc5228#section-8.2
*/
import {
QUOTED_TEXT,
HASH_COMMENT,
MULTILINE_LITERAL,
MULTILINE_DOTSTART
} from 'Sieve/RegEx';
import {
arrayToString
} from 'Sieve/Utils';
/**
* abstract
*/
export class GrammarString /*extends String*/
{
constructor(value = '')
{
this._value = value;
}
toString()
{
return this._value;
}
get value()
{
return this._value;
}
set value(value)
{
this._value = value;
}
get length()
{
return this._value.length;
}
}
/**
* abstract
*/
export class GrammarComment extends GrammarString
{
}
/**
* https://tools.ietf.org/html/rfc5228#section-2.9
*/
export class GrammarCommand
{
constructor(identifier)
{
this.identifier = identifier || this.constructor.name.toLowerCase().replace('command', '');
this.arguments = [];
this.commands = new GrammarCommands;
}
toString()
{
let result = this.identifier;
if (this.arguments.length) {
result += ' ' + arrayToString(this.arguments, ' ');
}
return result + (
this.commands.length ? ' ' + this.commands.toString() : ';'
);
}
getComparators()
{
return ['i;ascii-casemap'];
}
getMatchTypes()
{
return [':is', ':contains', ':matches'];
}
pushArguments(args)
{
this.arguments = args;
}
}
export class GrammarCommands extends Array
{
toString()
{
return this.length
? '{\r\n\t' + arrayToString(this, '\r\n\t') + '\r\n}'
: '{}';
}
push(value)
{
if (value instanceof GrammarCommand || value instanceof GrammarComment) {
super.push(value);
}
}
}
export class GrammarBracketComment extends GrammarComment
{
toString()
{
return '/* ' + super.toString() + ' */';
}
}
export class GrammarHashComment extends GrammarComment
{
toString()
{
return '# ' + super.toString();
}
}
export class GrammarNumber /*extends Number*/
{
constructor(value = '0')
{
this._value = value;
}
toString()
{
return this._value;
}
get value()
{
return this._value;
}
set value(value)
{
this._value = value;
}
}
export class GrammarStringList extends Array
{
toString()
{
if (1 < this.length) {
return '[' + this.join(',') + ']';
}
return this.length ? this[0] : '';
}
push(value)
{
if (!(value instanceof GrammarString)) {
value = new GrammarQuotedString(value);
}
super.push(value);
}
}
const StringListRegEx = RegExp('(?:^\\s*|\\s*,\\s*)(?:"(' + QUOTED_TEXT + ')"|text:[ \\t]*('
+ HASH_COMMENT + ')?\\r\\n'
+ '((?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*)'
+ '\\.\\r\\n)', 'gm');
GrammarStringList.fromString = list => {
let string,
obj = new GrammarStringList;
list = list.replace(/^[\r\n\t[]+/, '');
while ((string = StringListRegEx.exec(list))) {
if (string[3]) {
obj.push(new GrammarMultiLine(string[3], string[2]));
} else {
obj.push(new GrammarQuotedString(string[1]));
}
}
return obj;
}
export class GrammarQuotedString extends GrammarString
{
toString()
{
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-8.1
*/
export class GrammarMultiLine extends GrammarString
{
constructor(value, comment = '')
{
super();
this.value = value;
this.comment = comment;
}
toString()
{
return 'text:'
+ (this.comment ? '# ' + this.comment : '') + "\r\n"
+ this.value
+ "\r\n.\r\n";
}
}
const MultiLineRegEx = RegExp('text:[ \\t]*(' + HASH_COMMENT + ')?\\r\\n'
+ '((?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*)'
+ '\\.\\r\\n', 'm');
GrammarMultiLine.fromString = string => {
string = string.match(MultiLineRegEx);
if (string[2]) {
return new GrammarMultiLine(string[2].replace(/\r\n$/, ''), string[1]);
}
return new GrammarMultiLine();
}
/**
* https://tools.ietf.org/html/rfc5228#section-5
*/
export class GrammarTest
{
constructor(identifier)
{
this.identifier = identifier || this.constructor.name.toLowerCase().replace(/test$/, '');
// Almost every test has a comparator and match_type, so define them here
this.comparator = '',
this.match_type = ':is',
this.arguments = [];
}
toString()
{
return (this.identifier
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this.match_type ? ' ' + this.match_type : '')
+ ' ' + arrayToString(this.arguments, ' ')).trim();
}
pushArguments(args)
{
this.arguments = args;
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.2
* https://tools.ietf.org/html/rfc5228#section-5.3
*/
export class GrammarTestList extends Array
{
toString()
{
if (1 < this.length) {
// return '(\r\n\t' + arrayToString(this, ',\r\n\t') + '\r\n)';
return '(' + this.join(', ') + ')';
}
return this.length ? this[0] : '';
}
push(value)
{
if (!(value instanceof GrammarTest)) {
throw 'Not an instanceof Test';
}
super.push(value);
}
}

121
dev/Sieve/Model/Abstract.js Normal file
View file

@ -0,0 +1,121 @@
import { forEachObjectValue, forEachObjectEntry, koComputable } from 'Sieve/Utils';
function typeCast(curValue, newValue) {
if (null != curValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
}
if (Array.isArray(curValue) && !Array.isArray(newValue))
return [];
}
}
return newValue;
}
export class AbstractModel {
constructor() {
/*
if (new.target === AbstractModel) {
throw new Error("Can't instantiate AbstractModel!");
}
*/
this.disposables = [];
}
addObservables(observables) {
forEachObjectEntry(observables, (key, value) =>
this[key] || (this[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value))
);
}
addComputables(computables) {
forEachObjectEntry(computables, (key, fn) => this[key] = koComputable(fn));
}
addSubscribables(subscribables) {
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
}
/** Called by delegateRunOnDestroy */
onDestroy() {
/** dispose ko subscribables */
this.disposables.forEach(disposable => {
disposable && typeof disposable.dispose === 'function' && disposable.dispose();
});
/** clear object entries */
// forEachObjectEntry(this, (key, value) => {
forEachObjectValue(this, value => {
/** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */
// dispose(value);
/** clear object value */
// this[key] = null; // TODO: issue with Contacts view
});
// this.disposables = [];
}
/**
* @static
* @param {FetchJson} json
* @returns {boolean}
*/
static validJson(json) {
return !!(json && ('Object/'+this.name.replace('Model', '') === json['@Object']));
}
/**
* @static
* @param {FetchJson} json
* @returns {*Model}
*/
static reviveFromJson(json) {
let obj = this.validJson(json) ? new this() : null;
obj && obj.revivePropertiesFromJson(json);
return obj;
}
revivePropertiesFromJson(json) {
let model = this.constructor;
if (!model.validJson(json)) {
return false;
}
forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key])
{
case 'function':
if (ko.isObservable(this[key])) {
this[key](typeCast(this[key](), value));
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
}
// else console.log(model.name + '.' + key + ' is a function');
break;
case 'boolean':
case 'number':
case 'object':
case 'string':
this[key] = typeCast(this[key], value);
break;
// fall through
case 'undefined':
default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
} catch (e) {
console.log(model.name + '.' + key);
console.error(e);
}
});
return true;
}
}

View file

@ -1,14 +1,11 @@
import ko from 'ko';
import { koArrayWithDestroy } from 'Sieve/Utils';
import { isNonEmptyArray, pString } from 'Common/Utils';
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { i18n } from 'Common/Translator';
import { getFolderFromCacheList } from 'Common/Cache';
//import { getFolderFromCacheList } from 'Common/Cache';
import { AccountUserStore } from 'Stores/User/Account';
//import { AccountUserStore } from 'Stores/User/Account';
import { FilterConditionModel } from 'Model/FilterCondition';
import { AbstractModel } from 'Knoin/AbstractModel';
import { FilterConditionModel } from 'Sieve/Model/FilterCondition';
import { AbstractModel } from 'Sieve/Model/Abstract';
/**
* @enum {string}
@ -38,7 +35,7 @@ export class FilterModel extends AbstractModel {
this.addObservables({
enabled: true,
deleteAccess: false,
askDelete: false,
canBeDeleted: true,
name: '',
@ -65,11 +62,12 @@ export class FilterModel extends AbstractModel {
actionType: FilterAction.MoveTo
});
this.conditions = ko.observableArray();
this.conditions = koArrayWithDestroy();
const fGetRealFolderName = (folderFullNameRaw) => {
const folder = getFolderFromCacheList(folderFullNameRaw);
return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullNameRaw;
const fGetRealFolderName = folderFullName => {
// const folder = getFolderFromCacheList(folderFullName);
// return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName;
return folderFullName;
};
this.addComputables({
@ -79,23 +77,23 @@ export class FilterModel extends AbstractModel {
switch (this.actionType()) {
case FilterAction.MoveTo:
result = i18n(root + 'MOVE_TO', {
result = rl.i18n(root + 'MOVE_TO', {
FOLDER: fGetRealFolderName(actionValue)
});
break;
case FilterAction.Forward:
result = i18n(root + 'FORWARD_TO', {
result = rl.i18n(root + 'FORWARD_TO', {
EMAIL: actionValue
});
break;
case FilterAction.Vacation:
result = i18n(root + 'VACATION_MESSAGE');
result = rl.i18n(root + 'VACATION_MESSAGE');
break;
case FilterAction.Reject:
result = i18n(root + 'REJECT');
result = rl.i18n(root + 'REJECT');
break;
case FilterAction.Discard:
result = i18n(root + 'DISCARD');
result = rl.i18n(root + 'DISCARD');
break;
// no default
}
@ -211,11 +209,10 @@ export class FilterModel extends AbstractModel {
removeCondition(oConditionToDelete) {
this.conditions.remove(oConditionToDelete);
delegateRunOnDestroy(oConditionToDelete);
}
setRecipients() {
this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
}
/**
@ -226,16 +223,10 @@ export class FilterModel extends AbstractModel {
static reviveFromJson(json) {
const filter = super.reviveFromJson(json);
if (filter) {
filter.id = pString(json.ID);
filter.conditions([]);
if (isNonEmptyArray(json.Conditions)) {
filter.conditions(
json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
);
}
filter.id = filter.id ? '' + filter.id : '';
filter.conditions(
json.Conditions ? json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) : []
);
filter.actionKeep(0 != json.Keep);
filter.actionNoStop(0 == json.Stop);
filter.actionMarkAsRead(1 == json.MarkAsRead);

View file

@ -1,6 +1,6 @@
import ko from 'ko';
import { koComputable } from 'Sieve/Utils';
import { AbstractModel } from 'Knoin/AbstractModel';
import { AbstractModel } from 'Sieve/Model/Abstract';
/**
* @enum {string}
@ -43,7 +43,7 @@ export class FilterConditionModel extends AbstractModel {
valueSecondError: false
});
this.template = ko.computed(() => {
this.template = koComputable(() => {
const template = 'SettingsFiltersCondition';
switch (this.field()) {
case FilterConditionField.Body:

View file

@ -1,8 +1,6 @@
import ko from 'ko';
import { AbstractModel } from 'Knoin/AbstractModel';
import { FilterModel } from 'Model/Filter';
import { isNonEmptyArray, pString } from 'Common/Utils';
import { AbstractModel } from 'Sieve/Model/Abstract';
import { FilterModel } from 'Sieve/Model/Filter';
import { koArrayWithDestroy } from 'Sieve/Utils';
const SIEVE_FILE_NAME = 'rainloop.user';
@ -154,12 +152,12 @@ function filtersToSieveScript(filters)
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
}
paramValue = pString(filter.actionValueThird()).trim();
paramValue = (filter.actionValueThird() || '').trim();
if (paramValue.length) {
days = Math.max(1, parseInt(paramValue, 10));
}
paramValue = pString(filter.actionValueFourth()).trim()
paramValue = (filter.actionValueFourth() || '').trim()
if (paramValue.length) {
paramValue = paramValue.split(',').map(email =>
email.trim().length ? quote(email) : ''
@ -247,13 +245,6 @@ function sieveScriptToFilters(script)
}
}
}
/*
// TODO: branch sieveparser
// https://github.com/the-djmaze/snappymail/tree/sieveparser/dev/Sieve
else if (script.length && window.Sieve) {
let tree = window.Sieve.parseScript(script);
}
*/
return filters;
}
@ -269,13 +260,12 @@ export class SieveScriptModel extends AbstractModel
exists: false,
nameError: false,
bodyError: false,
deleteAccess: false,
askDelete: false,
canBeDeleted: true,
hasChanges: false
});
this.filters = ko.observableArray();
this.filters = koArrayWithDestroy();
// this.saving = ko.observable(false).extend({ debounce: 200 });
this.addSubscribables({
@ -297,8 +287,7 @@ export class SieveScriptModel extends AbstractModel
verify() {
this.nameError(!this.name().trim());
this.bodyError(this.allowFilters() ? !this.filters.length : !this.body().trim());
return !this.nameError() && !this.bodyError();
return !this.nameError();
}
toJson() {
@ -327,7 +316,7 @@ export class SieveScriptModel extends AbstractModel
if (script) {
if (script.allowFilters()) {
script.filters(
isNonEmptyArray(json.filters)
Array.isArray(json.filters) && json.filters.length
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
: sieveScriptToFilters(script.body())
);

407
dev/Sieve/Parser.js Normal file
View file

@ -0,0 +1,407 @@
/**
* https://tools.ietf.org/html/rfc5228#section-8
*/
import { capa, forEachObjectEntry } from 'Sieve/Utils';
import {
BRACKET_COMMENT,
HASH_COMMENT,
IDENTIFIER,
MULTI_LINE,
NUMBER,
QUOTED_STRING,
STRING_LIST,
TAG
} from 'Sieve/RegEx';
import {
GrammarBracketComment,
GrammarCommand,
GrammarHashComment,
GrammarMultiLine,
GrammarNumber,
GrammarQuotedString,
GrammarStringList,
GrammarTest,
GrammarTestList
} from 'Sieve/Grammar';
import {
ConditionalCommand,
DiscardCommand,
ElsIfCommand,
ElseCommand,
FileIntoCommand,
IfCommand,
KeepCommand,
RedirectCommand,
RequireCommand,
StopCommand
} from 'Sieve/Commands';
import {
AddressTest,
AllOfTest,
AnyOfTest,
EnvelopeTest,
ExistsTest,
FalseTest,
HeaderTest,
NotTest,
SizeTest,
TrueTest
} from 'Sieve/Tests';
import { BodyTest } from 'Sieve/Extensions/rfc5173';
import { EnvironmentTest } from 'Sieve/Extensions/rfc5183';
import { SetCommand, StringTest } from 'Sieve/Extensions/rfc5229';
import { VacationCommand } from 'Sieve/Extensions/rfc5230';
import {
SetFlagCommand,
AddFlagCommand,
RemoveFlagCommand,
HasFlagTest
} from 'Sieve/Extensions/rfc5232';
import { SpamTestTest, VirusTestTest } from 'Sieve/Extensions/rfc5235';
import { DateTest, CurrentDateTest } from 'Sieve/Extensions/rfc5260';
import { AddHeaderCommand, DeleteHeaderCommand } from 'Sieve/Extensions/rfc5293';
import { ErejectCommand, RejectCommand } from 'Sieve/Extensions/rfc5429';
import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from 'Sieve/Extensions/rfc5435';
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
import { IncludeCommand, ReturnCommand } from 'Sieve/Extensions/rfc6609';
const
AllCommands = {
// Control commands
if: IfCommand,
elsif: ElsIfCommand,
else: ElseCommand,
conditional: ConditionalCommand,
require: RequireCommand,
stop: StopCommand,
// Action commands
discard: DiscardCommand,
fileinto: FileIntoCommand,
keep: KeepCommand,
redirect: RedirectCommand,
// Test commands
address: AddressTest,
allof: AllOfTest,
anyof: AnyOfTest,
envelope: EnvelopeTest,
exists: ExistsTest,
false: FalseTest,
header: HeaderTest,
not: NotTest,
size: SizeTest,
true: TrueTest,
// rfc5173
body: BodyTest,
// rfc5183
environment: EnvironmentTest,
// rfc5229
set: SetCommand,
string: StringTest,
// rfc5230
vacation: VacationCommand,
// rfc5232
setflag: SetFlagCommand,
addflag: AddFlagCommand,
removeflag: RemoveFlagCommand,
hasflag: HasFlagTest,
// rfc5235
spamtest: SpamTestTest,
virustest: VirusTestTest,
// rfc5260
date: DateTest,
currentdate: CurrentDateTest,
// rfc5293
AddHeaderCommand,
DeleteHeaderCommand,
// rfc5429
ereject: ErejectCommand,
reject: RejectCommand,
// rfc5435
notify: NotifyCommand,
valid_notify_method: ValidNotifyMethodTest,
notify_method_capability: NotifyMethodCapabilityTest,
// rfc5463
ihave: IHaveTest,
error: ErrorCommand,
// rfc5490
mailboxexists: MailboxExistsTest,
metadata: MetadataTest,
metadataexists: MetadataExistsTest,
// rfc6609
include: IncludeCommand,
return: ReturnCommand
},
T_UNKNOWN = 0,
T_STRING_LIST = 1,
T_QUOTED_STRING = 2,
T_MULTILINE_STRING = 3,
T_HASH_COMMENT = 4,
T_BRACKET_COMMENT = 5,
T_BLOCK_START = 6,
T_BLOCK_END = 7,
T_LEFT_PARENTHESIS = 8,
T_RIGHT_PARENTHESIS = 9,
T_COMMA = 10,
T_SEMICOLON = 11,
T_TAG = 12,
T_IDENTIFIER = 13,
T_NUMBER = 14,
T_WHITESPACE = 15,
TokensRegEx = '(' + [
/* T_STRING_LIST */ STRING_LIST,
/* T_QUOTED_STRING */ QUOTED_STRING,
/* T_MULTILINE_STRING */ MULTI_LINE,
/* T_HASH_COMMENT */ HASH_COMMENT,
/* T_BRACKET_COMMENT */ BRACKET_COMMENT,
/* T_BLOCK_START */ '\\{',
/* T_BLOCK_END */ '\\}',
/* T_LEFT_PARENTHESIS */ '\\(', // anyof / allof
/* T_RIGHT_PARENTHESIS */ '\\)', // anyof / allof
/* T_COMMA */ ',',
/* T_SEMICOLON */ ';',
/* T_TAG */ TAG,
/* T_IDENTIFIER */ IDENTIFIER,
/* T_NUMBER */ NUMBER,
/* T_WHITESPACE */ '(?: |\\r\\n|\\t)+',
/* T_UNKNOWN */ '[^ \\r\\n\\t]+'
].join(')|(') + ')';
export const parseScript = (script, name = 'script.sieve') => {
script = script.replace(/\r?\n/g, '\r\n');
// Only activate available commands
const Commands = {};
forEachObjectEntry(AllCommands, (key, cmd) => {
const requires = (new cmd).require;
if (!requires
|| (Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string))
) {
Commands[key] = cmd;
}
});
let match,
line = 1,
tree = [],
// Create one regex to find the tokens
// Use exec() to forward since lastIndex
regex = RegExp(TokensRegEx, 'gm'),
levels = [],
command = null,
requires = [],
args = [];
const
error = message => {
// throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, name, line)
throw new SyntaxError(message + ' on line ' + line
+ ' around:\n\n' + script.substr(regex.lastIndex - 20, 30), name, line)
},
pushArg = arg => {
command || error('Argument not part of command');
let prev_arg = args[args.length-1];
if (':is' === arg || ':contains' === arg || ':matches' === arg) {
command.match_type = arg;
} else if (':value' === prev_arg || ':count' === prev_arg) {
// Sieve relational [RFC5231] match types
/^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
command.match_type = prev_arg + ' ' + arg;
--args.length;
// requires.push('relational');
} else if (':comparator' === prev_arg) {
command.comparator = arg;
--args.length;
} else {
args.push(arg);
}
},
pushArgs = () => {
if (args.length) {
command && command.pushArguments(args);
args = [];
}
};
levels.last = () => levels[levels.length - 1];
while ((match = regex.exec(script))) {
// the last element in match will contain the matched value and the key will be the type
let type = match.findIndex((v,i) => 0 < i && undefined !== v),
value = match[type];
// create the part
switch (type)
{
case T_IDENTIFIER: {
pushArgs();
value = value.toLowerCase();
let new_command;
if ('if' === value) {
new_command = new ConditionalCommand(value);
} else if ('elsif' === value || 'else' === value) {
// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
new_command = new ConditionalCommand(value);
} else if (Commands[value]) {
if ('allof' === value || 'anyof' === value) {
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
}
new_command = new Commands[value]();
} else {
console.error('Unknown command: ' + value);
if (command && (
command instanceof ConditionalCommand
|| command instanceof NotTest
|| command.tests instanceof GrammarTestList)) {
new_command = new GrammarTest(value);
} else {
new_command = new GrammarCommand(value);
}
}
if (new_command instanceof GrammarTest) {
if (command instanceof ConditionalCommand || command instanceof NotTest) {
// if/elsif/else new_command
// not new_command
command.test = new_command;
} else if (command.tests instanceof GrammarTestList) {
// allof/anyof .tests[] new_command
command.tests.push(new_command);
} else {
error('Test "' + value + '" not allowed in "' + command.identifier + '" command');
}
} else if (command) {
if (command.commands) {
command.commands.push(new_command);
} else {
error('commands not allowed in "' + command.identifier + '" command');
}
} else {
tree.push(new_command);
}
levels.push(new_command);
command = new_command;
if (command.require) {
(Array.isArray(command.require) ? command.require : [command.require])
.forEach(string => requires.push(string));
}
if (command.comparator) {
requires.push('comparator-' + command.comparator);
}
break; }
// Arguments
case T_TAG:
pushArg(value.toLowerCase());
break;
case T_STRING_LIST:
pushArg(GrammarStringList.fromString(value));
break;
case T_MULTILINE_STRING:
pushArg(GrammarMultiLine.fromString(value));
break;
case T_QUOTED_STRING:
pushArg(new GrammarQuotedString(value.substr(1,value.length-2)));
break;
case T_NUMBER:
pushArg(new GrammarNumber(value));
break;
// Comments
case T_BRACKET_COMMENT:
case T_HASH_COMMENT: {
let obj = (T_HASH_COMMENT == type)
? new GrammarHashComment(value.substr(1).trim())
: new GrammarBracketComment(value.substr(2, value.length-4));
if (command) {
if (!command.comments) {
command.comments = [];
}
(command.commands || command.comments).push(obj);
} else {
tree.push(obj);
}
break; }
case T_WHITESPACE:
// (command ? command.commands : tree).push(value.trim());
command || tree.push(value.trim());
break;
// Command end
case T_SEMICOLON:
command || error('Semicolon not at end of command');
pushArgs();
if (command instanceof RequireCommand) {
command.capabilities.forEach(string => requires.push(string.value));
}
levels.pop();
command = levels.last();
break;
// Command block
case T_BLOCK_START:
pushArgs();
// https://tools.ietf.org/html/rfc5228#section-2.9
// Action commands do not take tests or blocks
while (command && !(command instanceof ConditionalCommand)) {
levels.pop();
command = levels.last();
}
command || error('Block start not part of control command');
break;
case T_BLOCK_END:
(command instanceof ConditionalCommand) || error('Block end has no matching block start');
levels.pop();
// prev_command = command;
command = levels.last();
break;
// anyof / allof ( ... , ... )
case T_LEFT_PARENTHESIS:
pushArgs();
while (command && !(command.tests instanceof GrammarTestList)) {
levels.pop();
command = levels.last();
}
command || error('Test start not part of anyof/allof test');
break;
case T_RIGHT_PARENTHESIS:
pushArgs();
levels.pop();
command = levels.last();
(command.tests instanceof GrammarTestList) || error('Test end not part of test-list');
break;
case T_COMMA:
pushArgs();
// Must be inside PARENTHESIS aka test-list
while (command && !(command.tests instanceof GrammarTestList)) {
levels.pop();
command = levels.last();
}
command || error('Comma not part of test-list');
break;
case T_UNKNOWN:
error('Invalid token ' + value);
}
// Set current script position
line += (value.split('\n').length - 1); // (value.match(/\n/g) || []).length;
}
tree.requires = requires;
return tree;
};

33
dev/Sieve/README.md Normal file
View file

@ -0,0 +1,33 @@
https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml
- [ ] RFC2852 envelope-deliverby / redirect-deliverby
- [ ] RFC3461 envelope-dsn / redirect-dsn
- [x] RFC3894 copy
- [ ] RFC4790 comparator-*
- [x] RFC5173 body
- [x] RFC5183 environment
- [x] RFC5228 encoded-character / envelope / fileinto
- [x] RFC5229 variables
- [x] RFC5230 vacation
- [x] RFC5231 relational
- [x] RFC5232 imap4flags
- [x] RFC5233 subaddress
- [x] RFC5235 spamtest / spamtestplus / virustest
- [x] RFC5260 date / index
- [x] RFC5293 editheader
- [x] RFC5429 ereject / reject
- [x] RFC5435 enotify
- [x] RFC5463 ihave
- [x] RFC5490 mailbox / mboxmetadata / servermetadata
- [ ] RFC5703 enclose / extracttext / foreverypart / mime / replace
- [x] RFC6131 vacation-seconds
- [ ] RFC6134 extlists
- [ ] RFC6558 convert
- [x] RFC6609 include
- [ ] RFC6785 imapsieve
- [ ] RFC7352 duplicate
- [ ] RFC8579 special-use
- [ ] RFC8580 fcc
- [ ] RFC regex https://tools.ietf.org/html/draft-ietf-sieve-regex-01
- [ ] vnd.cyrus.*
- [ ] vnd.dovecot.*

203
dev/Sieve/RegEx.js Normal file
View file

@ -0,0 +1,203 @@
/**
* https://tools.ietf.org/html/rfc5228#section-8
*/
export const
/**************************************************
* https://tools.ietf.org/html/rfc5228#section-8.1
**************************************************/
/**
* octet-not-crlf = %x01-09 / %x0B-0C / %x0E-FF
* a single octet other than NUL, CR, or LF
*/
OCTET_NOT_CRLF = '[^\\x00\\r\\n]',
/**
* octet-not-period = %x01-09 / %x0B-0C / %x0E-2D / %x2F-FF
* a single octet other than NUL, CR, LF, or period
*/
OCTET_NOT_PERIOD = '[^\\x00\\r\\n\\.]',
/**
* octet-not-qspecial = %x01-09 / %x0B-0C / %x0E-21 / %x23-5B / %x5D-FF
* a single octet other than NUL, CR, LF, double-quote, or backslash
*/
OCTET_NOT_QSPECIAL = '[^\\x00\\r\\n"\\\\]',
/**
* hash-comment = "#" *octet-not-crlf CRLF
*/
HASH_COMMENT = '#' + OCTET_NOT_CRLF + '*\\r\\n',
/**
* QUANTIFIER = "K" / "M" / "G"
*/
QUANTIFIER = '[KMGkmg]',
/**
* quoted-safe = CRLF / octet-not-qspecial
* either a CRLF pair, OR a single octet other than NUL, CR, LF, double-quote, or backslash
*/
QUOTED_SAFE = '\\r\\n|' + OCTET_NOT_QSPECIAL,
/**
* quoted-special = "\" (DQUOTE / "\")
* represents just a double-quote or backslash
*/
QUOTED_SPECIAL = '\\\\\\\\|\\\\"',
/**
* quoted-text = *(quoted-safe / quoted-special / quoted-other)
*/
QUOTED_TEXT = '(?:' + QUOTED_SAFE + '|' + QUOTED_SPECIAL + ')*',
/**
* multiline-literal = [ octet-not-period *octet-not-crlf ] CRLF
*/
MULTILINE_LITERAL = OCTET_NOT_PERIOD + OCTET_NOT_CRLF + '*\\r\\n',
/**
* multiline-dotstart = "." 1*octet-not-crlf CRLF
; A line containing only "." ends the multi-line.
; Remove a leading '.' if followed by another '.'.
*/
MULTILINE_DOTSTART = '\\.' + OCTET_NOT_CRLF + '+\\r\\n',
/**
* not-star = CRLF / %x01-09 / %x0B-0C / %x0E-29 / %x2B-FF
* either a CRLF pair, OR a single octet other than NUL, CR, LF, or star
*/
// NOT_STAR: '\\r\\n|[^\\x00\\r\\n*]',
/**
* not-star-slash = CRLF / %x01-09 / %x0B-0C / %x0E-29 / %x2B-2E / %x30-FF
* either a CRLF pair, OR a single octet other than NUL, CR, LF, star, or slash
*/
// NOT_STAR_SLASH: '\\r\\n|[^\\x00\\r\\n*\\\\]',
/**
* STAR = "*"
*/
// STAR = '\\*',
/**
* bracket-comment = "/*" *not-star 1*STAR *(not-star-slash *not-star 1*STAR) "/"
*/
BRACKET_COMMENT = '/\\*[\\s\\S]*?\\*/',
/**
* identifier = (ALPHA / "_") *(ALPHA / DIGIT / "_")
*/
IDENTIFIER = '[a-zA-Z_][a-zA-Z0-9_]*',
/**
* multi-line = "text:" *(SP / HTAB) (hash-comment / CRLF)
*(multiline-literal / multiline-dotstart)
"." CRLF
*/
MULTI_LINE = 'text:[ \\t]*(?:' + HASH_COMMENT + ')?\\r\\n'
+ '(?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*'
+ '\\.\\r\\n',
/**
* number = 1*DIGIT [ QUANTIFIER ]
*/
NUMBER = '[0-9]+' + QUANTIFIER + '?',
/**
* quoted-string = DQUOTE quoted-text DQUOTE
*/
QUOTED_STRING = '"' + QUOTED_TEXT + '"',
/**
* tag = ":" identifier
*/
TAG = ':[a-zA-Z_][a-zA-Z0-9_]*',
/**
* comment = bracket-comment / hash-comment
*/
// COMMENT = BRACKET_COMMENT + '|' + HASH_COMMENT;
/**************************************************
* https://tools.ietf.org/html/rfc5228#section-8.2
**************************************************/
/**
* string = quoted-string / multi-line
*/
STRING = QUOTED_STRING + '|' + MULTI_LINE,
/**
* string-list = "[" string *("," string) "]" / string
* if there is only a single string, the brackets are optional
*/
STRING_LIST = '\\[\\s*(?:' + STRING + ')(?:\\s*,\\s*(?:' + STRING + '))*\\s*\\]',
// + '|(?:' + STRING + ')',
/**
* argument = string-list / number / tag
*/
ARGUMENT = STRING_LIST + '|' + STRING + '|' + NUMBER + '|' + TAG;
/**
* arguments = *argument [ test / test-list ]
* This is not possible with regular expressions
*/
// ARGUMENTS = '(?:\\s+' . self::ARGUMENT . ')*(\\s+?:' . self::TEST . '|' . self::TEST_LIST . ')?',
/**
* block = "{" commands "}"
* This is not possible with regular expressions
*/
// BLOCK = '{' . self::COMMANDS . '}',
/**
* command = identifier arguments (";" / block)
* This is not possible with regular expressions
*/
// COMMAND = self::IDENTIFIER . self::ARGUMENTS . '\\s+(?:;|' . self::BLOCK . ')',
/**
* commands = *command
* This is not possible with regular expressions
*/
// COMMANDS = '(?:' . self::COMMAND . ')*',
/**
* start = commands
* This is not possible with regular expressions
*/
// START = self::COMMANDS,
/**
* test = identifier arguments
* This is not possible with regular expressions
*/
// TEST = self::IDENTIFIER . self::ARGUMENTS,
/**
* test-list = "(" test *("," test) ")"
* This is not possible with regular expressions
*/
// TEST_LIST = '\\(\\s*' . self::TEST . '(?:\\s*,\\s*' . self::TEST . ')*\\s*\\)',
/**************************************************
* https://tools.ietf.org/html/rfc5228#section-8.3
**************************************************/
/**
* ADDRESS-PART = ":localpart" / ":domain" / ":all"
*/
// ADDRESS_PART = ':localpart|:domain|:all',
/**
* COMPARATOR = ":comparator" string
*/
// COMPARATOR = ':comparator\\s+(?:' + STRING + ')';
/**
* MATCH-TYPE = ":is" / ":contains" / ":matches"
*/
// MATCH_TYPE = ':is|:contains|:matches'

288
dev/Sieve/Tests.js Normal file
View file

@ -0,0 +1,288 @@
/**
* https://tools.ietf.org/html/rfc5228#section-5
*/
import {
GrammarNumber,
GrammarString,
GrammarStringList,
GrammarTest,
GrammarTestList
} from 'Sieve/Grammar';
const
isAddressPart = tag => ':localpart' === tag || ':domain' === tag || ':all' === tag || isSubAddressPart(tag),
// https://tools.ietf.org/html/rfc5233
isSubAddressPart = tag => ':user' === tag || ':detail' === tag;
/**
* https://tools.ietf.org/html/rfc5228#section-5.1
*/
export class AddressTest extends GrammarTest
{
constructor()
{
super();
this.address_part = ':all';
this.header_list = new GrammarStringList;
this.key_list = new GrammarStringList;
// rfc5260#section-6
// this.index = new GrammarNumber;
// this.last = false;
}
get require() {
let requires = [];
isSubAddressPart(this.address_part) && requires.push('subaddress');
(this.last || (this.index && this.index.value)) && requires.push('index');
return requires;
}
toString()
{
return 'address'
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.address_part
+ ' ' + this.match_type
+ ' ' + this.header_list.toString()
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (isAddressPart(arg)) {
this.address_part = arg;
} else if (':last' === arg) {
this.last = true;
} else if (':index' === args[i-1]) {
this.index.value = arg.value;
} else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
this[args[i+1] ? 'header_list' : 'key_list'] = arg;
// (args[i+1] ? this.header_list : this.key_list) = arg;
}
});
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.2
*/
export class AllOfTest extends GrammarTest
{
constructor()
{
super();
this.tests = new GrammarTestList;
}
toString()
{
return 'allof ' + this.tests.toString();
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.3
*/
export class AnyOfTest extends GrammarTest
{
constructor()
{
super();
this.tests = new GrammarTestList;
}
toString()
{
return 'anyof ' + this.tests.toString();
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.4
*/
export class EnvelopeTest extends GrammarTest
{
constructor()
{
super();
this.address_part = ':all';
this.envelope_part = new GrammarStringList;
this.key_list = new GrammarStringList;
}
get require() { return isSubAddressPart(this.address_part) ? ['envelope','subaddress'] : 'envelope'; }
toString()
{
return 'envelope'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.address_part
+ ' ' + this.match_type
+ ' ' + this.envelope_part.toString()
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (isAddressPart(arg)) {
this.address_part = arg;
} else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
this[args[i+1] ? 'envelope_part' : 'key_list'] = arg;
// (args[i+1] ? this.envelope_part : this.key_list) = arg;
}
});
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.5
*/
export class ExistsTest extends GrammarTest
{
constructor()
{
super();
this.header_names = new GrammarStringList;
}
toString()
{
return 'exists ' + this.header_names.toString();
}
pushArguments(args)
{
if (args[0] instanceof GrammarStringList) {
this.header_names = args;
} else if (args[0] instanceof GrammarString) {
this.header_names.push(args[0].value);
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.6
*/
export class FalseTest extends GrammarTest
{
toString()
{
return "false";
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.7
*/
export class HeaderTest extends GrammarTest
{
constructor()
{
super();
this.address_part = ':all';
this.header_names = new GrammarStringList;
this.key_list = new GrammarStringList;
// rfc5260#section-6
// this.index = new GrammarNumber;
// this.last = false;
}
get require() {
let requires = [];
isSubAddressPart(this.address_part) && requires.push('subaddress');
(this.last || (this.index && this.index.value)) && requires.push('index');
return requires;
}
toString()
{
return 'header'
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.header_names.toString()
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (isAddressPart(arg)) {
this.address_part = arg;
} else if (':last' === arg) {
this.last = true;
} else if (':index' === args[i-1]) {
this.index.value = arg.value;
} else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
this[args[i+1] ? 'header_names' : 'key_list'] = arg;
// (args[i+1] ? this.header_names : this.key_list) = arg;
}
});
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.8
*/
export class NotTest extends GrammarTest
{
constructor()
{
super();
this.test = new GrammarTest;
}
toString()
{
return 'not ' + this.test;
}
pushArguments()
{
throw 'No arguments';
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.9
*/
export class SizeTest extends GrammarTest
{
constructor()
{
super();
this.mode = ':over'; // :under
this.limit = 0;
}
toString()
{
return 'size ' + this.mode + ' ' + this.limit;
}
pushArguments(args)
{
args.forEach(arg => {
if (':over' === arg || ':under' === arg) {
this.mode = arg;
} else if (arg instanceof GrammarNumber) {
this.limit = arg;
}
});
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-5.10
*/
export class TrueTest extends GrammarTest
{
toString()
{
return 'true';
}
}

Some files were not shown because too many files have changed in this diff Show more