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,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);
}
};