Merge branch 'the-djmaze:master' into master

This commit is contained in:
cm-schl 2023-03-14 14:48:42 +01:00 committed by GitHub
commit 00a507ff97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
246 changed files with 4471 additions and 9935 deletions

View file

@ -316,5 +316,5 @@ dev_email = ""
dev_password = "" dev_password = ""
[version] [version]
current = "2.26.1" current = "2.26.4"
saved = "Sun, 18 Dec 2022 22:10:48 +0000" saved = "Sun, 18 Dec 2022 22:10:48 +0000"

View file

@ -26,7 +26,6 @@
# Header set Strict-Transport-Security "max-age=31536000" # Header set Strict-Transport-Security "max-age=31536000"
Header set imagetoolbar "no" Header set imagetoolbar "no"
# Header set X-Content-Type-Options "nosniff" # Header set X-Content-Type-Options "nosniff"
# Header set X-Frame-Options "DENY"
# Header set X-XSS-Protection "1; mode=block" # Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/" Header set Service-Worker-Allowed "/"

View file

@ -1,3 +1,104 @@
## 2.26.4 2023-02-24
## Added
- Add CSP frame-ancestors for
[#537](https://github.com/the-djmaze/snappymail/pull/537)
## Changed
- Reduce/simplify CSS footprint
- Use the System/Browser font by default by @HeySora
[#988](https://github.com/the-djmaze/snappymail/pull/988)
- Make layout fully responsive using matchMedia('(max-width: 799px)')
- Move brotli and gzip compress option to application.ini
- After page refreshes in background whilst editing a draft, the space bar stops working
[#860](https://github.com/the-djmaze/snappymail/pull/860)
- Updated Portuguese by @ner00
[#984](https://github.com/the-djmaze/snappymail/pull/984)
- Updated French by @hguilbert
[#985](https://github.com/the-djmaze/snappymail/pull/985)
## Fixed
- unset border-box for message body
[#990](https://github.com/the-djmaze/snappymail/pull/990)
- Unread email count badge shows -1
[#989](https://github.com/the-djmaze/snappymail/pull/989)
- unicode mailto: addresses not decoded
- Unicode email/url address matching
[#955](https://github.com/the-djmaze/snappymail/pull/955)
- Invalid Token if run in IFrame
[#537](https://github.com/the-djmaze/snappymail/pull/537)
### Removed
- rl.initData
## 2.26.3 2023-02-21
## Added
- Translate "Are you sure you want to exit?"
- stderr as Logger
## Changed
- Moved rl.fetch and rl.fetchJSON to boot.js so that AppData can be fetched as JSON
- Many AppData properties to JavaScript camelCase
- Cleanup Identity handling
- Merge GnuPG and OpenPGP.js passphrases
## Fixed
- Notification enum conflicts with window.Notification
- language selector didn't show current language in green
- Threads indicator got lost with new message cache handling
- messagesBodiesDom never cached previous messages
- Nextcloud File picker doesn't insert multiple links
[#981](https://github.com/the-djmaze/snappymail/pull/981)
- Call to protected method RainLoop\Enumerations\UploadError::getUserMessage()
[#982](https://github.com/the-djmaze/snappymail/pull/982)
- Issue with the top logo and text of a certain e-mail
[#953](https://github.com/the-djmaze/snappymail/pull/953)
- Facebook logo missing from emails sent out by it
[#954](https://github.com/the-djmaze/snappymail/pull/954)
### Removed
- Unused Squire code
## 2.26.2 2023-02-17
## Added
- Remove CSS white-space from messages to prevent annoying side scrolling
- Show error when trying to send empty message
[#974](https://github.com/the-djmaze/snappymail/pull/974)
- max_sys_loadavg as setting
[#971](https://github.com/the-djmaze/snappymail/pull/971)
## Changed
- Speedup and improved cleanHtml() and cleanCSS() handling
- Better handling of upload .eml files to a mailbox
- Rename Folder Hash to ETag as it is the more obvious name
- Moved AllowDraftAutosave option from Settings -> Security to Settings -> General -> Compose
- SmtpClient->Connect() use $oSettings->Ehlo
- Improved handling of `<style>` elements in messages
- French updated by @hguilbert
## Fixed
- Remove SVG elements properly from messages
[#972](https://github.com/the-djmaze/snappymail/pull/972)
- Sending large HTML messages slow/fail
[#962](https://github.com/the-djmaze/snappymail/pull/962)
- Nextcloud login failed
[#969](https://github.com/the-djmaze/snappymail/pull/969)
- DoMessageList() hash check was incorrect
- "Move to folder" button overlays folder sidebar on mobile
[#961](https://github.com/the-djmaze/snappymail/pull/961)
- Spanish translation SPAM and NOT SPAM strings are reversed
[#964](https://github.com/the-djmaze/snappymail/pull/964)
- Can't open Thunderbird PGP keys from decrypted message
[#958](https://github.com/the-djmaze/snappymail/pull/958)
- Can't close Sieve dialog
[#960](https://github.com/the-djmaze/snappymail/pull/960)
## 2.26.1 2023-02-14 ## 2.26.1 2023-02-14
## Added ## Added

View file

@ -141,28 +141,28 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.170.153 | 81.159 | |admin.js |2.170.153 | 78.971 |
|app.js |4.207.787 | 414.949 | |app.js |4.207.787 | 409.340 |
|boot.js | 868.735 | 2.711 | |boot.js | 868.735 | 4.142 |
|libs.js | 658.812 | 192.392 | |libs.js | 658.812 | 185.881 |
|sieve.js | 0 | 85.597 | |sieve.js | 0 | 85.599 |
|polyfills.js | 334.608 | 0 | |polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 | |serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 777.093 | |TOTAL |8.240.095 | 764.218 |
|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 | 256.831 | 40.198 | 73.606 | 13.281 | 60.877 | 11.916 | |admin.min.js | 256.831 | 38.936 | 73.606 | 13.036 | 60.877 | 11.694 |
|app.min.js | 515.367 | 190.317 |139.456 | 63.879 |110.485 | 54.792 | |app.min.js | 515.367 | 186.883 |139.456 | 63.102 |110.485 | 54.200 |
|boot.min.js | 84.659 | 1.600 | 26.998 | 940 | 23.643 | 767 | |boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|libs.min.js | 584.772 | 91.434 |180.901 | 33.893 |155.182 | 30.383 | |libs.min.js | 584.772 | 90.445 |180.901 | 33.592 |155.182 | 30.108 |
|sieve.min.js | 0 | 41.566 | 0 | 10.427 | 0 | 9.413 | |sieve.min.js | 0 | 41.525 | 0 | 10.435 | 0 | 9.401 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | |polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.217.635 | 283.351 |358.761 | 98.712 |299.485 | 85.942 | |TOTAL user |1.217.635 | 279.412 |358.761 | 97.896 |299.485 | 85.311 |
|TOTAL user+sieve |1.217.635 | 324.917 |358.761 |109.139 |299.485 | 95.355 | |TOTAL user+sieve |1.217.635 | 320.937 |358.761 |108.331 |299.485 | 94.712 |
|TOTAL admin | 959.099 | 133.232 |292.911 | 48.114 |249.877 | 43.066 | |TOTAL admin | 959.099 | 131.465 |292.911 | 47.830 |249.877 | 42.805 |
For a user its around 70% smaller and faster than traditional RainLoop. For a user it is around 72% smaller and faster than traditional RainLoop.
### CSS changes ### CSS changes
@ -189,12 +189,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.331 | 82.134 | 46.946 | 17.067 | 14.688 | |app.css | 340.331 | 81.526 | 46.946 | 16.960 | 14.620 |
|app.min.css | 274.947 | 66.337 | 39.647 | 15.095 | 13.285 | |app.min.css | 274.947 | 65.398 | 39.647 | 15.032 | 13.119 |
|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 | | 30.554 | | 6.984 | 6.085 | |admin.css | | 30.296 | | 6.939 | 6.066 |
|admin.min.css | | 24.752 | | 6.339 | 5.606 | |admin.min.css | | 24.420 | | 6.272 | 5.531 |
### PGP ### PGP
RainLoop uses the old OpenPGP.js v2 RainLoop uses the old OpenPGP.js v2
@ -208,7 +208,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |-------: |--------: |--------: | |--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|openpgp.min.js | 330.742 | 539.988 |102.388 | 167.371 | 84.241 | 137.413 | |openpgp.min.js | 330.742 | 541.176 |102.388 | 168.266 | 84.241 | 138.278 |
|openpgp.worker | 1.499 | | 824 | | 695 | | |openpgp.worker | 1.499 | | 824 | | 695 | |

View file

@ -4,16 +4,6 @@
//header('Strict-Transport-Security: max-age=31536000'); //header('Strict-Transport-Security: max-age=31536000');
/**
* Uncomment to use gzip compressed output
*/
//define('USE_GZIP', 1);
/**
* Uncomment to use brotli compressed output
*/
//define('USE_BROTLI', 1);
/** /**
* Uncomment to enable multiple domain installation. * Uncomment to enable multiple domain installation.
*/ */

View file

@ -160,11 +160,6 @@ $tar->addFromString('index.php', $index);
$zip->addFile('README.md'); $zip->addFile('README.md');
$tar->addFile('README.md'); $tar->addFile('README.md');
$data = file_get_contents('dev/serviceworker.js');
//$data = file_get_contents('snappymail/v/0.0.0/static/js/min/serviceworker.min.js');
$zip->addFromString('serviceworker.js', $data);
$tar->addFromString('serviceworker.js', $data);
$zip->close(); $zip->close();
$tar->compress(Phar::GZ); $tar->compress(Phar::GZ);

View file

@ -27,7 +27,7 @@ export class AdminApp extends AbstractApp {
rl.route.root(); rl.route.root();
setTimeout(() => location.href = '/', 1); setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) { } else if (SettingsGet('Auth')) {
this.weakPassword(SettingsGet('WeakPassword')); this.weakPassword(SettingsGet('weakPassword'));
startScreens([SettingsAdminScreen]); startScreens([SettingsAdminScreen]);
} else { } else {
startScreens([LoginAdminScreen]); startScreens([LoginAdminScreen]);

View file

@ -1,7 +1,7 @@
import 'External/User/ko'; import 'External/User/ko';
import { SMAudio } from 'Common/Audio'; import { SMAudio } from 'Common/Audio';
import { isArray, pString, pInt } from 'Common/Utils'; import { isArray, pInt } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser'; import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
import { import {
@ -156,22 +156,11 @@ export class AppUser extends AbstractApp {
? items.map(oValue => new AccountModel(oValue.email, oValue.name)) ? items.map(oValue => new AccountModel(oValue.email, oValue.name))
: [] : []
); );
AccountUserStore.unshift(new AccountModel(SettingsGet('MainEmail'), '', false)); AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
items = oData.Result.Identities; items = oData.Result.Identities;
IdentityUserStore(isArray(items) IdentityUserStore(isArray(items)
? items.map(identityData => { ? items.map(identityData => IdentityModel.reviveFromJson(identityData))
const identity = new IdentityModel(
pString(identityData.Id),
pString(identityData.Email)
);
identity.name(pString(identityData.Name));
identity.replyTo(pString(identityData.ReplyTo));
identity.bcc(pString(identityData.Bcc));
identity.signature(pString(identityData.Signature));
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
return identity;
})
: [] : []
); );
} }
@ -193,18 +182,17 @@ export class AppUser extends AbstractApp {
bootstart() { bootstart() {
super.bootstart(); super.bootstart();
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
addEventListener('beforeunload', event => { addEventListener('beforeunload', event => {
if (arePopupsVisible() || (ThemeStore.isMobile() && MessageUserStore.message())) { if (arePopupsVisible() || (!SettingsUserStore.layout() && MessageUserStore.message())) {
event.preventDefault(); event.preventDefault();
return event.returnValue = "Are you sure you want to exit?"; return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
} }
}, {capture: true}); }, {capture: true});
} }
refresh() { refresh() {
ThemeStore.populate(); ThemeStore.populate();
LanguageStore.language(SettingsGet('Language')); LanguageStore.language(SettingsGet('language'));
changeTheme(SettingsGet('Theme')); changeTheme(SettingsGet('Theme'));
this.start(); this.start();
} }
@ -263,18 +251,15 @@ export class AppUser extends AbstractApp {
PgpUserStore.init(); PgpUserStore.init();
// When auto-login is active setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
try {
navigator.registerProtocolHandler?.( // When auto-login is active
'mailto', navigator.registerProtocolHandler?.(
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s', 'mailto',
(SettingsGet('Title') || 'SnappyMail') location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
); (SettingsGet('title') || 'SnappyMail')
} catch (e) { );
console.error(e);
}
setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
} else { } else {
this.logout(); this.logout();
} }

View file

@ -1,9 +1,5 @@
import { MessageSetAction } from 'Common/EnumsUser'; let FOLDERS_CACHE = new Map,
import { isArray } from 'Common/Utils'; FOLDERS_HASH_MAP = new Map,
let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {},
MESSAGE_FLAGS_CACHE = {},
inboxFolderName = 'INBOX'; inboxFolderName = 'INBOX';
export const export const
@ -11,18 +7,10 @@ export const
* @returns {void} * @returns {void}
*/ */
clearCache = () => { clearCache = () => {
FOLDERS_CACHE = {}; FOLDERS_CACHE.clear();
FOLDERS_NAME_CACHE = {}; FOLDERS_HASH_MAP.clear();
MESSAGE_FLAGS_CACHE = {};
}, },
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {string}
*/
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
/** /**
* @returns {string} * @returns {string}
*/ */
@ -34,143 +22,35 @@ export const
setFolderInboxName = name => inboxFolderName = name, setFolderInboxName = name => inboxFolderName = name,
/** /**
* @param {string} folderHash * @param {string} fullNameHash
* @returns {string} * @returns {string}
*/ */
getFolderFullName = folderHash => (folderHash && FOLDERS_NAME_CACHE[folderHash]) || '', getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
/** /**
* @param {string} folderHash
* @param {string} folderFullName
* @param {?FolderModel} folder * @param {?FolderModel} folder
*/ */
setFolder = folder => { setFolder = folder => {
folder.hash = ''; folder.etag = '';
FOLDERS_CACHE[folder.fullName] = folder; FOLDERS_CACHE.set(folder.fullName, folder);
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName; FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
}, },
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @param {string} folderHash * @param {string} folderETag
*/ */
setFolderHash = (folderFullName, folderHash) => setFolderETag = (folderFullName, folderETag) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash), FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @returns {?FolderModel} * @returns {?FolderModel}
*/ */
getFolderFromCacheList = folderFullName => getFolderFromCacheList = folderFullName =>
FOLDERS_CACHE[folderFullName] || null, FOLDERS_CACHE.get(folderFullName),
/** /**
* @param {string} folderFullName * @param {string} folderFullName
*/ */
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName]; removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
export class MessageFlagsCache
{
/**
* @param {string} folderFullName
* @param {string} uid
* @param {string} flag
* @returns {bool}
*/
static hasFlag(folderFullName, uid, flag) {
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid]?.includes(flag);
}
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {?Array}
*/
static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid];
}
/**
* @param {string} folderFullName
* @param {string} uid
* @param {Array} flagsCache
*/
static setFor(folderFullName, uid, flags) {
if (isArray(flags)) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
}
}
/**
* @param {string} folderFullName
*/
static clearFolder(folderFullName) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
/**
* @param {(MessageModel|null)} message
*/
static initMessage(message) {
if (message) {
const uid = message.uid,
flags = this.getFor(message.folder, uid),
thread = message.threads();
isArray(flags) && message.flags(flags);
if (thread.length) {
message.hasUnseenSubMessage(!!thread.find(iSubUid =>
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
));
message.hasFlaggedSubMessage(!!thread.find(iSubUid =>
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
));
}
}
}
/**
* @param {(MessageModel|null)} message
*/
static store(message) {
message && this.setFor(message.folder, message.uid, message.flags());
}
/**
* @param {string} folder
* @param {string} uid
* @param {number} setAction
*/
static storeBySetAction(folder, uid, setAction) {
let flags = this.getFor(folder, uid) || [];
const
unread = flags.includes('\\seen') ? 0 : 1,
add = item => flags.includes(item) || flags.push(item),
remove = item => flags = flags.filter(flag => flag != item);
switch (setAction) {
case MessageSetAction.SetSeen:
add('\\seen');
break;
case MessageSetAction.UnsetSeen:
remove('\\seen');
break;
case MessageSetAction.SetFlag:
add('\\flagged');
break;
case MessageSetAction.UnsetFlag:
remove('\\flagged');
break;
// no default
}
this.setFor(folder, uid, flags);
return unread;
}
}

View file

@ -39,7 +39,7 @@ SaveSettingStatus = {
/** /**
* @enum {number} * @enum {number}
*/ */
Notification = { Notifications = {
RequestError: 1, RequestError: 1,
RequestAborted: 2, RequestAborted: 2,
RequestTimeout: 3, RequestTimeout: 3,

View file

@ -96,7 +96,7 @@ export const EditorDefaultType = {
* @enum {number} * @enum {number}
*/ */
export const Layout = { export const Layout = {
NoPreview: 0, // NoPreview: 0,
SidePreview: 1, SidePreview: 1,
BottomPreview: 2 BottomPreview: 2
}; };

View file

@ -1,7 +1,6 @@
import { isArray, arrayLength } from 'Common/Utils'; import { isArray, arrayLength } from 'Common/Utils';
import { import {
MessageFlagsCache, setFolderETag,
setFolderHash,
getFolderInboxName, getFolderInboxName,
getFolderFromCacheList getFolderFromCacheList
} from 'Common/Cache'; } from 'Common/Cache';
@ -9,8 +8,6 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { Settings } from 'Common/Globals';
import { serverRequest } from 'Common/Links';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
@ -114,8 +111,8 @@ folderInformation = (folder, list) => {
if (arrayLength(list)) { if (arrayLength(list)) {
list.forEach(messageListItem => { list.forEach(messageListItem => {
MessageFlagsCache.getFor(folder, messageListItem.uid) || uids.push(messageListItem.uid); uids.push(messageListItem.uid);
messageListItem.threads.forEach(uid => MessageFlagsCache.getFor(folder, uid) || uids.push(uid)); messageListItem.threads.forEach(uid => uids.push(uid));
}); });
count = uids.length; count = uids.length;
} }
@ -124,31 +121,21 @@ folderInformation = (folder, list) => {
Remote.request('FolderInformation', (iError, data) => { Remote.request('FolderInformation', (iError, data) => {
if (!iError && data.Result) { if (!iError && data.Result) {
const result = data.Result, const result = data.Result,
folderFromCache = getFolderFromCacheList(result.folder); folderFromCache = getFolderFromCacheList(result.name);
if (folderFromCache) { if (folderFromCache) {
const oldHash = folderFromCache.hash, const oldHash = folderFromCache.etag,
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails); unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
// folderFromCache.revivePropertiesFromJson(result); // folderFromCache.revivePropertiesFromJson(result);
folderFromCache.expires = Date.now(); folderFromCache.expires = Date.now();
folderFromCache.uidNext = result.uidNext; folderFromCache.uidNext = result.uidNext;
folderFromCache.hash = result.hash; folderFromCache.etag = result.etag;
folderFromCache.totalEmails(result.totalEmails); folderFromCache.totalEmails(result.totalEmails);
folderFromCache.unreadEmails(result.unreadEmails); folderFromCache.unreadEmails(result.unreadEmails);
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); MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
if (!oldHash || unreadCountChange || result.hash !== oldHash) { if (!oldHash || unreadCountChange || result.etag !== oldHash) {
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) { if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload(); MessagelistUserStore.reload();
/* /*
@ -165,8 +152,6 @@ folderInformation = (folder, list) => {
flagsUids: uids, flagsUids: uids,
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
}); });
} else if (SettingsUserStore.useThreads()) {
MessagelistUserStore.reloadFlagsAndCachedMessage();
} }
} }
}, },
@ -181,21 +166,18 @@ folderInformationMultiply = (boot = false) => {
if (!iError && arrayLength(oData.Result)) { if (!iError && arrayLength(oData.Result)) {
const utc = Date.now(); const utc = Date.now();
oData.Result.forEach(item => { oData.Result.forEach(item => {
const folder = getFolderFromCacheList(item.folder); const folder = getFolderFromCacheList(item.name);
if (folder) { if (folder) {
const oldHash = folder.hash, const oldHash = folder.etag,
unreadCountChange = folder.unreadEmails() !== item.unreadEmails; unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
// folder.revivePropertiesFromJson(item); // folder.revivePropertiesFromJson(item);
folder.expires = utc; folder.expires = utc;
folder.hash = item.hash; folder.etag = item.etag;
folder.totalEmails(item.totalEmails); folder.totalEmails(item.totalEmails);
folder.unreadEmails(item.unreadEmails); folder.unreadEmails(item.unreadEmails);
unreadCountChange && MessageFlagsCache.clearFolder(folder.fullName); if (!oldHash || item.etag !== oldHash) {
if (!oldHash || item.hash !== oldHash) {
if (folder.fullName === FolderUserStore.currentFolderFullName()) { if (folder.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload(); MessagelistUserStore.reload();
} }
@ -217,13 +199,13 @@ folderInformationMultiply = (boot = false) => {
moveOrDeleteResponseHelper = (iError, oData) => { moveOrDeleteResponseHelper = (iError, oData) => {
if (iError) { if (iError) {
setFolderHash(FolderUserStore.currentFolderFullName(), ''); setFolderETag(FolderUserStore.currentFolderFullName(), '');
alert(getNotification(iError)); alert(getNotification(iError));
} else if (FolderUserStore.currentFolder()) { } else if (FolderUserStore.currentFolder()) {
if (2 === arrayLength(oData.Result)) { if (2 === arrayLength(oData.Result)) {
setFolderHash(oData.Result[0], oData.Result[1]); setFolderETag(oData.Result[0], oData.Result[1]);
} else { } else {
setFolderHash(FolderUserStore.currentFolderFullName(), ''); setFolderETag(FolderUserStore.currentFolderFullName(), '');
} }
MessagelistUserStore.reload(!MessagelistUserStore.length); MessagelistUserStore.reload(!MessagelistUserStore.length);
} }
@ -286,28 +268,20 @@ moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) =>
}, },
dropFilesInFolder = (sFolderFullName, files) => { dropFilesInFolder = (sFolderFullName, files) => {
let count = 0, let count = files.length;
fn = () => 0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true);
for (const file of files) { for (const file of files) {
if ('message/rfc822' === file.type) { if ('message/rfc822' === file.type) {
++count;
let data = new FormData; let data = new FormData;
data.append('folder', sFolderFullName); data.append('folder', sFolderFullName);
data.append('appendFile', file); data.append('appendFile', file);
data.XToken = Settings.app('token'); Remote.request('FolderAppend', (iError, data)=>{
fetch(serverRequest('Append'), { iError && console.error(data.ErrorMessage);
method: 'POST', 0 == --count
mode: 'same-origin', && FolderUserStore.currentFolderFullName() == sFolderFullName
cache: 'no-cache', && MessagelistUserStore.reload(true, true);
redirect: 'error', }, data);
referrerPolicy: 'no-referrer', } else {
credentials: 'same-origin', --count;
body: data
})
.then(fn)
.catch(fn);
} }
} }
}; };

View file

@ -1,20 +1,19 @@
import { doc, elementById, $htmlCL } from 'Common/Globals'; import { doc, appEl, $htmlCL } from 'Common/Globals';
// Fullscreen must be on app, else other popups fail // Fullscreen must be on app, else other popups fail
export const export const
app = elementById('rl-app'), appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === app,
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc), exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
isFullscreen = ko.observable(false), isFullscreen = ko.observable(false),
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : app.requestFullscreen(); toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
if (app) { if (appEl) {
let event = 'fullscreenchange'; let event = 'fullscreenchange';
if (!app.requestFullscreen && app.webkitRequestFullscreen) { if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
app.requestFullscreen = app.webkitRequestFullscreen; appEl.requestFullscreen = appEl.webkitRequestFullscreen;
event = 'webkit'+event; event = 'webkit'+event;
} }
if (app.requestFullscreen) { if (appEl.requestFullscreen) {
doc.addEventListener(event, () => { doc.addEventListener(event, () => {
isFullscreen(appFullscreen()); isFullscreen(appFullscreen());
$htmlCL.toggle('rl-fullscreen', appFullscreen()); $htmlCL.toggle('rl-fullscreen', appFullscreen());

View file

@ -11,6 +11,8 @@ export const
elementById = id => doc.getElementById(id), elementById = id => doc.getElementById(id),
appEl = elementById('rl-app'),
Settings = rl.settings, Settings = rl.settings,
SettingsGet = Settings.get, SettingsGet = Settings.get,
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name], SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
@ -31,6 +33,11 @@ export const
new CustomEvent(name, {detail:detail, cancelable: !!cancelable}) new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
), ),
stopEvent = event => {
event.preventDefault();
event.stopPropagation();
},
formFieldFocused = () => doc.activeElement?.matches('input,textarea'), formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
addShortcut = (...args) => shortcuts.add(...args), addShortcut = (...args) => shortcuts.add(...args),

View file

@ -3,7 +3,7 @@ import { forEachObjectEntry, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
const const
tpl = createElement('template'), tmpl = createElement('template'),
htmlre = /[&<>"']/g, htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i, httpre = /^(https?:)?\/\//i,
htmlmap = { htmlmap = {
@ -14,10 +14,18 @@ const
"'": '&#x27;' "'": '&#x27;'
}, },
disallowedTags = [
'svg','script','title','link','base','meta',
'input','output','select','button','textarea',
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
// not supported by <template> element
// ,'html','head','body'
].join(','),
blockquoteSwitcher = () => { blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() && SettingsUserStore.collapseBlockquotes() &&
// tpl.content.querySelectorAll('blockquote').forEach(node => { // tmpl.content.querySelectorAll('blockquote').forEach(node => {
[...tpl.content.querySelectorAll('blockquote')].reverse().forEach(node => { [...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
const el = createElement('details', {class:'sm-bq-switcher'}); const el = createElement('details', {class:'sm-bq-switcher'});
el.innerHTML = '<summary>•••</summary>'; el.innerHTML = '<summary>•••</summary>';
node.replaceWith(el); node.replaceWith(el);
@ -27,11 +35,11 @@ const
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]), replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
url = /(^|\s|\n|\/?>)(https?:\/\/[-A-Z0-9+&#/%?=()~_|!:,.;]*[-A-Z0-9+&#/%=~()_|])/gi, urlRegExp = /https?:\/\/[^\p{C}\p{Z}]+[^\p{C}\p{Z}.]/gu,
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
email = /(^|\s|\n|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi, email = /(^|\r|\n|\p{C}\p{Z})((?:[^"(),.:;<>@[\]\\\p{C}\p{Z}]+(?:\.[^"(),.:;<>@[\]\\\p{C}\p{Z}]+)*|"(?:\\?[^"\\\p{C}\p{Z}])*")@[^@\p{C}\p{Z}]+[^@\p{C}\p{Z}.])/gui,
// rfc3966 // rfc3966
tel = /(^|\s|\n|\/?>)(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g, tel = /(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
// Strip tracking // Strip tracking
/** TODO: implement other url strippers like from /** TODO: implement other url strippers like from
@ -90,6 +98,13 @@ const
return url; return url;
}, },
cleanCSS = source =>
source.trim().replace(/-(ms|webkit)-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+(;|$)/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
,
/* /*
Parses given css string, and returns css object Parses given css string, and returns css object
keys as selectors and values are css rules keys as selectors and values are css rules
@ -161,7 +176,7 @@ const
// we have standard css // we have standard css
css.push({ css.push({
selector: selector, selector: selector,
rules: arr[6] rules: cleanCSS(arr[6])
}); });
} }
} }
@ -239,13 +254,6 @@ export const
// td // td
'colspan', 'rowspan', 'headers' 'colspan', 'rowspan', 'headers'
], ],
disallowedTags = [
'SVG','SCRIPT','TITLE','LINK','BASE','META',
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
// Not supported by <template> element
// ,'HTML','HEAD','BODY'
],
nonEmptyTags = [ nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG' 'A','B','EM','I','SPAN','STRONG'
]; ];
@ -256,9 +264,9 @@ export const
msgId = 0; msgId = 0;
} }
tpl.innerHTML = html tmpl.innerHTML = html
// Strip Microsoft comments // Strip Microsoft comments
.replace(/<!--\[if[\s\S]*?endif\]-->/gi, '') .replace(/<!--\[if[\s\S]*?-->/gi, '')
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>')) // .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
// Not supported by <template> element // Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '') // .replace(/<!doctype[^>]*>/gi, '')
@ -274,16 +282,17 @@ export const
// \MailSo\Base\HtmlUtils::ClearComments() // \MailSo\Base\HtmlUtils::ClearComments()
// https://github.com/the-djmaze/snappymail/issues/187 // https://github.com/the-djmaze/snappymail/issues/187
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT); const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
while (nodeIterator.nextNode()) { while (nodeIterator.nextNode()) {
nodeIterator.referenceNode.remove(); nodeIterator.referenceNode.remove();
} }
if (0 < bqLevel) { tmpl.content.querySelectorAll(
tpl.content.querySelectorAll(new Array(1 + bqLevel).fill('blockquote').join(' ')).forEach(node => node.remove()); disallowedTags
} + (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove());
tpl.content.querySelectorAll('*').forEach(oElement => { [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName, const name = oElement.tagName,
oStyle = oElement.style; oStyle = oElement.style;
@ -303,8 +312,7 @@ export const
} }
// \MailSo\Base\HtmlUtils::ClearTags() // \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name) if ('none' == oStyle.display
|| 'none' == oStyle.display
|| 'hidden' == oStyle.visibility || 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight) // || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight) // || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
@ -320,15 +328,16 @@ export const
hasAttribute = name => oElement.hasAttribute(name), hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '', getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value), setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => oElement.removeAttribute(name); delAttribute = name => {
let value = getAttribute(name);
oElement.removeAttribute(name);
return value;
};
if ('mail-body' === className) { if ('mail-body' === className) {
forEachObjectEntry(tasks, (name, cb) => { forEachObjectEntry(tasks, (name, cb) =>
if (hasAttribute(name)) { hasAttribute(name) && cb(delAttribute(name), oElement)
cb(getAttribute(name), oElement); );
delAttribute(name);
}
});
} else if (msgId && className) { } else if (msgId && className) {
oElement.className = className.replace(/(^|\s+)/g, '$1msg-'); oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
} }
@ -350,21 +359,21 @@ export const
if (!oStyle.backgroundImage) { if (!oStyle.backgroundImage) {
if ('TD' !== name && 'TH' !== name) { if ('TD' !== name && 'TH' !== name) {
// Make width responsive // Make width responsive
if (hasAttribute('width')) { if (hasAttribute('width') && !oStyle.width) {
value = getAttribute('width'); value = delAttribute('width');
oStyle.width = value.includes('%') ? value : value + 'px'; oStyle.width = value.includes('%') ? value : value + 'px';
delAttribute('width');
} }
value = oStyle.removeProperty('width'); value = oStyle.width;
if (parseInt(value,10) && !oStyle.maxWidth) { if (100 < parseInt(value,10) && !oStyle.maxWidth) {
oStyle.maxWidth = value; oStyle.maxWidth = value;
oStyle.width = '100%'; oStyle.width = '100%';
} else if (!value?.includes('%')) {
oStyle.removeProperty('width');
} }
// Make height responsive // Make height responsive
if (hasAttribute('height')) { if (hasAttribute('height')) {
value = getAttribute('height'); value = delAttribute('height');
oStyle.height = value.includes('%') ? value : value + 'px'; oStyle.height = value.includes('%') ? value : value + 'px';
delAttribute('height');
} }
value = oStyle.removeProperty('height'); value = oStyle.removeProperty('height');
if (value && !oStyle.maxHeight) { if (value && !oStyle.maxHeight) {
@ -402,8 +411,7 @@ export const
let skipStyle = false; let skipStyle = false;
if (hasAttribute('src')) { if (hasAttribute('src')) {
value = stripTracking(getAttribute('src')); value = stripTracking(delAttribute('src'));
delAttribute('src');
if ('IMG' === name) { if ('IMG' === name) {
oElement.loading = 'lazy'; oElement.loading = 'lazy';
@ -464,18 +472,15 @@ export const
} }
if (hasAttribute('background')) { if (hasAttribute('background')) {
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")'; oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
delAttribute('background');
} }
if (hasAttribute('bgcolor')) { if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = getAttribute('bgcolor'); oStyle.backgroundColor = delAttribute('bgcolor');
delAttribute('bgcolor');
} }
if (hasAttribute('color')) { if (hasAttribute('color')) {
oStyle.color = getAttribute('color'); oStyle.color = delAttribute('color');
delAttribute('color');
} }
if (!skipStyle) { if (!skipStyle) {
@ -540,8 +545,7 @@ export const
oStyle.removeProperty('color'); oStyle.removeProperty('color');
} }
// Drop Microsoft Office style properties oStyle.cssText = cleanCSS(oStyle.cssText);
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
} }
if (debug && aAttrsForRemove.length) { if (debug && aAttrsForRemove.length) {
@ -551,8 +555,8 @@ export const
blockquoteSwitcher(); blockquoteSwitcher();
// return tpl.content.firstChild; // return tmpl.content.firstChild;
result.html = tpl.innerHTML.trim(); result.html = tmpl.innerHTML.trim();
return result; return result;
}, },
@ -563,7 +567,7 @@ export const
htmlToPlain = html => { htmlToPlain = html => {
const const
hr = '⎯'.repeat(64), hr = '⎯'.repeat(64),
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn), forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => { blockquotes = node => {
let bq; let bq;
while ((bq = node.querySelector('blockquote'))) { while ((bq = node.querySelector('blockquote'))) {
@ -591,7 +595,7 @@ export const
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n'); html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
} }
tpl.innerHTML = html tmpl.innerHTML = html
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t') .replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n'); .replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
@ -641,16 +645,16 @@ export const
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`)); forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Convert line-breaks // Convert line-breaks
tpl.innerHTML = tpl.innerHTML tmpl.innerHTML = tmpl.innerHTML
.replace(/\n{3,}/gm, '\n\n') .replace(/\n{3,}/gm, '\n\n')
.replace(/\n<br[^>]*>/g, '\n') .replace(/\n<br[^>]*>/g, '\n')
.replace(/<br[^>]*>\n/g, '\n'); .replace(/<br[^>]*>\n/g, '\n');
forEach('br', br => br.replaceWith('\n')); forEach('br', br => br.replaceWith('\n'));
// Blockquotes must be last // Blockquotes must be last
blockquotes(tpl.content); blockquotes(tmpl.content);
return (tpl.content.textContent || '').trim(); return (tmpl.content.textContent || '').trim();
}, },
/** /**
@ -704,22 +708,22 @@ export const
aText = aNextText; aText = aNextText;
} while (bDo); } while (bDo);
tpl.innerHTML = aText.join('\n') tmpl.innerHTML = aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n') // .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(url, (...m) => { .replace(urlRegExp, (...m) => {
m[2] = stripTracking(m[2]); m[0] = stripTracking(m[0]);
return `${m[1]}<a href="${m[2]}" target="_blank">${m[2]}</a>`; return `<a href="${m[0]}" target="_blank">${m[0]}</a>`;
}) })
.replace(email, '$1<a href="mailto:$2">$2</a>') .replace(email, '$1<a href="mailto:$2">$2</a>')
.replace(tel, '$1<a href="$2">$2</a>') .replace(tel, '<a href="$1">$1</a>')
.replace(/~~~blockquote~~~\s*/g, '<blockquote>') .replace(/~~~blockquote~~~\s*/g, '<blockquote>')
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>') .replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>'); .replace(/\n/g, '<br>');
blockquoteSwitcher(); blockquoteSwitcher();
return tpl.innerHTML.trim(); return tmpl.innerHTML.trim();
}, },
WYSIWYGS = ko.observableArray(); WYSIWYGS = ko.observableArray();

View file

@ -31,7 +31,7 @@ export const
*/ */
serverRequestRaw = (type, hash) => serverRequestRaw = (type, hash) =>
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/' BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('AccountHash') ? + '0/' // Settings.get('accountHash') ?
+ (type + (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '') ? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: ''), : ''),

View file

@ -87,8 +87,7 @@ export class Selector {
* Below code is used to keep checked/focused/selected states when array is refreshed. * Below code is used to keep checked/focused/selected states when array is refreshed.
*/ */
let aCache = [], let aCheckedCache = [],
aCheckedCache = [],
mFocused = null, mFocused = null,
mSelected = null; mSelected = null;
@ -99,7 +98,6 @@ export class Selector {
items.forEach(item => { items.forEach(item => {
const uid = this.getItemUid(item); const uid = this.getItemUid(item);
if (uid) { if (uid) {
aCache.push(uid);
item.checked() && aCheckedCache.push(uid); item.checked() && aCheckedCache.push(uid);
if (!mFocused && item.focused()) { if (!mFocused && item.focused()) {
mFocused = uid; mFocused = uid;
@ -118,8 +116,7 @@ export class Selector {
koList.subscribe(aItems => { koList.subscribe(aItems => {
selectedItemUseCallback = false; selectedItemUseCallback = false;
koFocusedItem(null); this.unselect();
koSelectedItem(null);
if (isArray(aItems)) { if (isArray(aItems)) {
let temp, let temp,
@ -182,7 +179,6 @@ export class Selector {
} }
} }
aCache = [];
aCheckedCache = []; aCheckedCache = [];
mFocused = null; mFocused = null;
mSelected = null; mSelected = null;

View file

@ -1,5 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums'; import { Notifications, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links'; import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals'; import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils'; import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
@ -20,7 +20,7 @@ const
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(), i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
getNotificationMessage = code => { getNotificationMessage = code => {
let key = getKeyByValue(Notification, code); let key = getKeyByValue(Notifications, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : ''; return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
}, },
@ -176,7 +176,7 @@ export const
*/ */
getNotification = (code, message = '', defCode = 0) => { getNotification = (code, message = '', defCode = 0) => {
code = pInt(code); code = pInt(code);
if (Notification.ClientViewError === code && message) { if (Notifications.ClientViewError === code && message) {
return message; return message;
} }

View file

@ -17,12 +17,15 @@ export const
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled), item && undefined !== 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)))), b64Encode = data => btoa(unescape(encodeURIComponent(data))),
/* // Without deprecated 'unescape': /* // Without deprecated 'unescape':
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace( b64Encode = data => btoa(encodeURIComponent(data).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1) /%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)), )),
*/ */
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v); getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

View file

@ -1,11 +1,9 @@
import { MessageFlagsCache } from 'Common/Cache'; import { Notifications } from 'Common/Enums';
import { Notification } from 'Common/Enums';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser'; import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals'; import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { plainToHtml } from 'Common/Html'; import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { EmailCollectionModel } from 'Model/EmailCollection'; import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
@ -43,10 +41,11 @@ download = (link, name = "") => {
} }
}, },
downloadZip = (hashes, onError, fTrigger, folder) => { downloadZip = (name, hashes, onError, fTrigger, folder) => {
if (hashes.length) { if (hashes.length) {
let params = { let params = {
target: 'zip', target: 'zip',
filename: name,
hashes: hashes hashes: hashes
}; };
if (!onError) { if (!onError) {
@ -156,7 +155,7 @@ mailToHelper = mailToUrl => {
mailToUrl = mailToUrl.slice(7).split('?'); mailToUrl = mailToUrl.slice(7).split('?');
const const
email = mailToUrl[0], email = decodeURIComponent(mailToUrl[0]),
params = new URLSearchParams(mailToUrl[1]), params = new URLSearchParams(mailToUrl[1]),
to = params.get('to'), to = params.get('to'),
toEmailModel = value => EmailCollectionModel.fromString(value); toEmailModel = value => EmailCollectionModel.fromString(value);
@ -246,75 +245,78 @@ setLayoutResizer = (source, sClientSideKeyName, mode) =>
} }
}, },
viewMessage = (oMessage, popup) => {
if (popup) {
oMessage.viewPopupMessage();
} else {
MessageUserStore.error('');
let id = 'rl-msg-' + oMessage.hash,
body = oMessage.body || elementById(id);
if (!body) {
body = createElement('div',{
id:id,
hidden:'',
class:'b-text-part'
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
});
MessageUserStore.purgeCache();
}
body.message = oMessage;
oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain();
}
MessageUserStore.bodiesDom().append(body);
MessageUserStore.loading(false);
oMessage.body.hidden = false;
if (oMessage.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
}
},
populateMessageBody = (oMessage, popup) => { populateMessageBody = (oMessage, popup) => {
if (oMessage) { if (oMessage) {
popup || MessageUserStore.message(oMessage); popup || MessageUserStore.message(oMessage);
popup || MessageUserStore.loading(true); if (oMessage.body) {
Remote.message((iError, oData/*, bCached*/) => { viewMessage(oMessage, popup);
if (iError) { } else {
if (Notification.RequestAborted !== iError && !popup) { popup || MessageUserStore.loading(true);
MessageUserStore.message(null); Remote.message((iError, oData/*, bCached*/) => {
MessageUserStore.error(getNotification(iError)); if (iError) {
} if (Notifications.RequestAborted !== iError && !popup) {
} else { MessageUserStore.message(null);
let json = oData?.Result; MessageUserStore.error(getNotification(iError));
if (json }
&& MessageModel.validJson(json) } else {
&& oMessage.hash === json.hash let json = oData?.Result;
// && oMessage.folder === json.folder if (json
// && oMessage.uid == json.uid && oMessage.hash === json.hash
&& oMessage.revivePropertiesFromJson(json) // && oMessage.folder === json.folder
) { // && oMessage.uid == json.uid
&& oMessage.revivePropertiesFromJson(json)
) {
/* /*
if (bCached) { if (bCached) {
delete json.flags; delete json.flags;
}
*/
if (popup) {
oMessage.viewPopupMessage();
} else {
MessageUserStore.error('');
const messagesDom = MessageUserStore.bodiesDom();
if (messagesDom) {
let id = 'rl-msg-' + oMessage.hash,
body = elementById(id);
if (body) {
oMessage.body = body;
oMessage.isHtml(body.classList.contains('html'));
oMessage.hasImages(body.rlHasImages);
} else {
body = createElement('div',{
id:id,
hidden:'',
class:'b-text-part'
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
});
oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain();
}
MessageUserStore.purgeMessageBodyCache();
}
messagesDom.append(body);
oMessage.body.hidden = false;
} }
} oMessage.body.remove();
*/
MessageFlagsCache.initMessage(oMessage); viewMessage(oMessage, popup);
if (oMessage.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
} }
} }
} popup || MessageUserStore.loading(false);
popup || MessageUserStore.loading(false); }, oMessage.folder, oMessage.uid);
}, oMessage.folder, oMessage.uid); }
} }
}; };

View file

@ -27,11 +27,6 @@ const
SquireDefaultConfig = { SquireDefaultConfig = {
/* /*
blockTag: 'P',
undo: {
documentSizeThreshold: -1, // -1 means no threshold
undoLimit: -1 // -1 means no limit
},
addLinks: true // allow_smart_html_links addLinks: true // allow_smart_html_links
*/ */
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
@ -216,7 +211,7 @@ class SquireUI
cmd: () => { cmd: () => {
let node = squire.getSelectionClosest('IMG'), let node = squire.getSelectionClosest('IMG'),
src = prompt("Image", node?.src || "https://"); src = prompt("Image", node?.src || "https://");
src.length ? squire.insertImage(src) : (node && squire.detach(node)); src?.length ? squire.insertImage(src) : (node && squire.detach(node));
}, },
hint: 'Image URL' hint: 'Image URL'
}, },
@ -288,11 +283,6 @@ class SquireUI
toolbar.className = 'squire-toolbar btn-toolbar'; toolbar.className = 'squire-toolbar btn-toolbar';
let group, action/*, touchTap*/; let group, action/*, touchTap*/;
for (group in actions) { for (group in actions) {
/*
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
continue;
}
*/
let toolgroup = createElement('div'); let toolgroup = createElement('div');
toolgroup.className = 'btn-group'; toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group; toolgroup.id = 'squire-toolgroup-'+group;
@ -379,7 +369,6 @@ class SquireUI
squire.addEventListener('cursor', ); squire.addEventListener('cursor', );
squire.addEventListener('select', ); squire.addEventListener('select', );
squire.addEventListener('input', ); squire.addEventListener('input', );
squire.addEventListener('willPaste', );
squire.addEventListener( 'keydown keyup', monitorShiftKey ) squire.addEventListener( 'keydown keyup', monitorShiftKey )
squire.addEventListener( 'keydown', onKey ) squire.addEventListener( 'keydown', onKey )
*/ */

47
dev/External/ko.js vendored
View file

@ -28,6 +28,18 @@ export const
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(), dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
let fn = event => {
if (key == event.key) {
// stopEvent(event);
// element.dispatchEvent(new Event('change'));
fValueAccessor().call(model);
}
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
},
// 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);
@ -56,41 +68,18 @@ Object.assign(ko.bindingHandlers, {
}, },
onEnter: { onEnter: {
init: (element, fValueAccessor, fAllBindings, viewModel) => { init: (element, fValueAccessor, fAllBindings, model) =>
let fn = event => { onKey('Enter', element, fValueAccessor, fAllBindings, model)
if ('Enter' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
}
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
}
}, },
onEsc: { onEsc: {
init: (element, fValueAccessor, fAllBindings, viewModel) => { init: (element, fValueAccessor, fAllBindings, model) =>
let fn = event => { onKey('Escape', element, fValueAccessor, fAllBindings, model)
if ('Escape' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
}, },
onSpace: { onSpace: {
init: (element, fValueAccessor, fAllBindings, viewModel) => { init: (element, fValueAccessor, fAllBindings, model) =>
let fn = event => { onKey(' ', element, fValueAccessor, fAllBindings, model)
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
}, },
i18nUpdate: { i18nUpdate: {

View file

@ -4,6 +4,9 @@ import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Glob
import { forEachObjectEntry } from 'Common/Utils'; import { forEachObjectEntry } from 'Common/Utils';
import { i18nToNodes } from 'Common/Translator'; import { i18nToNodes } from 'Common/Translator';
import { leftPanelDisabled } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
let let
currentScreen = null, currentScreen = null,
defaultScreenName = ''; defaultScreenName = '';
@ -151,6 +154,7 @@ const
vm.onHide?.(); vm.onHide?.();
destroy && vm.viewModelDom.remove(); destroy && vm.viewModelDom.remove();
}); });
ThemeStore.isMobile() && leftPanelDisabled(true);
}, },
/** /**

View file

@ -1,3 +1,4 @@
//import { b64Encode } from 'Common/Utils';
const const
// RFC2045 // RFC2045
@ -68,6 +69,7 @@ export function ParseMime(text)
body = QPDecode(body); body = QPDecode(body);
} }
body = btoa(body); body = btoa(body);
// body = b64Encode(body);
} }
return 'data:' + this.headerValue('content-type') + ';base64,' + body; return 'data:' + this.headerValue('content-type') + ';base64,' + body;
} }

View file

@ -35,9 +35,7 @@ export function MimeToMessage(data, message)
attachment.url = part.dataUrl; attachment.url = part.dataUrl;
attachment.estimatedSize = part.body.length; attachment.estimatedSize = part.body.length;
/* /*
attachment.isThumbnail = false;
attachment.contentLocation = ''; attachment.contentLocation = '';
attachment.download = '';
attachment.folder = ''; attachment.folder = '';
attachment.uid = ''; attachment.uid = '';
attachment.mimeIndex = part.id; attachment.mimeIndex = part.id;

View file

@ -1,6 +1,8 @@
import ko from 'ko'; import ko from 'ko';
import { FileInfo, FileType } from 'Common/File'; import { FileInfo, FileType } from 'Common/File';
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
import { b64EncodeJSONSafe } from 'Common/Utils';
import { import {
attachmentDownload, attachmentDownload,
serverRequestRaw serverRequestRaw
@ -22,10 +24,8 @@ export class AttachmentModel extends AbstractModel {
this.fileName = ''; this.fileName = '';
this.fileNameExt = ''; this.fileNameExt = '';
this.fileType = FileType.Unknown; this.fileType = FileType.Unknown;
this.isThumbnail = false;
this.cId = ''; this.cId = '';
this.contentLocation = ''; this.contentLocation = '';
this.download = '';
this.folder = ''; this.folder = '';
this.uid = ''; this.uid = '';
this.url = ''; this.url = '';
@ -53,7 +53,7 @@ export class AttachmentModel extends AbstractModel {
} }
toggleChecked(self, event) { toggleChecked(self, event) {
event.stopPropagation(); stopEvent(event);
self.checked(!self.checked()); self.checked(!self.checked());
} }
@ -125,6 +125,17 @@ export class AttachmentModel extends AbstractModel {
); );
} }
get download() {
return b64EncodeJSONSafe({
folder: this.folder,
uid: this.uid,
mimeIndex: this.mimeIndex,
mimeType: this.mimeType,
fileName: this.fileName,
accountHash: SettingsGet('accountHash')
});
}
/** /**
* @returns {string} * @returns {string}
*/ */
@ -143,7 +154,7 @@ export class AttachmentModel extends AbstractModel {
* @returns {boolean} * @returns {boolean}
*/ */
hasThumbnail() { hasThumbnail() {
return this.isThumbnail && !this.isLinked(); return SettingsCapa('AttachmentThumbnails') && this.isImage() && !this.isLinked();
} }
/** /**

View file

@ -3,8 +3,9 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils'; import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser'; import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import { getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache'; import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
import { Settings, SettingsGet, fireEvent } from 'Common/Globals'; import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import { Notifications } from 'Common/Enums';
import * as Local from 'Storage/Client'; import * as Local from 'Storage/Client';
@ -14,7 +15,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { sortFolders } from 'Common/Folders'; import { sortFolders } from 'Common/Folders';
import { i18n, translateTrigger } from 'Common/Translator'; import { i18n, translateTrigger, getNotification } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
@ -91,6 +92,7 @@ export const
Remote.abort('Folders') Remote.abort('Folders')
.post('Folders', FolderUserStore.foldersLoading) .post('Folders', FolderUserStore.foldersLoading)
.then(data => { .then(data => {
clearCache();
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt(); FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
fCallback?.(true); fCallback?.(true);
// Repeat every 15 minutes? // Repeat every 15 minutes?
@ -127,8 +129,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
let oCacheFolder = getFolderFromCacheList(oFolder.fullName); let oCacheFolder = getFolderFromCacheList(oFolder.fullName);
if (oCacheFolder) { if (oCacheFolder) {
// oCacheFolder.revivePropertiesFromJson(oFolder); // oCacheFolder.revivePropertiesFromJson(oFolder);
if (oFolder.hash) { if (oFolder.etag) {
oCacheFolder.hash = oFolder.hash; oCacheFolder.etag = oFolder.etag;
} }
if (null != oFolder.totalEmails) { if (null != oFolder.totalEmails) {
oCacheFolder.totalEmails(oFolder.totalEmails); oCacheFolder.totalEmails(oFolder.totalEmails);
@ -167,22 +169,22 @@ export class FolderCollectionModel extends AbstractCollectionModel
break; break;
} }
// Flags // Flags
if (oFolder.flags.includes('\\sentmail')) { if (oFolder.attributes.includes('\\sentmail')) {
role = 'sent'; role = 'sent';
} }
if (oFolder.flags.includes('\\spam')) { if (oFolder.attributes.includes('\\spam')) {
role = 'junk'; role = 'junk';
} }
if (oFolder.flags.includes('\\bin')) { if (oFolder.attributes.includes('\\bin')) {
role = 'trash'; role = 'trash';
} }
if (oFolder.flags.includes('\\important')) { if (oFolder.attributes.includes('\\important')) {
role = 'important'; role = 'important';
} }
if (oFolder.flags.includes('\\starred')) { if (oFolder.attributes.includes('\\starred')) {
role = 'flagged'; role = 'flagged';
} }
if (oFolder.flags.includes('\\all') || oFolder.flags.includes('\\allmail')) { if (oFolder.attributes.includes('\\all') || oFolder.flags.includes('\\allmail')) {
role = 'all'; role = 'all';
} }
} }
@ -226,9 +228,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
name: name, name: name,
fullName: parentName, fullName: parentName,
delimiter: delimiter, delimiter: delimiter,
exists: false, attributes: ['\\nonexistent']
isSubscribed: false,
flags: ['\\nonexistent']
}); });
setFolder(pfolder); setFolder(pfolder);
result.splice(i, 0, pfolder); result.splice(i, 0, pfolder);
@ -300,7 +300,7 @@ export class FolderModel extends AbstractModel {
this.exists = true; this.exists = true;
this.hash = ''; this.etag = '';
this.id = 0; this.id = 0;
this.uidNext = 0; this.uidNext = 0;
@ -330,7 +330,8 @@ export class FolderModel extends AbstractModel {
tagsAllowed: false tagsAllowed: false
}); });
this.flags = ko.observableArray(); this.attributes = ko.observableArray();
// For messages
this.permanentFlags = ko.observableArray(); this.permanentFlags = ko.observableArray();
this.addSubscribables({ this.addSubscribables({
@ -379,6 +380,8 @@ export class FolderModel extends AbstractModel {
isFlagged: () => FolderUserStore.currentFolder() === this isFlagged: () => FolderUserStore.currentFolder() === this
&& MessagelistUserStore.listSearch().includes('flagged'), && MessagelistUserStore.listSearch().includes('flagged'),
// isSubscribed: () => this.attributes().includes('\\subscribed'),
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()), hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find( hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
@ -465,6 +468,49 @@ export class FolderModel extends AbstractModel {
}); });
} }
edit() {
this.canBeEdited() && this.editing(true);
}
unedit() {
this.editing(false);
}
rename() {
const folder = this,
nameToEdit = folder.nameForEdit().trim();
if (nameToEdit && folder.name() !== nameToEdit) {
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
folder: folder.fullName,
newFolderName: nameToEdit,
subscribe: folder.isSubscribed() ? 1 : 0
})
.then(data => {
folder.name(nameToEdit/*data.name*/);
if (folder.subFolders.length) {
Remote.setTrigger(FolderUserStore.foldersLoading, true);
// clearTimeout(Remote.foldersTimeout);
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
setTimeout(loadFolders, 500);
// TODO: rename all subfolders with folder.delimiter to prevent reload?
} else {
removeFolderFromCacheList(folder.fullName);
folder.fullName = data.Result.fullName;
setFolder(folder);
const parent = getFolderFromCacheList(folder.parentName);
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
}
})
.catch(error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notifications.CantRenameFolder)
+ '.\n' + error.message);
});
}
folder.editing(false);
}
/** /**
* For url safe '/#/mailbox/...' path * For url safe '/#/mailbox/...' path
*/ */
@ -491,6 +537,10 @@ export class FolderModel extends AbstractModel {
path.pop(); path.pop();
folder.parentName = path.join(folder.delimiter); folder.parentName = path.join(folder.delimiter);
folder.isSubscribed(folder.attributes.includes('\\subscribed'));
folder.exists = !folder.attributes.includes('\\nonexistent');
folder.selectable(folder.exists && !folder.attributes.includes('\\noselect'));
type && 'mail' != type && folder.kolabType(type); type && 'mail' != type && folder.kolabType(type);
} }
return folder; return folder;

View file

@ -6,12 +6,12 @@ export class IdentityModel extends AbstractModel {
* @param {string} id * @param {string} id
* @param {string} email * @param {string} email
*/ */
constructor(id, email) { constructor() {
super(); super();
addObservablesTo(this, { addObservablesTo(this, {
id: id || '', id: '',
email: email, email: '',
name: '', name: '',
replyTo: '', replyTo: '',

View file

@ -5,7 +5,7 @@ import { i18n } from 'Common/Translator';
import { doc, SettingsGet } from 'Common/Globals'; import { doc, SettingsGet } from 'Common/Globals';
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html'; import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
import { forEachObjectEntry } from 'Common/Utils'; import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { serverRequestRaw, proxy } from 'Common/Links'; import { serverRequestRaw, proxy } from 'Common/Links';
import { addObservablesTo, addComputablesTo } from 'External/ko'; import { addObservablesTo, addComputablesTo } from 'External/ko';
@ -21,7 +21,6 @@ import PreviewHTML from 'Html/PreviewMessage.html';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
//import { MessageFlagsCache } from 'Common/Cache';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
const const
@ -34,7 +33,6 @@ const
Remote.request('MessageSetKeyword', iError => { Remote.request('MessageSetKeyword', iError => {
if (!iError) { if (!iError) {
isSet ? flags.remove(lower) : flags.push(lower); isSet ? flags.remove(lower) : flags.push(lower);
// MessageFlagsCache.setFor(message.folder, message.uid, flags());
} }
}, { }, {
folder: message.folder, folder: message.folder,
@ -61,7 +59,6 @@ export class MessageModel extends AbstractModel {
this.folder = ''; this.folder = '';
this.uid = 0; this.uid = 0;
this.hash = ''; this.hash = '';
this.requestHash = '';
this.from = new EmailCollectionModel; this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel; this.to = new EmailCollectionModel;
this.cc = new EmailCollectionModel; this.cc = new EmailCollectionModel;
@ -88,7 +85,8 @@ export class MessageModel extends AbstractModel {
spamResult: '', spamResult: '',
isSpam: false, isSpam: false,
hasVirus: null, // or boolean when scanned hasVirus: null, // or boolean when scanned
dateTimeStampInUTC: 0, dateTimestamp: 0,
internalTimestamp: 0,
priority: MessagePriority.Normal, priority: MessagePriority.Normal,
senderEmailsString: '', senderEmailsString: '',
@ -180,6 +178,16 @@ export class MessageModel extends AbstractModel {
}); });
} }
get requestHash() {
return b64EncodeJSONSafe({
folder: this.folder,
uid: this.uid,
mimeType: 'message/rfc822',
fileName: (this.subject() || 'message-' + this.hash) + '.eml',
accountHash: SettingsGet('accountHash')
});
}
toggleTag(keyword) { toggleTag(keyword) {
toggleTag(this, keyword); toggleTag(this, keyword);
} }
@ -290,54 +298,51 @@ export class MessageModel extends AbstractModel {
return [[...toResult.values()], [...ccResult.values()]]; return [[...toResult.values()], [...ccResult.values()]];
} }
viewHtml() { viewBody(html) {
const body = this.body; const body = this.body;
if (body && this.html()) { if (body) {
let result = msgHtml(this); if (html) {
this.hasExternals(result.hasExternals); let result = msgHtml(this);
this.hasImages(body.rlHasImages = !!result.hasExternals); this.hasExternals(result.hasExternals);
this.hasImages(!!result.hasExternals);
body.innerHTML = result.html; body.innerHTML = result.html;
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
body.classList.toggle('html', 1); if ('always' === SettingsUserStore.viewImages()) {
body.classList.toggle('plain', 0); this.showExternalImages();
}
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) { if ('match' === SettingsUserStore.viewImages()) {
if ('always' === SettingsUserStore.viewImages()) { this.showExternalImages(1);
this.showExternalImages(); }
}
if ('match' === SettingsUserStore.viewImages()) {
this.showExternalImages(1);
} }
} else {
body.innerHTML = plainToHtml(
(this.plain()
? this.plain()
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[^\r\n]+)+|SIGNATURE-----[\s\S]*)/sg, '')
.trim()
: htmlToPlain(body.innerHTML)
)
);
this.hasImages(false);
} }
body.classList.toggle('html', html);
this.isHtml(true); body.classList.toggle('plain', !html);
this.isHtml(html);
return true; return true;
} }
} }
viewHtml() {
return this.html() && this.viewBody(true);
}
viewPlain() { viewPlain() {
const body = this.body; return this.viewBody(false);
if (body) {
body.classList.toggle('html', 0);
body.classList.toggle('plain', 1);
body.innerHTML = plainToHtml(
(this.plain()
? this.plain()
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
.trim()
: htmlToPlain(body.innerHTML)
)
);
this.isHtml(false);
this.hasImages(false);
return true;
}
} }
viewPopupMessage(print) { viewPopupMessage(print) {
const const
timeStampInUTC = this.dateTimeStampInUTC() || 0, timeStampInUTC = this.dateTimestamp() || 0,
ccLine = this.cc.toString(), ccLine = this.cc.toString(),
bccLine = this.bcc.toString(), bccLine = this.bcc.toString(),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null, m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
@ -430,7 +435,7 @@ export class MessageModel extends AbstractModel {
hasImages = true; hasImages = true;
}, },
attr = 'data-x-src', attr = 'data-x-src',
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages'); src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => { body.querySelectorAll('img[' + attr + ']').forEach(node => {
src = node.getAttribute(attr); src = node.getAttribute(attr);
if (isValid(src)) { if (isValid(src)) {
@ -447,7 +452,6 @@ export class MessageModel extends AbstractModel {
}); });
this.hasImages(hasImages); this.hasImages(hasImages);
body.rlHasImages = hasImages;
} }
} }

View file

@ -2,10 +2,6 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { MessageModel } from 'Model/Message'; import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import {
MessageFlagsCache
} from 'Common/Cache';
'use strict'; 'use strict';
export class MessageCollectionModel extends AbstractCollectionModel export class MessageCollectionModel extends AbstractCollectionModel
@ -13,17 +9,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
/* /*
constructor() { constructor() {
super(); super();
this.Filtered this.filtered
this.folder this.folder
this.folderHash
this.folderInfo
this.totalEmails this.totalEmails
this.unreadEmails this.totalThreads
this.threadUid this.threadUid
this.newMessages this.newMessages
this.offset this.offset
this.limit this.limit
this.search this.search
this.limited
} }
*/ */
@ -31,7 +26,7 @@ export class MessageCollectionModel extends AbstractCollectionModel
* @param {?Object} json * @param {?Object} json
* @returns {MessageCollectionModel} * @returns {MessageCollectionModel}
*/ */
static reviveFromJson(object, cached) { static reviveFromJson(object/*, cached*/) {
let msg = MessageUserStore.message(); let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => { return super.reviveFromJson(object, message => {
// If message is currently viewed, use that. // If message is currently viewed, use that.
@ -39,7 +34,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message); message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
if (message) { if (message) {
message.deleted(false); message.deleted(false);
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);
return message; return message;
} }
}); });

View file

@ -1,4 +1,4 @@
import { Notification } from 'Common/Enums'; import { Notifications } from 'Common/Enums';
import { isArray, pInt, pString } from 'Common/Utils'; import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links'; import { serverRequest } from 'Common/Links';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
@ -9,18 +9,18 @@ const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => { checkResponseError = data => {
const err = data ? data.ErrorCode : null; const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err) { if (Notifications.InvalidToken === err) {
console.error(getNotification(err)); console.error(getNotification(err));
// alert(getNotification(err)); // alert(getNotification(err));
rl.logoutReload(); rl.logoutReload();
} else if ([ } else if ([
Notification.AuthError, Notifications.AuthError,
Notification.ConnectionError, Notifications.ConnectionError,
Notification.DomainNotAllowed, Notifications.DomainNotAllowed,
Notification.AccountNotAllowed, Notifications.AccountNotAllowed,
Notification.MailServerError, Notifications.MailServerError,
Notification.UnknownNotification, Notifications.UnknownNotification,
Notification.UnknownError Notifications.UnknownError
].includes(err) ].includes(err)
) { ) {
if (7 < ++iJsonErrorCount) { if (7 < ++iJsonErrorCount) {
@ -65,7 +65,7 @@ class FetchError extends Error
{ {
constructor(code, message) { constructor(code, message) {
super(message); super(message);
this.code = code || Notification.JsonFalse; this.code = code || Notifications.JsonFalse;
} }
} }
@ -145,7 +145,7 @@ export class AbstractFetchRemote
iJsonErrorCount = 0; iJsonErrorCount = 0;
} else { } else {
checkResponseError(data); checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError iError = data.ErrorCode || Notifications.UnknownError
} }
} }
@ -197,7 +197,7 @@ export class AbstractFetchRemote
abort(action, 0, 1); abort(action, 0, 1);
if (!data) { if (!data) {
return Promise.reject(new FetchError(Notification.JsonParse)); return Promise.reject(new FetchError(Notifications.JsonParse));
} }
/* /*
let isCached = false, type = ''; let isCached = false, type = '';

View file

@ -24,7 +24,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
const const
// folder = getFolderFromCacheList(params.folder.fullName), // folder = getFolderFromCacheList(params.folder.fullName),
folder = getFolderFromCacheList(params.folder), folder = getFolderFromCacheList(params.folder),
folderHash = folder?.hash || ''; folderETag = folder?.etag || '';
params = Object.assign({ params = Object.assign({
offset: 0, offset: 0,
@ -40,8 +40,8 @@ class RemoteUserFetch extends AbstractFetchRemote {
} }
let sGetAdd = ''; let sGetAdd = '';
if (folderHash) { if (folderETag) {
params.hash = folderHash + '-' + SettingsGet('AccountHash'); params.hash = folderETag + '-' + SettingsGet('accountHash');
sGetAdd = 'MessageList/' + SUB_QUERY_PREFIX + '/' + b64EncodeJSONSafe(params); sGetAdd = 'MessageList/' + SUB_QUERY_PREFIX + '/' + b64EncodeJSONSafe(params);
params = {}; params = {};
} }
@ -77,7 +77,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
sFolderFullName, sFolderFullName,
iUid, iUid,
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0, AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
SettingsGet('AccountHash') SettingsGet('accountHash')
]) ])
); );

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { pString } from 'Common/Utils'; import { pString } from 'Common/Utils';
import { settings } from 'Common/Links'; import { settings } from 'Common/Links';
import { createElement, elementById } from 'Common/Globals'; import { createElement/*, elementById*/ } from 'Common/Globals';
import { AbstractScreen } from 'Knoin/AbstractScreen'; import { AbstractScreen } from 'Knoin/AbstractScreen';
import { i18nToNodes } from 'Common/Translator'; import { i18nToNodes } from 'Common/Translator';
@ -29,7 +29,8 @@ export class AbstractSettingsScreen extends AbstractScreen {
); );
if (RoutedSettingsViewModel) { if (RoutedSettingsViewModel) {
const vmPlace = elementById('rl-settings-subscreen'); // const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
const vmPlace = this.viewModels[1].__dom;
if (RoutedSettingsViewModel.__vm) { if (RoutedSettingsViewModel.__vm) {
settingsScreen = RoutedSettingsViewModel.__vm; settingsScreen = RoutedSettingsViewModel.__vm;
} else if (vmPlace) { } else if (vmPlace) {

View file

@ -1,15 +1,14 @@
import { Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { doc, createElement, leftPanelDisabled, Settings } from 'Common/Globals'; import { doc, createElement, Settings } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils'; import { pString, pInt } from 'Common/Utils';
import { moveAction } from 'Common/UtilsUser'; import { moveAction } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache'; import { getFolderFromHashMap, getFolderInboxName } from 'Common/Cache';
import { i18n, initOnStartOrLangChange } from 'Common/Translator'; import { i18n, initOnStartOrLangChange } from 'Common/Translator';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import { SystemDropDownUserView } from 'View/User/SystemDropDown'; import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { MailFolderList } from 'View/User/MailBox/FolderList'; import { MailFolderList } from 'View/User/MailBox/FolderList';
@ -57,11 +56,8 @@ export class MailBoxUserScreen extends AbstractScreen {
*/ */
onShow() { onShow() {
this.updateWindowTitle(); this.updateWindowTitle();
AppUserStore.focusedState('none'); AppUserStore.focusedState('none');
AppUserStore.focusedState(Scope.MessageList); AppUserStore.focusedState(Scope.MessageList);
ThemeStore.isMobile() && leftPanelDisabled(true);
} }
/** /**
@ -71,7 +67,7 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void} * @returns {void}
*/ */
onRoute(folderHash, page, search, messageUid) { onRoute(folderHash, page, search, messageUid) {
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, ''))); const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
if (folder) { if (folder) {
FolderUserStore.currentFolder(folder); FolderUserStore.currentFolder(folder);
MessagelistUserStore.page(1 > page ? 1 : page); MessagelistUserStore.page(1 > page ? 1 : page);

View file

@ -1,11 +1,10 @@
import { Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals'; import { keyScope, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins'; import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator'; import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings'; import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
@ -23,7 +22,7 @@ import { SettingsPaneUserView } from 'View/User/Settings/Pane';
export class SettingsUserScreen extends AbstractSettingsScreen { export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() { constructor() {
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]); super([SettingsMenuUserView, SettingsPaneUserView, SystemDropDownUserView]);
const views = [ const views = [
UserSettingsGeneral UserSettingsGeneral
@ -67,7 +66,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
onShow() { onShow() {
this.setSettingsTitle(); this.setSettingsTitle();
keyScope(Scope.Settings); keyScope(Scope.Settings);
ThemeStore.isMobile() && leftPanelDisabled(true);
} }
setSettingsTitle() { setSettingsTitle() {

View file

@ -16,6 +16,9 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
coreWarning: false, coreWarning: false,
coreVersion: '', coreVersion: '',
coreVersionCompare: -2, coreVersionCompare: -2,
load1: 0,
load5: 0,
load15: 0,
errorDesc: '' errorDesc: ''
}); });
this.coreChecking = ko.observable(false).extend({ throttle: 100 }); this.coreChecking = ko.observable(false).extend({ throttle: 100 });
@ -51,18 +54,21 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
} }
onBuild() { onBuild() {
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
// beforeShow() { // beforeShow() {
this.coreChecking(true); this.coreChecking(true);
Remote.request('AdminUpdateInfo', (iError, data) => { Remote.request('AdminInfo', (iError, data) => {
this.coreChecking(false); this.coreChecking(false);
if (!iError && data?.Result) { data = data?.Result;
if (!iError && data) {
this.load1(data.system.load?.[0]);
this.load5(data.system.load?.[1]);
this.load15(data.system.load?.[2]);
this.phpextensions(data.php);
this.coreReal(true); this.coreReal(true);
this.coreUpdatable(!!data.Result.updatable); this.coreUpdatable(!!data.core.updatable);
this.coreWarning(!!data.Result.warning); this.coreWarning(!!data.core.warning);
this.coreVersion(data.Result.version || ''); this.coreVersion(data.core.version || '');
this.coreVersionCompare(data.Result.versionCompare); this.coreVersionCompare(data.core.versionCompare);
} else { } else {
this.coreReal(false); this.coreReal(false);
this.coreWarning(false); this.coreWarning(false);

View file

@ -3,8 +3,8 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class AdminSettingsBranding extends AbstractViewSettings { export class AdminSettingsBranding extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
this.addSetting('Title'); this.addSetting('title');
this.addSetting('LoadingDescription'); this.addSetting('loadingDescription');
this.addSetting('FaviconUrl'); this.addSetting('faviconUrl');
} }
} }

View file

@ -13,16 +13,16 @@ export class AdminSettingsContacts extends AbstractViewSettings {
super(); super();
this.defaultOptionsAfterRender = defaultOptionsAfterRender; this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.addSetting('ContactsPdoDsn'); this.addSetting('contactsPdoDsn');
this.addSetting('ContactsPdoUser'); this.addSetting('contactsPdoUser');
this.addSetting('ContactsPdoPassword'); this.addSetting('contactsPdoPassword');
this.addSetting('ContactsPdoType', () => { this.addSetting('contactsPdoType', () => {
this.testContactsSuccess(false); this.testContactsSuccess(false);
this.testContactsError(false); this.testContactsError(false);
this.testContactsErrorMessage(''); this.testContactsErrorMessage('');
}); });
this.addSettings(['ContactsEnable','ContactsSync']); this.addSettings(['contactsEnable','contactsSync']);
addObservablesTo(this, { addObservablesTo(this, {
testing: false, testing: false,
@ -31,7 +31,7 @@ export class AdminSettingsContacts extends AbstractViewSettings {
testContactsErrorMessage: '' testContactsErrorMessage: ''
}); });
this.addSetting('ContactsSuggestionsLimit'); this.addSetting('contactsSuggestionsLimit');
const supportedTypes = SettingsGet('supportedPdoDrivers') || [], const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
types = [{ types = [{
@ -92,10 +92,10 @@ export class AdminSettingsContacts extends AbstractViewSettings {
this.testing(false); this.testing(false);
}, { }, {
ContactsPdoType: this.contactsPdoType(), contactsPdoType: this.contactsPdoType(),
ContactsPdoDsn: this.contactsPdoDsn(), contactsPdoDsn: this.contactsPdoDsn(),
ContactsPdoUser: this.contactsPdoUser(), contactsPdoUser: this.contactsPdoUser(),
ContactsPdoPassword: this.contactsPdoPassword() contactsPdoPassword: this.contactsPdoPassword()
} }
); );
} }

View file

@ -28,12 +28,12 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
const aLanguagesAdmin = Settings.app('languagesAdmin'); const aLanguagesAdmin = Settings.app('languagesAdmin');
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []); this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
this.languageAdmin = ko.observable(SettingsGet('LanguageAdmin')); this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
this.theme = ThemeStore.theme; this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes; this.themes = ThemeStore.themes;
this.addSettings(['AllowLanguagesOnSettings']); this.addSettings(['allowLanguagesOnSettings']);
addObservablesTo(this, { addObservablesTo(this, {
capaThemes: SettingsCapa('Themes'), capaThemes: SettingsCapa('Themes'),
@ -53,14 +53,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
*/ */
this.attachmentLimit = ko this.attachmentLimit = ko
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024)) .observable(SettingsGet('attachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 }); .extend({ debounce: 500 });
this.addSetting('Language'); this.addSetting('language');
this.addSetting('AttachmentLimit'); this.addSetting('attachmentLimit');
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger)); this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
this.uploadData = SettingsGet('PhpUploadSizes'); this.uploadData = SettingsGet('phpUploadSizes');
this.uploadDataDesc = this.uploadDataDesc =
(this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size) (this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
? [ ? [
@ -91,7 +91,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
this.languageAdminTrigger(SaveSettingStatus.Saving); this.languageAdminTrigger(SaveSettingStatus.Saving);
translatorReload(value, 1) translatorReload(value, 1)
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed)) .then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
.then(() => Remote.saveSetting('LanguageAdmin', value)); .then(() => Remote.saveSetting('languageAdmin', value));
}, },
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'), capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
@ -114,7 +114,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
showScreenPopup(LanguagesPopupView, [ showScreenPopup(LanguagesPopupView, [
this.languageAdmin, this.languageAdmin,
this.languagesAdmin(), this.languagesAdmin(),
SettingsGet('UserLanguageAdmin') SettingsGet('languageUsers')
]); ]);
} }
} }

View file

@ -3,7 +3,7 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class AdminSettingsLogin extends AbstractViewSettings { export class AdminSettingsLogin extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
this.addSetting('LoginDefaultDomain'); this.addSetting('loginDefaultDomain');
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin']); this.addSettings(['determineUserLanguage','determineUserDomain','allowLanguagesOnLogin']);
} }
} }

View file

@ -1,6 +1,6 @@
import ko from 'ko'; import ko from 'ko';
import { Notification } from 'Common/Enums'; import { Notifications } from 'Common/Enums';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { PackageAdminStore } from 'Stores/Admin/Package'; import { PackageAdminStore } from 'Stores/Admin/Package';
@ -15,7 +15,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
this.addSettings(['EnabledPlugins']); this.addSettings(['pluginsEnable']);
addObservablesTo(this, { addObservablesTo(this, {
packagesError: '' packagesError: ''
@ -67,7 +67,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) { if (iError) {
this.packagesError( this.packagesError(
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage) getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '') + (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
); );
} else if (data.Result.Reload) { } else if (data.Result.Reload) {
@ -113,7 +113,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) { if (iError) {
plugin.enabled(disable); plugin.enabled(disable);
this.packagesError( this.packagesError(
(Notification.UnsupportedPluginPackage === iError && data?.ErrorMessage) (Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
? data.ErrorMessage ? data.ErrorMessage
: getNotification(iError) : getNotification(iError)
); );

View file

@ -10,12 +10,12 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
this.addSettings(['UseLocalProxyForExternalImages']); this.addSettings(['useLocalProxyForExternalImages']);
this.weakPassword = rl.app.weakPassword; this.weakPassword = rl.app.weakPassword;
addObservablesTo(this, { addObservablesTo(this, {
adminLogin: SettingsGet('AdminLogin'), adminLogin: SettingsGet('adminLogin'),
adminLoginError: false, adminLoginError: false,
adminPassword: '', adminPassword: '',
adminPasswordNew: '', adminPasswordNew: '',
@ -68,7 +68,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value) capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
}); });
this.adminTOTP(SettingsGet('AdminTOTP')); this.adminTOTP(SettingsGet('adminTOTP'));
decorateKoCommands(this, { decorateKoCommands(this, {
saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword() saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()

View file

@ -21,7 +21,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
this.accounts = AccountUserStore; this.accounts = AccountUserStore;
this.loading = AccountUserStore.loading; this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore; this.identities = IdentityUserStore;
this.mainEmail = SettingsGet('MainEmail'); this.mainEmail = SettingsGet('mainEmail');
this.accountForDeletion = ko.observable(null).askDeleteHelper(); this.accountForDeletion = ko.observable(null).askDeleteHelper();
this.identityForDeletion = ko.observable(null).askDeleteHelper(); this.identityForDeletion = ko.observable(null).askDeleteHelper();
@ -88,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
accountsAndIdentitiesAfterMove() { accountsAndIdentitiesAfterMove() {
Remote.request('AccountsAndIdentitiesSortOrder', null, { Remote.request('AccountsAndIdentitiesSortOrder', null, {
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')), Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
Identities: IdentityUserStore.map(item => (item ? item.id() : "")) Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
}); });
} }

View file

@ -1,12 +1,11 @@
import ko from 'ko'; import ko from 'ko';
import { Notification } from 'Common/Enums'; import { Notifications } from 'Common/Enums';
import { FolderMetadataKeys } from 'Common/EnumsUser'; import { FolderMetadataKeys } from 'Common/EnumsUser';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache'; import { getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
import { defaultOptionsAfterRender } from 'Common/Utils'; import { defaultOptionsAfterRender } from 'Common/Utils';
import { sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n } from 'Common/Translator'; import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
@ -18,7 +17,6 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate'; import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem'; import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { loadFolders } from 'Model/FolderCollection';
const folderForDeletion = ko.observable(null).askDeleteHelper(); const folderForDeletion = ko.observable(null).askDeleteHelper();
@ -52,52 +50,10 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
this.folderForDeletion = folderForDeletion; this.folderForDeletion = folderForDeletion;
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'editing'] });
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value)); SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
SettingsUserStore.unhideKolabFolders.subscribe(value => Remote.saveSetting('UnhideKolabFolders', value)); SettingsUserStore.unhideKolabFolders.subscribe(value => Remote.saveSetting('UnhideKolabFolders', value));
} }
folderEditOnEnter(folder) {
const nameToEdit = folder?.nameForEdit().trim();
if (nameToEdit && folder.name() !== nameToEdit) {
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
folder: folder.fullName,
newFolderName: nameToEdit,
subscribe: folder.isSubscribed() ? 1 : 0
})
.then(data => {
folder.name(nameToEdit/*data.name*/);
if (folder.subFolders.length) {
Remote.setTrigger(FolderUserStore.foldersLoading, true);
// clearTimeout(Remote.foldersTimeout);
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
setTimeout(loadFolders, 500);
// TODO: rename all subfolders with folder.delimiter to prevent reload?
} else {
removeFolderFromCacheList(folder.fullName);
folder.fullName = data.Result.fullName;
setFolder(folder);
const parent = getFolderFromCacheList(folder.parentName);
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
}
})
.catch(error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantRenameFolder)
+ '.\n' + error.message);
});
}
// this.folderForEdit(null);
folder.editing(false);
}
folderEditOnEsc(folder) {
// this.folderForEdit(null);
folder?.editing(false);
}
onShow() { onShow() {
FolderUserStore.folderListError(''); FolderUserStore.folderListError('');
} }
@ -119,8 +75,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
&& folderToRemove.askDelete() && folderToRemove.askDelete()
) { ) {
if (0 < folderToRemove.totalEmails()) { if (0 < folderToRemove.totalEmails()) {
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder)); // FolderUserStore.folderListError(getNotification(Notifications.CantDeleteNonEmptyFolder));
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder)); folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
} else { } else {
folderForDeletion(null); folderForDeletion(null);
@ -129,7 +85,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
folder: folderToRemove.fullName folder: folderToRemove.fullName
}).then( }).then(
() => { () => {
// folderToRemove.flags.push('\\nonexistent'); // folderToRemove.attributes.push('\\nonexistent');
folderToRemove.selectable(false); folderToRemove.selectable(false);
// folderToRemove.isSubscribed(false); // folderToRemove.isSubscribed(false);
// folderToRemove.checkable(false); // folderToRemove.checkable(false);
@ -141,7 +97,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
}, },
error => { error => {
FolderUserStore.folderListError( FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantDeleteFolder) getNotification(error.code, '', Notifications.CantDeleteFolder)
+ '.\n' + error.message + '.\n' + error.message
); );
} }

View file

@ -17,7 +17,6 @@ import { LanguageStore } from 'Stores/Language';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity'; import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification'; import { NotificationUserStore } from 'Stores/User/Notification';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
@ -35,7 +34,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.soundNotification = SMAudio.notifications; this.soundNotification = SMAudio.notifications;
this.notificationSound = ko.observable(SettingsGet('NotificationSound')); this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds')); this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
this.desktopNotification = NotificationUserStore.enabled; this.desktopNotification = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed; this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
@ -44,12 +43,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', ['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel', 'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck' 'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck',
'showNextMessage'
].forEach(name => this[name] = SettingsUserStore[name]); ].forEach(name => this[name] = SettingsUserStore[name]);
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings'); this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
this.languageTrigger = ko.observable(SaveSettingStatus.Idle); this.languageTrigger = ko.observable(SaveSettingStatus.Idle);
@ -87,7 +87,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
layoutTypes: () => { layoutTypes: () => {
translateTrigger(); translateTrigger();
return [ return [
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') }, { id: 0, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') }, { id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') },
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') } { id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
]; ];
@ -102,10 +102,10 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('Layout'); this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel'); this.addSetting('MaxBlockquotesLevel');
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'AllowStyles', this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'RemoveColors', 'AllowStyles',
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes']); 'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes', 'AllowDraftAutosave', 'showNextMessage']);
const fReloadLanguageHelper = (saveSettingsStep) => () => { const fReloadLanguageHelper = (saveSettingsStep) => () => {
this.languageTrigger(saveSettingsStep); this.languageTrigger(saveSettingsStep);
@ -117,20 +117,12 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.languageTrigger(SaveSettingStatus.Saving); this.languageTrigger(SaveSettingStatus.Saving);
translatorReload(value) translatorReload(value)
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed)) .then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
.then(() => Remote.saveSetting('Language', value)); .then(() => Remote.saveSetting('language', value));
}, },
hourCycle: value => hourCycle: value =>
Remote.saveSetting('hourCycle', value), Remote.saveSetting('hourCycle', value),
removeColors: value => {
let dom = MessageUserStore.bodiesDom();
if (dom) {
dom.innerHTML = '';
}
Remote.saveSetting('RemoveColors', value);
},
notificationSound: value => { notificationSound: value => {
Remote.saveSetting('NotificationSound', value); Remote.saveSetting('NotificationSound', value);
Settings.set('NotificationSound', value); Settings.set('NotificationSound', value);

View file

@ -10,13 +10,13 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { GnuPGUserStore } from 'Stores/User/GnuPG'; import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP'; import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport'; import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate'; import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
//import Remote from 'Remote/User/Fetch';
export class UserSettingsSecurity extends AbstractViewSettings { export class UserSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
@ -46,10 +46,6 @@ export class UserSettingsSecurity extends AbstractViewSettings {
this.canOpenPGP = SettingsCapa('OpenPGP'); this.canOpenPGP = SettingsCapa('OpenPGP');
this.canGnuPG = GnuPGUserStore.isSupported(); this.canGnuPG = GnuPGUserStore.isSupported();
this.canMailvelope = !!window.mailvelope; this.canMailvelope = !!window.mailvelope;
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
} }
addOpenPgpKey() { addOpenPgpKey() {
@ -66,5 +62,11 @@ export class UserSettingsSecurity extends AbstractViewSettings {
* The iframe will be injected into the container identified by selector. * The iframe will be injected into the container identified by selector.
*/ */
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/); window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
/**
* https://github.com/the-djmaze/snappymail/issues/973
Remote.request('GetStoredPGPKeys', (iError, data) => {
console.dir([iError, data]);
});
*/
} }
} }

View file

@ -6,6 +6,7 @@ import { i18n } from 'Common/Translator';
import { SettingsCapa } from 'Common/Globals'; import { SettingsCapa } from 'Common/Globals';
import { ThemeStore, convertThemeName, changeTheme } from 'Stores/Theme'; import { ThemeStore, convertThemeName, changeTheme } from 'Stores/Theme';
import { addSubscribablesTo } from 'External/ko';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
@ -24,20 +25,22 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
this.fontSansSerif = ThemeStore.fontSansSerif; this.fontSansSerif = ThemeStore.fontSansSerif;
this.fontSerif = ThemeStore.fontSerif; this.fontSerif = ThemeStore.fontSerif;
this.fontMono = ThemeStore.fontMono; this.fontMono = ThemeStore.fontMono;
ThemeStore.fontSansSerif.subscribe(value => { addSubscribablesTo(ThemeStore, {
Remote.saveSettings(null, { fontSansSerif: value => {
fontSansSerif: value Remote.saveSettings(null, {
}); fontSansSerif: value
}); });
ThemeStore.fontSerif.subscribe(value => { },
Remote.saveSettings(null, { fontSerif: value => {
fontSerif: value Remote.saveSettings(null, {
}); fontSerif: value
}); });
ThemeStore.fontMono.subscribe(value => { },
Remote.saveSettings(null, { fontMono: value => {
fontMono: value Remote.saveSettings(null, {
}); fontMono: value
});
}
}); });
this.theme = ThemeStore.theme; this.theme = ThemeStore.theme;

View file

@ -25,13 +25,13 @@ export const
arr.map(item => item.toString?.() || item).join(separator), arr.map(item => item.toString?.() || item).join(separator),
/* /*
getNotificationMessage = code => { getNotificationMessage = code => {
let key = getKeyByValue(Notification, code); let key = getKeyByValue(Notifications, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : ''; return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
rl.i18n('NOTIFICATIONS/') rl.i18n('NOTIFICATIONS/')
}, },
getNotification = (code, message = '', defCode = 0) => { getNotification = (code, message = '', defCode = 0) => {
code = parseInt(code, 10) || 0; code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) { if (Notifications.ClientViewError === code && message) {
return message; return message;
} }

View file

@ -84,7 +84,7 @@ export class FilterPopupView extends rl.pluginPopupView {
this.defaultOptionsAfterRender = defaultOptionsAfterRender; this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.folderSelectList = koComputable(() => this.folderSelectList = koComputable(() =>
folderListOptionsBuilder( folderListOptionsBuilder(
[rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'], [rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX'],
[['', '']], [['', '']],
item => item?.localName() || '' item => item?.localName() || ''
) )

View file

@ -27,8 +27,8 @@ try {
setItem: (key, value) => { setItem: (key, value) => {
data[key] = ''+value; // forces the value to a string data[key] = ''+value; // forces the value to a string
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data)) document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
+"; expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString()) +";expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
+"; path=/; samesite=strict"; +";path=/;samesite=strict";
} }
}; };
} }

View file

@ -0,0 +1 @@
export const Passphrases = new Map();

View file

@ -11,8 +11,8 @@ export const LanguageStore = {
populate: function() { populate: function() {
const aLanguages = Settings.app('languages'); const aLanguages = Settings.app('languages');
this.languages(isArray(aLanguages) ? aLanguages : []); this.languages(isArray(aLanguages) ? aLanguages : []);
this.language(SettingsGet('Language')); this.language(SettingsGet('language'));
this.userLanguage(SettingsGet('UserLanguage')); this.userLanguage(SettingsGet('userLanguage'));
this.hourCycle(SettingsGet('hourCycle')); this.hourCycle(SettingsGet('hourCycle'));
} }
} }

View file

@ -1,13 +1,15 @@
import ko from 'ko'; import ko from 'ko';
import { $htmlCL, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals'; import { $htmlCL, appEl, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
import { isArray, arrayLength } from 'Common/Utils'; import { isArray, arrayLength } from 'Common/Utils';
import { cssLink, serverRequestRaw } from 'Common/Links'; import { cssLink, serverRequestRaw } from 'Common/Links';
import { SaveSettingStatus } from 'Common/Enums'; import { SaveSettingStatus } from 'Common/Enums';
import { addSubscribablesTo } from 'External/ko';
let __themeTimer = 0; let __themeTimer = 0;
export const export const
appEl = () => elementById('rl-app'), // Also see Styles/_Values.less @maxMobileWidth
isMobile = matchMedia('(max-width: 799px)'),
ThemeStore = { ThemeStore = {
theme: ko.observable(''), theme: ko.observable(''),
@ -17,7 +19,7 @@ export const
fontSansSerif: ko.observable(''), fontSansSerif: ko.observable(''),
fontSerif: ko.observable(''), fontSerif: ko.observable(''),
fontMono: ko.observable(''), fontMono: ko.observable(''),
isMobile: ko.observable($htmlCL.contains('rl-mobile')), isMobile: ko.observable(false),
populate: () => { populate: () => {
const themes = Settings.app('themes'); const themes = Settings.app('themes');
@ -25,8 +27,8 @@ export const
ThemeStore.themes(isArray(themes) ? themes : []); ThemeStore.themes(isArray(themes) ? themes : []);
ThemeStore.theme(SettingsGet('Theme')); ThemeStore.theme(SettingsGet('Theme'));
if (!ThemeStore.isMobile()) { if (!ThemeStore.isMobile()) {
ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName')); ThemeStore.userBackgroundName(SettingsGet('userBackgroundName'));
ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash')); ThemeStore.userBackgroundHash(SettingsGet('userBackgroundHash'));
} }
ThemeStore.fontSansSerif(SettingsGet('fontSansSerif')); ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
ThemeStore.fontSerif(SettingsGet('fontSerif')); ThemeStore.fontSerif(SettingsGet('fontSerif'));
@ -62,35 +64,44 @@ export const
convertThemeName = theme => theme.replace(/@[a-z]+$/, '').replace(/([A-Z])/g, ' $1').trim(); convertThemeName = theme => theme.replace(/@[a-z]+$/, '').replace(/([A-Z])/g, ' $1').trim();
ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value)); addSubscribablesTo(ThemeStore, {
fontSansSerif: value => {
if (null != value) {
let cl = appEl.classList;
cl.forEach(name => {
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
cl.remove(name);
}
});
value && cl.add('font'+value);
}
},
ThemeStore.fontSansSerif.subscribe(value => { fontSerif: value => {
if (null != value) { if (null != value) {
let cl = appEl().classList; let cl = appEl.classList;
cl.forEach(name => { cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) { value && cl.add('fontSerif'+value);
cl.remove(name); }
} },
});
value && cl.add('font'+value); fontMono: value => {
} if (null != value) {
}); let cl = appEl.classList;
ThemeStore.fontSerif.subscribe(value => { cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
if (null != value) { value && cl.add('fontMono'+value);
let cl = appEl().classList; }
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name)); },
value && cl.add('fontSerif'+value);
} userBackgroundHash: value => {
}); appEl.classList.toggle('UserBackground', !!value);
ThemeStore.fontMono.subscribe(value => { appEl.style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
if (null != value) {
let cl = appEl().classList;
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
value && cl.add('fontMono'+value);
} }
}); });
ThemeStore.userBackgroundHash.subscribe(value => { isMobile.onchange = e => {
appEl().classList.toggle('UserBackground', !!value); ThemeStore.isMobile(e.matches);
appEl().style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null; $htmlCL.toggle('rl-mobile', e.matches);
}); leftPanelDisabled(e.matches);
};
isMobile.onchange(isMobile);

View file

@ -1,9 +1,10 @@
import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals'; import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
import { addObservablesTo } from 'External/ko'; import { addObservablesTo } from 'External/ko';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { arePopupsVisible } from 'Knoin/Knoin';
export const AppUserStore = { export const AppUserStore = {
allowContacts: () => !!SettingsGet('ContactsIsAllowed') allowContacts: () => !!SettingsGet('contactsAllowed')
}; };
addObservablesTo(AppUserStore, { addObservablesTo(AppUserStore, {
@ -15,10 +16,9 @@ addObservablesTo(AppUserStore, {
AppUserStore.focusedState.subscribe(value => { AppUserStore.focusedState.subscribe(value => {
['FolderList','MessageList','MessageView'].forEach(name => { ['FolderList','MessageList','MessageView'].forEach(name => {
if (name === value) { if (name === value) {
keyScope(value); arePopupsVisible() || keyScope(value);
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value); ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
} }
let dom = elementById('V-Mail'+name); elementById('V-Mail'+name).classList.toggle('focused', name === value);
dom?.classList.toggle('focused', name === value);
}); });
}); });

View file

@ -2,6 +2,7 @@ import ko from 'ko';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { koComputable, addObservablesTo, koArrayWithDestroy } from 'External/ko'; import { koComputable, addObservablesTo, koArrayWithDestroy } from 'External/ko';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { Notifications } from 'Common/Enums';
export const ContactUserStore = koArrayWithDestroy(); export const ContactUserStore = koArrayWithDestroy();
@ -43,7 +44,7 @@ ContactUserStore.sync = fResultFunc => {
} catch (e) { } catch (e) {
ContactUserStore.syncing(false); ContactUserStore.syncing(false);
console.error(e); console.error(e);
fResultFunc?.(Notification.UnknownError); fResultFunc?.(Notifications.UnknownError);
} }
}, 'ContactsSync'); }, 'ContactsSync');
} }

View file

@ -199,11 +199,11 @@ FolderUserStore = new class {
saveSystemFolders(folders) { saveSystemFolders(folders) {
folders = folders || { folders = folders || {
Sent: FolderUserStore.sentFolder(), sent: FolderUserStore.sentFolder(),
Drafts: FolderUserStore.draftsFolder(), drafts: FolderUserStore.draftsFolder(),
Spam: FolderUserStore.spamFolder(), junk: FolderUserStore.spamFolder(),
Trash: FolderUserStore.trashFolder(), trash: FolderUserStore.trashFolder(),
Archive: FolderUserStore.archiveFolder() archive: FolderUserStore.archiveFolder()
}; };
forEachObjectEntry(folders, (k,v)=>Settings.set(k+'Folder',v)); forEachObjectEntry(folders, (k,v)=>Settings.set(k+'Folder',v));
rl.app.Remote.request('SystemFoldersUpdate', null, folders); rl.app.Remote.request('SystemFoldersUpdate', null, folders);

View file

@ -11,14 +11,15 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey'; import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { AskPopupView } from 'View/Popup/Ask'; import { AskPopupView } from 'View/Popup/Ask';
import { Passphrases } from 'Storage/Passphrases';
const const
passphrases = new Map(),
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') => { askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') => {
const key = privateKey.id, const key = privateKey.id,
pass = passphrases.has(key) pass = Passphrases.has(key)
? {password:passphrases.get(key), remember:false} ? {password:Passphrases.get(key), remember:false}
: await AskPopupView.password('GnuPG key<br>' + key + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt); : await AskPopupView.password('GnuPG key<br>' + key + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt);
pass && pass.remember && passphrases.set(key, pass.password); pass && pass.remember && Passphrases.set(key, pass.password);
return pass.password; return pass.password;
}, },
@ -83,7 +84,7 @@ export const GnuPGUserStore = new class {
key.armor = oData.Result; key.armor = oData.Result;
showScreenPopup(OpenPgpKeyPopupView, [key]); showScreenPopup(OpenPgpKeyPopupView, [key]);
} else { } else {
passphrases.delete(key.id); Passphrases.delete(key.id);
} }
}, { }, {
keyId: key.id, keyId: key.id,
@ -198,7 +199,7 @@ export const GnuPGUserStore = new class {
if (result?.Result && false !== result.Result.data) { if (result?.Result && false !== result.Result.data) {
return result.Result; return result.Result;
} }
passphrases.delete(key.id); Passphrases.delete(key.id);
} }
} }
} }

View file

@ -30,25 +30,21 @@ export const MessageUserStore = new class {
AppUserStore.focusedState(Scope.MessageList); AppUserStore.focusedState(Scope.MessageList);
exitFullscreen(); exitFullscreen();
} }
this.hideMessageBodies(); [...(this.bodiesDom()?.children || [])].forEach(el => el.hidden = true);
}, },
}); });
this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000); this.purgeCache = this.purgeCache.throttle(30000);
} }
purgeMessageBodyCache() { purgeCache(all) {
const messagesDom = this.bodiesDom(), const children = this.bodiesDom()?.children || [];
children = messagesDom?.children; let i = Math.max(0, children.length - (all ? 0 : 15));
if (children) { while (i--) {
while (15 < children.length) { children[i].remove();
children[0].remove(); if (children[i].message) {
children[i].message.body = null;
} }
} }
} }
hideMessageBodies() {
const messagesDom = this.bodiesDom();
messagesDom && Array.from(messagesDom.children).forEach(el => el.hidden = true);
}
}; };

View file

@ -1,17 +1,16 @@
import { koComputable, addObservablesTo, addComputablesTo } from 'External/ko'; import { koComputable, addObservablesTo, addComputablesTo } from 'External/ko';
import { SMAudio } from 'Common/Audio'; import { SMAudio } from 'Common/Audio';
import { Notification } from 'Common/Enums'; import { Notifications } from 'Common/Enums';
import { MessageSetAction } from 'Common/EnumsUser'; import { MessageSetAction } from 'Common/EnumsUser';
import { $htmlCL } from 'Common/Globals'; import { $htmlCL, fireEvent } from 'Common/Globals';
import { arrayLength, pInt, pString } from 'Common/Utils'; import { arrayLength, pString } from 'Common/Utils';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { import {
getFolderInboxName, getFolderInboxName,
getFolderFromCacheList, getFolderFromCacheList,
setFolderHash, setFolderETag
MessageFlagsCache
} from 'Common/Cache'; } from 'Common/Cache';
import { mailBox } from 'Common/Links'; import { mailBox } from 'Common/Links';
@ -174,7 +173,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage(); let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage();
if (bDropCurrentFolderCache) { if (bDropCurrentFolderCache) {
setFolderHash(FolderUserStore.currentFolderFullName(), ''); setFolderETag(FolderUserStore.currentFolderFullName(), '');
} }
if (bDropPagePosition) { if (bDropPagePosition) {
@ -198,7 +197,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let error = ''; let error = '';
if (iError) { if (iError) {
error = getNotification(iError); error = getNotification(iError);
if (Notification.RequestAborted !== iError) { if (Notifications.RequestAborted !== iError) {
MessagelistUserStore([]); MessagelistUserStore([]);
} }
} else { } else {
@ -207,27 +206,24 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
error = ''; error = '';
const const
folder = getFolderFromCacheList(collection.folder), folderInfo = collection.folder,
folderInfo = collection.folderInfo; folder = getFolderFromCacheList(folderInfo.name);
collection.folder = folderInfo.name;
if (folder && !bCached) { if (folder && !bCached) {
// folder.revivePropertiesFromJson(result); // folder.revivePropertiesFromJson(result);
folder.expires = Date.now(); folder.expires = Date.now();
folder.uidNext = folderInfo.uidNext; folder.uidNext = folderInfo.uidNext;
folder.hash = collection.folderHash; folder.etag = folderInfo.etag;
if (null != folderInfo.totalEmails) { if (null != folderInfo.totalEmails) {
folder.totalEmails(folderInfo.totalEmails); folder.totalEmails(folderInfo.totalEmails);
} }
if (null != folderInfo.unreadEmails) { if (null != folderInfo.unreadEmails) {
if (pInt(folder.unreadEmails()) !== pInt(folderInfo.unreadEmails)) {
MessageFlagsCache.clearFolder(folder.fullName);
}
folder.unreadEmails(folderInfo.unreadEmails); folder.unreadEmails(folderInfo.unreadEmails);
} }
folder.flags(folderInfo.flags); let flags = folderInfo.permanentFlags || [];
let flags = folderInfo.permanentFlags;
if (flags.includes('\\*')) { if (flags.includes('\\*')) {
let i = 6; let i = 6;
while (--i) { while (--i) {
@ -251,14 +247,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
MessagelistUserStore.threadUid(collection.threadUid); MessagelistUserStore.threadUid(collection.threadUid);
MessagelistUserStore.endHash( MessagelistUserStore.endHash(
collection.folder + folderInfo.name +
'|' + collection.search + '|' + collection.search +
'|' + MessagelistUserStore.threadUid() + '|' + MessagelistUserStore.threadUid() +
'|' + MessagelistUserStore.page() '|' + MessagelistUserStore.page()
); );
MessagelistUserStore.endThreadUid(collection.threadUid); MessagelistUserStore.endThreadUid(collection.threadUid);
const message = MessageUserStore.message(); const message = MessageUserStore.message();
if (message && collection.folder !== message.folder) { if (message && folderInfo.name !== message.folder) {
MessageUserStore.message(null); MessageUserStore.message(null);
} }
@ -280,7 +276,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
} else { } else {
MessagelistUserStore.count(0); MessagelistUserStore.count(0);
MessagelistUserStore([]); MessagelistUserStore([]);
error = getNotification(Notification.CantGetMessageList); error = getNotification(Notifications.CantGetMessageList);
} }
MessagelistUserStore.error(error); MessagelistUserStore.error(error);
} }
@ -306,22 +302,38 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
messages = messages || MessagelistUserStore.listChecked(); messages = messages || MessagelistUserStore.listChecked();
let folder, let folder,
alreadyUnread = 0, rootUids = [],
rootUids = messages.map(oMessage => oMessage?.uid).validUnique(), length;
length = rootUids.length;
if (iSetAction == MessageSetAction.SetSeen) {
messages.forEach(oMessage =>
oMessage.isUnseen() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\seen')
);
} else if (iSetAction == MessageSetAction.UnsetSeen) {
messages.forEach(oMessage =>
!oMessage.isUnseen() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\seen')
);
} else if (iSetAction == MessageSetAction.SetFlag) {
messages.forEach(oMessage =>
!oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\flagged')
);
} else if (iSetAction == MessageSetAction.UnsetFlag) {
messages.forEach(oMessage =>
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged')
);
}
rootUids = rootUids.validUnique();
length = rootUids.length;
if (sFolderFullName && length) { if (sFolderFullName && length) {
rootUids.forEach(sSubUid =>
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
);
switch (iSetAction) { switch (iSetAction) {
case MessageSetAction.SetSeen: case MessageSetAction.SetSeen:
length = 0; length = -length;
// fallthrough is intentionally // fallthrough is intentionally
case MessageSetAction.UnsetSeen: case MessageSetAction.UnsetSeen:
folder = getFolderFromCacheList(sFolderFullName); folder = getFolderFromCacheList(sFolderFullName);
if (folder) { if (folder) {
folder.unreadEmails(folder.unreadEmails() - alreadyUnread + length); folder.unreadEmails(Math.max(0, folder.unreadEmails() + length));
} }
Remote.request('MessageSetSeen', null, { Remote.request('MessageSetSeen', null, {
folder: sFolderFullName, folder: sFolderFullName,
@ -340,8 +352,6 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
break; break;
// no default // no default
} }
MessagelistUserStore.reloadFlagsAndCachedMessage();
} }
}; };
@ -370,22 +380,20 @@ MessagelistUserStore.removeMessagesFromList = (
messages.forEach(item => item?.isUnseen() && ++unseenCount); messages.forEach(item => item?.isUnseen() && ++unseenCount);
if (fromFolder) { if (fromFolder) {
fromFolder.hash = ''; fromFolder.etag = '';
if (!copy) { if (!copy) {
fromFolder.totalEmails( fromFolder.totalEmails(
0 <= fromFolder.totalEmails() - oUids.size ? fromFolder.totalEmails() - oUids.size : 0 0 <= fromFolder.totalEmails() - oUids.size ? fromFolder.totalEmails() - oUids.size : 0
); );
if (0 < unseenCount) { if (0 < unseenCount) {
fromFolder.unreadEmails( fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
0 <= fromFolder.unreadEmails() - unseenCount ? fromFolder.unreadEmails() - unseenCount : 0
);
} }
} }
} }
if (toFolder) { if (toFolder) {
toFolder.hash = ''; toFolder.etag = '';
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) { if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
unseenCount = 0; unseenCount = 0;
@ -405,6 +413,18 @@ MessagelistUserStore.removeMessagesFromList = (
} else { } else {
MessagelistUserStore.isIncomplete(true); MessagelistUserStore.isIncomplete(true);
// Select next email https://github.com/the-djmaze/snappymail/issues/968
if (currentMessage && 1 == messages.length && SettingsUserStore.showNextMessage()) {
let next = MessagelistUserStore.indexOf(currentMessage) + 1;
if (0 < next && (next = MessagelistUserStore()[next])) {
currentMessage = null;
fireEvent('mailbox.message.show', {
folder: next.folder,
uid: next.uid
});
}
}
messages.forEach(item => { messages.forEach(item => {
if (currentMessage && currentMessage.hash === item.hash) { if (currentMessage && currentMessage.hash === item.hash) {
currentMessage = null; currentMessage = null;
@ -455,9 +475,4 @@ MessagelistUserStore.removeMessagesFromList = (
) )
); );
} }
},
MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
MessagelistUserStore.forEach(message => MessageFlagsCache.initMessage(message));
MessageFlagsCache.initMessage(MessageUserStore.message());
}; };

View file

@ -1,4 +1,4 @@
import * as Links from 'Common/Links'; import { staticLink } from 'Common/Links';
import { addObservablesTo } from 'External/ko'; import { addObservablesTo } from 'External/ko';
import { fireEvent } from 'Common/Globals'; import { fireEvent } from 'Common/Globals';
@ -22,15 +22,19 @@ let DesktopNotifications = false,
WorkerNotifications = navigator.serviceWorker; WorkerNotifications = navigator.serviceWorker;
// Are Notifications supported in the service worker? // Are Notifications supported in the service worker?
if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) { if (WorkerNotifications) {
/* Listen for close requests from the ServiceWorker */ if (ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) {
WorkerNotifications.addEventListener('message', event => { /* Listen for close requests from the ServiceWorker */
const obj = JSON.parse(event.data); WorkerNotifications.addEventListener('message', event => {
'notificationclick' === obj?.action && dispatchMessage(obj.data); const obj = JSON.parse(event.data);
}); 'notificationclick' === obj?.action && dispatchMessage(obj.data);
});
} else {
console.log('ServiceWorkerRegistration.showNotification undefined');
WorkerNotifications = null;
}
} else { } else {
WorkerNotifications = null; console.log('ServiceWorker undefined');
console.log('ServiceWorker Notifications not supported');
} }
export const NotificationUserStore = new class { export const NotificationUserStore = new class {
@ -57,7 +61,7 @@ export const NotificationUserStore = new class {
if (DesktopNotifications && NotificationsGranted()) { if (DesktopNotifications && NotificationsGranted()) {
const options = { const options = {
body: text, body: text,
icon: imageSrc || Links.staticLink('images/icon-message-notification.png'), icon: imageSrc || staticLink('images/icon-message-notification.png'),
data: messageData data: messageData
}; };
if (messageData?.uid) { if (messageData?.uid) {
@ -65,8 +69,7 @@ export const NotificationUserStore = new class {
} }
if (WorkerNotifications) { if (WorkerNotifications) {
// Service-Worker-Allowed HTTP header to allow the scope. // Service-Worker-Allowed HTTP header to allow the scope.
WorkerNotifications.register('/serviceworker.js') WorkerNotifications.register(staticLink('js/serviceworker.js'), {scope:'/'})
// WorkerNotifications.register(Links.staticLink('js/serviceworker.js'), {scope:'/'})
.then(() => .then(() =>
WorkerNotifications.ready.then(registration => WorkerNotifications.ready.then(registration =>
/* Show the notification */ /* Show the notification */
@ -80,7 +83,10 @@ export const NotificationUserStore = new class {
) )
) )
) )
.catch(e => console.error(e)); .catch(e => {
console.error(e);
WorkerNotifications = null;
});
} else { } else {
const notification = new HTML5Notification(title, options); const notification = new HTML5Notification(title, options);
notification.show?.(); notification.show?.();

View file

@ -12,22 +12,23 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey'; import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { AskPopupView } from 'View/Popup/Ask'; import { AskPopupView } from 'View/Popup/Ask';
import { Passphrases } from 'Storage/Passphrases';
const const
findOpenPGPKey = (keys, query/*, sign*/) => findOpenPGPKey = (keys, query/*, sign*/) =>
keys.find(key => keys.find(key =>
key.emails.includes(query) || query == key.id || query == key.fingerprint key.emails.includes(query) || query == key.id || query == key.fingerprint
), ),
passphrases = new Map(),
decryptKey = async (privateKey, btnTxt = 'LABEL_SIGN') => { decryptKey = async (privateKey, btnTxt = 'LABEL_SIGN') => {
if (privateKey.key.isDecrypted()) { if (privateKey.key.isDecrypted()) {
return privateKey.key; return privateKey.key;
} }
const key = privateKey.id, const key = privateKey.id,
pass = passphrases.has(key) pass = Passphrases.has(key)
? {password:passphrases.get(key), remember:false} ? {password:Passphrases.get(key), remember:false}
: await AskPopupView.password( : await AskPopupView.password(
'OpenPGP.js key<br>' + privateKey.id + ' ' + privateKey.emails[0], 'OpenPGP.js key<br>' + key + ' ' + privateKey.emails[0],
'OPENPGP/'+btnTxt 'OPENPGP/'+btnTxt
); );
if (pass) { if (pass) {
@ -36,7 +37,7 @@ const
privateKey: privateKey.key, privateKey: privateKey.key,
passphrase passphrase
}); });
result && pass.remember && passphrases.set(key, passphrase); result && pass.remember && Passphrases.set(key, passphrase);
return result; return result;
} }
}, },

View file

@ -9,17 +9,15 @@ import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { GnuPGUserStore } from 'Stores/User/GnuPG'; import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP'; import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
// https://mailvelope.github.io/mailvelope/Keyring.html
let mailvelopeKeyring = null;
export const export const
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----', BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----', // BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
// BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----', // BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----',
PgpUserStore = new class { PgpUserStore = new class {
constructor() {
// https://mailvelope.github.io/mailvelope/Keyring.html
this.mailvelopeKeyring = null;
}
init() { init() {
if (SettingsCapa('OpenPGP') && window.crypto && crypto.getRandomValues) { if (SettingsCapa('OpenPGP') && window.crypto && crypto.getRandomValues) {
rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/openpgp.')) rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/openpgp.'))
@ -38,7 +36,7 @@ export const
identifier = identifier || SettingsGet('Email'); identifier = identifier || SettingsGet('Email');
if (window.mailvelope) { if (window.mailvelope) {
const fn = keyring => { const fn = keyring => {
this.mailvelopeKeyring = keyring; mailvelopeKeyring = keyring;
console.log('mailvelope ready'); console.log('mailvelope ready');
}; };
mailvelope.getKeyring().then(fn, err => { mailvelope.getKeyring().then(fn, err => {
@ -79,8 +77,7 @@ export const
async mailvelopeHasPublicKeyForEmails(recipients) { async mailvelopeHasPublicKeyForEmails(recipients) {
const const
keyring = this.mailvelopeKeyring, mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
mailvelope = keyring && await keyring.validKeyForAddress(recipients)
/*.then(LookupResult => Object.entries(LookupResult))*/, /*.then(LookupResult => Object.entries(LookupResult))*/,
entries = mailvelope && Object.entries(mailvelope); entries = mailvelope && Object.entries(mailvelope);
return entries && entries.filter(value => value[1]).length === recipients.length; return entries && entries.filter(value => value[1]).length === recipients.length;
@ -104,8 +101,7 @@ export const
} }
async getMailvelopePrivateKeyFor(email/*, sign*/) { async getMailvelopePrivateKeyFor(email/*, sign*/) {
let keyring = this.mailvelopeKeyring; if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
if (keyring && await keyring.hasPrivateKey({email:email})) {
return ['mailvelope', email]; return ['mailvelope', email];
} }
return false; return false;
@ -145,46 +141,48 @@ export const
} }
// Try Mailvelope (does not support inline images) // Try Mailvelope (does not support inline images)
try { if (mailvelopeKeyring) {
let emails = [...message.from,...message.to,...message.cc].validUnique(), try {
i = emails.length; let emails = [...message.from,...message.to,...message.cc].validUnique(),
while (i--) { i = emails.length;
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) { while (i--) {
/** if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer /**
* Creates an iframe to display an encrypted form * https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
*/ * Creates an iframe to display an encrypted form
// mailvelope.createEncryptedFormContainer('#mailvelope-form'); */
/** // mailvelope.createEncryptedFormContainer('#mailvelope-form');
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer /**
* Creates an iframe to display the decrypted content of the encrypted mail. * https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
*/ * Creates an iframe to display the decrypted content of the encrypted mail.
const body = message.body; */
body.textContent = ''; const body = message.body;
let result = await mailvelope.createDisplayContainer( body.textContent = '';
'#'+body.id, let result = await mailvelope.createDisplayContainer(
armoredText, '#'+body.id,
this.mailvelopeKeyring, armoredText,
{ mailvelopeKeyring,
senderAddress: sender {
// emails[i].email senderAddress: sender
} // emails[i].email
); }
if (result) { );
if (result.error?.message) { if (result) {
if ('PWD_DIALOG_CANCEL' !== result.error.code) { if (result.error?.message) {
alert(result.error.code + ': ' + result.error.message); if ('PWD_DIALOG_CANCEL' !== result.error.code) {
alert(result.error.code + ': ' + result.error.message);
}
} else {
body.classList.add('mailvelope');
return true;
} }
} else {
body.classList.add('mailvelope');
return true;
} }
break;
} }
break;
} }
} catch (err) {
console.error(err);
} }
} catch (err) {
console.error(err);
} }
// Now try GnuPG // Now try GnuPG
@ -217,19 +215,19 @@ export const
* So far this is only the autocrypt header. * So far this is only the autocrypt header.
*/ */
/* /*
this.mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers) mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers)
this.mailvelopeKeyring.addSyncHandler(syncHandlerObj) mailvelopeKeyring.addSyncHandler(syncHandlerObj)
this.mailvelopeKeyring.createKeyBackupContainer(selector, options) mailvelopeKeyring.createKeyBackupContainer(selector, options)
this.mailvelopeKeyring.createKeyGenContainer(selector, { mailvelopeKeyring.createKeyGenContainer(selector, {
// userIds: [], // userIds: [],
keySize: 4096 keySize: 4096
}) })
this.mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>) mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
this.mailvelopeKeyring.importPublicKey(armored) mailvelopeKeyring.importPublicKey(armored)
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject // https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
this.mailvelopeKeyring.addSyncHandler({ mailvelopeKeyring.addSyncHandler({
uploadSync uploadSync
downloadSync downloadSync
backup backup

View file

@ -26,6 +26,7 @@ export const SettingsUserStore = new class {
simpleAttachmentsList: 0, simpleAttachmentsList: 0,
useCheckboxesInList: 1, useCheckboxesInList: 1,
listGrouped: 0, listGrouped: 0,
showNextMessage: 0,
allowDraftAutosave: 1, allowDraftAutosave: 1,
useThreads: 0, useThreads: 0,
replySameFolder: 0, replySameFolder: 0,
@ -49,13 +50,12 @@ export const SettingsUserStore = new class {
self.init(); self.init();
self.usePreviewPane = koComputable(() => Layout.NoPreview !== self.layout() && !ThemeStore.isMobile()); self.usePreviewPane = koComputable(() => self.layout() && !ThemeStore.isMobile());
const toggleLayout = () => { const toggleLayout = () => {
const value = ThemeStore.isMobile() ? Layout.NoPreview : self.layout(); const value = ThemeStore.isMobile() ? 0 : self.layout();
$htmlCL.toggle('rl-no-preview-pane', Layout.NoPreview === value); $htmlCL.toggle('sm-msgView-side', Layout.SidePreview === value);
$htmlCL.toggle('rl-side-preview-pane', Layout.SidePreview === value); $htmlCL.toggle('sm-msgView-bottom', Layout.BottomPreview === value);
$htmlCL.toggle('rl-bottom-preview-pane', Layout.BottomPreview === value);
fireEvent('rl-layout', value); fireEvent('rl-layout', value);
}; };
self.layout.subscribe(toggleLayout); self.layout.subscribe(toggleLayout);
@ -65,7 +65,7 @@ export const SettingsUserStore = new class {
let iAutoLogoutTimer; let iAutoLogoutTimer;
self.delayLogout = (() => { self.delayLogout = (() => {
clearTimeout(iAutoLogoutTimer); clearTimeout(iAutoLogoutTimer);
if (0 < self.autoLogout() && !SettingsGet('AccountSignMe')) { if (0 < self.autoLogout() && !SettingsGet('accountSignMe')) {
iAutoLogoutTimer = setTimeout( iAutoLogoutTimer = setTimeout(
rl.app.logout, rl.app.logout,
self.autoLogout() * 60000 self.autoLogout() * 60000
@ -96,6 +96,7 @@ export const SettingsUserStore = new class {
self.simpleAttachmentsList(SettingsGet('simpleAttachmentsList')); self.simpleAttachmentsList(SettingsGet('simpleAttachmentsList'));
self.useCheckboxesInList(SettingsGet('UseCheckboxesInList')); self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
self.listGrouped(SettingsGet('listGrouped')); self.listGrouped(SettingsGet('listGrouped'));
self.showNextMessage(SettingsGet('showNextMessage'));
self.allowDraftAutosave(SettingsGet('AllowDraftAutosave')); self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
self.useThreads(SettingsGet('UseThreads')); self.useThreads(SettingsGet('UseThreads'));
self.replySameFolder(SettingsGet('ReplySameFolder')); self.replySameFolder(SettingsGet('ReplySameFolder'));

View file

@ -1,7 +1,7 @@
:root { :root {
--fontSans: Verdana, Geneva, "Bitstream Vera Sans", "DejaVu LGC Sans", Arial, sans-serif; --fontSans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--fontSerif: "Nimbus Roman No9 L", "Times New Roman", Times, FreeSerif, serif; --fontSerif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--fontMono: "Liberation Mono", Monaco, Menlo, Consolas, "Courier New", FreeMono, Courier, monospace; --fontMono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
#rl-app.fontArial { #rl-app.fontArial {

View file

@ -39,6 +39,14 @@
height: calc(100% - 50px - @rlLowMargin); height: calc(100% - 50px - @rlLowMargin);
margin: 50px @rlLowMargin 0 0; margin: 50px @rlLowMargin 0 0;
background-color: var(--panel-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px);
box-shadow: var(--smMainShadow);
overflow-y: auto;
padding: 20px;
scroll-behavior: smooth;
> .b-toolbar { > .b-toolbar {
color: #fff; color: #fff;
display: flex; display: flex;
@ -56,28 +64,13 @@
} }
} }
.rl-mobile #V-AdminPane {
margin-right: 0;
}
.btn.btn-logout { .btn.btn-logout {
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
} }
#rl-settings-subscreen {
background-color: var(--settings-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px);
box-shadow: var(--smMainShadow);
height: 100%;
overflow-y: auto;
padding: 20px;
scroll-behavior: smooth;
}
/* desktop */ /* desktop */
@media screen and (min-width: 1000px) { @media screen and (min-width: @maxMobileWidth + 1px) {
.toggleLeft { .toggleLeft {
display: none; display: none;
} }
@ -90,16 +83,16 @@
} }
} }
/* mobile and tablet */ @media screen and (max-width: @maxMobileWidth) {
@media screen and (max-width: 999px) {
#rl-settings-subscreen {
padding: 10px;
}
#rl-right { #rl-right {
min-width: calc(100% - @rlLowMargin); min-width: calc(100% - @rlLowMargin);
} }
#V-AdminPane {
margin-right: 0;
padding: 10px;
}
html.rl-left-panel-disabled { html.rl-left-panel-disabled {
#rl-left { #rl-left {
width: 0; width: 0;

View file

@ -34,7 +34,7 @@
} }
} }
@media screen and (max-width: 733px) { @media screen and (max-width: 480px) {
#V-PopupsLanguages label { #V-PopupsLanguages label {
width: 100%; width: 100%;
} }

View file

@ -10,10 +10,6 @@
z-index: 5; z-index: 5;
} }
@glass-color: #fff;
@glass-error-color: #f76260;
@glass-m-color: rgba(255, 255, 255, .8);
.LoginView { .LoginView {
.descWrapper { .descWrapper {
@ -29,18 +25,18 @@
border: var(--login-border, 1px solid rgba(255, 255, 255, .2)); border: var(--login-border, 1px solid rgba(255, 255, 255, .2));
border-radius: var(--login-border-radius, 7px); border-radius: var(--login-border-radius, 7px);
box-shadow: var(--login-box-shadow); box-shadow: var(--login-box-shadow);
color: var(--login-color, @glass-m-color); color: var(--login-color, rgba(255, 255, 255, .8));
margin: 0; margin: 0;
padding: 40px 40px 20px 40px; padding: 40px 40px 20px 40px;
position: relative; position: relative;
* { * {
color: var(--login-color, @glass-m-color); color: var(--login-color, rgba(255, 255, 255, .8));
} }
.controls.error * { .controls.error * {
color: @glass-error-color; color: #f76260;
border-color: @glass-error-color; border-color: #f76260;
} }
&.submitting > * { &.submitting > * {
@ -77,17 +73,17 @@
padding-right: 30px; padding-right: 30px;
&:placeholder { &:placeholder {
color: @glass-color; color: #fff;
} }
&:focus, &:hover { &:focus, &:hover {
border-color: @glass-color; border-color: #fff;
} }
} }
input, .btn { input, .btn {
background: none; background: none;
border: 1px solid @glass-m-color; border: 1px solid rgba(255, 255, 255, .8);
} }
.fontastic + input { .fontastic + input {
@ -107,7 +103,7 @@
font-size: 13px; font-size: 13px;
&:hover, &:active { &:hover, &:active {
border-color: @glass-color; border-color: #fff;
} }
} }

View file

@ -4,7 +4,7 @@ html.list-loading body {
cursor: progress; cursor: progress;
} }
@media screen and (min-width: 1000px) { @media screen and (min-width: @maxMobileWidth + 1px) {
#rl-app { #rl-app {
background-image: var(--main-bg-image); background-image: var(--main-bg-image);
background-size: var(--main-bg-size); background-size: var(--main-bg-size);
@ -73,13 +73,14 @@ dialog:not(.animate) {
/** /**
* https://github.com/the-djmaze/snappymail/issues/686 * https://github.com/the-djmaze/snappymail/issues/686
@media screen and (max-width: 999px) {
*/ */
.rl-mobile dialog { @media screen and (max-width: @maxMobileWidth) {
margin: 0 auto; dialog {
/* max-height: calc(100vh - 86px);*/ margin: 0 auto;
/* max-height: 100dvh;*/ /* max-height: calc(100vh - 86px);*/
width: 100%; /* max-height: 100dvh;*/
width: 100%;
}
} }
dialog > header { dialog > header {

View file

@ -1,5 +1,4 @@
#rl-settings-subscreen,
.g-ui-user-select-none { .g-ui-user-select-none {
user-select: none; user-select: none;
-webkit-touch-callout: none; -webkit-touch-callout: none;
@ -159,20 +158,7 @@ summary.legend {
padding: 0 9px; padding: 0 9px;
} }
.alert a:not(.close) { @media screen and (max-width: @maxMobileWidth) {
text-decoration: underline;
}
.alert.alert-info a:not(.close) {
color: var(--info-clr, #3a87ad);
}
.alert.alert-error a:not(.close) {
color: var(--error-clr, #b94a48);
}
/* mobile and tablet */
@media screen and (max-width: 999px) {
.form-horizontal { .form-horizontal {
.control-group { .control-group {
> label { > label {

View file

@ -10,7 +10,7 @@
margin-right:1em; margin-right:1em;
} }
@media screen and (max-width: 410px) { @media screen and (max-width: 480px) {
#advancedsearchform label { #advancedsearchform label {
width: 100%; width: 100%;
} }

View file

@ -10,7 +10,7 @@
100% {background-position: 60px 0;} 100% {background-position: 60px 0;}
} }
@media screen and (min-width: 1000px) { @media screen and (min-width: @maxMobileWidth + 1px) {
.b-folders li .anim-action-class { .b-folders li .anim-action-class {
animation: highlight-folder-row 0.5s linear; animation: highlight-folder-row 0.5s linear;

View file

@ -45,7 +45,7 @@
padding-top: 50px; padding-top: 50px;
text-align: center; text-align: center;
font-size: 24px; font-size: 24px;
color: #666; opacity: 0.5;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
} }
@ -131,18 +131,20 @@
top: 5px; top: 5px;
bottom: 5px; bottom: 5px;
border: 2px #777 dashed; border: 2px var(--border-color, #ddd) dashed;
z-index: 300; z-index: 300;
line-height: 119px; line-height: 119px;
text-align: center; text-align: center;
background-color: #efefef; background-color: var(--dialog-bg-clr, #fff);
font-size: 24px; font-size: 24px;
border-radius: 10px; border-radius: 10px;
opacity: 0.9;
&.dragAndDropOver { &.dragAndDropOver {
background-color: #fff; border-color: var(--main-color, #fff);
opacity: 1;
} }
} }
} }

View file

@ -195,7 +195,7 @@
} }
} }
html:not(rl-mobile) { @media screen and (min-width: @maxMobileWidth + 1px) {
.hideContactListCheckbox { .hideContactListCheckbox {
.checkboxItem { .checkboxItem {
visibility: hidden; visibility: hidden;
@ -203,7 +203,7 @@ html:not(rl-mobile) {
} }
} }
@media screen and (max-width: 900px) { @media screen and (max-width: @maxMobileWidth) {
#V-PopupsContacts { #V-PopupsContacts {
width: 100%; width: 100%;
} }

View file

@ -7,6 +7,12 @@
height: 100%; height: 100%;
} }
.focused .b-folders a.focused {
background-color: var(--folders-focused-bg-color, #333);
border-left-color: #fff;
color: var(--folders-focused-color, #eee);
}
.b-folders { .b-folders {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -38,7 +44,6 @@
} }
.b-toolbar { .b-toolbar {
padding: 10px 0 10px @rlLowMargin;
color: #fff; color: #fff;
} }
@ -56,7 +61,6 @@
hr { hr {
margin: 10px; margin: 10px;
border-color: #999;
} }
li { li {
@ -101,22 +105,12 @@
background-color: var(--folders-selected-bg-color, #333); background-color: var(--folders-selected-bg-color, #333);
color: var(--folders-selected-color, #eee); color: var(--folders-selected-color, #eee);
} }
&.focused {
background-color: var(--folders-focused-bg-color, #333);
color: var(--folders-focused-color, #eee);
}
} }
&:not(.selectable) { &:not(.selectable) {
cursor: default; cursor: default;
font-style: italic; font-style: italic;
} }
&.focused {
background-color: #888;
border-left-color: #fff;
}
&.unread-sub { &.unread-sub {
font-weight: bold; font-weight: bold;
} }
@ -213,24 +207,19 @@
/**/ /**/
} }
.btn { #rl-left .buttonCompose {
&.buttonCompose { max-height: 2em;
max-height: 2em; max-width: fit-content;
max-width: fit-content; overflow: hidden;
overflow: hidden; width: calc(100% - 48px);
width: calc(100% - 48px);
}
} }
html.rl-left-panel-disabled { html.rl-left-panel-disabled {
.buttonComposeText{ .buttonComposeText,
.buttonContacts {
display: none; display: none;
} }
.btn.buttonCompose { #rl-left .buttonCompose {
width: auto; width: auto;
} }
.btn.buttonContacts {
margin-top: 10px !important;
margin-left: 0 !important;
}
} }

View file

@ -12,6 +12,10 @@
/* transition: width 0.3s ease-out;*/ /* transition: width 0.3s ease-out;*/
width: @rlLeftWidth; width: @rlLeftWidth;
z-index: 0; z-index: 0;
.b-toolbar {
padding: 10px 0 10px @rlLowMargin;
}
} }
#rl-right:not([hidden]) { #rl-right:not([hidden]) {
@ -19,9 +23,6 @@
flex-grow: 1; flex-grow: 1;
width: 20%; width: 20%;
} }
.rl-bottom-preview-pane #rl-right {
flex-direction: column;
}
/* /*
.resizable::after { .resizable::after {
@ -41,47 +42,12 @@
} }
*/ */
.resizer {
background: #aaa;
background: rgba(255,255,255,0.5);
display: none;
opacity: 0;
position: absolute;
z-index: 102;
}
.resizer:hover {
opacity: 1;
}
html:not(.rl-left-panel-disabled) #rl-left { html:not(.rl-left-panel-disabled) #rl-left {
resize: horizontal; overflow: auto; resize: horizontal; overflow: auto;
min-width: 155px; min-width: 155px;
max-width: 350px; max-width: 350px;
} }
#rl-left > .resizer,
.rl-side-preview-pane #V-MailMessageList .resizer {
cursor: ew-resize;
cursor: col-resize;
right: 0;
top: 0;
bottom: 0;
width: 5px;
}
.rl-bottom-preview-pane #V-MailMessageList .resizer {
cursor: ns-resize;
cursor: row-resize;
bottom: 0;
left: 0;
right: 0;
height: 5px;
}
html:not(.rl-left-panel-disabled) #rl-left > .resizer,
#V-MailMessageList .resizer {
display: block;
}
#top-system-dropdown-id::after, #top-system-dropdown-id::after,
#button-add-prop-dropdown-id::after { #button-add-prop-dropdown-id::after {
content: '▼'; content: '▼';
@ -140,32 +106,6 @@ html:not(.rl-left-panel-disabled) #rl-left > .resizer,
margin-right: 8px; margin-right: 8px;
} }
/* desktop */
@media screen and (min-width: 1000px) {
}
/* desktop-large */
@media screen and (min-width: 1401px) {
#rl-left {
width: @rlLeftWidth + 20;
}
}
/* mobile and tablet */
@media screen and (max-width: 999px) {
#rl-left {
width: 155px;
}
#rl-settings-subscreen {
padding: 10px;
}
.dropdown-menu a {
line-height: 2.5;
}
}
html:not(.rl-ctrl-key-pressed) .visible-on-ctrl-btn, html:not(.rl-ctrl-key-pressed) .visible-on-ctrl-btn,
html.rl-ctrl-key-pressed .hidden-on-ctrl-btn, html.rl-ctrl-key-pressed .hidden-on-ctrl-btn,
.show-on-panel-disabled { .show-on-panel-disabled {
@ -191,32 +131,6 @@ html.rl-left-panel-disabled {
} }
} }
html.rl-mobile #rl-left > .resizer,
html.rl-no-preview-pane #rl-right .resizer {
display: none !important;
}
html.rl-mobile {
#rl-left {
background: var(--main-bg-color, #aaa);
border-right: 1px solid var(--border-color, #ddd);
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 50vw;
z-index: 3;
}
&.rl-left-panel-disabled #rl-left {
display: none;
}
}
html.rl-mobile .hide-mobile,
html:not(.rl-mobile) .show-mobile {
display: none !important;
}
.e-paginator { .e-paginator {
a { a {
@ -237,3 +151,85 @@ html:not(.rl-mobile) .show-mobile {
} }
} }
} }
.resizer {
display: none;
}
@media screen and (max-width: @maxMobileWidth) {
#rl-app {
#V-SettingsPane {
margin-right: 0;
padding: 10px;
}
}
#rl-left {
background: var(--main-bg-color, #aaa);
border-right: 1px solid var(--border-color, #ddd);
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 50vw;
z-index: 3;
}
.rl-left-panel-disabled #rl-left {
display: none;
}
.dropdown-menu a {
line-height: 2.5;
}
.hide-mobile {
display: none !important;
}
}
/* desktop */
@media screen and (min-width: @maxMobileWidth + 1px) {
.toggleLeft,
#V-MailMessageList .buttonCompose {
display: none !important;
}
.sm-msgView-bottom #rl-right {
flex-direction: column;
}
.resizer {
background: #aaa;
background: rgba(255,255,255,0.5);
opacity: 0;
position: absolute;
bottom: 0;
right: 0;
z-index: 102;
}
.resizer:hover {
opacity: 1;
}
html:not(.rl-left-panel-disabled) #rl-left > .resizer,
.sm-msgView-side #V-MailMessageList .resizer {
cursor: ew-resize;
cursor: col-resize;
display: block;
top: 0;
width: 5px;
}
.sm-msgView-bottom #V-MailMessageList .resizer {
cursor: ns-resize;
cursor: row-resize;
display: block;
left: 0;
height: 5px;
}
}
/* desktop-large */
@media screen and (min-width: 1401px) {
#rl-left {
width: @rlLeftWidth + 20;
}
}

View file

@ -1,34 +1,9 @@
html.rl-mobile, html.rl-no-preview-pane {
.message-selected #V-MailMessageList {
display: none;
}
}
#V-MailMessageList { #V-MailMessageList {
position: relative; position: relative;
}
.rl-side-preview-pane #V-MailMessageList {
height: 100%;
}
.rl-no-preview-pane #V-MailMessageList {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.rl-side-preview-pane #V-MailMessageList .messageList {
max-width: 50vw;
min-width: 320px;
overflow: auto;
resize: horizontal;
width: 35vw;
}
.rl-bottom-preview-pane #V-MailMessageList .messageList {
height: 35vh;
max-height: 50vh;
min-height: 200px;
overflow: auto;
resize: vertical;
}
#V-MailMessageList.focused .messageList { #V-MailMessageList.focused .messageList {
border-color: #9d9d9d; border-color: #9d9d9d;
@ -47,7 +22,7 @@ html.rl-mobile, html.rl-no-preview-pane {
.messageList { .messageList {
height: calc(100% - 60px); height: calc(100% - 60px);
background-color: #fff; background-color: var(--panel-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa); border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px); border-radius: var(--border-radius, 5px);
display: flex; display: flex;
@ -71,7 +46,7 @@ html.rl-mobile, html.rl-no-preview-pane {
} }
.b-footer { .b-footer {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
padding: 7px; padding: 7px;
@ -196,18 +171,6 @@ html.rl-mobile, html.rl-no-preview-pane {
} }
} }
html:not(rl-mobile) {
.hideMessageListCheckbox {
.checkboxCheckAll {
visibility: hidden;
}
.messageCheckbox {
display: none;
}
}
}
.messageListItem > div + div { .messageListItem > div + div {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
@ -396,20 +359,55 @@ html:not(rl-mobile) {
} }
} }
@media screen and (min-width: 1000px) { html:not(.sm-msgView-bottom):not(.sm-msgView-side) .message-selected #V-MailMessageList {
display: none;
}
@media screen and (max-width: @maxMobileWidth) {
#V-MailMessageList .btn-toolbar:not(.hasChecked) .onCheckedShow,
#V-MailMessageList .hasChecked .onCheckedHide {
display: none;
}
}
@media screen and (min-width: @maxMobileWidth + 1px) {
.messageList { .messageList {
.listDragOver { .listDragOver {
transition: all 400ms ease; transition: all 400ms ease;
} }
} }
}
/* desktop-large */ .hideMessageListCheckbox {
@media screen and (min-width: 1401px) { .checkboxCheckAll {
.messageListItem { visibility: hidden;
font-size: 13px; }
time { .checkboxMessage {
font-size: 13px; display: none;
}
}
.sm-msgView-side #V-MailMessageList {
width: unset;
.messageList {
max-width: 50vw;
min-width: 320px;
overflow: auto;
resize: horizontal;
width: 35vw;
}
}
.sm-msgView-bottom #V-MailMessageList {
height: unset;
width: unset;
.messageList {
height: 35vh;
max-height: 50vh;
min-height: 200px;
overflow: auto;
resize: vertical;
} }
} }
} }
@ -425,7 +423,7 @@ html:not(rl-mobile) {
top: 0; top: 0;
} }
.rl-side-preview-pane, .rl-mobile { .sm-msgView-side, .rl-mobile {
.messageListItem > div + div { .messageListItem > div + div {
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -30,28 +30,14 @@
} }
} }
html.rl-no-preview-pane {
#rl-right:not(.message-selected) #V-MailMessageView {
display: none;
}
}
#V-MailMessageView .top-toolbar { #V-MailMessageView .top-toolbar {
color: #fff; color: #fff;
padding: 10px 0; padding: 10px 0;
} visibility: visible;
html.rl-no-preview-pane #V-MailMessageView .top-toolbar {
padding-left: 1px;
}
html:not(.rl-mobile):not(.rl-no-preview-pane) #V-MailMessageView .top-toolbar {
visibility: hidden;
}
html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
display: none;
} }
.messageView { .messageView {
background-color: #fff; background-color: var(--panel-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa); border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px); border-radius: var(--border-radius, 5px);
height: calc(100% - 50px); height: calc(100% - 50px);
@ -140,7 +126,7 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
.messageItemHeader { .messageItemHeader {
background-color: #f8f8f8; background-color: var(--message-header-bg-clr, #eee);
border-bottom: 1px solid var(--border-color, #bbb); border-bottom: 1px solid var(--border-color, #bbb);
border-radius: var(--border-radius, 5px) var(--border-radius, 5px) 0 0; border-radius: var(--border-radius, 5px) var(--border-radius, 5px) 0 0;
padding: 10px; padding: 10px;
@ -215,7 +201,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
} }
.openpgp-control { .openpgp-control {
margin: 0.5em;
padding: 0.5em; padding: 0.5em;
span { span {
@ -248,9 +233,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
display: none; display: none;
} }
} }
.b-text-part > iframe {
min-height: 50vh;
}
.thread-controls { .thread-controls {
.dropdown-toggle { .dropdown-toggle {
@ -271,7 +253,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
#messageItem { #messageItem {
color: #000;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
scroll-behavior: smooth; scroll-behavior: smooth;
@ -283,11 +264,13 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
padding: 50px 0; padding: 50px 0;
} }
.bodySubHeader {
background-color: var(--message-header-bg-clr, #eee);
}
.bodySubHeader > * { .bodySubHeader > * {
background-color: #eee;
border-bottom: 1px solid var(--border-color, #bbb); border-bottom: 1px solid var(--border-color, #bbb);
cursor: pointer; cursor: pointer;
padding: 10px 15px; padding: 10px;
} }
.pgpInfo { .pgpInfo {
@ -300,14 +283,8 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
} }
} }
.readReceipt {
background-color: #ffffd9;
}
.attachmentsPlace { .attachmentsPlace {
padding: 10px 10px 6px 10px;
background: #eee;
border-bottom: 1px solid var(--border-color, #bbb); border-bottom: 1px solid var(--border-color, #bbb);
position: relative; position: relative;
@ -336,9 +313,8 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
} }
.attachmentsControls { .attachmentsControls {
padding: 7px 5px 7px 14px; padding-top: 7px;
background: #e8e8e8; border-top: 1px solid var(--border-color, #bbb);
border-bottom: 1px solid var(--border-color, #bbb);
} }
.attachmentsControls > * { .attachmentsControls > * {
display: inline-block; display: inline-block;
@ -365,8 +341,12 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
height: 100%; height: 100%;
padding: 10px; padding: 10px;
* {
box-sizing: unset;
}
a { a {
color: blue; color: var(--link-color, blue);
text-decoration: underline; text-decoration: underline;
&:visited { &:visited {
@ -382,6 +362,10 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
opacity: 0.8; opacity: 0.8;
padding: 0 10px; padding: 0 10px;
margin: 0; margin: 0;
blockquote {
opacity: 1;
}
} }
&.html { &.html {
@ -471,15 +455,34 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
} }
} }
html.rl-no-preview-pane .messageView { .mailvelope iframe {
border: 1px solid var(--border-color, #aaa); min-height: 400px;
box-shadow: var(--smMainShadow);
} }
html.rl-bottom-preview-pane .messageView { html:not(.sm-msgView-bottom):not(.sm-msgView-side) {
height: 100%; #rl-right:not(.message-selected) #V-MailMessageView {
display: none;
}
} }
@media screen and (max-width: 1400px) {
html.sm-msgView-side #V-MailMessageView .top-toolbar {
visibility: hidden;
}
}
html.sm-msgView-bottom {
#V-MailMessageView .top-toolbar {
display: none;
}
.messageView {
height: 100%;
}
}
/*
@media all and (display-mode: fullscreen) {
*/
html.rl-fullscreen { html.rl-fullscreen {
#rl-left, #rl-left,

View file

@ -3,9 +3,10 @@
} }
#V-PopupsOpenPgpKey { #V-PopupsOpenPgpKey {
.key-viewer { pre {
margin: 0; margin: 0;
max-height: 500px; max-height: 500px;
padding: 0;
overflow: auto; overflow: auto;
} }
} }
@ -13,9 +14,8 @@
#V-PopupsOpenPgpImport { #V-PopupsOpenPgpImport {
max-width: 645px; max-width: 645px;
.inputKey { textarea {
font-family: var(--fontMono); font-family: var(--fontMono);
width: 600px; width: 600px;
} }
} }

View file

@ -1,24 +1,5 @@
#V-SettingsMenu { #V-SettingsMenu {
.b-footer {
position: absolute;
bottom: 10px;
right: 0;
left: 0;
padding: 0 10px 0 5px;
z-index: 101;
}
.b-content {
position: absolute;
top: 50px + @rlLowMargin + 10px;
bottom: @rlLowMargin;
left: 0;
right: 0;
overflow: hidden;
}
nav { nav {
a { a {
@ -51,15 +32,20 @@
#V-SettingsPane { #V-SettingsPane {
height: calc(100% - 50px - @rlLowMargin); height: calc(100% - 50px - @rlLowMargin);
margin: 50px @rlLowMargin 0 0; margin: 50px @rlLowMargin 0 0;
padding:20px;
width: 100%; width: 100%;
background-color: var(--panel-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px);
box-shadow: var(--smMainShadow);
overflow-y: auto;
scroll-behavior: smooth;
.btn-toolbar { .btn-toolbar {
position: absolute; position: absolute;
top: 0; top: 8px;
right: 0; left: @rlLowMargin;
left: 0;
padding: 8px 0;
color: #fff;
} }
td { td {
@ -68,22 +54,7 @@
} }
} }
.rl-mobile #V-SettingsPane { .firefox-drag #V-SettingsPane,
margin-right: 0;
}
#rl-settings-subscreen {
background-color: var(--settings-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px);
box-shadow: var(--smMainShadow);
height: 100%;
overflow-y: auto;
padding:20px;
scroll-behavior: smooth;
}
.firefox-drag #rl-settings-subscreen,
.firefox-drag .modal-body { .firefox-drag .modal-body {
overflow: hidden; overflow: hidden;
} }

View file

@ -1,8 +1,6 @@
#top-system-dropdown-id { #top-system-dropdown-id {
overflow: hidden; padding: 0 6px;
padding-left: 8px;
padding-right: 6px;
} }
.rl-left-panel-disabled #more-list-dropdown-id + .dropdown-menu { .rl-left-panel-disabled #more-list-dropdown-id + .dropdown-menu {
@ -50,16 +48,15 @@
} }
.accountPlace { .accountPlace {
background-color: rgba(0, 0, 0, 0.5); border-right: 1px solid var(--btn-border-clr, rgba(0, 0, 0, 0.2));
border-radius: 4px;
color: #fff;
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 16px;
line-height: 29px; line-height: 22px;
margin: 0 6px -3px 0;
max-width: 25vw; max-width: 25vw;
overflow: hidden; overflow-x: hidden;
padding: 1px 8px; padding-right: 6px;
text-overflow: ellipsis; text-overflow: ellipsis;
text-shadow: 0 1px 0 #000; text-shadow: 0 1px 0 #000;
white-space: nowrap; white-space: nowrap;

View file

@ -19,7 +19,7 @@
white-space: nowrap; white-space: nowrap;
z-index: 2001; z-index: 2001;
} }
@media only screen and (max-width: 600px) { @media screen and (max-width: @maxMobileWidth) {
[data-rainloopErrorTip]::before { [data-rainloopErrorTip]::before {
min-width: 60vw; min-width: 60vw;
white-space: normal; white-space: normal;

View file

@ -2,11 +2,16 @@
@rlLeftWidth: 200px; @rlLeftWidth: 200px;
@rlLowMargin: 8px; @rlLowMargin: 8px;
/* mobile and tablet */
@maxMobileWidth: 799px;
:root { :root {
--smDialogShrink: 20px; --smDialogShrink: 20px;
--smMainShadow: 0 2px 8px rgba(0, 0, 0, 0.2); --smMainShadow: 0 2px 8px rgba(0, 0, 0, 0.2);
} }
.rl-mobile { /* For Firefox */
/* For Firefox */ @media screen and (max-width: @maxMobileWidth) {
--smDialogShrink: 25px; :root {
--smDialogShrink: 25px;
}
} }

View file

@ -1,7 +1,7 @@
import ko from 'ko'; import ko from 'ko';
import { import {
Notification, Notifications,
UploadErrorCode UploadErrorCode
} from 'Common/Enums'; } from 'Common/Enums';
@ -11,7 +11,7 @@ import {
FolderType FolderType
} from 'Common/EnumsUser'; } from 'Common/EnumsUser';
import { pInt, isArray, arrayLength } from 'Common/Utils'; import { pInt, isArray, arrayLength, b64Encode } from 'Common/Utils';
import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html'; import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko'; import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
@ -19,8 +19,8 @@ import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { folderInformation, 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 { setFolderETag } from 'Common/Cache';
import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut, createElement } from 'Common/Globals'; import { 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';
@ -57,7 +57,7 @@ const
tpl = createElement('template'), tpl = createElement('template'),
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'), base64_encode = text => b64Encode(text).match(/.{1,76}/g).join('\r\n'),
getEmail = value => addressparser(value)[0]?.email || false, getEmail = value => addressparser(value)[0]?.email || false,
@ -72,7 +72,7 @@ const
reloadDraftFolder = () => { reloadDraftFolder = () => {
const draftsFolder = FolderUserStore.draftsFolder(); const draftsFolder = FolderUserStore.draftsFolder();
if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) { if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) {
setFolderHash(draftsFolder, ''); setFolderETag(draftsFolder, '');
if (FolderUserStore.currentFolderFullName() === draftsFolder) { if (FolderUserStore.currentFolderFullName() === draftsFolder) {
MessagelistUserStore.reload(true); MessagelistUserStore.reload(true);
} else { } else {
@ -441,16 +441,6 @@ export class ComposePopupView extends AbstractViewPopup {
this.sendError(false); this.sendError(false);
this.sending(true); this.sending(true);
if (3 === arrayLength(this.aDraftInfo)) {
const flagsCache = MessageFlagsCache.getFor(this.aDraftInfo[2], this.aDraftInfo[1]);
if (isArray(flagsCache)) {
flagsCache.push(('forward' === this.aDraftInfo[0]) ? '$forwarded' : '\\answered');
MessageFlagsCache.setFor(this.aDraftInfo[2], this.aDraftInfo[1], flagsCache);
MessagelistUserStore.reloadFlagsAndCachedMessage();
setFolderHash(this.aDraftInfo[2], '');
}
}
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder; sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
this.getMessageRequestParams(sSentFolder).then(params => { this.getMessageRequestParams(sSentFolder).then(params => {
@ -458,19 +448,23 @@ export class ComposePopupView extends AbstractViewPopup {
(iError, data) => { (iError, data) => {
this.sending(false); this.sending(false);
if (iError) { if (iError) {
if (Notification.CantSaveMessage === iError) { if (Notifications.CantSaveMessage === iError) {
this.sendSuccessButSaveError(true); this.sendSuccessButSaveError(true);
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?.ErrorMessage) this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|| getNotification(Notification.CantSendMessage)); || getNotification(Notifications.CantSendMessage));
} }
} else { } else {
this.close(); this.close();
} }
setFolderHash(this.draftsFolder(), ''); setFolderETag(this.draftsFolder(), '');
setFolderHash(sSentFolder, ''); setFolderETag(sSentFolder, '');
if (3 === arrayLength(this.aDraftInfo)) {
const folder = this.aDraftInfo[2];
setFolderETag(folder, '');
}
reloadDraftFolder(); reloadDraftFolder();
}, },
params, params,
@ -492,57 +486,59 @@ export class ComposePopupView extends AbstractViewPopup {
} }
saveCommand() { saveCommand() {
if (FolderUserStore.draftsFolderNotEnabled()) { if (!this.saving() && !this.sending()) {
showScreenPopup(FolderSystemPopupView, [FolderType.Drafts]); if (FolderUserStore.draftsFolderNotEnabled()) {
} else { showScreenPopup(FolderSystemPopupView, [FolderType.Drafts]);
this.savedError(false); } else {
this.saving(true); this.savedError(false);
this.autosaveStart(); this.saving(true);
this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => { this.autosaveStart();
Remote.request('SaveMessage', this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => {
(iError, oData) => { Remote.request('SaveMessage',
let result = false; (iError, oData) => {
let result = false;
this.saving(false); this.saving(false);
if (!iError) { if (!iError) {
if (oData.Result.folder && oData.Result.uid) { if (oData.Result.folder && oData.Result.uid) {
result = true; result = true;
if (this.bFromDraft) { if (this.bFromDraft) {
const message = MessageUserStore.message(); const message = MessageUserStore.message();
if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) { if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) {
MessageUserStore.message(null); MessageUserStore.message(null);
}
} }
this.draftsFolder(oData.Result.folder);
this.draftUid(oData.Result.uid);
this.savedTime(new Date);
if (this.bFromDraft) {
setFolderETag(this.draftsFolder(), '');
}
setFolderETag(FolderUserStore.draftsFolder(), '');
} }
this.draftsFolder(oData.Result.folder);
this.draftUid(oData.Result.uid);
this.savedTime(new Date);
if (this.bFromDraft) {
setFolderHash(this.draftsFolder(), '');
}
setFolderHash(FolderUserStore.draftsFolder(), '');
} }
}
if (!result) { if (!result) {
this.savedError(true); this.savedError(true);
this.savedErrorDesc(getNotification(Notification.CantSaveMessage)); this.savedErrorDesc(getNotification(Notifications.CantSaveMessage));
} }
reloadDraftFolder(); reloadDraftFolder();
}, },
params, params,
200000 200000
); );
}).catch(e => { }).catch(e => {
this.saving(false); this.saving(false);
this.savedError(true); this.savedError(true);
this.savedErrorDesc(getNotification(Notification.CantSaveMessage) + ': ' + e); this.savedErrorDesc(getNotification(Notifications.CantSaveMessage) + ': ' + e);
}); });
}
} }
} }
@ -569,12 +565,7 @@ export class ComposePopupView extends AbstractViewPopup {
skipCommand() { skipCommand() {
ComposePopupView.inEdit(true); ComposePopupView.inEdit(true);
if ( if (!FolderUserStore.draftsFolderNotEnabled() && SettingsUserStore.allowDraftAutosave()) {
!this.saving() &&
!this.sending() &&
!FolderUserStore.draftsFolderNotEnabled() &&
SettingsUserStore.allowDraftAutosave()
) {
this.saveCommand(); this.saveCommand();
} }
@ -597,8 +588,6 @@ export class ComposePopupView extends AbstractViewPopup {
&& !FolderUserStore.draftsFolderNotEnabled() && !FolderUserStore.draftsFolderNotEnabled()
&& SettingsUserStore.allowDraftAutosave() && SettingsUserStore.allowDraftAutosave()
&& !this.isEmptyForm(false) && !this.isEmptyForm(false)
&& !this.saving()
&& !this.sending()
&& !this.savedError() && !this.savedError()
) { ) {
this.saveCommand(); this.saveCommand();
@ -617,7 +606,7 @@ export class ComposePopupView extends AbstractViewPopup {
data.Result.map(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 (Notifications.RequestAborted !== iError) {
fResponse([]); fResponse([]);
} }
}, },
@ -870,9 +859,9 @@ export class ComposePopupView extends AbstractViewPopup {
// https://github.com/the-djmaze/snappymail/issues/491 // https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = message.bodyAsHTML(); tpl.innerHTML = message.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img => tpl.content.querySelectorAll('img').forEach(img => {
img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title) img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
); });
sText = tpl.innerHTML.trim(); sText = tpl.innerHTML.trim();
switch (msgComposeType) { switch (msgComposeType) {
@ -1010,7 +999,7 @@ export class ComposePopupView extends AbstractViewPopup {
clickElement: dom.querySelector('#composeUploadButton'), clickElement: dom.querySelector('#composeUploadButton'),
dragAndDropElement: dom.querySelector('.b-attachment-place') dragAndDropElement: dom.querySelector('.b-attachment-place')
}), }),
attachmentSizeLimit = pInt(SettingsGet('AttachmentLimit')); attachmentSizeLimit = pInt(SettingsGet('attachmentLimit'));
oJua oJua
.on('onDragEnter', () => { .on('onDragEnter', () => {
@ -1124,12 +1113,12 @@ export class ComposePopupView extends AbstractViewPopup {
return false; return false;
}); });
if (Settings.app('allowCtrlEnterOnCompose')) { addShortcut('enter', 'meta', ScopeCompose, () => {
addShortcut('enter', 'meta', ScopeCompose, () => { // if (SettingsUserStore.allowCtrlEnterOnCompose()) {
this.sendCommand(); this.sendCommand();
return false; return false;
}); // }
} });
addShortcut('mailsend', '', ScopeCompose, () => { addShortcut('mailsend', '', ScopeCompose, () => {
this.sendCommand(); this.sendCommand();
return false; return false;
@ -1297,7 +1286,7 @@ export class ComposePopupView extends AbstractViewPopup {
*/ */
let text = this.oEditor.getData(), let text = this.oEditor.getData(),
encrypted = PgpUserStore.isEncrypted(text), encrypted = PgpUserStore.isEncrypted(text),
size = SettingsGet('PhpUploadSizes')['post_max_size'], size = SettingsGet('phpUploadSizes')['post_max_size'],
quota = pInt(size); quota = pInt(size);
switch (size.slice(-1)) { switch (size.slice(-1)) {
case 'G': quota *= 1024; // fallthrough case 'G': quota *= 1024; // fallthrough
@ -1409,7 +1398,10 @@ export class ComposePopupView extends AbstractViewPopup {
encrypt = this.pgpEncrypt() && this.canPgpEncrypt(), encrypt = this.pgpEncrypt() && this.canPgpEncrypt(),
isHtml = this.oEditor.isHtml(); isHtml = this.oEditor.isHtml();
let Text = this.oEditor.getData(); let Text = this.oEditor.getData().trim();
if (!Text.length) {
throw 'Message body is empty';
}
if (isHtml) { if (isHtml) {
let l; let l;
do { do {

View file

@ -1,6 +1,6 @@
import { koComputable, addObservablesTo } from 'External/ko'; import { koComputable, addObservablesTo } from 'External/ko';
import { Notification } from 'Common/Enums'; import { Notifications } from 'Common/Enums';
import { defaultOptionsAfterRender } from 'Common/Utils'; import { defaultOptionsAfterRender } from 'Common/Utils';
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders'; import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
@ -65,7 +65,7 @@ export class FolderCreatePopupView extends AbstractViewPopup {
}, },
error => { error => {
FolderUserStore.folderListError( FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantCreateFolder) getNotification(error.code, '', Notifications.CantCreateFolder)
+ '.\n' + error.message); + '.\n' + error.message);
} }
); );

View file

@ -1,4 +1,5 @@
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import { addObservablesTo } from 'External/ko';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
@ -6,49 +7,34 @@ import Remote from 'Remote/User/Fetch';
import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { IdentityModel } from 'Model/Identity';
export class IdentityPopupView extends AbstractViewPopup { export class IdentityPopupView extends AbstractViewPopup {
constructor() { constructor() {
super('Identity'); super('Identity');
this.id = '';
addObservablesTo(this, { addObservablesTo(this, {
id: '',
edit: false, edit: false,
owner: false,
email: '', email: '',
emailFocused: false, emailFocused: false,
name: '', name: '',
nameFocused: false,
replyTo: '', replyTo: '',
replyToFocused: false, showReplyTo: false,
bcc: '', bcc: '',
bccFocused: false, showBcc: false,
bccHasError: false,
signature: '', signature: '',
signatureInsertBefore: false, signatureInsertBefore: false,
showBcc: false,
showReplyTo: false,
submitRequest: false, submitRequest: false,
submitError: '' submitError: ''
}); });
addSubscribablesTo(this, {
replyTo: value => {
if (false === this.showReplyTo() && value.length) {
this.showReplyTo(true);
}
},
bcc: value => {
if (false === this.showBcc() && value.length) {
this.showBcc(true);
}
}
});
/* /*
this.email.valueHasMutated(); this.email.valueHasMutated();
this.replyTo.valueHasMutated(); this.replyTo.valueHasMutated();
@ -61,9 +47,8 @@ export class IdentityPopupView extends AbstractViewPopup {
this.signature?.__fetchEditorValue?.(); this.signature?.__fetchEditorValue?.();
this.submitRequest(true); this.submitRequest(true);
const data = new FormData(form); const data = new FormData(form);
data.set('Id', this.id); data.set('Id', this.id());
data.set('Signature', this.signature()); data.set('Signature', this.signature());
data.set('SignatureInsertBefore', this.signatureInsertBefore() ? 1 : 0);
Remote.request('IdentityUpdate', iError => { Remote.request('IdentityUpdate', iError => {
this.submitRequest(false); this.submitRequest(false);
if (iError) { if (iError) {
@ -89,32 +74,23 @@ export class IdentityPopupView extends AbstractViewPopup {
if (identity) { if (identity) {
this.edit(true); this.edit(true);
this.id = identity.id() || '';
this.name(identity.name());
this.email(identity.email());
this.replyTo(identity.replyTo());
this.bcc(identity.bcc());
this.signature(identity.signature());
this.signatureInsertBefore(identity.signatureInsertBefore());
this.owner(!this.id);
} else { } else {
this.edit(false); this.edit(false);
identity = new IdentityModel;
this.id = Jua.randomId(); identity.id(Jua.randomId());
this.name('');
this.email('');
this.replyTo('');
this.bcc('');
this.signature('');
this.signatureInsertBefore(false);
this.owner(false);
} }
this.id(identity.id() || '');
this.name(identity.name());
this.email(identity.email());
this.replyTo(identity.replyTo());
this.showReplyTo(0 < identity.replyTo().length);
this.bcc(identity.bcc());
this.showBcc(0 < identity.bcc().length);
this.signature(identity.signature());
this.signatureInsertBefore(identity.signatureInsertBefore());
} }
afterShow() { afterShow() {
this.owner() || this.emailFocused(true); this.id() ? this.emailFocused(true) : this.nameFocused(true);
} }
} }

View file

@ -1,5 +1,4 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { convertLangName } from 'Common/Translator'; import { convertLangName } from 'Common/Translator';
@ -8,46 +7,19 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
export class LanguagesPopupView extends AbstractViewPopup { export class LanguagesPopupView extends AbstractViewPopup {
constructor() { constructor() {
super('Languages'); super('Languages');
this.fLang = null; this.fLang = null;
this.userLanguage = ko.observable(''); this.languages = ko.observableArray();
this.langs = ko.observableArray();
this.languages = koComputable(() => {
const userLanguage = this.userLanguage();
return this.langs.map(language => ({
key: language,
user: language === userLanguage,
selected: ko.observable(false),
fullName: convertLangName(language)
}));
});
this.langs.subscribe(() => this.setLanguageSelection());
}
languageTooltipName(language) {
return convertLangName(language, true);
}
setLanguageSelection() {
const currentLang = this.fLang ? ko.unwrap(this.fLang) : '';
this.languages().forEach(item => item.selected(item.key === currentLang));
}
beforeShow() {
this.fLang = null;
this.userLanguage('');
this.langs([]);
} }
onShow(fLanguage, langs, userLanguage) { onShow(fLanguage, langs, userLanguage) {
this.fLang = fLanguage; this.fLang = fLanguage;
this.userLanguage(userLanguage || ''); this.languages(langs.map(language => ({
key: language,
this.langs(langs); user: userLanguage === language,
selected: fLanguage?.() === language,
fullName: convertLangName(language),
title: convertLangName(language, true)
})));
} }
changeLanguage(lang) { changeLanguage(lang) {

View file

@ -1,4 +1,4 @@
import { Notification } from 'Common/Enums'; import { Notifications } from 'Common/Enums';
import { ClientSideKeyNameLastSignMe } from 'Common/EnumsUser'; import { ClientSideKeyNameLastSignMe } from 'Common/EnumsUser';
import { SettingsGet, fireEvent } from 'Common/Globals'; import { SettingsGet, fireEvent } from 'Common/Globals';
import { getNotification, translatorReload, convertLangName } from 'Common/Translator'; import { getNotification, translatorReload, convertLangName } from 'Common/Translator';
@ -26,7 +26,7 @@ export class LoginUserView extends AbstractViewLogin {
super(); super();
addObservablesTo(this, { addObservablesTo(this, {
loadingDesc: SettingsGet('LoadingDescription'), loadingDesc: SettingsGet('loadingDescription'),
email: SettingsGet('DevEmail'), email: SettingsGet('DevEmail'),
password: SettingsGet('DevPassword'), password: SettingsGet('DevPassword'),
@ -44,7 +44,7 @@ export class LoginUserView extends AbstractViewLogin {
signMeType: SignMeUnused signMeType: SignMeUnused
}); });
this.allowLanguagesOnLogin = !!SettingsGet('AllowLanguagesOnLogin'); this.allowLanguagesOnLogin = !!SettingsGet('allowLanguagesOnLogin');
this.language = LanguageStore.language; this.language = LanguageStore.language;
this.languages = LanguageStore.languages; this.languages = LanguageStore.languages;
@ -110,8 +110,8 @@ export class LoginUserView extends AbstractViewLogin {
if (valid) { if (valid) {
this.submitRequest(true); this.submitRequest(true);
data.set('Language', this.bSendLanguage ? this.language() : ''); data.set('language', this.bSendLanguage ? this.language() : '');
data.set('SignMe', this.signMe() ? 1 : 0); data.set('signMe', this.signMe() ? 1 : 0);
Remote.request('Login', Remote.request('Login',
(iError, oData) => { (iError, oData) => {
fireEvent('sm-user-login-response', { fireEvent('sm-user-login-response', {
@ -120,11 +120,11 @@ export class LoginUserView extends AbstractViewLogin {
}); });
if (iError) { if (iError) {
this.submitRequest(false); this.submitRequest(false);
if (Notification.InvalidInputArgument == iError) { if (Notifications.InvalidInputArgument == iError) {
iError = Notification.AuthError; iError = Notifications.AuthError;
} }
this.submitError(getNotification(iError, oData?.ErrorMessage, this.submitError(getNotification(iError, oData?.ErrorMessage,
Notification.UnknownNotification)); Notifications.UnknownNotification));
this.submitErrorAdditional(oData?.ErrorMessageAdditional); this.submitErrorAdditional(oData?.ErrorMessageAdditional);
} else { } else {
rl.setData(oData.Result); rl.setData(oData.Result);
@ -142,7 +142,7 @@ export class LoginUserView extends AbstractViewLogin {
onBuild(dom) { onBuild(dom) {
super.onBuild(dom); super.onBuild(dom);
const signMe = (SettingsGet('SignMe') || '').toLowerCase(); const signMe = (SettingsGet('signMe') || '').toLowerCase();
switch (signMe) { switch (signMe) {
case 'defaultoff': case 'defaultoff':

View file

@ -1,9 +1,9 @@
import ko from 'ko'; import ko from 'ko';
import { Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { addShortcut } from 'Common/Globals'; import { addShortcut, stopEvent } from 'Common/Globals';
import { mailBox, settings } from 'Common/Links'; import { mailBox, settings } from 'Common/Links';
//import { setFolderHash } from 'Common/Cache'; //import { setFolderETag } from 'Common/Cache';
import { addComputablesTo } from 'External/ko'; import { addComputablesTo } from 'External/ko';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
@ -80,8 +80,7 @@ export class MailFolderList extends AbstractViewLeft {
setExpandedFolder(folder.fullName, collapsed); setExpandedFolder(folder.fullName, collapsed);
folder.collapsed(!collapsed); folder.collapsed(!collapsed);
event.preventDefault(); stopEvent(event);
event.stopPropagation();
return; return;
} }
} }
@ -105,7 +104,7 @@ export class MailFolderList extends AbstractViewLeft {
} }
/* /*
if (folder.fullName === FolderUserStore.currentFolderFullName()) { if (folder.fullName === FolderUserStore.currentFolderFullName()) {
setFolderHash(folder.fullName, ''); setFolderETag(folder.fullName, '');
} }
*/ */
let search = ''; let search = '';
@ -174,15 +173,6 @@ export class MailFolderList extends AbstractViewLeft {
moveAction(false); moveAction(false);
return false; return false;
}); });
AppUserStore.focusedState.subscribe(value => {
let el = qs('li a.focused');
el?.classList.remove('focused');
if (Scope.FolderList === value) {
el = qs('li a.selected');
el?.classList.add('focused');
}
});
} }
scrollToFocused() { scrollToFocused() {

View file

@ -8,7 +8,7 @@ import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
import { doc, import { doc,
leftPanelDisabled, toggleLeftPanel, leftPanelDisabled, toggleLeftPanel,
Settings, SettingsCapa, Settings, SettingsCapa,
addEventsListeners, addEventsListeners, stopEvent,
addShortcut, registerShortcut, formFieldFocused addShortcut, registerShortcut, formFieldFocused
} from 'Common/Globals'; } from 'Common/Globals';
@ -24,10 +24,7 @@ import { i18n } from 'Common/Translator';
import { dropFilesInFolder } from 'Common/Folders'; import { dropFilesInFolder } from 'Common/Folders';
import { import { getFolderFromCacheList } from 'Common/Cache';
getFolderFromCacheList,
MessageFlagsCache
} from 'Common/Cache';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
@ -90,7 +87,7 @@ export class MailMessageList extends AbstractViewRight {
this.composeInEdit = ComposePopupView.inEdit; this.composeInEdit = ComposePopupView.inEdit;
this.isMobile = ThemeStore.isMobile; this.isMobile = ThemeStore.isMobile; // Obsolete
this.leftPanelDisabled = leftPanelDisabled; this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel; this.toggleLeftPanel = toggleLeftPanel;
@ -146,10 +143,6 @@ export class MailMessageList extends AbstractViewRight {
return c && MessagelistUserStore().length > c; return c && MessagelistUserStore().length > c;
}, },
mobileCheckedStateShow: () => ThemeStore.isMobile() ? MessagelistUserStore.hasChecked() : 1,
mobileCheckedStateHide: () => ThemeStore.isMobile() ? !MessagelistUserStore.hasChecked() : 1,
listGrouped: () => { listGrouped: () => {
let uid = MessagelistUserStore.threadUid(), let uid = MessagelistUserStore.threadUid(),
sort = FolderUserStore.sortMode() || 'DATE'; sort = FolderUserStore.sortMode() || 'DATE';
@ -179,7 +172,7 @@ export class MailMessageList extends AbstractViewRight {
rtf = Intl.RelativeTimeFormat rtf = Intl.RelativeTimeFormat
? new Intl.RelativeTimeFormat(doc.documentElement.lang, { numeric: "auto" }) : 0; ? new Intl.RelativeTimeFormat(doc.documentElement.lang, { numeric: "auto" }) : 0;
MessagelistUserStore.forEach(msg => { MessagelistUserStore.forEach(msg => {
let dt = (new Date(msg.dateTimeStampInUTC() * 1000)), let dt = (new Date(msg.dateTimestamp() * 1000)),
date, date,
ymd = Ymd(dt); ymd = Ymd(dt);
if (!current || ymd != current.id) { if (!current || ymd != current.id) {
@ -367,13 +360,19 @@ export class MailMessageList extends AbstractViewRight {
]); ]);
} }
/**
* Download selected messages
*/
downloadZipCommand() { downloadZipCommand() {
let hashes = []/*, uids = []*/; let hashes = []/*, uids = []*/;
// MessagelistUserStore.forEach(message => message.checked() && uids.push(message.uid)); // MessagelistUserStore.forEach(message => message.checked() && uids.push(message.uid));
MessagelistUserStore.forEach(message => message.checked() && hashes.push(message.requestHash)); MessagelistUserStore.forEach(message => message.checked() && hashes.push(message.requestHash));
downloadZip(hashes, null, null, MessagelistUserStore().folder); downloadZip(null, hashes, null, null, MessagelistUserStore().folder);
} }
/**
* Download attachments of selected messages
*/
downloadAttachCommand() { downloadAttachCommand() {
let hashes = []; let hashes = [];
MessagelistUserStore.forEach(message => { MessagelistUserStore.forEach(message => {
@ -385,7 +384,7 @@ export class MailMessageList extends AbstractViewRight {
}); });
} }
}); });
downloadZip(hashes); downloadZip(null, hashes);
} }
deleteWithoutMoveCommand() { deleteWithoutMoveCommand() {
@ -410,10 +409,9 @@ export class MailMessageList extends AbstractViewRight {
} }
moveCommand(vm, event) { moveCommand(vm, event) {
if (this.mobileCheckedStateShow()) { if (MessagelistUserStore.hasChecked()) {
if (vm && event?.preventDefault) { if (vm && event?.preventDefault) {
event.preventDefault(); stopEvent(event);
event.stopPropagation();
} }
let b = moveAction(); let b = moveAction();
@ -475,15 +473,11 @@ export class MailMessageList extends AbstractViewRight {
folder.unreadEmails(0); folder.unreadEmails(0);
} }
MessageFlagsCache.clearFolder(sFolderFullName);
Remote.request('MessageSetSeenToAll', null, { Remote.request('MessageSetSeenToAll', null, {
folder: sFolderFullName, folder: sFolderFullName,
setAction: 1, setAction: 1,
threadUids: uids.join(',') threadUids: uids.join(',')
}); });
MessagelistUserStore.reloadFlagsAndCachedMessage();
} }
} }
} }
@ -574,7 +568,7 @@ export class MailMessageList extends AbstractViewRight {
fToggle = () => { fToggle = () => {
let layout = SettingsUserStore.layout(); let layout = SettingsUserStore.layout();
setLayoutResizer(top, ClientSideKeyNameMessageListSize, setLayoutResizer(top, ClientSideKeyNameMessageListSize,
(ThemeStore.isMobile() || Layout.NoPreview === layout) (ThemeStore.isMobile() || !layout)
? 0 ? 0
: (Layout.SidePreview === layout ? 'Width' : 'Height') : (Layout.SidePreview === layout ? 'Width' : 'Height')
); );

View file

@ -13,11 +13,11 @@ import {
import { import {
elementById, elementById,
leftPanelDisabled,
keyScopeReal, keyScopeReal,
Settings, Settings,
SettingsCapa, SettingsCapa,
fireEvent, fireEvent,
stopEvent,
addShortcut, addShortcut,
registerShortcut registerShortcut
} from 'Common/Globals'; } from 'Common/Globals';
@ -30,15 +30,12 @@ import { SMAudio } from 'Common/Audio';
import { i18n } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { MessageFlagsCache } from 'Common/Cache';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder'; import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import * as Local from 'Storage/Client'; import * as Local from 'Storage/Client';
@ -128,7 +125,6 @@ export class MailMessageView extends AbstractViewRight {
this.message = currentMessage; this.message = currentMessage;
this.messageLoadingThrottle = MessageUserStore.loading; this.messageLoadingThrottle = MessageUserStore.loading;
this.messagesBodiesDom = MessageUserStore.bodiesDom;
this.messageError = MessageUserStore.error; this.messageError = MessageUserStore.error;
this.fullScreenMode = isFullscreen; this.fullScreenMode = isFullscreen;
@ -166,7 +162,7 @@ export class MailMessageView extends AbstractViewRight {
listAttachments: () => currentMessage()?.attachments() listAttachments: () => currentMessage()?.attachments()
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()), .filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
hasAttachments: () => this.listAttachments().length, hasAttachments: () => this.listAttachments()?.length,
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(), canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(),
@ -280,23 +276,19 @@ export class MailMessageView extends AbstractViewRight {
onBuild(dom) { onBuild(dom) {
const eqs = (ev, s) => ev.target.closestWithin(s, dom); const eqs = (ev, s) => ev.target.closestWithin(s, dom);
dom.addEventListener('click', event => { dom.addEventListener('click', event => {
ThemeStore.isMobile() && leftPanelDisabled(true);
let el = eqs(event, 'a'); let el = eqs(event, 'a');
if (el && 0 === event.button && mailToHelper(el.href)) { if (el && 0 === event.button && mailToHelper(el.href)) {
event.preventDefault(); stopEvent(event);
event.stopPropagation();
return; return;
} }
if (eqs(event, '.attachmentsPlace .showPreview')) { if (eqs(event, '.attachmentsPlace .showPreview')) {
event.stopPropagation();
return; return;
} }
el = eqs(event, '.attachmentsPlace .showPreplay'); el = eqs(event, '.attachmentsPlace .showPreplay');
if (el) { if (el) {
event.stopPropagation(); stopEvent(event);
const attachment = ko.dataFor(el); const attachment = ko.dataFor(el);
if (attachment && SMAudio.supported) { if (attachment && SMAudio.supported) {
switch (true) { switch (true) {
@ -343,13 +335,6 @@ export class MailMessageView extends AbstractViewRight {
} }
}); });
AppUserStore.focusedState.subscribe(value => {
if (Scope.MessageView !== value) {
this.scrollMessageToTop();
this.scrollMessageToLeft();
}
});
keyScopeReal.subscribe(value => this.messageDomFocused(Scope.MessageView === value)); keyScopeReal.subscribe(value => this.messageDomFocused(Scope.MessageView === value));
// initShortcuts // initShortcuts
@ -459,10 +444,12 @@ export class MailMessageView extends AbstractViewRight {
} }
return false; return false;
}); });
MessageUserStore.bodiesDom(dom.querySelector('.bodyText'));
} }
scrollMessageToTop() { scrollMessageToTop() {
oMessageScrollerDom().scrollTop = (50 < oMessageScrollerDom().scrollTop) ? 50 : 0; oMessageScrollerDom().scrollTop = 0;
} }
scrollMessageToLeft() { scrollMessageToLeft() {
@ -479,7 +466,9 @@ export class MailMessageView extends AbstractViewRight {
const hashes = (currentMessage()?.attachments || []) const hashes = (currentMessage()?.attachments || [])
.map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '') .map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '')
.filter(v => v); .filter(v => v);
downloadZip(hashes, downloadZip(
currentMessage().subject(),
hashes,
() => this.downloadAsZipError(true), () => this.downloadAsZipError(true),
this.downloadAsZipLoading this.downloadAsZipLoading
); );
@ -524,8 +513,6 @@ export class MailMessageView extends AbstractViewRight {
if (!iError) { if (!iError) {
oMessage.flags.push('$mdnsent'); oMessage.flags.push('$mdnsent');
// oMessage.flags.valueHasMutated(); // oMessage.flags.valueHasMutated();
MessageFlagsCache.store(oMessage);
MessagelistUserStore.reloadFlagsAndCachedMessage();
} }
}, { }, {
messageFolder: oMessage.folder, messageFolder: oMessage.folder,

View file

@ -1,5 +1,6 @@
import { settings } from 'Common/Links'; import { settings } from 'Common/Links';
import { mailbox } from 'Common/Links';
import { getFolderInboxName } from 'Common/Cache';
import { AbstractViewLeft } from 'Knoin/AbstractViews'; import { AbstractViewLeft } from 'Knoin/AbstractViews';
export class SettingsMenuUserView extends AbstractViewLeft { export class SettingsMenuUserView extends AbstractViewLeft {
@ -15,4 +16,8 @@ export class SettingsMenuUserView extends AbstractViewLeft {
link(route) { link(route) {
return settings(route); return settings(route);
} }
backToInbox() {
hasher.setHash(mailbox(getFolderInboxName()));
}
} }

View file

@ -1,5 +1,3 @@
import { mailbox } from 'Common/Links';
import { getFolderInboxName } from 'Common/Cache';
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals'; import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
@ -11,7 +9,6 @@ export class SettingsPaneUserView extends AbstractViewRight {
constructor() { constructor() {
super(); super();
this.isMobile = ThemeStore.isMobile;
this.leftPanelDisabled = leftPanelDisabled; this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel; this.toggleLeftPanel = toggleLeftPanel;
} }
@ -25,8 +22,4 @@ export class SettingsPaneUserView extends AbstractViewRight {
ThemeStore.isMobile() && !event.target.closestWithin('.toggleLeft', dom) && leftPanelDisabled(true) ThemeStore.isMobile() && !event.target.closestWithin('.toggleLeft', dom) && leftPanelDisabled(true)
); );
} }
backToInbox() {
hasher.setHash(mailbox(getFolderInboxName()));
}
} }

View file

@ -12,13 +12,10 @@ import { KeyboardShortcutsHelpPopupView } from 'View/Popup/KeyboardShortcutsHelp
import { AccountPopupView } from 'View/Popup/Account'; import { AccountPopupView } from 'View/Popup/Account';
import { ContactsPopupView } from 'View/Popup/Contacts'; import { ContactsPopupView } from 'View/Popup/Contacts';
import { doc, leftPanelDisabled, fireEvent, SettingsCapa, registerShortcut } from 'Common/Globals'; import { fireEvent, stopEvent, SettingsCapa, registerShortcut } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
//import { clearCache } from 'Common/Cache';
//import { koComputable } from 'External/ko'; //import { koComputable } from 'External/ko';
import { addObservablesTo } from 'External/ko'; import { addObservablesTo } from 'External/ko';
@ -56,8 +53,7 @@ export class SystemDropDownUserView extends AbstractViewRight {
let email = 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(); stopEvent(event);
event.stopPropagation();
Remote.request('AccountSwitch', Remote.request('AccountSwitch',
(iError/*, oData*/) => { (iError/*, oData*/) => {
if (iError) { if (iError) {
@ -69,10 +65,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
} else { } else {
/* // Not working yet /* // Not working yet
forEachObjectEntry(oData.Result, (key, value) => rl.settings.set(key, value)); forEachObjectEntry(oData.Result, (key, value) => rl.settings.set(key, value));
clearCache(); // MessageUserStore.message();
// MessageUserStore.setMessage(); // MessageUserStore.purgeCache();
// MessageUserStore.purgeMessageBodyCache();
// MessageUserStore.hideMessageBodies();
MessagelistUserStore([]); MessagelistUserStore([]);
// FolderUserStore.folderList([]); // FolderUserStore.folderList([]);
loadFolders(value => { loadFolders(value => {
@ -113,14 +107,6 @@ export class SystemDropDownUserView extends AbstractViewRight {
this.allowContacts && showScreenPopup(ContactsPopupView); this.allowContacts && showScreenPopup(ContactsPopupView);
} }
toggleLayout()
{
const mobile = !ThemeStore.isMobile();
doc.cookie = 'rllayout=' + (mobile ? 'mobile' : 'desktop') + '; samesite=strict';
ThemeStore.isMobile(mobile);
leftPanelDisabled(mobile);
}
logoutClick() { logoutClick() {
rl.app.logout(); rl.app.logout();
} }

View file

@ -3,30 +3,26 @@
const const
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path, qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
eId = id => doc.getElementById('rl-'+id), eId = id => doc.getElementById('rl-'+id),
app = eId('app'), admin = '1' == eId('app')?.dataset?.admin,
admin = app && '1' == app.dataset.admin,
layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
showError = msg => { toggle = div => {
let div = eId('loading-error');
div.append(msg);
eId('loading').hidden = true; eId('loading').hidden = true;
div.hidden = false; div.hidden = false;
}, },
showError = msg => {
let div = eId('loading-error');
div.append(msg);
toggle(div);
},
loadScript = src => { loadScript = src => src ? new Promise((resolve, reject) => {
if (!src) {
throw new Error('src should not be empty.');
}
return new Promise((resolve, reject) => {
const script = doc.createElement('script'); const script = doc.createElement('script');
script.onload = () => resolve(); script.onload = () => resolve();
script.onerror = () => reject('Failed loading ' + src); script.onerror = () => reject('Failed loading ' + src);
script.src = src; script.src = src;
// script.async = true; // script.async = true;
doc.head.append(script); doc.head.append(script);
}); }) : Promise.reject('src is empty');
};
try { try {
let smctoken = doc.cookie.match(/(^|;) ?smctoken=([^;]+)/); let smctoken = doc.cookie.match(/(^|;) ?smctoken=([^;]+)/);
@ -37,8 +33,6 @@ try {
smctoken = encodeURIComponent(btoa(String.fromCharCode(...data))); smctoken = encodeURIComponent(btoa(String.fromCharCode(...data)));
} }
localStorage.setItem('smctoken', smctoken); localStorage.setItem('smctoken', smctoken);
// doc.cookie = `smctoken=${smctoken};path=${doc.location.pathname};samesite=strict;secure";
// doc.cookie = 'smctoken='+smctoken+";path=/;samesite=lax";
doc.cookie = 'smctoken='+smctoken+";path=/;samesite=strict"; doc.cookie = 'smctoken='+smctoken+";path=/;samesite=strict";
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -46,8 +40,6 @@ try {
let RL_APP_DATA = {}; let RL_APP_DATA = {};
doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout[2] || (!layout && 1000 > innerWidth));
window.rl = { window.rl = {
adminArea: () => admin, adminArea: () => admin,
@ -58,9 +50,94 @@ window.rl = {
}, },
setTitle: title => setTitle: title =>
doc.title = (title || '') + (RL_APP_DATA.Title ? (title ? ' - ' : '') + RL_APP_DATA.Title : ''), doc.title = (title || '') + (RL_APP_DATA.title ? (title ? ' - ' : '') + RL_APP_DATA.title : ''),
initData: appData => { setData: appData => {
RL_APP_DATA = appData;
rl.app.refresh();
},
loadScript: loadScript,
fetch: (resource, init, postData) => {
init = Object.assign({
mode: 'same-origin',
cache: 'no-cache',
redirect: 'error',
referrerPolicy: 'no-referrer',
credentials: 'same-origin',
headers: {}
}, init);
let asJSON = 1,
XToken = RL_APP_DATA.System?.token,
object = {};
if (postData) {
init.method = 'POST';
if (postData instanceof FormData) {
postData.forEach((value, key) => {
if (value instanceof File) {
asJSON = 0;
} else if (!Reflect.has(object, key)) {
object[key] = value;
} else {
Array.isArray(object[key]) || (object[key] = [object[key]]);
object[key].push(value);
}
});
if (asJSON) {
postData = object;
// postData.XToken = XToken;
} else {
XToken && postData.set('XToken', XToken);
}
}
if (asJSON) {
init.headers['Content-Type'] = 'application/json';
postData = JSON.stringify(postData);
}
init.body = postData;
}
XToken && (init.headers['X-SM-Token'] = XToken);
// init.headers = new Headers(init.headers);
return fetch(resource, init);
},
fetchJSON: (resource, init, postData) => {
init = Object.assign({ headers: {} }, init);
init.headers.Accept = 'application/json';
return rl.fetch(resource, init, postData).then(response => {
if (response.ok) {
/* TODO: use this for non-developers?
response.clone()
let data = response.text();
try {
return JSON.parse(data);
} catch (e) {
console.error(e);
// console.log(data);
return Promise.reject(Notifications.JsonParse);
return {
Result: false,
ErrorCode: 952, // Notifications.JsonParse
ErrorMessage: e.message,
ErrorMessageAdditional: data
}
}
*/
return response.json();
}
return Promise.reject('Network response error: ' + response.status);
});
}
};
if (!navigator.cookieEnabled) {
toggle(eId('NoCookie'));
} else if (![].flat) {
toggle(eId('BadBrowser'));
} else {
rl.fetchJSON(qUri(`${admin ? 'Admin' : ''}AppData/0/${Math.random().toString().slice(2)}/`))
.then(appData => {
RL_APP_DATA = appData; RL_APP_DATA = appData;
const url = appData.StaticLibsJs, const url = appData.StaticLibsJs,
cb = () => rl.app.bootstart(); cb = () => rl.app.bootstart();
@ -68,31 +145,14 @@ window.rl = {
.then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`))) .then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
.then(() => appData.PluginsLink ? loadScript(qUri(appData.PluginsLink)) : Promise.resolve()) .then(() => appData.PluginsLink ? loadScript(qUri(appData.PluginsLink)) : Promise.resolve())
.then(() => rl.app .then(() => rl.app
? cb() ? cb()
: doc.addEventListener('readystatechange', () => 'complete' == doc.readyState && cb()) : doc.addEventListener('readystatechange', () => 'complete' == doc.readyState && cb())
) )
.catch(e => { .catch(e => {
showError(e); showError(e);
throw e; throw e;
}); });
}, })
setData: appData => {
RL_APP_DATA = appData;
rl.app.refresh();
},
loadScript: loadScript
};
if (!navigator.cookieEnabled) {
eId('loading').hidden = true;
eId('NoCookie').hidden = false;
} else if (![].flat) {
eId('loading').hidden = true;
eId('BadBrowser').hidden = false;
} else {
loadScript(qUri(`${admin ? 'Admin' : ''}AppData/0/${Math.random().toString().slice(2)}/`))
.catch(e => showError(e)); .catch(e => showError(e));
} }

64
dev/bootstrap.js vendored
View file

@ -1,10 +1,6 @@
import { Settings } from 'Common/Globals';
import { i18n } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { root } from 'Common/Links'; import { root } from 'Common/Links';
import { isArray } from 'Common/Utils';
export default App => { export default App => {
rl.app = App; rl.app = App;
@ -33,64 +29,4 @@ export default App => {
on: () => hasher.active = true on: () => hasher.active = true
}; };
rl.fetch = (resource, init, postData) => {
init = Object.assign({
mode: 'same-origin',
cache: 'no-cache',
redirect: 'error',
referrerPolicy: 'no-referrer',
credentials: 'same-origin',
headers: {}
}, init);
if (postData) {
init.method = 'POST';
init.headers['Content-Type'] = 'application/json';
if (postData instanceof FormData) {
const object = {};
postData.forEach((value, key) => {
if (!Reflect.has(object, key)){
object[key] = value;
} else {
isArray(object[key]) || (object[key] = [object[key]]);
object[key].push(value);
}
});
postData = object;
}
postData.XToken = Settings.app('token');
init.body = JSON.stringify(postData);
}
init.headers['X-SM-Token'] = Settings.app('token');
// init.headers = new Headers(init.headers);
return fetch(resource, init);
};
rl.fetchJSON = (resource, init, postData) => {
init = Object.assign({ headers: {} }, init);
init.headers.Accept = 'application/json';
return rl.fetch(resource, init, postData).then(response => {
if (!response.ok) {
return Promise.reject('Network response error: ' + response.status);
}
/* TODO: use this for non-developers?
response.clone()
let data = response.text();
try {
return JSON.parse(data);
} catch (e) {
console.error(e);
// console.log(data);
return Promise.reject(Notification.JsonParse);
return {
Result: false,
ErrorCode: 952, // Notification.JsonParse
ErrorMessage: e.message,
ErrorMessageAdditional: data
}
}
*/
return response.json();
});
};
}; };

8
dev/prototype.js vendored
View file

@ -53,7 +53,7 @@
return function(...args) { return function(...args) {
timer && clearTimeout(timer); timer && clearTimeout(timer);
timer = setTimeout(()=>{ timer = setTimeout(()=>{
func.apply(this, args) func.apply(this, args);
timer = 0; timer = 0;
}, ms); }, ms);
}; };
@ -68,12 +68,10 @@
Function.prototype.throttle = function(ms) { Function.prototype.throttle = function(ms) {
let func = this, timer; let func = this, timer;
return function(...args) { return function(...args) {
if (!timer) { timer = timer || setTimeout(()=>{
timer = setTimeout(()=>{ func.apply(this, args);
func.apply(this, args)
timer = 0; timer = 0;
}, ms); }, ms);
}
}; };
}; };
} }

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.26.1</upstream>. This app packages SnappyMail <upstream>2.26.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.26.1 VERSION=2.26.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

@ -3,7 +3,7 @@
<id>snappymail</id> <id>snappymail</id>
<name>SnappyMail</name> <name>SnappyMail</name>
<summary>SnappyMail Webmail</summary> <summary>SnappyMail Webmail</summary>
<version>2.26.1</version> <version>2.26.4</version>
<licence>agpl</licence> <licence>agpl</licence>
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author> <author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.** <description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
@ -39,7 +39,7 @@ There, click on the link to go to the SnappyMail admin panel.
<repository>https://github.com/the-djmaze/snappymail/tree/master/integrations/nextcloud</repository> <repository>https://github.com/the-djmaze/snappymail/tree/master/integrations/nextcloud</repository>
<bugs>https://github.com/the-djmaze/snappymail/issues</bugs> <bugs>https://github.com/the-djmaze/snappymail/issues</bugs>
<dependencies> <dependencies>
<php min-version="7.4" min-int-size="64" /> <php min-version="7.4" min-int-size="32" />
<lib>mbstring</lib> <lib>mbstring</lib>
<lib>zlib</lib> <lib>zlib</lib>
<!-- <!--

View file

@ -16,7 +16,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
function __construct() { function __construct() {
$CSP = \RainLoop\Api::getCSP(); $CSP = \RainLoop\Api::getCSP();
$this->allowedScriptDomains = \array_unique(\array_merge($this->allowedScriptDomains, $CSP->script)); $this->allowedScriptDomains = \array_unique(\array_merge($this->allowedScriptDomains, $CSP->get('script-src')));
$this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'unsafe-inline'", "'unsafe-eval'"]); $this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'unsafe-inline'", "'unsafe-eval'"]);
// Nextcloud only sets 'strict-dynamic' when browserSupportsCspV3() ? // Nextcloud only sets 'strict-dynamic' when browserSupportsCspV3() ?
@ -24,12 +24,12 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
? $this->useStrictDynamic(true) // NC24+ ? $this->useStrictDynamic(true) // NC24+
: $this->addAllowedScriptDomain("'strict-dynamic'"); : $this->addAllowedScriptDomain("'strict-dynamic'");
$this->allowedImageDomains = \array_unique(\array_merge($this->allowedImageDomains, $CSP->img)); $this->allowedImageDomains = \array_unique(\array_merge($this->allowedImageDomains, $CSP->get('img-src')));
$this->allowedStyleDomains = \array_unique(\array_merge($this->allowedStyleDomains, $CSP->style)); $this->allowedStyleDomains = \array_unique(\array_merge($this->allowedStyleDomains, $CSP->get('style-src')));
$this->allowedStyleDomains = \array_diff($this->allowedStyleDomains, ["'unsafe-inline'"]); $this->allowedStyleDomains = \array_diff($this->allowedStyleDomains, ["'unsafe-inline'"]);
$this->allowedFrameDomains = \array_unique(\array_merge($this->allowedFrameDomains, $CSP->frame)); $this->allowedFrameDomains = \array_unique(\array_merge($this->allowedFrameDomains, $CSP->get('frame-src')));
$this->reportTo = \array_unique(\array_merge($this->reportTo, $CSP->report_to)); $this->reportTo = \array_unique(\array_merge($this->reportTo, $CSP->report_to));
} }

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