Merge remote-tracking branch 'origin/master' into addressbook

This commit is contained in:
djmaze 2022-09-02 15:15:01 +02:00
commit 1a5b8819fa
204 changed files with 2050 additions and 2773 deletions

0
.docker/dev/nginx/ssl.sh Normal file → Executable file
View file

View file

@ -304,5 +304,5 @@ dev_email = ""
dev_password = "" dev_password = ""
[version] [version]
current = "2.17.0" current = "2.17.4"
saved = "Fri, 04 Mar 2022 08:55:26 +0000" saved = "Fri, 04 Mar 2022 08:55:26 +0000"

View file

@ -61,8 +61,6 @@ rl-watch-js:
rl-build: rl-build:
@docker compose run --no-deps --rm node gulp all @docker compose run --no-deps --rm node gulp all
rl-build-pro:
@docker compose run --no-deps --rm node gulp all --pro
yarn-install: yarn-install:
@docker compose run --no-deps --rm node yarn install @docker compose run --no-deps --rm node yarn install

View file

@ -35,7 +35,7 @@ And don't forget to read the [RainLoop documentation](https://www.rainloop.net/d
http://www.gnu.org/licenses/agpl-3.0.html http://www.gnu.org/licenses/agpl-3.0.html
Copyright (c) 2020 - 2022 SnappyMail Copyright (c) 2020 - 2022 SnappyMail
Copyright (c) 2013 - 2021 RainLoop Copyright (c) 2013 - 2022 RainLoop
## Modifications ## Modifications
@ -93,11 +93,11 @@ This fork of RainLoop has the following changes:
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy. This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
Supported are: Supported are:
* Chrome 69+ * Chrome 80+
* Edge 79+ * Edge 80+
* Firefox 78+ * Firefox 78+
* Opera 56+ * Opera 67+
* Safari 12+ * Safari 13.1+
### Removal of old JavaScript ### Removal of old JavaScript
@ -136,30 +136,30 @@ The result is faster and smaller download code (good for mobile networks).
* Removed ProgressJS * Removed ProgressJS
RainLoop 1.15 vs SnappyMail RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.158.025 | 80.521 | |admin.js |2.170.153 | 81.750 |
|app.js |4.215.733 | 404.700 | |app.js |4.207.787 | 404.253 |
|boot.js | 672.433 | 2.050 | |boot.js | 868.735 | 2.050 |
|libs.js | 647.679 | 197.160 | |libs.js | 658.812 | 197.270 |
|sieve.js | 0 | 86.362 | |sieve.js | 0 | 86.196 |
|polyfills.js | 325.908 | 0 | |polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 | |serviceworker.js | 0 | 285 |
|TOTAL |8.019.778 | 771.078 | |TOTAL |8.240.095 | 771.804 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: | |--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 255.514 | 40.289 | 73.899 | 13.383 | 60.674 | 11.971 | |admin.min.js | 256.831 | 40.814 | 73.606 | 13.591 | 60.877 | 12.166 |
|app.min.js | 516.000 | 191.607 |140.430 | 62.036 |110.657 | 53.258 | |app.min.js | 515.367 | 190.898 |139.456 | 61.959 |110.485 | 53.232 |
|boot.min.js | 66.456 | 1.252 | 22.553 | 778 | 20.043 | 628 | |boot.min.js | 84.659 | 1.252 | 26.998 | 778 | 23.643 | 628 |
|libs.min.js | 574.626 | 95.110 |177.280 | 35.149 |151.855 | 31.461 | |libs.min.js | 584.772 | 95.026 |180.901 | 35.140 |155.182 | 31.455 |
|sieve.min.js | 0 | 42.091 | 0 | 10.543 | 0 | 9.511 | |sieve.min.js | 0 | 41.963 | 0 | 10.494 | 0 | 9.460 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | |polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.189.690 | 287.969 |351.061 | 97.963 |292.627 | 85.347 | |TOTAL user |1.217.635 | 287.176 |358.761 | 97.877 |299.485 | 85.315 |
|TOTAL user+sieve |1.189.690 | 330.060 |351.061 |108.506 |292.627 | 94.858 | |TOTAL user+sieve |1.217.635 | 329.139 |358.761 |108.371 |299.485 | 94.775 |
|TOTAL admin | 929.204 | 136.651 |285.047 | 49.310 |242.644 | 44.060 | |TOTAL admin | 959.099 | 137.092 |292.911 | 49.509 |249.877 | 44.249 |
For a user its around 70% smaller and faster than traditional RainLoop. For a user its around 70% smaller and faster than traditional RainLoop.
@ -187,12 +187,12 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.334 | 82.180 | 46.959 | 16.944 | 14.591 | |app.css | 340.331 | 81.838 | 46.946 | 16.907 | 14.541 |
|app.min.css | 274.791 | 66.070 | 39.618 | 15.014 | 13.212 | |app.min.css | 274.947 | 65.769 | 39.647 | 14.962 | 13.163 |
|boot.css | | 1.326 | | 664 | 545 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 29.828 | | 6.777 | 5.875 | |admin.css | | 29.721 | | 6.762 | 5.867 |
|admin.min.css | | 23.991 | | 6.156 | 5.419 | |admin.min.css | | 23.880 | | 6.124 | 5.382 |
### PGP ### PGP
RainLoop uses the old OpenPGP.js v2 RainLoop uses the old OpenPGP.js v2

View file

@ -2,8 +2,15 @@
define('ROOT_DIR', dirname(__DIR__)); define('ROOT_DIR', dirname(__DIR__));
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins'); define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
$destPath = __DIR__ . 'build/dist/releases/plugins/';
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true); is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(PLUGINS_DEST_DIR, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
}
$manifest = []; $manifest = [];
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php'; require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
$keys = [ $keys = [
@ -38,8 +45,6 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
$manifest_item['type'] = 'plugin'; $manifest_item['type'] = 'plugin';
$manifest_item['id'] = $name; $manifest_item['id'] = $name;
$manifest_item['file'] = "plugins/{$name}-{$version}.tgz"; $manifest_item['file'] = "plugins/{$name}-{$version}.tgz";
ksort($manifest_item);
$manifest[$name] = $manifest_item;
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar"; $tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz"; $tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
@unlink($tgz_destination); @unlink($tgz_destination);
@ -58,6 +63,13 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
unlink($phar_destination); unlink($phar_destination);
rename("{$phar_destination}.gz", $phar_destination); rename("{$phar_destination}.gz", $phar_destination);
} }
if (isset($options['sign'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));
}
ksort($manifest_item);
$manifest[$name] = $manifest_item;
} else { } else {
echo "- {$name} {$version}\n"; echo "- {$name} {$version}\n";
} }

View file

@ -24,7 +24,6 @@ import {
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { import {
MessageFlagsCache,
getFolderInboxName, getFolderInboxName,
getFolderFromCacheList getFolderFromCacheList
} from 'Common/Cache'; } from 'Common/Cache';
@ -60,11 +59,11 @@ import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { AskPopupView } from 'View/Popup/Ask'; import { AskPopupView } from 'View/Popup/Ask';
import { import {
folderInformation,
folderInformationMultiply, folderInformationMultiply,
refreshFoldersInterval, refreshFoldersInterval,
messagesMoveHelper, messagesMoveHelper,
messagesDeleteHelper, messagesDeleteHelper
fetchFolderInformation
} from 'Common/Folders'; } from 'Common/Folders';
import { loadFolders } from 'Model/FolderCollection'; import { loadFolders } from 'Model/FolderCollection';
@ -216,51 +215,7 @@ export class AppUser extends AbstractApp {
* @param {Array=} list = [] * @param {Array=} list = []
*/ */
folderInformation(folder, list) { folderInformation(folder, list) {
if (folder && folder.trim()) { folderInformation(folder, list);
fetchFolderInformation(
(iError, data) => {
if (!iError && data.Result) {
const result = data.Result,
folderFromCache = getFolderFromCacheList(result.Folder);
if (folderFromCache) {
const oldHash = folderFromCache.hash,
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
// folderFromCache.revivePropertiesFromJson(result);
folderFromCache.expires = Date.now();
folderFromCache.uidNext = result.UidNext;
folderFromCache.hash = result.Hash;
folderFromCache.totalEmails(result.totalEmails);
folderFromCache.unreadEmails(result.unreadEmails);
if (unreadCountChange) {
MessageFlagsCache.clearFolder(folderFromCache.fullName);
}
if (result.MessagesFlags.length) {
result.MessagesFlags.forEach(message =>
MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
);
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.NewMessages);
if (!oldHash || unreadCountChange || result.Hash !== oldHash) {
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
} else if (getFolderInboxName() === folderFromCache.fullName) {
Remote.messageList(null, {Folder: getFolderInboxName()}, true);
}
}
}
}
},
folder,
list
);
}
} }
logout() { logout() {
@ -309,9 +264,9 @@ export class AppUser extends AbstractApp {
setInterval(() => { setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(), const cF = FolderUserStore.currentFolderFullName(),
iF = getFolderInboxName(); iF = getFolderInboxName();
this.folderInformation(iF); folderInformation(iF);
if (iF !== cF) { if (iF !== cF) {
this.folderInformation(cF); folderInformation(cF);
} }
folderInformationMultiply(); folderInformationMultiply();
}, refreshFoldersInterval); }, refreshFoldersInterval);
@ -323,7 +278,7 @@ export class AppUser extends AbstractApp {
setTimeout(() => { setTimeout(() => {
const cF = FolderUserStore.currentFolderFullName(); const cF = FolderUserStore.currentFolderFullName();
if (getFolderInboxName() !== cF) { if (getFolderInboxName() !== cF) {
this.folderInformation(cF); folderInformation(cF);
} }
FolderUserStore.hasCapability('LIST-STATUS') || folderInformationMultiply(true); FolderUserStore.hasCapability('LIST-STATUS') || folderInformationMultiply(true);
}, 1000); }, 1000);

View file

@ -4,7 +4,7 @@ import { addObservablesTo } from 'External/ko';
let notificator = null, let notificator = null,
player = null, player = null,
canPlay = type => player && !!player.canPlayType(type).replace('no', ''), canPlay = type => !!player?.canPlayType(type).replace('no', ''),
audioCtx = window.AudioContext || window.webkitAudioContext, audioCtx = window.AudioContext || window.webkitAudioContext,
@ -92,7 +92,7 @@ export const SMAudio = new class {
} }
pause() { pause() {
player && player.pause(); player?.pause();
fireEvent('audio.stop'); fireEvent('audio.stop');
} }

View file

@ -37,8 +37,7 @@ export const
* @param {string} folderHash * @param {string} folderHash
* @returns {string} * @returns {string}
*/ */
getFolderFullName = folderHash => getFolderFullName = folderHash => (folderHash && FOLDERS_NAME_CACHE[folderHash]) || '',
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
/** /**
* @param {string} folderHash * @param {string} folderHash
@ -79,9 +78,7 @@ export class MessageFlagsCache
* @returns {bool} * @returns {bool}
*/ */
static hasFlag(folderFullName, uid, flag) { static hasFlag(folderFullName, uid, flag) {
return MESSAGE_FLAGS_CACHE[folderFullName] return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid]?.includes(flag);
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
} }
/** /**
@ -90,7 +87,7 @@ export class MessageFlagsCache
* @returns {?Array} * @returns {?Array}
*/ */
static getFor(folderFullName, uid) { static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]; return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid];
} }
/** /**

View file

@ -76,7 +76,6 @@ export const
ClientSideKeyNameExpandedFolders = 3, ClientSideKeyNameExpandedFolders = 3,
ClientSideKeyNameFolderListSize = 4, ClientSideKeyNameFolderListSize = 4,
ClientSideKeyNameMessageListSize = 5, ClientSideKeyNameMessageListSize = 5,
ClientSideKeyNameLastReplyAction = 6,
ClientSideKeyNameLastSignMe = 7, ClientSideKeyNameLastSignMe = 7,
ClientSideKeyNameMessageHeaderFullInfo = 9, ClientSideKeyNameMessageHeaderFullInfo = 9,
ClientSideKeyNameMessageAttachmentControls = 10; ClientSideKeyNameMessageAttachmentControls = 10;

View file

@ -256,7 +256,7 @@ export const FileInfo = {
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '') .map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
.validUnique(); .validUnique();
return (icons && 1 === icons.length && 'icon-file' !== icons[0]) return (1 === icons?.length && 'icon-file' !== icons[0])
? icons[0] ? icons[0]
: 'icon-attachment'; : 'icon-attachment';
} }

View file

@ -56,7 +56,7 @@ fetchFolderInformation = (fCallback, folder, list = []) => {
Remote.request('FolderInformation', fCallback, { Remote.request('FolderInformation', fCallback, {
Folder: folder, Folder: folder,
FlagsUids: uids, FlagsUids: uids,
UidNext: (folderFromCache && folderFromCache.uidNext) || 0 // Used to check for new messages UidNext: folderFromCache?.uidNext || 0 // Used to check for new messages
}); });
} else if (SettingsUserStore.useThreads()) { } else if (SettingsUserStore.useThreads()) {
MessagelistUserStore.reloadFlagsAndCachedMessage(); MessagelistUserStore.reloadFlagsAndCachedMessage();
@ -129,6 +129,58 @@ folderListOptionsBuilder = (
// Every 5 minutes // Every 5 minutes
refreshFoldersInterval = 300000, refreshFoldersInterval = 300000,
/**
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation = (folder, list) => {
if (folder?.trim()) {
fetchFolderInformation(
(iError, data) => {
if (!iError && data.Result) {
const result = data.Result,
folderFromCache = getFolderFromCacheList(result.Folder);
if (folderFromCache) {
const oldHash = folderFromCache.hash,
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
// folderFromCache.revivePropertiesFromJson(result);
folderFromCache.expires = Date.now();
folderFromCache.uidNext = result.UidNext;
folderFromCache.hash = result.Hash;
folderFromCache.totalEmails(result.totalEmails);
folderFromCache.unreadEmails(result.unreadEmails);
if (unreadCountChange) {
MessageFlagsCache.clearFolder(folderFromCache.fullName);
}
if (result.MessagesFlags.length) {
result.MessagesFlags.forEach(message =>
MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
);
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.NewMessages);
if (!oldHash || unreadCountChange || result.Hash !== oldHash) {
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
} else if (getFolderInboxName() === folderFromCache.fullName) {
Remote.messageList(null, {Folder: getFolderInboxName()}, true);
}
}
}
}
},
folder,
list
);
}
},
/** /**
* @param {boolean=} boot = false * @param {boolean=} boot = false
*/ */
@ -162,7 +214,7 @@ folderInformationMultiply = (boot = false) => {
} else if (unreadCountChange } else if (unreadCountChange
&& folder.fullName === FolderUserStore.currentFolderFullName() && folder.fullName === FolderUserStore.currentFolderFullName()
&& MessagelistUserStore.length) { && MessagelistUserStore.length) {
rl.app.folderInformation(folder.fullName, MessagelistUserStore()); folderInformation(folder.fullName, MessagelistUserStore());
} }
} }
}); });
@ -197,7 +249,7 @@ messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
isSpam = sSpamFolder === toFolderFullName, isSpam = sSpamFolder === toFolderFullName,
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName; isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
Remote.request('MessageMove', Remote.abort('MessageList').request('MessageMove',
moveOrDeleteResponseHelper, moveOrDeleteResponseHelper,
{ {
FromFolder: fromFolderFullName, FromFolder: fromFolderFullName,
@ -205,23 +257,17 @@ messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
Uids: uidsForMove.join(','), Uids: uidsForMove.join(','),
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0, MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : '' Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
}, }
null,
'',
['MessageList']
); );
}, },
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => { messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
Remote.request('MessageDelete', Remote.abort('MessageList').request('MessageDelete',
moveOrDeleteResponseHelper, moveOrDeleteResponseHelper,
{ {
Folder: sFromFolderFullName, Folder: sFromFolderFullName,
Uids: aUidForRemove.join(',') Uids: aUidForRemove.join(',')
}, }
null,
'',
['MessageList']
); );
}, },

View file

@ -31,7 +31,7 @@ export const
new CustomEvent(name, {detail:detail, cancelable: !!cancelable}) new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
), ),
formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'), formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
addShortcut = (...args) => shortcuts.add(...args), addShortcut = (...args) => shortcuts.add(...args),

View file

@ -1,6 +1,5 @@
import { createElement, SettingsGet } from 'Common/Globals'; import { createElement } from 'Common/Globals';
import { forEachObjectEntry, pInt } from 'Common/Utils'; import { forEachObjectEntry, pInt } from 'Common/Utils';
import { proxy } from 'Common/Links';
const const
tpl = createElement('template'), tpl = createElement('template'),
@ -29,7 +28,7 @@ export const
* @param {string} text * @param {string} text
* @returns {string} * @returns {string}
*/ */
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]), encodeHtml = text => (text?.toString?.() || '' + text).replace(htmlre, m => htmlmap[m]),
/** /**
* Clears the Message Html for viewing * Clears the Message Html for viewing
@ -39,7 +38,6 @@ export const
cleanHtml = (html, oAttachments, removeColors) => { cleanHtml = (html, oAttachments, removeColors) => {
const const
debug = false, // Config()->Get('debug', 'enable', false); debug = false, // Config()->Get('debug', 'enable', false);
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'), detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
result = { result = {
@ -49,7 +47,7 @@ export const
findAttachmentByCid = cid => oAttachments.findByCid(cid), findAttachmentByCid = cid => oAttachments.findByCid(cid),
findLocationByCid = cid => { findLocationByCid = cid => {
const attachment = findAttachmentByCid(cid); const attachment = findAttachmentByCid(cid);
return attachment && attachment.contentLocation ? attachment : 0; return attachment?.contentLocation ? attachment : 0;
}, },
// convert body attributes to CSS // convert body attributes to CSS
@ -100,7 +98,7 @@ export const
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP' 'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
], ],
nonEmptyTags = [ nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG','TABLE' 'A','B','EM','I','SPAN','STRONG'
]; ];
tpl.innerHTML = html tpl.innerHTML = html
@ -138,7 +136,7 @@ export const
} }
// if (['CENTER','FORM'].includes(name)) { // if (['CENTER','FORM'].includes(name)) {
if ('FORM' === name || 'O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) { if ('FORM' === name || 'O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
replaceWithChildren(oElement); ('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return; return;
} }
/* /*
@ -229,52 +227,58 @@ export const
value = getAttribute('src'); value = getAttribute('src');
delAttribute('src'); delAttribute('src');
let attachment; if ('IMG' === name) {
let attachment;
if (detectHiddenImages if (detectHiddenImages
&& 'IMG' === name && (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height'))) || ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width'))) || [
|| [ 'email.microsoftemail.com/open',
'email.microsoftemail.com/open', 'github.com/notifications/beacon/',
'github.com/notifications/beacon/', 'mandrillapp.com/track/open',
'mandrillapp.com/track/open', 'list-manage.com/track/open'
'list-manage.com/track/open' ].filter(uri => value.toLowerCase().includes(uri)).length
].filter(uri => value.toLowerCase().includes(uri)).length )) {
)) { skipStyle = true;
skipStyle = true; setAttribute('style', 'display:none');
setAttribute('style', 'display:none'); setAttribute('data-x-src-hidden', value);
setAttribute('data-x-hidden-src', value);
}
else if ((attachment = findLocationByCid(value)))
{
if (attachment.download) {
oElement.loading = 'lazy';
oElement.src = attachment.linkPreview();
attachment.isLinked(true);
} }
} else if ((attachment = findLocationByCid(value)))
else if ('cid:' === value.slice(0, 4)) {
{ if (attachment.download) {
attachment = findAttachmentByCid(value.slice(4)); oElement.loading = 'lazy';
if (attachment && attachment.download) { oElement.src = attachment.linkPreview();
oElement.src = attachment.linkPreview(); attachment.isLinked(true);
attachment.isInline(true); }
attachment.isLinked(true); }
else if ('cid:' === value.slice(0, 4))
{
value = value.slice(4);
setAttribute('data-x-src-cid', value);
attachment = findAttachmentByCid(value);
if (attachment?.download) {
oElement.src = attachment.linkPreview();
attachment.isInline(true);
attachment.isLinked(true);
}
}
else if (/^(https?:)?\/\//i.test(value))
{
setAttribute('data-x-src', value);
result.hasExternals = true;
}
else if ('data:image/' === value.slice(0, 11))
{
oElement.src = value;
}
else
{
setAttribute('data-x-src-broken', value);
} }
}
else if (/^(https?:)?\/\//i.test(value))
{
setAttribute('data-x-src', useProxy ? proxy(value) : value);
result.hasExternals = true;
}
else if ('data:image/' === value.slice(0, 11))
{
setAttribute('src', value);
} }
else else
{ {
setAttribute('data-x-broken-src', value); setAttribute('data-x-src-broken', value);
} }
} }
@ -316,14 +320,14 @@ export const
let lowerUrl = found.toLowerCase(); let lowerUrl = found.toLowerCase();
if ('cid:' === lowerUrl.slice(0, 4)) { if ('cid:' === lowerUrl.slice(0, 4)) {
const attachment = findAttachmentByCid(found); const attachment = findAttachmentByCid(found);
if (attachment && attachment.linkPreview && name) { if (attachment?.linkPreview && name) {
oStyle[property] = "url('" + attachment.linkPreview() + "')"; oStyle[property] = "url('" + attachment.linkPreview() + "')";
attachment.isInline(true); attachment.isInline(true);
attachment.isLinked(true); attachment.isLinked(true);
} }
} else if (/^(https?:)?\/\//.test(lowerUrl)) { } else if (/^(https?:)?\/\//.test(lowerUrl)) {
result.hasExternals = true; result.hasExternals = true;
urls_remote.push([property, useProxy ? proxy(found) : value]); urls_remote.push([property, found]);
} else if ('data:image/' === lowerUrl.slice(0, 11)) { } else if ('data:image/' === lowerUrl.slice(0, 11)) {
oStyle[property] = value; oStyle[property] = value;
} else { } else {
@ -430,9 +434,8 @@ export const
parent = node, parent = node,
ordered = 'OL' == node.tagName, ordered = 'OL' == node.tagName,
i = 0; i = 0;
while (parent && parent.parentNode && parent.parentNode.closest) { while ((parent = parent?.parentNode?.closest('ol,ul'))) {
parent = parent.parentNode.closest('ol,ul'); prefix = ' ' + prefix;
parent && (prefix = ' ' + prefix);
} }
node.querySelectorAll(':scope > li').forEach(li => { node.querySelectorAll(':scope > li').forEach(li => {
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * ')); li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
@ -563,7 +566,7 @@ export class HtmlEditor {
editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer)); editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
editor.on('mode', () => { editor.on('mode', () => {
this.blurTrigger(); this.blurTrigger();
this.onModeChange && this.onModeChange(!this.isPlain()); this.onModeChange?.(!this.isPlain());
}); });
} }
} }
@ -571,7 +574,7 @@ export class HtmlEditor {
blurTrigger() { blurTrigger() {
if (this.onBlur) { if (this.onBlur) {
clearTimeout(this.blurTimer); clearTimeout(this.blurTimer);
this.blurTimer = setTimeout(() => this.onBlur && this.onBlur(), 200); this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
} }
} }
@ -681,7 +684,7 @@ export class HtmlEditor {
hasFocus() { hasFocus() {
try { try {
return this.editor && !!this.editor.focusManager.hasFocus; return !!this.editor?.focusManager.hasFocus;
} catch (e) { } catch (e) {
return false; return false;
} }

View file

@ -49,10 +49,7 @@ export function runSettingsViewModelHooks(admin) {
* @param {string} name * @param {string} name
* @returns {?} * @returns {?}
*/ */
rl.pluginSettingsGet = (pluginSection, name) => { rl.pluginSettingsGet = (pluginSection, name) =>
let plugins = SettingsGet('Plugins'); SettingsGet('Plugins')?.[pluginSection]?.[name];
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
};
rl.pluginPopupView = AbstractViewPopup; rl.pluginPopupView = AbstractViewPopup;

View file

@ -422,7 +422,7 @@ export class Selector {
lineUid = ''; lineUid = '';
const uid = this.getItemUid(item); const uid = this.getItemUid(item);
if (event && event.shiftKey) { if (event?.shiftKey) {
if (uid && this.sLastUid && uid !== this.sLastUid) { if (uid && this.sLastUid && uid !== this.sLastUid) {
list = this.list(); list = this.list();
checked = item.checked(); checked = item.checked();

View file

@ -142,7 +142,7 @@ export const
* @param {Function=} langCallback = null * @param {Function=} langCallback = null
*/ */
initOnStartOrLangChange = (startCallback, langCallback = null) => { initOnStartOrLangChange = (startCallback, langCallback = null) => {
startCallback && startCallback(); startCallback?.();
startCallback && trigger.subscribe(startCallback); startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback); langCallback && trigger.subscribe(langCallback);
}, },

View file

@ -26,8 +26,7 @@ export const
.trim(), .trim(),
defaultOptionsAfterRender = (domItem, item) => defaultOptionsAfterRender = (domItem, item) =>
domItem && item && undefined !== item.disabled item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string // unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))), b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),

View file

@ -131,7 +131,7 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
* @returns {boolean} * @returns {boolean}
*/ */
mailToHelper = mailToUrl => { mailToHelper = mailToUrl => {
if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) { if ('mailto:' === mailToUrl?.slice(0, 7).toLowerCase()) {
mailToUrl = mailToUrl.slice(7).split('?'); mailToUrl = mailToUrl.slice(7).split('?');
const const
@ -181,7 +181,7 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
target.removeAttribute('style'); target.removeAttribute('style');
source.removeAttribute('style'); source.removeAttribute('style');
} }
source.observer && source.observer.disconnect(); source.observer?.disconnect();
// source.classList.toggle('resizable', mode); // source.classList.toggle('resizable', mode);
if (mode) { if (mode) {
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode), const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode),
@ -248,7 +248,7 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
} }
source.layoutResizer.mode = mode; source.layoutResizer.mode = mode;
source.layoutResizer.key = sClientSideKeyName; source.layoutResizer.key = sClientSideKeyName;
source.observer && source.observer.observe(source, { box: 'border-box' }); source.observer?.observe(source, { box: 'border-box' });
} }
}, },
@ -266,7 +266,7 @@ populateMessageBody = (oMessage, preload) => {
oMessage = preload ? oMessage : null; oMessage = preload ? oMessage : null;
let let
isNew = false, isNew = false,
json = oData && oData.Result, json = oData?.Result,
message = oMessage || MessageUserStore.message(); message = oMessage || MessageUserStore.message();
if ( if (

View file

@ -12,7 +12,7 @@ export class AbstractInput {
this.value = params.value || ''; this.value = params.value || '';
this.label = params.label || ''; this.label = params.label || '';
this.enable = null == params.enable ? true : params.enable; this.enable = null == params.enable ? true : params.enable;
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null; this.trigger = params.trigger?.subscribe ? params.trigger : null;
this.placeholder = params.placeholder || ''; this.placeholder = params.placeholder || '';
this.labeled = null != params.label; this.labeled = null != params.label;

View file

@ -2,7 +2,7 @@ import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email'; import { EmailModel } from 'Model/Email';
const contentType = 'snappymail/emailaddress', const contentType = 'snappymail/emailaddress',
getAddressKey = li => li && li.emailaddress && li.emailaddress.key; getAddressKey = li => li?.emailaddress?.key;
let dragAddress, datalist; let dragAddress, datalist;
@ -19,7 +19,7 @@ export class EmailAddressesComponent {
const self = this, const self = this,
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event // In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set // In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul, validDropzone = () => dragAddress?.li.parentNode !== self.ul,
fnDrag = e => validDropzone() && e.preventDefault(); fnDrag = e => validDropzone() && e.preventDefault();
self.element = element; self.element = element;
@ -116,7 +116,7 @@ export class EmailAddressesComponent {
value, value,
items => { items => {
self._resetDatalist(); self._resetDatalist();
items && items.forEach(item => datalist.append(new Option(item))); items?.forEach(item => datalist.append(new Option(item)));
} }
) )
} }
@ -262,7 +262,7 @@ export class EmailAddressesComponent {
if (v.obj) { if (v.obj) {
let li = createElement('li',{title:v.obj.toLine(),draggable:'true'}), let li = createElement('li',{title:v.obj.toLine(),draggable:'true'}),
el = createElement('span'); el = createElement('span');
el.append(v.obj.toLine(true, false, true)); el.append(v.obj.toLine(true));
li.append(el); li.append(el);
el = createElement('a',{href:'#', class:'ficon'}); el = createElement('a',{href:'#', class:'ficon'});

View file

@ -176,8 +176,8 @@ class SquireUI
quote: { quote: {
html: '"', html: '"',
cmd: () => { cmd: () => {
let parent = this.getParentNodeName('UL,OL'); let parent = squire.getSelectionClosest('UL,OL,BLOCKQUOTE')?.nodeName;
(parent && 'BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel(); ('BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
}, },
hint: 'Blockquote' hint: 'Blockquote'
}, },
@ -198,11 +198,10 @@ class SquireUI
link: { link: {
html: '🔗', html: '🔗',
cmd: () => { cmd: () => {
if ('A' === this.getParentNodeName()) { let node = squire.getSelectionClosest('A'),
squire.removeLink(); url = prompt("Link", node?.href || "https://");
} else { if (url != null) {
let url = prompt("Link","https://"); url.length ? squire.makeLink(url) : (node && squire.removeLink());
url != null && url.length && squire.makeLink(url);
} }
}, },
hint: 'Link' hint: 'Link'
@ -210,12 +209,9 @@ class SquireUI
imageUrl: { imageUrl: {
html: '🖼️', html: '🖼️',
cmd: () => { cmd: () => {
if ('IMG' === this.getParentNodeName()) { let node = squire.getSelectionClosest('IMG'),
// squire.removeLink(); src = prompt("Image", node?.src || "https://");
} else { src.length ? squire.insertImage(src) : (node && squire.detach(node));
let src = prompt("Image","https://");
src != null && src.length && squire.insertImage(src);
}
}, },
hint: 'Image URL' hint: 'Image URL'
}, },
@ -385,15 +381,10 @@ class SquireUI
this.squire.focus(); this.squire.focus();
} }
getParentNodeName(selector) {
let parent = this.squire.getSelectionClosest(selector);
return parent ? parent.nodeName : null;
}
doList(type) { doList(type) {
let parent = this.getParentNodeName('UL,OL'), let parent = this.squire.getSelectionClosest('UL,OL')?.nodeName,
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'}; fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
(parent && parent == type) ? this.squire.removeList() : this.squire[fn[type]](); (parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
} }
testPresenceinSelection(format, validation) { testPresenceinSelection(format, validation) {
@ -412,7 +403,7 @@ class SquireUI
} }
this.mode = mode; // 'wysiwyg' or 'plain' this.mode = mode; // 'wysiwyg' or 'plain'
cl.add('squire-mode-'+mode); cl.add('squire-mode-'+mode);
this.onModeChange && this.onModeChange(); this.onModeChange?.();
setTimeout(()=>this.focus(),1); setTimeout(()=>this.focus(),1);
} }
} }

View file

@ -14,8 +14,8 @@ const rlContentType = 'snappymail/action',
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event // In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
// In Chrome Mobile dataTransfer.types.includes(rlContentType) fails, only text/plain is set // In Chrome Mobile dataTransfer.types.includes(rlContentType) fails, only text/plain is set
dragMessages = () => dragData && 'messages' === dragData.action, dragMessages = () => 'messages' === dragData?.action,
dragSortable = () => dragData && 'sortable' === dragData.action, dragSortable = () => 'sortable' === dragData?.action,
setDragAction = (e, action, effect, data, img) => { setDragAction = (e, action, effect, data, img) => {
dragData = { dragData = {
action: action, action: action,
@ -45,8 +45,8 @@ Object.assign(ko.bindingHandlers, {
let editor = null; let editor = null;
const fValue = fValueAccessor(), const fValue = fValueAccessor(),
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()), fUpdateEditorValue = () => fValue?.__editor?.setHtmlOrPlain(fValue()),
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()), fUpdateKoValue = () => fValue?.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
fOnReady = () => { fOnReady = () => {
fValue.__editor = editor; fValue.__editor = editor;
fUpdateEditorValue(); fUpdateEditorValue();
@ -76,7 +76,7 @@ Object.assign(ko.bindingHandlers, {
focused = fValue.focused; focused = fValue.focused;
element.addresses = new EmailAddressesComponent(element, { element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => focused && focused(!!value), focusCallback: value => focused?.(!!value),
autoCompleteSource: fAllBindings.get('autoCompleteSource'), autoCompleteSource: fAllBindings.get('autoCompleteSource'),
onChange: value => fValue(value) onChange: value => fValue(value)
}); });
@ -137,7 +137,7 @@ Object.assign(ko.bindingHandlers, {
if (dragMessages()) { if (dragMessages()) {
fnStop(e); fnStop(e);
element.classList.add('droppableHover'); element.classList.add('droppableHover');
if (folder && folder.collapsed()) { if (folder?.collapsed()) {
dragTimer.start(() => { dragTimer.start(() => {
folder.collapsed(false); folder.collapsed(false);
setExpandedFolder(folder.fullName, true); setExpandedFolder(folder.fullName, true);
@ -153,7 +153,7 @@ Object.assign(ko.bindingHandlers, {
fnStop(e); fnStop(e);
if (dragMessages() && ['move','copy'].includes(e.dataTransfer.effectAllowed)) { if (dragMessages() && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
let data = dragData.data; let data = dragData.data;
if (folder && data && data.folder && isArray(data.uids)) { if (folder && data?.folder && isArray(data.uids)) {
moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey); moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
} }
} }
@ -220,7 +220,7 @@ Object.assign(ko.bindingHandlers, {
options.list(arr); options.list(arr);
} }
dragData = null; dragData = null;
options.afterMove && options.afterMove(); options.afterMove?.();
} }
} }
}); });

8
dev/External/ko.js vendored
View file

@ -26,14 +26,14 @@ export const
addSubscribablesTo = (target, subscribables) => addSubscribablesTo = (target, subscribables) =>
forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)), forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
dispose = disposable => disposable && isFunction(disposable.dispose) && disposable.dispose(), dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
// With this we don't need delegateRunOnDestroy // With this we don't need delegateRunOnDestroy
koArrayWithDestroy = data => { koArrayWithDestroy = data => {
data = ko.observableArray(data); data = ko.observableArray(data);
data.subscribe(changes => data.subscribe(changes =>
changes.forEach(item => changes.forEach(item =>
'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy() 'deleted' === item.status && null == item.moved && item.value.onDestroy?.()
) )
, data, 'arrayChange'); , data, 'arrayChange');
return data; return data;
@ -172,12 +172,12 @@ ko.extenders.toggleSubscribeProperty = (target, options) => {
const prop = options[1]; const prop = options[1];
if (prop) { if (prop) {
target.subscribe( target.subscribe(
prev => prev && prev[prop] && prev[prop](false), prev => prev?.[prop]?.(false),
options[0], options[0],
'beforeChange' 'beforeChange'
); );
target.subscribe(next => next && next[prop] && next[prop](true), options[0]); target.subscribe(next => next?.[prop]?.(true), options[0]);
} }
return target; return target;

View file

@ -50,8 +50,7 @@ export class AbstractModel {
// forEachObjectEntry(this, (key, value) => { // forEachObjectEntry(this, (key, value) => {
forEachObjectValue(this, value => { forEachObjectValue(this, value => {
/** clear CollectionModel */ /** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value; (ko.isObservableArray(value) ? value() : value)?.onDestroy?.();
arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */ /** destroy ko.observable/ko.computed? */
// dispose(value); // dispose(value);
/** clear object value */ /** clear object value */
@ -76,7 +75,7 @@ export class AbstractModel {
*/ */
static reviveFromJson(json) { static reviveFromJson(json) {
let obj = this.validJson(json) ? new this() : null; let obj = this.validJson(json) ? new this() : null;
obj && obj.revivePropertiesFromJson(json); obj?.revivePropertiesFromJson(json);
return obj; return obj;
} }

View file

@ -128,7 +128,7 @@ export class AbstractViewSettings
addSubscribablesTo(this, { addSubscribablesTo(this, {
[prop]: (value => { [prop]: (value => {
this[trigger](SaveSettingsStep.Animate); this[trigger](SaveSettingsStep.Animate);
valueCb && valueCb(value); valueCb?.(value);
rl.app.Remote.saveSetting(name, value, rl.app.Remote.saveSetting(name, value,
iError => { iError => {
this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult); this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);

View file

@ -10,10 +10,7 @@ let
defaultScreenName = ''; defaultScreenName = '';
const const
autofocus = dom => { autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
const af = dom.querySelector('[autofocus]');
af && af.focus();
},
visiblePopups = new Set, visiblePopups = new Set,
@ -21,7 +18,7 @@ const
* @param {string} screenName * @param {string} screenName
* @returns {?Object} * @returns {?Object}
*/ */
screen = screenName => screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null, screen = screenName => (screenName && SCREENS[screenName]) || null,
/** /**
* @param {Function} ViewModelClass * @param {Function} ViewModelClass
@ -77,10 +74,10 @@ const
if (e.target === vmDom) { if (e.target === vmDom) {
if (vmDom.classList.contains('animate')) { if (vmDom.classList.contains('animate')) {
autofocus(vmDom); autofocus(vmDom);
vm.afterShow && vm.afterShow(); vm.afterShow?.();
} else { } else {
vmDom.close(); vmDom.close();
vm.afterHide && vm.afterHide(); vm.afterHide?.();
} }
} }
}; };
@ -101,7 +98,7 @@ const
}); });
} else { } else {
visiblePopups.delete(vm); visiblePopups.delete(vm);
vm.onHide && vm.onHide(); vm.onHide?.();
vm.keyScope.unset(); vm.keyScope.unset();
vmDom.classList.remove('animate'); // trigger the transitions vmDom.classList.remove('animate'); // trigger the transitions
} }
@ -118,7 +115,7 @@ const
vm vm
); );
vm.onBuild && vm.onBuild(vmDom); vm.onBuild?.(vmDom);
fireEvent('rl-view-model', vm); fireEvent('rl-view-model', vm);
} else { } else {
@ -142,10 +139,10 @@ const
}, },
hideScreen = (screenToHide, destroy) => { hideScreen = (screenToHide, destroy) => {
screenToHide.onHide && screenToHide.onHide(); screenToHide.onHide?.();
forEachViewModel(screenToHide, (vm, dom) => { forEachViewModel(screenToHide, (vm, dom) => {
dom.hidden = true; dom.hidden = true;
vm.onHide && vm.onHide(); vm.onHide?.();
destroy && vm.viewModelDom.remove(); destroy && vm.viewModelDom.remove();
}); });
}, },
@ -155,7 +152,7 @@ const
* @returns {void} * @returns {void}
*/ */
hideScreenPopup = ViewModelClassToHide => { hideScreenPopup = ViewModelClassToHide => {
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) { if (ViewModelClassToHide?.__vm && ViewModelClassToHide?.__dom) {
ViewModelClassToHide.__vm.modalVisible(false); ViewModelClassToHide.__vm.modalVisible(false);
} }
}, },
@ -190,7 +187,7 @@ const
} }
} }
if (vmScreen && vmScreen.__started) { if (vmScreen?.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen; isSameScreen = currentScreen && vmScreen === currentScreen;
if (!vmScreen.__builded) { if (!vmScreen.__builded) {
@ -200,7 +197,7 @@ const
buildViewModel(ViewModelClass, vmScreen) buildViewModel(ViewModelClass, vmScreen)
); );
vmScreen.onBuild && vmScreen.onBuild(); vmScreen.onBuild?.();
} }
setTimeout(() => { setTimeout(() => {
@ -214,19 +211,19 @@ const
// show screen // show screen
if (!isSameScreen) { if (!isSameScreen) {
vmScreen.onShow && vmScreen.onShow(); vmScreen.onShow?.();
forEachViewModel(vmScreen, (vm, dom) => { forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow && vm.beforeShow(); vm.beforeShow?.();
i18nToNodes(dom); i18nToNodes(dom);
dom.hidden = false; dom.hidden = false;
vm.onShow && vm.onShow(); vm.onShow?.();
autofocus(dom); autofocus(dom);
}); });
} }
// -- // --
vmScreen.__cross && vmScreen.__cross.parse(subPart); vmScreen.__cross?.parse(subPart);
}, 1); }, 1);
} }
} }
@ -248,11 +245,11 @@ export const
if (vm) { if (vm) {
params = params || []; params = params || [];
vm.beforeShow && vm.beforeShow(...params); vm.beforeShow?.(...params);
vm.modalVisible(true); vm.modalVisible(true);
vm.onShow && vm.onShow(...params); vm.onShow?.(...params);
} }
}, },
@ -295,7 +292,7 @@ export const
const c = elementById('rl-content'), l = elementById('rl-loading'); const c = elementById('rl-content'), l = elementById('rl-loading');
c && (c.hidden = false); c && (c.hidden = false);
l && l.remove(); l?.remove();
}, },
/** /**

View file

@ -8,12 +8,26 @@ export function ParseMime(text)
{ {
class MimePart class MimePart
{ {
/*
constructor() {
this.id = 0;
this.start = 0;
this.end = 0;
this.parts = [];
this.bodyStart = 0;
this.bodyEnd = 0;
this.boundary = '';
this.bodyText = '';
this.headers = {};
}
*/
header(name) { header(name) {
return this.headers && this.headers[name]; return this.headers[name];
} }
headerValue(name) { headerValue(name) {
return (this.header(name) || {value:null}).value; return this.header(name)?.value;
} }
get raw() { get raw() {
@ -26,12 +40,21 @@ export function ParseMime(text)
get body() { get body() {
let body = this.bodyRaw, let body = this.bodyRaw,
// charset = this.header('content-type')?.params.charset,
encoding = this.headerValue('content-transfer-encoding'); encoding = this.headerValue('content-transfer-encoding');
if ('quoted-printable' == encoding) { if ('quoted-printable' == encoding) {
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut); body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
} else if ('base64' == encoding) { } else if ('base64' == encoding) {
body = atob(body.replace(/\r?\n/g, '')); body = atob(body.replace(/\r?\n/g, ''));
} }
/*
try {
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
return new TextDecoder(charset).decode(Uint8Array.from(body, c => c.charCodeAt(0)));
// return new TextDecoder(charset).decode(new TextEncoder().encode(body));
} catch (e) {
}
*/
return body; return body;
} }
@ -51,21 +74,17 @@ export function ParseMime(text)
forEach(fn) { forEach(fn) {
fn(this); fn(this);
if (this.parts) { this.parts.forEach(part => part.forEach(fn));
this.parts.forEach(part => part.forEach(fn));
}
} }
getByContentType(type) { getByContentType(type) {
if (type == this.headerValue('content-type')) { if (type == this.headerValue('content-type')) {
return this; return this;
} }
if (this.parts) { let i = 0, p = this.parts, part;
let i = 0, p = this.parts, part; for (i; i < p.length; ++i) {
for (i; i < p.length; ++i) { if ((part = p[i].getByContentType(type))) {
if ((part = p[i].getByContentType(type))) { return part;
return part;
}
} }
} }
} }
@ -73,18 +92,19 @@ export function ParseMime(text)
const ParsePart = (mimePart, start_pos = 0, id = '') => const ParsePart = (mimePart, start_pos = 0, id = '') =>
{ {
let part = new MimePart; let part = new MimePart,
headers = {};
if (id) { if (id) {
part.id = id; part.id = id;
part.start = start_pos; part.start = start_pos;
part.end = start_pos + mimePart.length; part.end = start_pos + mimePart.length;
} }
part.parts = [];
// get headers // get headers
let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/); let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/);
if (head) { if (head) {
head = head[0]; head = head[0];
let headers = {};
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => { head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
let match = header.match(/^([^:]+):\s*([^;]+)/), let match = header.match(/^([^:]+):\s*([^;]+)/),
params = {}; params = {};
@ -96,7 +116,6 @@ export function ParseMime(text)
params: params params: params
}; };
}); });
part.headers = headers;
// get body // get body
part.bodyStart = start_pos + head.length; part.bodyStart = start_pos + head.length;
@ -106,7 +125,6 @@ export function ParseMime(text)
let boundary = headers['content-type'].params.boundary; let boundary = headers['content-type'].params.boundary;
if (boundary) { if (boundary) {
part.boundary = boundary; part.boundary = boundary;
part.parts = [];
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'), let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
body = mimePart.slice(head.length), body = mimePart.slice(head.length),
bodies = body.split(regex), bodies = body.split(regex),
@ -125,6 +143,8 @@ export function ParseMime(text)
} }
} }
part.headers = headers;
return part; return part;
}; };

View file

@ -12,7 +12,7 @@ export class AbstractCollectionModel extends Array
} }
onDestroy() { onDestroy() {
this.forEach(item => item.onDestroy && item.onDestroy()); this.forEach(item => item.onDestroy?.());
} }
/** /**

View file

@ -1,4 +1,3 @@
import { encodeHtml } from 'Common/Html';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
@ -284,7 +283,7 @@ export class EmailModel extends AbstractModel {
*/ */
static reviveFromJson(json) { static reviveFromJson(json) {
const email = super.reviveFromJson(json); const email = super.reviveFromJson(json);
email && email.clearDuplicateName(); email?.clearDuplicateName();
return email; return email;
} }
@ -333,45 +332,13 @@ export class EmailModel extends AbstractModel {
/** /**
* @param {boolean} friendlyView = false * @param {boolean} friendlyView = false
* @param {boolean=} wrapWithLink = false
* @param {boolean=} useEncodeHtml = false
* @returns {string} * @returns {string}
*/ */
toLine(friendlyView, wrapWithLink, useEncodeHtml) { toLine(friendlyView) {
let result = '', let result = this.email;
toLink = (to, txt) => '<a href="mailto:' + to + '" target="_blank" tabindex="-1">' + encodeHtml(txt) + '</a>'; return (result && this.name)
if (this.email) { ? (friendlyView ? this.name : '"' + this.name + '" <' + result + '>')
if (friendlyView && this.name) { : result;
result = wrapWithLink
? toLink(
encodeHtml(this.email) + '?to=' + encodeURIComponent('"' + this.name + '" <' + this.email + '>'),
this.name
)
: (useEncodeHtml ? encodeHtml(this.name) : this.name);
} else {
result = this.email;
if (this.name) {
if (wrapWithLink) {
result =
encodeHtml('"' + this.name + '" <')
+ toLink(
encodeHtml(this.email) + '?to=' + encodeURIComponent('"' + this.name + '" <' + this.email + '>'),
result
)
+ encodeHtml('>');
} else {
result = '"' + this.name + '" <' + result + '>';
if (useEncodeHtml) {
result = encodeHtml(result);
}
}
} else if (wrapWithLink) {
result = toLink(encodeHtml(this.email), this.email);
}
}
}
return result;
} }
static splitEmailLine(line) { static splitEmailLine(line) {
@ -384,7 +351,7 @@ export class EmailModel extends AbstractModel {
? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '') ? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '')
: null; : null;
if (address && address.email) { if (address?.email) {
exists = true; exists = true;
} }

View file

@ -30,7 +30,7 @@ export class EmailCollectionModel extends AbstractCollectionModel
toStringClear() { toStringClear() {
const result = []; const result = [];
this.forEach(email => { this.forEach(email => {
if (email && email.email && email.name) { if (email?.email && email?.name) {
result.push(email.email); result.push(email.email);
} }
}); });

View file

@ -100,8 +100,8 @@ export const
.post('Folders', FolderUserStore.foldersLoading) .post('Folders', FolderUserStore.foldersLoading)
.then(data => { .then(data => {
data = FolderCollectionModel.reviveFromJson(data.Result); data = FolderCollectionModel.reviveFromJson(data.Result);
data && data.storeIt(); data?.storeIt();
fCallback && fCallback(true); fCallback?.(true);
// Repeat every 15 minutes? // Repeat every 15 minutes?
// this.foldersTimeout = setTimeout(loadFolders, 900000); // this.foldersTimeout = setTimeout(loadFolders, 900000);
}) })
@ -128,7 +128,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
*/ */
static reviveFromJson(object) { static reviveFromJson(object) {
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders); const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
if (object && object.SystemFolders) { if (object?.SystemFolders) {
forEachObjectEntry(SystemFolders, key => forEachObjectEntry(SystemFolders, key =>
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]] SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
); );

View file

@ -3,10 +3,10 @@ import ko from 'ko';
import { MessagePriority } from 'Common/EnumsUser'; import { MessagePriority } from 'Common/EnumsUser';
import { i18n } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { doc } from 'Common/Globals'; import { doc, SettingsGet } from 'Common/Globals';
import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html'; import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils'; import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw } from 'Common/Links'; import { serverRequestRaw, proxy } from 'Common/Links';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
@ -89,11 +89,15 @@ const
}) })
}, },
/**
* @param {EmailCollectionModel} emails
* @param {Object} unic
* @param {Map} localEmails
*/
replyHelper = (emails, unic, localEmails) => { replyHelper = (emails, unic, localEmails) => {
emails.forEach(email => { emails.forEach(email => {
if (undefined === unic[email.email]) { if (!unic[email.email] && !localEmails.has(email.email)) {
unic[email.email] = true; localEmails.set(email.email, email);
localEmails.push(email);
} }
}); });
}; };
@ -319,7 +323,7 @@ export class MessageModel extends AbstractModel {
*/ */
fromDkimData() { fromDkimData() {
let result = ['none', '']; let result = ['none', ''];
if (1 === arrayLength(this.from) && this.from[0] && this.from[0].dkimStatus) { if (1 === arrayLength(this.from) && this.from[0]?.dkimStatus) {
result = [this.from[0].dkimStatus, this.from[0].dkimValue || '']; result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
} }
@ -375,7 +379,6 @@ export class MessageModel extends AbstractModel {
focused: this.focused(), focused: this.focused(),
important: this.isImportant(), important: this.isImportant(),
withAttachments: !!this.attachments().length, withAttachments: !!this.attachments().length,
emptySubject: !this.subject(),
// hasChildrenMessage: 1 < this.threadsLen(), // hasChildrenMessage: 1 < this.threadsLen(),
hasUnseenSubMessage: this.hasUnseenSubMessage(), hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage() hasFlaggedSubMessage: this.hasFlaggedSubMessage()
@ -396,7 +399,7 @@ export class MessageModel extends AbstractModel {
* @returns {string} * @returns {string}
*/ */
fromAsSingleEmail() { fromAsSingleEmail() {
return isArray(this.from) && this.from[0] ? this.from[0].email : ''; return (isArray(this.from) && this.from[0]?.email) || '';
} }
/** /**
@ -415,50 +418,36 @@ export class MessageModel extends AbstractModel {
/** /**
* @param {Object} excludeEmails * @param {Object} excludeEmails
* @param {boolean=} last = false
* @returns {Array} * @returns {Array}
*/ */
replyEmails(excludeEmails, last) { replyEmails(excludeEmails) {
const result = [], const
unic = undefined === excludeEmails ? {} : excludeEmails; result = new Map(),
unic = excludeEmails || {};
replyHelper(this.replyTo, unic, result); replyHelper(this.replyTo, unic, result);
if (!result.length) { result.size || replyHelper(this.from, unic, result);
replyHelper(this.from, unic, result); return result.size ? [...result.values()] : [this.to[0]];
}
if (!result.length && !last) {
return this.replyEmails({}, true);
}
return result;
} }
/** /**
* @param {Object} excludeEmails * @param {Object} excludeEmails
* @param {boolean=} last = false
* @returns {Array.<Array>} * @returns {Array.<Array>}
*/ */
replyAllEmails(excludeEmails, last) { replyAllEmails(excludeEmails) {
let data = []; const
const toResult = [], toResult = new Map(),
ccResult = [], ccResult = new Map(),
unic = undefined === excludeEmails ? {} : excludeEmails; unic = excludeEmails || {};
replyHelper(this.replyTo, unic, toResult); replyHelper(this.replyTo, unic, toResult);
if (!toResult.length) { if (!toResult.size) {
replyHelper(this.from, unic, toResult); replyHelper(this.from, unic, toResult);
} }
replyHelper(this.to, unic, toResult); replyHelper(this.to, unic, toResult);
replyHelper(this.cc, unic, ccResult); replyHelper(this.cc, unic, ccResult);
if (!toResult.length && !last) { return [[...toResult.values()], [...ccResult.values()]];
data = this.replyAllEmails({}, true);
return [data[0], ccResult];
}
return [toResult, ccResult];
} }
viewHtml() { viewHtml() {
@ -617,16 +606,18 @@ export class MessageModel extends AbstractModel {
this.hasImages(false); this.hasImages(false);
body.rlHasImages = false; body.rlHasImages = false;
let attr = 'data-x-src'; let attr = 'data-x-src',
body.querySelectorAll('[' + attr + ']').forEach(node => { src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
if (node.matches('img')) { body.querySelectorAll('img[' + attr + ']').forEach(node => {
node.loading = 'lazy'; node.loading = 'lazy';
} src = node.getAttribute(attr);
node.src = node.getAttribute(attr); node.src = useProxy ? proxy(src) : src;
}); });
body.querySelectorAll('[data-x-style-url]').forEach(node => { body.querySelectorAll('[data-x-style-url]').forEach(node => {
JSON.parse(node.dataset.xStyleUrl).forEach(data => node.style[data[0]] = "url('" + data[1] + "')"); JSON.parse(node.dataset.xStyleUrl).forEach(data =>
node.style[data[0]] = "url('" + (useProxy ? proxy(data[1]) : data[1]) + "')"
);
}); });
} }
} }

View file

@ -49,7 +49,7 @@ fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
} else { } else {
params.Action = action; params.Action = action;
} }
abort(action); // abort(action);
const controller = new AbortController(), const controller = new AbortController(),
signal = controller.signal; signal = controller.signal;
oRequests[action] = controller; oRequests[action] = controller;
@ -123,23 +123,20 @@ export class AbstractFetchRemote
* @param {Object=} oParameters * @param {Object=} oParameters
* @param {?number=} iTimeout * @param {?number=} iTimeout
* @param {string=} sGetAdd = '' * @param {string=} sGetAdd = ''
* @param {Array=} aAbortActions = []
*/ */
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) { request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
params = params || {}; params = params || {};
const start = Date.now(); const start = Date.now();
if (sAction && abortActions) { abortActions && console.error('abortActions is obsolete');
abortActions.forEach(actionToAbort => abort(actionToAbort));
}
fetchJSON(sAction, pString(sGetAdd), fetchJSON(sAction, pString(sGetAdd),
params, params,
undefined === iTimeout ? 30000 : pInt(iTimeout), undefined === iTimeout ? 30000 : pInt(iTimeout),
data => { data => {
let cached = false; let cached = false;
if (data && data.Time) { if (data?.Time) {
cached = pInt(data.Time) > Date.now() - start; cached = pInt(data.Time) > Date.now() - start;
} }
@ -194,7 +191,7 @@ export class AbstractFetchRemote
if (trigger) { if (trigger) {
value = !!value; value = !!value;
(isArray(trigger) ? trigger : [trigger]).forEach(fTrigger => { (isArray(trigger) ? trigger : [trigger]).forEach(fTrigger => {
fTrigger && fTrigger(value); fTrigger?.(value);
}); });
} }
} }
@ -210,12 +207,12 @@ export class AbstractFetchRemote
} }
/* /*
let isCached = false, type = ''; let isCached = false, type = '';
if (data && data.Time) { if (data?.Time) {
isCached = pInt(data.Time) > microtime() - start; isCached = pInt(data.Time) > microtime() - start;
} }
// backward capability // backward capability
switch (true) { switch (true) {
case 'success' === textStatus && data && data.Result && action === data.Action: case 'success' === textStatus && data?.Result && action === data.Action:
type = AbstractFetchRemote.SUCCESS; type = AbstractFetchRemote.SUCCESS;
break; break;
case 'abort' === textStatus && (!data || !data.__aborted__): case 'abort' === textStatus && (!data || !data.__aborted__):

View file

@ -24,13 +24,13 @@ class RemoteUserFetch extends AbstractFetchRemote {
const const
sFolderFullName = pString(params.Folder), sFolderFullName = pString(params.Folder),
folder = getFolderFromCacheList(sFolderFullName), folder = getFolderFromCacheList(sFolderFullName),
folderHash = (folder && folder.hash) || ''; folderHash = folder?.hash || '';
params = Object.assign({ params = Object.assign({
Offset: 0, Offset: 0,
Limit: SettingsUserStore.messagesPerPage(), Limit: SettingsUserStore.messagesPerPage(),
Search: '', Search: '',
UidNext: (folder && folder.uidNext) || 0, // Used to check for new messages UidNext: folder?.uidNext || 0, // Used to check for new messages
Sort: FolderUserStore.sortMode(), Sort: FolderUserStore.sortMode(),
Hash: folderHash + SettingsGet('AccountHash') Hash: folderHash + SettingsGet('AccountHash')
}, params); }, params);
@ -51,12 +51,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
params = {}; params = {};
} }
bSilent || this.abort('MessageList');
this.request('MessageList', this.request('MessageList',
fCallback, fCallback,
params, params,
60000, // 60 seconds before aborting 60000, // 60 seconds before aborting
sGetAdd, sGetAdd
bSilent ? [] : ['MessageList']
); );
} }
@ -71,7 +71,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
iUid = pInt(iUid); iUid = pInt(iUid);
if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) { if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
this.request('Message', this.abort('Message').request('Message',
fCallback, fCallback,
{}, {},
null, null,
@ -83,8 +83,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
iUid, iUid,
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0, AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
SettingsGet('AccountHash') SettingsGet('AccountHash')
]), ])
['Message']
); );
return true; return true;

View file

@ -53,7 +53,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
settingsScreen settingsScreen
); );
settingsScreen.onBuild && settingsScreen.onBuild(viewModelDom); settingsScreen.onBuild?.(viewModelDom);
} else { } else {
console.log('Cannot find sub settings view model position: SettingsSubScreen'); console.log('Cannot find sub settings view model position: SettingsSubScreen');
} }
@ -67,10 +67,10 @@ export class AbstractSettingsScreen extends AbstractScreen {
this.oCurrentSubScreen = settingsScreen; this.oCurrentSubScreen = settingsScreen;
// show // show
settingsScreen.beforeShow && settingsScreen.beforeShow(); settingsScreen.beforeShow?.();
i18nToNodes(settingsScreen.viewModelDom); i18nToNodes(settingsScreen.viewModelDom);
settingsScreen.viewModelDom.hidden = false; settingsScreen.viewModelDom.hidden = false;
settingsScreen.onShow && settingsScreen.onShow(); settingsScreen.onShow?.();
this.menu.forEach(item => { this.menu.forEach(item => {
item.selected( item.selected(
@ -90,7 +90,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
onHide() { onHide() {
let subScreen = this.oCurrentSubScreen; let subScreen = this.oCurrentSubScreen;
if (subScreen) { if (subScreen) {
subScreen.onHide && subScreen.onHide(); subScreen.onHide?.();
subScreen.viewModelDom.hidden = true; subScreen.viewModelDom.hidden = true;
} }
} }

View file

@ -1,10 +1,10 @@
import { Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser'; import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
import { doc, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals'; import { doc, createElement, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils'; import { pString, pInt } from 'Common/Utils';
import { setLayoutResizer } from 'Common/UtilsUser'; import { setLayoutResizer } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache'; import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { i18n } from 'Common/Translator'; import { i18n, initOnStartOrLangChange } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
@ -22,6 +22,12 @@ import { AbstractScreen } from 'Knoin/AbstractScreen';
export class MailBoxUserScreen extends AbstractScreen { export class MailBoxUserScreen extends AbstractScreen {
constructor() { constructor() {
var styleSheet = createElement('style');
doc.head.appendChild(styleSheet);
initOnStartOrLangChange(() =>
styleSheet.innerText = '.subjectParent:empty::after,.subjectParent .subject:empty::after'
+'{content:"'+i18n('MESSAGE/EMPTY_SUBJECT_TEXT')+'"}'
);
super('mailbox', [ super('mailbox', [
SystemDropDownUserView, SystemDropDownUserView,
MailFolderList, MailFolderList,
@ -89,7 +95,7 @@ export class MailBoxUserScreen extends AbstractScreen {
/* // Disabled in SystemDropDown.html /* // Disabled in SystemDropDown.html
const email = AccountUserStore.email(); const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item => AccountUserStore.accounts.forEach(item =>
item && email === item.email && item.count(e.detail) email === item?.email && item?.count(e.detail)
); );
*/ */
this.updateWindowTitle(); this.updateWindowTitle();

View file

@ -57,7 +57,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
this.coreChecking(true); this.coreChecking(true);
Remote.request('AdminUpdateInfo', (iError, data) => { Remote.request('AdminUpdateInfo', (iError, data) => {
this.coreChecking(false); this.coreChecking(false);
if (!iError && data && data.Result) { if (!iError && data?.Result) {
this.coreReal(true); this.coreReal(true);
this.coreUpdatable(!!data.Result.Updatable); this.coreUpdatable(!!data.Result.Updatable);
this.coreWarning(!!data.Result.Warning); this.coreWarning(!!data.Result.Warning);
@ -79,7 +79,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
this.coreUpdating(false); this.coreUpdating(false);
this.coreVersion(''); this.coreVersion('');
this.coreVersionCompare(-2); this.coreVersionCompare(-2);
if (!iError && data && data.Result) { if (!iError && data?.Result) {
this.coreReal(true); this.coreReal(true);
window.location.reload(); window.location.reload();
} else { } else {

View file

@ -85,7 +85,7 @@ export class AdminSettingsContacts extends AbstractViewSettings {
this.testContactsSuccess(true); this.testContactsSuccess(true);
} else { } else {
this.testContactsError(true); this.testContactsError(true);
if (data && data.Result) { if (data?.Result) {
this.testContactsErrorMessage(data.Result.Message || ''); this.testContactsErrorMessage(data.Result.Message || '');
} else { } else {
this.testContactsErrorMessage(''); this.testContactsErrorMessage('');

View file

@ -68,7 +68,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
this.uploadData = SettingsGet('PhpUploadSizes'); this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadDataDesc = this.uploadDataDesc =
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size) (this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
? [ ? [
this.uploadData.upload_max_filesize this.uploadData.upload_max_filesize
? 'upload_max_filesize = ' + this.uploadData.upload_max_filesize + '; ' ? 'upload_max_filesize = ' + this.uploadData.upload_max_filesize + '; '

View file

@ -24,9 +24,9 @@ export class AdminSettingsPackages extends AbstractViewSettings {
this.packages = PackageAdminStore; this.packages = PackageAdminStore;
addComputablesTo(this, { addComputablesTo(this, {
packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated), packagesCurrent: () => PackageAdminStore().filter(item => item?.installed && !item.canBeUpdated),
packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated), packagesUpdate: () => PackageAdminStore().filter(item => item?.installed && item.canBeUpdated),
packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed), packagesAvailable: () => PackageAdminStore().filter(item => !item?.installed),
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden') visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
}); });
@ -59,7 +59,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
requestHelper(packageToRequest, install) { requestHelper(packageToRequest, install) {
return (iError, data) => { return (iError, data) => {
PackageAdminStore.forEach(item => { PackageAdminStore.forEach(item => {
if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) { if (packageToRequest && item?.loading?.() && packageToRequest.file === item.file) {
packageToRequest.loading(false); packageToRequest.loading(false);
item.loading(false); item.loading(false);
} }
@ -109,11 +109,11 @@ export class AdminSettingsPackages extends AbstractViewSettings {
let disable = plugin.enabled(); let disable = plugin.enabled();
plugin.enabled(!disable); plugin.enabled(!disable);
Remote.request('AdminPluginDisable', Remote.request('AdminPluginDisable',
(iError, data) => { (iError, data) => {
if (iError) { if (iError) {
plugin.enabled(disable); plugin.enabled(disable);
this.packagesError( this.packagesError(
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) (Notification.UnsupportedPluginPackage === iError && data?.ErrorMessage)
? data.ErrorMessage ? data.ErrorMessage
: getNotification(iError) : getNotification(iError)
); );

View file

@ -21,28 +21,46 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
adminPasswordNew: '', adminPasswordNew: '',
adminPasswordNew2: '', adminPasswordNew2: '',
adminPasswordNewError: false, adminPasswordNewError: false,
adminTOTP: SettingsGet('AdminTOTP'), adminTOTP: '',
adminPasswordUpdateError: false, saveError: false,
adminPasswordUpdateSuccess: false, saveSuccess: false,
viewQRCode: '',
capaOpenPGP: SettingsCapa('OpenPGP') capaOpenPGP: SettingsCapa('OpenPGP')
}); });
const reset = () => { const reset = () => {
this.adminPasswordUpdateError(false); this.saveError(false);
this.adminPasswordUpdateSuccess(false); this.saveSuccess(false);
this.adminPasswordNewError(false); this.adminPasswordNewError(false);
}; };
addSubscribablesTo(this, { addSubscribablesTo(this, {
adminPassword: () => { adminPassword: () => {
this.adminPasswordUpdateError(false); this.saveError(false);
this.adminPasswordUpdateSuccess(false); this.saveSuccess(false);
}, },
adminLogin: () => this.adminLoginError(false), adminLogin: () => this.adminLoginError(false),
adminTOTP: value => {
if (/[A-Z2-7]{16,}/.test(value) && 0 == value.length * 5 % 8) {
Remote.request('AdminQRCode', (iError, data) => {
if (!iError) {
console.dir({data:data});
this.viewQRCode(data.Result);
}
}, {
'username': this.adminLogin(),
'TOTP': this.adminTOTP()
});
} else {
this.viewQRCode('');
}
},
adminPasswordNew: reset, adminPasswordNew: reset,
adminPasswordNew2: reset, adminPasswordNew2: reset,
@ -50,12 +68,14 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value) capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
}); });
this.adminTOTP(SettingsGet('AdminTOTP'));
decorateKoCommands(this, { decorateKoCommands(this, {
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword() saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()
}); });
} }
saveNewAdminPasswordCommand() { saveAdminUserCommand() {
if (!this.adminLogin().trim()) { if (!this.adminLogin().trim()) {
this.adminLoginError(true); this.adminLoginError(true);
return false; return false;
@ -66,18 +86,18 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
return false; return false;
} }
this.adminPasswordUpdateError(false); this.saveError(false);
this.adminPasswordUpdateSuccess(false); this.saveSuccess(false);
Remote.request('AdminPasswordUpdate', (iError, data) => { Remote.request('AdminPasswordUpdate', (iError, data) => {
if (iError) { if (iError) {
this.adminPasswordUpdateError(true); this.saveError(true);
} else { } else {
this.adminPassword(''); this.adminPassword('');
this.adminPasswordNew(''); this.adminPasswordNew('');
this.adminPasswordNew2(''); this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true); this.saveSuccess(true);
this.weakPassword(!!data.Result.Weak); this.weakPassword(!!data.Result.Weak);
} }

View file

@ -30,7 +30,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
} }
editAccount(account) { editAccount(account) {
if (account && account.isAdditional()) { if (account?.isAdditional()) {
showScreenPopup(AccountPopupView, [account]); showScreenPopup(AccountPopupView, [account]);
} }
} }
@ -48,7 +48,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
* @returns {void} * @returns {void}
*/ */
deleteAccount(accountToRemove) { deleteAccount(accountToRemove) {
if (accountToRemove && accountToRemove.askDelete()) { if (accountToRemove?.askDelete()) {
this.accountForDeletion(null); this.accountForDeletion(null);
if (accountToRemove) { if (accountToRemove) {
this.accounts.remove((account) => accountToRemove === account); this.accounts.remove((account) => accountToRemove === account);
@ -72,7 +72,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
* @returns {void} * @returns {void}
*/ */
deleteIdentity(identityToRemove) { deleteIdentity(identityToRemove) {
if (identityToRemove && identityToRemove.askDelete()) { if (identityToRemove?.askDelete()) {
this.identityForDeletion(null); this.identityForDeletion(null);
if (identityToRemove) { if (identityToRemove) {

View file

@ -16,13 +16,12 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
this.syncUser = ContactUserStore.syncUser; this.syncUser = ContactUserStore.syncUser;
this.syncPass = ContactUserStore.syncPass; this.syncPass = ContactUserStore.syncPass;
const i18nSyncMode = key => i18n('SETTINGS_CONTACTS/SYNC_' + key);
this.syncModeOptions = koComputable(() => { this.syncModeOptions = koComputable(() => {
translatorTrigger(); translatorTrigger();
return [ return [
{ id: 0, name: i18nSyncMode('NO') }, { id: 0, name: i18n('GLOBAL/NO') },
{ id: 1, name: i18nSyncMode('YES') }, { id: 1, name: i18n('GLOBAL/YES') },
{ id: 2, name: i18nSyncMode('READ') }, { id: 2, name: i18n('SETTINGS_CONTACTS/SYNC_READ') },
]; ];
}); });

View file

@ -50,6 +50,6 @@ export class UserSettingsFilters /*extends AbstractViewSettings*/ {
} }
onShow() { onShow() {
window.Sieve && window.Sieve.updateList(); window.Sieve?.updateList();
} }
} }

View file

@ -1,5 +1,4 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums'; import { Notification } from 'Common/Enums';
import { FolderMetadataKeys } from 'Common/EnumsUser'; import { FolderMetadataKeys } from 'Common/EnumsUser';
@ -48,14 +47,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
this.folderListError = FolderUserStore.folderListError; this.folderListError = FolderUserStore.folderListError;
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed; this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
this.loading = koComputable(() => { this.loading = FolderUserStore.foldersChanging;
const loading = FolderUserStore.foldersLoading(),
creating = FolderUserStore.foldersCreating(),
deleting = FolderUserStore.foldersDeleting(),
renaming = FolderUserStore.foldersRenaming();
return loading || creating || deleting || renaming;
});
this.folderForDeletion = folderForDeletion; this.folderForDeletion = folderForDeletion;

View file

@ -30,11 +30,6 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.language = LanguageStore.language; this.language = LanguageStore.language;
this.languages = LanguageStore.languages; this.languages = LanguageStore.languages;
this.messageReadDelay = SettingsUserStore.messageReadDelay;
this.messagesPerPage = SettingsUserStore.messagesPerPage;
this.editorDefaultType = SettingsUserStore.editorDefaultType;
this.layout = SettingsUserStore.layout;
this.soundNotification = SMAudio.notifications; this.soundNotification = SMAudio.notifications;
this.notificationSound = ko.observable(SettingsGet('NotificationSound')); this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
@ -43,14 +38,14 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.desktopNotification = NotificationUserStore.enabled; this.desktopNotification = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed; this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
this.viewHTML = SettingsUserStore.viewHTML;
this.showImages = SettingsUserStore.showImages;
this.removeColors = SettingsUserStore.removeColors;
this.hideDeleted = SettingsUserStore.hideDeleted;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
this.threadsAllowed = AppUserStore.threadsAllowed; this.threadsAllowed = AppUserStore.threadsAllowed;
this.useThreads = SettingsUserStore.useThreads;
this.replySameFolder = SettingsUserStore.replySameFolder; ['layout', 'messageReadDelay', 'messagesPerPage',
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'showImages', 'removeColors', 'hideDeleted',
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction'
].forEach(name => this[name] = SettingsUserStore[name]);
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings'); this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
this.languageTrigger = ko.observable(SaveSettingsStep.Idle); this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
@ -78,6 +73,14 @@ export class UserSettingsGeneral extends AbstractViewSettings {
]; ];
}, },
msgDefaultActions: () => {
translatorTrigger();
return [
{ id: 1, name: i18n('MESSAGE/BUTTON_REPLY') }, // ComposeType.Reply,
{ id: 2, name: i18n('MESSAGE/BUTTON_REPLY_ALL') } // ComposeType.ReplyAll
];
},
layoutTypes: () => { layoutTypes: () => {
translatorTrigger(); translatorTrigger();
return [ return [
@ -89,11 +92,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
}); });
this.addSetting('EditorDefaultType'); this.addSetting('EditorDefaultType');
this.addSetting('MsgDefaultAction');
this.addSetting('MessageReadDelay'); this.addSetting('MessageReadDelay');
this.addSetting('MessagesPerPage'); this.addSetting('MessagesPerPage');
this.addSetting('Layout', () => MessagelistUserStore([])); this.addSetting('Layout', () => MessagelistUserStore([]));
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'UseCheckboxesInList', 'ReplySameFolder', this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'UseCheckboxesInList', 'ReplySameFolder',
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
'DesktopNotifications', 'SoundNotification']); 'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => { const fReloadLanguageHelper = (saveSettingsStep) => () => {

View file

@ -74,7 +74,7 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
.on('onComplete', (id, result, data) => { .on('onComplete', (id, result, data) => {
themeBackground.loading(false); themeBackground.loading(false);
if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) { if (result && id && data?.Result?.Name && data?.Result?.Hash) {
themeBackground.name(data.Result.Name); themeBackground.name(data.Result.Name);
themeBackground.hash(data.Result.Hash); themeBackground.hash(data.Result.Hash);
} else { } else {
@ -94,11 +94,7 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
} }
} }
if (!errorMsg && data.ErrorMessage) { themeBackground.error(errorMsg || data.ErrorMessage || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
errorMsg = data.ErrorMessage;
}
themeBackground.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
} }
return true; return true;

View file

@ -17,7 +17,7 @@ function filtersToSieveScript(filters)
'' ''
]; ];
const quote = string => '"' + string.trim().replace(/(\\|")/, '\\\\$1') + '"'; const quote = string => '"' + string.trim().replace(/(\\|")/g, '\\$1') + '"';
const StripSpaces = string => string.replace(/\s+/, ' ').trim(); const StripSpaces = string => string.replace(/\s+/, ' ').trim();
// conditionToSieveScript // conditionToSieveScript
@ -294,8 +294,9 @@ export class SieveScriptModel extends AbstractModel
return { return {
name: this.name(), name: this.name(),
active: this.active() ? 1 : 0, active: this.active() ? 1 : 0,
body: this.body(), body: this.body()
filters: this.filters.map(item => item.toJson()) // body: this.allowFilters() ? this.body() : this.filtersToRaw()
// filters: this.filters.map(item => item.toJson())
}; };
} }

View file

@ -13,8 +13,7 @@ import {
const const
// import { defaultOptionsAfterRender } from 'Common/Utils'; // import { defaultOptionsAfterRender } from 'Common/Utils';
defaultOptionsAfterRender = (domItem, item) => defaultOptionsAfterRender = (domItem, item) =>
domItem && item && undefined !== item.disabled item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
// import { folderListOptionsBuilder } from 'Common/Folders'; // import { folderListOptionsBuilder } from 'Common/Folders';
/** /**

View file

@ -22,8 +22,8 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
saveError: false, saveError: false,
errorText: '', errorText: '',
rawActive: false, rawActive: false,
allowToggle: false,
script: null, script: null,
saving: false,
sieveCapabilities: '', sieveCapabilities: '',
availableActions: '', availableActions: '',
@ -31,8 +31,6 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
availableTests: '' availableTests: ''
}); });
this.saving = false;
this.filterForDeletion = ko.observable(null).askDeleteHelper(); this.filterForDeletion = ko.observable(null).askDeleteHelper();
} }
@ -48,7 +46,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
saveScript() { saveScript() {
let self = this, let self = this,
script = self.script(); script = self.script();
if (!self.saving/* && script.hasChanges()*/) { if (!self.saving()/* && script.hasChanges()*/) {
this.errorText(''); this.errorText('');
self.saveError(false); self.saveError(false);
@ -62,21 +60,21 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
} }
try { try {
parseScript(this.script().body()); parseScript(script.body());
} catch (e) { } catch (e) {
this.errorText(e.message); this.errorText(e.message);
return; return;
} }
self.saving = true; self.saving(true);
if (self.allowToggle()) { if (script.allowFilters()) {
script.body(script.filtersToRaw()); script.body(script.filtersToRaw());
} }
Remote.request('FiltersScriptSave', Remote.request('FiltersScriptSave',
(iError, data) => { (iError, data) => {
self.saving = false; self.saving(false);
if (iError) { if (iError) {
self.saveError(true); self.saveError(true);
@ -85,6 +83,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
script.exists() || scripts.push(script); script.exists() || scripts.push(script);
script.exists(true); script.exists(true);
script.hasChanges(false); script.hasChanges(false);
// this.close();
} }
}, },
script.toJson() script.toJson()
@ -110,17 +109,18 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
const clonedFilter = filter.assignTo(); const clonedFilter = filter.assignTo();
FilterPopupView.showModal([ FilterPopupView.showModal([
clonedFilter, clonedFilter,
() => clonedFilter.assignTo(filter), () => {
clonedFilter.assignTo(filter);
const script = this.script();
script.hasChanges(script.body() != script.filtersToRaw());
},
true true
]); ]);
} }
toggleFiltersRaw() { toggleFiltersRaw() {
let script = this.script(), notRaw = !this.rawActive(); const script = this.script(), notRaw = !this.rawActive();
if (notRaw) { notRaw && script.body(script.filtersToRaw());
script.body(script.filtersToRaw());
script.hasChanges(script.hasChanges());
}
this.rawActive(notRaw); this.rawActive(notRaw);
} }
@ -140,10 +140,8 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
this.availableTests([...Object.keys(availableTests())].join(', ')); this.availableTests([...Object.keys(availableTests())].join(', '));
oScript = oScript || new SieveScriptModel(); oScript = oScript || new SieveScriptModel();
let raw = !oScript.allowFilters();
this.script(oScript); this.script(oScript);
this.rawActive(raw); this.rawActive(!oScript.allowFilters());
this.allowToggle(!raw);
this.saveError(false); this.saveError(false);
this.errorText(''); this.errorText('');

View file

@ -22,7 +22,7 @@ PackageAdminStore.fetch = () => {
const loading = {}; const loading = {};
PackageAdminStore.forEach(item => { PackageAdminStore.forEach(item => {
if (item && item.loading()) { if (item?.loading()) {
loading[item.file] = item; loading[item.file] = item;
} }
}); });

View file

@ -21,6 +21,6 @@ AppUserStore.focusedState.subscribe(value => {
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value); ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
} }
let dom = elementById('V-Mail'+name); let dom = elementById('V-Mail'+name);
dom && dom.classList.toggle('focused', name === value); dom?.classList.toggle('focused', name === value);
}); });
}); });

View file

@ -33,12 +33,12 @@ ContactUserStore.sync = fResultFunc => {
line = JSON.parse(line); line = JSON.parse(line);
if ('ContactsSync' === line.Action) { if ('ContactsSync' === line.Action) {
ContactUserStore.syncing(false); ContactUserStore.syncing(false);
fResultFunc && fResultFunc(line.ErrorCode, line); fResultFunc?.(line.ErrorCode, line);
} }
} catch (e) { } catch (e) {
ContactUserStore.syncing(false); ContactUserStore.syncing(false);
console.error(e); console.error(e);
fResultFunc && fResultFunc(Notification.UnknownError); fResultFunc?.(Notification.UnknownError);
} }
}, 'ContactsSync'); }, 'ContactsSync');
} }

View file

@ -64,7 +64,7 @@ export const FolderUserStore = new class {
foldersChanging: () => foldersChanging: () =>
self.foldersLoading() | self.foldersCreating() | self.foldersDeleting() | self.foldersRenaming(), self.foldersLoading() | self.foldersCreating() | self.foldersDeleting() | self.foldersRenaming(),
folderListSystemNames: () => { systemFoldersNames: () => {
const list = [getFolderInboxName()], const list = [getFolderInboxName()],
others = [self.sentFolder(), self.draftsFolder(), self.spamFolder(), self.trashFolder(), self.archiveFolder()]; others = [self.sentFolder(), self.draftsFolder(), self.spamFolder(), self.trashFolder(), self.archiveFolder()];
@ -74,21 +74,15 @@ export const FolderUserStore = new class {
return list; return list;
}, },
folderListSystem: () => systemFolders: () =>
self.folderListSystemNames().map(name => getFolderFromCacheList(name)).filter(v => v) self.systemFoldersNames().map(name => getFolderFromCacheList(name)).filter(v => v)
}); });
const const
subscribeRemoveSystemFolder = observable => { subscribeRemoveSystemFolder = observable => {
observable.subscribe(() => { observable.subscribe(() => getFolderFromCacheList(observable())?.type(FolderType.User), self, 'beforeChange');
const folder = getFolderFromCacheList(observable());
folder && folder.type(FolderType.User);
}, self, 'beforeChange');
}, },
fSetSystemFolderType = type => value => { fSetSystemFolderType = type => value => getFolderFromCacheList(value)?.type(type);
const folder = getFolderFromCacheList(value);
folder && folder.type(type);
};
subscribeRemoveSystemFolder(self.sentFolder); subscribeRemoveSystemFolder(self.sentFolder);
subscribeRemoveSystemFolder(self.draftsFolder); subscribeRemoveSystemFolder(self.draftsFolder);
@ -134,8 +128,7 @@ export const FolderUserStore = new class {
fSearchFunction = (list) => { fSearchFunction = (list) => {
list.forEach(folder => { list.forEach(folder => {
if ( if (
folder && folder?.selectable() &&
folder.selectable() &&
folder.exists && folder.exists &&
timeout > folder.expires && timeout > folder.expires &&
(folder.isSystemFolder() || (folder.isSubscribed() && (folder.checkable() || !bDisplaySpecSetting))) (folder.isSystemFolder() || (folder.isSubscribed() && (folder.checkable() || !bDisplaySpecSetting)))
@ -143,7 +136,7 @@ export const FolderUserStore = new class {
timeouts.push([folder.expires, folder.fullName]); timeouts.push([folder.expires, folder.fullName]);
} }
if (folder && folder.subFolders.length) { if (folder?.subFolders.length) {
fSearchFunction(folder.subFolders()); fSearchFunction(folder.subFolders());
} }
}); });
@ -157,13 +150,14 @@ export const FolderUserStore = new class {
const folder = getFolderFromCacheList(aItem[1]); const folder = getFolderFromCacheList(aItem[1]);
if (folder) { if (folder) {
folder.expires = utc; folder.expires = utc;
// result.indexOf(aItem[1]) ||
result.push(aItem[1]); result.push(aItem[1]);
} }
return limit <= result.length; return limit <= result.length;
}); });
return result.filter((value, index, self) => self.indexOf(value) == index); return result;
} }
saveSystemFolders(folders) { saveSystemFolders(folders) {

View file

@ -38,7 +38,7 @@ export const GnuPGUserStore = new class {
this.privateKeys([]); this.privateKeys([]);
Remote.request('GnupgGetKeys', Remote.request('GnupgGetKeys',
(iError, oData) => { (iError, oData) => {
if (oData && oData.Result) { if (oData?.Result) {
this.keyring = oData.Result; this.keyring = oData.Result;
const initKey = (key, isPrivate) => { const initKey = (key, isPrivate) => {
const aEmails = []; const aEmails = [];
@ -73,7 +73,7 @@ export const GnuPGUserStore = new class {
key.view = () => { key.view = () => {
const fetch = pass => Remote.request('GnupgExportKey', const fetch = pass => Remote.request('GnupgExportKey',
(iError, oData) => { (iError, oData) => {
if (oData && oData.Result) { if (oData?.Result) {
key.armor = oData.Result; key.armor = oData.Result;
showScreenPopup(OpenPgpKeyPopupView, [key]); showScreenPopup(OpenPgpKeyPopupView, [key]);
} }
@ -111,10 +111,10 @@ export const GnuPGUserStore = new class {
importKey(key, callback) { importKey(key, callback) {
Remote.request('GnupgImportKey', Remote.request('GnupgImportKey',
(iError, oData) => { (iError, oData) => {
if (oData && oData.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) { if (oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
this.loadKeyrings(); this.loadKeyrings();
} }
callback && callback(iError, oData); callback?.(iError, oData);
}, { }, {
Key: key Key: key
} }
@ -131,10 +131,10 @@ export const GnuPGUserStore = new class {
storeKeyPair(keyPair, callback) { storeKeyPair(keyPair, callback) {
Remote.request('PgpStoreKeyPair', Remote.request('PgpStoreKeyPair',
(iError, oData) => { (iError, oData) => {
if (oData && oData.Result) { if (oData?.Result) {
// this.gnupgKeyring = oData.Result; // this.gnupgKeyring = oData.Result;
} }
callback && callback(iError, oData); callback?.(iError, oData);
}, keyPair }, keyPair
); );
} }
@ -187,7 +187,7 @@ export const GnuPGUserStore = new class {
} }
if (null !== params.Passphrase) { if (null !== params.Passphrase) {
const result = await Remote.post('GnupgDecrypt', null, params); const result = await Remote.post('GnupgDecrypt', null, params);
if (result && result.Result) { if (result?.Result) {
return result.Result; return result.Result;
} }
} }
@ -208,7 +208,7 @@ export const GnuPGUserStore = new class {
data.SigPart = data.SigPart.body; data.SigPart = data.SigPart.body;
} }
let response = await Remote.post('MessagePgpVerify', null, data); let response = await Remote.post('MessagePgpVerify', null, data);
if (response && response.Result) { if (response?.Result) {
return { return {
fingerprint: response.Result.fingerprint, fingerprint: response.Result.fingerprint,
success: 0 == response.Result.status // GOODSIG success: 0 == response.Result.status // GOODSIG

View file

@ -42,7 +42,7 @@ export const MessageUserStore = new class {
purgeMessageBodyCache() { purgeMessageBodyCache() {
const messagesDom = this.bodiesDom(), const messagesDom = this.bodiesDom(),
children = messagesDom && messagesDom.children; children = messagesDom?.children;
if (children) { if (children) {
while (15 < children.length) { while (15 < children.length) {
children[0].remove(); children[0].remove();

View file

@ -270,8 +270,7 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
let folder, let folder,
alreadyUnread = 0, alreadyUnread = 0,
rootUids = messages.map(oMessage => oMessage && oMessage.uid ? oMessage.uid : null) rootUids = messages.map(oMessage => oMessage?.uid).validUnique(),
.validUnique(),
length = rootUids.length; length = rootUids.length;
if (sFolderFullName && length) { if (sFolderFullName && length) {
@ -333,7 +332,7 @@ MessagelistUserStore.removeMessagesFromList = (
? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid))) ? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
: []; : [];
messages.forEach(item => item && item.isUnseen() && ++unseenCount); messages.forEach(item => item?.isUnseen() && ++unseenCount);
if (fromFolder) { if (fromFolder) {
fromFolder.hash = ''; fromFolder.hash = '';
@ -387,7 +386,7 @@ MessagelistUserStore.removeMessagesFromList = (
if (MessagelistUserStore.threadUid()) { if (MessagelistUserStore.threadUid()) {
if ( if (
messageList.length && messageList.length &&
messageList.find(item => item && item.deleted() && item.uid == MessagelistUserStore.threadUid()) messageList.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
) { ) {
const message = messageList.find(item => item && !item.deleted()); const message = messageList.find(item => item && !item.deleted());
let setHash; let setHash;

View file

@ -6,7 +6,7 @@ import { fireEvent } from 'Common/Globals';
* Might not work due to the new ServiceWorkerRegistration.showNotification * Might not work due to the new ServiceWorkerRegistration.showNotification
*/ */
const HTML5Notification = window.Notification, const HTML5Notification = window.Notification,
HTML5NotificationStatus = () => (HTML5Notification && HTML5Notification.permission) || 'denied', HTML5NotificationStatus = () => HTML5Notification?.permission || 'denied',
NotificationsDenied = () => 'denied' === HTML5NotificationStatus(), NotificationsDenied = () => 'denied' === HTML5NotificationStatus(),
NotificationsGranted = () => 'granted' === HTML5NotificationStatus(), NotificationsGranted = () => 'granted' === HTML5NotificationStatus(),
dispatchMessage = data => { dispatchMessage = data => {
@ -26,7 +26,7 @@ if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistratio
/* Listen for close requests from the ServiceWorker */ /* Listen for close requests from the ServiceWorker */
WorkerNotifications.addEventListener('message', event => { WorkerNotifications.addEventListener('message', event => {
const obj = JSON.parse(event.data); const obj = JSON.parse(event.data);
obj && 'notificationclick' === obj.action && dispatchMessage(obj.data); 'notificationclick' === obj?.action && dispatchMessage(obj.data);
}); });
} else { } else {
WorkerNotifications = null; WorkerNotifications = null;
@ -60,7 +60,7 @@ export const NotificationUserStore = new class {
icon: imageSrc || Links.staticLink('css/images/icon-message-notification.png'), icon: imageSrc || Links.staticLink('css/images/icon-message-notification.png'),
data: messageData data: messageData
}; };
if (messageData && messageData.Uid) { if (messageData?.Uid) {
options.tag = messageData.Uid; options.tag = messageData.Uid;
} }
if (WorkerNotifications) { if (WorkerNotifications) {
@ -83,7 +83,7 @@ export const NotificationUserStore = new class {
.catch(e => console.error(e)); .catch(e => console.error(e));
} else { } else {
const notification = new HTML5Notification(title, options); const notification = new HTML5Notification(title, options);
notification.show && notification.show(); notification.show?.();
notification.onclick = messageData ? () => dispatchMessage(messageData) : null; notification.onclick = messageData ? () => dispatchMessage(messageData) : null;
setTimeout(() => notification.close(), 7000); setTimeout(() => notification.close(), 7000);
} }

View file

@ -18,8 +18,21 @@ const
key.emails.includes(query) || query == key.id || query == key.fingerprint key.emails.includes(query) || query == key.id || query == key.fingerprint
), ),
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') => decryptKey = async (privateKey, btnTxt = 'LABEL_SIGN') => {
await AskPopupView.password('OpenPGP.js key<br>' + privateKey.id + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt), if (privateKey.key.isDecrypted()) {
return privateKey.key;
}
const passphrase = await AskPopupView.password(
'OpenPGP.js key<br>' + privateKey.id + ' ' + privateKey.emails[0],
'OPENPGP/'+btnTxt
);
if (null !== passphrase) {
return await openpgp.decryptKey({
privateKey: privateKey.key,
passphrase
});
}
},
/** /**
* OpenPGP.js v5 removed the localStorage (keyring) * OpenPGP.js v5 removed the localStorage (keyring)
@ -178,19 +191,12 @@ export const OpenPGPUserStore = new class {
} }
} }
if (privateKey) try { if (privateKey) try {
const passphrase = await askPassphrase(privateKey, 'BUTTON_DECRYPT'); const decryptedKey = await decryptKey(privateKey, 'BUTTON_DECRYPT');
if (decryptedKey) {
if (null !== passphrase) { const publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/);
const
publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/),
decryptedKey = await openpgp.decryptKey({
privateKey: privateKey.key,
passphrase
});
return await openpgp.decrypt({ return await openpgp.decrypt({
message, message,
verificationKeys: publicKey && publicKey.key, verificationKeys: publicKey?.key,
// expectSigned: true, // expectSigned: true,
// signature: '', // Detached signature // signature: '', // Detached signature
decryptionKeys: decryptedKey decryptionKeys: decryptedKey
@ -246,18 +252,14 @@ export const OpenPGPUserStore = new class {
* https://docs.openpgpjs.org/global.html#sign * https://docs.openpgpjs.org/global.html#sign
*/ */
async sign(text, privateKey, detached) { async sign(text, privateKey, detached) {
const passphrase = await askPassphrase(privateKey); const signingKey = await decryptKey(privateKey);
if (null !== passphrase) { if (signingKey) {
privateKey = await openpgp.decryptKey({
privateKey: privateKey.key,
passphrase
});
const message = detached const message = detached
? await openpgp.createMessage({ text: text }) ? await openpgp.createMessage({ text: text })
: await openpgp.createCleartextMessage({ text: text }); : await openpgp.createCleartextMessage({ text: text });
return await openpgp.sign({ return await openpgp.sign({
message: message, message: message,
signingKeys: privateKey, signingKeys: signingKey,
detached: !!detached detached: !!detached
}); });
} }
@ -272,14 +274,10 @@ export const OpenPGPUserStore = new class {
recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key); recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
if (count === recipients.length) { if (count === recipients.length) {
if (signPrivateKey) { if (signPrivateKey) {
const passphrase = await askPassphrase(signPrivateKey); signPrivateKey = await decryptKey(signPrivateKey);
if (null === passphrase) { if (!signPrivateKey) {
return; return;
} }
signPrivateKey = await openpgp.decryptKey({
privateKey: signPrivateKey.key,
passphrase
});
} }
return await openpgp.encrypt({ return await openpgp.encrypt({
message: await openpgp.createMessage({ text: text }), message: await openpgp.createMessage({ text: text }),

View file

@ -169,7 +169,7 @@ export const
} }
); );
if (result) { if (result) {
if (result.error && result.error.message) { if (result.error?.message) {
if ('PWD_DIALOG_CANCEL' !== result.error.code) { if ('PWD_DIALOG_CANCEL' !== result.error.code) {
alert(result.error.code + ': ' + result.error.message); alert(result.error.code + ': ' + result.error.message);
} }

View file

@ -1,7 +1,7 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko'; import { koComputable } from 'External/ko';
import { Layout, EditorDefaultType } from 'Common/EnumsUser'; import { Layout, EditorDefaultType, ComposeType } from 'Common/EnumsUser';
import { pInt } from 'Common/Utils'; import { pInt } from 'Common/Utils';
import { addObservablesTo } from 'External/ko'; import { addObservablesTo } from 'External/ko';
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals'; import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
@ -22,6 +22,13 @@ export const SettingsUserStore = new class {
] ]
}); });
self.msgDefaultAction = ko.observable(1).extend({
limitedList: [
ComposeType.Reply,
ComposeType.ReplyAll
]
});
self.messagesPerPage = ko.observable(25).extend({ debounce: 999 }); self.messagesPerPage = ko.observable(25).extend({ debounce: 999 });
self.messageReadDelay = ko.observable(5).extend({ debounce: 999 }); self.messageReadDelay = ko.observable(5).extend({ debounce: 999 });
@ -36,7 +43,12 @@ export const SettingsUserStore = new class {
replySameFolder: 0, replySameFolder: 0,
hideUnsubscribed: 0, hideUnsubscribed: 0,
hideDeleted: 1, hideDeleted: 1,
autoLogout: 0 autoLogout: 0,
requestReadReceipt: 0,
requestDsn: 0,
pgpSign: 0,
pgpEncrypt: 0
}); });
self.init(); self.init();
@ -74,6 +86,7 @@ export const SettingsUserStore = new class {
self.messagesPerPage(pInt(SettingsGet('MessagesPerPage'))); self.messagesPerPage(pInt(SettingsGet('MessagesPerPage')));
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay'))); self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
self.autoLogout(pInt(SettingsGet('AutoLogout'))); self.autoLogout(pInt(SettingsGet('AutoLogout')));
self.msgDefaultAction(SettingsGet('MsgDefaultAction'));
self.viewHTML(SettingsGet('ViewHTML')); self.viewHTML(SettingsGet('ViewHTML'));
self.showImages(SettingsGet('ShowImages')); self.showImages(SettingsGet('ShowImages'));
@ -85,5 +98,10 @@ export const SettingsUserStore = new class {
self.hideUnsubscribed(SettingsGet('HideUnsubscribed')); self.hideUnsubscribed(SettingsGet('HideUnsubscribed'));
self.hideDeleted(SettingsGet('HideDeleted')); self.hideDeleted(SettingsGet('HideDeleted'));
self.requestReadReceipt(SettingsGet('requestReadReceipt'));
self.requestDsn(SettingsGet('requestDsn'));
self.pgpSign(SettingsGet('pgpSign'));
self.pgpEncrypt(SettingsGet('pgpEncrypt'));
} }
}; };

View file

@ -1,10 +1,4 @@
.UserBackground body {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
#rl-content { #rl-content {
display:flex; display:flex;
height: 100%; height: 100%;

View file

@ -5,4 +5,9 @@
padding: 3em 15px; padding: 3em 15px;
text-align: center; text-align: center;
} }
button:focus {
box-shadow: 0 0 1px inset;
}
} }

View file

@ -86,9 +86,8 @@
} }
input, .btn { input, .btn {
background: none !important; background: none;
border: 1px solid @glass-m-color; border: 1px solid @glass-m-color;
color: @glass-color;
} }
.fontastic + input { .fontastic + input {

View file

@ -1,6 +1,6 @@
.UserBackground body { .UserBackground body {
background-size: contain; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
} }

View file

@ -246,6 +246,11 @@ html:not(rl-mobile) {
white-space: nowrap; white-space: nowrap;
} }
.subjectParent:empty {
font-style: italic;
opacity: 0.5;
}
.threadsCountParent { .threadsCountParent {
display: inline; display: inline;
overflow: hidden; overflow: hidden;
@ -266,11 +271,6 @@ html:not(rl-mobile) {
border-color: #666; border-color: #666;
} }
&.emptySubject .subjectParent {
font-style: italic;
opacity: 0.5;
}
.threads-len { .threads-len {
border-radius: 6px; border-radius: 6px;
border: 1px solid #ccc; border: 1px solid #ccc;

View file

@ -113,12 +113,16 @@ html.rl-no-preview-pane {
margin-bottom: 8px; margin-bottom: 8px;
} }
.subject, .emptySubjectText { .subject {
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.subject:empty {
font-style: italic;
opacity: 0.5;
}
.messageButtons { .messageButtons {
margin-top: 5px; margin-top: 5px;
@ -166,12 +170,6 @@ html.rl-no-preview-pane {
} }
} }
.emptySubjectText {
font-style: italic;
font-weight: normal;
color: #999;
}
.hasVirus { .hasVirus {
background: #f00; background: #f00;
color: #fff; color: #fff;

View file

@ -38,7 +38,7 @@ export class AccountPopupView extends AbstractViewPopup {
this.submitRequest(false); this.submitRequest(false);
if (iError) { if (iError) {
this.submitError(getNotification(iError)); this.submitError(getNotification(iError));
this.submitErrorAdditional((data && data.ErrorMessageAdditional) || ''); this.submitErrorAdditional(data?.ErrorMessageAdditional);
} else { } else {
rl.app.accountsAndIdentities(); rl.app.accountsAndIdentities();
this.close(); this.close();
@ -54,7 +54,7 @@ export class AccountPopupView extends AbstractViewPopup {
} }
onShow(account) { onShow(account) {
if (account && account.isAdditional()) { if (account?.isAdditional()) {
this.isNew(false); this.isNew(false);
this.email(account.email); this.email(account.email);
} else { } else {

View file

@ -1,6 +1,7 @@
import { koComputable } from 'External/ko'; import { koComputable } from 'External/ko';
import { i18n, trigger as translatorTrigger } from 'Common/Translator'; import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { pString } from 'Common/Utils';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -16,6 +17,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
to: '', to: '',
subject: '', subject: '',
text: '', text: '',
repliedValue: -1,
selectedDateValue: -1, selectedDateValue: -1,
selectedTreeValue: '', selectedTreeValue: '',
@ -26,9 +28,18 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
this.showMultisearch = koComputable(() => FolderUserStore.hasCapability('MULTISEARCH')); this.showMultisearch = koComputable(() => FolderUserStore.hasCapability('MULTISEARCH'));
this.repliedOptions = koComputable(() => {
translatorTrigger();
return [
{ id: -1, name: '' },
{ id: 1, name: i18n('GLOBAL/YES') },
{ id: 0, name: i18n('GLOBAL/NO') }
];
});
this.selectedDates = koComputable(() => { this.selectedDates = koComputable(() => {
translatorTrigger(); translatorTrigger();
let prefix = 'SEARCH/LABEL_ADV_DATE_'; let prefix = 'SEARCH/LABEL_DATE_';
return [ return [
{ id: -1, name: i18n(prefix + 'ALL') }, { id: -1, name: i18n(prefix + 'ALL') },
{ id: 3, name: i18n(prefix + '3_DAYS') }, { id: 3, name: i18n(prefix + '3_DAYS') },
@ -42,7 +53,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
this.selectedTree = koComputable(() => { this.selectedTree = koComputable(() => {
translatorTrigger(); translatorTrigger();
let prefix = 'SEARCH/LABEL_ADV_SUBFOLDERS_'; let prefix = 'SEARCH/LABEL_SUBFOLDERS_';
return [ return [
{ id: '', name: i18n(prefix + 'NONE') }, { id: '', name: i18n(prefix + 'NONE') },
{ id: 'subtree-one', name: i18n(prefix + 'SUBTREE_ONE') }, { id: 'subtree-one', name: i18n(prefix + 'SUBTREE_ONE') },
@ -60,66 +71,61 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
this.close(); this.close();
} }
parseSearchStringValue(search) {
const parts = (search || '').split(/[\s]+/g);
parts.forEach(part => {
switch (part) {
case 'has:attachment':
this.hasAttachment(true);
break;
case 'is:unseen,flagged':
this.starred(true);
/* falls through */
case 'is:unseen':
this.unseen(true);
break;
// no default
}
});
}
buildSearchString() { buildSearchString() {
const const
self = this,
data = new FormData(), data = new FormData(),
append = (key, value) => value.length && data.append(key, value); append = (key, value) => value.length && data.append(key, value);
append('from', this.from().trim()); append('from', self.from().trim());
append('to', this.to().trim()); append('to', self.to().trim());
append('subject', this.subject().trim()); append('subject', self.subject().trim());
append('text', this.text().trim()); append('text', self.text().trim());
append('in', this.selectedTreeValue()); append('in', self.selectedTreeValue());
if (-1 < this.selectedDateValue()) { if (-1 < self.selectedDateValue()) {
let d = new Date(); let d = new Date();
d.setDate(d.getDate() - this.selectedDateValue()); d.setDate(d.getDate() - self.selectedDateValue());
append('since', d.toISOString().split('T')[0]); append('since', d.toISOString().split('T')[0]);
} }
let result = new URLSearchParams(data).toString(); let result = new URLSearchParams(data).toString();
if (this.hasAttachment()) { if (self.hasAttachment()) {
result += '&attachment'; result += '&attachment';
} }
if (this.unseen()) { if (self.unseen()) {
result += '&unseen'; result += '&unseen';
} }
if (this.starred()) { if (self.starred()) {
result += '&flagged'; result += '&flagged';
} }
if (1 == self.repliedValue()) {
result += '&answered';
}
if (0 == self.repliedValue()) {
result += '&unanswered';
}
return result.replace(/^&+/, ''); return result.replace(/^&+/, '');
} }
onShow(search) { onShow(search) {
this.from(''); const self = this,
this.to(''); params = new URLSearchParams('?'+search);
this.subject(''); self.from(pString(params.get('from')));
this.text(''); self.to(pString(params.get('to')));
self.subject(pString(params.get('subject')));
this.selectedDateValue(-1); self.text(pString(params.get('text')));
this.hasAttachment(false); self.selectedTreeValue(pString(params.get('in')));
this.starred(false); // self.selectedDateValue(params.get('since'));
this.unseen(false); self.selectedDateValue(-1);
self.hasAttachment(params.has('attachment'));
this.parseSearchStringValue(search); self.starred(params.has('flagged'));
self.unseen(params.has('unseen'));
if (params.has('answered')) {
self.repliedValue(1);
} else if (params.has('unanswered')) {
self.repliedValue(0);
}
} }
} }

View file

@ -11,6 +11,8 @@ export class AskPopupView extends AbstractViewPopup {
askDesc: '', askDesc: '',
yesButton: '', yesButton: '',
noButton: '', noButton: '',
username: '',
askUsername: false,
passphrase: '', passphrase: '',
askPass: false askPass: false
}); });
@ -40,15 +42,19 @@ export class AskPopupView extends AbstractViewPopup {
* @param {boolean=} focusOnShow = true * @param {boolean=} focusOnShow = true
* @returns {void} * @returns {void}
*/ */
onShow(sAskDesc, fYesFunc = null, fNoFunc = null, focusOnShow = true, askPass = false, btnText = '') { onShow(sAskDesc, fYesFunc = null, fNoFunc = null, focusOnShow = true, ask = 0, btnText = '') {
this.askDesc(sAskDesc || ''); this.askDesc(sAskDesc || '');
this.askPass(askPass); this.askUsername(ask & 2);
this.askPass(ask & 1);
this.username('');
this.passphrase(''); this.passphrase('');
this.yesButton(i18n(btnText || 'POPUPS_ASK/BUTTON_YES')); this.yesButton(i18n(btnText || 'GLOBAL/YES'));
this.noButton(i18n(askPass ? 'GLOBAL/CANCEL' : 'POPUPS_ASK/BUTTON_NO')); this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
this.fYesAction = fYesFunc; this.fYesAction = fYesFunc;
this.fNoAction = fNoFunc; this.fNoAction = fNoFunc;
this.focusOnShow = focusOnShow ? (askPass ? 'input[type="password"]' : '.buttonYes') : ''; this.focusOnShow = focusOnShow
? (ask ? 'input[type="'+(ask&2?'text':'password')+'"]' : '.buttonYes')
: '';
} }
afterShow() { afterShow() {
@ -63,12 +69,15 @@ export class AskPopupView extends AbstractViewPopup {
onBuild() { onBuild() {
// shortcuts.add('tab', 'shift', 'Ask', () => { // shortcuts.add('tab', 'shift', 'Ask', () => {
shortcuts.add('tab,arrowright,arrowleft', '', 'Ask', () => { shortcuts.add('tab,arrowright,arrowleft', '', 'Ask', () => {
let btn = this.querySelector('.buttonYes'); let yes = this.querySelector('.buttonYes'),
if (btn.matches(':focus')) { no = this.querySelector('.buttonNo');
btn = this.querySelector('.buttonNo'); if (yes.matches(':focus')) {
no.focus();
return false;
} else if (no.matches(':focus')) {
yes.focus();
return false;
} }
btn.focus();
return false;
}); });
} }
} }
@ -80,7 +89,20 @@ AskPopupView.password = function(sAskDesc, btnText) {
() => resolve(this.__vm.passphrase()), () => resolve(this.__vm.passphrase()),
() => resolve(null), () => resolve(null),
true, true,
true, 1,
btnText
]);
});
}
AskPopupView.credentials = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
() => resolve({username:this.__vm.username(), password:this.__vm.passphrase()}),
() => resolve(null),
true,
3,
btnText btnText
]); ]);
}); });

View file

@ -16,11 +16,11 @@ import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
import { koArrayWithDestroy } from 'External/ko'; import { koArrayWithDestroy } from 'External/ko';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { messagesDeleteHelper } from 'Common/Folders'; import { folderInformation, messagesDeleteHelper } from 'Common/Folders';
import { serverRequest } from 'Common/Links'; import { serverRequest } from 'Common/Links';
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator'; import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
import { MessageFlagsCache, setFolderHash } from 'Common/Cache'; import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut } from 'Common/Globals'; import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut, createElement } from 'Common/Globals';
//import { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen'; //import { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
@ -50,9 +50,13 @@ import { ThemeStore } from 'Stores/Theme';
let alreadyFullscreen; let alreadyFullscreen;
*/ */
let oLastMessage;
const const
ScopeCompose = 'Compose', ScopeCompose = 'Compose',
tpl = createElement('template'),
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'), base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'),
email = new EmailModel(), email = new EmailModel(),
@ -77,7 +81,7 @@ const
if (FolderUserStore.currentFolderFullName() === draftsFolder) { if (FolderUserStore.currentFolderFullName() === draftsFolder) {
MessagelistUserStore.reload(true); MessagelistUserStore.reload(true);
} else { } else {
rl.app.folderInformation(draftsFolder); folderInformation(draftsFolder);
} }
} }
}, },
@ -209,8 +213,8 @@ export class ComposePopupView extends AbstractViewPopup {
super('Compose'); super('Compose');
const fEmailOutInHelper = (context, identity, name, isIn) => { const fEmailOutInHelper = (context, identity, name, isIn) => {
if (identity && context && identity[name]() && (isIn ? true : context[name]())) { const identityEmail = context && identity?.[name]();
const identityEmail = identity[name](); if (identityEmail && (isIn ? true : context[name]())) {
let list = context[name]().trim().split(','); let list = context[name]().trim().split(',');
list = list.filter(email => { list = list.filter(email => {
@ -218,15 +222,12 @@ export class ComposePopupView extends AbstractViewPopup {
return email && identityEmail.trim() !== email; return email && identityEmail.trim() !== email;
}); });
if (isIn) { isIn && list.push(identityEmail);
list.push(identityEmail);
}
context[name](list.join(',')); context[name](list.join(','));
} }
}; };
this.oLastMessage = null;
this.oEditor = null; this.oEditor = null;
this.aDraftInfo = null; this.aDraftInfo = null;
this.sInReplyTo = ''; this.sInReplyTo = '';
@ -352,7 +353,7 @@ export class ComposePopupView extends AbstractViewPopup {
}, },
attachmentsInProcess: () => this.attachments.filter(item => item && !item.complete()), attachmentsInProcess: () => this.attachments.filter(item => item && !item.complete()),
attachmentsInError: () => this.attachments.filter(item => item && item.error()), attachmentsInError: () => this.attachments.filter(item => item?.error()),
attachmentsCount: () => this.attachments.length, attachmentsCount: () => this.attachments.length,
attachmentsInErrorCount: () => this.attachmentsInError.length, attachmentsInErrorCount: () => this.attachmentsInError.length,
@ -508,7 +509,7 @@ export class ComposePopupView extends AbstractViewPopup {
alternative.children.push(data); alternative.children.push(data);
data = alternative; data = alternative;
} }
if (sign && !draft && sign[1]) { if (!draft && sign?.[1]) {
if ('openpgp' == sign[0]) { if ('openpgp' == sign[0]) {
// Doesn't sign attachments // Doesn't sign attachments
params.Html = params.Text = ''; params.Html = params.Text = '';
@ -608,7 +609,7 @@ export class ComposePopupView extends AbstractViewPopup {
this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim()); this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
} else { } else {
this.sendError(true); this.sendError(true);
this.sendErrorDesc(getNotification(iError, data && data.ErrorMessage) this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|| getNotification(Notification.CantSendMessage)); || getNotification(Notification.CantSendMessage));
} }
} else { } else {
@ -755,11 +756,11 @@ export class ComposePopupView extends AbstractViewPopup {
// getAutocomplete // getAutocomplete
emailsSource(value, fResponse) { emailsSource(value, fResponse) {
Remote.request('Suggestions', Remote.abort('Suggestions').request('Suggestions',
(iError, data) => { (iError, data) => {
if (!iError && isArray(data.Result)) { if (!iError && isArray(data.Result)) {
fResponse( fResponse(
data.Result.map(item => (item && item[0] ? (new EmailModel(item[0], item[1])).toLine() : null)) data.Result.map(item => (item?.[0] ? (new EmailModel(item[0], item[1])).toLine() : null))
.filter(v => v) .filter(v => v)
); );
} else if (Notification.RequestAborted !== iError) { } else if (Notification.RequestAborted !== iError) {
@ -769,15 +770,12 @@ export class ComposePopupView extends AbstractViewPopup {
{ {
Query: value Query: value
// ,Page: 1 // ,Page: 1
}, }
null,
'',
['Suggestions']
); );
} }
selectIdentity(identity) { selectIdentity(identity) {
identity = identity && identity.item; identity = identity?.item;
if (identity) { if (identity) {
this.currentIdentity(identity); this.currentIdentity(identity);
this.setSignatureFromIdentity(identity); this.setSignatureFromIdentity(identity);
@ -828,7 +826,7 @@ export class ComposePopupView extends AbstractViewPopup {
this.editor(editor => { this.editor(editor => {
let signature = identity.signature() || '', let signature = identity.signature() || '',
isHtml = ':HTML:' === signature.slice(0, 6), isHtml = ':HTML:' === signature.slice(0, 6),
fromLine = this.oLastMessage ? emailArrayToStringLineHelper(this.oLastMessage.from, true) : ''; fromLine = oLastMessage ? emailArrayToStringLineHelper(oLastMessage.from, true) : '';
if (fromLine) { if (fromLine) {
signature = signature.replace(/{{FROM-FULL}}/g, fromLine); signature = signature.replace(/{{FROM-FULL}}/g, fromLine);
if (!fromLine.includes(' ') && 0 < fromLine.indexOf('@')) { if (!fromLine.includes(' ') && 0 < fromLine.indexOf('@')) {
@ -909,24 +907,20 @@ export class ComposePopupView extends AbstractViewPopup {
sText = '', sText = '',
identity = null, identity = null,
aDraftInfo = null, aDraftInfo = null,
message = null; message = 1 === arrayLength(oMessageOrArray)
? oMessageOrArray[0]
: (isArray(oMessageOrArray) ? null : oMessageOrArray);
const excludeEmail = {}, const
// excludeEmail = new Set(),
excludeEmail = {},
mEmail = AccountUserStore.email(), mEmail = AccountUserStore.email(),
lineComposeType = sType || ComposeType.Empty; lineComposeType = sType || ComposeType.Empty;
if (oMessageOrArray) { oLastMessage = message;
message =
1 === arrayLength(oMessageOrArray)
? oMessageOrArray[0]
: isArray(oMessageOrArray)
? null
: oMessageOrArray;
}
this.oLastMessage = message; if (mEmail) {
// excludeEmail.add(mEmail);
if (null !== mEmail) {
excludeEmail[mEmail] = true; excludeEmail[mEmail] = true;
} }
@ -934,6 +928,7 @@ export class ComposePopupView extends AbstractViewPopup {
identity = findIdentityByMessage(lineComposeType, message); identity = findIdentityByMessage(lineComposeType, message);
if (identity) { if (identity) {
// excludeEmail.add(identity.email());
excludeEmail[identity.email()] = true; excludeEmail[identity.email()] = true;
} }
@ -1030,16 +1025,22 @@ export class ComposePopupView extends AbstractViewPopup {
// no default // no default
} }
sText = message.bodyAsHTML();
let encrypted; let encrypted;
// https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = message.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img =>
img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
);
sText = tpl.innerHTML.trim();
switch (lineComposeType) { switch (lineComposeType) {
case ComposeType.Reply: case ComposeType.Reply:
case ComposeType.ReplyAll: case ComposeType.ReplyAll:
sFrom = message.fromToLine(false, true); sFrom = message.fromToLine(false, true);
sText = '<br><br><p>' + i18n('COMPOSE/REPLY_MESSAGE_TITLE', { DATETIME: sDate, EMAIL: sFrom }) sText = '<br><br><p>' + i18n('COMPOSE/REPLY_MESSAGE_TITLE', { DATETIME: sDate, EMAIL: sFrom })
+ ':</p><blockquote>' + ':</p><blockquote>'
+ sText.replace(/<img[^>]+>/g, '').replace(/<a\s[^>]+><\/a>/g, '').trim() + sText.trim()
+ '</blockquote>'; + '</blockquote>';
break; break;
@ -1068,6 +1069,7 @@ export class ComposePopupView extends AbstractViewPopup {
case ComposeType.ForwardAsAttachment: case ComposeType.ForwardAsAttachment:
sText = ''; sText = '';
break; break;
default: default:
encrypted = PgpUserStore.isEncrypted(sText); encrypted = PgpUserStore.isEncrypted(sText);
if (encrypted) { if (encrypted) {
@ -1131,7 +1133,8 @@ export class ComposePopupView extends AbstractViewPopup {
this.setFocusInPopup(); this.setFocusInPopup();
} }
const downloads = this.getAttachmentsDownloadsForUpload(); // item.CID item.isInline item.isLinked
const downloads = this.attachments.filter(item => item && !item.tempName()).map(item => item.id);
if (arrayLength(downloads)) { if (arrayLength(downloads)) {
Remote.request('MessageUploadAttachments', Remote.request('MessageUploadAttachments',
(iError, oData) => { (iError, oData) => {
@ -1148,7 +1151,7 @@ export class ComposePopupView extends AbstractViewPopup {
}); });
} else { } else {
this.attachments.forEach(attachment => { this.attachments.forEach(attachment => {
if (attachment && attachment.fromMessage) { if (attachment?.fromMessage) {
attachment attachment
.waiting(false) .waiting(false)
.uploading(false) .uploading(false)
@ -1175,7 +1178,7 @@ export class ComposePopupView extends AbstractViewPopup {
if (!this.to()) { if (!this.to()) {
this.to.focused(true); this.to.focused(true);
} else if (!this.to.focused()) { } else if (!this.to.focused()) {
this.oEditor && this.oEditor.focus(); this.oEditor?.focus();
} }
}, 100); }, 100);
} }
@ -1261,7 +1264,7 @@ export class ComposePopupView extends AbstractViewPopup {
}) })
.on('onComplete', (id, result, data) => { .on('onComplete', (id, result, data) => {
const attachment = this.getAttachmentById(id), const attachment = this.getAttachmentById(id),
response = (data && data.Result) || {}, response = data?.Result || {},
errorCode = response.ErrorCode, errorCode = response.ErrorCode,
attachmentJson = result && response.Attachment; attachmentJson = result && response.Attachment;
@ -1349,7 +1352,7 @@ export class ComposePopupView extends AbstractViewPopup {
prepareAttachmentsForSendOrSave() { prepareAttachmentsForSendOrSave() {
const result = {}; const result = {};
this.attachments.forEach(item => { this.attachments.forEach(item => {
if (item && item.complete() && item.tempName() && item.enabled()) { if (item?.complete() && item?.tempName() && item?.enabled()) {
result[item.tempName()] = [item.fileName(), item.isInline ? '1' : '0', item.CID, item.contentLocation]; result[item.tempName()] = [item.fileName(), item.isInline ? '1' : '0', item.CID, item.contentLocation];
} }
}); });
@ -1376,7 +1379,7 @@ export class ComposePopupView extends AbstractViewPopup {
oJua || attachment.waiting(false).uploading(true); oJua || attachment.waiting(false).uploading(true);
attachment.cancel = () => { attachment.cancel = () => {
this.attachments.remove(attachment); this.attachments.remove(attachment);
oJua && oJua.cancel(attachment.id); oJua?.cancel(attachment.id);
}; };
this.attachments.push(attachment); this.attachments.push(attachment);
view && this.attachmentsArea(); view && this.attachmentsArea();
@ -1402,35 +1405,22 @@ export class ComposePopupView extends AbstractViewPopup {
if (message) { if (message) {
if (ComposeType.ForwardAsAttachment === type) { if (ComposeType.ForwardAsAttachment === type) {
this.addMessageAsAttachment(message); this.addMessageAsAttachment(message);
} else { } else if ([
ComposeType.Reply, ComposeType.ReplyAll,
ComposeType.Forward, ComposeType.Draft, ComposeType.EditAsNew
].includes(type)) {
message.attachments.forEach(item => { message.attachments.forEach(item => {
let add = false; const attachment = new ComposeAttachmentModel(
switch (type) { item.download,
case ComposeType.Reply: item.fileName,
case ComposeType.ReplyAll: item.estimatedSize,
break; item.isInline(),
item.isLinked(),
case ComposeType.Forward: item.cid,
case ComposeType.Draft: item.contentLocation
case ComposeType.EditAsNew: );
add = true; attachment.fromMessage = true;
break; this.addAttachment(attachment);
// no default
}
if (add) {
const attachment = new ComposeAttachmentModel(
item.download,
item.fileName,
item.estimatedSize,
item.isInline(),
item.isLinked(),
item.cid,
item.contentLocation
);
attachment.fromMessage = true;
this.addAttachment(attachment);
}
}); });
} }
} }
@ -1443,7 +1433,7 @@ export class ComposePopupView extends AbstractViewPopup {
isEmptyForm(includeAttachmentInProgress = true) { isEmptyForm(includeAttachmentInProgress = true) {
const withoutAttachment = includeAttachmentInProgress const withoutAttachment = includeAttachmentInProgress
? !this.attachments.length ? !this.attachments.length
: !this.attachments.some(item => item && item.complete()); : !this.attachments.some(item => item?.complete());
return ( return (
!this.to.length && !this.to.length &&
@ -1463,8 +1453,8 @@ export class ComposePopupView extends AbstractViewPopup {
this.replyTo(''); this.replyTo('');
this.subject(''); this.subject('');
this.requestDsn(false); this.requestDsn(SettingsUserStore.requestDsn());
this.requestReadReceipt(false); this.requestReadReceipt(SettingsUserStore.requestReadReceipt());
this.markAsImportant(false); this.markAsImportant(false);
this.bodyArea(); this.bodyArea();
@ -1485,8 +1475,8 @@ export class ComposePopupView extends AbstractViewPopup {
this.showBcc(false); this.showBcc(false);
this.showReplyTo(false); this.showReplyTo(false);
this.pgpSign(false); this.pgpSign(SettingsUserStore.pgpSign());
this.pgpEncrypt(false); this.pgpEncrypt(SettingsUserStore.pgpEncrypt());
this.attachments([]); this.attachments([]);
@ -1499,20 +1489,11 @@ export class ComposePopupView extends AbstractViewPopup {
this.sending(false); this.sending(false);
this.saving(false); this.saving(false);
this.oEditor && this.oEditor.clear(); this.oEditor?.clear();
this.dropMailvelope(); this.dropMailvelope();
} }
/**
* @returns {Array}
*/
getAttachmentsDownloadsForUpload() {
return this.attachments.filter(item => item && !item.tempName()).map(
item => item.id
);
}
mailvelopeArea() { mailvelopeArea() {
if (!this.mailvelope) { if (!this.mailvelope) {
/** /**

View file

@ -78,7 +78,7 @@ export class ContactsPopupView extends AbstractViewPopup {
contactsPaginator: computedPaginatorHelper(this.contactsPage, pagecount), contactsPaginator: computedPaginatorHelper(this.contactsPage, pagecount),
contactsCheckedOrSelected: () => { contactsCheckedOrSelected: () => {
const checked = ContactUserStore.filter(item => item.checked && item.checked()), const checked = ContactUserStore.filter(item => item.checked?.()),
selected = this.selectorContact(); selected = this.selectorContact();
return selected return selected
@ -128,7 +128,7 @@ export class ContactsPopupView extends AbstractViewPopup {
const data = oItem.getNameAndEmailHelper(), const data = oItem.getNameAndEmailHelper(),
email = data ? new EmailModel(data[0], data[1]) : null; email = data ? new EmailModel(data[0], data[1]) : null;
if (email && email.validate()) { if (email?.validate()) {
return email; return email;
} }
} }
@ -235,7 +235,7 @@ export class ContactsPopupView extends AbstractViewPopup {
if (this.contactsCheckedOrSelected().length) { if (this.contactsCheckedOrSelected().length) {
Remote.request('ContactsDelete', Remote.request('ContactsDelete',
(iError, oData) => { (iError, oData) => {
if (500 < (!iError && oData && oData.Time ? pInt(oData.Time) : 0)) { if (500 < (!iError && oData?.Time) ? pInt(oData.Time) : 0) {
this.reloadContactList(this.bDropPageAfterDelete); this.reloadContactList(this.bDropPageAfterDelete);
} else { } else {
setTimeout(() => this.reloadContactList(this.bDropPageAfterDelete), 500); setTimeout(() => this.reloadContactList(this.bDropPageAfterDelete), 500);
@ -274,7 +274,7 @@ export class ContactsPopupView extends AbstractViewPopup {
} }
ContactUserStore.loading(true); ContactUserStore.loading(true);
Remote.request('Contacts', Remote.abort('Contacts').request('Contacts',
(iError, data) => { (iError, data) => {
let count = 0, let count = 0,
list = []; list = [];
@ -299,10 +299,7 @@ export class ContactsPopupView extends AbstractViewPopup {
Offset: offset, Offset: offset,
Limit: CONTACTS_PER_PAGE, Limit: CONTACTS_PER_PAGE,
Search: this.search() Search: this.search()
}, }
null,
'',
['Contacts']
); );
} }

View file

@ -8,6 +8,8 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { DomainAdminStore } from 'Stores/Admin/Domain'; import { DomainAdminStore } from 'Stores/Admin/Domain';
import { AskPopupView } from 'View/Popup/Ask';
const domainToParams = oDomain => ({ const domainToParams = oDomain => ({
Name: oDomain.name(), Name: oDomain.name(),
@ -179,39 +181,46 @@ export class DomainPopupView extends AbstractViewPopup {
testConnectionCommand() { testConnectionCommand() {
this.clearTesting(false); this.clearTesting(false);
this.testing(true); // https://github.com/the-djmaze/snappymail/issues/477
AskPopupView.credentials('IMAP', 'GLOBAL/TEST').then(credentials => {
if (credentials) {
this.testing(true);
const params = domainToParams(this);
params.username = credentials.username;
params.password = credentials.password;
Remote.request('AdminDomainTest',
(iError, oData) => {
this.testing(false);
if (iError) {
this.testingImapError(true);
this.testingSieveError(true);
this.testingSmtpError(true);
} else {
this.testingDone(true);
this.testingImapError(true !== oData.Result.Imap);
this.testingSieveError(true !== oData.Result.Sieve);
this.testingSmtpError(true !== oData.Result.Smtp);
Remote.request('AdminDomainTest', if (this.testingImapError() && oData.Result.Imap) {
(iError, oData) => { this.testingImapErrorDesc('');
this.testing(false); this.testingImapErrorDesc(oData.Result.Imap);
if (iError) { }
this.testingImapError(true);
this.testingSieveError(true);
this.testingSmtpError(true);
} else {
this.testingDone(true);
this.testingImapError(true !== oData.Result.Imap);
this.testingSieveError(true !== oData.Result.Sieve);
this.testingSmtpError(true !== oData.Result.Smtp);
if (this.testingImapError() && oData.Result.Imap) { if (this.testingSieveError() && oData.Result.Sieve) {
this.testingImapErrorDesc(''); this.testingSieveErrorDesc('');
this.testingImapErrorDesc(oData.Result.Imap); this.testingSieveErrorDesc(oData.Result.Sieve);
} }
if (this.testingSieveError() && oData.Result.Sieve) { if (this.testingSmtpError() && oData.Result.Smtp) {
this.testingSieveErrorDesc(''); this.testingSmtpErrorDesc('');
this.testingSieveErrorDesc(oData.Result.Sieve); this.testingSmtpErrorDesc(oData.Result.Smtp);
} }
}
if (this.testingSmtpError() && oData.Result.Smtp) { },
this.testingSmtpErrorDesc(''); params
this.testingSmtpErrorDesc(oData.Result.Smtp); );
} }
} });
},
domainToParams(this)
);
} }
onDomainCreateOrSaveResponse(iError) { onDomainCreateOrSaveResponse(iError) {
@ -239,7 +248,7 @@ export class DomainPopupView extends AbstractViewPopup {
if (oDomain) { if (oDomain) {
this.enableSmartPorts(false); this.enableSmartPorts(false);
this.edit(true); this.edit(true);
forEachObjectEntry(oDomain, (key, value) => this[key] && this[key](value)); forEachObjectEntry(oDomain, (key, value) => this[key]?.(value));
this.enableSmartPorts(true); this.enableSmartPorts(true);
} }
} }

View file

@ -19,7 +19,7 @@ export class FolderSystemPopupView extends AbstractViewPopup {
this.folderSelectList = koComputable(() => this.folderSelectList = koComputable(() =>
folderListOptionsBuilder( folderListOptionsBuilder(
FolderUserStore.folderListSystemNames(), FolderUserStore.systemFoldersNames(),
[ [
['', i18n('POPUPS_SYSTEM_FOLDERS/SELECT_CHOOSE_ONE')], ['', i18n('POPUPS_SYSTEM_FOLDERS/SELECT_CHOOSE_ONE')],
[UNUSED_OPTION_VALUE, i18n('POPUPS_SYSTEM_FOLDERS/SELECT_UNUSE_NAME')] [UNUSED_OPTION_VALUE, i18n('POPUPS_SYSTEM_FOLDERS/SELECT_UNUSE_NAME')]

View file

@ -63,9 +63,7 @@ export class IdentityPopupView extends AbstractViewPopup {
submitForm() { submitForm() {
if (!this.submitRequest()) { if (!this.submitRequest()) {
if (this.signature && this.signature.__fetchEditorValue) { this.signature?.__fetchEditorValue?.();
this.signature.__fetchEditorValue();
}
if (!this.emailHasError()) { if (!this.emailHasError()) {
this.emailHasError(!this.email().trim()); this.emailHasError(!this.email().trim());

View file

@ -51,7 +51,7 @@ export class LanguagesPopupView extends AbstractViewPopup {
} }
changeLanguage(lang) { changeLanguage(lang) {
this.fLang && this.fLang(lang); this.fLang?.(lang);
this.close(); this.close();
} }
} }

View file

@ -94,7 +94,7 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
showError(e) { showError(e) {
console.log(e); console.log(e);
if (e && e.message) { if (e?.message) {
this.submitError(e.message); this.submitError(e.message);
} }
} }

View file

@ -114,9 +114,9 @@ export class LoginUserView extends AbstractViewLogin {
if (Notification.InvalidInputArgument == iError) { if (Notification.InvalidInputArgument == iError) {
iError = Notification.AuthError; iError = Notification.AuthError;
} }
this.submitError(getNotification(iError, (oData ? oData.ErrorMessage : ''), this.submitError(getNotification(iError, oData?.ErrorMessage,
Notification.UnknownNotification)); Notification.UnknownNotification));
this.submitErrorAdditional((oData && oData.ErrorMessageAdditional) || ''); this.submitErrorAdditional(oData?.ErrorMessageAdditional);
} else { } else {
rl.setData(oData.Result); rl.setData(oData.Result);
} }

View file

@ -31,9 +31,7 @@ export class MailFolderList extends AbstractViewLeft {
this.composeInEdit = AppUserStore.composeInEdit; this.composeInEdit = AppUserStore.composeInEdit;
this.folderList = FolderUserStore.folderList; this.systemFolders = FolderUserStore.systemFolders;
this.folderListSystem = FolderUserStore.folderListSystem;
this.foldersChanging = FolderUserStore.foldersChanging;
this.moveAction = moveAction; this.moveAction = moveAction;
@ -84,7 +82,7 @@ export class MailFolderList extends AbstractViewLeft {
} }
el = eqs(event, 'a'); el = eqs(event, 'a');
if (el && el.matches('.selectable')) { if (el?.matches('.selectable')) {
event.preventDefault(); event.preventDefault();
const folder = ko.dataFor(el); const folder = ko.dataFor(el);
if (folder) { if (folder) {
@ -172,10 +170,10 @@ export class MailFolderList extends AbstractViewLeft {
AppUserStore.focusedState.subscribe(value => { AppUserStore.focusedState.subscribe(value => {
let el = qs('li a.focused'); let el = qs('li a.focused');
el && el.classList.remove('focused'); el?.classList.remove('focused');
if (Scope.FolderList === value) { if (Scope.FolderList === value) {
el = qs('li a.selected'); el = qs('li a.selected');
el && el.classList.add('focused'); el?.classList.add('focused');
} }
}); });
} }

View file

@ -20,7 +20,7 @@ import { isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
import { mailBox, serverRequest } from 'Common/Links'; import { mailBox, serverRequest } from 'Common/Links';
import { Selector } from 'Common/Selector'; import { Selector } from 'Common/Selector';
import { i18n, initOnStartOrLangChange } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { import {
getFolderFromCacheList, getFolderFromCacheList,
@ -55,14 +55,14 @@ const
*/ */
listAction = (...args) => MessagelistUserStore.setAction(...args); listAction = (...args) => MessagelistUserStore.setAction(...args);
let
iGoToUpOrDownTimeout = 0,
sLastSearchValue = '';
export class MailMessageList extends AbstractViewRight { export class MailMessageList extends AbstractViewRight {
constructor() { constructor() {
super(); super();
this.emptySubjectValue = '';
this.iGoToUpOrDownTimeout = 0;
this.newMoveToFolder = !!SettingsGet('NewMoveToFolder'); this.newMoveToFolder = !!SettingsGet('NewMoveToFolder');
this.allowSearch = SettingsCapa('Search'); this.allowSearch = SettingsCapa('Search');
@ -76,19 +76,10 @@ export class MailMessageList extends AbstractViewRight {
this.isMobile = ThemeStore.isMobile; this.isMobile = ThemeStore.isMobile;
this.leftPanelDisabled = leftPanelDisabled; this.leftPanelDisabled = leftPanelDisabled;
this.messageListSearch = MessagelistUserStore.listSearch;
this.messageListError = MessagelistUserStore.error;
this.popupVisibility = arePopupsVisible; this.popupVisibility = arePopupsVisible;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList; this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
this.messageListThreadUid = MessagelistUserStore.endThreadUid;
this.messageListIsLoading = MessagelistUserStore.isLoading;
initOnStartOrLangChange(() => this.emptySubjectValue = i18n('MESSAGE_LIST/EMPTY_SUBJECT_TEXT'));
this.userUsageProc = FolderUserStore.quotaPercentage; this.userUsageProc = FolderUserStore.quotaPercentage;
this.addObservables({ this.addObservables({
@ -99,15 +90,13 @@ export class MailMessageList extends AbstractViewRight {
dragOverArea: null, dragOverArea: null,
dragOverBodyArea: null, dragOverBodyArea: null,
inputMessageListSearchFocus: false focusSearch: false
}); });
// append drag and drop // append drag and drop
this.dragOver = ko.observable(false).extend({ throttle: 1 }); this.dragOver = ko.observable(false).extend({ throttle: 1 });
this.dragOverEnter = ko.observable(false).extend({ throttle: 1 }); this.dragOverEnter = ko.observable(false).extend({ throttle: 1 });
this.sLastSearchValue = '';
this.addComputables({ this.addComputables({
sortSupported: () => sortSupported: () =>
@ -136,9 +125,9 @@ export class MailMessageList extends AbstractViewRight {
} }
}, },
inputProxyMessageListSearch: { inputSearch: {
read: MessagelistUserStore.mainSearch, read: MessagelistUserStore.mainSearch,
write: value => this.sLastSearchValue = value write: value => sLastSearchValue = value
}, },
isIncompleteChecked: () => { isIncompleteChecked: () => {
@ -146,33 +135,28 @@ export class MailMessageList extends AbstractViewRight {
return c && MessagelistUserStore().length > c; return c && MessagelistUserStore().length > c;
}, },
hasMessages: () => 0 < MessagelistUserStore().length,
isSpamFolder: () => (FolderUserStore.spamFolder() || 0) === MessagelistUserStore().Folder, isSpamFolder: () => (FolderUserStore.spamFolder() || 0) === MessagelistUserStore().Folder,
isSpamDisabled: () => UNUSED_OPTION_VALUE === FolderUserStore.spamFolder(),
isTrashFolder: () => (FolderUserStore.trashFolder() || 0) === MessagelistUserStore().Folder, isTrashFolder: () => (FolderUserStore.trashFolder() || 0) === MessagelistUserStore().Folder,
isDraftFolder: () => (FolderUserStore.draftsFolder() || 0) === MessagelistUserStore().Folder, isDraftFolder: () => (FolderUserStore.draftsFolder() || 0) === MessagelistUserStore().Folder,
isSentFolder: () => (FolderUserStore.sentFolder() || 0) === MessagelistUserStore().Folder, archiveAllowed: () =>
(FolderUserStore.archiveFolder() || 0) !== MessagelistUserStore().Folder
&& UNUSED_OPTION_VALUE !== FolderUserStore.archiveFolder()
&& !this.isDraftFolder(),
isArchiveFolder: () => (FolderUserStore.archiveFolder() || 0) === MessagelistUserStore().Folder, spamAllowed: () => UNUSED_OPTION_VALUE !== FolderUserStore.spamFolder()
&& (FolderUserStore.sentFolder() || 0) !== MessagelistUserStore().Folder
&& !this.isDraftFolder(),
isArchiveDisabled: () => UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder(), isSpamVisible: () => !this.isSpamFolder() && this.spamAllowed(),
isArchiveVisible: () => !this.isArchiveFolder() && !this.isArchiveDisabled() && !this.isDraftFolder(), isUnSpamVisible: () => this.isSpamFolder() && this.spamAllowed(),
isSpamVisible: () => mobileCheckedStateShow: () => ThemeStore.isMobile() ? MessagelistUserStore.listChecked().length : 1,
!this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder(),
isUnSpamVisible: () => mobileCheckedStateHide: () => ThemeStore.isMobile() ? !MessagelistUserStore.listChecked().length : 1,
this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder(),
mobileCheckedStateShow: () => ThemeStore.isMobile() ? 0 < MessagelistUserStore.listChecked().length : true,
mobileCheckedStateHide: () => ThemeStore.isMobile() ? !MessagelistUserStore.listChecked().length : true,
sortText: () => { sortText: () => {
let mode = FolderUserStore.sortMode(), let mode = FolderUserStore.sortMode(),
@ -188,8 +172,6 @@ export class MailMessageList extends AbstractViewRight {
} }
}); });
this.hasCheckedOrSelectedLines = MessagelistUserStore.hasCheckedOrSelected,
this.selector = new Selector( this.selector = new Selector(
MessagelistUserStore, MessagelistUserStore,
MessagelistUserStore.selectedMessage, MessagelistUserStore.selectedMessage,
@ -228,7 +210,7 @@ export class MailMessageList extends AbstractViewRight {
const sFolder = e.detail.Folder, iUid = e.detail.Uid; const sFolder = e.detail.Folder, iUid = e.detail.Uid;
const message = MessagelistUserStore.find( const message = MessagelistUserStore.find(
item => item && sFolder === item.folder && iUid == item.uid item => sFolder === item?.folder && iUid == item?.uid
); );
if ('INBOX' === sFolder) { if ('INBOX' === sFolder) {
@ -344,7 +326,7 @@ export class MailMessageList extends AbstractViewRight {
moveNewCommand(vm, event) { moveNewCommand(vm, event) {
if (this.newMoveToFolder && this.mobileCheckedStateShow()) { if (this.newMoveToFolder && this.mobileCheckedStateShow()) {
if (vm && event && event.preventDefault) { if (vm && event?.preventDefault) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
@ -364,8 +346,8 @@ export class MailMessageList extends AbstractViewRight {
return false; return false;
} }
clearTimeout(this.iGoToUpOrDownTimeout); clearTimeout(iGoToUpOrDownTimeout);
this.iGoToUpOrDownTimeout = setTimeout(() => { iGoToUpOrDownTimeout = setTimeout(() => {
let prev, next, temp, current; let prev, next, temp, current;
this.messageListPaginator().find(item => { this.messageListPaginator().find(item => {
@ -412,7 +394,7 @@ export class MailMessageList extends AbstractViewRight {
cancelSearch() { cancelSearch() {
MessagelistUserStore.mainSearch(''); MessagelistUserStore.mainSearch('');
this.inputMessageListSearchFocus(false); this.focusSearch(false);
} }
cancelThreadUid() { cancelThreadUid() {
@ -446,7 +428,7 @@ export class MailMessageList extends AbstractViewRight {
getDragData(event) { getDragData(event) {
const item = ko.dataFor(doc.elementFromPoint(event.clientX, event.clientY)); const item = ko.dataFor(doc.elementFromPoint(event.clientX, event.clientY));
item && item.checked && item.checked(true); item?.checked?.(true);
const uids = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(); const uids = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
item && !uids.includes(item.uid) && uids.push(item.uid); item && !uids.includes(item.uid) && uids.push(item.uid);
return uids.length ? { return uids.length ? {
@ -589,7 +571,7 @@ export class MailMessageList extends AbstractViewRight {
} }
gotoThread(message) { gotoThread(message) {
if (message && 0 < message.threadsLen()) { if (0 < message?.threadsLen()) {
MessagelistUserStore.pageBeforeThread(MessagelistUserStore.page()); MessagelistUserStore.pageBeforeThread(MessagelistUserStore.page());
hasher.setHash( hasher.setHash(
@ -643,7 +625,7 @@ export class MailMessageList extends AbstractViewRight {
el && this.gotoThread(ko.dataFor(el)); el && this.gotoThread(ko.dataFor(el));
}, },
dblclick: event => { dblclick: event => {
let el = eqs(event, '.actionHandle'); let el = eqs(event, '.actionHandle');
el && this.gotoThread(ko.dataFor(el)); el && this.gotoThread(ko.dataFor(el));
} }
}); });
@ -684,7 +666,7 @@ export class MailMessageList extends AbstractViewRight {
addShortcut('enter,open', '', Scope.MessageList, () => { addShortcut('enter,open', '', Scope.MessageList, () => {
if (formFieldFocused()) { if (formFieldFocused()) {
MessagelistUserStore.mainSearch(this.sLastSearchValue); MessagelistUserStore.mainSearch(sLastSearchValue);
return false; return false;
} }
if (MessageUserStore.message() && this.useAutoSelect()) { if (MessageUserStore.message() && this.useAutoSelect()) {
@ -735,15 +717,10 @@ export class MailMessageList extends AbstractViewRight {
}); });
registerShortcut('t', '', [Scope.MessageList], () => { registerShortcut('t', '', [Scope.MessageList], () => {
let message = MessagelistUserStore.selectedMessage(); let message = MessagelistUserStore.selectedMessage() || MessagelistUserStore.focusedMessage();
if (!message) { if (0 < message?.threadsLen()) {
message = MessagelistUserStore.focusedMessage();
}
if (message && 0 < message.threadsLen()) {
this.gotoThread(message); this.gotoThread(message);
} }
return false; return false;
}); });
@ -778,7 +755,7 @@ export class MailMessageList extends AbstractViewRight {
if (SettingsCapa('Search')) { if (SettingsCapa('Search')) {
// search input focus // search input focus
addShortcut('/', '', [Scope.MessageList, Scope.MessageView], () => { addShortcut('/', '', [Scope.MessageList, Scope.MessageView], () => {
this.inputMessageListSearchFocus(true); this.focusSearch(true);
return false; return false;
}); });
} }

View file

@ -7,7 +7,6 @@ import { Scope } from 'Common/Enums';
import { import {
ComposeType, ComposeType,
ClientSideKeyNameLastReplyAction,
ClientSideKeyNameMessageHeaderFullInfo, ClientSideKeyNameMessageHeaderFullInfo,
ClientSideKeyNameMessageAttachmentControls, ClientSideKeyNameMessageAttachmentControls,
FolderType, FolderType,
@ -94,10 +93,11 @@ export class MailMessageView extends AbstractViewRight {
} }
}, this.messageVisibility); }, this.messageVisibility);
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
this.addObservables({ this.addObservables({
showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls), showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
downloadAsZipLoading: false, downloadAsZipLoading: false,
lastReplyAction_: '',
showFullInfo: '1' === Local.get(ClientSideKeyNameMessageHeaderFullInfo), showFullInfo: '1' === Local.get(ClientSideKeyNameMessageHeaderFullInfo),
moreDropdownTrigger: false, moreDropdownTrigger: false,
@ -134,19 +134,10 @@ export class MailMessageView extends AbstractViewRight {
allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'), allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'),
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
&& (currentMessage() ? currentMessage().attachments : []) && (currentMessage()?.attachments || [])
.filter(item => item && !item.isLinked() && item.checked() && item.download) .filter(item => item?.download && !item?.isLinked() && item?.checked())
.length, .length,
lastReplyAction: {
read: this.lastReplyAction_,
write: value => this.lastReplyAction_(
[ComposeType.Reply, ComposeType.ReplyAll, ComposeType.Forward].includes(value)
? ComposeType.Reply
: value
)
},
tagsAllowed: () => FolderUserStore.currentFolder() ? FolderUserStore.currentFolder().tagsAllowed() : false, tagsAllowed: () => FolderUserStore.currentFolder() ? FolderUserStore.currentFolder().tagsAllowed() : false,
messageVisibility: () => !MessageUserStore.loading() && !!currentMessage(), messageVisibility: () => !MessageUserStore.loading() && !!currentMessage(),
@ -182,8 +173,6 @@ export class MailMessageView extends AbstractViewRight {
}); });
this.addSubscribables({ this.addSubscribables({
lastReplyAction_: value => Local.set(ClientSideKeyNameLastReplyAction, value),
message: message => { message: message => {
MessageUserStore.activeDom(null); MessageUserStore.activeDom(null);
@ -207,8 +196,6 @@ export class MailMessageView extends AbstractViewRight {
showFullInfo: value => Local.set(ClientSideKeyNameMessageHeaderFullInfo, value ? '1' : '0') showFullInfo: value => Local.set(ClientSideKeyNameMessageHeaderFullInfo, value ? '1' : '0')
}); });
this.lastReplyAction(Local.get(ClientSideKeyNameLastReplyAction) || ComposeType.Reply);
// commands // commands
this.replyCommand = createCommandReplyHelper(ComposeType.Reply); this.replyCommand = createCommandReplyHelper(ComposeType.Reply);
this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll); this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll);
@ -256,7 +243,6 @@ export class MailMessageView extends AbstractViewRight {
* @returns {void} * @returns {void}
*/ */
replyOrforward(sType) { replyOrforward(sType) {
this.lastReplyAction(sType);
showMessageComposer([sType, currentMessage()]); showMessageComposer([sType, currentMessage()]);
} }
@ -300,7 +286,7 @@ export class MailMessageView extends AbstractViewRight {
el = eqs(event, '.attachmentsPlace .attachmentItem .attachmentNameParent'); el = eqs(event, '.attachmentsPlace .attachmentItem .attachmentNameParent');
if (el) { if (el) {
const attachment = ko.dataFor(el); const attachment = ko.dataFor(el);
if (attachment && attachment.linkDownload()) { if (attachment?.linkDownload()) {
if ('message/rfc822' == attachment.mimeType) { if ('message/rfc822' == attachment.mimeType) {
// TODO // TODO
rl.fetch(attachment.linkDownload()).then(response => { rl.fetch(attachment.linkDownload()).then(response => {
@ -407,7 +393,7 @@ export class MailMessageView extends AbstractViewRight {
// toggle message blockquotes // toggle message blockquotes
registerShortcut('b', '', [Scope.MessageList, Scope.MessageView], () => { registerShortcut('b', '', [Scope.MessageList, Scope.MessageView], () => {
const message = currentMessage(); const message = currentMessage();
if (message && message.body) { if (message?.body) {
message.body.querySelectorAll('.rlBlockquoteSwitcher').forEach(node => node.click()); message.body.querySelectorAll('.rlBlockquoteSwitcher').forEach(node => node.click());
return false; return false;
} }
@ -425,7 +411,7 @@ export class MailMessageView extends AbstractViewRight {
// print // print
addShortcut('p,printscreen', 'meta', [Scope.MessageView, Scope.MessageList], () => { addShortcut('p,printscreen', 'meta', [Scope.MessageView, Scope.MessageList], () => {
currentMessage() && currentMessage().printMessage(); currentMessage()?.printMessage();
return false; return false;
}); });
@ -524,7 +510,7 @@ export class MailMessageView extends AbstractViewRight {
downloadAsZip() { downloadAsZip() {
const hashes = (currentMessage() ? currentMessage().attachments : []) const hashes = (currentMessage() ? currentMessage().attachments : [])
.map(item => (item && !item.isLinked() && item.checked() ? item.download : '')) .map(item => item?.checked() && !item?.isLinked() ? item.download : '')
.filter(v => v); .filter(v => v);
if (hashes.length) { if (hashes.length) {
Remote.post('AttachmentsActions', this.downloadAsZipLoading, { Remote.post('AttachmentsActions', this.downloadAsZipLoading, {
@ -532,7 +518,7 @@ export class MailMessageView extends AbstractViewRight {
Hashes: hashes Hashes: hashes
}) })
.then(result => { .then(result => {
let hash = result && result.Result && result.Result.FileHash; let hash = result?.Result?.FileHash;
if (hash) { if (hash) {
download(attachmentDownload(hash), hash+'.zip'); download(attachmentDownload(hash), hash+'.zip');
} else { } else {
@ -591,7 +577,7 @@ export class MailMessageView extends AbstractViewRight {
if (result.data) { if (result.data) {
MimeToMessage(result.data, oMessage); MimeToMessage(result.data, oMessage);
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain(); oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
if (result.signatures && result.signatures.length) { if (result.signatures?.length) {
oMessage.pgpSigned(true); oMessage.pgpSigned(true);
oMessage.pgpVerified({ oMessage.pgpVerified({
signatures: result.signatures, signatures: result.signatures,
@ -610,7 +596,7 @@ export class MailMessageView extends AbstractViewRight {
oMessage.pgpVerified(result); oMessage.pgpVerified(result);
} }
/* /*
if (result && result.success) { if (result?.success) {
i18n('OPENPGP/GOOD_SIGNATURE', { i18n('OPENPGP/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')' USER: validKey.user + ' (' + validKey.id + ')'
}); });
@ -618,7 +604,7 @@ export class MailMessageView extends AbstractViewRight {
} else { } else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds additional = keyIds
? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ') ? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
: ''; : '';
i18n('OPENPGP/ERROR', { i18n('OPENPGP/ERROR', {

View file

@ -52,7 +52,7 @@ export class SystemDropDownUserView extends AbstractViewRight {
} }
accountClick(account, event) { accountClick(account, event) {
let email = account && account.email; let email = account?.email;
if (email && 0 === event.button && AccountUserStore.email() != email) { if (email && 0 === event.button && AccountUserStore.email() != email) {
AccountUserStore.loading(true); AccountUserStore.loading(true);
event.preventDefault(); event.preventDefault();

View file

@ -70,7 +70,7 @@
htmlDrag = b => doc.documentElement.classList.toggle('firefox-drag', b), htmlDrag = b => doc.documentElement.classList.toggle('firefox-drag', b),
setDragImage = (src, xOffset, yOffset) => { setDragImage = (src, xOffset, yOffset) => {
img && img.remove(); img?.remove();
if (src) { if (src) {
// create drag image from custom element or drag source // create drag image from custom element or drag source
img = src.cloneNode(true); img = src.cloneNode(true);
@ -85,7 +85,7 @@
if (dragSource) { if (dragSource) {
clearInterval(holdInterval); clearInterval(holdInterval);
// dispose of drag image element // dispose of drag image element
img && img.remove(); img?.remove();
isDragging && dispatchEvent(lastTouch, 'dragend', dragSource); isDragging && dispatchEvent(lastTouch, 'dragend', dragSource);
img = dragSource = lastTouch = lastTarget = dataTransfer = holdInterval = null; img = dragSource = lastTouch = lastTarget = dataTransfer = holdInterval = null;
isDragging = allowDrop = false; isDragging = allowDrop = false;
@ -163,6 +163,17 @@
return false; return false;
}; };
/*
doc.addEventListener('pointerdown', e => {
doc.addEventListener('pointermove', e => {
e.clientX
});
doc.setPointerCapture(e.pointerId);
});
doc.addEventListener('pointerup', e => {
doc.releasePointerCapture(e.pointerId);
});
*/
doc.addEventListener('touchstart', e => { doc.addEventListener('touchstart', e => {
// clear all variables // clear all variables
reset(); reset();

View file

@ -64,9 +64,8 @@ const
keydown = event => { keydown = event => {
let key = (event.key || '').toLowerCase().replace(' ','space'), let key = (event.key || '').toLowerCase().replace(' ','space'),
modifiers = ['alt','ctrl','meta','shift'].filter(v => event[v+'Key']).join('+'); modifiers = ['alt','ctrl','meta','shift'].filter(v => event[v+'Key']).join('+');
scope[key] && scope[key][modifiers] && scope[key][modifiers].forEach(cmd => exec(event, cmd)); scope[key]?.[modifiers]?.forEach(cmd => exec(event, cmd));
!event.defaultPrevented && _scope !== 'all' && _scopes.all[key] && _scopes.all[key][modifiers] !event.defaultPrevented && _scope !== 'all' && _scopes.all[key]?.[modifiers]?.forEach(cmd => exec(event, cmd));
&& _scopes.all[key][modifiers].forEach(cmd => exec(event, cmd));
}; };
win.shortcuts = shortcuts; win.shortcuts = shortcuts;

View file

@ -17,7 +17,6 @@ import { SieveScriptPopupView } from 'Sieve/View/Script';
window.Sieve = { window.Sieve = {
capa: capa, capa: capa,
scripts: scripts, scripts: scripts,
setError: setError,
loading: loading, loading: loading,
serverError: serverError, serverError: serverError,
serverErrorDesc: serverErrorDesc, serverErrorDesc: serverErrorDesc,

View file

@ -1,4 +1,4 @@
/* RainLoop Webmail (c) RainLoop Team | Licensed under AGPL 3 */ /* RainLoop Webmail (c) RainLoop Team | Licensed under MIT */
const gulp = require('gulp'); const gulp = require('gulp');
const { cleanStatic } = require('./tasks/common'); const { cleanStatic } = require('./tasks/common');

View file

@ -6,9 +6,9 @@ if (!defined('APP_VERSION'))
define('APP_INDEX_ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR); define('APP_INDEX_ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR);
} }
if (file_exists('snappymail/v/'.APP_VERSION.'/include.php')) if (file_exists(APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php'))
{ {
include 'snappymail/v/'.APP_VERSION.'/include.php'; include APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php';
} }
else else
{ {

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.17.0</upstream>. This app packages SnappyMail <upstream>2.17.4</upstream>.
SnappyMail is a simple, modern, lightweight & fast web-based email client. SnappyMail is a simple, modern, lightweight & fast web-based email client.

View file

@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway # If you change the extraction below, be sure to test on scaleway
VERSION=2.17.0 VERSION=2.17.4
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \ RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \ unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \ rm /tmp/snappymail.zip && \

View file

@ -1 +1 @@
2.17.0 2.17.4

View file

@ -4,7 +4,7 @@
<name>SnappyMail</name> <name>SnappyMail</name>
<summary>SnappyMail Webmail</summary> <summary>SnappyMail Webmail</summary>
<description>Simple, modern and fast web-based email client. After enabling in Nextcloud, go to Nextcloud admin panel, "Additionnal settings" and you will see a "SnappyMail webmail" section. There, click on the link to go to the SnappyMail admin panel. The default user/password is admin/12345. This version is based on SnappyMail 2.6.0 (2021-07).</description> <description>Simple, modern and fast web-based email client. After enabling in Nextcloud, go to Nextcloud admin panel, "Additionnal settings" and you will see a "SnappyMail webmail" section. There, click on the link to go to the SnappyMail admin panel. The default user/password is admin/12345. This version is based on SnappyMail 2.6.0 (2021-07).</description>
<version>2.17.0</version> <version>2.17.4</version>
<licence>agpl</licence> <licence>agpl</licence>
<author>SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author> <author>SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
<namespace>SnappyMail</namespace> <namespace>SnappyMail</namespace>

View file

@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions() # script_snappymail_versions()
sub script_snappymail_versions sub script_snappymail_versions
{ {
return ( "2.17.0" ); return ( "2.17.4" );
} }
sub script_snappymail_version_desc sub script_snappymail_version_desc

View file

@ -3,7 +3,7 @@
"title": "SnappyMail", "title": "SnappyMail",
"description": "Simple, modern & fast web-based email client", "description": "Simple, modern & fast web-based email client",
"private": true, "private": true,
"version": "2.17.0", "version": "2.17.4",
"homepage": "https://snappymail.eu", "homepage": "https://snappymail.eu",
"author": { "author": {
"name": "DJ Maze", "name": "DJ Maze",

View file

@ -107,6 +107,8 @@ abstract class HtmlUtils
$aRemove = array(); $aRemove = array();
$sIdRight = \md5(\microtime());
$aNodes = $oBody->getElementsByTagName('*'); $aNodes = $oBody->getElementsByTagName('*');
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement)
{ {
@ -114,56 +116,30 @@ abstract class HtmlUtils
if (\in_array($sTagNameLower, $aRemoveTags)) if (\in_array($sTagNameLower, $aRemoveTags))
{ {
$aRemove[] = @$oElement; $aRemove[] = $oElement;
continue; continue;
} }
if ($oElement->hasAttribute('data-x-src-cid')) // images
{ if ($oElement->hasAttribute('data-x-src-broken') || $oElement->hasAttribute('data-x-src-hidden')) {
$aRemove[] = $oElement;
continue;
}
if ($oElement->hasAttribute('data-x-src-cid')) {
$sCid = $oElement->getAttribute('data-x-src-cid'); $sCid = $oElement->getAttribute('data-x-src-cid');
$oElement->removeAttribute('data-x-src-cid'); $oElement->removeAttribute('data-x-src-cid');
if (!empty($sCid)) {
if (!empty($sCid))
{
$aFoundCids[] = $sCid; $aFoundCids[] = $sCid;
@$oElement->removeAttribute('src');
$oElement->setAttribute('src', 'cid:'.$sCid); $oElement->setAttribute('src', 'cid:'.$sCid);
} }
} }
if ($oElement->hasAttribute('data-x-src')) {
if ($oElement->hasAttribute('data-x-src-location'))
{
$sSrc = $oElement->getAttribute('data-x-src-location');
$oElement->removeAttribute('data-x-src-location');
if (!empty($sSrc))
{
$aFoundContentLocationUrls[] = $sSrc;
@$oElement->removeAttribute('src');
$oElement->setAttribute('src', $sSrc);
}
}
if ($oElement->hasAttribute('data-x-broken-src'))
{
$oElement->setAttribute('src', $oElement->getAttribute('data-x-broken-src'));
$oElement->removeAttribute('data-x-broken-src');
}
if ($oElement->hasAttribute('data-x-src'))
{
$oElement->setAttribute('src', $oElement->getAttribute('data-x-src')); $oElement->setAttribute('src', $oElement->getAttribute('data-x-src'));
$oElement->removeAttribute('data-x-src'); $oElement->removeAttribute('data-x-src');
} }
// style attribute images // style attribute images
$aCid = array(); $aCid = array();
if ($oElement->hasAttribute('data-x-style-cid')) {
$aCid = \json_decode($oElement->getAttribute('data-x-style-cid'), true);
$oElement->removeAttribute('data-x-style-cid');
}
if ($oElement->hasAttribute('data-x-style-url')) { if ($oElement->hasAttribute('data-x-style-url')) {
$aCid = \array_merge($aCid, \json_decode($oElement->getAttribute('data-x-style-url'), true)); $aCid = \array_merge($aCid, \json_decode($oElement->getAttribute('data-x-style-url'), true));
$oElement->removeAttribute('data-x-style-url'); $oElement->removeAttribute('data-x-style-url');
@ -205,7 +181,7 @@ abstract class HtmlUtils
$sSrc = $oElement->getAttribute('src'); $sSrc = $oElement->getAttribute('src');
if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) if ('data:image/' === \strtolower(\substr($sSrc, 0, 11)))
{ {
$sHash = \md5($sSrc); $sHash = \md5($sSrc) . '@' . $sIdRight;
$aFoundDataURL[$sHash] = $sSrc; $aFoundDataURL[$sHash] = $sSrc;
$oElement->setAttribute('src', 'cid:'.$sHash); $oElement->setAttribute('src', 'cid:'.$sHash);

View file

@ -59,10 +59,7 @@ abstract class Utils
return 'iso-8859-8'; return 'iso-8859-8';
} }
$sEncoding = \preg_replace('/^cp-([\d])/', 'cp$1', $sEncoding); return \preg_replace('/^(cp-?|windows?)(12[\d])/', 'windows-$1', $sEncoding);
$sEncoding = \preg_replace('/^windows?12/', 'windows-12', $sEncoding);
return $sEncoding;
} }
public static function NormalizeCharsetByValue(string $sCharset, string $sValue) : string public static function NormalizeCharsetByValue(string $sCharset, string $sValue) : string
@ -159,14 +156,13 @@ abstract class Utils
$sInputFromEncoding = static::$RenameEncoding[$sInputFromEncoding]; $sInputFromEncoding = static::$RenameEncoding[$sInputFromEncoding];
} }
if (!static::MbSupportedEncoding($sInputFromEncoding)) { if (!static::MbSupportedEncoding($sInputFromEncoding)) {
if (\function_exists('iconv')) {
$sResult = \iconv($sInputFromEncoding, "{$sInputToEncoding}//IGNORE", $sInputString);
return (false !== $sResult) ? $sResult : $sInputString;
}
\error_log("Unsupported encoding {$sInputFromEncoding}"); \error_log("Unsupported encoding {$sInputFromEncoding}");
$sInputFromEncoding = null; $sInputFromEncoding = null;
// return $sInputString; // return $sInputString;
/*
if (\function_exists('iconv')) {
return \iconv($sInputFromEncoding, $sInputToEncoding, $sInputString);
}
*/
} }
} }
@ -179,12 +175,10 @@ abstract class Utils
public static function ConvertEncoding(string $sInputString, string $sInputFromEncoding, string $sInputToEncoding) : string public static function ConvertEncoding(string $sInputString, string $sInputFromEncoding, string $sInputToEncoding) : string
{ {
$sResult = $sInputString;
$sFromEncoding = static::NormalizeCharset($sInputFromEncoding); $sFromEncoding = static::NormalizeCharset($sInputFromEncoding);
$sToEncoding = static::NormalizeCharset($sInputToEncoding); $sToEncoding = static::NormalizeCharset($sInputToEncoding);
if ('' === \trim($sResult) || ($sFromEncoding === $sToEncoding && Enumerations\Charset::UTF_8 !== $sFromEncoding)) if ('' === \trim($sInputString) || ($sFromEncoding === $sToEncoding && Enumerations\Charset::UTF_8 !== $sFromEncoding))
{ {
return $sInputString; return $sInputString;
} }

View file

@ -116,8 +116,8 @@ abstract class SearchCriterias
UNFLAGGED UNFLAGGED
SEEN SEEN
UNSEEN UNSEEN
ANSWERED ANSWERED
UNANSWERED UNANSWERED
DELETED DELETED
UNDELETED UNDELETED
DRAFT DRAFT
@ -129,7 +129,6 @@ abstract class SearchCriterias
public static function fromString(\MailSo\Imap\ImapClient $oImapClient, string $sFolderName, string $sSearch, bool $bHideDeleted, bool &$bUseCache = true) : string public static function fromString(\MailSo\Imap\ImapClient $oImapClient, string $sFolderName, string $sSearch, bool $bHideDeleted, bool &$bUseCache = true) : string
{ {
$bUseCache = true;
$iTimeFilter = 0; $iTimeFilter = 0;
$aCriteriasResult = array(); $aCriteriasResult = array();
@ -281,7 +280,7 @@ abstract class SearchCriterias
} }
if ($bHideDeleted && !\in_array('DELETED', $aCriteriasResult) && !\in_array('UNDELETED', $aCriteriasResult)) { if ($bHideDeleted && !\in_array('DELETED', $aCriteriasResult) && !\in_array('UNDELETED', $aCriteriasResult)) {
$aCriteriasResult['UNDELETED'] = true; $aCriteriasResult[] = 'UNDELETED';
} }
if (\MailSo\Config::$MessageListPermanentFilter) { if (\MailSo\Config::$MessageListPermanentFilter) {
@ -387,7 +386,7 @@ abstract class SearchCriterias
$aCache = array(); $aCache = array();
$sSearch = \MailSo\Base\Utils::StripSpaces($sSearch); $sSearch = \MailSo\Base\Utils::StripSpaces($sSearch);
$sSearch = \trim(\preg_replace('/('.static::RegEx.'): /i', '\\1:', $sSearch)); $sSearch = \trim(\preg_replace('/('.static::RegEx.'):\\s+/i', '\\1:', $sSearch));
$mMatch = array(); $mMatch = array();
if (\preg_match_all('/".*?(?<!\\\)"/', $sSearch, $mMatch)) { if (\preg_match_all('/".*?(?<!\\\)"/', $sSearch, $mMatch)) {
@ -414,7 +413,7 @@ abstract class SearchCriterias
} }
} }
if (\preg_match_all('/('.static::RegEx.'):([^\s]*)/i', $sSearch, $mMatch)) { if (\preg_match_all('/('.static::RegEx.'):([^\\s]*)/i', $sSearch, $mMatch)) {
if (\is_array($mMatch[0])) { if (\is_array($mMatch[0])) {
foreach ($mMatch[0] as $sToken) { foreach ($mMatch[0] as $sToken) {
$sSearch = \str_replace($sToken, '', $sSearch); $sSearch = \str_replace($sToken, '', $sSearch);

View file

@ -748,7 +748,7 @@ class MailClient
$aResultUids = false; $aResultUids = false;
$bUidsFromCacher = false; $bUidsFromCacher = false;
$bUseCacheAfterSearch = true; $bUseCacheAfterSearch = $oCacher && $oCacher->IsInited();
$sSerializedHash = ''; $sSerializedHash = '';
$sSerializedLog = ''; $sSerializedLog = '';
@ -756,25 +756,23 @@ class MailClient
$bUseSortIfSupported = $bUseSortIfSupported && !\strlen($sSearch) && $this->oImapClient->IsSupported('SORT'); $bUseSortIfSupported = $bUseSortIfSupported && !\strlen($sSearch) && $this->oImapClient->IsSupported('SORT');
$sSearchCriterias = \MailSo\Imap\SearchCriterias::fromString($this->oImapClient, $sFolderName, $sSearch, $oParams->bHideDeleted, $bUseCacheAfterSearch); $sSearchCriterias = \MailSo\Imap\SearchCriterias::fromString($this->oImapClient, $sFolderName, $sSearch, $oParams->bHideDeleted, $bUseCacheAfterSearch);
if ($bUseCacheAfterSearch && $oCacher && $oCacher->IsInited()) // Disabled for now as there are many cases that change the result
{ $bUseCacheAfterSearch = false;
if ($bUseCacheAfterSearch) {
$sSerializedHash = 'GetUids/'. $sSerializedHash = 'GetUids/'.
($bUseSortIfSupported ? 'S' . $sSort : 'N').'/'. ($bUseSortIfSupported ? 'S' . $sSort : 'N').'/'.
$this->GenerateImapClientHash().'/'. $this->GenerateImapClientHash().'/'.
$sFolderName.'/'.$sSearchCriterias; $sFolderName.'/'.$sSearchCriterias;
$sSerializedLog = '"'.$sFolderName.'" / '.$sSearchCriterias.''; $sSerializedLog = '"'.$sFolderName.'" / '.$sSearchCriterias.'';
// $sSerialized = $oCacher->Get($sSerializedHash); $sSerialized = $oCacher->Get($sSerializedHash);
if (!empty($sSerialized)) if (!empty($sSerialized)) {
{
$aSerialized = \json_decode($sSerialized, true); $aSerialized = \json_decode($sSerialized, true);
if (\is_array($aSerialized) && isset($aSerialized['FolderHash'], $aSerialized['Uids']) && if (\is_array($aSerialized) && isset($aSerialized['FolderHash'], $aSerialized['Uids']) &&
$sFolderHash === $aSerialized['FolderHash'] && $sFolderHash === $aSerialized['FolderHash'] &&
\is_array($aSerialized['Uids']) \is_array($aSerialized['Uids'])
) ) {
{ if ($this->oLogger) {
if ($this->oLogger)
{
$this->oLogger->Write('Get Serialized UIDS from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']'); $this->oLogger->Write('Get Serialized UIDS from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']');
} }
@ -784,8 +782,7 @@ class MailClient
} }
} }
if (!\is_array($aResultUids)) if (!$bUidsFromCacher) {
{
if ($bUseSortIfSupported) { if ($bUseSortIfSupported) {
// $this->oImapClient->IsSupported('ESORT') // $this->oImapClient->IsSupported('ESORT')
// $aResultUids = $this->oImapClient->MessageSimpleESort(array($sSort ?: 'REVERSE DATE'), $sSearchCriterias)['ALL']; // $aResultUids = $this->oImapClient->MessageSimpleESort(array($sSort ?: 'REVERSE DATE'), $sSearchCriterias)['ALL'];
@ -796,15 +793,13 @@ class MailClient
$aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, true, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8'); $aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, true, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8');
} }
if (!$bUidsFromCacher && $bUseCacheAfterSearch && \is_array($aResultUids) && $oCacher && $oCacher->IsInited() && \strlen($sSerializedHash)) if ($bUseCacheAfterSearch) {
{
$oCacher->Set($sSerializedHash, \json_encode(array( $oCacher->Set($sSerializedHash, \json_encode(array(
'FolderHash' => $sFolderHash, 'FolderHash' => $sFolderHash,
'Uids' => $aResultUids 'Uids' => $aResultUids
))); )));
if ($this->oLogger) if ($this->oLogger) {
{
$this->oLogger->Write('Save Serialized UIDS to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']'); $this->oLogger->Write('Save Serialized UIDS to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']');
} }
} }

View file

@ -284,7 +284,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
\array_pop($aResponse); \array_pop($aResponse);
} }
$sScript = \implode("\n", $aResponse); $sScript = \implode("\r\n", $aResponse);
} }
return $sScript; return $sScript;
@ -296,6 +296,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/ */
public function PutScript(string $sScriptName, string $sScriptSource) : self public function PutScript(string $sScriptName, string $sScriptSource) : self
{ {
$sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource);
$this->sendRequest('PUTSCRIPT "'.$sScriptName.'" {'.\strlen($sScriptSource).'+}'); $this->sendRequest('PUTSCRIPT "'.$sScriptName.'" {'.\strlen($sScriptSource).'+}');
$this->sendRequestWithCheck($sScriptSource); $this->sendRequestWithCheck($sScriptSource);
@ -308,6 +309,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/ */
public function CheckScript(string $sScriptSource) : self public function CheckScript(string $sScriptSource) : self
{ {
$sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource);
$this->sendRequest('CHECKSCRIPT {'.\strlen($sScriptSource).'+}'); $this->sendRequest('CHECKSCRIPT {'.\strlen($sScriptSource).'+}');
$this->sendRequestWithCheck($sScriptSource); $this->sendRequestWithCheck($sScriptSource);

View file

@ -724,6 +724,7 @@ class Actions
'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false), 'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false),
'MessagesPerPage' => (int) $oConfig->Get('webmail', 'messages_per_page', 25), 'MessagesPerPage' => (int) $oConfig->Get('webmail', 'messages_per_page', 25),
'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5), 'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5),
'MsgDefaultAction' => 1,
'SoundNotification' => true, 'SoundNotification' => true,
'NotificationSound' => 'new-mail', 'NotificationSound' => 'new-mail',
'DesktopNotifications' => true, 'DesktopNotifications' => true,
@ -851,12 +852,18 @@ class Actions
} }
$aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType'])); $aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType']));
$aResult['requestReadReceipt'] = (bool) $oSettings->GetConf('requestReadReceipt', false);
$aResult['requestDsn'] = (bool) $oSettings->GetConf('requestDsn', false);
$aResult['pgpSign'] = (bool) $oSettings->GetConf('pgpSign', false);
$aResult['pgpEncrypt'] = (bool) $oSettings->GetConf('pgpEncrypt', false);
$aResult['ViewHTML'] = (bool)$oSettings->GetConf('ViewHTML', $aResult['ViewHTML']); $aResult['ViewHTML'] = (bool)$oSettings->GetConf('ViewHTML', $aResult['ViewHTML']);
$aResult['ShowImages'] = (bool)$oSettings->GetConf('ShowImages', $aResult['ShowImages']); $aResult['ShowImages'] = (bool)$oSettings->GetConf('ShowImages', $aResult['ShowImages']);
$aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']); $aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']);
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']); $aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
$aResult['MessagesPerPage'] = (int)$oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']); $aResult['MessagesPerPage'] = (int)$oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']);
$aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']); $aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']);
$aResult['MsgDefaultAction'] = (int)$oSettings->GetConf('MsgDefaultAction', $aResult['MsgDefaultAction']);
$aResult['SoundNotification'] = (bool)$oSettings->GetConf('SoundNotification', $aResult['SoundNotification']); $aResult['SoundNotification'] = (bool)$oSettings->GetConf('SoundNotification', $aResult['SoundNotification']);
$aResult['NotificationSound'] = (string)$oSettings->GetConf('NotificationSound', $aResult['NotificationSound']); $aResult['NotificationSound'] = (string)$oSettings->GetConf('NotificationSound', $aResult['NotificationSound']);
$aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']); $aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']);

View file

@ -39,25 +39,12 @@ trait Filters
$sName = $this->GetActionParam('name', ''); $sName = $this->GetActionParam('name', '');
$aFilters = array();
if (\RainLoop\Providers\Filters\SieveStorage::SIEVE_FILE_NAME === $sName) {
$aIncFilters = $this->GetActionParam('filters', array());
foreach ($aIncFilters as $aFilter) {
if (\is_array($aFilter)) {
$oFilter = new \RainLoop\Providers\Filters\Classes\Filter();
if ($oFilter->FromJSON($aFilter)) {
$aFilters[] = $oFilter;
}
}
}
}
if ($this->GetActionParam('active', false)) { if ($this->GetActionParam('active', false)) {
// $this->FiltersProvider()->ActivateScript($oAccount, $sName); // $this->FiltersProvider()->ActivateScript($oAccount, $sName);
} }
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save( return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save(
$oAccount, $sName, $aFilters, $this->GetActionParam('body', '') $oAccount, $sName, $this->GetActionParam('body', '')
)); ));
} }

Some files were not shown because too many files have changed in this diff Show more