snappymail/dev/Remote/AbstractFetch.js
S-A-L13 876ed221c3
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>
2024-03-04 17:00:27 +01:00

232 lines
5.9 KiB
JavaScript

import { Notifications } from 'Common/Enums';
import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links';
import { getNotification } from 'Common/Translator';
let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
if (Notifications.InvalidToken === err) {
console.error(getNotification(err));
// alert(getNotification(err));
rl.logoutReload();
} else if ([
Notifications.AuthError,
Notifications.ConnectionError,
Notifications.DomainNotAllowed,
Notifications.AccountNotAllowed,
Notifications.MailServerError,
Notifications.UnknownError
].includes(err)
) {
if (7 < ++iJsonErrorCount) {
rl.logoutReload();
}
}
},
oRequests = {},
abort = (sAction, sReason, bClearOnly) => {
let controller = oRequests[sAction];
oRequests[sAction] = null;
if (controller) {
clearTimeout(controller.timeoutId);
bClearOnly || controller.abort(new DOMException(sAction, sReason || 'AbortError'));
}
},
fetchJSON = (action, sUrl, params, timeout, jsonCallback) => {
if (params) {
if (params instanceof FormData) {
params.set('Action', action);
} else {
params.Action = action;
}
}
// Don't abort, read https://github.com/the-djmaze/snappymail/issues/487
// abort(action, 0, 1);
const controller = new AbortController(),
signal = controller.signal;
oRequests[action] = controller;
// Currently there is no way to combine multiple signals, so AbortSignal.timeout() not possible
controller.timeoutId = timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
return rl.fetchJSON(sUrl, {signal: signal}, params).then(data => {
abort(action, 0, 1);
return jsonCallback ? jsonCallback(data) : Promise.resolve(data);
}).catch(err => {
clearTimeout(controller.timeoutId);
err.aborted = signal.aborted;
return Promise.reject(err);
});
};
class FetchError extends Error
{
constructor(code, message) {
super(message);
this.code = code || Notifications.JsonFalse;
}
}
export class AbstractFetchRemote
{
abort(sAction, sReason) {
abort(sAction, sReason);
return this;
}
/**
* Allows quicker visual responses to the user.
* Can be used to stream lines of json encoded data, but does not work on all servers.
* Apache needs 'flushpackets' like in <Proxy "fcgi://...." flushpackets=on></Proxy>
*/
streamPerLine(fCallback, sGetAdd, postData) {
rl.fetch(getURL(sGetAdd), {}, postData)
.then(response => response.body)
.then(body => {
let buffer = '';
const
// Firefox TextDecoderStream is not defined
// reader = body.pipeThrough(new TextDecoderStream()).getReader();
reader = body.getReader(),
re = /\r\n|\n|\r/gm,
utf8decoder = new TextDecoder(),
processText = ({ done, value }) => {
buffer += value ? utf8decoder.decode(value, {stream: true}) : '';
for (;;) {
let result = re.exec(buffer);
if (!result) {
if (done) {
break;
}
reader.read().then(processText);
return;
}
fCallback(buffer.slice(0, result.index));
buffer = buffer.slice(result.index + 1);
re.lastIndex = 0;
}
// last line didn't end in a newline char
buffer.length && fCallback(buffer);
};
reader.read().then(processText);
})
}
/**
* @param {?Function} fCallback
* @param {string} sAction
* @param {Object=} oParameters
* @param {?number=} iTimeout
* @param {string=} sGetAdd = ''
*/
request(sAction, fCallback, params, iTimeout, sGetAdd) {
params = params || {};
const start = Date.now();
fetchJSON(sAction, getURL(sGetAdd),
sGetAdd ? null : (params || {}),
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
let iError = 0;
if (data) {
/*
if (sAction !== data.Action) {
console.log(sAction + ' !== ' + data.Action);
}
*/
if (data.Result) {
iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notifications.UnknownError
}
}
fCallback && fCallback(
iError,
data,
/**
* Responses like "304 Not Modified" are returned as "200 OK"
* This is an attempt to detect if the request comes from cache.
* But when client has wrong date/time, it will fail.
*/
data?.epoch && data.epoch < Math.floor(start / 1000) - 60
);
}
)
.catch(err => {
console.error({fetchError:err});
fCallback && fCallback(
'TimeoutError' == err.name ? 3 : (err.name == 'AbortError' ? 2 : 1),
err
);
});
}
setTrigger(trigger, value) {
if (trigger) {
value = !!value;
(isArray(trigger) ? trigger : [trigger]).forEach(fTrigger => {
fTrigger?.(value);
});
}
}
get(action, url) {
return fetchJSON(action, url);
}
post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true);
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
data => {
abort(action, 0, 1);
if (!data) {
return Promise.reject(new FetchError(Notifications.JsonParse));
}
/*
let isCached = false, type = '';
if (data?.epoch) {
isCached = data.epoch > microtime() - start;
}
// backward capability
switch (true) {
case 'success' === textStatus && data?.Result && action === data.Action:
type = AbstractFetchRemote.SUCCESS;
break;
case 'abort' === textStatus && (!data || !data.__aborted__):
type = AbstractFetchRemote.ABORT;
break;
default:
type = AbstractFetchRemote.ERROR;
break;
}
*/
this.setTrigger(fTrigger, false);
if (!data.Result || action !== data.Action) {
checkResponseError(data);
return Promise.reject(new FetchError(
data ? data.ErrorCode : 0,
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
));
}
return data;
}
);
}
}
Object.assign(AbstractFetchRemote.prototype, {
SUCCESS : 0,
ERROR : 1,
ABORT : 2
});