mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-01 05:29:20 +03:00
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:
commit
f7f56b3789
794 changed files with 96444 additions and 74188 deletions
|
|
@ -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: ''
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
220
dev/Stores/User/GnuPG.js
Normal 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);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
435
dev/Stores/User/Messagelist.js
Normal file
435
dev/Stores/User/Messagelist.js
Normal 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());
|
||||
};
|
||||
|
|
@ -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
294
dev/Stores/User/OpenPGP.js
Normal 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';
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -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
|
||||
});
|
||||
*/
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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'));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue