mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-10-30 09:03:33 +03:00
Update Fork (#2)
* Cleanup OpenPgpImportPopupView code * update polish translation * small fix * Added Import S/MIME certificate popup And much better handling of the sign and encrypt options * bugfix: store in Passphrases * Resolve #1448 * pre-verify S/MIME opaque signed messages so we have a body to view * Fix timestampToString() for future dates * Move php8.php to /app/libraries/polyfill/ * Improved Settings handling to prevent bugs in outer code * Changed AbstractProvider::IsActive() to be abstract * Example for #1449 * bugfix: previous IsActive() commit * OpenSSL required due to S/MIME * Use get_debug_type() instead of gettype() * update polish translation * Make all Enumerations classes abstract * Added search functionality in Admin -> Config And removed the unused ['capa']['quota'] * Cleanup Quota handling * OPEN_PGP should be OPENPGP as it is one word * Improve Capa handling * Resolve #1451 * Bugfix TypeError: b64Encode(...).match(...) is null * Small StorageType change * Bugfix: mailvelope editor failed * Bugfix: undefined getMailvelopePrivateKeyFor() * Bugfix: MIME parser RegExp didn't escape `boundary` which caused issues * Return detailed info on PgpImportKey * Show GnuPG verify error * Sort PGP keys by email and id * Sort S/MIME certificates on emailAddress else validTo * S/MIME import from signature use `BEGIN PKCS7` * Optionally use existing private key to generate S/MIME certificate * Chaned some error_log() to MailSo Logger() * Force reload of S/MIME certificates list on import * Make better use of SnappyMail\SensitiveString * Fix view PGP key button * Mask all POST data that has a key which contains `pass` * v2.35.1 * Resolve #1455 * Improved GnuPG error handling * Update pt/pt-PT translation * update Polish translation * Drop support for gnupg pecl extension as it fails with "no passphrase" issues * Resolve #1456 * Resolve #1458 * v2.35.2 * fix changelog * Resolve #1461 * Update pt/pt-PT translation * compact-composer plugin v1.0.0 * Resolve #1462 * Fix decrypt error message * `new Error()` to `Error()` * Resolve #1463 * Show url for #1466 * Simplify SignMe/Remember me code * Simplify language Notifications * Bugfix: SetPassword expects \SnappyMail\SensitiveString * https://github.com/the-djmaze/snappymail/issues/1450#issuecomment-1972147950 * improve: fire the 'squire2-toolbar' event after more props are added * improve: add dark theme support and use 'button' element as menu trigger for consistent styling * fix: use compact template in non-destructive way (do not replace the PopupsCompose template if a different wysiwyg is used) * Update admin.json * Update user.json * CSS rainloopErrorTip location * Improved error handling on PGP and S/MIME decrypt * KnockoutJS remove unused `beforeRemove` * KnockoutJS drop unused `as` * KnockoutJS simplify renderMode because only 1 option is used * KnoutJS cleanup templating.js a bit * KnockoutJS drop unused `bindingRewriteValidators` * KnockoutJS drop the twoWayBindings code * KnockoutJS simplify virtualElements binding check * KnockoutJS simplify applyBindingsToNodeInternal * KnockoutJS use Array.isArray * KnockoutJS drop alias `textinput` for `textInput` * KnockoutJS scramble `createChildContext` * KnockoutJS scramble `controlsDescendantBindings` * KnockoutJS scramble `exportDependencies` * KnockoutJS drop unused `throttleEvaluation` * KnockoutJS drop unused `valueAllowUnset` * KnockoutJS drop unused `templateNodes` * KnockoutJS drop unused `optionsCaption` * KnockoutJS drop unused `dontLimitMoves` * KnockoutJS drop unused `uniqueName` * KnockoutJS drop IE leftovers * KnockoutJS drop unused `preprocess` * KnockoutJS drop unused "disposeWhenNodeIsRemoved" and "disposeWhen" * KnockoutJS don't scramble exportDependencies. controlsDescendantBindings, createChildContext * KnockoutJS drop unused `$parentContext` and `$parents` * KnockoutJS drop unused `$rawData` * Knockoutjs built latest * KnockoutJS drop unused template options `nodes`, `if`, `ifnot` * KnockoutJS use more Array.isArray * KnockoutJS cleanup code a bit * KnockoutJS primitiveTypes can just be checked with Object() * KnockoutJS rebuilt * Verify S/MIME signed automatically and log Exception * Automatically verify PGP and S/MIME signed messages * `new Error` to `Error` * By default throw AccountNotAllowed as confused in #1478 * GPG use pinentries for decrypt, sign and export * Better GPG error handling * GPG show error on view/export * OpenPGP fix handling of importing keys * Make "verify signatures automatically" optional, as it requires more IMAP fetching * S/MIME don't post identity key and certificate, just fetch from server * Show error to old browsers, instead of crashing * Automatically verify S/MIME decrypted signed message --------- Co-authored-by: the-djmaze <> Co-authored-by: tinola <tinola@poczta.onet.pl> Co-authored-by: Maarten <3752035+the-djmaze@users.noreply.github.com> Co-authored-by: lmperfis <joint.striker@gmail.com> Co-authored-by: Sergey Mosin <sergey@srgdev.com> Co-authored-by: hguilbert <51283484+hguilbert@users.noreply.github.com>
This commit is contained in:
parent
3bd073a38b
commit
876ed221c3
218 changed files with 10266 additions and 4267 deletions
|
|
@ -95,7 +95,6 @@ Notifications = {
|
|||
JsonParse: 952,
|
||||
// JsonTimeout: 953,
|
||||
|
||||
UnknownNotification: 998,
|
||||
UnknownError: 999,
|
||||
|
||||
// Admin
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const
|
|||
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notifications, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
||||
},
|
||||
|
||||
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||
|
|
@ -102,8 +102,7 @@ export const
|
|||
|
||||
timestampToString = (timeStampInUTC, formatStr) => {
|
||||
const now = Date.now(),
|
||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||
|
||||
time = 0 < timeStampInUTC ? timeStampInUTC * 1000 : (0 === timeStampInUTC ? now : 0);
|
||||
if (31536000000 < time) {
|
||||
const m = new Date(time), h = LanguageStore.hourCycle();
|
||||
switch (formatStr) {
|
||||
|
|
@ -212,7 +211,7 @@ export const
|
|||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
||||
script.onerror = () => reject(Error('Language '+language+' failed'));
|
||||
script.src = langLink(language, admin);
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class JCard {
|
|||
if (input) {
|
||||
// read from jCard
|
||||
if (typeof input !== 'object') {
|
||||
throw new Error('error reading vcard')
|
||||
throw Error('error reading vcard')
|
||||
}
|
||||
this.parseFromJCard(input)
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ export class JCard {
|
|||
arg = new VCardProperty(String(arg), value, params, type);
|
||||
}
|
||||
if (!(arg instanceof VCardProperty)) {
|
||||
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||
}
|
||||
let field = arg.getField();
|
||||
this.props.set(field, [arg]);
|
||||
|
|
@ -101,7 +101,7 @@ export class JCard {
|
|||
arg = new VCardProperty(String(arg), value, params, type);
|
||||
}
|
||||
if (!(arg instanceof VCardProperty)) {
|
||||
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||
}
|
||||
// VCardProperty arguments
|
||||
let field = arg.getField();
|
||||
|
|
@ -125,14 +125,14 @@ export class JCard {
|
|||
else if (arg instanceof VCardProperty) {
|
||||
let propArray = this.props.get(arg.getField());
|
||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
||||
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||
propArray.splice(propArray.indexOf(arg), 1);
|
||||
if (propArray.length === 0)
|
||||
this.props.delete(arg.getField());
|
||||
}
|
||||
// incorrect arguments
|
||||
else
|
||||
throw new Error('invalid argument of VCard.remove(), expects ' +
|
||||
throw Error('invalid argument of VCard.remove(), expects ' +
|
||||
'string and optional param filter or a VCardProperty');
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ export class JCard {
|
|||
parseFullName(options) {
|
||||
let n = this.getOne('n');
|
||||
if (n === undefined) {
|
||||
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||
}
|
||||
let fnString = '';
|
||||
// Position in n -> position in fn
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class VCardProperty {
|
|||
}
|
||||
// invalid property
|
||||
else {
|
||||
throw new Error('invalid Property constructor');
|
||||
throw Error('invalid Property constructor');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
5
dev/External/ko.js
vendored
5
dev/External/ko.js
vendored
|
|
@ -66,8 +66,7 @@ Object.assign(ko.bindingHandlers, {
|
|||
},
|
||||
update: (element, fValueAccessor) => {
|
||||
let value = ko.unwrap(fValueAccessor());
|
||||
value = isFunction(value) ? value() : value;
|
||||
errorTip(element, value);
|
||||
errorTip(element, isFunction(value) ? value() : value);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -107,7 +106,7 @@ Object.assign(ko.bindingHandlers, {
|
|||
const command = fValueAccessor();
|
||||
|
||||
if (!command || !command.canExecute) {
|
||||
throw new Error('Value should be a command');
|
||||
throw Error('Value should be a command');
|
||||
}
|
||||
|
||||
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class AbstractModel {
|
|||
constructor() {
|
||||
/*
|
||||
if (new.target === AbstractModel) {
|
||||
throw new Error("Can't instantiate AbstractModel!");
|
||||
throw Error("Can't instantiate AbstractModel!");
|
||||
}
|
||||
*/
|
||||
Object.defineProperty(this, 'disposables', {value: []});
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export function ParseMime(text)
|
|||
let boundary = headers['content-type']?.params.boundary;
|
||||
if (boundary) {
|
||||
part.boundary = boundary;
|
||||
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
|
||||
let regex = new RegExp('(?:^|\r?\n)--' + RegExp.escape(boundary) + '(?:--)?(?:\r?\n|$)', 'g'),
|
||||
body = mimePart.slice(head.length),
|
||||
bodies = body.split(regex),
|
||||
pos = part.bodyStart;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export class AbstractCollectionModel extends Array
|
|||
constructor() {
|
||||
/*
|
||||
if (new.target === AbstractCollectionModel) {
|
||||
throw new Error("Can't instantiate AbstractCollectionModel!");
|
||||
throw Error("Can't instantiate AbstractCollectionModel!");
|
||||
}
|
||||
*/
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ export class IdentityModel extends AbstractModel {
|
|||
});
|
||||
|
||||
addComputablesTo(this, {
|
||||
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----')
|
||||
// smimeKeyValid: () => this.smimeKeyEncrypted() | this.smimeKey().includes('-----BEGIN PRIVATE KEY-----')
|
||||
// smimeCertificateValid: () => this.smimeKey().includes('-----BEGIN CERTIFICATE-----')
|
||||
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
|
||||
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()),
|
||||
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import { LanguageStore } from 'Stores/Language';
|
|||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { MimeToMessage } from 'Mime/Utils';
|
||||
|
||||
const
|
||||
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||
|
||||
|
|
@ -118,12 +120,10 @@ export class MessageModel extends AbstractModel {
|
|||
encrypted: false,
|
||||
|
||||
pgpSigned: null,
|
||||
pgpVerified: null,
|
||||
pgpEncrypted: null,
|
||||
pgpDecrypted: false,
|
||||
|
||||
smimeSigned: null,
|
||||
smimeVerified: null,
|
||||
smimeEncrypted: null,
|
||||
smimeDecrypted: false,
|
||||
|
||||
|
|
@ -196,6 +196,10 @@ export class MessageModel extends AbstractModel {
|
|||
return options;
|
||||
}
|
||||
});
|
||||
|
||||
this.smimeSigned.subscribe(value =>
|
||||
value?.body && MimeToMessage(value.body, this)
|
||||
);
|
||||
}
|
||||
|
||||
get requestHash() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ checkResponseError = data => {
|
|||
Notifications.DomainNotAllowed,
|
||||
Notifications.AccountNotAllowed,
|
||||
Notifications.MailServerError,
|
||||
Notifications.UnknownNotification,
|
||||
Notifications.UnknownError
|
||||
].includes(err)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,32 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
|||
|
||||
constructor() {
|
||||
this.config = ko.observableArray();
|
||||
this.search = ko.observableArray();
|
||||
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
||||
|
||||
this.search.subscribe(value => {
|
||||
const v = value.toLowerCase(),
|
||||
qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
|
||||
match = node => node.textContent.toLowerCase().includes(v);
|
||||
if (v.length) {
|
||||
qsa(this.viewModelDom, 'tbody', tbody => {
|
||||
let show = match(tbody.querySelector('th'));
|
||||
if (show) {
|
||||
qsa(tbody, '[hidden]', n => n.hidden = false);
|
||||
} else {
|
||||
qsa(tbody, 'tbody td:first-child', td => {
|
||||
let hide = !match(td);
|
||||
show = show || !hide;
|
||||
// td.closest('tr').hidden = hide;
|
||||
td.parentNode.hidden = hide;
|
||||
});
|
||||
}
|
||||
tbody.hidden = !show;
|
||||
});
|
||||
} else {
|
||||
qsa(this.viewModelDom, 'table [hidden]', n => n.hidden = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeShow() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['useLocalProxyForExternalImages']);
|
||||
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
|
||||
|
||||
this.weakPassword = rl.app.weakPassword;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
|||
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||
|
||||
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
|
|
@ -62,14 +63,14 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
|
||||
importToOpenPGP() {
|
||||
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
|
||||
(iError, oData) => {
|
||||
if (!iError && oData.Result) {
|
||||
oData.Result.forEach(key => OpenPGPUserStore.importKey(key));
|
||||
}
|
||||
}
|
||||
(iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
|
||||
);
|
||||
}
|
||||
|
||||
importToSMime() {
|
||||
showScreenPopup(SMimeImportPopupView);
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
/**
|
||||
* Create an iframe to display the Mailvelope keyring settings.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class AbstractModel {
|
|||
constructor() {
|
||||
/*
|
||||
if (new.target === AbstractModel) {
|
||||
throw new Error("Can't instantiate AbstractModel!");
|
||||
throw Error("Can't instantiate AbstractModel!");
|
||||
}
|
||||
*/
|
||||
Object.defineProperty(this, 'disposables', {value: []});
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ export const GnuPGUserStore = new class {
|
|||
this.keyring = null;
|
||||
this.publicKeys([]);
|
||||
this.privateKeys([]);
|
||||
Remote.request('GnupgGetKeys',
|
||||
SettingsCapa('GnuPG')
|
||||
&& Remote.request('GnupgGetKeys',
|
||||
(iError, oData) => {
|
||||
if (oData?.Result) {
|
||||
this.keyring = oData.Result;
|
||||
|
|
@ -68,11 +69,10 @@ export const GnuPGUserStore = new class {
|
|||
}
|
||||
};
|
||||
if (isPrivate) {
|
||||
key.password = async (btnTxt = 'SIGN') => {
|
||||
const pass = await Passphrases.ask(
|
||||
key,
|
||||
key.password = async btnTxt => {
|
||||
const pass = await Passphrases.ask(key,
|
||||
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
|
||||
'CRYPTO/'+btnTxt
|
||||
btnTxt
|
||||
);
|
||||
pass && pass.remember && Passphrases.set(key, pass.password);
|
||||
return pass?.password;
|
||||
|
|
@ -82,8 +82,8 @@ export const GnuPGUserStore = new class {
|
|||
if (key.armor) {
|
||||
callback && callback();
|
||||
} else {
|
||||
let pass = isPrivate ? await key.password('POPUP_VIEW_TITLE') : '';
|
||||
if (null != pass) {
|
||||
let pass = isPrivate ? await key.password('OPENPGP/POPUP_VIEW_TITLE') : '';
|
||||
if (null != pass) try {
|
||||
const result = await Remote.post('GnupgExportKey', null, {
|
||||
keyId: key.id,
|
||||
isPrivate: isPrivate,
|
||||
|
|
@ -95,15 +95,22 @@ export const GnuPGUserStore = new class {
|
|||
} else {
|
||||
Passphrases.delete(key);
|
||||
}
|
||||
} catch (e) {
|
||||
Passphrases.delete(key);
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
return key.armor;
|
||||
};
|
||||
key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key]));
|
||||
return key;
|
||||
};
|
||||
this.publicKeys(oData.Result.public.map(key => initKey(key, 0)));
|
||||
this.privateKeys(oData.Result.private.map(key => initKey(key, 1)));
|
||||
},
|
||||
collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
|
||||
sort = keys => keys.sort(
|
||||
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
|
||||
);
|
||||
this.publicKeys(sort(oData.Result.public.map(key => initKey(key, 0))));
|
||||
this.privateKeys(sort(oData.Result.private.map(key => initKey(key, 1))));
|
||||
console.log('gnupg ready');
|
||||
}
|
||||
}
|
||||
|
|
@ -178,22 +185,27 @@ export const GnuPGUserStore = new class {
|
|||
uid: message.uid,
|
||||
partId: pgpInfo.partId,
|
||||
keyId: key.id,
|
||||
passphrase: await key.password('DECRYPT'),
|
||||
passphrase: await key.password('CRYPTO/DECRYPT'),
|
||||
data: '' // message.plain() optional
|
||||
}
|
||||
if (null !== params.passphrase) {
|
||||
const result = await Remote.post('GnupgDecrypt', null, params);
|
||||
if (result?.Result && false !== result.Result.data) {
|
||||
return result.Result;
|
||||
if (null != params.passphrase) {
|
||||
try {
|
||||
const response = await Remote.post('GnupgDecrypt', null, params);
|
||||
if (response?.Result?.data) {
|
||||
return response.Result;
|
||||
}
|
||||
throw response;
|
||||
} catch (e) {
|
||||
Passphrases.delete(key);
|
||||
throw e;
|
||||
}
|
||||
Passphrases.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async verify(message) {
|
||||
let data = message.pgpSigned(); // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||
let data = message.pgpSigned(); // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||
if (data) {
|
||||
data = { ...data }; // clone
|
||||
// const sender = message.from[0].email;
|
||||
|
|
@ -208,14 +220,15 @@ export const GnuPGUserStore = new class {
|
|||
if (response?.Result) {
|
||||
return {
|
||||
fingerprint: response.Result.fingerprint,
|
||||
success: 0 == response.Result.status // GOODSIG
|
||||
success: 0 == response.Result.status, // GOODSIG
|
||||
error: response.Result.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sign(privateKey) {
|
||||
return await privateKey.password();
|
||||
return await privateKey.password('CRYPTO/SIGN');
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
124
dev/Stores/User/Mailvelope.js
Normal file
124
dev/Stores/User/Mailvelope.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { SettingsGet } from 'Common/Globals';
|
||||
|
||||
// https://mailvelope.github.io/mailvelope/Keyring.html
|
||||
let mailvelopeKeyring = null;
|
||||
|
||||
export const MailvelopeUserStore = {
|
||||
keyring: null,
|
||||
|
||||
loadKeyring(identifier) {
|
||||
identifier = identifier || SettingsGet('Email');
|
||||
if (window.mailvelope) {
|
||||
const fn = keyring => {
|
||||
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 {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
addEventListener('mailvelope-disconnect', event => {
|
||||
alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
|
||||
}, false);
|
||||
} else {
|
||||
addEventListener('mailvelope', () => this.loadKeyring(identifier));
|
||||
}
|
||||
},
|
||||
|
||||
async hasPublicKeyForEmails(recipients) {
|
||||
const
|
||||
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
|
||||
/*.then(LookupResult => Object.entries(LookupResult))*/,
|
||||
entries = mailvelope && Object.entries(mailvelope);
|
||||
return entries && entries.filter(value => value[1]).length === recipients.length;
|
||||
},
|
||||
|
||||
async getPrivateKeyFor(email/*, sign*/) {
|
||||
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
|
||||
return ['mailvelope', email];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async decrypt(message) {
|
||||
// Try Mailvelope (does not support inline images)
|
||||
if (mailvelopeKeyring) {
|
||||
const sender = message.from[0].email,
|
||||
armoredText = message.plain();
|
||||
try {
|
||||
let emails = [...message.from,...message.to,...message.cc].validUnique(),
|
||||
i = emails.length;
|
||||
while (i--) {
|
||||
if (await this.getPrivateKeyFor(emails[i].email)) {
|
||||
/**
|
||||
* 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 = '';
|
||||
let result = await mailvelope.createDisplayContainer(
|
||||
'#'+body.id,
|
||||
armoredText,
|
||||
mailvelopeKeyring,
|
||||
{
|
||||
senderAddress: sender
|
||||
// emails[i].email
|
||||
}
|
||||
);
|
||||
if (result) {
|
||||
if (result.error?.message) {
|
||||
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
|
||||
alert(result.error.code + ': ' + result.error.message);
|
||||
}
|
||||
} else {
|
||||
body.classList.add('mailvelope');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns headers that should be added to an outgoing email.
|
||||
* So far this is only the autocrypt header.
|
||||
*/
|
||||
/*
|
||||
mailvelopeKeyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de')
|
||||
.then(function(additional) {
|
||||
console.log('additionalHeadersForOutgoingEmail', additional);
|
||||
// logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."}
|
||||
});
|
||||
|
||||
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
|
||||
mailvelopeKeyring.createKeyBackupContainer(selector, options)
|
||||
mailvelopeKeyring.createKeyGenContainer(selector, {
|
||||
// userIds: [],
|
||||
keySize: 4096
|
||||
})
|
||||
|
||||
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
|
||||
mailvelopeKeyring.importPublicKey(armored)
|
||||
|
||||
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
|
||||
mailvelopeKeyring.addSyncHandler({
|
||||
uploadSync
|
||||
downloadSync
|
||||
backup
|
||||
restore
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
|
@ -24,8 +24,7 @@ const
|
|||
return privateKey.key;
|
||||
}
|
||||
const key = privateKey.id,
|
||||
pass = await Passphrases.ask(
|
||||
key,
|
||||
pass = await Passphrases.ask(privateKey,
|
||||
'OpenPGP.js key<br>' + key + ' ' + privateKey.emails[0],
|
||||
'CRYPTO/'+btnTxt
|
||||
);
|
||||
|
|
@ -35,11 +34,20 @@ const
|
|||
privateKey: privateKey.key,
|
||||
passphrase
|
||||
});
|
||||
result && pass.remember && Passphrases.set(key, passphrase);
|
||||
result && pass.remember && Passphrases.set(privateKey, passphrase);
|
||||
return result;
|
||||
}
|
||||
},
|
||||
|
||||
collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
|
||||
sort = keys => keys.sort(
|
||||
// (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.fingerprint, b.fingerprint)
|
||||
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
|
||||
),
|
||||
dedup = keys => sort((keys || [])
|
||||
.filter((v, i, a) => a.findIndex(entry => entry.fingerprint == v.fingerprint) === i)
|
||||
),
|
||||
|
||||
/**
|
||||
* OpenPGP.js v5 removed the localStorage (keyring)
|
||||
* This should be compatible with the old OpenPGP.js v2
|
||||
|
|
@ -69,15 +77,11 @@ const
|
|||
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.emails = key.users.map(user => user.userID.email).filter(email => email);
|
||||
this.armor = armor;
|
||||
this.askDelete = ko.observable(false);
|
||||
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||
|
|
@ -85,6 +89,15 @@ class OpenPgpKeyModel {
|
|||
// key.getPrimaryUser()
|
||||
}
|
||||
|
||||
/*
|
||||
get id() { return this.key.getKeyID().toHex().toUpperCase(); }
|
||||
get fingerprint() { return this.key.getFingerprint(); }
|
||||
get can_encrypt() { return !!this.key.getEncryptionKey(); }
|
||||
get can_sign() { return !!this.key.getSigningKey(); }
|
||||
get emails() { return this.key.users.map(user => user.userID.email).filter(email => email); }
|
||||
get armor() { return this.key.armor(); }
|
||||
*/
|
||||
|
||||
view() {
|
||||
showScreenPopup(OpenPgpKeyPopupView, [this]);
|
||||
}
|
||||
|
|
@ -115,10 +128,6 @@ export const OpenPGPUserStore = new class {
|
|||
|
||||
loadKeyrings() {
|
||||
if (window.openpgp) {
|
||||
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}),
|
||||
dedup = keys => (keys || [])
|
||||
.filter((v, i, a) => a.findIndex(entry => entry.fingerprint == v.fingerprint) === i)
|
||||
.sort((a, b) => collator.compare(a.emails[0], b.emails[0]));
|
||||
loadOpenPgpKeys(publicKeysItem).then(keys => {
|
||||
this.publicKeys(dedup(keys));
|
||||
console.log('openpgp.js public keys loaded');
|
||||
|
|
@ -138,22 +147,29 @@ export const OpenPGPUserStore = new class {
|
|||
}
|
||||
|
||||
importKey(armoredKey) {
|
||||
window.openpgp && openpgp.readKey({armoredKey:armoredKey}).then(key => {
|
||||
if (!key.err) {
|
||||
key = new OpenPgpKeyModel(armoredKey, key);
|
||||
if (key.key.isPrivate()) {
|
||||
if (!this.privateKeys.find(entry => entry.fingerprint == key.fingerprint)) {
|
||||
this.privateKeys.push(key);
|
||||
storeOpenPgpKeys(this.privateKeys, privateKeysItem);
|
||||
}
|
||||
} else {
|
||||
if (!this.publicKeys.find(entry => entry.fingerprint == key.fingerprint)) {
|
||||
this.publicKeys.push(key);
|
||||
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
|
||||
}
|
||||
this.importKeys([armoredKey]);
|
||||
}
|
||||
|
||||
async importKeys(keys) {
|
||||
if (window.openpgp) {
|
||||
const privateKeys = this.privateKeys(),
|
||||
publicKeys = this.publicKeys();
|
||||
for (const armoredKey of keys) try {
|
||||
let key = await openpgp.readKey({armoredKey:armoredKey});
|
||||
if (!key.err) {
|
||||
key = new OpenPgpKeyModel(armoredKey, key);
|
||||
const keys = key.key.isPrivate() ? privateKeys : publicKeys;
|
||||
keys.find(entry => entry.fingerprint == key.fingerprint)
|
||||
|| keys.push(key);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, armoredKey);
|
||||
}
|
||||
});
|
||||
this.privateKeys(sort(privateKeys));
|
||||
this.publicKeys(sort(publicKeys));
|
||||
storeOpenPgpKeys(privateKeys, privateKeysItem);
|
||||
storeOpenPgpKeys(publicKeys, publicKeysItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -229,7 +245,7 @@ export const OpenPGPUserStore = new class {
|
|||
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
|
||||
*/
|
||||
async verify(message) {
|
||||
const data = message.pgpSigned(), // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||
const data = message.pgpSigned(), // { partId: "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;
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
|||
|
||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||
import { MailvelopeUserStore } from 'Stores/User/Mailvelope';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
// https://mailvelope.github.io/mailvelope/Keyring.html
|
||||
let mailvelopeKeyring = null;
|
||||
|
||||
export const
|
||||
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
|
||||
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
|
||||
|
|
@ -37,32 +35,9 @@ export const
|
|||
}
|
||||
|
||||
loadKeyrings(identifier) {
|
||||
identifier = identifier || SettingsGet('Email');
|
||||
if (window.mailvelope) {
|
||||
const fn = keyring => {
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
|
||||
MailvelopeUserStore.loadKeyring(identifier);
|
||||
OpenPGPUserStore.loadKeyrings();
|
||||
|
||||
if (SettingsCapa('GnuPG')) {
|
||||
GnuPGUserStore.loadKeyrings();
|
||||
}
|
||||
GnuPGUserStore.loadKeyrings();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,22 +67,14 @@ export const
|
|||
}
|
||||
);
|
||||
}
|
||||
OpenPGPUserStore.isSupported() && OpenPGPUserStore.importKey(key);
|
||||
}
|
||||
|
||||
async mailvelopeHasPublicKeyForEmails(recipients) {
|
||||
const
|
||||
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
|
||||
/*.then(LookupResult => Object.entries(LookupResult))*/,
|
||||
entries = mailvelope && Object.entries(mailvelope);
|
||||
return entries && entries.filter(value => value[1]).length === recipients.length;
|
||||
OpenPGPUserStore.importKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if verifying/encrypting a message is possible with given email addresses.
|
||||
* Returns the first library that can.
|
||||
*/
|
||||
async hasPublicKeyForEmails(recipients) {
|
||||
hasPublicKeyForEmails(recipients) {
|
||||
if (recipients.length) {
|
||||
if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
|
||||
return 'gnupg';
|
||||
|
|
@ -119,40 +86,15 @@ export const
|
|||
return false;
|
||||
}
|
||||
|
||||
async getMailvelopePrivateKeyFor(email/*, sign*/) {
|
||||
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
|
||||
return ['mailvelope', email];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
||||
if (key) {
|
||||
return ['gnupg', key];
|
||||
}
|
||||
|
||||
// return await this.getMailvelopePrivateKeyFor(email, 1);
|
||||
}
|
||||
|
||||
async decrypt(message) {
|
||||
const sender = message.from[0].email,
|
||||
armoredText = message.plain();
|
||||
const armoredText = message.plain();
|
||||
if (!this.isEncrypted(armoredText)) {
|
||||
throw Error('Not armored text');
|
||||
}
|
||||
|
||||
// Try OpenPGP.js
|
||||
if (OpenPGPUserStore.isSupported()) {
|
||||
const sender = message.from[0].email;
|
||||
let result = await OpenPGPUserStore.decrypt(armoredText, sender);
|
||||
if (result) {
|
||||
return result;
|
||||
|
|
@ -160,68 +102,20 @@ export const
|
|||
}
|
||||
|
||||
// Try Mailvelope (does not support inline images)
|
||||
if (mailvelopeKeyring) {
|
||||
try {
|
||||
let emails = [...message.from,...message.to,...message.cc].validUnique(),
|
||||
i = emails.length;
|
||||
while (i--) {
|
||||
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
|
||||
/**
|
||||
* 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 = '';
|
||||
let result = await mailvelope.createDisplayContainer(
|
||||
'#'+body.id,
|
||||
armoredText,
|
||||
mailvelopeKeyring,
|
||||
{
|
||||
senderAddress: sender
|
||||
// emails[i].email
|
||||
}
|
||||
);
|
||||
if (result) {
|
||||
if (result.error?.message) {
|
||||
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
|
||||
alert(result.error.code + ': ' + result.error.message);
|
||||
}
|
||||
} else {
|
||||
body.classList.add('mailvelope');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Now try GnuPG
|
||||
return GnuPGUserStore.decrypt(message);
|
||||
return (await MailvelopeUserStore.decrypt(message))
|
||||
// Or try GnuPG
|
||||
|| GnuPGUserStore.decrypt(message);
|
||||
}
|
||||
|
||||
async verify(message) {
|
||||
const signed = message.pgpSigned();
|
||||
const signed = message.pgpSigned(),
|
||||
sender = message.from[0].email;
|
||||
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);
|
||||
}
|
||||
if (openpgp) {
|
||||
// OpenPGP only when inline, else we must download the whole message
|
||||
if (!signed.sigPartId && OpenPGPUserStore.hasPublicKeyForEmails([sender])) {
|
||||
return OpenPGPUserStore.verify(message);
|
||||
}
|
||||
if (gnupg) {
|
||||
if (GnuPGUserStore.hasPublicKeyForEmails([sender])) {
|
||||
return GnuPGUserStore.verify(message);
|
||||
}
|
||||
// Mailvelope can't
|
||||
|
|
@ -248,35 +142,4 @@ export const
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns headers that should be added to an outgoing email.
|
||||
* So far this is only the autocrypt header.
|
||||
*/
|
||||
/*
|
||||
mailvelopeKeyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de')
|
||||
.then(function(additional) {
|
||||
console.log('additionalHeadersForOutgoingEmail', additional);
|
||||
// logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."}
|
||||
});
|
||||
|
||||
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
|
||||
mailvelopeKeyring.createKeyBackupContainer(selector, options)
|
||||
mailvelopeKeyring.createKeyGenContainer(selector, {
|
||||
// userIds: [],
|
||||
keySize: 4096
|
||||
})
|
||||
|
||||
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
|
||||
mailvelopeKeyring.importPublicKey(armored)
|
||||
|
||||
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
|
||||
mailvelopeKeyring.addSyncHandler({
|
||||
uploadSync
|
||||
downloadSync
|
||||
backup
|
||||
restore
|
||||
});
|
||||
*/
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ SMimeUserStore.loadCertificates = () => {
|
|||
SMimeUserStore.loading(true);
|
||||
Remote.request('SMimeGetCertificates', (iError, oData) => {
|
||||
SMimeUserStore.loading(false);
|
||||
iError || SMimeUserStore(oData.Result);
|
||||
const collator = new Intl.Collator(undefined, {sensitivity: 'base'});
|
||||
iError || SMimeUserStore(oData.Result.sort(
|
||||
(a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t)
|
||||
));
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 100px;
|
||||
color: var(--folders-color, #333);
|
||||
}
|
||||
|
||||
hr {
|
||||
|
|
@ -77,22 +78,18 @@
|
|||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
line-height: 34px;
|
||||
background-color: transparent;
|
||||
vertical-align: middle;
|
||||
color: var(--folders-disabled-color, #666);
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
color: var(--folders-color, #333);
|
||||
display: block;
|
||||
line-height: 34px;
|
||||
padding: 0 2em 0 @folderItemPadding;
|
||||
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
z-index: 1;
|
||||
|
||||
&.selectable {
|
||||
|
||||
color: var(--folders-color, #333);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -115,6 +112,7 @@
|
|||
}
|
||||
}
|
||||
&:not(.selectable) {
|
||||
color: var(--folders-disabled-color, #666);
|
||||
cursor: default;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
color: red;
|
||||
content: attr(data-rainloopErrorTip);
|
||||
font-size: 90%;
|
||||
left: 100%;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
padding: 0.25em;
|
||||
position : absolute;
|
||||
|
|
|
|||
|
|
@ -123,4 +123,8 @@ html[dir="rtl"] {
|
|||
border-right: 6px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
[data-rainloopErrorTip]::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
|
|||
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||
import { MailvelopeUserStore } from 'Stores/User/Mailvelope';
|
||||
//import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||
import { Passphrases } from 'Storage/Passphrases';
|
||||
|
|
@ -65,7 +66,7 @@ const
|
|||
|
||||
tpl = createElement('template'),
|
||||
|
||||
base64_encode = text => b64Encode(text).match(/.{1,76}/g).join('\r\n'),
|
||||
base64_encode = text => text ? b64Encode(text).match(/.{1,76}/g).join('\r\n') : '',
|
||||
|
||||
getEmail = value => addressparser(value)[0]?.email || false,
|
||||
|
||||
|
|
@ -255,13 +256,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
doSign: false,
|
||||
doEncrypt: false,
|
||||
|
||||
pgpSignKey: false,
|
||||
canPgpEncrypt: false,
|
||||
canMailvelope: false,
|
||||
|
||||
canSMimeSign: false,
|
||||
canSMimeEncrypt: false,
|
||||
|
||||
draftsFolder: '',
|
||||
draftUid: 0,
|
||||
sending: false,
|
||||
|
|
@ -284,6 +278,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
});
|
||||
|
||||
this.attachments = koArrayWithDestroy();
|
||||
this.encryptOptions = koArrayWithDestroy();
|
||||
this.signOptions = koArrayWithDestroy();
|
||||
|
||||
this.dragAndDropOver = ko.observable(false).extend({ debounce: 1 });
|
||||
this.dragAndDropVisible = ko.observable(false).extend({ debounce: 1 });
|
||||
|
|
@ -336,11 +332,9 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
|
||||
isDraft: () => this.draftsFolder() && this.draftUid(),
|
||||
|
||||
canSign: () => {
|
||||
let s = this.canSMimeSign();
|
||||
return this.pgpSignKey() || s;
|
||||
},
|
||||
canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(),
|
||||
canEncrypt: () => this.encryptOptions().length,
|
||||
canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
|
||||
canSign: () => this.signOptions().length,
|
||||
|
||||
identitiesOptions: () =>
|
||||
IdentityUserStore.map(item => ({
|
||||
|
|
@ -361,24 +355,14 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
|
||||
currentIdentity: value => {
|
||||
if (value) {
|
||||
const smime = !!(value.smimeKey() && value.smimeCertificate());
|
||||
this.from(value.formattedName());
|
||||
this.doEncrypt(value.pgpEncrypt()/* || SettingsUserStore.pgpEncrypt()*/);
|
||||
this.doSign(smime || value.pgpSign()/* || SettingsUserStore.pgpSign()*/);
|
||||
this.canSMimeSign(smime);
|
||||
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
|
||||
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
|
||||
}
|
||||
},
|
||||
|
||||
from: value => {
|
||||
this.pgpSignKey(false);
|
||||
value = getEmail(value);
|
||||
value && PgpUserStore.getKeyForSigning(value).then(result => {
|
||||
console.log({
|
||||
email: value,
|
||||
pgpSignKey:result
|
||||
});
|
||||
this.pgpSignKey(result)
|
||||
});
|
||||
from: () => {
|
||||
this.initSign();
|
||||
this.initEncrypt();
|
||||
},
|
||||
|
||||
|
|
@ -1381,29 +1365,55 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
].join(',').split(',').map(value => getEmail(value.trim())).validUnique();
|
||||
}
|
||||
|
||||
initEncrypt() {
|
||||
const recipients = this.allRecipients();
|
||||
PgpUserStore.hasPublicKeyForEmails(recipients).then(result => {
|
||||
console.log({canPgpEncrypt:result});
|
||||
this.canPgpEncrypt(result);
|
||||
});
|
||||
PgpUserStore.mailvelopeHasPublicKeyForEmails(recipients).then(result => {
|
||||
console.log({canMailvelope:result});
|
||||
this.canMailvelope(result);
|
||||
if (!result) {
|
||||
/**
|
||||
* Checks if signing a message is possible with from email address.
|
||||
* And sets all that can.
|
||||
*/
|
||||
initSign() {
|
||||
let options = [],
|
||||
identity = this.currentIdentity(),
|
||||
email = getEmail(this.from()),
|
||||
key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
|
||||
key && options.push(['OpenPGP', key]);
|
||||
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
||||
key && options.push(['GnuPG', key]);
|
||||
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
|
||||
&& options.push(['S/MIME']);
|
||||
console.dir({signOptions: options});
|
||||
this.signOptions(options);
|
||||
}
|
||||
|
||||
async initEncrypt() {
|
||||
const recipients = this.allRecipients(),
|
||||
options = [];
|
||||
|
||||
if (recipients.length) {
|
||||
GnuPGUserStore.hasPublicKeyForEmails(recipients)
|
||||
&& options.push('GnuPG');
|
||||
|
||||
OpenPGPUserStore.hasPublicKeyForEmails(recipients)
|
||||
&& options.push('OpenPGP');
|
||||
|
||||
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
|
||||
options.push('Mailvelope');
|
||||
} else {
|
||||
'mailvelope' === this.viewArea() && this.bodyArea();
|
||||
// this.dropMailvelope();
|
||||
}
|
||||
});
|
||||
const count = recipients.length,
|
||||
identity = this.currentIdentity(),
|
||||
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null,
|
||||
length = count ? recipients.filter(email =>
|
||||
email == from
|
||||
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
|
||||
).length : 0;
|
||||
this.canSMimeEncrypt(length && length === count);
|
||||
console.log({canSMimeEncrypt:this.canSMimeEncrypt()});
|
||||
|
||||
const count = recipients.length,
|
||||
identity = this.currentIdentity(),
|
||||
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
|
||||
count
|
||||
&& count === recipients.filter(email =>
|
||||
email == from
|
||||
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
|
||||
).length
|
||||
&& options.push('S/MIME');
|
||||
}
|
||||
|
||||
console.dir({encryptOptions:options});
|
||||
this.encryptOptions(options);
|
||||
}
|
||||
|
||||
async getMessageRequestParams(sSaveFolder, draft)
|
||||
|
|
@ -1464,8 +1474,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
linkedData: []
|
||||
},
|
||||
recipients = draft ? [identity.email()] : this.allRecipients(),
|
||||
sign = !draft && this.doSign() && (this.pgpSignKey() || this.canSMimeSign()),
|
||||
encrypt = this.doEncrypt() && (this.canPgpEncrypt() || this.canSMimeEncrypt()),
|
||||
signOptions = !draft && this.doSign() && this.signOptions(),
|
||||
encryptOptions = this.doEncrypt() && this.encryptOptions(),
|
||||
isHtml = this.oEditor.isHtml();
|
||||
|
||||
if (isHtml) {
|
||||
|
|
@ -1492,7 +1502,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
params.autocrypt.push({addr:k, keydata:v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g).trim()})
|
||||
);
|
||||
*/
|
||||
} else if (sign || encrypt) {
|
||||
} else if (signOptions.length || encryptOptions.length) {
|
||||
if (!draft && !hasAttachments && !Text.length) {
|
||||
throw i18n('COMPOSE/ERROR_EMPTY_BODY');
|
||||
}
|
||||
|
|
@ -1512,9 +1522,10 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
alternative.children.push(data);
|
||||
data = alternative;
|
||||
}
|
||||
if (sign) {
|
||||
if (sign?.[1]) {
|
||||
if ('openpgp' == sign[0]) {
|
||||
let sign = true;
|
||||
for (let i = 0; i < signOptions.length; ++i) {
|
||||
if ('OpenPGP' == signOptions[i][0]) {
|
||||
try {
|
||||
// Doesn't sign attachments
|
||||
params.html = params.plain = '';
|
||||
let signed = new MimePart;
|
||||
|
|
@ -1525,34 +1536,53 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
let signature = new MimePart;
|
||||
signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"';
|
||||
signature.headers['Content-Transfer-Encoding'] = '7Bit';
|
||||
signature.body = await OpenPGPUserStore.sign(data.toString(), sign[1], 1);
|
||||
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
|
||||
signed.children.push(signature);
|
||||
params.signed = signed.toString();
|
||||
params.boundary = signed.boundary;
|
||||
data = signed;
|
||||
} else if ('gnupg' == sign[0]) {
|
||||
// TODO: sign in PHP fails
|
||||
// params.signData = data.toString();
|
||||
params.signFingerprint = sign[1].fingerprint;
|
||||
params.signPassphrase = await GnuPGUserStore.sign(sign[1]);
|
||||
} else {
|
||||
throw 'Signing with ' + sign[0] + ' not yet implemented';
|
||||
} catch (e) {
|
||||
sign = false;
|
||||
console.error(e);
|
||||
}
|
||||
} else if (this.canSMimeSign()) {
|
||||
params.signCertificate = identity.smimeCertificate();
|
||||
params.signPrivateKey = identity.smimeKey();
|
||||
break;
|
||||
}
|
||||
if ('GnuPG' == signOptions[i][0]) {
|
||||
// TODO: sign in PHP fails
|
||||
let pass = await GnuPGUserStore.sign(signOptions[i][1]);
|
||||
if (null != pass) {
|
||||
// params.signData = data.toString();
|
||||
params.signFingerprint = signOptions[i][1].fingerprint;
|
||||
params.signPassphrase = pass;
|
||||
} else {
|
||||
sign = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ('S/MIME' == signOptions[i][0]) {
|
||||
// TODO: sign in PHP fails
|
||||
params.sign = 'S/MIME';
|
||||
// params.signCertificate = identity.smimeCertificate();
|
||||
// params.signPrivateKey = identity.smimeKey();
|
||||
if (identity.smimeKeyEncrypted()) {
|
||||
const pass = await Passphrases.ask(
|
||||
params.signPrivateKey,
|
||||
const pass = await Passphrases.ask(identity,
|
||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||
'CRYPTO/DECRYPT'
|
||||
'CRYPTO/SIGN'
|
||||
);
|
||||
params.signPassphrase = pass?.password;
|
||||
// pass && pass.remember && Passphrases.set(identity, pass.password);
|
||||
if (null != pass) {
|
||||
params.signPassphrase = pass.password;
|
||||
// pass.remember && Passphrases.set(identity, pass.password);
|
||||
} else {
|
||||
sign = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (encrypt) {
|
||||
if (signOptions.length && !sign) {
|
||||
throw 'Signing failed';
|
||||
}
|
||||
|
||||
if (encryptOptions.length) {
|
||||
const autocrypt = () =>
|
||||
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
|
||||
params.autocrypt.push({
|
||||
|
|
@ -1560,24 +1590,30 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
|
||||
})
|
||||
);
|
||||
if ('openpgp' == encrypt) {
|
||||
// Doesn't encrypt attachments
|
||||
params.encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
|
||||
params.signed = '';
|
||||
autocrypt();
|
||||
} else if ('gnupg' == encrypt) {
|
||||
// Does encrypt attachments
|
||||
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
|
||||
autocrypt();
|
||||
} else if (this.canSMimeEncrypt() && identity && identity.smimeKey() && identity.smimeCertificate()) {
|
||||
params.encryptCertificates = [identity.smimeCertificate()];
|
||||
SMimeUserStore.forEach(certificate => {
|
||||
certificate.emailAddress != identity.email()
|
||||
&& recipients.includes(certificate.emailAddress)
|
||||
&& params.encryptCertificates.push(certificate.id)
|
||||
});
|
||||
} else {
|
||||
throw 'Encryption with ' + encrypt + ' not yet implemented';
|
||||
for (let i = 0; i < encryptOptions.length; ++i) {
|
||||
if ('OpenPGP' == encryptOptions[i]) {
|
||||
// Doesn't encrypt attachments
|
||||
params.encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
|
||||
params.signed = '';
|
||||
autocrypt();
|
||||
break;
|
||||
}
|
||||
if ('GnuPG' == encryptOptions[i]) {
|
||||
// Does encrypt attachments
|
||||
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
|
||||
autocrypt();
|
||||
break;
|
||||
}
|
||||
if ('S/MIME' == encryptOptions[i]) {
|
||||
params.encryptCertificates = [identity.smimeCertificate()];
|
||||
SMimeUserStore.forEach(certificate => {
|
||||
certificate.emailAddress != identity.email()
|
||||
&& recipients.includes(certificate.emailAddress)
|
||||
&& params.encryptCertificates.push(certificate.id)
|
||||
});
|
||||
break;
|
||||
}
|
||||
// We skip Mailvelope as it has its own window
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,13 @@ export class IdentityPopupView extends AbstractViewPopup {
|
|||
if (oData.Result.x509) {
|
||||
identity.smimeKey(oData.Result.pkey);
|
||||
identity.smimeCertificate(oData.Result.x509);
|
||||
} else {
|
||||
this.submitError(oData.ErrorMessage);
|
||||
}
|
||||
}, {
|
||||
name: identity.name(),
|
||||
email: identity.email(),
|
||||
privateKey: identity.smimeKey(),
|
||||
passphrase: pass.password
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,36 +78,30 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
|
|||
this.keyError(!keyTrimmed);
|
||||
this.keyErrorMessage('');
|
||||
|
||||
if (!keyTrimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let match = null,
|
||||
count = 30,
|
||||
done = false;
|
||||
// eslint-disable-next-line max-len
|
||||
const reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi;
|
||||
|
||||
do {
|
||||
match = reg.exec(keyTrimmed);
|
||||
if (match && 0 < count) {
|
||||
if (match[0] && match[1] && match[2] && match[1] === match[2]) {
|
||||
const GnuPG = this.saveGnuPG() && GnuPGUserStore.isSupported(),
|
||||
backup = this.saveServer();
|
||||
PgpUserStore.importKey(this.key(), GnuPG, backup);
|
||||
}
|
||||
--count;
|
||||
if (keyTrimmed) {
|
||||
let match = null,
|
||||
count = 30,
|
||||
done = false;
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
} while (!done);
|
||||
const GnuPG = this.saveGnuPG() && GnuPGUserStore.isSupported(),
|
||||
backup = this.saveServer(),
|
||||
// eslint-disable-next-line max-len
|
||||
reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi;
|
||||
|
||||
if (this.keyError()) {
|
||||
return;
|
||||
do {
|
||||
match = reg.exec(keyTrimmed);
|
||||
if (match && 0 < count) {
|
||||
if (match[0] && match[1] && match[2] && match[1] === match[2]) {
|
||||
PgpUserStore.importKey(this.key(), GnuPG, backup);
|
||||
}
|
||||
--count;
|
||||
done = false;
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
onShow(key) {
|
||||
|
|
|
|||
50
dev/View/Popup/SMimeImport.js
Normal file
50
dev/View/Popup/SMimeImport.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { addObservablesTo } from 'External/ko';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export class SMimeImportPopupView extends AbstractViewPopup {
|
||||
constructor() {
|
||||
super('SMimeImport');
|
||||
|
||||
addObservablesTo(this, {
|
||||
pem: '',
|
||||
pemError: false,
|
||||
pemErrorMessage: '',
|
||||
pemValid: false
|
||||
});
|
||||
|
||||
this.pem.subscribe(value => {
|
||||
this.pemError(false);
|
||||
this.pemErrorMessage('');
|
||||
this.pemValid(value && value.includes('-----BEGIN CERTIFICATE-----'));
|
||||
});
|
||||
}
|
||||
|
||||
submitForm() {
|
||||
if (this.pemValid()) {
|
||||
Remote.request('SMimeImportCertificate',
|
||||
(iError, oData) => {
|
||||
if (iError) {
|
||||
this.pemError(true);
|
||||
this.pemErrorMessage(getNotification(iError, oData?.ErrorMessage));
|
||||
// oData?.ErrorMessageAdditional;
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
{pem:this.pem()}
|
||||
);
|
||||
} else {
|
||||
this.pemError(true);
|
||||
}
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.pem('');
|
||||
this.pemError(false);
|
||||
this.pemErrorMessage('');
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
iError = Notifications.AuthError;
|
||||
}
|
||||
this.submitError(getNotification(iError, oData?.ErrorMessage,
|
||||
Notifications.UnknownNotification));
|
||||
Notifications.UnknownError));
|
||||
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
|
||||
} else {
|
||||
rl.setData(oData.Result);
|
||||
|
|
@ -138,25 +138,20 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
onBuild(dom) {
|
||||
super.onBuild(dom);
|
||||
|
||||
const signMe = (SettingsGet('signMe') || '').toLowerCase();
|
||||
|
||||
let signMe = (SettingsGet('signMe') || '').toLowerCase();
|
||||
switch (signMe) {
|
||||
case 'defaultoff':
|
||||
case 'defaulton':
|
||||
this.signMeType(
|
||||
'defaulton' === signMe ? SignMeOn : SignMeOff
|
||||
);
|
||||
|
||||
case SignMeOff:
|
||||
case SignMeOn:
|
||||
switch (Local.get(ClientSideKeyNameLastSignMe)) {
|
||||
case '-1-':
|
||||
this.signMeType(SignMeOn);
|
||||
signMe = SignMeOn;
|
||||
break;
|
||||
case '-0-':
|
||||
this.signMeType(SignMeOff);
|
||||
signMe = SignMeOff;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
this.signMeType(signMe);
|
||||
break;
|
||||
default:
|
||||
this.signMeType(SignMeUnused);
|
||||
|
|
|
|||
|
|
@ -570,34 +570,39 @@ export class MailMessageView extends AbstractViewRight {
|
|||
}
|
||||
|
||||
pgpDecrypt() {
|
||||
const oMessage = currentMessage();
|
||||
const oMessage = currentMessage(),
|
||||
data = oMessage.pgpEncrypted();
|
||||
delete data.error;
|
||||
PgpUserStore.decrypt(oMessage).then(result => {
|
||||
if (result) {
|
||||
oMessage.pgpDecrypted(true);
|
||||
if (result.data) {
|
||||
MimeToMessage(result.data, oMessage);
|
||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||
if (result.signatures?.length) {
|
||||
oMessage.pgpSigned(true);
|
||||
oMessage.pgpVerified({
|
||||
signatures: result.signatures,
|
||||
success: !!result.signatures.length
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!result) {
|
||||
// TODO: translate
|
||||
alert('Decryption failed, canceled or not possible');
|
||||
throw Error('Decryption failed, canceled or not possible');
|
||||
}
|
||||
oMessage.pgpDecrypted(true);
|
||||
if (result.data) {
|
||||
MimeToMessage(result.data, oMessage);
|
||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||
if (result.signatures?.length) {
|
||||
oMessage.pgpSigned({
|
||||
signatures: result.signatures,
|
||||
success: !!result.signatures.length
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
.catch(e => {
|
||||
data.error = e.message;
|
||||
})
|
||||
.finally(() => {
|
||||
oMessage.pgpEncrypted(data);
|
||||
});
|
||||
}
|
||||
|
||||
pgpVerify(/*self, event*/) {
|
||||
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
||||
PgpUserStore.verify(oMessage).then(result => {
|
||||
if (result) {
|
||||
oMessage.pgpVerified(result);
|
||||
oMessage.pgpSigned(result);
|
||||
} else {
|
||||
alert('Verification failed or no valid public key found');
|
||||
}
|
||||
|
|
@ -624,16 +629,17 @@ export class MailMessageView extends AbstractViewRight {
|
|||
|
||||
async smimeDecrypt() {
|
||||
const message = currentMessage();
|
||||
let pass, data = message.smimeEncrypted(); // { partId: "1" }
|
||||
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
|
||||
identity = IdentityUserStore.find(item => addresses.includes(item.email()));
|
||||
identity = IdentityUserStore.find(item => addresses.includes(item.email())),
|
||||
data = message.smimeEncrypted(); // { partId: "1" }
|
||||
if (data && identity) {
|
||||
data = { ...data }; // clone
|
||||
data.folder = message.folder;
|
||||
data.uid = message.uid;
|
||||
// data.bodyPart = data.bodyPart?.raw;
|
||||
data.certificate = identity.smimeCertificate();
|
||||
data.privateKey = identity.smimeKey();
|
||||
delete data.error;
|
||||
let pass, params = { ...data }; // clone
|
||||
params.folder = message.folder;
|
||||
params.uid = message.uid;
|
||||
// params.bodyPart = params.bodyPart?.raw;
|
||||
params.certificate = identity.smimeCertificate();
|
||||
params.privateKey = identity.smimeKey();
|
||||
if (identity.smimeKeyEncrypted()) {
|
||||
pass = await Passphrases.ask(identity,
|
||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||
|
|
@ -642,36 +648,44 @@ export class MailMessageView extends AbstractViewRight {
|
|||
if (!pass) {
|
||||
return;
|
||||
}
|
||||
data.passphrase = pass?.password;
|
||||
params.passphrase = pass?.password;
|
||||
}
|
||||
Remote.post('SMimeDecryptMessage', null, data).then(response => {
|
||||
if (response?.Result) {
|
||||
Remote.post('SMimeDecryptMessage', null, params).then(response => {
|
||||
if (response?.Result?.data) {
|
||||
message.smimeDecrypted(true);
|
||||
MimeToMessage(response.Result, message);
|
||||
MimeToMessage(response.Result.data, message);
|
||||
message.html() ? message.viewHtml() : message.viewPlain();
|
||||
pass && pass.remember && Passphrases.set(identity, pass.password);
|
||||
if ('signed' in response.Result) {
|
||||
message.smimeSigned(response.Result.signed);
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
data.error = e.message
|
||||
})
|
||||
.finally(() => {
|
||||
message.smimeEncrypted(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
smimeVerify(/*self, event*/) {
|
||||
const message = currentMessage();
|
||||
let data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||
const message = currentMessage(),
|
||||
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||
if (data) {
|
||||
data = { ...data }; // clone
|
||||
data.folder = message.folder;
|
||||
data.uid = message.uid;
|
||||
data.bodyPart = data.bodyPart?.raw;
|
||||
data.sigPart = data.sigPart?.bodyRaw;
|
||||
Remote.post('SMimeVerifyMessage', null, data).then(response => {
|
||||
const params = { ...data }; // clone
|
||||
params.folder = message.folder;
|
||||
params.uid = message.uid;
|
||||
params.bodyPart = data.bodyPart?.raw;
|
||||
params.sigPart = data.sigPart?.bodyRaw;
|
||||
Remote.post('SMimeVerifyMessage', null, params).then(response => {
|
||||
if (response?.Result) {
|
||||
if (response.Result.body) {
|
||||
MimeToMessage(response.Result.body, message);
|
||||
message.html() ? message.viewHtml() : message.viewPlain();
|
||||
response.Result.body = null;
|
||||
}
|
||||
message.smimeVerified(response.Result);
|
||||
data.success = response.Result.success;
|
||||
message.smimeSigned(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const
|
||||
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
|
||||
eId = id => doc.getElementById('rl-'+id),
|
||||
admin = '1' == eId('app')?.dataset?.admin,
|
||||
admin = '1' == eId('app').dataset.admin,
|
||||
|
||||
toggle = div => {
|
||||
eId('loading').hidden = true;
|
||||
|
|
@ -69,7 +69,7 @@ window.rl = {
|
|||
headers: {}
|
||||
}, init);
|
||||
let asJSON = 1,
|
||||
XToken = RL_APP_DATA.System?.token,
|
||||
XToken = (RL_APP_DATA.System || {}).token,
|
||||
object = {};
|
||||
if (postData) {
|
||||
init.method = 'POST';
|
||||
|
|
|
|||
7
dev/prototype.js
vendored
7
dev/prototype.js
vendored
|
|
@ -44,6 +44,13 @@
|
|||
return template.content.firstChild;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://github.com/tc39/proposal-regex-escaping
|
||||
*/
|
||||
if (!RegExp.escape){
|
||||
RegExp.escape = s => String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time the function is executed,
|
||||
* it will delay the execution with the given amount of milliseconds.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue