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

@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.

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']);

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