mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-08 22:18:28 +03:00
* 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>
233 lines
6 KiB
JavaScript
233 lines
6 KiB
JavaScript
import ko from 'ko';
|
|
import { Notifications, UploadErrorCode } from 'Common/Enums';
|
|
import { langLink } from 'Common/Links';
|
|
import { doc, createElement } from 'Common/Globals';
|
|
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
|
import { pInt } from 'Common/Utils';
|
|
import { LanguageStore } from 'Stores/Language';
|
|
|
|
let I18N_DATA = {};
|
|
|
|
const
|
|
init = () => {
|
|
if (rl.I18N) {
|
|
I18N_DATA = rl.I18N;
|
|
rl.I18N = null;
|
|
doc.documentElement.dir = I18N_DATA.LANG_DIR;
|
|
return 1;
|
|
}
|
|
},
|
|
|
|
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
|
|
|
getNotificationMessage = code => {
|
|
let key = getKeyByValue(Notifications, code);
|
|
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
|
},
|
|
|
|
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
|
|
|
export const
|
|
translateTrigger = ko.observable(false),
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
|
|
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
|
|
relativeTime = seconds => {
|
|
let unit = 'second',
|
|
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
|
|
i = 5,
|
|
abs = Math.abs(seconds);
|
|
while (i--) {
|
|
if (t[i][0] <= abs) {
|
|
seconds = Math.round(seconds / t[i][0]);
|
|
unit = t[i][1];
|
|
break;
|
|
}
|
|
}
|
|
if (Intl.RelativeTimeFormat) {
|
|
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
|
|
return rtf.format(seconds, unit);
|
|
}
|
|
// Safari < 14
|
|
abs = Math.abs(seconds);
|
|
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
|
|
plural = rl.relativeTime.plural(abs);
|
|
return (rtf[plural] || rtf).replace('{0}', abs);
|
|
},
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @param {Object=} valueList
|
|
* @param {string=} defaulValue
|
|
* @returns {string}
|
|
*/
|
|
i18n = (key, valueList, defaulValue) => {
|
|
let result = null == defaulValue ? key : defaulValue;
|
|
let path = key.split('/');
|
|
if (I18N_DATA[path[0]] && path[1]) {
|
|
result = I18N_DATA[path[0]][path[1]] || result;
|
|
}
|
|
valueList && forEachObjectEntry(valueList, (key, value) => {
|
|
result = result.replace('%' + key + '%', value);
|
|
});
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* @param {Object} elements
|
|
* @param {boolean=} animate = false
|
|
*/
|
|
i18nToNodes = element =>
|
|
setTimeout(() =>
|
|
element.querySelectorAll('[data-i18n]').forEach(element => {
|
|
const key = element.dataset.i18n;
|
|
if ('[' === key[0]) {
|
|
switch (key.slice(1, 6)) {
|
|
case 'html]':
|
|
element.innerHTML = i18n(key.slice(6));
|
|
break;
|
|
case 'place':
|
|
element.placeholder = i18n(key.slice(13));
|
|
break;
|
|
case 'title':
|
|
element.title = i18n(key.slice(7));
|
|
break;
|
|
// no default
|
|
}
|
|
} else {
|
|
element.textContent = i18n(key);
|
|
}
|
|
})
|
|
, 1),
|
|
|
|
timestampToString = (timeStampInUTC, formatStr) => {
|
|
const now = Date.now(),
|
|
time = 0 < timeStampInUTC ? timeStampInUTC * 1000 : (0 === timeStampInUTC ? now : 0);
|
|
if (31536000000 < time) {
|
|
const m = new Date(time), h = LanguageStore.hourCycle();
|
|
switch (formatStr) {
|
|
case 'FROMNOW':
|
|
return fromNow(m);
|
|
case 'AUTO': {
|
|
// 4 hours
|
|
if (14400000 >= now - time)
|
|
return fromNow(m);
|
|
const date = new Date,
|
|
dt = date.setHours(0,0,0,0);
|
|
return (time > dt - 86400000)
|
|
? i18n(
|
|
time > dt ? 'MESSAGE_LIST/TODAY_AT' : 'MESSAGE_LIST/YESTERDAY_AT',
|
|
{TIME: m.format('LT',0,h)}
|
|
)
|
|
: m.format(
|
|
date.getFullYear() === m.getFullYear()
|
|
? {day: '2-digit', month: 'short', hour: 'numeric', minute: 'numeric'}
|
|
: {dateStyle: 'medium', timeStyle: 'short'}
|
|
, 0, h);
|
|
}
|
|
case 'FULL':
|
|
return m.format('LLL',0,h);
|
|
default:
|
|
return m.format(formatStr,0,h);
|
|
}
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
timeToNode = (element, time) => {
|
|
try {
|
|
if (time) {
|
|
element.dateTime = new Date(time * 1000).toISOString();
|
|
} else {
|
|
time = Date.parse(element.dateTime) / 1000;
|
|
}
|
|
|
|
let key = element.dataset.timeFormat;
|
|
if (key) {
|
|
element.textContent = timestampToString(time, key);
|
|
if ('FULL' !== key && 'FROMNOW' !== key) {
|
|
element.title = timestampToString(time, 'FULL');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// prevent knockout crashes
|
|
console.error(e);
|
|
}
|
|
},
|
|
|
|
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
|
|
|
|
/**
|
|
* @param {Function} startCallback
|
|
* @param {Function=} langCallback = null
|
|
*/
|
|
initOnStartOrLangChange = (startCallback, langCallback) => {
|
|
startCallback?.();
|
|
startCallback && translateTrigger.subscribe(startCallback);
|
|
langCallback && translateTrigger.subscribe(langCallback);
|
|
},
|
|
|
|
/**
|
|
* @param {number} code
|
|
* @param {*=} message = ''
|
|
* @param {*=} defCode = null
|
|
* @returns {string}
|
|
*/
|
|
getNotification = (code, message = '', defCode = 0) => {
|
|
code = pInt(code);
|
|
if (Notifications.ClientViewError === code && message) {
|
|
return message;
|
|
}
|
|
|
|
return getNotificationMessage(code)
|
|
|| getNotificationMessage(pInt(defCode))
|
|
|| '';
|
|
},
|
|
|
|
/**
|
|
* @param {*} code
|
|
* @returns {string}
|
|
*/
|
|
getUploadErrorDescByCode = code => {
|
|
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
|
|
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
|
|
},
|
|
|
|
/**
|
|
* @param {boolean} admin
|
|
* @param {string} language
|
|
*/
|
|
translatorReload = (language, admin) =>
|
|
new Promise((resolve, reject) => {
|
|
const script = createElement('script');
|
|
script.onload = () => {
|
|
// reload the data
|
|
if (init()) {
|
|
i18nToNodes(doc);
|
|
translateTrigger(!translateTrigger());
|
|
// admin || reloadTime();
|
|
}
|
|
script.remove();
|
|
resolve();
|
|
};
|
|
script.onerror = () => reject(Error('Language '+language+' failed'));
|
|
script.src = langLink(language, admin);
|
|
// script.async = true;
|
|
doc.head.append(script);
|
|
}),
|
|
|
|
/**
|
|
*
|
|
* @param {string} language
|
|
* @param {boolean=} isEng = false
|
|
* @returns {string}
|
|
*/
|
|
convertLangName = (language, isEng = false) =>
|
|
i18n(
|
|
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
|
null,
|
|
language
|
|
);
|
|
|
|
init();
|