Merge branch 'master' into UserMailTemplates

# Conflicts:
#	dev/App/User.js
#	dev/Common/Enums.js
#	dev/Screen/User/Settings.js
#	dev/Settings/Admin/General.js
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
This commit is contained in:
djmaze 2022-03-15 15:00:14 +01:00
commit f7f56b3789
794 changed files with 96444 additions and 74188 deletions

View file

@ -7,17 +7,19 @@ DomainAdminStore.loading = ko.observable(false);
DomainAdminStore.fetch = () => {
DomainAdminStore.loading(true);
Remote.domainList((iError, data) => {
DomainAdminStore.loading(false);
if (!iError) {
DomainAdminStore(
Object.entries(data.Result).map(([name, [enabled, alias]]) => ({
name: name,
disabled: ko.observable(!enabled),
alias: alias,
deleteAccess: ko.observable(false)
}))
);
}
});
Remote.request('AdminDomainList',
(iError, data) => {
DomainAdminStore.loading(false);
if (!iError) {
DomainAdminStore(
data.Result.map(item => {
item.disabled = ko.observable(item.disabled);
item.askDelete = ko.observable(false);
return item;
})
);
}
}, {
IncludeAliases: 1
});
};

View file

@ -12,7 +12,7 @@ PackageAdminStore.error = ko.observable('');
PackageAdminStore.fetch = () => {
PackageAdminStore.loading(true);
Remote.packagesList((iError, data) => {
Remote.request('AdminPackagesList', (iError, data) => {
PackageAdminStore.loading(false);
if (iError) {
PackageAdminStore.real(false);

View file

@ -9,17 +9,17 @@ export const ThemeStore = {
userBackgroundHash: ko.observable(''),
isMobile: ko.observable($htmlCL.contains('rl-mobile')),
populate: function(){
populate: () => {
const themes = Settings.app('themes');
this.themes(isArray(themes) ? themes : []);
this.theme(SettingsGet('Theme'));
if (!this.isMobile()) {
this.userBackgroundName(SettingsGet('UserBackgroundName'));
this.userBackgroundHash(SettingsGet('UserBackgroundHash'));
ThemeStore.themes(isArray(themes) ? themes : []);
ThemeStore.theme(SettingsGet('Theme'));
if (!ThemeStore.isMobile()) {
ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName'));
ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash'));
}
leftPanelDisabled(this.isMobile());
leftPanelDisabled(ThemeStore.isMobile());
}
};

View file

@ -1,32 +1,13 @@
import ko from 'ko';
import { SettingsGet } from 'Common/Globals';
import { addObservablesTo } from 'Common/Utils';
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
export const AccountUserStore = {
accounts: ko.observableArray(),
accounts: koArrayWithDestroy(),
loading: ko.observable(false).extend({ debounce: 100 }),
getEmailAddresses: () => AccountUserStore.accounts.map(item => item ? item.email : null).filter(v => v),
accountsUnreadCount: ko.computed(() => 0),
// accountsUnreadCount: ko.computed(() => {
// let result = 0;
// AccountUserStore.accounts().forEach(item => {
// if (item) {
// result += item.count();
// }
// });
// return result;
// }),
populate: () => {
AccountUserStore.email(SettingsGet('Email'));
AccountUserStore.parentEmail(SettingsGet('ParentEmail'));
}
getEmailAddresses: () => AccountUserStore.accounts.map(item => item.email)
};
addObservablesTo(AccountUserStore, {
email: '',
parentEmail: '',
signature: ''
});

View file

@ -1,6 +1,5 @@
import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, SettingsGet } from 'Common/Globals';
import { addObservablesTo } from 'Common/Utils';
import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
import { addObservablesTo } from 'External/ko';
import { ThemeStore } from 'Stores/Theme';
export const AppUserStore = {
@ -8,7 +7,7 @@ export const AppUserStore = {
};
addObservablesTo(AppUserStore, {
focusedState: Scope.None,
focusedState: 'none',
threadsAllowed: false,
@ -16,12 +15,12 @@ addObservablesTo(AppUserStore, {
});
AppUserStore.focusedState.subscribe(value => {
switch (value) {
case Scope.MessageList:
case Scope.MessageView:
case Scope.FolderList:
['FolderList','MessageList','MessageView'].forEach(name => {
if (name === value) {
keyScope(value);
ThemeStore.isMobile() && leftPanelDisabled(Scope.FolderList !== value);
break;
}
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
}
let dom = elementById('V-Mail'+name);
dom && dom.classList.toggle('focused', name === value);
});
});

View file

@ -1,26 +1,51 @@
import ko from 'ko';
import { SettingsGet } from 'Common/Globals';
import { addObservablesTo } from 'Common/Utils';
import { pInt } from 'Common/Utils';
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
import Remote from 'Remote/User/Fetch';
export const ContactUserStore = ko.observableArray();
export const ContactUserStore = koArrayWithDestroy();
ContactUserStore.loading = ko.observable(false).extend({ debounce: 200 });
ContactUserStore.importing = ko.observable(false).extend({ debounce: 200 });
ContactUserStore.syncing = ko.observable(false).extend({ debounce: 200 });
addObservablesTo(ContactUserStore, {
allowSync: false,
allowSync: false, // Admin setting
enableSync: false,
syncUrl: '',
syncUser: '',
syncPass: ''
});
ContactUserStore.populate = function() {
this.allowSync(!!SettingsGet('ContactsSyncIsAllowed'));
this.enableSync(!!SettingsGet('EnableContactsSync'));
this.syncUrl(SettingsGet('ContactsSyncUrl'));
this.syncUser(SettingsGet('ContactsSyncUser'));
this.syncPass(SettingsGet('ContactsSyncPassword'));
/**
* @param {Function} fResultFunc
* @returns {void}
*/
ContactUserStore.sync = fResultFunc => {
if (ContactUserStore.enableSync()
&& !ContactUserStore.importing()
&& !ContactUserStore.syncing()
) {
ContactUserStore.syncing(true);
Remote.request('ContactsSync', (iError, oData) => {
ContactUserStore.syncing(false);
fResultFunc && fResultFunc(iError, oData);
}, null, 200000);
}
};
ContactUserStore.init = () => {
let value = !!SettingsGet('ContactsSyncIsAllowed');
ContactUserStore.allowSync(value);
if (value) {
ContactUserStore.enableSync(!!SettingsGet('EnableContactsSync'));
ContactUserStore.syncUrl(SettingsGet('ContactsSyncUrl'));
ContactUserStore.syncUser(SettingsGet('ContactsSyncUser'));
ContactUserStore.syncPass(SettingsGet('ContactsSyncPassword'));
setTimeout(ContactUserStore.sync, 10000);
value = pInt(SettingsGet('ContactsSyncInterval'));
value = 5 <= value ? (320 >= value ? value : 320) : 20;
setInterval(ContactUserStore.sync, value * 60000 + 5000);
}
};

View file

@ -1,15 +1,18 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { FolderType, FolderSortMode } from 'Common/EnumsUser';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'Common/Utils';
import { forEachObjectEntry } from 'Common/Utils';
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
import { Settings, SettingsGet } from 'Common/Globals';
//import Remote from 'Remote/User/Fetch'; Circular dependency
import { Settings } from 'Common/Globals';
//import Remote from 'Remote/User/Fetch'; // Circular dependency
export const FolderUserStore = new class {
constructor() {
addObservablesTo(this, {
const self = this;
addObservablesTo(self, {
/**
* To use "checkable" option in /#/settings/folders
* When true, getNextFolderNames only lists system and "checkable" folders
@ -19,14 +22,13 @@ export const FolderUserStore = new class {
*/
displaySpecSetting: false,
/**
* If the IMAP server supports SORT
*/
sortSupported: false,
// sortMode: '',
quotaLimit: 0,
quotaUsage: 0,
sentFolder: '',
draftFolder: '',
draftsFolder: '',
spamFolder: '',
trashFolder: '',
archiveFolder: '',
@ -42,77 +44,77 @@ export const FolderUserStore = new class {
foldersInboxUnreadCount: 0
});
this.sortMode = ko.observable('').extend({ limitedList: Object.values(FolderSortMode) });
self.sortMode = ko.observable('').extend({ limitedList: Object.values(FolderSortMode) });
this.namespace = '';
self.namespace = '';
this.folderList = ko.observableArray();
self.folderList = ko.observableArray(/*new FolderCollectionModel*/);
this.currentFolder = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'selected'] });
self.capabilities = ko.observableArray();
this.sieveAllowFileintoInbox = !!SettingsGet('SieveAllowFileintoInbox');
self.currentFolder = ko.observable(null).extend({ toggleSubscribeProperty: [self, 'selected'] });
addComputablesTo(this, {
addComputablesTo(self, {
draftFolderNotEnabled: () => !this.draftFolder() || UNUSED_OPTION_VALUE === this.draftFolder(),
draftsFolderNotEnabled: () => !self.draftsFolder() || UNUSED_OPTION_VALUE === self.draftsFolder(),
// foldersListWithSingleInboxRootFolder
/** returns true when there are no non-system folders in the root of the folders tree */
singleRootFolder: () => {
let multiple = false;
this.folderList.forEach(folder => {
let subscribed = folder.subscribed(),
hasSub = folder.hasSubscribedSubfolders();
multiple |= (!folder.isSystemFolder() || (hasSub && !folder.isInbox())) && (subscribed || hasSub)
});
return !multiple;
},
currentFolderFullNameRaw: () => (this.currentFolder() ? this.currentFolder().fullNameRaw : ''),
currentFolderFullName: () => (this.currentFolder() ? this.currentFolder().fullName : ''),
currentFolderFullNameHash: () => (this.currentFolder() ? this.currentFolder().fullNameHash : ''),
currentFolderFullName: () => (self.currentFolder() ? self.currentFolder().fullName : ''),
currentFolderFullNameHash: () => (self.currentFolder() ? self.currentFolder().fullNameHash : ''),
foldersChanging: () =>
this.foldersLoading() | this.foldersCreating() | this.foldersDeleting() | this.foldersRenaming(),
self.foldersLoading() | self.foldersCreating() | self.foldersDeleting() | self.foldersRenaming(),
folderListSystemNames: () => {
const list = [getFolderInboxName()],
others = [this.sentFolder(), this.draftFolder(), this.spamFolder(), this.trashFolder(), this.archiveFolder()];
others = [self.sentFolder(), self.draftsFolder(), self.spamFolder(), self.trashFolder(), self.archiveFolder()];
this.folderList().length &&
self.folderList().length &&
others.forEach(name => name && UNUSED_OPTION_VALUE !== name && list.push(name));
return list;
},
folderListSystem: () =>
this.folderListSystemNames().map(name => getFolderFromCacheList(name)).filter(v => v)
self.folderListSystemNames().map(name => getFolderFromCacheList(name)).filter(v => v)
});
const
fRemoveSystemFolderType = (observable) => () => {
const folder = getFolderFromCacheList(observable());
folder && folder.type(FolderType.User);
subscribeRemoveSystemFolder = observable => {
observable.subscribe(() => {
const folder = getFolderFromCacheList(observable());
folder && folder.type(FolderType.User);
}, self, 'beforeChange');
},
fSetSystemFolderType = type => value => {
const folder = getFolderFromCacheList(value);
folder && folder.type(type);
};
this.sentFolder.subscribe(fRemoveSystemFolderType(this.sentFolder), this, 'beforeChange');
this.draftFolder.subscribe(fRemoveSystemFolderType(this.draftFolder), this, 'beforeChange');
this.spamFolder.subscribe(fRemoveSystemFolderType(this.spamFolder), this, 'beforeChange');
this.trashFolder.subscribe(fRemoveSystemFolderType(this.trashFolder), this, 'beforeChange');
this.archiveFolder.subscribe(fRemoveSystemFolderType(this.archiveFolder), this, 'beforeChange');
subscribeRemoveSystemFolder(self.sentFolder);
subscribeRemoveSystemFolder(self.draftsFolder);
subscribeRemoveSystemFolder(self.spamFolder);
subscribeRemoveSystemFolder(self.trashFolder);
subscribeRemoveSystemFolder(self.archiveFolder);
addSubscribablesTo(this, {
addSubscribablesTo(self, {
sentFolder: fSetSystemFolderType(FolderType.Sent),
draftFolder: fSetSystemFolderType(FolderType.Drafts),
draftsFolder: fSetSystemFolderType(FolderType.Drafts),
spamFolder: fSetSystemFolderType(FolderType.Spam),
trashFolder: fSetSystemFolderType(FolderType.Trash),
archiveFolder: fSetSystemFolderType(FolderType.Archive)
});
self.quotaPercentage = koComputable(() => {
const quota = self.quotaLimit(), usage = self.quotaUsage();
return 0 < quota ? Math.ceil((usage / quota) * 100) : 0;
});
}
/**
* If the IMAP server supports SORT, METADATA
*/
hasCapability(name) {
return this.capabilities().includes(name);
}
/**
@ -134,7 +136,7 @@ export const FolderUserStore = new class {
timeout > folder.expires &&
(folder.isSystemFolder() || (folder.subscribed() && (folder.checkable() || !bDisplaySpecSetting)))
) {
timeouts.push([folder.expires, folder.fullNameRaw]);
timeouts.push([folder.expires, folder.fullName]);
}
if (folder && folder.subFolders.length) {
@ -162,13 +164,13 @@ export const FolderUserStore = new class {
saveSystemFolders(folders) {
folders = folders || {
SentFolder: FolderUserStore.sentFolder(),
DraftFolder: FolderUserStore.draftFolder(),
SpamFolder: FolderUserStore.spamFolder(),
TrashFolder: FolderUserStore.trashFolder(),
ArchiveFolder: FolderUserStore.archiveFolder()
Sent: FolderUserStore.sentFolder(),
Drafts: FolderUserStore.draftsFolder(),
Spam: FolderUserStore.spamFolder(),
Trash: FolderUserStore.trashFolder(),
Archive: FolderUserStore.archiveFolder()
};
Object.entries(folders).forEach(([k,v])=>Settings.set(k,v));
rl.app.Remote.saveSystemFolders(()=>0, folders);
forEachObjectEntry(folders, (k,v)=>Settings.set(k+'Folder',v));
rl.app.Remote.request('SystemFoldersUpdate', null, folders);
}
};

220
dev/Stores/User/GnuPG.js Normal file
View file

@ -0,0 +1,220 @@
import ko from 'ko';
import { SettingsCapa } from 'Common/Globals';
//import { EmailModel } from 'Model/Email';
//import { OpenPgpKeyModel } from 'Model/OpenPgpKey';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { AskPopupView } from 'View/Popup/Ask';
const
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') =>
await AskPopupView.password('GnuPG key<br>' + privateKey.id + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt),
findGnuPGKey = (keys, query, sign) =>
keys.find(key =>
key[sign ? 'can_sign' : 'can_decrypt']
&& (key.emails.includes(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
);
export const GnuPGUserStore = new class {
constructor() {
/**
* PECL gnupg / PEAR Crypt_GPG
* [ {email, can_encrypt, can_sign}, ... ]
*/
this.keyring;
this.publicKeys = ko.observableArray();
this.privateKeys = ko.observableArray();
}
loadKeyrings() {
this.keyring = null;
this.publicKeys([]);
this.privateKeys([]);
Remote.request('GnupgGetKeys',
(iError, oData) => {
if (oData && oData.Result) {
this.keyring = oData.Result;
const initKey = (key, isPrivate) => {
const aEmails = [];
key.id = key.subkeys[0].keyid;
key.fingerprint = key.subkeys[0].fingerprint;
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
key.emails = aEmails;
key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper();
key.remove = () => {
if (key.askDelete()) {
Remote.request('GnupgDeleteKey',
(iError, oData) => {
if (oData && oData.Result) {
if (isPrivate) {
this.privateKeys.remove(key);
} else {
this.publicKeys.remove(key);
}
}
}, {
KeyId: key.id,
isPrivate: isPrivate
}
);
}
};
key.view = () => {
const fetch = pass => Remote.request('GnupgExportKey',
(iError, oData) => {
if (oData && oData.Result) {
key.armor = oData.Result;
showScreenPopup(OpenPgpKeyPopupView, [key]);
}
}, {
KeyId: key.id,
isPrivate: isPrivate,
Passphrase: pass
}
);
if (isPrivate) {
askPassphrase(key, 'POPUP_VIEW_TITLE').then(passphrase => {
(null !== passphrase) && fetch(passphrase);
});
} else {
fetch('');
}
};
return key;
};
this.publicKeys(oData.Result.public.map(key => initKey(key, 0)));
this.privateKeys(oData.Result.private.map(key => initKey(key, 1)));
console.log('gnupg ready');
}
}
);
}
/**
* @returns {boolean}
*/
isSupported() {
return SettingsCapa('GnuPG');
}
importKey(key, callback) {
Remote.request('GnupgImportKey',
(iError, oData) => {
if (oData && oData.Result) {
// this.gnupgKeyring = oData.Result;
}
callback && callback(iError, oData);
}, {
Key: key
}
);
}
/**
keyPair.privateKey
keyPair.publicKey
keyPair.revocationCertificate
keyPair.onServer
keyPair.inGnuPG
*/
storeKeyPair(keyPair, callback) {
Remote.request('PgpStoreKeyPair',
(iError, oData) => {
if (oData && oData.Result) {
// this.gnupgKeyring = oData.Result;
}
callback && callback(iError, oData);
}, keyPair
);
}
/**
* Checks if verifying/encrypting a message is possible with given email addresses.
*/
hasPublicKeyForEmails(recipients) {
const count = recipients.length,
length = count ? recipients.filter(email =>
// (key.can_verify || key.can_encrypt) &&
this.publicKeys.find(key => key.emails.includes(email))
).length : 0;
return length && length === count;
}
getPublicKeyFingerprints(recipients) {
const fingerprints = [];
recipients.forEach(email => {
fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint);
});
return fingerprints;
}
getPrivateKeyFor(query, sign) {
return findGnuPGKey(this.privateKeys, query, sign);
}
async decrypt(message) {
const
pgpInfo = message.pgpEncrypted();
if (pgpInfo) {
let ids = [message.to[0].email].concat(pgpInfo.KeyIds),
i = ids.length, key;
while (i--) {
key = findGnuPGKey(this.privateKeys, ids[i]);
if (key) {
break;
}
}
if (key) {
// Also check message.from[0].email
let params = {
Folder: message.folder,
Uid: message.uid,
PartId: pgpInfo.PartId,
KeyId: key.id,
Passphrase: await askPassphrase(key, 'BUTTON_DECRYPT'),
Data: '' // message.plain() optional
}
if (null !== params.Passphrase) {
const result = await Remote.post('GnupgDecrypt', null, params);
if (result && result.Result) {
return result.Result;
}
}
}
}
}
async verify(message) {
let data = message.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
if (data) {
data = { ...data }; // clone
// const sender = message.from[0].email;
// let mode = await this.hasPublicKeyForEmails([sender]);
data.Folder = message.folder;
data.Uid = message.uid;
if (data.BodyPart) {
data.BodyPart = data.BodyPart.raw;
data.SigPart = data.SigPart.body;
}
let response = await Remote.post('MessagePgpVerify', null, data);
if (response && response.Result) {
return {
fingerprint: response.Result.fingerprint,
success: 0 == response.Result.status // GOODSIG
};
}
}
}
async sign(privateKey) {
return await askPassphrase(privateKey);
}
};

View file

@ -1,6 +1,6 @@
import ko from 'ko';
import { koArrayWithDestroy } from 'External/ko';
export const IdentityUserStore = ko.observableArray();
export const IdentityUserStore = koArrayWithDestroy();
IdentityUserStore.getIDS = () => IdentityUserStore.map(item => (item ? item.id() : null))
.filter(value => null !== value);

View file

@ -1,200 +1,30 @@
import ko from 'ko';
import { Scope, Notification } from 'Common/Enums';
import { MessageSetAction } from 'Common/EnumsUser';
import { doc, $htmlCL, createElement, elementById } from 'Common/Globals';
import { arrayLength, pInt, pString, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
import { plainToHtml } from 'Common/UtilsUser';
import {
getFolderInboxName,
addNewMessageCache,
setFolderUidNext,
getFolderFromCacheList,
setFolderHash,
MessageFlagsCache,
addRequestedMessage,
clearNewMessageCache
} from 'Common/Cache';
import { mailBox } from 'Common/Links';
import { i18n, getNotification } from 'Common/Translator';
import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageModel } from 'Model/Message';
import { MessageCollectionModel } from 'Model/MessageCollection';
import { Scope } from 'Common/Enums';
import { elementById } from 'Common/Globals';
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp';
import { SettingsUserStore } from 'Stores/User/Settings';
import { NotificationUserStore } from 'Stores/User/Notification';
//import Remote from 'Remote/User/Fetch'; Circular dependency
const
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;
},
/*eslint-disable max-len*/
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
findEmailAndLinks = html => html
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
.replace(email, '$1<a href="mailto:$2">$2</a>'),
isChecked = item => item.checked(),
// Removes background and color
// Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability
removeColors = html => {
let l;
do {
l = html.length;
html = html
.replace(/(<[^>]+[;"'])\s*background(-[a-z]+)?\s*:[^;"']+/gi, '$1')
.replace(/(<[^>]+[;"'])\s*color\s*:[^;"']+/gi, '$1')
.replace(/(<[^>]+)\s(bg)?color=("[^"]+"|'[^']+')/gi, '$1');
} while (l != html.length)
return html;
};
let MessageSeenTimer;
doc.body.append(hcont);
export const MessageUserStore = new class {
constructor() {
this.staticMessage = new MessageModel();
this.list = ko.observableArray().extend({ debounce: 0 });
addObservablesTo(this, {
listCount: 0,
listSearch: '',
listThreadUid: 0,
listPage: 1,
listPageBeforeThread: 1,
listError: '',
listEndFolder: '',
listEndSearch: '',
listEndThreadUid: 0,
listEndPage: 1,
listLoading: false,
listLoadingAnimation: false,
listIsNotCompleted: false,
listCompleteLoading: false,
selectorMessageSelected: null,
selectorMessageFocused: null,
// message viewer
message: null,
messageViewTrigger: false,
messageError: '',
messageLoading: false,
messageFullScreenMode: false,
error: '',
loading: false,
fullScreen: false,
// Cache mail bodies
messagesBodiesDom: null,
messageActiveDom: null
bodiesDom: null,
activeDom: null
});
this.listDisableAutoSelect = ko.observable(false).extend({ falseTimeout: 500 });
// Computed Observables
addComputablesTo(this, {
listEndHash: () =>
this.listEndFolder() +
'|' +
this.listEndSearch() +
'|' +
this.listEndThreadUid() +
'|' +
this.listEndPage(),
listPageCount: () => Math.max(1, Math.ceil(this.listCount() / SettingsUserStore.messagesPerPage())),
mainMessageListSearch: {
read: this.listSearch,
write: value => rl.route.setHash(
mailBox(FolderUserStore.currentFolderFullNameHash(), 1, value.toString().trim(), this.listThreadUid())
)
},
isMessageSelected: () => null !== this.message(),
listCheckedOrSelected: () => {
const
selectedMessage = this.selectorMessageSelected(),
focusedMessage = this.selectorMessageFocused(),
checked = this.list.filter(item => isChecked(item) || item === selectedMessage);
return checked.length ? checked : (focusedMessage ? [focusedMessage] : []);
},
listCheckedOrSelectedUidsWithSubMails: () => {
let result = [];
this.listCheckedOrSelected().forEach(message => {
result.push(message.uid);
if (1 < message.threadsLen()) {
result = result.concat(message.threads()).unique();
}
});
return result;
}
});
this.listChecked = ko
.computed(() => this.list.filter(isChecked))
.extend({ rateLimit: 0 });
this.hasCheckedMessages = ko
.computed(() => !!this.list.find(isChecked))
.extend({ rateLimit: 0 });
this.hasCheckedOrSelected = ko
.computed(() => !!(this.selectorMessageSelected()
|| this.selectorMessageFocused()
|| this.list.find(item => item.checked())))
.extend({ rateLimit: 50 });
// Subscribers
let timer = 0, fn = this.listLoadingAnimation;
addSubscribablesTo(this, {
listCompleteLoading: value => {
if (value) {
fn(value);
} else if (fn()) {
clearTimeout(timer);
timer = setTimeout(() => {
fn(value);
timer = 0;
}, 700);
} else {
fn(value);
}
},
listLoadingAnimation: value => $htmlCL.toggle('list-loading', value),
listLoading: value =>
this.listCompleteLoading(value || this.listIsNotCompleted()),
listIsNotCompleted: value =>
this.listCompleteLoading(value || this.listLoading()),
message: message => {
clearTimeout(MessageSeenTimer);
clearTimeout(this.MessageSeenTimer);
elementById('rl-right').classList.toggle('message-selected', !!message);
if (message) {
if (!SettingsUserStore.usePreviewPane()) {
AppUserStore.focusedState(Scope.MessageView);
@ -202,24 +32,21 @@ export const MessageUserStore = new class {
} else {
AppUserStore.focusedState(Scope.MessageList);
this.messageFullScreenMode(false);
this.fullScreen(false);
this.hideMessageBodies();
}
},
listEndFolder: folder => {
const message = this.message();
if (message && folder && folder !== message.folder) {
this.message(null);
}
}
});
this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000);
}
toggleFullScreen() {
MessageUserStore.fullScreen(!MessageUserStore.fullScreen());
}
purgeMessageBodyCache() {
const messagesDom = this.messagesBodiesDom(),
const messagesDom = this.bodiesDom(),
children = messagesDom && messagesDom.children;
if (children) {
while (15 < children.length) {
@ -228,470 +55,8 @@ export const MessageUserStore = new class {
}
}
initUidNextAndNewMessages(folder, uidNext, newMessages) {
if (getFolderInboxName() === folder && uidNext) {
if (arrayLength(newMessages)) {
newMessages.forEach(item => addNewMessageCache(folder, item.Uid));
NotificationUserStore.playSoundNotification();
const len = newMessages.length;
if (3 < len) {
NotificationUserStore.displayDesktopNotification(
AccountUserStore.email(),
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
COUNT: len
}),
{ Url: mailBox(newMessages[0].Folder, 1) }
);
} else {
newMessages.forEach(item => {
NotificationUserStore.displayDesktopNotification(
EmailCollectionModel.reviveFromJson(item.From).toString(),
item.Subject,
{ Folder: item.Folder, Uid: item.Uid }
);
});
}
}
setFolderUidNext(folder, uidNext);
}
}
hideMessageBodies() {
const messagesDom = this.messagesBodiesDom();
const messagesDom = this.bodiesDom();
messagesDom && Array.from(messagesDom.children).forEach(el => el.hidden = true);
}
/**
* @param {string} fromFolderFullNameRaw
* @param {Array} uidForRemove
* @param {string=} toFolderFullNameRaw = ''
* @param {boolean=} copy = false
*/
removeMessagesFromList(fromFolderFullNameRaw, uidForRemove, toFolderFullNameRaw = '', copy = false) {
uidForRemove = uidForRemove.map(mValue => pInt(mValue));
let unseenCount = 0,
messageList = this.list,
currentMessage = this.message();
const trashFolder = FolderUserStore.trashFolder(),
spamFolder = FolderUserStore.spamFolder(),
fromFolder = getFolderFromCacheList(fromFolderFullNameRaw),
toFolder = toFolderFullNameRaw ? getFolderFromCacheList(toFolderFullNameRaw) : null,
currentFolderFullNameRaw = FolderUserStore.currentFolderFullNameRaw(),
messages =
currentFolderFullNameRaw === fromFolderFullNameRaw
? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
: [];
messages.forEach(item => {
if (item && item.isUnseen()) {
++unseenCount;
}
});
if (fromFolder && !copy) {
fromFolder.messageCountAll(
0 <= fromFolder.messageCountAll() - uidForRemove.length ? fromFolder.messageCountAll() - uidForRemove.length : 0
);
if (0 < unseenCount) {
fromFolder.messageCountUnread(
0 <= fromFolder.messageCountUnread() - unseenCount ? fromFolder.messageCountUnread() - unseenCount : 0
);
}
}
if (toFolder) {
if (trashFolder === toFolder.fullNameRaw || spamFolder === toFolder.fullNameRaw) {
unseenCount = 0;
}
toFolder.messageCountAll(toFolder.messageCountAll() + uidForRemove.length);
if (0 < unseenCount) {
toFolder.messageCountUnread(toFolder.messageCountUnread() + unseenCount);
}
toFolder.actionBlink(true);
}
if (messages.length) {
if (copy) {
messages.forEach(item => item.checked(false));
} else {
this.listIsNotCompleted(true);
messages.forEach(item => {
if (currentMessage && currentMessage.hash === item.hash) {
currentMessage = null;
this.message(null);
}
item.deleted(true);
});
setTimeout(() => messages.forEach(item => messageList.remove(item)), 350);
}
}
if (fromFolderFullNameRaw) {
setFolderHash(fromFolderFullNameRaw, '');
}
if (toFolderFullNameRaw) {
setFolderHash(toFolderFullNameRaw, '');
}
if (this.listThreadUid()) {
if (
messageList.length &&
!!messageList.find(item => !!(item && item.deleted() && item.uid == this.listThreadUid()))
) {
const message = messageList.find(item => item && !item.deleted());
if (message && this.listThreadUid() != message.uid) {
this.listThreadUid(message.uid);
rl.route.setHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
this.listPage(),
this.listSearch(),
this.listThreadUid()
),
true,
true
);
} else if (!message) {
if (1 < this.listPage()) {
this.listPage(this.listPage() - 1);
rl.route.setHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
this.listPage(),
this.listSearch(),
this.listThreadUid()
),
true,
true
);
} else {
this.listThreadUid(0);
rl.route.setHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
this.listPageBeforeThread(),
this.listSearch()
),
true,
true
);
}
}
}
}
}
/**
* @param {Object} messageTextBody
*/
initBlockquoteSwitcher(messageTextBody) {
messageTextBody && messageTextBody.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
let h = node.clientHeight || getRealHeight(node);
if (0 === h || 100 < h) {
const el = Element.fromHTML('<span class="rlBlockquoteSwitcher">•••</span>');
node.classList.add('rl-bq-switcher','hidden-bq');
node.before(el);
el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
}
}
});
}
/**
* @param {Object} messageTextBody
* @param {Object} message
*/
initOpenPgpControls(messageTextBody, message) {
messageTextBody && messageTextBody.querySelectorAll('.b-plain-openpgp:not(.inited)').forEach(node =>
PgpUserStore.initMessageBodyControls(node, message)
);
}
setMessage(data, cached, oMessage) {
let isNew = false,
json = data && data.Result,
messagesDom = this.messagesBodiesDom(),
selectedMessage = this.selectorMessageSelected(),
message = oMessage || this.message();
if (
json &&
MessageModel.validJson(json) &&
message &&
message.folder === json.Folder
) {
const threads = message.threads();
if (message.uid != json.Uid && 1 < threads.length && threads.includes(json.Uid)) {
message = oMessage ? null : MessageModel.reviveFromJson(json);
if (message) {
message.threads(threads);
MessageFlagsCache.initMessage(message);
this.message(this.staticMessage.populateByMessageListItem(message));
message = this.message();
isNew = true;
}
}
if (message && message.uid == json.Uid) {
oMessage || this.messageError('');
if (cached) {
delete json.IsSeen;
delete json.IsFlagged;
delete json.IsAnswered;
delete json.IsForwarded;
delete json.IsReadReceipt;
delete json.IsDeleted;
}
message.revivePropertiesFromJson(json);
addRequestedMessage(message.folder, message.uid);
if (messagesDom) {
let body = null,
id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, '');
const textBody = elementById(id);
if (textBody) {
message.body = textBody;
message.fetchDataFromDom();
messagesDom.append(textBody);
} else {
let isHtml = !!json.Html,
plain = '',
resultHtml = '<pre></pre>';
if (isHtml) {
resultHtml = json.Html.toString();
if (SettingsUserStore.removeColors()) {
resultHtml = removeColors(resultHtml);
}
} else if (json.Plain) {
resultHtml = plainToHtml(json.Plain.toString());
if ((message.isPgpSigned() || message.isPgpEncrypted()) && PgpUserStore.capaOpenPGP()) {
plain = pString(json.Plain);
const pre = createElement('pre');
if (message.isPgpSigned()) {
pre.className = 'b-plain-openpgp signed';
pre.textContent = plain;
} else if (message.isPgpEncrypted()) {
pre.className = 'b-plain-openpgp encrypted';
pre.textContent = plain;
} else {
pre.innerHTML = resultHtml;
}
resultHtml = pre.outerHTML;
} else {
resultHtml = '<pre>' + resultHtml + '</pre>';
}
}
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
+ (isHtml ? 'html' : 'plain') + '">'
+ findEmailAndLinks(resultHtml)
+ '</div>');
if (isHtml) {
// Drop Microsoft Office style properties
const rgbRE = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/g,
hex = n => ('0' + parseInt(n).toString(16)).slice(-2);
body.querySelectorAll('[style*=mso]').forEach(el =>
el.setAttribute('style', el.style.cssText.replace(rgbRE, (m,r,g,b) => '#' + hex(r) + hex(g) + hex(b)))
);
}
body.rlIsHtml = isHtml;
body.rlHasImages = !!json.HasExternals;
message.body = body;
message.isHtml(isHtml);
message.hasImages(body.rlHasImages);
messagesDom.append(body);
if (json.HasInternals) {
message.showInternalImages();
}
if (message.hasImages() && SettingsUserStore.showImages()) {
message.showExternalImages();
}
this.purgeMessageBodyCache();
}
oMessage || this.messageActiveDom(message.body);
oMessage || this.hideMessageBodies();
if (body) {
this.initOpenPgpControls(body, message);
this.initBlockquoteSwitcher(body);
}
oMessage || (message.body.hidden = false);
oMessage && message.viewPopupMessage();
}
MessageFlagsCache.initMessage(message);
if (message.isUnseen()) {
MessageSeenTimer = setTimeout(
() => rl.app.messageListAction(message.folder, MessageSetAction.SetSeen, [message]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
if (isNew) {
if (
selectedMessage &&
message &&
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
) {
this.selectorMessageSelected(null);
if (1 === this.list.length) {
this.selectorMessageFocused(null);
}
} else if (!selectedMessage && message) {
selectedMessage = this.list.find(
subMessage =>
subMessage &&
subMessage.folder === message.folder &&
subMessage.uid == message.uid
);
if (selectedMessage) {
this.selectorMessageSelected(selectedMessage);
this.selectorMessageFocused(selectedMessage);
}
}
}
}
}
}
selectMessage(oMessage) {
if (oMessage) {
this.message(this.staticMessage.populateByMessageListItem(oMessage));
this.populateMessageBody(this.message());
} else {
this.message(null);
}
}
selectMessageByFolderAndUid(sFolder, iUid) {
if (sFolder && iUid) {
this.message(this.staticMessage.populateByMessageListItem(null));
this.message().folder = sFolder;
this.message().uid = iUid;
this.populateMessageBody(this.message());
} else {
this.message(null);
}
}
populateMessageBody(oMessage, preload) {
if (oMessage) {
preload || this.hideMessageBodies();
preload || this.messageLoading(true);
rl.app.Remote.message((iError, oData, bCached) => {
if (iError) {
if (Notification.RequestAborted !== iError && !preload) {
this.message(null);
this.messageError(getNotification(iError));
}
} else {
this.setMessage(oData, bCached, preload ? oMessage : null);
}
preload || this.messageLoading(false);
}, oMessage.folder, oMessage.uid);
}
}
/**
* @param {Array} list
* @returns {string}
*/
calculateMessageListHash(list) {
return list.map(message => '' + message.hash + '_' + message.threadsLen() + '_' + message.flagHash()).join(
'|'
);
}
setMessageList(data, cached) {
const collection = MessageCollectionModel.reviveFromJson(data.Result, cached);
if (collection) {
let unreadCountChange = false;
const iCount = collection.MessageResultCount,
iOffset = collection.Offset,
folder = getFolderFromCacheList(collection.Folder);
if (folder && !cached) {
folder.expires = Date.now();
setFolderHash(collection.Folder, collection.FolderHash);
if (null != collection.MessageCount) {
folder.messageCountAll(collection.MessageCount);
}
if (null != collection.MessageUnseenCount) {
if (pInt(folder.messageCountUnread()) !== pInt(collection.MessageUnseenCount)) {
unreadCountChange = true;
MessageFlagsCache.clearFolder(folder.fullNameRaw);
}
folder.messageCountUnread(collection.MessageUnseenCount);
}
this.initUidNextAndNewMessages(folder.fullNameRaw, collection.UidNext, collection.NewMessages);
}
this.listCount(iCount);
this.listSearch(pString(collection.Search));
this.listPage(Math.ceil(iOffset / SettingsUserStore.messagesPerPage() + 1));
this.listThreadUid(data.Result.ThreadUid);
this.listEndFolder(collection.Folder);
this.listEndSearch(this.listSearch());
this.listEndThreadUid(this.listThreadUid());
this.listEndPage(this.listPage());
this.listDisableAutoSelect(true);
this.list(collection);
this.listIsNotCompleted(false);
clearNewMessageCache();
if (folder && (cached || unreadCountChange || SettingsUserStore.useThreads())) {
rl.app.folderInformation(folder.fullNameRaw, collection);
}
} else {
this.listCount(0);
this.list([]);
this.listError(getNotification(Notification.CantGetMessageList));
}
}
};

View file

@ -0,0 +1,435 @@
import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums';
import { MessageSetAction } from 'Common/EnumsUser';
import { $htmlCL } from 'Common/Globals';
import { arrayLength, pInt, pString } from 'Common/Utils';
import { addObservablesTo, addComputablesTo } from 'External/ko';
import {
getFolderInboxName,
addNewMessageCache,
setFolderUidNext,
getFolderFromCacheList,
setFolderHash,
MessageFlagsCache,
clearNewMessageCache
} from 'Common/Cache';
import { mailBox } from 'Common/Links';
import { i18n, getNotification } from 'Common/Translator';
import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageCollectionModel } from 'Model/MessageCollection';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
import { NotificationUserStore } from 'Stores/User/Notification';
import { SettingsUserStore } from 'Stores/User/Settings';
import Remote from 'Remote/User/Fetch';
const
isChecked = item => item.checked(),
replaceHash = hash => {
rl.route.off();
hasher.replaceHash(hash);
rl.route.on();
}
export const MessagelistUserStore = ko.observableArray().extend({ debounce: 0 });
addObservablesTo(MessagelistUserStore, {
count: 0,
listSearch: '',
threadUid: 0,
page: 1,
pageBeforeThread: 1,
error: '',
endHash: '',
endThreadUid: 0,
loading: false,
// Happens when message(s) removed from list
isIncomplete: false,
selectedMessage: null,
focusedMessage: null
});
MessagelistUserStore.disableAutoSelect = ko.observable(false).extend({ falseTimeout: 500 });
// Computed Observables
addComputablesTo(MessagelistUserStore, {
isLoading: () => {
const value = MessagelistUserStore.loading() | MessagelistUserStore.isIncomplete();
$htmlCL.toggle('list-loading', value);
return value;
},
pageCount: () => Math.max(1, Math.ceil(MessagelistUserStore.count() / SettingsUserStore.messagesPerPage())),
mainSearch: {
read: MessagelistUserStore.listSearch,
write: value => hasher.setHash(
mailBox(FolderUserStore.currentFolderFullNameHash(), 1,
value.toString().trim(), MessagelistUserStore.threadUid())
)
},
listCheckedOrSelected: () => {
const
selectedMessage = MessagelistUserStore.selectedMessage(),
focusedMessage = MessagelistUserStore.focusedMessage(),
checked = MessagelistUserStore.filter(item => isChecked(item) || item === selectedMessage);
return checked.length ? checked : (focusedMessage ? [focusedMessage] : []);
},
listCheckedOrSelectedUidsWithSubMails: () => {
let result = [];
MessagelistUserStore.listCheckedOrSelected().forEach(message => {
result.push(message.uid);
if (1 < message.threadsLen()) {
result = result.concat(message.threads()).unique();
}
});
return result;
}
});
MessagelistUserStore.listChecked = koComputable(
() => MessagelistUserStore.filter(isChecked)).extend({ rateLimit: 0 }
);
MessagelistUserStore.hasCheckedMessages = koComputable(
() => !!MessagelistUserStore.find(isChecked)
).extend({ rateLimit: 0 });
MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
!!(MessagelistUserStore.selectedMessage()
|| MessagelistUserStore.focusedMessage()
|| MessagelistUserStore.find(item => item.checked()))
).extend({ rateLimit: 50 });
MessagelistUserStore.initUidNextAndNewMessages = (folder, uidNext, newMessages) => {
if (getFolderInboxName() === folder && uidNext) {
if (arrayLength(newMessages)) {
newMessages.forEach(item => addNewMessageCache(folder, item.Uid));
NotificationUserStore.playSoundNotification();
const len = newMessages.length;
if (3 < len) {
NotificationUserStore.displayDesktopNotification(
AccountUserStore.email(),
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
COUNT: len
}),
{ Url: mailBox(newMessages[0].Folder) }
);
} else {
newMessages.forEach(item => {
NotificationUserStore.displayDesktopNotification(
EmailCollectionModel.reviveFromJson(item.From).toString(),
item.Subject,
{ Folder: item.Folder, Uid: item.Uid }
);
});
}
}
setFolderUidNext(folder, uidNext);
}
}
/**
* @param {boolean=} bDropPagePosition = false
* @param {boolean=} bDropCurrenFolderCache = false
*/
MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrenFolderCache = false) => {
let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage();
if (bDropCurrenFolderCache) {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
}
if (bDropPagePosition) {
MessagelistUserStore.page(1);
MessagelistUserStore.pageBeforeThread(1);
iOffset = 0;
replaceHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
MessagelistUserStore.page(),
MessagelistUserStore.listSearch(),
MessagelistUserStore.threadUid()
)
);
}
MessagelistUserStore.loading(true);
MessagelistUserStore.error('');
Remote.messageList(
(iError, oData, bCached) => {
if (iError) {
if (Notification.RequestAborted !== iError) {
MessagelistUserStore([]);
MessagelistUserStore.error(getNotification(iError));
}
} else {
const collection = MessageCollectionModel.reviveFromJson(oData.Result, bCached);
if (collection) {
let unreadCountChange = false;
const
folder = getFolderFromCacheList(collection.Folder);
if (folder && !bCached) {
folder.expires = Date.now();
setFolderHash(collection.Folder, collection.FolderHash);
if (null != collection.MessageCount) {
folder.messageCountAll(collection.MessageCount);
}
if (null != collection.MessageUnseenCount) {
if (pInt(folder.messageCountUnread()) !== pInt(collection.MessageUnseenCount)) {
unreadCountChange = true;
MessageFlagsCache.clearFolder(folder.fullName);
}
folder.messageCountUnread(collection.MessageUnseenCount);
}
MessagelistUserStore.initUidNextAndNewMessages(folder.fullName, collection.UidNext, collection.NewMessages);
}
MessagelistUserStore.count(collection.MessageResultCount);
MessagelistUserStore.listSearch(pString(collection.Search));
MessagelistUserStore.page(Math.ceil(collection.Offset / SettingsUserStore.messagesPerPage() + 1));
MessagelistUserStore.threadUid(collection.ThreadUid);
MessagelistUserStore.endHash(
collection.Folder +
'|' + collection.Search +
'|' + MessagelistUserStore.threadUid() +
'|' + MessagelistUserStore.page()
);
MessagelistUserStore.endThreadUid(collection.ThreadUid);
const message = MessageUserStore.message();
if (message && collection.Folder !== message.folder) {
MessageUserStore.message(null);
}
MessagelistUserStore.disableAutoSelect(true);
MessagelistUserStore(collection);
MessagelistUserStore.isIncomplete(false);
clearNewMessageCache();
if (folder && (bCached || unreadCountChange || SettingsUserStore.useThreads())) {
rl.app.folderInformation(folder.fullName, collection);
}
} else {
MessagelistUserStore.count(0);
MessagelistUserStore([]);
MessagelistUserStore.error(getNotification(Notification.CantGetMessageList));
}
}
MessagelistUserStore.loading(false);
},
{
Folder: FolderUserStore.currentFolderFullName(),
Offset: iOffset,
Limit: SettingsUserStore.messagesPerPage(),
Search: MessagelistUserStore.listSearch(),
ThreadUid: MessagelistUserStore.threadUid()
}
);
};
/**
* @param {string} sFolderFullName
* @param {number} iSetAction
* @param {Array=} messages = null
*/
MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
messages = messages || MessagelistUserStore.listChecked();
let folder = null,
alreadyUnread = 0,
rootUids = messages.map(oMessage => oMessage && oMessage.uid ? oMessage.uid : null)
.validUnique(),
length = rootUids.length;
if (sFolderFullName && length) {
switch (iSetAction) {
case MessageSetAction.SetSeen:
length = 0;
// fallthrough is intentionally
case MessageSetAction.UnsetSeen:
rootUids.forEach(sSubUid =>
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
);
folder = getFolderFromCacheList(sFolderFullName);
if (folder) {
folder.messageCountUnread(folder.messageCountUnread() - alreadyUnread + length);
}
Remote.request('MessageSetSeen', null, {
Folder: sFolderFullName,
Uids: rootUids.join(','),
SetAction: iSetAction == MessageSetAction.SetSeen ? 1 : 0
});
break;
case MessageSetAction.SetFlag:
case MessageSetAction.UnsetFlag:
rootUids.forEach(sSubUid =>
MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
);
Remote.request('MessageSetFlagged', null, {
Folder: sFolderFullName,
Uids: rootUids.join(','),
SetAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
});
break;
// no default
}
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
};
/**
* @param {string} fromFolderFullName
* @param {Array} uidForRemove
* @param {string=} toFolderFullName = ''
* @param {boolean=} copy = false
*/
MessagelistUserStore.removeMessagesFromList = (
fromFolderFullName, uidForRemove, toFolderFullName = '', copy = false
) => {
uidForRemove = uidForRemove.map(mValue => pInt(mValue));
let unseenCount = 0,
messageList = MessagelistUserStore,
currentMessage = MessageUserStore.message();
const trashFolder = FolderUserStore.trashFolder(),
spamFolder = FolderUserStore.spamFolder(),
fromFolder = getFolderFromCacheList(fromFolderFullName),
toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
messages =
FolderUserStore.currentFolderFullName() === fromFolderFullName
? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
: [];
messages.forEach(item => {
if (item && item.isUnseen()) {
++unseenCount;
}
});
if (fromFolder && !copy) {
fromFolder.messageCountAll(
0 <= fromFolder.messageCountAll() - uidForRemove.length ? fromFolder.messageCountAll() - uidForRemove.length : 0
);
if (0 < unseenCount) {
fromFolder.messageCountUnread(
0 <= fromFolder.messageCountUnread() - unseenCount ? fromFolder.messageCountUnread() - unseenCount : 0
);
}
}
if (toFolder) {
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
unseenCount = 0;
}
toFolder.messageCountAll(toFolder.messageCountAll() + uidForRemove.length);
if (0 < unseenCount) {
toFolder.messageCountUnread(toFolder.messageCountUnread() + unseenCount);
}
toFolder.actionBlink(true);
}
if (messages.length) {
if (copy) {
messages.forEach(item => item.checked(false));
} else {
MessagelistUserStore.isIncomplete(true);
messages.forEach(item => {
if (currentMessage && currentMessage.hash === item.hash) {
currentMessage = null;
MessageUserStore.message(null);
}
item.deleted(true);
});
setTimeout(() => messages.forEach(item => messageList.remove(item)), 350);
}
}
if (fromFolderFullName) {
setFolderHash(fromFolderFullName, '');
}
if (toFolderFullName) {
setFolderHash(toFolderFullName, '');
}
if (MessagelistUserStore.threadUid()) {
if (
messageList.length &&
!!messageList.find(item => !!(item && item.deleted() && item.uid == MessagelistUserStore.threadUid()))
) {
const message = messageList.find(item => item && !item.deleted());
let setHash;
if (!message) {
if (1 < MessagelistUserStore.page()) {
MessagelistUserStore.page(MessagelistUserStore.page() - 1);
setHash = 1;
} else {
MessagelistUserStore.threadUid(0);
replaceHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
MessagelistUserStore.pageBeforeThread(),
MessagelistUserStore.listSearch()
)
);
}
} else if (MessagelistUserStore.threadUid() != message.uid) {
MessagelistUserStore.threadUid(message.uid);
setHash = 1;
}
if (setHash) {
replaceHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
MessagelistUserStore.page(),
MessagelistUserStore.listSearch(),
MessagelistUserStore.threadUid()
)
);
}
}
}
},
MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
MessagelistUserStore.forEach(message => MessageFlagsCache.initMessage(message));
MessageFlagsCache.initMessage(MessageUserStore.message());
};

