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 = ""
[version]
current = "2.26.1"
current = "2.26.4"
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 imagetoolbar "no"
# Header set X-Content-Type-Options "nosniff"
# Header set X-Frame-Options "DENY"
# Header set X-XSS-Protection "1; mode=block"
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
## Added

View file

@ -141,28 +141,28 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.170.153 | 81.159 |
|app.js |4.207.787 | 414.949 |
|boot.js | 868.735 | 2.711 |
|libs.js | 658.812 | 192.392 |
|sieve.js | 0 | 85.597 |
|admin.js |2.170.153 | 78.971 |
|app.js |4.207.787 | 409.340 |
|boot.js | 868.735 | 4.142 |
|libs.js | 658.812 | 185.881 |
|sieve.js | 0 | 85.599 |
|polyfills.js | 334.608 | 0 |
|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 |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 40.198 | 73.606 | 13.281 | 60.877 | 11.916 |
|app.min.js | 515.367 | 190.317 |139.456 | 63.879 |110.485 | 54.792 |
|boot.min.js | 84.659 | 1.600 | 26.998 | 940 | 23.643 | 767 |
|libs.min.js | 584.772 | 91.434 |180.901 | 33.893 |155.182 | 30.383 |
|sieve.min.js | 0 | 41.566 | 0 | 10.427 | 0 | 9.413 |
|admin.min.js | 256.831 | 38.936 | 73.606 | 13.036 | 60.877 | 11.694 |
|app.min.js | 515.367 | 186.883 |139.456 | 63.102 |110.485 | 54.200 |
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|libs.min.js | 584.772 | 90.445 |180.901 | 33.592 |155.182 | 30.108 |
|sieve.min.js | 0 | 41.525 | 0 | 10.435 | 0 | 9.401 |
|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+sieve |1.217.635 | 324.917 |358.761 |109.139 |299.485 | 95.355 |
|TOTAL admin | 959.099 | 133.232 |292.911 | 48.114 |249.877 | 43.066 |
|TOTAL user |1.217.635 | 279.412 |358.761 | 97.896 |299.485 | 85.311 |
|TOTAL user+sieve |1.217.635 | 320.937 |358.761 |108.331 |299.485 | 94.712 |
|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
@ -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 |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 82.134 | 46.946 | 17.067 | 14.688 |
|app.min.css | 274.947 | 66.337 | 39.647 | 15.095 | 13.285 |
|app.css | 340.331 | 81.526 | 46.946 | 16.960 | 14.620 |
|app.min.css | 274.947 | 65.398 | 39.647 | 15.032 | 13.119 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.554 | | 6.984 | 6.085 |
|admin.min.css | | 24.752 | | 6.339 | 5.606 |
|admin.css | | 30.296 | | 6.939 | 6.066 |
|admin.min.css | | 24.420 | | 6.272 | 5.531 |
### PGP
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.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 | |

View file

@ -4,16 +4,6 @@
//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.
*/

View file

@ -160,11 +160,6 @@ $tar->addFromString('index.php', $index);
$zip->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();
$tar->compress(Phar::GZ);

View file

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

View file

@ -1,7 +1,7 @@
import 'External/User/ko';
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 {
@ -156,22 +156,11 @@ export class AppUser extends AbstractApp {
? 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;
IdentityUserStore(isArray(items)
? items.map(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;
})
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
: []
);
}
@ -193,18 +182,17 @@ export class AppUser extends AbstractApp {
bootstart() {
super.bootstart();
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
addEventListener('beforeunload', event => {
if (arePopupsVisible() || (ThemeStore.isMobile() && MessageUserStore.message())) {
if (arePopupsVisible() || (!SettingsUserStore.layout() && MessageUserStore.message())) {
event.preventDefault();
return event.returnValue = "Are you sure you want to exit?";
return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
}
}, {capture: true});
}
refresh() {
ThemeStore.populate();
LanguageStore.language(SettingsGet('Language'));
LanguageStore.language(SettingsGet('language'));
changeTheme(SettingsGet('Theme'));
this.start();
}
@ -263,18 +251,15 @@ export class AppUser extends AbstractApp {
PgpUserStore.init();
// When auto-login is active
try {
navigator.registerProtocolHandler?.(
'mailto',
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
(SettingsGet('Title') || 'SnappyMail')
);
} catch (e) {
console.error(e);
}
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
// When auto-login is active
navigator.registerProtocolHandler?.(
'mailto',
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
(SettingsGet('title') || 'SnappyMail')
);
setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
} else {
this.logout();
}

View file

@ -1,9 +1,5 @@
import { MessageSetAction } from 'Common/EnumsUser';
import { isArray } from 'Common/Utils';
let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {},
MESSAGE_FLAGS_CACHE = {},
let FOLDERS_CACHE = new Map,
FOLDERS_HASH_MAP = new Map,
inboxFolderName = 'INBOX';
export const
@ -11,18 +7,10 @@ export const
* @returns {void}
*/
clearCache = () => {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
FOLDERS_CACHE.clear();
FOLDERS_HASH_MAP.clear();
},
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {string}
*/
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
/**
* @returns {string}
*/
@ -34,143 +22,35 @@ export const
setFolderInboxName = name => inboxFolderName = name,
/**
* @param {string} folderHash
* @param {string} fullNameHash
* @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
*/
setFolder = folder => {
folder.hash = '';
FOLDERS_CACHE[folder.fullName] = folder;
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
folder.etag = '';
FOLDERS_CACHE.set(folder.fullName, folder);
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
},
/**
* @param {string} folderFullName
* @param {string} folderHash
* @param {string} folderETag
*/
setFolderHash = (folderFullName, folderHash) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
setFolderETag = (folderFullName, folderETag) =>
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
/**
* @param {string} folderFullName
* @returns {?FolderModel}
*/
getFolderFromCacheList = folderFullName =>
FOLDERS_CACHE[folderFullName] || null,
FOLDERS_CACHE.get(folderFullName),
/**
* @param {string} folderFullName
*/
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[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;
}
}
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);

View file

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

View file

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

