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:
S-A-L13 2024-03-04 17:00:27 +01:00 • committed by GitHub
parent 3bd073a38b
commit 876ed221c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
218 changed files with 10266 additions and 4267 deletions

View file

@ -95,7 +95,6 @@ Notifications = {
JsonParse: 952,
// JsonTimeout: 953,
UnknownNotification: 998,
UnknownError: 999,
// Admin

View file

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

View file

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

View file

@ -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
View file

@ -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(

View file

@ -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: []});

View file

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

View file

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

View file

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

View file

@ -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() {

View file

@ -19,7 +19,6 @@ checkResponseError = data => {
Notifications.DomainNotAllowed,
Notifications.AccountNotAllowed,
Notifications.MailServerError,
Notifications.UnknownNotification,
Notifications.UnknownError
].includes(err)
) {

View file

@ -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() {

View file

@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
this.addSettings(['useLocalProxyForExternalImages']);
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
this.weakPassword = rl.app.weakPassword;

View file

@ -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.

View file

@ -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: []});

View file

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

View 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
});
*/
};

View file

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

View file

@ -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
});
*/
};

View file

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

View file

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

View file

@ -9,7 +9,7 @@
color: red;
content: attr(data-rainloopErrorTip);
font-size: 90%;
left: 100%;
left: 0;
opacity: 1;
padding: 0.25em;
position : absolute;

View file

@ -123,4 +123,8 @@ html[dir="rtl"] {
border-right: 6px solid #eee;
}
}
[data-rainloopErrorTip]::before {
left: 100%;
}
}

View file

@ -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
}
}
}

View file

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

View file

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

View 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('');
}
}

View file

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

View file

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

View file

@ -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
View file

@ -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.