View file

@ -1,8 +1,7 @@
import ko from 'ko';
import { SMAudio } from 'Common/Audio';
import { SettingsGet } from 'Common/Globals';
import * as Links from 'Common/Links';
import { addObservablesTo } from 'External/ko';
import { fireEvent } from 'Common/Globals';
/**
* Might not work due to the new ServiceWorkerRegistration.showNotification
@ -14,9 +13,9 @@ const HTML5Notification = window.Notification,
dispatchMessage = data => {
focus();
if (data.Folder && data.Uid) {
dispatchEvent(new CustomEvent('mailbox.message.show', {detail:data}));
fireEvent('mailbox.message.show', data);
} else if (data.Url) {
rl.route.setHash(data.Url);
hasher.setHash(data.Url);
}
};
@ -37,11 +36,13 @@ if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistratio
export const NotificationUserStore = new class {
constructor() {
this.enableSoundNotification = ko.observable(false);
addObservablesTo(this, {
enableSoundNotification: false,
this.enableDesktopNotification = ko.observable(false)/*.extend({ notify: 'always' })*/;
enableDesktopNotification: false,/*.extend({ notify: 'always' })*/
this.isDesktopNotificationAllowed = ko.observable(!NotificationsDenied());
isDesktopNotificationAllowed: !NotificationsDenied()
});
this.enableDesktopNotification.subscribe(value => {
DesktopNotifications = !!value;
@ -101,9 +102,4 @@ export const NotificationUserStore = new class {
}
}
}
populate() {
this.enableSoundNotification(!!SettingsGet('SoundNotification'));
this.enableDesktopNotification(!!SettingsGet('DesktopNotifications'));
}
};

294
dev/Stores/User/OpenPGP.js Normal file
View file

@ -0,0 +1,294 @@
/**
* OpenPGP.js
*/
import ko from 'ko';
import { arrayLength } from 'Common/Utils';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { AskPopupView } from 'View/Popup/Ask';
const
findOpenPGPKey = (keys, query/*, sign*/) =>
keys.find(key =>
key.emails.includes(query) || query == key.id || query == key.fingerprint
),
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') =>
await AskPopupView.password('OpenPGP.js key<br>' + privateKey.id + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt),
/**
* OpenPGP.js v5 removed the localStorage (keyring)
* This should be compatible with the old OpenPGP.js v2
*/
publicKeysItem = 'openpgp-public-keys',
privateKeysItem = 'openpgp-private-keys',
storage = window.localStorage,
loadOpenPgpKeys = async itemname => {
let keys = [], key,
armoredKeys = JSON.parse(storage.getItem(itemname)),
i = arrayLength(armoredKeys);
while (i--) {
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
if (!key.err) {
keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
}
}
return keys;
},
storeOpenPgpKeys = (keys, section) => {
let armoredKeys = keys.map(item => item.armor);
if (armoredKeys.length) {
storage.setItem(section, JSON.stringify(armoredKeys));
} else {
storage.removeItem(section);
}
};
class OpenPgpKeyModel {
constructor(armor, key) {
this.key = key;
const aEmails = [];
if (key.users) {
key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
}
this.id = key.getKeyID().toHex().toUpperCase();
this.fingerprint = key.getFingerprint();
this.can_encrypt = !!key.getEncryptionKey();
this.can_sign = !!key.getSigningKey();
this.emails = aEmails;
this.armor = armor;
this.askDelete = ko.observable(false);
this.openForDeletion = ko.observable(null).askDeleteHelper();
// key.getUserIDs()
// key.getPrimaryUser()
}
view() {
showScreenPopup(OpenPgpKeyPopupView, [this]);
}
remove() {
if (this.askDelete()) {
if (this.key.isPrivate()) {
OpenPGPUserStore.privateKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
} else {
OpenPGPUserStore.publicKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem);
}
}
}
/*
toJSON() {
return this.armor;
}
*/
}
export const OpenPGPUserStore = new class {
constructor() {
this.publicKeys = ko.observableArray();
this.privateKeys = ko.observableArray();
}
loadKeyrings() {
loadOpenPgpKeys(publicKeysItem).then(keys => {
this.publicKeys(keys || []);
console.log('openpgp.js public keys loaded');
});
loadOpenPgpKeys(privateKeysItem).then(keys => {
this.privateKeys(keys || [])
console.log('openpgp.js private keys loaded');
});
}
/**
* @returns {boolean}
*/
isSupported() {
return !!window.openpgp;
}
importKey(armoredKey) {
openpgp.readKey({armoredKey:armoredKey}).then(key => {
if (!key.err) {
if (key.isPrivate()) {
this.privateKeys.push(new OpenPgpKeyModel(armoredKey, key));
storeOpenPgpKeys(this.privateKeys, privateKeysItem);
} else {
this.publicKeys.push(new OpenPgpKeyModel(armoredKey, key));
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
}
}
});
}
/**
keyPair.privateKey
keyPair.publicKey
keyPair.revocationCertificate
*/
storeKeyPair(keyPair) {
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
});
openpgp.readKey({armoredKey:keyPair.privateKey}).then(key => {
this.privateKeys.push(new OpenPgpKeyModel(keyPair.privateKey, key));
storeOpenPgpKeys(this.privateKeys, privateKeysItem);
});
}
/**
* Checks if verifying/encrypting a message is possible with given email addresses.
*/
hasPublicKeyForEmails(recipients) {
const count = recipients.length,
length = count ? recipients.filter(email =>
this.publicKeys().find(key => key.emails.includes(email))
).length : 0;
return length && length === count;
}
getPrivateKeyFor(query/*, sign*/) {
return findOpenPGPKey(this.privateKeys, query/*, sign*/);
}
/**
* https://docs.openpgpjs.org/#encrypt-and-decrypt-string-data-with-pgp-keys
*/
async decrypt(armoredText, sender)
{
const message = await openpgp.readMessage({ armoredMessage: armoredText }),
privateKeys = this.privateKeys(),
msgEncryptionKeyIDs = message.getEncryptionKeyIDs().map(key => key.bytes);
// Find private key that can decrypt message
let i = privateKeys.length, privateKey;
while (i--) {
if ((await privateKeys[i].key.getDecryptionKeys()).find(
key => msgEncryptionKeyIDs.includes(key.getKeyID().bytes)
)) {
privateKey = privateKeys[i];
break;
}
}
if (privateKey) try {
const passphrase = await askPassphrase(privateKey, 'BUTTON_DECRYPT');
if (null !== passphrase) {
const
publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/),
decryptedKey = await openpgp.decryptKey({
privateKey: privateKey.key,
passphrase
});
return await openpgp.decrypt({
message,
verificationKeys: publicKey && publicKey.key,
// expectSigned: true,
// signature: '', // Detached signature
decryptionKeys: decryptedKey
});
}
} catch (err) {
alert(err);
console.error(err);
}
}
/**
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
*/
async verify(message) {
const data = message.pgpSigned(), // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
publicKey = this.publicKeys().find(key => key.emails.includes(message.from[0].email));
if (data && publicKey) {
data.Folder = message.folder;
data.Uid = message.uid;
data.GnuPG = 0;
let response;
if (data.SigPartId) {
response = await Remote.post('MessagePgpVerify', null, data);
} else if (data.BodyPart) {
response = { Result: { text: data.BodyPart.raw, signature: data.SigPart.body } };
} else {
response = { Result: { text: message.plain(), signature: null } };
}
if (response) {
const signature = response.Result.signature
? await openpgp.readSignature({ armoredSignature: response.Result.signature })
: null;
const signedMessage = signature
? await openpgp.createMessage({ text: response.Result.text })
: await openpgp.readCleartextMessage({ cleartextMessage: response.Result.text });
// (signature||signedMessage).getSigningKeyIDs();
let result = await openpgp.verify({
message: signedMessage,
verificationKeys: publicKey.key,
// expectSigned: true, // !!detachedSignature
signature: signature
});
return {
fingerprint: publicKey.fingerprint,
success: result && !!result.signatures.length
};
}
}
}
/**
* https://docs.openpgpjs.org/global.html#sign
*/
async sign(text, privateKey, detached) {
const passphrase = await askPassphrase(privateKey);
if (null !== passphrase) {
privateKey = await openpgp.decryptKey({
privateKey: privateKey.key,
passphrase
});
const message = detached
? await openpgp.createMessage({ text: text })
: await openpgp.createCleartextMessage({ text: text });
return await openpgp.sign({
message: message,
signingKeys: privateKey,
detached: !!detached
});
}
throw 'Sign cancelled';
}
/**
* https://docs.openpgpjs.org/global.html#encrypt
*/
async encrypt(text, recipients, signPrivateKey) {
const count = recipients.length;
recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
if (count === recipients.length) {
if (signPrivateKey) {
const passphrase = await askPassphrase(signPrivateKey);
if (null === passphrase) {
return;
}
signPrivateKey = await openpgp.decryptKey({
privateKey: signPrivateKey.key,
passphrase
});
}
return await openpgp.encrypt({
message: await openpgp.createMessage({ text: text }),
encryptionKeys: recipients.map(pkey => pkey.key),
signingKeys: signPrivateKey
// signature
});
}
throw 'Encrypt failed';
}
};

View file

@ -1,370 +1,235 @@
import ko from 'ko';
import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { staticLink } from 'Common/Links';
import { i18n } from 'Common/Translator';
import { isArray, arrayLength, pString, addComputablesTo } from 'Common/Utils';
import { createElement } from 'Common/Globals';
//import { showScreenPopup } from 'Knoin/Knoin';
import { AccountUserStore } from 'Stores/User/Account';
//import { EmailModel } from 'Model/Email';
//import { OpenPgpKeyModel } from 'Model/OpenPgpKey';
import { showScreenPopup } from 'Knoin/Knoin';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp';
export const
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
// BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----',
function controlsHelper(dom, verControl, success, title, text)
{
dom.classList.toggle('error', !success);
dom.classList.toggle('success', success);
verControl.classList.toggle('error', !success);
verControl.classList.toggle('success', success);
dom.title = verControl.title = title;
if (undefined !== text) {
dom.textContent = text.trim();
}
}
function domControlEncryptedClickHelper(store, dom, armoredMessage, recipients) {
return function() {
let message = null;
if (this.classList.contains('success')) {
return false;
PgpUserStore = new class {
constructor() {
// https://mailvelope.github.io/mailvelope/Keyring.html
this.mailvelopeKeyring = null;
}
try {
message = store.openpgp.message.readArmored(armoredMessage);
} catch (e) {
console.log(e);
init() {
if (SettingsCapa('OpenPGP') && window.crypto && crypto.getRandomValues) {
rl.loadScript(staticLink('js/min/openpgp.min.js'))
.then(() => this.loadKeyrings())
.catch(e => {
this.loadKeyrings();
console.error(e);
});
} else {
this.loadKeyrings();
}
}
if (message && message.getText && message.verify && message.decrypt) {
store.decryptMessage(
message,
recipients,
(validPrivateKey, decryptedMessage, validPublicKey, signingKeyIds) => {
if (decryptedMessage) {
if (validPublicKey) {
controlsHelper(
dom,
this,
true,
i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
USER: validPublicKey.user + ' (' + validPublicKey.id + ')'
}),
decryptedMessage.getText()
);
} else if (validPrivateKey) {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
: '';
controlsHelper(
dom,
this,
false,
i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : ''),
decryptedMessage.getText()
);
} else {
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
}
loadKeyrings(identifier) {
identifier = identifier || SettingsGet('Email');
if (window.mailvelope) {
const fn = keyring => {
this.mailvelopeKeyring = keyring;
console.log('mailvelope ready');
};
mailvelope.getKeyring().then(fn, err => {
if (identifier) {
// attempt to create a new keyring for this app/user
mailvelope.createKeyring(identifier).then(fn, err => console.error(err));
} else {
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
console.error(err);
}
});
addEventListener('mailvelope-disconnect', event => {
alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
}, false);
} else {
addEventListener('mailvelope', () => this.loadKeyrings(identifier));
}
if (OpenPGPUserStore.isSupported()) {
OpenPGPUserStore.loadKeyrings();
}
if (SettingsCapa('GnuPG')) {
GnuPGUserStore.loadKeyrings();
}
}
/**
* @returns {boolean}
*/
isSupported() {
return !!(OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported() || window.mailvelope);
}
/**
* @returns {boolean}
*/
isEncrypted(text) {
return 0 === text.trim().indexOf(BEGIN_PGP_MESSAGE);
}
async mailvelopeHasPublicKeyForEmails(recipients) {
const
keyring = this.mailvelopeKeyring,
mailvelope = keyring && await keyring.validKeyForAddress(recipients)
/*.then(LookupResult => Object.entries(LookupResult))*/,
entries = mailvelope && Object.entries(mailvelope);
return entries && entries.filter(value => value[1]).length === recipients.length;
}
/**
* Checks if verifying/encrypting a message is possible with given email addresses.
* Returns the first library that can.
*/
async hasPublicKeyForEmails(recipients) {
const count = recipients.length;
if (count) {
if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
return 'gnupg';
}
);
return false;
}
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
return false;
};
}
function domControlSignedClickHelper(store, dom, armoredMessage) {
return function() {
let message = null;
if (this.classList.contains('success') || this.classList.contains('error')) {
return false;
}
try {
message = store.openpgp.cleartext.readArmored(armoredMessage);
} catch (e) {
console.log(e);
}
if (message && message.getText && message.verify) {
store.verifyMessage(message, (validKey, signingKeyIds) => {
if (validKey) {
controlsHelper(
dom,
this,
true,
i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
}),
message.getText()
);
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
: '';
controlsHelper(
dom,
this,
false,
i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : '')
);
if (OpenPGPUserStore.hasPublicKeyForEmails(recipients)) {
return 'openpgp';
}
});
}
return false;
}
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
return false;
};
}
export const PgpUserStore = new class {
constructor() {
this.capaOpenPGP = ko.observable(false);
this.openpgp = null;
this.openpgpkeys = ko.observableArray();
this.openpgpKeyring = null;
addComputablesTo(this, {
openpgpkeysPublic: () => this.openpgpkeys.filter(item => item && !item.isPrivate),
openpgpkeysPrivate: () => this.openpgpkeys.filter(item => item && item.isPrivate)
});
}
/**
* @returns {boolean}
*/
isSupported() {
return !!this.openpgp;
}
findKeyByHex(keys, hash) {
return keys.find(item => hash && item && (hash === item.id || item.ids.includes(hash)));
}
findPublicKeyByHex(hash) {
return this.findKeyByHex(this.openpgpkeysPublic(), hash);
}
findPrivateKeyByHex(hash) {
return this.findKeyByHex(this.openpgpkeysPrivate(), hash);
}
findPublicKeysByEmail(email) {
return this.openpgpkeysPublic().map(item => {
const key = item && item.emails.includes(email) ? item : null;
return key ? key.getNativeKeys() : [null];
}).flat().filter(v => v);
}
findPublicKeysBySigningKeyIds(signingKeyIds) {
return signingKeyIds.map(id => {
const key = id && id.toHex ? this.findPublicKeyByHex(id.toHex()) : null;
return key ? key.getNativeKeys() : [null];
}).flat().filter(v => v);
}
findPrivateKeysByEncryptionKeyIds(encryptionKeyIds, recipients, returnWrapKeys) {
let result = isArray(encryptionKeyIds)
? encryptionKeyIds.map(id => {
const key = id && id.toHex ? this.findPrivateKeyByHex(id.toHex()) : null;
return key ? (returnWrapKeys ? [key] : key.getNativeKeys()) : [null];
}).flat().filter(v => v)
: [];
if (!result.length && arrayLength(recipients)) {
result = recipients.map(sEmail => {
const keys = sEmail ? this.findAllPrivateKeysByEmailNotNative(sEmail) : null;
return keys
? returnWrapKeys
? keys
: keys.map(key => key.getNativeKeys()).flat()
: [null];
}).flat().validUnique(key => key.id);
async getMailvelopePrivateKeyFor(email/*, sign*/) {
let keyring = this.mailvelopeKeyring;
if (keyring && await keyring.hasPrivateKey({email:email})) {
return ['mailvelope', email];
}
return false;
}
return result;
}
/**
* Checks if signing a message is possible with given email address.
* Returns the first library that can.
*/
async getKeyForSigning(email) {
let key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
if (key) {
return ['openpgp', key];
}
/**
* @param {string} email
* @returns {?}
*/
findPublicKeyByEmailNotNative(email) {
return this.openpgpkeysPublic().find(item => item && item.emails.includes(email)) || null;
}
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
if (key) {
return ['gnupg', key];
}
/**
* @param {string} email
* @returns {?}
*/
findPrivateKeyByEmailNotNative(email) {
return this.openpgpkeysPrivate().find(item => item && item.emails.includes(email)) || null;
}
// return await this.getMailvelopePrivateKeyFor(email, 1);
}
/**
* @param {string} email
* @returns {?}
*/
findAllPublicKeysByEmailNotNative(email) {
return this.openpgpkeysPublic().filter(item => item && item.emails.includes(email)) || null;
}
async decrypt(message) {
const sender = message.from[0].email,
armoredText = message.plain();
/**
* @param {string} email
* @returns {?}
*/
findAllPrivateKeysByEmailNotNative(email) {
return this.openpgpkeysPrivate().filter(item => item && item.emails.includes(email)) || null;
}
if (!this.isEncrypted(armoredText)) {
return;
}
/**
* @param {string} email
* @param {string=} password
* @returns {?}
*/
findPrivateKeyByEmail(email, password) {
let privateKey = null;
const key = this.openpgpkeysPrivate().find(item => item && item.emails.includes(email));
// Try OpenPGP.js
let result = await OpenPGPUserStore.decrypt(armoredText, sender);
if (result) {
return result;
}
if (key) {
// Try Mailvelope (does not support inline images)
try {
privateKey = key.getNativeKeys()[0] || null;
if (privateKey) {
privateKey.decrypt(pString(password));
}
} catch (e) {
privateKey = null;
}
}
return privateKey;
}
/**
* @param {string=} password
* @returns {?}
*/
findSelfPrivateKey(password) {
return this.findPrivateKeyByEmail(AccountUserStore.email(), password);
}
decryptMessage(message, recipients, fCallback) {
if (message && message.getEncryptionKeyIds) {
const privateKeys = this.findPrivateKeysByEncryptionKeyIds(message.getEncryptionKeyIds(), recipients, true);
if (privateKeys && privateKeys.length) {
showScreenPopup(MessageOpenPgpPopupView, [
(decryptedKey) => {
if (decryptedKey) {
message.decrypt(decryptedKey).then(
(decryptedMessage) => {
let privateKey = null;
if (decryptedMessage) {
privateKey = this.findPrivateKeyByHex(decryptedKey.primaryKey.keyid.toHex());
if (privateKey) {
this.verifyMessage(decryptedMessage, (oValidKey, aSigningKeyIds) => {
fCallback(privateKey, decryptedMessage, oValidKey || null, aSigningKeyIds || null);
});
} else {
fCallback(privateKey, decryptedMessage);
}
} else {
fCallback(privateKey, decryptedMessage);
}
},
() => {
fCallback(null, null);
}
);
} else {
fCallback(null, null);
let key = await this.getMailvelopePrivateKeyFor(message.to[0].email);
if (key) {
/**
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
* Creates an iframe to display an encrypted form
*/
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
/**
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
* Creates an iframe to display the decrypted content of the encrypted mail.
*/
const body = message.body;
body.textContent = '';
result = await mailvelope.createDisplayContainer(
'#'+body.id,
armoredText,
this.mailvelopeKeyring,
{
senderAddress: sender
}
},
privateKeys
]);
return false;
}
}
fCallback(null, null);
return false;
}
verifyMessage(message, fCallback) {
if (message && message.getSigningKeyIds) {
const signingKeyIds = message.getSigningKeyIds();
if (signingKeyIds && signingKeyIds.length) {
const publicKeys = this.findPublicKeysBySigningKeyIds(signingKeyIds);
if (publicKeys && publicKeys.length) {
try {
const result = message.verify(publicKeys),
valid = (isArray(result) ? result : []).find(item => item && item.valid && item.keyid);
if (valid && valid.keyid && valid.keyid && valid.keyid.toHex) {
fCallback(this.findPublicKeyByHex(valid.keyid.toHex()));
);
if (result) {
if (result.error && result.error.message) {
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
alert(result.error.code + ': ' + result.error.message);
}
} else {
body.classList.add('mailvelope');
return true;
}
} catch (e) {
console.log(e);
}
}
fCallback(null, signingKeyIds);
return false;
} catch (err) {
console.error(err);
}
// Now try GnuPG
return GnuPGUserStore.decrypt(message);
}
fCallback(null);
return false;
}
/**
* @param {*} dom
* @param {MessageModel} rainLoopMessage
*/
initMessageBodyControls(dom, rainLoopMessage) {
const cl = dom && dom.classList;
if (!cl.contains('inited')) {
cl.add('inited');
const encrypted = cl.contains('encrypted'),
signed = cl.contains('signed'),
recipients = rainLoopMessage ? rainLoopMessage.getEmails(['from', 'to', 'cc']) : [];
let verControl = null;
if (encrypted || signed) {
const domText = dom.textContent;
verControl = Element.fromHTML('<div class="b-openpgp-control"><i class="fontastic">🔒</i></div>');
if (encrypted) {
verControl.title = i18n('MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC');
verControl.addEventListener('click', domControlEncryptedClickHelper(this, dom, domText, recipients));
} else {
verControl.title = i18n('MESSAGE/PGP_SIGNED_MESSAGE_DESC');
verControl.addEventListener('click', domControlSignedClickHelper(this, dom, domText));
async verify(message) {
const signed = message.pgpSigned();
if (signed) {
const sender = message.from[0].email,
gnupg = GnuPGUserStore.hasPublicKeyForEmails([sender]),
openpgp = OpenPGPUserStore.hasPublicKeyForEmails([sender]);
// Detached signature use GnuPG first, else we must download whole message
if (gnupg && signed.SigPartId) {
return GnuPGUserStore.verify(message);
}
dom.before(verControl, createElement('div'));
if (openpgp) {
return OpenPGPUserStore.verify(message);
}
if (gnupg) {
return GnuPGUserStore.verify(message);
}
// Mailvelope can't
// https://github.com/mailvelope/mailvelope/issues/434
}
}
}
};
/**
* Returns headers that should be added to an outgoing email.
* So far this is only the autocrypt header.
*/
/*
this.mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers)
this.mailvelopeKeyring.addSyncHandler(syncHandlerObj)
this.mailvelopeKeyring.createKeyBackupContainer(selector, options)
this.mailvelopeKeyring.createKeyGenContainer(selector, {
// userIds: [],
keySize: 4096
})
this.mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
this.mailvelopeKeyring.importPublicKey(armored)
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
this.mailvelopeKeyring.addSyncHandler({
uploadSync
downloadSync
backup
restore
});
*/
};

View file

@ -1,24 +0,0 @@
import ko from 'ko';
export const QuotaUserStore = new class {
constructor() {
this.quota = ko.observable(0);
this.usage = ko.observable(0);
this.percentage = ko.computed(() => {
const quota = this.quota(),
usage = this.usage();
return 0 < quota ? Math.ceil((usage / quota) * 100) : 0;
});
}
/**
* @param {number} quota
* @param {number} usage
*/
populateData(quota, usage) {
this.quota(quota * 1024);
this.usage(usage * 1024);
}
};

View file

@ -1,17 +1,21 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { Layout, EditorDefaultType } from 'Common/EnumsUser';
import { pInt, addObservablesTo } from 'Common/Utils';
import { $htmlCL, SettingsGet } from 'Common/Globals';
import { pInt } from 'Common/Utils';
import { addObservablesTo } from 'External/ko';
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
export const SettingsUserStore = new class {
constructor() {
this.layout = ko
.observable(pInt(SettingsGet('Layout')))
const self = this;
self.layout = ko
.observable(1)
.extend({ limitedList: Object.values(Layout) });
this.editorDefaultType = ko.observable(SettingsGet('EditorDefaultType')).extend({
self.editorDefaultType = ko.observable('Html').extend({
limitedList: [
EditorDefaultType.Html,
EditorDefaultType.Plain,
@ -20,43 +24,66 @@ export const SettingsUserStore = new class {
]
});
this.messagesPerPage = ko.observable(pInt(SettingsGet('MPP'))).extend({ debounce: 999 });
self.messagesPerPage = ko.observable(25).extend({ debounce: 999 });
this.messageReadDelay = ko.observable(pInt(SettingsGet('MessageReadDelay'))).extend({ debounce: 999 });
self.messageReadDelay = ko.observable(5).extend({ debounce: 999 });
addObservablesTo(this, {
showImages: !!SettingsGet('ShowImages'),
removeColors: !!SettingsGet('RemoveColors'),
useCheckboxesInList: !!(ThemeStore.isMobile() || SettingsGet('UseCheckboxesInList')),
allowDraftAutosave: !!SettingsGet('AllowDraftAutosave'),
useThreads: !!SettingsGet('UseThreads'),
replySameFolder: !!SettingsGet('ReplySameFolder'),
hideUnsubscribed: !!SettingsGet('HideUnsubscribed'),
autoLogout: pInt(SettingsGet('AutoLogout'))
addObservablesTo(self, {
viewHTML: 1,
showImages: 0,
removeColors: 0,
useCheckboxesInList: 1,
allowDraftAutosave: 1,
useThreads: 0,
replySameFolder: 0,
hideUnsubscribed: 0,
autoLogout: 0
});
this.usePreviewPane = ko.computed(() => Layout.NoPreview !== this.layout() && !ThemeStore.isMobile());
self.init();
self.usePreviewPane = koComputable(() => Layout.NoPreview !== self.layout() && !ThemeStore.isMobile());
const toggleLayout = () => {
const value = ThemeStore.isMobile() ? Layout.NoPreview : this.layout();
const value = ThemeStore.isMobile() ? Layout.NoPreview : self.layout();
$htmlCL.toggle('rl-no-preview-pane', Layout.NoPreview === value);
$htmlCL.toggle('rl-side-preview-pane', Layout.SidePreview === value);
$htmlCL.toggle('rl-bottom-preview-pane', Layout.BottomPreview === value);
dispatchEvent(new CustomEvent('rl-layout', {detail:value}));
fireEvent('rl-layout', value);
};
this.layout.subscribe(toggleLayout);
self.layout.subscribe(toggleLayout);
ThemeStore.isMobile.subscribe(toggleLayout);
toggleLayout();
let iAutoLogoutTimer;
this.delayLogout = (() => {
self.delayLogout = (() => {
clearTimeout(iAutoLogoutTimer);
if (0 < this.autoLogout() && !SettingsGet('AccountSignMe')) {
if (0 < self.autoLogout() && !SettingsGet('AccountSignMe')) {
iAutoLogoutTimer = setTimeout(
rl.app.logout,
this.autoLogout() * 60000
self.autoLogout() * 60000
);
}
}).throttle(5000);
}
init() {
const self = this;
self.editorDefaultType(SettingsGet('EditorDefaultType'));
self.layout(pInt(SettingsGet('Layout')));
self.messagesPerPage(pInt(SettingsGet('MessagesPerPage')));
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
self.autoLogout(pInt(SettingsGet('AutoLogout')));
self.viewHTML(SettingsGet('ViewHTML'));
self.showImages(SettingsGet('ShowImages'));
self.removeColors(SettingsGet('RemoveColors'));
self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
self.useThreads(SettingsGet('UseThreads'));
self.replySameFolder(SettingsGet('ReplySameFolder'));
self.hideUnsubscribed(SettingsGet('HideUnsubscribed'));
}
};

View file

@ -1,8 +1,8 @@
import ko from 'ko';
import { koArrayWithDestroy } from 'External/ko';
export const SieveUserStore = {
// capabilities
capa: ko.observableArray(),
// Sieve scripts SieveScriptModel
scripts: ko.observableArray()
scripts: koArrayWithDestroy()
}