View file

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

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
export const
app = elementById('rl-app'),
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === app,
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
isFullscreen = ko.observable(false),
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : app.requestFullscreen();
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
if (app) {
if (appEl) {
let event = 'fullscreenchange';
if (!app.requestFullscreen && app.webkitRequestFullscreen) {
app.requestFullscreen = app.webkitRequestFullscreen;
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
event = 'webkit'+event;
}
if (app.requestFullscreen) {
if (appEl.requestFullscreen) {
doc.addEventListener(event, () => {
isFullscreen(appFullscreen());
$htmlCL.toggle('rl-fullscreen', appFullscreen());

View file

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

View file

@ -3,7 +3,7 @@ import { forEachObjectEntry, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
const
tpl = createElement('template'),
tmpl = createElement('template'),
htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i,
htmlmap = {
@ -14,10 +14,18 @@ const
"'": '&#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 = () => {
SettingsUserStore.collapseBlockquotes() &&
// tpl.content.querySelectorAll('blockquote').forEach(node => {
[...tpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
const el = createElement('details', {class:'sm-bq-switcher'});
el.innerHTML = '<summary>•••</summary>';
node.replaceWith(el);
@ -27,11 +35,11 @@ const
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
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
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
/** TODO: implement other url strippers like from
@ -90,6 +98,13 @@ const
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
keys as selectors and values are css rules
@ -161,7 +176,7 @@ const
// we have standard css
css.push({
selector: selector,
rules: arr[6]
rules: cleanCSS(arr[6])
});
}
}
@ -239,13 +254,6 @@ export const
// td
'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 = [
'A','B','EM','I','SPAN','STRONG'
];
@ -256,9 +264,9 @@ export const
msgId = 0;
}
tpl.innerHTML = html
tmpl.innerHTML = html
// 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>'))
// Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '')
@ -274,16 +282,17 @@ export const
// \MailSo\Base\HtmlUtils::ClearComments()
// 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()) {
nodeIterator.referenceNode.remove();
}
if (0 < bqLevel) {
tpl.content.querySelectorAll(new Array(1 + bqLevel).fill('blockquote').join(' ')).forEach(node => node.remove());
}
tmpl.content.querySelectorAll(
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,
oStyle = oElement.style;
@ -303,8 +312,7 @@ export const
}
// \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name)
|| 'none' == oStyle.display
if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
@ -320,15 +328,16 @@ export const
hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
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) {
forEachObjectEntry(tasks, (name, cb) => {
if (hasAttribute(name)) {
cb(getAttribute(name), oElement);
delAttribute(name);
}
});
forEachObjectEntry(tasks, (name, cb) =>
hasAttribute(name) && cb(delAttribute(name), oElement)
);
} else if (msgId && className) {
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
}
@ -350,21 +359,21 @@ export const
if (!oStyle.backgroundImage) {
if ('TD' !== name && 'TH' !== name) {
// Make width responsive
if (hasAttribute('width')) {
value = getAttribute('width');
if (hasAttribute('width') && !oStyle.width) {
value = delAttribute('width');
oStyle.width = value.includes('%') ? value : value + 'px';
delAttribute('width');
}
value = oStyle.removeProperty('width');
if (parseInt(value,10) && !oStyle.maxWidth) {
value = oStyle.width;
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
oStyle.maxWidth = value;
oStyle.width = '100%';
} else if (!value?.includes('%')) {
oStyle.removeProperty('width');
}
// Make height responsive
if (hasAttribute('height')) {
value = getAttribute('height');
value = delAttribute('height');
oStyle.height = value.includes('%') ? value : value + 'px';
delAttribute('height');
}
value = oStyle.removeProperty('height');
if (value && !oStyle.maxHeight) {
@ -402,8 +411,7 @@ export const
let skipStyle = false;
if (hasAttribute('src')) {
value = stripTracking(getAttribute('src'));
delAttribute('src');
value = stripTracking(delAttribute('src'));
if ('IMG' === name) {
oElement.loading = 'lazy';
@ -464,18 +472,15 @@ export const
}
if (hasAttribute('background')) {
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
delAttribute('background');
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
}
if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = getAttribute('bgcolor');
delAttribute('bgcolor');
oStyle.backgroundColor = delAttribute('bgcolor');
}
if (hasAttribute('color')) {
oStyle.color = getAttribute('color');
delAttribute('color');
oStyle.color = delAttribute('color');
}
if (!skipStyle) {
@ -540,8 +545,7 @@ export const
oStyle.removeProperty('color');
}
// Drop Microsoft Office style properties
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
oStyle.cssText = cleanCSS(oStyle.cssText);
}
if (debug && aAttrsForRemove.length) {
@ -551,8 +555,8 @@ export const
blockquoteSwitcher();
// return tpl.content.firstChild;
result.html = tpl.innerHTML.trim();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
return result;
},
@ -563,7 +567,7 @@ export const
htmlToPlain = html => {
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => {
let bq;
while ((bq = node.querySelector('blockquote'))) {
@ -591,7 +595,7 @@ export const
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(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
@ -641,16 +645,16 @@ export const
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Convert line-breaks
tpl.innerHTML = tpl.innerHTML
tmpl.innerHTML = tmpl.innerHTML
.replace(/\n{3,}/gm, '\n\n')
.replace(/\n<br[^>]*>/g, '\n')
.replace(/<br[^>]*>\n/g, '\n');
forEach('br', br => br.replaceWith('\n'));
// 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;
} while (bDo);
tpl.innerHTML = aText.join('\n')
tmpl.innerHTML = aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(url, (...m) => {
m[2] = stripTracking(m[2]);
return `${m[1]}<a href="${m[2]}" target="_blank">${m[2]}</a>`;
.replace(urlRegExp, (...m) => {
m[0] = stripTracking(m[0]);
return `<a href="${m[0]}" target="_blank">${m[0]}</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(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>');
blockquoteSwitcher();
return tpl.innerHTML.trim();
return tmpl.innerHTML.trim();
},
WYSIWYGS = ko.observableArray();

View file

@ -31,7 +31,7 @@ export const
*/
serverRequestRaw = (type, hash) =>
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('AccountHash') ?
+ '0/' // Settings.get('accountHash') ?
+ (type
? 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.
*/
let aCache = [],
aCheckedCache = [],
let aCheckedCache = [],
mFocused = null,
mSelected = null;
@ -99,7 +98,6 @@ export class Selector {
items.forEach(item => {
const uid = this.getItemUid(item);
if (uid) {
aCache.push(uid);
item.checked() && aCheckedCache.push(uid);
if (!mFocused && item.focused()) {
mFocused = uid;
@ -118,8 +116,7 @@ export class Selector {
koList.subscribe(aItems => {
selectedItemUseCallback = false;
koFocusedItem(null);
koSelectedItem(null);
this.unselect();
if (isArray(aItems)) {
let temp,
@ -182,7 +179,6 @@ export class Selector {
}
}
aCache = [];
aCheckedCache = [];
mFocused = null;
mSelected = null;

View file

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

View file

@ -17,12 +17,15 @@ export const
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
// 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':
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)
)),
*/
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

View file

@ -1,11 +1,9 @@
import { MessageFlagsCache } from 'Common/Cache';
import { Notification } from 'Common/Enums';
import { Notifications } from 'Common/Enums';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator';
import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
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) {
let params = {
target: 'zip',
filename: name,
hashes: hashes
};
if (!onError) {
@ -156,7 +155,7 @@ mailToHelper = mailToUrl => {
mailToUrl = mailToUrl.slice(7).split('?');
const
email = mailToUrl[0],
email = decodeURIComponent(mailToUrl[0]),
params = new URLSearchParams(mailToUrl[1]),
to = params.get('to'),
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) => {
if (oMessage) {
popup || MessageUserStore.message(oMessage);
popup || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notification.RequestAborted !== iError && !popup) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
let json = oData?.Result;
if (json
&& MessageModel.validJson(json)
&& oMessage.hash === json.hash
// && oMessage.folder === json.folder
// && oMessage.uid == json.uid
&& oMessage.revivePropertiesFromJson(json)
) {
if (oMessage.body) {
viewMessage(oMessage, popup);
} else {
popup || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notifications.RequestAborted !== iError && !popup) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
let json = oData?.Result;
if (json
&& oMessage.hash === json.hash
// && oMessage.folder === json.folder
// && oMessage.uid == json.uid
&& oMessage.revivePropertiesFromJson(json)
) {
/*
if (bCached) {
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;
if (bCached) {
delete json.flags;
}
}
MessageFlagsCache.initMessage(oMessage);
if (oMessage.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
oMessage.body.remove();
*/
viewMessage(oMessage, popup);
}
}
}
popup || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
popup || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
}
}
};

View file

@ -27,11 +27,6 @@ const
SquireDefaultConfig = {
/*
blockTag: 'P',
undo: {
documentSizeThreshold: -1, // -1 means no threshold
undoLimit: -1 // -1 means no limit
},
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
@ -216,7 +211,7 @@ class SquireUI
cmd: () => {
let node = squire.getSelectionClosest('IMG'),
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'
},
@ -288,11 +283,6 @@ class SquireUI
toolbar.className = 'squire-toolbar btn-toolbar';
let group, action/*, touchTap*/;
for (group in actions) {
/*
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
continue;
}
*/
let toolgroup = createElement('div');
toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group;
@ -379,7 +369,6 @@ class SquireUI
squire.addEventListener('cursor', );
squire.addEventListener('select', );
squire.addEventListener('input', );
squire.addEventListener('willPaste', );
squire.addEventListener( 'keydown keyup', monitorShiftKey )
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(),
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
koArrayWithDestroy = data => {
data = ko.observableArray(data);
@ -56,41 +68,18 @@ Object.assign(ko.bindingHandlers, {
},
onEnter: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
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));
}
init: (element, fValueAccessor, fAllBindings, model) =>
onKey('Enter', element, fValueAccessor, fAllBindings, model)
},
onEsc: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
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));
}
init: (element, fValueAccessor, fAllBindings, model) =>
onKey('Escape', element, fValueAccessor, fAllBindings, model)
},
onSpace: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
init: (element, fValueAccessor, fAllBindings, model) =>
onKey(' ', element, fValueAccessor, fAllBindings, model)
},
i18nUpdate: {

View file

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

View file

@ -1,3 +1,4 @@
//import { b64Encode } from 'Common/Utils';
const
// RFC2045
@ -68,6 +69,7 @@ export function ParseMime(text)
body = QPDecode(body);
}
body = btoa(body);
// body = b64Encode(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.estimatedSize = part.body.length;
/*
attachment.isThumbnail = false;
attachment.contentLocation = '';
attachment.download = '';
attachment.folder = '';
attachment.uid = '';
attachment.mimeIndex = part.id;

View file

@ -1,6 +1,8 @@
import ko from 'ko';
import { FileInfo, FileType } from 'Common/File';
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
import { b64EncodeJSONSafe } from 'Common/Utils';
import {
attachmentDownload,
serverRequestRaw
@ -22,10 +24,8 @@ export class AttachmentModel extends AbstractModel {
this.fileName = '';
this.fileNameExt = '';
this.fileType = FileType.Unknown;
this.isThumbnail = false;
this.cId = '';
this.contentLocation = '';
this.download = '';
this.folder = '';
this.uid = '';
this.url = '';
@ -53,7 +53,7 @@ export class AttachmentModel extends AbstractModel {
}
toggleChecked(self, event) {
event.stopPropagation();
stopEvent(event);
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}
*/
@ -143,7 +154,7 @@ export class AttachmentModel extends AbstractModel {
* @returns {boolean}
*/
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 { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
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 { Notifications } from 'Common/Enums';
import * as Local from 'Storage/Client';
@ -14,7 +15,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import { sortFolders } from 'Common/Folders';
import { i18n, translateTrigger } from 'Common/Translator';
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel';
@ -91,6 +92,7 @@ export const
Remote.abort('Folders')
.post('Folders', FolderUserStore.foldersLoading)
.then(data => {
clearCache();
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
fCallback?.(true);
// Repeat every 15 minutes?
@ -127,8 +129,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
let oCacheFolder = getFolderFromCacheList(oFolder.fullName);
if (oCacheFolder) {
// oCacheFolder.revivePropertiesFromJson(oFolder);
if (oFolder.hash) {
oCacheFolder.hash = oFolder.hash;
if (oFolder.etag) {
oCacheFolder.etag = oFolder.etag;
}
if (null != oFolder.totalEmails) {
oCacheFolder.totalEmails(oFolder.totalEmails);
@ -167,22 +169,22 @@ export class FolderCollectionModel extends AbstractCollectionModel
break;
}
// Flags
if (oFolder.flags.includes('\\sentmail')) {
if (oFolder.attributes.includes('\\sentmail')) {
role = 'sent';
}
if (oFolder.flags.includes('\\spam')) {
if (oFolder.attributes.includes('\\spam')) {
role = 'junk';
}
if (oFolder.flags.includes('\\bin')) {
if (oFolder.attributes.includes('\\bin')) {
role = 'trash';
}
if (oFolder.flags.includes('\\important')) {
if (oFolder.attributes.includes('\\important')) {
role = 'important';
}
if (oFolder.flags.includes('\\starred')) {
if (oFolder.attributes.includes('\\starred')) {
role = 'flagged';
}
if (oFolder.flags.includes('\\all') || oFolder.flags.includes('\\allmail')) {
if (oFolder.attributes.includes('\\all') || oFolder.flags.includes('\\allmail')) {
role = 'all';
}
}
@ -226,9 +228,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
name: name,
fullName: parentName,
delimiter: delimiter,
exists: false,
isSubscribed: false,
flags: ['\\nonexistent']
attributes: ['\\nonexistent']
});
setFolder(pfolder);
result.splice(i, 0, pfolder);
@ -300,7 +300,7 @@ export class FolderModel extends AbstractModel {
this.exists = true;
this.hash = '';
this.etag = '';
this.id = 0;
this.uidNext = 0;
@ -330,7 +330,8 @@ export class FolderModel extends AbstractModel {
tagsAllowed: false
});
this.flags = ko.observableArray();
this.attributes = ko.observableArray();
// For messages
this.permanentFlags = ko.observableArray();
this.addSubscribables({
@ -379,6 +380,8 @@ export class FolderModel extends AbstractModel {
isFlagged: () => FolderUserStore.currentFolder() === this
&& MessagelistUserStore.listSearch().includes('flagged'),
// isSubscribed: () => this.attributes().includes('\\subscribed'),
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
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
*/
@ -491,6 +537,10 @@ export class FolderModel extends AbstractModel {
path.pop();
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);
}
return folder;

View file

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

View file

@ -5,7 +5,7 @@ import { i18n } from 'Common/Translator';
import { doc, SettingsGet } from 'Common/Globals';
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 { addObservablesTo, addComputablesTo } from 'External/ko';
@ -21,7 +21,6 @@ import PreviewHTML from 'Html/PreviewMessage.html';
import { LanguageStore } from 'Stores/Language';
//import { MessageFlagsCache } from 'Common/Cache';
import Remote from 'Remote/User/Fetch';
const
@ -34,7 +33,6 @@ const
Remote.request('MessageSetKeyword', iError => {
if (!iError) {
isSet ? flags.remove(lower) : flags.push(lower);
// MessageFlagsCache.setFor(message.folder, message.uid, flags());
}
}, {
folder: message.folder,
@ -61,7 +59,6 @@ export class MessageModel extends AbstractModel {
this.folder = '';
this.uid = 0;
this.hash = '';
this.requestHash = '';
this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel;
this.cc = new EmailCollectionModel;
@ -88,7 +85,8 @@ export class MessageModel extends AbstractModel {
spamResult: '',
isSpam: false,
hasVirus: null, // or boolean when scanned
dateTimeStampInUTC: 0,
dateTimestamp: 0,
internalTimestamp: 0,
priority: MessagePriority.Normal,
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(this, keyword);
}
@ -290,54 +298,51 @@ export class MessageModel extends AbstractModel {
return [[...toResult.values()], [...ccResult.values()]];
}
viewHtml() {
viewBody(html) {
const body = this.body;
if (body && this.html()) {
let result = msgHtml(this);
this.hasExternals(result.hasExternals);
this.hasImages(body.rlHasImages = !!result.hasExternals);
body.innerHTML = result.html;
body.classList.toggle('html', 1);
body.classList.toggle('plain', 0);
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
if ('always' === SettingsUserStore.viewImages()) {
this.showExternalImages();
}
if ('match' === SettingsUserStore.viewImages()) {
this.showExternalImages(1);
if (body) {
if (html) {
let result = msgHtml(this);
this.hasExternals(result.hasExternals);
this.hasImages(!!result.hasExternals);
body.innerHTML = result.html;
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
if ('always' === SettingsUserStore.viewImages()) {
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);
}
this.isHtml(true);
body.classList.toggle('html', html);
body.classList.toggle('plain', !html);
this.isHtml(html);
return true;
}
}
viewHtml() {
return this.html() && this.viewBody(true);
}
viewPlain() {
const body = this.body;
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;
}
return this.viewBody(false);
}
viewPopupMessage(print) {
const
timeStampInUTC = this.dateTimeStampInUTC() || 0,
timeStampInUTC = this.dateTimestamp() || 0,
ccLine = this.cc.toString(),
bccLine = this.bcc.toString(),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
@ -430,7 +435,7 @@ export class MessageModel extends AbstractModel {
hasImages = true;
},
attr = 'data-x-src',
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => {
src = node.getAttribute(attr);
if (isValid(src)) {
@ -447,7 +452,6 @@ export class MessageModel extends AbstractModel {
});
this.hasImages(hasImages);
body.rlHasImages = hasImages;
}
}

View file

@ -2,10 +2,6 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message';
import {
MessageFlagsCache
} from 'Common/Cache';
'use strict';
export class MessageCollectionModel extends AbstractCollectionModel
@ -13,17 +9,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
/*
constructor() {
super();
this.Filtered
this.filtered
this.folder
this.folderHash
this.folderInfo
this.totalEmails
this.unreadEmails
this.totalThreads
this.threadUid
this.newMessages
this.offset
this.limit
this.search
this.limited
}
*/
@ -31,7 +26,7 @@ export class MessageCollectionModel extends AbstractCollectionModel
* @param {?Object} json
* @returns {MessageCollectionModel}
*/
static reviveFromJson(object, cached) {
static reviveFromJson(object/*, cached*/) {
let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => {
// 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);
if (message) {
message.deleted(false);
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(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 { serverRequest } from 'Common/Links';
import { getNotification } from 'Common/Translator';
@ -9,18 +9,18 @@ const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err) {
if (Notifications.InvalidToken === err) {
console.error(getNotification(err));
// alert(getNotification(err));
rl.logoutReload();
} else if ([
Notification.AuthError,
Notification.ConnectionError,
Notification.DomainNotAllowed,
Notification.AccountNotAllowed,
Notification.MailServerError,
Notification.UnknownNotification,
Notification.UnknownError
Notifications.AuthError,
Notifications.ConnectionError,
Notifications.DomainNotAllowed,
Notifications.AccountNotAllowed,
Notifications.MailServerError,
Notifications.UnknownNotification,
Notifications.UnknownError
].includes(err)
) {
if (7 < ++iJsonErrorCount) {
@ -65,7 +65,7 @@ class FetchError extends Error
{
constructor(code, message) {
super(message);
this.code = code || Notification.JsonFalse;
this.code = code || Notifications.JsonFalse;
}
}
@ -145,7 +145,7 @@ export class AbstractFetchRemote
iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError
iError = data.ErrorCode || Notifications.UnknownError
}
}
@ -197,7 +197,7 @@ export class AbstractFetchRemote
abort(action, 0, 1);
if (!data) {
return Promise.reject(new FetchError(Notification.JsonParse));
return Promise.reject(new FetchError(Notifications.JsonParse));
}
/*
let isCached = false, type = '';

View file

@ -24,7 +24,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
const
// folder = getFolderFromCacheList(params.folder.fullName),
folder = getFolderFromCacheList(params.folder),
folderHash = folder?.hash || '';
folderETag = folder?.etag || '';
params = Object.assign({
offset: 0,
@ -40,8 +40,8 @@ class RemoteUserFetch extends AbstractFetchRemote {
}
let sGetAdd = '';
if (folderHash) {
params.hash = folderHash + '-' + SettingsGet('AccountHash');
if (folderETag) {
params.hash = folderETag + '-' + SettingsGet('accountHash');
sGetAdd = 'MessageList/' + SUB_QUERY_PREFIX + '/' + b64EncodeJSONSafe(params);
params = {};
}
@ -77,7 +77,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
sFolderFullName,
iUid,
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 { settings } from 'Common/Links';
import { createElement, elementById } from 'Common/Globals';
import { createElement/*, elementById*/ } from 'Common/Globals';
import { AbstractScreen } from 'Knoin/AbstractScreen';
import { i18nToNodes } from 'Common/Translator';
@ -29,7 +29,8 @@ export class AbstractSettingsScreen extends AbstractScreen {
);
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) {
settingsScreen = RoutedSettingsViewModel.__vm;
} else if (vmPlace) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
this.accounts = AccountUserStore;
this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore;
this.mainEmail = SettingsGet('MainEmail');
this.mainEmail = SettingsGet('mainEmail');
this.accountForDeletion = ko.observable(null).askDeleteHelper();
this.identityForDeletion = ko.observable(null).askDeleteHelper();
@ -88,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
accountsAndIdentitiesAfterMove() {
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() : ""))
});
}

View file

@ -1,12 +1,11 @@
import ko from 'ko';
import { Notification } from 'Common/Enums';
import { Notifications } from 'Common/Enums';
import { FolderMetadataKeys } from 'Common/EnumsUser';
import { getNotification } from 'Common/Translator';
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
import { getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
@ -18,7 +17,6 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { loadFolders } from 'Model/FolderCollection';
const folderForDeletion = ko.observable(null).askDeleteHelper();
@ -52,52 +50,10 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
this.folderForDeletion = folderForDeletion;
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'editing'] });
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', 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() {
FolderUserStore.folderListError('');
}
@ -119,8 +75,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
&& folderToRemove.askDelete()
) {
if (0 < folderToRemove.totalEmails()) {
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
// FolderUserStore.folderListError(getNotification(Notifications.CantDeleteNonEmptyFolder));
folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
} else {
folderForDeletion(null);
@ -129,7 +85,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
folder: folderToRemove.fullName
}).then(
() => {
// folderToRemove.flags.push('\\nonexistent');
// folderToRemove.attributes.push('\\nonexistent');
folderToRemove.selectable(false);
// folderToRemove.isSubscribed(false);
// folderToRemove.checkable(false);
@ -141,7 +97,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
},
error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantDeleteFolder)
getNotification(error.code, '', Notifications.CantDeleteFolder)
+ '.\n' + error.message
);
}

View file

@ -17,7 +17,6 @@ import { LanguageStore } from 'Stores/Language';
import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
@ -35,7 +34,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.soundNotification = SMAudio.notifications;
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
this.desktopNotification = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
@ -44,12 +43,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck'
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck',
'showNextMessage'
].forEach(name => this[name] = SettingsUserStore[name]);
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
this.languageTrigger = ko.observable(SaveSettingStatus.Idle);
@ -87,7 +87,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
layoutTypes: () => {
translateTrigger();
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.BottomPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
];
@ -102,10 +102,10 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel');
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'AllowStyles',
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'RemoveColors', 'AllowStyles',
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes']);
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes', 'AllowDraftAutosave', 'showNextMessage']);
const fReloadLanguageHelper = (saveSettingsStep) => () => {
this.languageTrigger(saveSettingsStep);
@ -117,20 +117,12 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.languageTrigger(SaveSettingStatus.Saving);
translatorReload(value)
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
.then(() => Remote.saveSetting('Language', value));
.then(() => Remote.saveSetting('language', value));
},
hourCycle: value =>
Remote.saveSetting('hourCycle', value),
removeColors: value => {
let dom = MessageUserStore.bodiesDom();
if (dom) {
dom.innerHTML = '';
}
Remote.saveSetting('RemoveColors', value);
},
notificationSound: value => {
Remote.saveSetting('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 { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
//import Remote from 'Remote/User/Fetch';
export class UserSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
@ -46,10 +46,6 @@ export class UserSettingsSecurity extends AbstractViewSettings {
this.canOpenPGP = SettingsCapa('OpenPGP');
this.canGnuPG = GnuPGUserStore.isSupported();
this.canMailvelope = !!window.mailvelope;
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
}
addOpenPgpKey() {
@ -66,5 +62,11 @@ export class UserSettingsSecurity extends AbstractViewSettings {
* The iframe will be injected into the container identified by selector.
*/
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 { ThemeStore, convertThemeName, changeTheme } from 'Stores/Theme';
import { addSubscribablesTo } from 'External/ko';
import Remote from 'Remote/User/Fetch';
@ -24,20 +25,22 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
this.fontSansSerif = ThemeStore.fontSansSerif;
this.fontSerif = ThemeStore.fontSerif;
this.fontMono = ThemeStore.fontMono;
ThemeStore.fontSansSerif.subscribe(value => {
Remote.saveSettings(null, {
fontSansSerif: value
});
});
ThemeStore.fontSerif.subscribe(value => {
Remote.saveSettings(null, {
fontSerif: value
});
});
ThemeStore.fontMono.subscribe(value => {
Remote.saveSettings(null, {
fontMono: value
});
addSubscribablesTo(ThemeStore, {
fontSansSerif: value => {
Remote.saveSettings(null, {
fontSansSerif: value
});
},
fontSerif: value => {
Remote.saveSettings(null, {
fontSerif: value
});
},
fontMono: value => {
Remote.saveSettings(null, {
fontMono: value
});
}
});
this.theme = ThemeStore.theme;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,15 @@
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 { cssLink, serverRequestRaw } from 'Common/Links';
import { SaveSettingStatus } from 'Common/Enums';
import { addSubscribablesTo } from 'External/ko';
let __themeTimer = 0;
export const
appEl = () => elementById('rl-app'),
// Also see Styles/_Values.less @maxMobileWidth
isMobile = matchMedia('(max-width: 799px)'),
ThemeStore = {
theme: ko.observable(''),
@ -17,7 +19,7 @@ export const
fontSansSerif: ko.observable(''),
fontSerif: ko.observable(''),
fontMono: ko.observable(''),
isMobile: ko.observable($htmlCL.contains('rl-mobile')),
isMobile: ko.observable(false),
populate: () => {
const themes = Settings.app('themes');
@ -25,8 +27,8 @@ export const
ThemeStore.themes(isArray(themes) ? themes : []);
ThemeStore.theme(SettingsGet('Theme'));
if (!ThemeStore.isMobile()) {
ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName'));
ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash'));
ThemeStore.userBackgroundName(SettingsGet('userBackgroundName'));
ThemeStore.userBackgroundHash(SettingsGet('userBackgroundHash'));
}
ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
ThemeStore.fontSerif(SettingsGet('fontSerif'));
@ -62,35 +64,44 @@ export const
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 => {
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.fontSerif.subscribe(value => {
if (null != value) {
let cl = appEl().classList;
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
value && cl.add('fontSerif'+value);
}
});
ThemeStore.fontMono.subscribe(value => {
if (null != value) {
let cl = appEl().classList;
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
value && cl.add('fontMono'+value);
fontSerif: value => {
if (null != value) {
let cl = appEl.classList;
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
value && cl.add('fontSerif'+value);
}
},
fontMono: value => {
if (null != value) {
let cl = appEl.classList;
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
value && cl.add('fontMono'+value);
}
},
userBackgroundHash: value => {
appEl.classList.toggle('UserBackground', !!value);
appEl.style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
}
});
ThemeStore.userBackgroundHash.subscribe(value => {
appEl().classList.toggle('UserBackground', !!value);
appEl().style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
});
isMobile.onchange = e => {
ThemeStore.isMobile(e.matches);
$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 { addObservablesTo } from 'External/ko';
import { ThemeStore } from 'Stores/Theme';
import { arePopupsVisible } from 'Knoin/Knoin';
export const AppUserStore = {
allowContacts: () => !!SettingsGet('ContactsIsAllowed')
allowContacts: () => !!SettingsGet('contactsAllowed')
};
addObservablesTo(AppUserStore, {
@ -15,10 +16,9 @@ addObservablesTo(AppUserStore, {
AppUserStore.focusedState.subscribe(value => {
['FolderList','MessageList','MessageView'].forEach(name => {
if (name === value) {
keyScope(value);
arePopupsVisible() || keyScope(value);
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
}
let dom = elementById('V-Mail'+name);
dom?.classList.toggle('focused', name === value);
elementById('V-Mail'+name).classList.toggle('focused', name === value);
});
});

View file

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

View file

@ -199,11 +199,11 @@ FolderUserStore = new class {
saveSystemFolders(folders) {
folders = folders || {
Sent: FolderUserStore.sentFolder(),
Drafts: FolderUserStore.draftsFolder(),
Spam: FolderUserStore.spamFolder(),
Trash: FolderUserStore.trashFolder(),
Archive: FolderUserStore.archiveFolder()
sent: FolderUserStore.sentFolder(),
drafts: FolderUserStore.draftsFolder(),
junk: FolderUserStore.spamFolder(),
trash: FolderUserStore.trashFolder(),
archive: FolderUserStore.archiveFolder()
};
forEachObjectEntry(folders, (k,v)=>Settings.set(k+'Folder',v));
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 { AskPopupView } from 'View/Popup/Ask';
import { Passphrases } from 'Storage/Passphrases';
const
passphrases = new Map(),
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') => {
const key = privateKey.id,
pass = passphrases.has(key)
? {password:passphrases.get(key), remember:false}
pass = Passphrases.has(key)
? {password:Passphrases.get(key), remember:false}
: 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;
},
@ -83,7 +84,7 @@ export const GnuPGUserStore = new class {
key.armor = oData.Result;
showScreenPopup(OpenPgpKeyPopupView, [key]);
} else {
passphrases.delete(key.id);
Passphrases.delete(key.id);
}
}, {
keyId: key.id,
@ -198,7 +199,7 @@ export const GnuPGUserStore = new class {
if (result?.Result && false !== result.Result.data) {
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);
exitFullscreen();
}
this.hideMessageBodies();
[...(this.bodiesDom()?.children || [])].forEach(el => el.hidden = true);
},
});
this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000);
this.purgeCache = this.purgeCache.throttle(30000);
}
purgeMessageBodyCache() {
const messagesDom = this.bodiesDom(),
children = messagesDom?.children;
if (children) {
while (15 < children.length) {
children[0].remove();
purgeCache(all) {
const children = this.bodiesDom()?.children || [];
let i = Math.max(0, children.length - (all ? 0 : 15));
while (i--) {
children[i].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 { SMAudio } from 'Common/Audio';
import { Notification } from 'Common/Enums';
import { Notifications } from 'Common/Enums';
import { MessageSetAction } from 'Common/EnumsUser';
import { $htmlCL } from 'Common/Globals';
import { arrayLength, pInt, pString } from 'Common/Utils';
import { $htmlCL, fireEvent } from 'Common/Globals';
import { arrayLength, pString } from 'Common/Utils';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import {
getFolderInboxName,
getFolderFromCacheList,
setFolderHash,
MessageFlagsCache
setFolderETag
} from 'Common/Cache';
import { mailBox } from 'Common/Links';
@ -174,7 +173,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage();
if (bDropCurrentFolderCache) {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
setFolderETag(FolderUserStore.currentFolderFullName(), '');
}
if (bDropPagePosition) {
@ -198,7 +197,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let error = '';
if (iError) {
error = getNotification(iError);
if (Notification.RequestAborted !== iError) {
if (Notifications.RequestAborted !== iError) {
MessagelistUserStore([]);
}
} else {
@ -207,27 +206,24 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
error = '';
const
folder = getFolderFromCacheList(collection.folder),
folderInfo = collection.folderInfo;
folderInfo = collection.folder,
folder = getFolderFromCacheList(folderInfo.name);
collection.folder = folderInfo.name;
if (folder && !bCached) {
// folder.revivePropertiesFromJson(result);
folder.expires = Date.now();
folder.uidNext = folderInfo.uidNext;
folder.hash = collection.folderHash;
folder.etag = folderInfo.etag;
if (null != folderInfo.totalEmails) {
folder.totalEmails(folderInfo.totalEmails);
}
if (null != folderInfo.unreadEmails) {
if (pInt(folder.unreadEmails()) !== pInt(folderInfo.unreadEmails)) {
MessageFlagsCache.clearFolder(folder.fullName);
}
folder.unreadEmails(folderInfo.unreadEmails);
}
folder.flags(folderInfo.flags);
let flags = folderInfo.permanentFlags;
let flags = folderInfo.permanentFlags || [];
if (flags.includes('\\*')) {
let i = 6;
while (--i) {
@ -251,14 +247,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
MessagelistUserStore.threadUid(collection.threadUid);
MessagelistUserStore.endHash(
collection.folder +
folderInfo.name +
'|' + collection.search +
'|' + MessagelistUserStore.threadUid() +
'|' + MessagelistUserStore.page()
);
MessagelistUserStore.endThreadUid(collection.threadUid);
const message = MessageUserStore.message();
if (message && collection.folder !== message.folder) {
if (message && folderInfo.name !== message.folder) {
MessageUserStore.message(null);
}
@ -280,7 +276,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
} else {
MessagelistUserStore.count(0);
MessagelistUserStore([]);
error = getNotification(Notification.CantGetMessageList);
error = getNotification(Notifications.CantGetMessageList);
}
MessagelistUserStore.error(error);
}
@ -306,22 +302,38 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
messages = messages || MessagelistUserStore.listChecked();
let folder,
alreadyUnread = 0,
rootUids = messages.map(oMessage => oMessage?.uid).validUnique(),
length = rootUids.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) {
rootUids.forEach(sSubUid =>
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
);
switch (iSetAction) {
case MessageSetAction.SetSeen:
length = 0;
length = -length;
// fallthrough is intentionally
case MessageSetAction.UnsetSeen:
folder = getFolderFromCacheList(sFolderFullName);
if (folder) {
folder.unreadEmails(folder.unreadEmails() - alreadyUnread + length);
folder.unreadEmails(Math.max(0, folder.unreadEmails() + length));
}
Remote.request('MessageSetSeen', null, {
folder: sFolderFullName,
@ -340,8 +352,6 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
break;
// no default
}
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
};
@ -370,22 +380,20 @@ MessagelistUserStore.removeMessagesFromList = (
messages.forEach(item => item?.isUnseen() && ++unseenCount);
if (fromFolder) {
fromFolder.hash = '';
fromFolder.etag = '';
if (!copy) {
fromFolder.totalEmails(
0 <= fromFolder.totalEmails() - oUids.size ? fromFolder.totalEmails() - oUids.size : 0
);
if (0 < unseenCount) {
fromFolder.unreadEmails(
0 <= fromFolder.unreadEmails() - unseenCount ? fromFolder.unreadEmails() - unseenCount : 0
);
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
}
}
}
if (toFolder) {
toFolder.hash = '';
toFolder.etag = '';
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
unseenCount = 0;
@ -405,6 +413,18 @@ MessagelistUserStore.removeMessagesFromList = (
} else {
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 => {
if (currentMessage && currentMessage.hash === item.hash) {
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 { fireEvent } from 'Common/Globals';
@ -22,15 +22,19 @@ let DesktopNotifications = false,
WorkerNotifications = navigator.serviceWorker;
// Are Notifications supported in the service worker?
if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) {
/* Listen for close requests from the ServiceWorker */
WorkerNotifications.addEventListener('message', event => {
const obj = JSON.parse(event.data);
'notificationclick' === obj?.action && dispatchMessage(obj.data);
});
if (WorkerNotifications) {
if (ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) {
/* Listen for close requests from the ServiceWorker */
WorkerNotifications.addEventListener('message', event => {
const obj = JSON.parse(event.data);
'notificationclick' === obj?.action && dispatchMessage(obj.data);
});
} else {
console.log('ServiceWorkerRegistration.showNotification undefined');
WorkerNotifications = null;
}
} else {
WorkerNotifications = null;
console.log('ServiceWorker Notifications not supported');
console.log('ServiceWorker undefined');
}
export const NotificationUserStore = new class {
@ -57,7 +61,7 @@ export const NotificationUserStore = new class {
if (DesktopNotifications && NotificationsGranted()) {
const options = {
body: text,
icon: imageSrc || Links.staticLink('images/icon-message-notification.png'),
icon: imageSrc || staticLink('images/icon-message-notification.png'),
data: messageData
};
if (messageData?.uid) {
@ -65,8 +69,7 @@ export const NotificationUserStore = new class {
}
if (WorkerNotifications) {
// Service-Worker-Allowed HTTP header to allow the scope.
WorkerNotifications.register('/serviceworker.js')
// WorkerNotifications.register(Links.staticLink('js/serviceworker.js'), {scope:'/'})
WorkerNotifications.register(staticLink('js/serviceworker.js'), {scope:'/'})
.then(() =>
WorkerNotifications.ready.then(registration =>
/* 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 {
const notification = new HTML5Notification(title, options);
notification.show?.();

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
:root {
--fontSans: Verdana, Geneva, "Bitstream Vera Sans", "DejaVu LGC Sans", Arial, sans-serif;
--fontSerif: "Nimbus Roman No9 L", "Times New Roman", Times, FreeSerif, serif;
--fontMono: "Liberation Mono", Monaco, Menlo, Consolas, "Courier New", FreeMono, Courier, monospace;
--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: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--fontMono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
#rl-app.fontArial {

View file

@ -39,6 +39,14 @@
height: calc(100% - 50px - @rlLowMargin);
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 {
color: #fff;
display: flex;
@ -56,28 +64,13 @@
}
}
.rl-mobile #V-AdminPane {
margin-right: 0;
}
.btn.btn-logout {
padding-left: 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 */
@media screen and (min-width: 1000px) {
@media screen and (min-width: @maxMobileWidth + 1px) {
.toggleLeft {
display: none;
}
@ -90,16 +83,16 @@
}
}
/* mobile and tablet */
@media screen and (max-width: 999px) {
#rl-settings-subscreen {
padding: 10px;
}
@media screen and (max-width: @maxMobileWidth) {
#rl-right {
min-width: calc(100% - @rlLowMargin);
}
#V-AdminPane {
margin-right: 0;
padding: 10px;
}
html.rl-left-panel-disabled {
#rl-left {
width: 0;

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
#rl-settings-subscreen,
.g-ui-user-select-none {
user-select: none;
-webkit-touch-callout: none;
@ -159,20 +158,7 @@ summary.legend {
padding: 0 9px;
}
.alert a:not(.close) {
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) {
@media screen and (max-width: @maxMobileWidth) {
.form-horizontal {
.control-group {
> label {

View file

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

View file

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

View file

@ -45,7 +45,7 @@
padding-top: 50px;
text-align: center;
font-size: 24px;
color: #666;
opacity: 0.5;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
@ -131,18 +131,20 @@
top: 5px;
bottom: 5px;
border: 2px #777 dashed;
border: 2px var(--border-color, #ddd) dashed;
z-index: 300;
line-height: 119px;
text-align: center;
background-color: #efefef;
background-color: var(--dialog-bg-clr, #fff);
font-size: 24px;
border-radius: 10px;
opacity: 0.9;
&.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 {
.checkboxItem {
visibility: hidden;
@ -203,7 +203,7 @@ html:not(rl-mobile) {
}
}
@media screen and (max-width: 900px) {
@media screen and (max-width: @maxMobileWidth) {
#V-PopupsContacts {
width: 100%;
}

View file

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

View file

@ -12,6 +12,10 @@
/* transition: width 0.3s ease-out;*/
width: @rlLeftWidth;
z-index: 0;
.b-toolbar {
padding: 10px 0 10px @rlLowMargin;
}
}
#rl-right:not([hidden]) {
@ -19,9 +23,6 @@
flex-grow: 1;
width: 20%;
}
.rl-bottom-preview-pane #rl-right {
flex-direction: column;
}
/*
.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 {
resize: horizontal; overflow: auto;
min-width: 155px;
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,
#button-add-prop-dropdown-id::after {
content: '▼';
@ -140,32 +106,6 @@ html:not(.rl-left-panel-disabled) #rl-left > .resizer,
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.rl-ctrl-key-pressed .hidden-on-ctrl-btn,
.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 {
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 {
position: relative;
}
.rl-side-preview-pane #V-MailMessageList {
height: 100%;
}
.rl-no-preview-pane #V-MailMessageList {
height: 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 {
border-color: #9d9d9d;
@ -47,7 +22,7 @@ html.rl-mobile, html.rl-no-preview-pane {
.messageList {
height: calc(100% - 60px);
background-color: #fff;
background-color: var(--panel-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px);
display: flex;
@ -71,7 +46,7 @@ html.rl-mobile, html.rl-no-preview-pane {
}
.b-footer {
display: flex;
display: flex;
flex-shrink: 0;
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 {
display: flex;
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 {
.listDragOver {
transition: all 400ms ease;
}
}
}
/* desktop-large */
@media screen and (min-width: 1401px) {
.messageListItem {
font-size: 13px;
time {
font-size: 13px;
.hideMessageListCheckbox {
.checkboxCheckAll {
visibility: hidden;
}
.checkboxMessage {
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;
}
.rl-side-preview-pane, .rl-mobile {
.sm-msgView-side, .rl-mobile {
.messageListItem > div + div {
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 {
color: #fff;
padding: 10px 0;
}
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;
visibility: visible;
}
.messageView {
background-color: #fff;
background-color: var(--panel-bg-clr, #fff);
border: 1px solid var(--border-color, #aaa);
border-radius: var(--border-radius, 5px);
height: calc(100% - 50px);
@ -140,7 +126,7 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
.messageItemHeader {
background-color: #f8f8f8;
background-color: var(--message-header-bg-clr, #eee);
border-bottom: 1px solid var(--border-color, #bbb);
border-radius: var(--border-radius, 5px) var(--border-radius, 5px) 0 0;
padding: 10px;
@ -215,7 +201,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
}
.openpgp-control {
margin: 0.5em;
padding: 0.5em;
span {
@ -248,9 +233,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
display: none;
}
}
.b-text-part > iframe {
min-height: 50vh;
}
.thread-controls {
.dropdown-toggle {
@ -271,7 +253,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
#messageItem {
color: #000;
height: 100%;
overflow: auto;
scroll-behavior: smooth;
@ -283,11 +264,13 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
padding: 50px 0;
}
.bodySubHeader {
background-color: var(--message-header-bg-clr, #eee);
}
.bodySubHeader > * {
background-color: #eee;
border-bottom: 1px solid var(--border-color, #bbb);
cursor: pointer;
padding: 10px 15px;
padding: 10px;
}
.pgpInfo {
@ -300,14 +283,8 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
}
}
.readReceipt {
background-color: #ffffd9;
}
.attachmentsPlace {
padding: 10px 10px 6px 10px;
background: #eee;
border-bottom: 1px solid var(--border-color, #bbb);
position: relative;
@ -336,9 +313,8 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
}
.attachmentsControls {
padding: 7px 5px 7px 14px;
background: #e8e8e8;
border-bottom: 1px solid var(--border-color, #bbb);
padding-top: 7px;
border-top: 1px solid var(--border-color, #bbb);
}
.attachmentsControls > * {
display: inline-block;
@ -365,8 +341,12 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
height: 100%;
padding: 10px;
* {
box-sizing: unset;
}
a {
color: blue;
color: var(--link-color, blue);
text-decoration: underline;
&:visited {
@ -382,6 +362,10 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
opacity: 0.8;
padding: 0 10px;
margin: 0;
blockquote {
opacity: 1;
}
}
&.html {
@ -471,15 +455,34 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
}
}
html.rl-no-preview-pane .messageView {
border: 1px solid var(--border-color, #aaa);
box-shadow: var(--smMainShadow);
.mailvelope iframe {
min-height: 400px;
}
html.rl-bottom-preview-pane .messageView {
height: 100%;
html:not(.sm-msgView-bottom):not(.sm-msgView-side) {
#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 {
#rl-left,

View file

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

View file

@ -1,24 +1,5 @@
#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 {
a {
@ -51,15 +32,20 @@
#V-SettingsPane {
height: calc(100% - 50px - @rlLowMargin);
margin: 50px @rlLowMargin 0 0;
padding:20px;
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 {
position: absolute;
top: 0;
right: 0;
left: 0;
padding: 8px 0;
color: #fff;
top: 8px;
left: @rlLowMargin;
}
td {
@ -68,22 +54,7 @@
}
}
.rl-mobile #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 #V-SettingsPane,
.firefox-drag .modal-body {
overflow: hidden;
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import ko from 'ko';
import {
Notification,
Notifications,
UploadErrorCode
} from 'Common/Enums';
@ -11,7 +11,7 @@ import {
FolderType
} 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 { 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 { serverRequest } from 'Common/Links';
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut, createElement } from 'Common/Globals';
import { setFolderETag } from 'Common/Cache';
import { SettingsCapa, SettingsGet, elementById, addShortcut, createElement } from 'Common/Globals';
//import { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
import { AppUserStore } from 'Stores/User/App';
@ -57,7 +57,7 @@ const
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,
@ -72,7 +72,7 @@ const
reloadDraftFolder = () => {
const draftsFolder = FolderUserStore.draftsFolder();
if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) {
setFolderHash(draftsFolder, '');
setFolderETag(draftsFolder, '');
if (FolderUserStore.currentFolderFullName() === draftsFolder) {
MessagelistUserStore.reload(true);
} else {
@ -441,16 +441,6 @@ export class ComposePopupView extends AbstractViewPopup {
this.sendError(false);
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;
this.getMessageRequestParams(sSentFolder).then(params => {
@ -458,19 +448,23 @@ export class ComposePopupView extends AbstractViewPopup {
(iError, data) => {
this.sending(false);
if (iError) {
if (Notification.CantSaveMessage === iError) {
if (Notifications.CantSaveMessage === iError) {
this.sendSuccessButSaveError(true);
this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
} else {
this.sendError(true);
this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|| getNotification(Notification.CantSendMessage));
|| getNotification(Notifications.CantSendMessage));
}
} else {
this.close();
}
setFolderHash(this.draftsFolder(), '');
setFolderHash(sSentFolder, '');
setFolderETag(this.draftsFolder(), '');
setFolderETag(sSentFolder, '');
if (3 === arrayLength(this.aDraftInfo)) {
const folder = this.aDraftInfo[2];
setFolderETag(folder, '');
}
reloadDraftFolder();
},
params,
@ -492,57 +486,59 @@ export class ComposePopupView extends AbstractViewPopup {
}
saveCommand() {
if (FolderUserStore.draftsFolderNotEnabled()) {
showScreenPopup(FolderSystemPopupView, [FolderType.Drafts]);
} else {
this.savedError(false);
this.saving(true);
this.autosaveStart();
this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => {
Remote.request('SaveMessage',
(iError, oData) => {
let result = false;
if (!this.saving() && !this.sending()) {
if (FolderUserStore.draftsFolderNotEnabled()) {
showScreenPopup(FolderSystemPopupView, [FolderType.Drafts]);
} else {
this.savedError(false);
this.saving(true);
this.autosaveStart();
this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => {
Remote.request('SaveMessage',
(iError, oData) => {
let result = false;
this.saving(false);
this.saving(false);
if (!iError) {
if (oData.Result.folder && oData.Result.uid) {
result = true;
if (!iError) {
if (oData.Result.folder && oData.Result.uid) {
result = true;
if (this.bFromDraft) {
const message = MessageUserStore.message();
if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) {
MessageUserStore.message(null);
if (this.bFromDraft) {
const message = MessageUserStore.message();
if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) {
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) {
this.savedError(true);
this.savedErrorDesc(getNotification(Notification.CantSaveMessage));
}
if (!result) {
this.savedError(true);
this.savedErrorDesc(getNotification(Notifications.CantSaveMessage));
}
reloadDraftFolder();
},
params,
200000
);
}).catch(e => {
this.saving(false);
this.savedError(true);
this.savedErrorDesc(getNotification(Notification.CantSaveMessage) + ': ' + e);
});
reloadDraftFolder();
},
params,
200000
);
}).catch(e => {
this.saving(false);
this.savedError(true);
this.savedErrorDesc(getNotification(Notifications.CantSaveMessage) + ': ' + e);
});
}
}
}
@ -569,12 +565,7 @@ export class ComposePopupView extends AbstractViewPopup {
skipCommand() {
ComposePopupView.inEdit(true);
if (
!this.saving() &&
!this.sending() &&
!FolderUserStore.draftsFolderNotEnabled() &&
SettingsUserStore.allowDraftAutosave()
) {
if (!FolderUserStore.draftsFolderNotEnabled() && SettingsUserStore.allowDraftAutosave()) {
this.saveCommand();
}
@ -597,8 +588,6 @@ export class ComposePopupView extends AbstractViewPopup {
&& !FolderUserStore.draftsFolderNotEnabled()
&& SettingsUserStore.allowDraftAutosave()
&& !this.isEmptyForm(false)
&& !this.saving()
&& !this.sending()
&& !this.savedError()
) {
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))
.filter(v => v)
);
} else if (Notification.RequestAborted !== iError) {
} else if (Notifications.RequestAborted !== iError) {
fResponse([]);
}
},
@ -870,9 +859,9 @@ export class ComposePopupView extends AbstractViewPopup {
// https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = message.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img =>
img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
);
tpl.content.querySelectorAll('img').forEach(img => {
img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
});
sText = tpl.innerHTML.trim();
switch (msgComposeType) {
@ -1010,7 +999,7 @@ export class ComposePopupView extends AbstractViewPopup {
clickElement: dom.querySelector('#composeUploadButton'),
dragAndDropElement: dom.querySelector('.b-attachment-place')
}),
attachmentSizeLimit = pInt(SettingsGet('AttachmentLimit'));
attachmentSizeLimit = pInt(SettingsGet('attachmentLimit'));
oJua
.on('onDragEnter', () => {
@ -1124,12 +1113,12 @@ export class ComposePopupView extends AbstractViewPopup {
return false;
});
if (Settings.app('allowCtrlEnterOnCompose')) {
addShortcut('enter', 'meta', ScopeCompose, () => {
addShortcut('enter', 'meta', ScopeCompose, () => {
// if (SettingsUserStore.allowCtrlEnterOnCompose()) {
this.sendCommand();
return false;
});
}
// }
});
addShortcut('mailsend', '', ScopeCompose, () => {
this.sendCommand();
return false;
@ -1297,7 +1286,7 @@ export class ComposePopupView extends AbstractViewPopup {
*/
let text = this.oEditor.getData(),
encrypted = PgpUserStore.isEncrypted(text),
size = SettingsGet('PhpUploadSizes')['post_max_size'],
size = SettingsGet('phpUploadSizes')['post_max_size'],
quota = pInt(size);
switch (size.slice(-1)) {
case 'G': quota *= 1024; // fallthrough
@ -1409,7 +1398,10 @@ export class ComposePopupView extends AbstractViewPopup {
encrypt = this.pgpEncrypt() && this.canPgpEncrypt(),
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) {
let l;
do {

View file

@ -1,6 +1,6 @@
import { koComputable, addObservablesTo } from 'External/ko';
import { Notification } from 'Common/Enums';
import { Notifications } from 'Common/Enums';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
import { getNotification } from 'Common/Translator';
@ -65,7 +65,7 @@ export class FolderCreatePopupView extends AbstractViewPopup {
},
error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantCreateFolder)
getNotification(error.code, '', Notifications.CantCreateFolder)
+ '.\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';
@ -6,49 +7,34 @@ import Remote from 'Remote/User/Fetch';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { IdentityModel } from 'Model/Identity';
export class IdentityPopupView extends AbstractViewPopup {
constructor() {
super('Identity');
this.id = '';
addObservablesTo(this, {
id: '',
edit: false,
owner: false,
email: '',
emailFocused: false,
name: '',
nameFocused: false,
replyTo: '',
replyToFocused: false,
showReplyTo: false,
bcc: '',
bccFocused: false,
bccHasError: false,
showBcc: false,
signature: '',
signatureInsertBefore: false,
showBcc: false,
showReplyTo: false,
submitRequest: false,
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.replyTo.valueHasMutated();
@ -61,9 +47,8 @@ export class IdentityPopupView extends AbstractViewPopup {
this.signature?.__fetchEditorValue?.();
this.submitRequest(true);
const data = new FormData(form);
data.set('Id', this.id);
data.set('Id', this.id());
data.set('Signature', this.signature());
data.set('SignatureInsertBefore', this.signatureInsertBefore() ? 1 : 0);
Remote.request('IdentityUpdate', iError => {
this.submitRequest(false);
if (iError) {
@ -89,32 +74,23 @@ export class IdentityPopupView extends AbstractViewPopup {
if (identity) {
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 {
this.edit(false);
this.id = Jua.randomId();
this.name('');
this.email('');
this.replyTo('');
this.bcc('');
this.signature('');
this.signatureInsertBefore(false);
this.owner(false);
identity = new IdentityModel;
identity.id(Jua.randomId());
}
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() {
this.owner() || this.emailFocused(true);
this.id() ? this.emailFocused(true) : this.nameFocused(true);
}
}

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import ko from 'ko';
import { Scope } from 'Common/Enums';
import { addShortcut } from 'Common/Globals';
import { addShortcut, stopEvent } from 'Common/Globals';
import { mailBox, settings } from 'Common/Links';
//import { setFolderHash } from 'Common/Cache';
//import { setFolderETag } from 'Common/Cache';
import { addComputablesTo } from 'External/ko';
import { AppUserStore } from 'Stores/User/App';
@ -80,8 +80,7 @@ export class MailFolderList extends AbstractViewLeft {
setExpandedFolder(folder.fullName, collapsed);
folder.collapsed(!collapsed);
event.preventDefault();
event.stopPropagation();
stopEvent(event);
return;
}
}
@ -105,7 +104,7 @@ export class MailFolderList extends AbstractViewLeft {
}
/*
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
setFolderHash(folder.fullName, '');
setFolderETag(folder.fullName, '');
}
*/
let search = '';
@ -174,15 +173,6 @@ export class MailFolderList extends AbstractViewLeft {
moveAction(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() {

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import { settings } from 'Common/Links';
import { mailbox } from 'Common/Links';
import { getFolderInboxName } from 'Common/Cache';
import { AbstractViewLeft } from 'Knoin/AbstractViews';
export class SettingsMenuUserView extends AbstractViewLeft {
@ -15,4 +16,8 @@ export class SettingsMenuUserView extends AbstractViewLeft {
link(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 { MessageUserStore } from 'Stores/User/Message';
@ -11,7 +9,6 @@ export class SettingsPaneUserView extends AbstractViewRight {
constructor() {
super();
this.isMobile = ThemeStore.isMobile;
this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel;
}
@ -25,8 +22,4 @@ export class SettingsPaneUserView extends AbstractViewRight {
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 { ContactsPopupView } from 'View/Popup/Contacts';
import { doc, leftPanelDisabled, fireEvent, SettingsCapa, registerShortcut } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
import { fireEvent, stopEvent, SettingsCapa, registerShortcut } from 'Common/Globals';
import Remote from 'Remote/User/Fetch';
import { getNotification } from 'Common/Translator';
//import { clearCache } from 'Common/Cache';
//import { koComputable } from 'External/ko';
import { addObservablesTo } from 'External/ko';
@ -56,8 +53,7 @@ export class SystemDropDownUserView extends AbstractViewRight {
let email = account?.email;
if (email && 0 === event.button && AccountUserStore.email() != email) {
AccountUserStore.loading(true);
event.preventDefault();
event.stopPropagation();
stopEvent(event);
Remote.request('AccountSwitch',
(iError/*, oData*/) => {
if (iError) {
@ -69,10 +65,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
} else {
/* // Not working yet
forEachObjectEntry(oData.Result, (key, value) => rl.settings.set(key, value));
clearCache();
// MessageUserStore.setMessage();
// MessageUserStore.purgeMessageBodyCache();
// MessageUserStore.hideMessageBodies();
// MessageUserStore.message();
// MessageUserStore.purgeCache();
MessagelistUserStore([]);
// FolderUserStore.folderList([]);
loadFolders(value => {
@ -113,14 +107,6 @@ export class SystemDropDownUserView extends AbstractViewRight {
this.allowContacts && showScreenPopup(ContactsPopupView);
}
toggleLayout()
{
const mobile = !ThemeStore.isMobile();
doc.cookie = 'rllayout=' + (mobile ? 'mobile' : 'desktop') + '; samesite=strict';
ThemeStore.isMobile(mobile);
leftPanelDisabled(mobile);
}
logoutClick() {
rl.app.logout();
}

View file

@ -3,30 +3,26 @@
const
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
eId = id => doc.getElementById('rl-'+id),
app = eId('app'),
admin = app && '1' == app.dataset.admin,
layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
admin = '1' == eId('app')?.dataset?.admin,
showError = msg => {
let div = eId('loading-error');
div.append(msg);
toggle = div => {
eId('loading').hidden = true;
div.hidden = false;
},
showError = msg => {
let div = eId('loading-error');
div.append(msg);
toggle(div);
},
loadScript = src => {
if (!src) {
throw new Error('src should not be empty.');
}
return new Promise((resolve, reject) => {
loadScript = src => src ? new Promise((resolve, reject) => {
const script = doc.createElement('script');
script.onload = () => resolve();
script.onerror = () => reject('Failed loading ' + src);
script.src = src;
// script.async = true;
doc.head.append(script);
});
};
}) : Promise.reject('src is empty');
try {
let smctoken = doc.cookie.match(/(^|;) ?smctoken=([^;]+)/);
@ -37,8 +33,6 @@ try {
smctoken = encodeURIComponent(btoa(String.fromCharCode(...data)));
}
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";
} catch (e) {
console.error(e);
@ -46,8 +40,6 @@ try {
let RL_APP_DATA = {};
doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout[2] || (!layout && 1000 > innerWidth));
window.rl = {
adminArea: () => admin,
@ -58,9 +50,94 @@ window.rl = {
},
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;
const url = appData.StaticLibsJs,
cb = () => rl.app.bootstart();
@ -68,31 +145,14 @@ window.rl = {
.then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
.then(() => appData.PluginsLink ? loadScript(qUri(appData.PluginsLink)) : Promise.resolve())
.then(() => rl.app
? cb()
: doc.addEventListener('readystatechange', () => 'complete' == doc.readyState && cb())
? cb()
: doc.addEventListener('readystatechange', () => 'complete' == doc.readyState && cb())
)
.catch(e => {
showError(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));
}

64
dev/bootstrap.js vendored
View file

@ -1,10 +1,6 @@
import { Settings } from 'Common/Globals';
import { i18n } from 'Common/Translator';
import { root } from 'Common/Links';
import { isArray } from 'Common/Utils';
export default App => {
rl.app = App;
@ -33,64 +29,4 @@ export default App => {
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) {
timer && clearTimeout(timer);
timer = setTimeout(()=>{
func.apply(this, args)
func.apply(this, args);
timer = 0;
}, ms);
};
@ -68,12 +68,10 @@
Function.prototype.throttle = function(ms) {
let func = this, timer;
return function(...args) {
if (!timer) {
timer = setTimeout(()=>{
func.apply(this, args)
timer = timer || setTimeout(()=>{
func.apply(this, args);
timer = 0;
}, 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.

View file

@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# 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 && \
unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \

View file

@ -3,7 +3,7 @@
<id>snappymail</id>
<name>SnappyMail</name>
<summary>SnappyMail Webmail</summary>
<version>2.26.1</version>
<version>2.26.4</version>
<licence>agpl</licence>
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
<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>
<bugs>https://github.com/the-djmaze/snappymail/issues</bugs>
<dependencies>
<php min-version="7.4" min-int-size="64" />
<php min-version="7.4" min-int-size="32" />
<lib>mbstring</lib>
<lib>zlib</lib>
<!--

View file

@ -16,7 +16,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
function __construct() {
$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'"]);
// Nextcloud only sets 'strict-dynamic' when browserSupportsCspV3() ?
@ -24,12 +24,12 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
? $this->useStrictDynamic(true) // NC24+
: $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->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));
}

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