From 198cc852c3b0812dc5032b543931117eb07ebb1c Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Sun, 29 Sep 2024 11:50:00 +0200
Subject: [PATCH 01/45] O365 option to use Azure or Personal accounts #1645
---
plugins/login-o365/LoginOAuth2.js | 4 +++-
plugins/login-o365/index.php | 18 +++++++++++++-----
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/plugins/login-o365/LoginOAuth2.js b/plugins/login-o365/LoginOAuth2.js
index 4afc930bf..dd8c843ed 100644
--- a/plugins/login-o365/LoginOAuth2.js
+++ b/plugins/login-o365/LoginOAuth2.js
@@ -1,11 +1,13 @@
(rl => {
const client_id = rl.pluginSettingsGet('login-o365', 'client_id'),
+ // https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
+ query = rl.pluginSettingsGet('login-o365', 'personal') ? '' : '?',
tenant = rl.pluginSettingsGet('login-o365', 'tenant'),
login = () => {
document.location = 'https://login.microsoftonline.com/'+tenant+'/oauth2/v2.0/authorize?' + (new URLSearchParams({
response_type: 'code',
client_id: client_id,
- redirect_uri: document.location.href.replace(/\/$/, '') + '/LoginO365',
+ redirect_uri: document.location.href.replace(/\/$/, '') + '/' + query + 'LoginO365',
scope: [
// Associate personal info
'openid',
diff --git a/plugins/login-o365/index.php b/plugins/login-o365/index.php
index 902c46d15..62050f429 100644
--- a/plugins/login-o365/index.php
+++ b/plugins/login-o365/index.php
@@ -7,8 +7,9 @@
*
* https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps
*
- * redirect_uri=https://{DOMAIN}/?LoginO365
- * redirect_uri=https://{DOMAIN}/LoginO365
+ * https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
+ * Azure: redirect_uri=https://{DOMAIN}/?LoginO365
+ * Personal: redirect_uri=https://{DOMAIN}/LoginO365
*/
use RainLoop\Model\MainAccount;
@@ -18,8 +19,8 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Office365/Outlook OAuth2',
- VERSION = '0.2',
- RELEASE = '2024-08-13',
+ VERSION = '0.3',
+ RELEASE = '2024-09-29',
REQUIRED = '2.36.1',
CATEGORY = 'Login',
DESCRIPTION = 'Office365/Outlook IMAP, Sieve & SMTP login using RFC 7628 OAuth2';
@@ -47,6 +48,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
public function httpPaths(array &$aPaths) : void
{
+ // Personal accounts workaround
if (!empty($_SERVER['PATH_INFO']) && \str_ends_with($_SERVER['PATH_INFO'], 'LoginO365')) {
$aPaths = ['LoginO365'];
}
@@ -113,7 +115,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
$iExpires += $aResponse['expires_in'];
$oO365->setAccessToken($sAccessToken);
- $aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo"');
+ $aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo');
if (200 != $aUserInfo['code']) {
throw new \RuntimeException("HTTP: {$aResponse['code']}");
}
@@ -154,6 +156,12 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
public function configMapping() : array
{
return [
+ \RainLoop\Plugins\Property::NewInstance('personal')
+ ->SetLabel('Use with personal accounts')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(true)
+ ->SetAllowedInJs()
+ ->SetDescription('Sign in users with personal Microsoft accounts such as Outlook.com (Hotmail)'),
\RainLoop\Plugins\Property::NewInstance('client_id')
->SetLabel('Client ID')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
From dc7eed4c80a9dc3d8e40da71a79604c87c7ab8a3 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Sun, 29 Sep 2024 12:01:28 +0200
Subject: [PATCH 02/45] Resolve #1780
---
dev/View/User/MailBox/MessageView.js | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js
index 3dcf461fc..5a879466e 100644
--- a/dev/View/User/MailBox/MessageView.js
+++ b/dev/View/User/MailBox/MessageView.js
@@ -20,7 +20,8 @@ import {
fireEvent,
stopEvent,
addShortcut,
- registerShortcut
+ registerShortcut,
+ formFieldFocused
} from 'Common/Globals';
import { arrayLength } from 'Common/Utils';
@@ -467,8 +468,10 @@ export class MailMessageView extends AbstractViewRight {
});
addShortcut('b', 'shift', [ScopeMessageList, ScopeMessageView], () => {
- currentMessage()?.swapColors?.();
- return false;
+ if (!formFieldFocused()) {
+ currentMessage()?.swapColors?.();
+ return false;
+ }
});
addShortcut('arrowup,arrowleft', 'meta', [ScopeMessageList, ScopeMessageView], () => {
From 126208166d99a3e5f4d8a506a2c53afa8974c4b2 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Mon, 30 Sep 2024 14:57:46 +0200
Subject: [PATCH 03/45] Bugfix: composer dialog scroll got broken in v2.28
---
dev/Styles/User/Compose.less | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/dev/Styles/User/Compose.less b/dev/Styles/User/Compose.less
index 8e26aef0e..56eae3dd1 100644
--- a/dev/Styles/User/Compose.less
+++ b/dev/Styles/User/Compose.less
@@ -22,12 +22,18 @@
.tabs {
height: 100%;
+ max-height: 100%;
+ overflow: auto;
}
.tabs > label > * + * {
margin-left: 0.5em;
}
+ .tabs .tab-content {
+ overflow: auto;
+ }
+
.textAreaParent, .attachmentAreaParent {
height: 100%;
min-height: 200px;
From 9b5eaf4b5fa243b645cdbaeaf52fb6f42a158e32 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Mon, 30 Sep 2024 14:58:53 +0200
Subject: [PATCH 04/45] Bugfix: Composer dialog "from" triangle wrong position
due to font changes
---
snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html
index fb5be1b5a..c92e56714 100644
--- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html
+++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html
@@ -85,7 +85,7 @@
|
-
+
From beb418080f53338b9ddd38ff2e2cc3b52d245045 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Mon, 30 Sep 2024 16:55:01 +0200
Subject: [PATCH 05/45] Squire: prevent collapse of empty elements so that the
caret can be placed in there. This eliminates the use of hacky inside.
---
dev/Styles/User/SquireUI.less | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/dev/Styles/User/SquireUI.less b/dev/Styles/User/SquireUI.less
index 646ee4dd1..d34c684a9 100644
--- a/dev/Styles/User/SquireUI.less
+++ b/dev/Styles/User/SquireUI.less
@@ -56,6 +56,11 @@
.squire-wysiwyg {
font-family: var(--fontSans);
+ /* Prevent collapse of empty elements so that the caret can be placed in there */
+ *:empty::before {
+ content: "\200B";
+ }
+
ul {
padding-left: 40px;
li {
From f43c2c92b5e2401073cc0120447627cfe2fdfc9f Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Mon, 30 Sep 2024 17:01:50 +0200
Subject: [PATCH 06/45] Squire sanitizeToDOMFragment use cleanHTML
---
dev/Common/Html.js | 11 +++++++----
dev/External/SquireUI.js | 29 ++---------------------------
2 files changed, 9 insertions(+), 31 deletions(-)
diff --git a/dev/Common/Html.js b/dev/Common/Html.js
index 23df1e025..3f86fe58c 100644
--- a/dev/Common/Html.js
+++ b/dev/Common/Html.js
@@ -23,7 +23,7 @@ const
// Structural Elements:
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
'article','aside','header','footer','main','section',
- 'details','summary',
+ 'details','summary','nav',
// List Elements
'dd','dl','dt','li','ol','ul',
// Text Formatting Elements
@@ -282,6 +282,8 @@ export const
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
+ // others
+ //'class', 'id', 'target'
];
if (SettingsUserStore.allowStyles()) {
@@ -332,7 +334,7 @@ export const
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
tmpl.content.querySelectorAll(
- ':not('+allowedTags+')'
+ ':not('+allowedTags+'),a:empty,span:empty'
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove());
/* // Is this slower or faster?
@@ -356,7 +358,7 @@ export const
});
*/
- [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
+ msgId && [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
@@ -631,7 +633,7 @@ export const
}
});
- blockquoteSwitcher();
+ msgId && blockquoteSwitcher();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
@@ -819,6 +821,7 @@ export const
};
rl.Utils = {
+ cleanHtml: cleanHtml,
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml,
htmlToMarkdown: htmlToMarkdown
diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js
index af2bdff42..fe07975d2 100644
--- a/dev/External/SquireUI.js
+++ b/dev/External/SquireUI.js
@@ -3,10 +3,6 @@
(doc => {
const
- removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
- allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
- allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
-
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey() + ' + ',
@@ -21,38 +17,17 @@ const
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
- getFragmentOfChildren = parent => {
- let frag = doc.createDocumentFragment();
- frag.append(...parent.childNodes);
- return frag;
- },
-
SquireDefaultConfig = {
/*
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
- tpl.innerHTML = (html||'')
+ html = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(//g,'')
.replace(/]*>\s*<\/span>/gi,'')
.trim();
- tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
- if (isPaste) {
- tpl.querySelectorAll(removeElements).forEach(el => el.remove());
- tpl.querySelectorAll('*').forEach(el => {
- if (!el.matches(allowedElements)) {
- el.replaceWith(getFragmentOfChildren(el));
- } else if (el.hasAttributes()) {
- [...el.attributes].forEach(attr => {
- let name = attr.name.toLowerCase();
- if (!allowedAttributes.includes(name)) {
- el.removeAttribute(name);
- }
- });
- }
- });
- }
+ tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
return tpl.content;
}
};
From 4b802c0cb35aeee2616d9e15737c528e7da7fcd2 Mon Sep 17 00:00:00 2001
From: Matheus Stolf
Date: Mon, 30 Sep 2024 16:06:49 -0300
Subject: [PATCH 07/45] Fixed the translation of the fields MIDDLE_NAME,
NAME_PREFIX, NAME_SUFFIX to pt-BR
---
snappymail/v/0.0.0/app/localization/pt-BR/user.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/snappymail/v/0.0.0/app/localization/pt-BR/user.json b/snappymail/v/0.0.0/app/localization/pt-BR/user.json
index 57843d841..0ac96760e 100644
--- a/snappymail/v/0.0.0/app/localization/pt-BR/user.json
+++ b/snappymail/v/0.0.0/app/localization/pt-BR/user.json
@@ -219,9 +219,9 @@
"DISPLAY_NAME": "Nome de exibição",
"LAST_NAME": "Sobrenome",
"FIRST_NAME": "Primeiro nome",
- "MIDDLE_NAME": "Middle name",
- "NAME_PREFIX": "Name prefix",
- "NAME_SUFFIX": "Name suffix",
+ "MIDDLE_NAME": "Nomes adicionais",
+ "NAME_PREFIX": "Prefixo do nome",
+ "NAME_SUFFIX": "Sufixo do nome",
"NICK_NAME": "Apelido",
"LABEL_READ_ONLY": "Somente Leitura",
"ADD_MENU_LABEL": "Adicionar",
From 584728bc893a866d163ab868db2ad0ff7e1fcce4 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Tue, 1 Oct 2024 17:56:43 +0200
Subject: [PATCH 08/45] Squire: improved handling of BR elements
---
dev/Styles/User/SquireUI.less | 3 +-
vendors/squire2/dist/squire-raw.js | 443 ++++++++++++-----------------
2 files changed, 191 insertions(+), 255 deletions(-)
diff --git a/dev/Styles/User/SquireUI.less b/dev/Styles/User/SquireUI.less
index d34c684a9..1a9c46ce3 100644
--- a/dev/Styles/User/SquireUI.less
+++ b/dev/Styles/User/SquireUI.less
@@ -56,10 +56,11 @@
.squire-wysiwyg {
font-family: var(--fontSans);
- /* Prevent collapse of empty elements so that the caret can be placed in there */
+ /* Prevent collapse of empty elements so that the caret can be placed in there
*:empty::before {
content: "\200B";
}
+ */
ul {
padding-left: 40px;
diff --git a/vendors/squire2/dist/squire-raw.js b/vendors/squire2/dist/squire-raw.js
index d12426e59..4367a29dd 100644
--- a/vendors/squire2/dist/squire-raw.js
+++ b/vendors/squire2/dist/squire-raw.js
@@ -349,203 +349,6 @@
return range;
};
- // source/node/MergeSplit.ts
- var fixCursor = (node) => {
- let fixer = null;
- if (node instanceof Text) {
- return node;
- }
- if (isInline(node)) {
- let child = node.firstChild;
- if (cantFocusEmptyTextNodes) {
- while (child && child instanceof Text && !child.data) {
- node.removeChild(child);
- child = node.firstChild;
- }
- }
- if (!child) {
- if (cantFocusEmptyTextNodes) {
- fixer = document.createTextNode(ZWS);
- } else {
- fixer = document.createTextNode("");
- }
- }
- } else if ((node instanceof Element || node instanceof DocumentFragment) && !node.querySelector("BR")) {
- fixer = createElement("BR");
- let parent = node;
- let child;
- while ((child = parent.lastElementChild) && !isInline(child)) {
- parent = child;
- }
- node = parent;
- }
- if (fixer) {
- try {
- node.appendChild(fixer);
- } catch (error) {
- }
- }
- return node;
- };
- var fixContainer = (container, root) => {
- let wrapper = null;
- [...container.childNodes].forEach((child) => {
- const isBR = child.nodeName === "BR";
- if (!isBR && child.parentNode == root && isInline(child)) {
- wrapper || (wrapper = createElement("DIV"));
- wrapper.append(child);
- } else if (isBR || wrapper) {
- wrapper || (wrapper = createElement("DIV"));
- fixCursor(wrapper);
- if (isBR) {
- child.replaceWith(wrapper);
- } else {
- container.insertBefore(wrapper, child);
- }
- wrapper = null;
- }
- isContainer(child) && fixContainer(child, root);
- });
- wrapper && container.append(fixCursor(wrapper));
- return container;
- };
- var split = (node, offset, stopNode, root) => {
- if (node instanceof Text && node !== stopNode) {
- if (typeof offset !== "number") {
- throw new Error("Offset must be a number to split text node!");
- }
- if (!node.parentNode) {
- throw new Error("Cannot split text node with no parent!");
- }
- return split(node.parentNode, node.splitText(offset), stopNode, root);
- }
- let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset;
- const parent = node.parentNode;
- if (!parent || node === stopNode || !(node instanceof Element)) {
- return nodeAfterSplit;
- }
- const clone = node.cloneNode(false);
- while (nodeAfterSplit) {
- const next = nodeAfterSplit.nextSibling;
- clone.append(nodeAfterSplit);
- nodeAfterSplit = next;
- }
- if (node instanceof HTMLOListElement && getClosest(node, root, "BLOCKQUOTE")) {
- clone.start = (+node.start || 1) + node.childNodes.length - 1;
- }
- fixCursor(node);
- fixCursor(clone);
- node.after(clone);
- return split(parent, clone, stopNode, root);
- };
- var _mergeInlines = (node, fakeRange) => {
- const children = node.childNodes;
- let l = children.length;
- const frags = [];
- while (l--) {
- const child = children[l];
- const prev = l ? children[l - 1] : null;
- if (prev && isInline(child) && areAlike(child, prev)) {
- if (fakeRange.startContainer === child) {
- fakeRange.startContainer = prev;
- fakeRange.startOffset += getLength(prev);
- }
- if (fakeRange.endContainer === child) {
- fakeRange.endContainer = prev;
- fakeRange.endOffset += getLength(prev);
- }
- if (fakeRange.startContainer === node) {
- if (fakeRange.startOffset > l) {
- --fakeRange.startOffset;
- } else if (fakeRange.startOffset === l) {
- fakeRange.startContainer = prev;
- fakeRange.startOffset = getLength(prev);
- }
- }
- if (fakeRange.endContainer === node) {
- if (fakeRange.endOffset > l) {
- --fakeRange.endOffset;
- } else if (fakeRange.endOffset === l) {
- fakeRange.endContainer = prev;
- fakeRange.endOffset = getLength(prev);
- }
- }
- detach(child);
- if (child instanceof Text) {
- prev.appendData(child.data);
- } else {
- frags.push(empty(child));
- }
- } else if (child instanceof Element) {
- let frag;
- while (frag = frags.pop()) {
- child.append(frag);
- }
- _mergeInlines(child, fakeRange);
- }
- }
- };
- var mergeInlines = (node, range) => {
- const element = node instanceof Text ? node.parentNode : node;
- if (element instanceof Element) {
- const fakeRange = {
- startContainer: range.startContainer,
- startOffset: range.startOffset,
- endContainer: range.endContainer,
- endOffset: range.endOffset
- };
- _mergeInlines(element, fakeRange);
- range.setStart(fakeRange.startContainer, fakeRange.startOffset);
- range.setEnd(fakeRange.endContainer, fakeRange.endOffset);
- }
- };
- var mergeWithBlock = (block, next, range, root) => {
- let container = next;
- let parent;
- let offset;
- while ((parent = container.parentNode) && parent !== root && parent instanceof Element && parent.childNodes.length === 1) {
- container = parent;
- }
- detach(container);
- offset = block.childNodes.length;
- const last = block.lastChild;
- if (last && last.nodeName === "BR") {
- last.remove();
- --offset;
- }
- block.append(empty(next));
- range.setStart(block, offset);
- range.collapse(true);
- mergeInlines(block, range);
- };
- var mergeContainers = (node, root) => {
- const prev = node.previousSibling;
- const first = node.firstChild;
- const isListItem = node.nodeName === "LI";
- if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) {
- return;
- }
- if (prev && areAlike(prev, node)) {
- if (!isContainer(prev)) {
- if (!isListItem) {
- return;
- }
- const block = createElement("DIV");
- block.append(empty(prev));
- prev.append(block);
- }
- detach(node);
- const needsFix = !isContainer(node);
- prev.append(empty(node));
- needsFix && fixContainer(prev, root);
- first && mergeContainers(first, root);
- } else if (isListItem) {
- const block = createElement("DIV");
- node.insertBefore(block, first);
- fixCursor(block);
- }
- };
-
// source/Clean.ts
var styleToSemantic = {
"font-weight": {
@@ -731,23 +534,20 @@
}
}
};
- var cleanupBRs = (node, root, keepForBlankLine) => {
+ var cleanupBRs = (node) => {
const brs = node.querySelectorAll("BR");
const brBreaksLine = [];
let l = brs.length;
for (let i = 0; i < l; ++i) {
- brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine);
+ brBreaksLine[i] = isLineBreak(brs[i], false);
}
while (l--) {
const br = brs[l];
const parent = br.parentNode;
- if (!parent) {
- continue;
- }
- if (!brBreaksLine[l]) {
- detach(br);
- } else if (!isInline(parent)) {
- fixContainer(parent, root);
+ if (parent) {
+ if (!brBreaksLine[l]) {
+ detach(br);
+ }
}
}
};
@@ -755,6 +555,164 @@
return text.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");
};
+ // source/node/MergeSplit.ts
+ var fixCursor = (node) => {
+ if ((node instanceof Element || node instanceof DocumentFragment) && !isInline(node) && !node.children.length && !node.textContent.length) {
+ node.appendChild(createElement("BR"));
+ }
+ return node;
+ };
+ var fixContainer = (container) => {
+ let wrapper = null;
+ [...container.childNodes].forEach((child) => {
+ if (isInline(child)) {
+ wrapper || (wrapper = createElement("DIV"));
+ wrapper.append(child);
+ } else if (wrapper) {
+ (wrapper.children.length || wrapper.textContent.trim().length) && container.insertBefore(wrapper, child);
+ wrapper = null;
+ }
+ });
+ wrapper && container.append(wrapper);
+ return container;
+ };
+ var split = (node, offset, stopNode, root) => {
+ if (node instanceof Text && node !== stopNode) {
+ if (typeof offset !== "number") {
+ throw new Error("Offset must be a number to split text node!");
+ }
+ if (!node.parentNode) {
+ throw new Error("Cannot split text node with no parent!");
+ }
+ return split(node.parentNode, node.splitText(offset), stopNode, root);
+ }
+ let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset;
+ const parent = node.parentNode;
+ if (!parent || node === stopNode || !(node instanceof Element)) {
+ return nodeAfterSplit;
+ }
+ const clone = node.cloneNode(false);
+ while (nodeAfterSplit) {
+ const next = nodeAfterSplit.nextSibling;
+ clone.append(nodeAfterSplit);
+ nodeAfterSplit = next;
+ }
+ if (node instanceof HTMLOListElement && getClosest(node, root, "BLOCKQUOTE")) {
+ clone.start = (+node.start || 1) + node.childNodes.length - 1;
+ }
+ fixCursor(node);
+ fixCursor(clone);
+ node.after(clone);
+ return split(parent, clone, stopNode, root);
+ };
+ var _mergeInlines = (node, fakeRange) => {
+ const children = node.childNodes;
+ let l = children.length;
+ const frags = [];
+ while (l--) {
+ const child = children[l];
+ const prev = l ? children[l - 1] : null;
+ if (prev && isInline(child) && areAlike(child, prev)) {
+ if (fakeRange.startContainer === child) {
+ fakeRange.startContainer = prev;
+ fakeRange.startOffset += getLength(prev);
+ }
+ if (fakeRange.endContainer === child) {
+ fakeRange.endContainer = prev;
+ fakeRange.endOffset += getLength(prev);
+ }
+ if (fakeRange.startContainer === node) {
+ if (fakeRange.startOffset > l) {
+ --fakeRange.startOffset;
+ } else if (fakeRange.startOffset === l) {
+ fakeRange.startContainer = prev;
+ fakeRange.startOffset = getLength(prev);
+ }
+ }
+ if (fakeRange.endContainer === node) {
+ if (fakeRange.endOffset > l) {
+ --fakeRange.endOffset;
+ } else if (fakeRange.endOffset === l) {
+ fakeRange.endContainer = prev;
+ fakeRange.endOffset = getLength(prev);
+ }
+ }
+ detach(child);
+ if (child instanceof Text) {
+ prev.appendData(child.data);
+ } else {
+ frags.push(empty(child));
+ }
+ } else if (child instanceof Element) {
+ let frag;
+ while (frag = frags.pop()) {
+ child.append(frag);
+ }
+ _mergeInlines(child, fakeRange);
+ }
+ }
+ };
+ var mergeInlines = (node, range) => {
+ const element = node instanceof Text ? node.parentNode : node;
+ if (element instanceof Element) {
+ const fakeRange = {
+ startContainer: range.startContainer,
+ startOffset: range.startOffset,
+ endContainer: range.endContainer,
+ endOffset: range.endOffset
+ };
+ _mergeInlines(element, fakeRange);
+ range.setStart(fakeRange.startContainer, fakeRange.startOffset);
+ range.setEnd(fakeRange.endContainer, fakeRange.endOffset);
+ }
+ };
+ var mergeWithBlock = (block, next, range, root) => {
+ let container = next;
+ let parent;
+ let offset;
+ while ((parent = container.parentNode) && parent !== root && parent instanceof Element && parent.childNodes.length === 1) {
+ container = parent;
+ }
+ detach(container);
+ offset = block.childNodes.length;
+ const last = block.lastChild;
+ if (last && last.nodeName === "BR") {
+ last.remove();
+ --offset;
+ }
+ block.append(empty(next));
+ range.setStart(block, offset);
+ range.collapse(true);
+ mergeInlines(block, range);
+ };
+ var mergeContainers = (node, root) => {
+ const prev = node.previousSibling;
+ const first = node.firstChild;
+ const isListItem = node.nodeName === "LI";
+ if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) {
+ return;
+ }
+ if (prev && areAlike(prev, node)) {
+ if (!isContainer(prev)) {
+ if (!isListItem) {
+ return;
+ }
+ const block = createElement("DIV");
+ block.append(empty(prev));
+ prev.append(block);
+ }
+ detach(node);
+ const needsFix = !isContainer(node);
+ prev.append(empty(node));
+ needsFix && fixContainer(prev);
+ first && mergeContainers(first, root);
+ } else if (isListItem) {
+ const block = createElement("DIV");
+ node.insertBefore(block, first);
+ fixCursor(block);
+ }
+ };
+
// source/node/Block.ts
var getBlockWalker = (node, root) => {
const walker = createTreeWalker(root, SHOW_ELEMENT, isBlock);
@@ -1059,7 +1017,7 @@
var insertTreeFragmentIntoRange = (range, frag, root) => {
const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild);
let node;
- fixContainer(frag, root);
+ fixContainer(frag);
node = frag;
while (node = getNextBlock(node, root)) {
fixCursor(node);
@@ -1080,7 +1038,7 @@
range.collapse(true);
let container = range.endContainer;
let offset = range.endOffset;
- cleanupBRs(block, root, false);
+ cleanupBRs(block);
if (isInline(container)) {
const nodeAfterSplit = split(
container,
@@ -1480,7 +1438,7 @@
return;
}
let current = startBlock;
- fixContainer(current.parentNode, root);
+ fixContainer(current.parentNode);
const previous = getPreviousBlock(current, root);
if (previous) {
if (!previous.isContentEditable) {
@@ -1547,7 +1505,7 @@
if (!current) {
return;
}
- fixContainer(current.parentNode, root);
+ fixContainer(current.parentNode);
next = getNextBlock(current, root);
if (next) {
if (!next.isContentEditable) {
@@ -1945,8 +1903,6 @@
_makeConfig(userConfig) {
const config = {
blockTag: "DIV",
- blockAttributes: null,
- tagAttributes: {},
classNames: {
color: "color",
fontFamily: "font",
@@ -2549,8 +2505,8 @@
const frag = this._config.sanitizeToDOMFragment(html, this);
const root = this._root;
cleanTree(frag, this._config);
- cleanupBRs(frag, root, false);
- fixContainer(frag, root);
+ cleanupBRs(frag);
+ fixContainer(frag);
let node = frag;
let child = node.firstChild;
if (!child || child.nodeName === "BR") {
@@ -2596,7 +2552,7 @@
this.addDetectedLinks(frag, frag);
}
cleanTree(frag, this._config);
- cleanupBRs(frag, root, false);
+ cleanupBRs(frag);
removeEmptyInlines(frag);
frag.normalize();
let node = frag;
@@ -2728,13 +2684,8 @@
const lines = plainText.split("\n");
const config = this._config;
const tag = config.blockTag;
- const attributes = config.blockAttributes;
const closeBlock = "" + tag + ">";
- let openBlock = "<" + tag;
- for (const attr in attributes) {
- openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"';
- }
- openBlock += ">";
+ const openBlock = "<" + tag + ">";
for (let i = 0, l = lines.length; i < l; ++i) {
let line = lines[i];
line = escapeHTML(line).replace(/ (?=(?: |$))/g, " ");
@@ -3034,7 +2985,6 @@
{
href: url
},
- this._config.tagAttributes.a,
attributes
);
return this.changeFormat(
@@ -3065,7 +3015,6 @@
(node2) => !getClosest(node2, root || this._root, "A")
);
const linkRegExp = this.linkRegExp;
- const defaultAttributes = this._config.tagAttributes.a;
let node;
while (node = walker.nextNode()) {
const parent = node.parentNode;
@@ -3082,12 +3031,9 @@
}
const child = createElement(
"A",
- Object.assign(
- {
- href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0]
- },
- defaultAttributes
- )
+ {
+ href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0]
+ }
);
child.textContent = data.slice(index, endIndex);
parent.insertBefore(child, node);
@@ -3107,7 +3053,7 @@
createDefaultBlock(children) {
const config = this._config;
return fixCursor(
- createElement(config.blockTag, config.blockAttributes, children)
+ createElement(config.blockTag, null, children)
);
}
splitBlock(lineBreakOnly, range) {
@@ -3197,21 +3143,15 @@
}
node = range.startContainer;
const offset = range.startOffset;
- let splitTag = this.tagAfterSplit[block.nodeName];
+ let splitTag = this.tagAfterSplit[block.nodeName] || this._config.blockTag;
nodeAfterSplit = split(
node,
offset,
block.parentNode,
this._root
);
- const config = this._config;
- let splitProperties = null;
- if (!splitTag) {
- splitTag = config.blockTag;
- splitProperties = config.blockAttributes;
- }
- if (!hasTagAttributes(nodeAfterSplit, splitTag, splitProperties)) {
- block = createElement(splitTag, splitProperties);
+ if (!hasTagAttributes(nodeAfterSplit, splitTag)) {
+ block = createElement(splitTag);
if (nodeAfterSplit.dir) {
block.dir = nodeAfterSplit.dir;
}
@@ -3376,11 +3316,9 @@
this._recordUndoState(range, this._isInUndoState);
const type = list.nodeName;
let newParent = startLi.previousSibling;
- let listAttrs;
let next;
if (newParent.nodeName !== type) {
- listAttrs = this._config.tagAttributes[type.toLowerCase()];
- newParent = createElement(type, listAttrs);
+ newParent = createElement(type);
list.insertBefore(newParent, startLi);
}
do {
@@ -3450,9 +3388,6 @@
}
_makeList(frag, type) {
const walker = getBlockWalker(frag, this._root);
- const tagAttributes = this._config.tagAttributes;
- const listAttrs = tagAttributes[type.toLowerCase()];
- const listItemAttrs = tagAttributes.li;
let node;
while (node = walker.nextNode()) {
if (node.parentNode instanceof HTMLLIElement) {
@@ -3460,7 +3395,7 @@
walker.currentNode = node.lastChild;
}
if (!(node instanceof HTMLLIElement)) {
- const newLi = createElement("LI", listItemAttrs);
+ const newLi = createElement("LI");
if (node.dir) {
newLi.dir = node.dir;
}
@@ -3469,7 +3404,7 @@
prev.append(newLi);
detach(node);
} else {
- replaceWith(node, createElement(type, listAttrs, [newLi]));
+ replaceWith(node, createElement(type, null, [newLi]));
}
newLi.append(empty(node));
walker.currentNode = newLi;
@@ -3479,7 +3414,7 @@
if (tag !== type && /^[OU]L$/.test(tag)) {
replaceWith(
node,
- createElement(type, listAttrs, [empty(node)])
+ createElement(type, null, [empty(node)])
);
}
}
@@ -3502,7 +3437,7 @@
for (let i = 0, l = lists.length; i < l; ++i) {
const list = lists[i];
const listFrag = empty(list);
- fixContainer(listFrag, root);
+ fixContainer(listFrag);
replaceWith(list, listFrag);
}
for (let i = 0, l = items.length; i < l; ++i) {
@@ -3510,7 +3445,7 @@
if (isBlock(item)) {
replaceWith(item, this.createDefaultBlock([empty(item)]));
} else {
- fixContainer(item, root);
+ fixContainer(item);
replaceWith(item, empty(item));
}
}
@@ -3523,7 +3458,7 @@
this.modifyBlocks(
(frag) => createElement(
"BLOCKQUOTE",
- this._config.tagAttributes.blockquote,
+ null,
[frag]
),
range
@@ -3608,7 +3543,7 @@
}
output.normalize();
return fixCursor(
- createElement("PRE", this._config.tagAttributes.pre, [
+ createElement("PRE", null, [
output
])
);
@@ -3618,7 +3553,7 @@
this.changeFormat(
{
tag: "CODE",
- attributes: this._config.tagAttributes.code
+ attributes: null
},
null,
range
@@ -3654,7 +3589,7 @@
node.parentNode.insertBefore(contents, node);
node.data = value;
}
- fixContainer(pre, root);
+ fixContainer(pre);
replaceWith(pre, empty(pre));
}
return frag;
From f7f8d2d5429e248c8a2ceb012a420816d9871e1e Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Tue, 1 Oct 2024 18:05:26 +0200
Subject: [PATCH 09/45] Bugfix the new msgId handling due to Squire
sanitizeToDOMFragment using cleanHTML
---
dev/Common/Html.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/dev/Common/Html.js b/dev/Common/Html.js
index 3f86fe58c..0e8029fbd 100644
--- a/dev/Common/Html.js
+++ b/dev/Common/Html.js
@@ -239,6 +239,8 @@ export const
linkedData: []
},
+ isMsg = !!msgId,
+
findAttachmentByCid = cId => oAttachments.findByCid(cId),
findLocationByCid = cId => {
const attachment = findAttachmentByCid(cId);
@@ -358,7 +360,7 @@ export const
});
*/
- msgId && [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
+ isMsg && [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
@@ -633,7 +635,7 @@ export const
}
});
- msgId && blockquoteSwitcher();
+ isMsg && blockquoteSwitcher();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
From 68045f50f627b5a25b05fb0e777caaae0f1ecae7 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Wed, 2 Oct 2024 01:52:15 +0200
Subject: [PATCH 10/45] Resolve #1787
---
dev/View/User/MailBox/MessageList.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js
index 4329dfb46..a55062be5 100644
--- a/dev/View/User/MailBox/MessageList.js
+++ b/dev/View/User/MailBox/MessageList.js
@@ -629,8 +629,10 @@ export class MailMessageList extends AbstractViewRight {
}
},
dblclick: event => {
- let el = eqs(event, '.messageListItem');
- el && this.gotoThread(ko.dataFor(el));
+ let msg = ko.dataFor(eqs(event, '.messageListItem'));
+ if (msg) {
+ msg.threadsLen() ? this.gotoThread(msg) : toggleFullscreen();
+ }
}
});
From d3de9fae6bfa2de0040047ed8b95077d72a57b16 Mon Sep 17 00:00:00 2001
From: tinola
Date: Wed, 2 Oct 2024 07:29:08 +0200
Subject: [PATCH 11/45] Update user.json
update Polish translation
---
.../v/0.0.0/app/localization/pl/user.json | 32 +++++++++----------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/snappymail/v/0.0.0/app/localization/pl/user.json b/snappymail/v/0.0.0/app/localization/pl/user.json
index 24ea42e1e..b4a11b0af 100644
--- a/snappymail/v/0.0.0/app/localization/pl/user.json
+++ b/snappymail/v/0.0.0/app/localization/pl/user.json
@@ -89,18 +89,18 @@
"ARCHIVE_NAME": "Archiwum"
},
"IMAP": {
- "ACL_RIGHTS": "Rights",
- "ACL_A": "Administer",
- "ACL_E": "Expunge",
- "ACL_I": "Insert messages",
- "ACL_K": "Create sub-folders",
- "ACL_L": "Show messages",
- "ACL_P": "Send mail",
- "ACL_R": "Select folder",
- "ACL_S": "\\SEEN tag",
- "ACL_T": "\\DELETED tag",
- "ACL_W": "Other tags",
- "ACL_X": "Delete\/Rename folder"
+ "ACL_RIGHTS": "Uprawnienia",
+ "ACL_A": "Administracja",
+ "ACL_E": "Usuń",
+ "ACL_I": "Wstaw wiadomości",
+ "ACL_K": "Utwórz podfoldery",
+ "ACL_L": "Pokaż wiadomości",
+ "ACL_P": "Wyślij mail",
+ "ACL_R": "Wybierz folder",
+ "ACL_S": "\\Tag przeczytane",
+ "ACL_T": "\\Tag usunięte",
+ "ACL_W": "Inne tagi",
+ "ACL_X": "Usuń\/Zmień nazwę folder(-u)"
},
"MESSAGE_LIST": {
"BUTTON_RELOAD": "Odśwież listę wiadomości",
@@ -219,9 +219,9 @@
"DISPLAY_NAME": "Nazwa wyświetlana",
"LAST_NAME": "Nazwisko",
"FIRST_NAME": "Imię",
- "MIDDLE_NAME": "Middle name",
- "NAME_PREFIX": "Name prefix",
- "NAME_SUFFIX": "Name suffix",
+ "MIDDLE_NAME": "Drugie imię",
+ "NAME_PREFIX": "Przedrostek imienia",
+ "NAME_SUFFIX": "Przyrostek imienia",
"NICK_NAME": "Pseudonim",
"LABEL_READ_ONLY": "Tylko do odczytu",
"ADD_MENU_LABEL": "Dodaj",
@@ -537,7 +537,7 @@
"TO_MANY_FOLDERS_DESC_2": "Pokazaliśmy tylko część z nich aby uniknąć problemów z wydajnością.",
"HELP_DELETE_FOLDER": "Usuń folder",
"HELP_SHOW_HIDE_FOLDER": "Pokaż\/ukryj folder",
- "HELP_CHECK_FOR_NEW_MESSAGES": "Sprawdzaj nowych wiadomości",
+ "HELP_CHECK_FOR_NEW_MESSAGES": "Sprawdź czy są nowe wiadomości",
"HIDE_UNSUBSCRIBED": "Ukryj niesubskrybowane foldery",
"UNHIDE_KOLAB_FOLDERS": "Pokaż foldery Kolab",
"TYPE_CALENDAR": "Kalendarz",
From 3f8f17a62e4ca0cbb6daf24907e538b3e3c3209e Mon Sep 17 00:00:00 2001
From: tinola
Date: Wed, 2 Oct 2024 07:33:07 +0200
Subject: [PATCH 12/45] Update admin.json
update Polish translation (admin page)
---
snappymail/v/0.0.0/app/localization/pl/admin.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/snappymail/v/0.0.0/app/localization/pl/admin.json b/snappymail/v/0.0.0/app/localization/pl/admin.json
index 6066b3575..855374b15 100644
--- a/snappymail/v/0.0.0/app/localization/pl/admin.json
+++ b/snappymail/v/0.0.0/app/localization/pl/admin.json
@@ -136,11 +136,11 @@
"SECURE_OPTION_NONE": "Bez zabezpieczeń",
"LABEL_ALLOW_SIEVE_SCRIPTS": "Zezwól na skrypty sieve",
"LABEL_USE_SHORT_LOGIN": "Użyj krótkiego loginu",
- "LABEL_LOWERCASE_LOGIN": "Lowercase login (case-insensitive)",
+ "LABEL_LOWERCASE_LOGIN": "Logowanie małymi literami (bez uwzględniania ich wielkości)",
"LABEL_USE_AUTH": "Użyj uwierzytelnienia",
"LABEL_SET_SENDER": "Użyj loginu jako nadawcy",
"LABEL_AUTH_PLAIN_LINE": "Wymuś logowanie pojedynczą komendą AUTH PLAIN",
- "LABEL_USE_PHP_MAIL": "Użyj funkcji php 'mail()'",
+ "LABEL_USE_PHP_MAIL": "Użyj funkcji PHP 'mail()'",
"BUTTON_WHITE_LIST": "Biała lista",
"NEW_DOMAIN_DESC": "Konfiguracja tej domeny pozwala na pracę z adresami e-mail: %NAME%<\/strong>",
"WHITE_LIST_ALERT": "Lista użytkowników domeny, którzy mogą uzyskać dostęp z tego webmaila.\nUżyj spacji do rozdzielenia.\n",
From 115f8c8374caefa10fde38d4bfeda0aa030e9ca2 Mon Sep 17 00:00:00 2001
From: Patrick Niebeling
Date: Wed, 2 Oct 2024 14:32:03 +0200
Subject: [PATCH 13/45] In Order to
https://github.com/nextcloud/server/pull/45819 this change will adjust
boarders to the new smaller boarder radius.
---
.../v/0.0.0/themes/NextcloudV25+/styles.css | 44 +++++++++----------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
index 6d4cceeb7..98622007a 100644
--- a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
+++ b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
@@ -52,8 +52,8 @@
--nc-animation-quick: var(--animation-quick, 100ms);
--nc-animation-slow: var(--animation-slow, 300ms);
--nc-border-radius: var(--border-radius, 3px);
- --nc-border-radius-large: var(--border-radius-large, 10px);
- --nc-border-radius-pill: var(--border-radius-pill, 100px);
+ --nc-border-radius-large: var(--border-radius-large, 8px);
+ --nc-border-radius-pill: var("--border-radius-pill", 100px);
--nc-default-grid-baseline: var(--default-grid-baseline, 4px);
--nc-navigation-width: var(--navigation-width, 300px);
--nc-sidebar-min-width: var(--sidebar-min-width, 300px);
@@ -116,7 +116,7 @@
--nc-animation-quick: var(--animation-quick, 100ms);
--nc-animation-slow: var(--animation-slow, 300ms);
--nc-border-radius: var(--border-radius, 3px);
- --nc-border-radius-large: var(--border-radius-large, 10px);
+ --nc-border-radius-large: var(--border-radius-large, 8px);
--nc-border-radius-pill: var(--border-radius-pill, 100px);
--nc-default-grid-baseline: var(--default-grid-baseline, 4px);
--nc-navigation-width: var(--navigation-width, 300px);
@@ -353,7 +353,7 @@
color: var(--nc-color-main-text);
background-color: var(--nc-color-background-dark);
border: 1px solid var(--nc-color-border-dark) !important;
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
font-size: var(--nc-default-font-size);
text-shadow: none;
box-shadow: none;
@@ -529,42 +529,42 @@
#rl-app .btn-group .btn:nth-child(1 of :not([style*="display: none;"])),
[dir="rtl"] #rl-app .btn-group .btn:nth-last-child(1 of :not([style*="display: none;"])) {
border-radius: 0 !important;
- border-top-left-radius: var(--nc-border-radius-pill) !important;
- border-bottom-left-radius: var(--nc-border-radius-pill) !important;
+ border-top-left-radius: var(--nc-border-radius-large) !important;
+ border-bottom-left-radius: var(--nc-border-radius-large) !important;
}
/* fallback */
#rl-app .btn-group .btn:first-of-type,
[dir="rtl"] #rl-app .btn-group .btn:last-of-type {
border-radius: 0;
- border-top-left-radius: var(--nc-border-radius-pill);
- border-bottom-left-radius: var(--nc-border-radius-pill);
+ border-top-left-radius: var(--nc-border-radius-large);
+ border-bottom-left-radius: var(--nc-border-radius-large);
}
#rl-app .btn-group .btn:nth-last-child(1 of :not([style*="display: none;"])),
[dir="rtl"] #rl-app .btn-group .btn:nth-child(1 of :not([style*="display: none;"])) {
border-radius: 0 !important;
- border-top-right-radius: var(--nc-border-radius-pill) !important;
- border-bottom-right-radius: var(--nc-border-radius-pill) !important;
+ border-top-right-radius: var(--nc-border-radius-large) !important;
+ border-bottom-right-radius: var(--nc-border-radius-large) !important;
}
/* fallback */
#rl-app .btn-group .btn:last-of-type,
[dir="rtl"] #rl-app .btn-group .btn:first-of-type {
border-radius: 0;
- border-top-right-radius: var(--nc-border-radius-pill);
- border-bottom-right-radius: var(--nc-border-radius-pill);
+ border-top-right-radius: var(--nc-border-radius-large);
+ border-bottom-right-radius: var(--nc-border-radius-large);
}
#rl-app .btn-group .btn:nth-child(1 of :not([style*="display: none;"])):nth-last-child(1 of :not([style*="display: none;"])),
[dir="rtl"] #rl-app .btn-group .btn:nth-child(1 of :not([style*="display: none;"])):nth-last-child(1 of :not([style*="display: none;"])) {
- border-radius: var(--nc-border-radius-pill) !important;
+ border-radius: var(--nc-border-radius-large) !important;
}
/* fallback */
#rl-app .btn-group .btn:first-of-type:last-of-type,
[dir="rtl"] #rl-app .btn-group .btn:first-of-type:last-of-type {
- border-radius: var(--nc-border-radius-pill) !important;
+ border-radius: var(--nc-border-radius-large) !important;
}
#rl-app .btn-group .btn.fontastic:nth-child(1 of :not([style*="display: none;"])),
@@ -1338,7 +1338,7 @@ html.rl-left-panel-disabled #rl-app .b-folders .b-toolbar {
padding: 0 2em 0 15px;
height: 38px;
line-height: 38px !important;
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
color: unset;
border: none;
}
@@ -1390,7 +1390,7 @@ html.rl-left-panel-disabled #rl-app .b-folders .b-folders-system a.selectable {
#rl-app .b-folders hr {
border-top: solid var(--nc-color-main-text);
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
opacity: .1;
}
@@ -1441,7 +1441,7 @@ html.rl-left-panel-disabled #rl-app .b-folders .b-folders-system a.selectable {
#rl-app .b-footer.btn-toolbar .btn-group .btn.fontastic:nth-child(1 of :not([style*="display: none;"])):nth-last-child(1 of :not([style*="display: none;"])),
[dir="rtl"] #rl-app .b-footer.btn-toolbar .btn-group .btn:nth-child(1 of :not([style*="display: none;"])):nth-last-child(1 of :not([style*="display: none;"])),
[dir="rtl"] #rl-app .b-footer.btn-toolbar .btn-group .btn.fontastic:nth-child(1 of :not([style*="display: none;"])):nth-last-child(1 of :not([style*="display: none;"])) {
- border-radius: var(--nc-border-radius-pill) !important;
+ border-radius: var(--nc-border-radius-large) !important;
}
/* fallback */
@@ -1449,7 +1449,7 @@ html.rl-left-panel-disabled #rl-app .b-folders .b-folders-system a.selectable {
#rl-app .b-footer.btn-toolbar .btn-group .btn.fontastic,
[dir="rtl"] #rl-app .b-footer.btn-toolbar .btn-group .btn,
[dir="rtl"] #rl-app .b-footer.btn-toolbar .btn-group .btn.fontastic {
- border-radius: var(--nc-border-radius-pill) !important;
+ border-radius: var(--nc-border-radius-large) !important;
}
#rl-app .b-footer.btn-toolbar .btn:hover,
@@ -1826,17 +1826,17 @@ html.sm-msgView-bottom #rl-app .messageView {
#rl-app .messageView .messageItemHeader .informationShort meter::-webkit-meter-optimum-value {
background-color: var(--nc-color-success);
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
}
#rl-app .messageView .messageItemHeader .informationShort meter::-webkit-meter-suboptimum-value {
background-color: var(--nc-color-warning);
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
}
#rl-app .messageView .messageItemHeader .informationShort meter::-webkit-meter-even-less-good-value {
background-color: var(--nc-color-error);
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
}
#rl-app .messageView .messageItemHeader .hasVirus {
@@ -1876,7 +1876,7 @@ html.sm-msgView-bottom #rl-app .messageView {
}
#rl-app .messageView .messageAssignedTags span {
- border-radius: var(--nc-border-radius-pill);
+ border-radius: var(--nc-border-radius-large);
background: unset;
padding: 2px 5px;
}
From 907a9cefece99037ec6cf669621646b2aa90e891 Mon Sep 17 00:00:00 2001
From: Patrick Niebeling
Date: Thu, 3 Oct 2024 11:34:55 +0200
Subject: [PATCH 14/45] Remove ""
Signed-off-by: Patrick Niebeling
---
snappymail/v/0.0.0/themes/NextcloudV25+/styles.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
index 98622007a..f50ea822d 100644
--- a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
+++ b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
@@ -53,7 +53,7 @@
--nc-animation-slow: var(--animation-slow, 300ms);
--nc-border-radius: var(--border-radius, 3px);
--nc-border-radius-large: var(--border-radius-large, 8px);
- --nc-border-radius-pill: var("--border-radius-pill", 100px);
+ --nc-border-radius-pill: var(--border-radius-pill, 100px);
--nc-default-grid-baseline: var(--default-grid-baseline, 4px);
--nc-navigation-width: var(--navigation-width, 300px);
--nc-sidebar-min-width: var(--sidebar-min-width, 300px);
From d6ef5ff70ab68704a5da3115ee90b8cd87ca84d6 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Sun, 6 Oct 2024 17:32:47 +0200
Subject: [PATCH 15/45] Squire paste image working again.
https://github.com/the-djmaze/snappymail/issues/1389#issuecomment-2389678680
---
dev/Common/Html.js | 116 +++++++++++++++--------------
dev/External/SquireUI.js | 38 ++++++++++
vendors/squire2/dist/squire-raw.js | 37 ++++-----
3 files changed, 112 insertions(+), 79 deletions(-)
diff --git a/dev/Common/Html.js b/dev/Common/Html.js
index 0e8029fbd..0be300ec9 100644
--- a/dev/Common/Html.js
+++ b/dev/Common/Html.js
@@ -360,7 +360,7 @@ export const
});
*/
- isMsg && [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
+ [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
@@ -485,71 +485,73 @@ export const
*/
let skipStyle = false;
- value = delAttribute('src');
- if (value) {
- if ('IMG' === name) {
- oElement.loading = 'lazy';
- let attachment;
- if (value.startsWith('cid:'))
- {
- value = value.slice(4);
- setAttribute('data-x-src-cid', value);
- attachment = findAttachmentByCid(value);
- if (attachment?.download) {
- oElement.src = attachment.linkPreview();
- oElement.title += ' ('+attachment.fileName+')';
- attachment.isInline(true);
- attachment.isLinked(true);
+ if (isMsg) {
+ value = isMsg && delAttribute('src');
+ if (value) {
+ if ('IMG' === name) {
+ oElement.loading = 'lazy';
+ let attachment;
+ if (value.startsWith('cid:'))
+ {
+ value = value.slice(4);
+ setAttribute('data-x-src-cid', value);
+ attachment = findAttachmentByCid(value);
+ if (attachment?.download) {
+ oElement.src = attachment.linkPreview();
+ oElement.title += ' ('+attachment.fileName+')';
+ attachment.isInline(true);
+ attachment.isLinked(true);
+ }
}
- }
- else if ((attachment = findLocationByCid(value)))
- {
- if (attachment.download) {
- oElement.src = attachment.linkPreview();
- attachment.isLinked(true);
+ else if ((attachment = findLocationByCid(value)))
+ {
+ if (attachment.download) {
+ oElement.src = attachment.linkPreview();
+ attachment.isLinked(true);
+ }
}
- }
- else if (detectHiddenImages
- && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
- || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
- || (oStyle.width && 2 > pInt(oStyle.width))
- || [
- 'email.microsoftemail.com/open',
- 'github.com/notifications/beacon/',
- '/track/open', // mandrillapp.com list-manage.com
- 'google-analytics.com'
- ].filter(uri => value.toLowerCase().includes(uri)).length
- )) {
- skipStyle = true;
- oStyle.display = 'none';
-// setAttribute('style', 'display:none');
- setAttribute('data-x-src-hidden', value);
-// result.tracking = true;
- }
- else if (httpre.test(value))
- {
- let src = stripTracking(value);
- if (src != value) {
- result.tracking = true;
- setAttribute('data-x-src-tracking', value);
+ else if (detectHiddenImages
+ && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
+ || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
+ || (oStyle.width && 2 > pInt(oStyle.width))
+ || [
+ 'email.microsoftemail.com/open',
+ 'github.com/notifications/beacon/',
+ '/track/open', // mandrillapp.com list-manage.com
+ 'google-analytics.com'
+ ].filter(uri => value.toLowerCase().includes(uri)).length
+ )) {
+ skipStyle = true;
+ oStyle.display = 'none';
+ // setAttribute('style', 'display:none');
+ setAttribute('data-x-src-hidden', value);
+ // result.tracking = true;
+ }
+ else if (httpre.test(value))
+ {
+ let src = stripTracking(value);
+ if (src != value) {
+ result.tracking = true;
+ setAttribute('data-x-src-tracking', value);
+ }
+ setAttribute('data-x-src', src);
+ result.hasExternals = true;
+ oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
+ }
+ else if (value.startsWith('data:image/'))
+ {
+ oElement.src = value;
+ }
+ else
+ {
+ setAttribute('data-x-src-broken', value);
}
- setAttribute('data-x-src', src);
- result.hasExternals = true;
- oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
- }
- else if (value.startsWith('data:image/'))
- {
- oElement.src = value;
}
else
{
setAttribute('data-x-src-broken', value);
}
}
- else
- {
- setAttribute('data-x-src-broken', value);
- }
}
if (hasAttribute('background')) {
diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js
index fe07975d2..b1e50fcad 100644
--- a/dev/External/SquireUI.js
+++ b/dev/External/SquireUI.js
@@ -386,6 +386,44 @@ class SquireUI
changes.redo.input.disabled = !e.detail.canRedo;
});
+ squire.addEventListener('pasteImage', e => {
+ const items = e.detail.clipboardData.items;
+ let l = items.length;
+ while (l--) {
+ const item = items[l];
+ if (/^image\/(png|jpeg|webp)/.test(item.type)) {
+ let reader = new FileReader();
+ reader.onload = event => {
+ let img = createElement("img"),
+ canvas = createElement("canvas"),
+ ctx = canvas.getContext('2d');
+ img.onload = ()=>{
+ ctx.drawImage(img, 0, 0);
+ let width = img.width, height = img.height;
+ if (width > height) {
+ // Landscape
+ if (width > 1024) {
+ height = height * 1024 / width;
+ width = 1024;
+ }
+ } else if (height > 1024) {
+ // Portrait
+ width = width * 1024 / height;
+ height = 1024;
+ }
+ canvas.width = width;
+ canvas.height = height;
+ ctx.drawImage(img, 0, 0, width, height);
+ squire.insertHTML(' ', true);
+ };
+ img.src = event.target.result;
+ }
+ reader.readAsDataURL(item.getAsFile());
+ break;
+ }
+ }
+ });
+
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
// squire.addEventListener('focus', () => shortcuts.off());
diff --git a/vendors/squire2/dist/squire-raw.js b/vendors/squire2/dist/squire-raw.js
index 4367a29dd..d69f0abe5 100644
--- a/vendors/squire2/dist/squire-raw.js
+++ b/vendors/squire2/dist/squire-raw.js
@@ -58,7 +58,7 @@
var notWS = /[^ \t\r\n]/;
// source/node/Category.ts
- var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/;
+ var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/;
var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IMG"]);
var UNKNOWN = 0;
var INLINE = 1;
@@ -198,7 +198,7 @@
// okay if data is 'undefined' here.
notWS.test(node.data)
);
- var isLineBreak = (br, isLBIfEmptyBlock) => {
+ var isLineBreak = (br) => {
let block = br.parentNode;
while (isInline(block)) {
block = block.parentNode;
@@ -209,7 +209,7 @@
notWSTextNode
);
walker.currentNode = br;
- return !!walker.nextNode() || isLBIfEmptyBlock && !walker.previousNode();
+ return !!walker.nextNode();
};
var removeZWS = (root, keepNode) => {
const walker = createTreeWalker(root, SHOW_TEXT);
@@ -267,7 +267,7 @@
textChild = prev;
}
startContainer = textChild;
- startOffset = textChild.data.length;
+ startOffset = textChild.length;
}
}
break;
@@ -279,7 +279,7 @@
while (!(endContainer instanceof Text)) {
const child = endContainer.childNodes[endOffset - 1];
if (!child || isLeaf(child)) {
- if (child && child.nodeName === "BR" && !isLineBreak(child, false)) {
+ if (child && child.nodeName === "BR" && !isLineBreak(child)) {
--endOffset;
continue;
}
@@ -323,7 +323,7 @@
if (endContainer === endMax || endContainer === root) {
break;
}
- if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset], false)) {
+ if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset])) {
++endOffset;
}
if (endOffset !== getLength(endContainer)) {
@@ -529,30 +529,23 @@
if (isInline(child) && !child.firstChild) {
node.removeChild(child);
}
- } else if (child instanceof Text && !child.data) {
+ } else if (child instanceof Text && !child.length) {
node.removeChild(child);
}
}
};
var cleanupBRs = (node) => {
- const brs = node.querySelectorAll("BR");
- const brBreaksLine = [];
+ const brs = node.querySelectorAll("BR:last-child");
let l = brs.length;
- for (let i = 0; i < l; ++i) {
- brBreaksLine[i] = isLineBreak(brs[i], false);
- }
while (l--) {
const br = brs[l];
- const parent = br.parentNode;
- if (parent) {
- if (!brBreaksLine[l]) {
- detach(br);
- }
+ if (!isLineBreak(br)) {
+ br.remove();
}
}
};
var escapeHTML = (text) => {
- return text.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");
+ return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
};
// source/node/MergeSplit.ts
@@ -980,7 +973,7 @@
const iterator = createTreeWalker(root, SHOW_ELEMENT_OR_TEXT);
let afterNode = startContainer;
let afterOffset = startOffset;
- if (!(afterNode instanceof Text) || afterOffset === afterNode.data.length) {
+ if (!(afterNode instanceof Text) || afterOffset === afterNode.length) {
afterNode = getAdjacentInlineNode(iterator, "nextNode", afterNode);
afterOffset = 0;
}
@@ -993,7 +986,7 @@
afterNode || (startContainer instanceof Text ? startContainer : startContainer.childNodes[startOffset] || startContainer)
);
if (beforeNode instanceof Text) {
- beforeOffset = beforeNode.data.length;
+ beforeOffset = beforeNode.length;
}
}
let node = null;
@@ -3171,7 +3164,7 @@
nodeAfterSplit = child;
break;
}
- while (child && child instanceof Text && !child.data) {
+ while (child && child instanceof Text && !child.length) {
next = child.nextSibling;
if (!next || next.nodeName === "BR") {
break;
@@ -3517,7 +3510,7 @@
const brBreaksLine = [];
let l = nodes.length;
for (let i = 0; i < l; ++i) {
- brBreaksLine[i] = isLineBreak(nodes[i], false);
+ brBreaksLine[i] = isLineBreak(nodes[i]);
}
while (l--) {
const br = nodes[l];
From 03d99f19c9737b20a04e71236f7cf275feba9cd7 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Sun, 6 Oct 2024 23:46:36 +0200
Subject: [PATCH 16/45] Resolve #1765
---
snappymail/v/0.0.0/themes/NextcloudV25+/styles.css | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
index f50ea822d..6da7601f8 100644
--- a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
+++ b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
@@ -1534,7 +1534,8 @@ html.rl-left-panel-disabled[dir="rtl"] #rl-app #V-MailFolderList .b-footer.btn-t
#rl-app .messageView {
height: calc(100% - 65px);
margin: 0 5px;
- border: none;
+ border: 0;
+ border-top: 1px solid transparent;
box-shadow: none !important;
background: var(--nc-color-main-background);
}
From ef724b4b4afb0e1fc493ebc64ea51950a36a5d32 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Sun, 6 Oct 2024 23:46:53 +0200
Subject: [PATCH 17/45] Resolve #1775
---
snappymail/v/0.0.0/themes/NextcloudV25+/styles.css | 4 ----
1 file changed, 4 deletions(-)
diff --git a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
index 6da7601f8..6a307ebc9 100644
--- a/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
+++ b/snappymail/v/0.0.0/themes/NextcloudV25+/styles.css
@@ -1158,10 +1158,6 @@ html.rl-mobile.rl-left-panel-disabled #rl-app #rl-left {
color: var(--nc-color-primary-text);
}
-:not(#rl-app) ol, ul {
- margin: unset;
-}
-
#rl-app .control-group {
margin-bottom: 0.5em;
}
From ba85eda7f0bf7481f19874fd3d803dd58433ff84 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Mon, 7 Oct 2024 00:03:35 +0200
Subject: [PATCH 18/45] Remove #1737
---
plugins/nextcloud/index.php | 1 -
plugins/nextcloud/js/message.js | 467 +-----------------
plugins/nextcloud/js/webdav.js | 52 +-
plugins/nextcloud/langs/en.json | 14 +-
.../templates/PopupsNextcloudInvites.html | 8 -
5 files changed, 12 insertions(+), 530 deletions(-)
delete mode 100644 plugins/nextcloud/templates/PopupsNextcloudInvites.html
diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php
index 1f9f8b5e6..13256a3c1 100644
--- a/plugins/nextcloud/index.php
+++ b/plugins/nextcloud/index.php
@@ -35,7 +35,6 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addTemplate('templates/PopupsNextcloudFiles.html');
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
- $this->addTemplate('templates/PopupsNextcloudInvites.html');
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
// $this->addHook('login.credentials', 'loginCredentials');
diff --git a/plugins/nextcloud/js/message.js b/plugins/nextcloud/js/message.js
index bab6793a4..f5154b745 100644
--- a/plugins/nextcloud/js/message.js
+++ b/plugins/nextcloud/js/message.js
@@ -21,18 +21,9 @@
// https://github.com/nextcloud/calendar/issues/4684
if (cfg.CalDAV) {
- attachmentsControls.append(Element.fromHTML(`
+ attachmentsControls.append(Element.fromHTML(`
`));
- attachmentsControls.append(Element.fromHTML(`
-
- `))
- attachmentsControls.append(Element.fromHTML(`
-
- `))
- attachmentsControls.append(Element.fromHTML(`
-
- `))
}
}
/*
@@ -116,169 +107,18 @@
};
view.nextcloudICS = ko.observable(null);
- view.nextcloudICSOldInvitation = ko.observable(null);
- view.nextcloudICSNewInvitation = ko.observable(null);
- view.nextcloudICSLastInvitation = ko.observable(null);
- view.nextcloudICSShow = ko.observable(null);
-
- view.nextcloudICSCalendar = ko.observable(null);
- view.filteredEventsUrls = ko.observable([]);
-
- view.nextcloudSaveICS = async () => {
+ view.nextcloudSaveICS = () => {
let VEVENT = view.nextcloudICS();
- VEVENT = await view.handleUpdatedRecurrentEvents(VEVENT)
- VEVENT && rl.nextcloud.selectCalendar(VEVENT)
+ VEVENT && rl.nextcloud.selectCalendar()
+ .then(href => href && rl.nextcloud.calendarPut(href, VEVENT));
}
-
- view.handleUpdatedRecurrentEvents = async (VEVENT) => {
-
- const uid = VEVENT.UID
-
- let makePUTRequest = false
-
- const filteredEventUrls = view.filteredEventUrls
-
- if (filteredEventUrls.length > 1) {
- // console.warn('filteredEventUrls.length > 1')
- }
- else if (filteredEventUrls.length == 1) {
- const eventUrl = filteredEventUrls[0]
- const eventText = await fetchEvent(eventUrl)
-
- // don't do anything for equal cards
- if (VEVENT.rawText == eventText) {
- return VEVENT
- }
-
- const newVeventsText = extractVEVENTs(VEVENT.rawText)
- const oldVeventsText = extractVEVENTs(eventText)
-
- // if there's only one event in the old card, it can't be an edit of the exception card
- if (oldVeventsText.length == 1) {
- const newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventsText[0])
-
- // if there's a property RECURRENCE-ID in the new card, it's an recurrence exception event
- if (newRecurrenceId.length > 0) {
- const updatedVEVENT = {
- 'SUMMARY' : VEVENT.SUMMARY,
- 'UID' : VEVENT.UID,
- 'rawText' : mergeEventTexts(eventText, newVeventsText[0])
- }
- VEVENT = updatedVEVENT
- makePUTRequest = true
- }
- else {
- return VEVENT
- }
- }
- // if there's more than one event in the old card, it's possible to be an inclusion of a new exception card
- // or update of old exception card
- else {
- let recurrenceIdMatch = false
-
- let isUpdate = false
- const updateData = {
- 'oldEventIndex': null,
- 'newEventIndex': null,
- }
-
- // check if it's an update
- for (let i = 0; i < oldVeventsText.length; i++) {
- let oldVeventText = oldVeventsText[i]
-
- for (let j = 0; j < newVeventsText.length; j++) {
- let newVeventText = newVeventsText[j]
-
- let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText)
- let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText)
- let oldSequence = extractProperties('SEQUENCE', oldVeventText)
- let newSequence = extractProperties('SEQUENCE', newVeventText)
-
- if (oldRecurrenceId.length == 0 || newRecurrenceId.length == 0 || oldSequence.length == 0 || newSequence.length == 0) {
- continue
- }
-
- if (oldRecurrenceId[0] == newRecurrenceId[0]) {
- if (newSequence[0] > oldSequence[0]) {
- isUpdate = true
-
- updateData.oldEventIndex = i
- updateData.newEventIndex = j
-
- i = oldVeventsText.length
- j = newVeventsText.length
- }
- }
- }
- }
-
- // if it's an update...
- if (isUpdate) {
- // substitute old event text for new event text
- const oldEventStart = eventText.indexOf(oldVeventsText[updateData.oldEventIndex])
- const oldEventEnd = oldEventStart + oldVeventsText[updateData.oldEventIndex].length
-
- const newEvent = eventText.substring(0, oldEventStart) + newVeventsText[updateData.newEventIndex] + eventText.substring(oldEventEnd)
-
- const updatedVEVENT = {
- 'SUMMARY' : VEVENT.SUMMARY,
- 'UID': VEVENT.UID,
- 'rawText' : newEvent
- }
- VEVENT = updatedVEVENT
- makePUTRequest = true
- }
- // if it's not an update, it's an inclusion, as there's no match of RECURRENCE-ID
- else {
- const updatedVEVENT = {
- 'SUMMARY' : VEVENT.SUMMARY,
- 'UID' : VEVENT.UID,
- 'rawText' : mergeEventTexts(eventText, newVeventsText[0])
- }
- VEVENT = updatedVEVENT
- makePUTRequest = true
- }
- }
- }
-
- if (makePUTRequest) {
- let href = "/" + (filteredEventUrls[0].split('/').slice(5, -1)[0])
-
- rl.nextcloud.calendarPut(href, VEVENT, (response) => {
- if (response.status != 201 && response.status != 204) {
- InvitesPopupView.showModal([
- rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_TITLE'),
- rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_BODY', {eventName: VEVENT.SUMMARY})
- ])
- return
- }
- InvitesPopupView.showModal([
- rl.i18n('NEXTCLOUD/EVENT_UPDATED_TITLE'),
- rl.i18n('NEXTCLOUD/EVENT_UPDATED_BODY', {eventName: VEVENT.SUMMARY})
- ])
- })
-
- return null
- }
-
- return VEVENT
- }
-
-
-
/**
* TODO
*/
view.message.subscribe(msg => {
view.nextcloudICS(null);
- view.nextcloudICSOldInvitation(null);
- view.nextcloudICSNewInvitation(null);
- view.nextcloudICSLastInvitation(null);
- view.nextcloudICSShow(view.nextcloudICS())
-
-
if (msg && cfg.CalDAV) {
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
@@ -286,7 +126,7 @@
// fetch it and parse the VEVENT
rl.fetch(ics.linkDownload())
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
- .then(async (text) => {
+ .then(text => {
let VEVENT,
VALARM,
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
@@ -337,158 +177,6 @@
shouldReply: VEVENT.shouldReply()
});
view.nextcloudICS(VEVENT);
- view.nextcloudICSShow(true);
-
-
- // try to get calendars, save
- const calendarUrls = await fetchCalendarUrls()
-
- const filteredEventUrls = []
- for (let i = 0; i < calendarUrls.length; i++) {
- let calendarUrl = calendarUrls[i]
-
- const skipCalendars = ['/inbox/', '/outbox/', '/trashbin/']
- let skip = false
- for (let j = 0; j < skipCalendars.length; j++) {
- if (calendarUrl.includes(skipCalendars[j])) {
- skip = true
- break
- }
- }
- if (skip) {
- continue
- }
-
- // try to get event
- const eventUrls = await fetchEventUrl(calendarUrl, VEVENT.UID)
-
- if (eventUrls.length == 0) {
- continue
- }
-
- eventUrls.forEach((url) => {
- filteredEventUrls.push(url)
- })
- }
- view.filteredEventUrls = filteredEventUrls
-
- // if there's none, save in view.nextcloudICS
- if (filteredEventUrls.length == 0) {
- view.nextcloudICS(VEVENT);
- view.nextcloudICSShow(true)
- }
- // if there's some...
- else {
- const savedEvent = await fetchEvent(filteredEventUrls[0])
-
- const newVeventsText = extractVEVENTs(VEVENT.rawText)
- const oldVeventsText = extractVEVENTs(savedEvent)
-
- // if there's more than one event in the old card, it's possible to be inclusion of new exception card
- // or updated of old exception card
- if (oldVeventsText.length > 1) {
- let recurrenceIdMatch = false
-
- let oldNewestCreated = null
- let newNewestCreated = null
-
- // check if it's an update
- for (let i = 0; i < oldVeventsText.length; i++) {
- let oldVeventText = oldVeventsText[i]
-
- for (let j = 0; j < newVeventsText.length; j++) {
- let newVeventText = newVeventsText[j]
-
- let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText)
- let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText)
- let oldSequence = extractProperties('SEQUENCE', oldVeventText)
- let newSequence = extractProperties('SEQUENCE', newVeventText)
-
- if (newRecurrenceId.length == 0 && oldRecurrenceId.length == 1) {
- view.nextcloudICSShow(false)
- view.nextcloudICSOldInvitation(true)
- }
-
- if (oldRecurrenceId.length > 0 && newRecurrenceId.length > 0 && oldSequence.length > 0 && newSequence.length > 0) {
- if (oldRecurrenceId[0] == newRecurrenceId[0]) {
- if (newSequence[0] < oldSequence[0]) {
- view.nextcloudICSOldInvitation(true)
- view.nextcloudICSShow(false)
- }
- else if (newSequence[0] == oldSequence[0]) {
- view.nextcloudICSLastInvitation(true)
- view.nextcloudICSShow(false)
- }
- else if (newSequence[0] > oldSequence[0]) {
- view.nextcloudICSNewInvitation(true)
- view.nextcloudICSShow(false)
- }
-
- // exit for loops
- j = newVeventsText.length
- i = oldVeventsText.length
- }
- }
-
- let oldCreated = extractProperties('CREATED', oldVeventText)
- let newCreated = extractProperties('CREATED', newVeventText)
-
- if (oldCreated.length == 0 || newCreated.length == 0) {
- continue
- }
-
- const formattedOldDate = oldCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
- const formattedNewDate = newCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
-
- const oldDate = new Date(formattedOldDate)
- const newDate = new Date(formattedNewDate)
-
- if (oldNewestCreated == null || oldDate > oldNewestCreated) {
- oldNewestCreated = oldDate
- }
- if (newNewestCreated == null || newDate > newNewestCreated) {
- newNewestCreated = newDate
- }
- }
- }
-
- if (newNewestCreated != null && oldNewestCreated != null) {
- if (newNewestCreated < oldNewestCreated) {
- view.nextcloudICSOldInvitation(true)
- view.nextcloudICSShow(false)
- }
- else if (newNewestCreated == oldNewestCreated) {
- view.nextcloudICSLastInvitation(true)
- view.nextcloudICSShow(false)
- }
- }
- }
- else {
- const oldLastModified = extractProperties('LAST-MODIFIED', oldVeventsText[0])
- const newLastModified = extractProperties('LAST-MODIFIED', newVeventsText[0])
-
- if (oldLastModified.length == 1 && newLastModified.length == 1) {
- const formattedOldDate = oldLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
- const formattedNewDate = newLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
-
- const oldDate = new Date(formattedOldDate)
- const newDate = new Date(formattedNewDate)
-
- if (newDate > oldDate) {
- view.nextcloudICSNewInvitation(true)
- view.nextcloudICSShow(false)
- }
- else if (newDate < oldDate) {
- view.nextcloudICSOldInvitation(true)
- view.nextcloudICSShow(false)
- }
- else {
- view.nextcloudICSLastInvitation(true)
- view.nextcloudICSShow(false)
- }
- }
- }
- }
}
});
}
@@ -498,148 +186,3 @@
});
})(window.rl);
-
-
-async function fetchCalendarUrls () {
- const username = OC.currentUser
- const requestToken = OC.requestToken
-
- const url = '/remote.php/dav/calendars/' + username
- const response = await fetch(url, {
- 'method': 'PROPFIND',
- 'headers': {
- 'Depth': '1',
- 'Content-Type': 'application/xml',
- 'requesttoken' : requestToken
- }
- })
-
- if (!response.ok) {
- throw new Error('Error fetching calendars', response)
- }
-
- const responseText = await response.text()
-
- const parser = new DOMParser()
- const xmlDoc = parser.parseFromString(responseText, 'application/xml')
-
- const calendarUrls = []
-
- const hrefElements = xmlDoc.getElementsByTagName('d:href')
- for (let i = 1; i < hrefElements.length; i++) {
- let calendarUrl = hrefElements[i].textContent.trim()
- calendarUrls.push(calendarUrl)
- }
-
- return calendarUrls
-}
-
-
-async function fetchEventUrl (calendarUrl, uid) {
- const requestToken = OC.requestToken
-
- const xmlRequestBody = `
-
-
-
-
-
-
-
-
- ${uid}
-
-
-
-
-`
-
- const response = await fetch(calendarUrl, {
- 'method': 'REPORT',
- 'headers': {
- 'Content-Type': 'application/xml',
- 'Depth': 1,
- 'requesttoken': requestToken
- },
- 'body': xmlRequestBody
- })
-
- const responseText = await response.text()
-
- const parser = new DOMParser()
- const xmlDoc = parser.parseFromString(responseText, 'application/xml')
-
- const hrefElements = xmlDoc.getElementsByTagName('d:href')
- const hrefValues = Array.from(hrefElements).map(element => element.textContent)
-
- return hrefValues
-}
-
-
-async function fetchEvent (eventUrl) {
- const requestToken = OC.requestToken
- const response = await fetch(eventUrl, {
- 'method': 'GET',
- 'headers': {
- 'requesttoken': requestToken
- }
- })
- const responseText = await response.text()
-
- return responseText
-}
-
-
-function extractVEVENTs(text) {
- let lastEndIndex = 0
- const vEvents = []
-
- let textToSearch = ""
- let beginIndex = 0
- let endIndex = 0
- let foundVevent = false
-
- while (true) {
- textToSearch = text.substring(lastEndIndex)
-
- beginIndex = textToSearch.indexOf('BEGIN:VEVENT')
- if (beginIndex == -1) {
- break
- }
- endIndex = textToSearch.substring(beginIndex).indexOf('END:VEVENT')
- if (endIndex == -1) {
- break
- }
- endIndex += beginIndex
-
- lastEndIndex = lastEndIndex + endIndex + 'END:VEVENT'.length
-
- foundVevent = textToSearch.substring(beginIndex + 'BEGIN:VEVENT'.length, endIndex)
-
- vEvents.push(foundVevent)
- }
-
- return vEvents
-}
-
-
-function extractProperties (property, text) {
- const matches = text.match(`${property}.*`)
-
- if (matches == null) {
- return []
- }
-
- const separatedMatches = matches.map((match) => {
- return match.substring(property.length + 1)
- })
-
- return separatedMatches
-}
-
-
-function mergeEventTexts (oldEventText, newEventText) {
- const appendIndex = oldEventText.indexOf('END:VEVENT') + 'END:VEVENT'.length
- const updatedEventText = oldEventText.substring(0, appendIndex) + "\nBEGIN:VEVENT" + newEventText + "END:VEVENT" + oldEventText.substring(appendIndex)
- return updatedEventText
-}
diff --git a/plugins/nextcloud/js/webdav.js b/plugins/nextcloud/js/webdav.js
index 4f2e2cafd..24aa1f226 100644
--- a/plugins/nextcloud/js/webdav.js
+++ b/plugins/nextcloud/js/webdav.js
@@ -320,28 +320,12 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
}
onBuild(dom) {
- let modalObj = this
this.tree = dom.querySelector('#sm-nc-calendars');
this.tree.addEventListener('click', event => {
let el = event.target;
if (el.matches('button')) {
this.select = el.href;
- let VEVENT = this.VEVENT
-
- rl.nextcloud.calendarPut(this.select, this.VEVENT, (response) => {
- if (response.status != 201 && response.status != 204) {
- InvitesPopupView.showModal([
- rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_TITLE'),
- rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_BODY', {eventName: VEVENT.SUMMARY}),
- () => {modalObj.close()}
- ])
- }
- InvitesPopupView.showModal([
- rl.i18n('MESSAGE/EVENT_ADDED_TITLE'),
- rl.i18n('MESSAGE/EVENT_ADDED_BODY', {eventName: VEVENT.SUMMARY}),
- () => {modalObj.close()}
- ])
- })
+ this.close();
}
});
}
@@ -391,10 +375,9 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
treeElement.appendChild(li);
}
// Happens after showModal()
- beforeShow(fResolve, VEVENT) {
+ beforeShow(fResolve) {
this.select = '';
this.fResolve = fResolve;
- this.VEVENT = VEVENT
this.tree.innerHTML = '';
davFetch('calendars', '/', {
method: 'PROPFIND',
@@ -449,15 +432,14 @@ close() {}
}
rl.nextcloud = {
- selectCalendar: (VEVENT) =>
+ selectCalendar: () =>
new Promise(resolve => {
NextcloudCalendarsPopupView.showModal([
href => resolve(href),
- VEVENT
]);
}),
- calendarPut: (path, event, callback) => {
+ calendarPut: (path, event) => {
davFetch('calendars', path + '/' + event.UID + '.ics', {
method: 'PUT',
headers: {
@@ -481,8 +463,6 @@ rl.nextcloud = {
// response.text().then(text => console.error({status:response.status, body:text}));
Promise.reject(new Error({ response }));
}
-
- callback && callback(response)
});
},
@@ -520,27 +500,3 @@ function getElementsInNamespaces(xmlDocument, tagName) {
}
return results;
}
-
-
-class InvitesPopupView extends rl.pluginPopupView {
- constructor() {
- super('NextcloudInvites')
- }
-
- onBuild(dom) {
- this.title = dom.querySelector('#sm-invites-popup-title')
- this.body = dom.querySelector('#sm-invites-popup-body')
- }
-
- beforeShow(title, body, onHide_) {
- this.title.innerHTML = title
- this.body.innerHTML = body
- this.onHide_ = onHide_
- }
-
- onHide() {
- if (this.onHide_) {
- this.onHide_()
- }
- }
-}
diff --git a/plugins/nextcloud/langs/en.json b/plugins/nextcloud/langs/en.json
index d7843c3b9..2c19d11c2 100644
--- a/plugins/nextcloud/langs/en.json
+++ b/plugins/nextcloud/langs/en.json
@@ -2,21 +2,13 @@
"NEXTCLOUD": {
"SAVE_ATTACHMENTS": "Save in Nextcloud",
"SAVE_EML": "Save as .eml in Nextcloud",
+ "SAVE_ICS": "Add to calendar",
"SELECT_FOLDER": "Select folder",
"SELECT_FILES": "Select file(s)",
"ATTACH_FILES": "Attach Nextcloud files",
+ "SELECT_CALENDAR": "Select calendar",
"FILE_ATTACH": "attach",
"FILE_INTERNAL": "internal",
- "FILE_PUBLIC": "public",
-
- "SELECT_CALENDAR": "Select calendar",
- "SAVE_ICS": "Add to calendar",
- "OLD_INVITATION": "Old invitation",
- "UPDATE_ON_MY_CALENDAR": "Update invitation",
- "LAST_INVITATION": "Last invitation",
- "EVENT_UPDATE_FAILURE_TITLE": "Update failed",
- "EVENT_UPDATE_FAILURE_BODY": "Why, i have no clue",
- "EVENT_UPDATED_TITLE": "Updated",
- "EVENT_UPDATED_BODY": "Why, i have no clue"
+ "FILE_PUBLIC": "public"
}
}
diff --git a/plugins/nextcloud/templates/PopupsNextcloudInvites.html b/plugins/nextcloud/templates/PopupsNextcloudInvites.html
deleted file mode 100644
index 6056fb2ed..000000000
--- a/plugins/nextcloud/templates/PopupsNextcloudInvites.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
From 28a8e3c157452db2824f5b321b75dc75cb05393d Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Tue, 8 Oct 2024 13:06:43 +0200
Subject: [PATCH 19/45] Added the proper logging for #1706
---
plugins/custom-login-mapping/index.php | 21 ++++++++-----------
.../libraries/RainLoop/Actions/Accounts.php | 1 +
.../libraries/RainLoop/Actions/UserAuth.php | 1 +
.../app/libraries/RainLoop/Model/Account.php | 5 +++--
.../app/libraries/RainLoop/Model/Domain.php | 4 ++--
.../libraries/RainLoop/Providers/Domain.php | 13 +++++-------
6 files changed, 21 insertions(+), 24 deletions(-)
diff --git a/plugins/custom-login-mapping/index.php b/plugins/custom-login-mapping/index.php
index f282b0f60..e7d287b5f 100644
--- a/plugins/custom-login-mapping/index.php
+++ b/plugins/custom-login-mapping/index.php
@@ -12,6 +12,7 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
+ // Happens only at DoLogin | ServiceSso | DoAccountSetup
$this->addHook('login.credentials', 'FilterLoginCredentials');
}
@@ -29,18 +30,14 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
if (!empty($sMapping)) {
$aLines = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sMapping));
foreach ($aLines as $sLine) {
- if (false !== \strpos($sLine, ':')) {
- $aData = \explode(':', $sLine, 3);
- if (\is_array($aData) && !empty($aData[0]) && isset($aData[1])) {
- $aData = \array_map('trim', $aData);
- if ($sEmail === $aData[0]) {
- if (\strlen($aData[1])) {
- $sImapUser = $aData[1];
- }
- if (isset($aData[2]) && \strlen($aData[2])) {
- $sSmtpUser = $aData[2];
- }
- }
+ $aData = \explode(':', $sLine, 3);
+ if (\is_array($aData) && isset($aData[1]) && \trim($aData[0]) === $sEmail) {
+ $aData = \array_map('trim', $aData);
+ if (\strlen($aData[1])) {
+ $sImapUser = $aData[1];
+ }
+ if (isset($aData[2]) && \strlen($aData[2])) {
+ $sSmtpUser = $aData[2];
}
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php
index da6b9e199..2a4e40bb0 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php
@@ -69,6 +69,7 @@ trait Accounts
}
/**
+ * Add/Edit additional account
* @throws \MailSo\RuntimeException
*/
public function DoAccountSetup(): array
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php
index f61af74db..f497fedd0 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php
@@ -207,6 +207,7 @@ trait UserAuth
// Test the login
$oImapClient = new \MailSo\Imap\ImapClient;
+ $oImapClient->SetLogger($this->Logger());
$this->imapConnect($oAccount, false, $oImapClient);
}
$this->SetAdditionalAuthToken($oAccount);
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php
index 00a0651f9..d3dcd9154 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php
@@ -165,7 +165,8 @@ abstract class Account implements \JsonSerializable
// $aAccountHash['login'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['login']);
$oAccount = new static;
$oAccount->sEmail = \SnappyMail\IDN::emailToAscii($aAccountHash['email']);
- $oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']);
+// $oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']);
+ $oAccount->sImapUser = $aAccountHash['login'];
$oAccount->setImapPass(new SensitiveString($aAccountHash['pass']));
$oAccount->oDomain = $oDomain;
$oActions->Plugins()->RunHook('filter.account', array($oAccount));
@@ -286,7 +287,7 @@ abstract class Account implements \JsonSerializable
*/
/**
- * @deprecated
+ * @deprecated since v2.36.1
*/
public function IncLogin() : string
{
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php
index a65f2235b..b9c7d3c86 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php
@@ -106,7 +106,7 @@ class Domain implements \JsonSerializable
public function ValidateWhiteList(string $sEmail) : bool
{
- $sW = \trim($this->whiteList);
+ $sW = $this->whiteList;
if (!$sW) {
return true;
}
@@ -210,7 +210,7 @@ class Domain implements \JsonSerializable
$oDomain->SMTP->setSender = !empty($aDomain['smtp_set_sender']);
$oDomain->SMTP->usePhpMail = !empty($aDomain['smtp_php_mail']);
- $oDomain->whiteList = \trim($aDomain['white_list'] ?? '');
+ $oDomain->whiteList = $aDomain['white_list'] ?? '';
$oDomain->Normalize();
}
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php
index c465c19cb..41b18a840 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php
@@ -6,15 +6,9 @@ use RainLoop\Exceptions\ClientException;
class Domain extends AbstractProvider
{
- /**
- * @var Domain\DomainInterface
- */
- private $oDriver;
+ private Domain\DomainInterface $oDriver;
- /**
- * @var \RainLoop\Plugins\Manager
- */
- private $oPlugins;
+ private \RainLoop\Plugins\Manager $oPlugins;
public function __construct(Domain\DomainInterface $oDriver, \RainLoop\Plugins\Manager $oPlugins)
{
@@ -86,10 +80,13 @@ class Domain extends AbstractProvider
{
$oDomain = $this->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true);
if (!$oDomain) {
+ $this->logWrite("{$sEmail} has no domain configuration", \LOG_INFO, 'domain');
throw new ClientException(Notifications::DomainNotAllowed);
}
if (!$oDomain->ValidateWhiteList($sEmail)) {
+ $this->logWrite("{$sEmail} not whitelisted in {$oDomain->Name()}", \LOG_WARNING, 'domain');
throw new ClientException(Notifications::AccountNotAllowed);
+// throw new ClientException(Notifications::AccountNotAllowed, null, "{$sEmail} not whitelisted in {$oDomain->Name()}");
}
return $oDomain;
}
From 6f2f40b9837f2accd67089a6bed9e1a5546e0a1a Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Tue, 8 Oct 2024 17:49:21 +0200
Subject: [PATCH 20/45] Prepare for #1793
---
dev/Sieve/View/Filter.js | 2 +-
dev/View/Popup/Compose.js | 38 ++++++++---------
plugins/send-save-in/LICENSE | 20 +++++++++
plugins/send-save-in/index.php | 23 +++++++++++
plugins/send-save-in/savein.js | 75 ++++++++++++++++++++++++++++++++++
5 files changed, 138 insertions(+), 20 deletions(-)
create mode 100644 plugins/send-save-in/LICENSE
create mode 100644 plugins/send-save-in/index.php
create mode 100644 plugins/send-save-in/savein.js
diff --git a/dev/Sieve/View/Filter.js b/dev/Sieve/View/Filter.js
index ee7799d75..41bf85998 100644
--- a/dev/Sieve/View/Filter.js
+++ b/dev/Sieve/View/Filter.js
@@ -66,7 +66,7 @@ export class FilterPopupView extends rl.pluginPopupView {
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
- this.folderSelectList = koComputable(() => folderListOptionsBuilder());
+ this.folderSelectList = koComputable(folderListOptionsBuilder);
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js
index 581ad9f21..302017dfb 100644
--- a/dev/View/Popup/Compose.js
+++ b/dev/View/Popup/Compose.js
@@ -457,10 +457,21 @@ export class ComposePopupView extends AbstractViewPopup {
this.from(IdentityUserStore()[0].formattedName());
}
- sendCommand() {
- const identity = this.currentIdentity();
- let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
+ sentFolder()
+ {
+ let sSentFolder = this.currentIdentity()?.sentFolder?.() || FolderUserStore.sentFolder();
+ if (SettingsUserStore.replySameFolder()) {
+ if (
+ 3 === arrayLength(this.aDraftInfo) &&
+ this.aDraftInfo[2]?.length
+ ) {
+ sSentFolder = this.aDraftInfo[2];
+ }
+ }
+ return UNUSED_OPTION_VALUE === sSentFolder ? null : sSentFolder;
+ }
+ sendCommand() {
this.attachmentsInProcessError(false);
this.attachmentsInErrorError(false);
this.emptyToError(false);
@@ -478,16 +489,8 @@ export class ComposePopupView extends AbstractViewPopup {
}
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
- if (SettingsUserStore.replySameFolder()) {
- if (
- 3 === arrayLength(this.aDraftInfo) &&
- this.aDraftInfo[2]?.length
- ) {
- sSentFolder = this.aDraftInfo[2];
- }
- }
-
- if (!sSentFolder) {
+ const sSentFolder = this.sentFolder();
+ if ('' === sSentFolder) {
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
} else {
const sendError = e => {
@@ -507,8 +510,6 @@ export class ComposePopupView extends AbstractViewPopup {
this.sendError(false);
this.sending(true);
- sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
-
const sendMessage = params => {
Remote.request('SendMessage',
(iError, data) => {
@@ -538,7 +539,7 @@ export class ComposePopupView extends AbstractViewPopup {
this.sendError(true);
sendFailed(iError, data);
// Remove remembered passphrase as it could be wrong
- let key = ('S/MIME' === params.sign) ? identity : null;
+ let key = ('S/MIME' === params.sign) ? this.currentIdentity() : null;
params.signFingerprint
&& this.signOptions.forEach(option => ('GnuPG' === option[0]) && (key = option[1]));
key && Passphrases.delete(key);
@@ -560,10 +561,9 @@ export class ComposePopupView extends AbstractViewPopup {
this.close();
}
setFolderETag(this.draftsFolder(), '');
- setFolderETag(sSentFolder, '');
+ setFolderETag(params.saveFolder, '');
if (3 === arrayLength(this.aDraftInfo)) {
- const folder = this.aDraftInfo[2];
- setFolderETag(folder, '');
+ setFolderETag(this.aDraftInfo[2], '');
}
reloadDraftFolder();
},
diff --git a/plugins/send-save-in/LICENSE b/plugins/send-save-in/LICENSE
new file mode 100644
index 000000000..f709b02e2
--- /dev/null
+++ b/plugins/send-save-in/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 RainLoop Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/send-save-in/index.php b/plugins/send-save-in/index.php
new file mode 100644
index 000000000..e3b4e3fe6
--- /dev/null
+++ b/plugins/send-save-in/index.php
@@ -0,0 +1,23 @@
+UseLangs(true); // start use langs folder
+ $this->addJs('savein.js');
+ }
+}
diff --git a/plugins/send-save-in/savein.js b/plugins/send-save-in/savein.js
new file mode 100644
index 000000000..b95e18892
--- /dev/null
+++ b/plugins/send-save-in/savein.js
@@ -0,0 +1,75 @@
+(rl => {
+ const templateId = 'PopupsCompose',
+ folderListOptionsBuilder = () => {
+ const
+ aResult = [{
+ id: '',
+ name: '',
+ system: false,
+ disabled: false
+ }],
+ sDeepPrefix = '\u00A0\u00A0\u00A0',
+ showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
+
+ foldersWalk = folders => {
+ folders.forEach(oItem => {
+ if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
+ aResult.push({
+ id: oItem.fullName,
+ name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
+ system: false,
+ disabled: !oItem.selectable()
+ });
+ }
+
+ if (oItem.subFolders.length) {
+ foldersWalk(oItem.subFolders());
+ }
+ });
+ };
+
+
+ // FolderUserStore.folderList()
+ foldersWalk(rl.app.folderList() || []);
+
+ return aResult;
+ };
+
+ let oldSentFolderFn;
+
+ addEventListener('rl-view-model.create', e => {
+ if (templateId === e.detail.viewModelTemplateID) {
+ const view = e.detail; // ComposePopupView
+
+ view.sentFolderValue = ko.observable('');
+ view.sentFolderSelectList = ko.computed(folderListOptionsBuilder, {'pure':true});
+ view.defaultOptionsAfterRender = (domItem, item) =>
+ item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled);
+
+ oldSentFolderFn = view.sentFolder.bind(view);
+ view.sentFolder = () => view.sentFolderValue() || oldSentFolderFn();
+
+ document.getElementById(templateId).content.querySelector('.b-header tbody').append(Element.fromHTML(`
+
+ | Store in |
+
+
+ (When send, store a copy of the message in the selected folder)
+ |
+ `));
+
+ view.currentIdentity.subscribe(()=>{
+ view.sentFolderValue(oldSentFolderFn());
+ });
+ }
+ });
+
+ addEventListener('rl-vm-visible', e => {
+ if (templateId === e.detail.viewModelTemplateID) {
+ const view = e.detail; // ComposePopupView
+ view.sentFolderValue(oldSentFolderFn());
+ }
+ });
+
+})(window.rl);
From 0a4cea5e4c97bb5a904ad883c93f59f6413aa9c0 Mon Sep 17 00:00:00 2001
From: Sergey Mosin
Date: Tue, 8 Oct 2024 09:47:47 -0400
Subject: [PATCH 21/45] make sure that KO bindings are re-applied
---
.../compact-composer/js/CompactComposer.js | 28 +++++++++++++------
1 file changed, 19 insertions(+), 9 deletions(-)
diff --git a/plugins/compact-composer/js/CompactComposer.js b/plugins/compact-composer/js/CompactComposer.js
index 288fb497c..5baea90a3 100644
--- a/plugins/compact-composer/js/CompactComposer.js
+++ b/plugins/compact-composer/js/CompactComposer.js
@@ -17,18 +17,28 @@
addEventListener('rl-view-model', e => {
const vm = e.detail;
if ('PopupsCompose' === vm.viewModelTemplateID && rl.settings.get('editorWysiwyg') === 'CompactComposer') {
- vm.querySelector('.tabs label[for="tab-body"]').dataset.bind = "visible: canMailvelope";
+ // add visible binding to the label
+ const bodyLabel = vm.querySelector('.tabs label[for="tab-body"]');
+ bodyLabel.dataset.bind = 'visible: canMailvelope';
+ // re-apply the binding
+ const labelNext = bodyLabel.nextElementSibling;
+ ko.removeNode(bodyLabel);
+ labelNext.parentElement.insertBefore(bodyLabel, labelNext);
+ ko.applyBindingAccessorsToNode(bodyLabel, null, vm);
+
// Now move the attachments tab to the bottom of the screen
- const
- input = vm.querySelector('.tabs input[value="attachments"]'),
- label = vm.querySelector('.tabs label[for="tab-attachments"]'),
- area = vm.querySelector('.tabs .attachmentAreaParent');
- input.remove();
- label.remove();
- area.remove();
+ const area = vm.querySelector('.tabs .attachmentAreaParent');
+ vm.querySelector('.tabs input[value="attachments"]').remove();
+ vm.querySelector('.tabs label[for="tab-attachments"]').remove();
+ area.querySelector('.no-attachments-desc').remove();
area.classList.add('compact');
- area.querySelector('.b-attachment-place').dataset.bind = "visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}";
vm.viewModelDom.append(area);
+ // Add and re-apply the bindings for the attachment-place
+ const place = area.querySelector('.b-attachment-place');
+ ko.removeNode(place);
+ area.insertBefore(place, area.firstElementChild);
+ place.dataset.bind = 'visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}';
+ ko.applyBindingAccessorsToNode(place, null, vm);
// There is a better way to do this probably,
// but we need this for drag and drop to work
e.detail.attachmentsArea = e.detail.bodyArea;
From 48b0519cb490bc72e754a7a92bab666c651d9afb Mon Sep 17 00:00:00 2001
From: Sergey Mosin
Date: Tue, 8 Oct 2024 14:58:02 -0400
Subject: [PATCH 22/45] use 'Utils.cleanHtml' as pasteSanitizer + minor tweaks
---
plugins/compact-composer/css/composer.css | 5 ++++
.../compact-composer/js/CompactComposer.js | 23 +++----------------
2 files changed, 8 insertions(+), 20 deletions(-)
diff --git a/plugins/compact-composer/css/composer.css b/plugins/compact-composer/css/composer.css
index 8e0bfff15..c2afc31c6 100644
--- a/plugins/compact-composer/css/composer.css
+++ b/plugins/compact-composer/css/composer.css
@@ -1,3 +1,8 @@
+/* Prevent collapse of empty elements so that the caret can be placed in there */
+.CompactComposer *:empty::before {
+ content: "\200B";
+}
+
.CompactComposer .squire-toolbar {
padding-top: 4px;
padding-bottom: 0;
diff --git a/plugins/compact-composer/js/CompactComposer.js b/plugins/compact-composer/js/CompactComposer.js
index 5baea90a3..01d020bb3 100644
--- a/plugins/compact-composer/js/CompactComposer.js
+++ b/plugins/compact-composer/js/CompactComposer.js
@@ -46,10 +46,6 @@
});
const
- removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
- allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
- allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
-
// TODO: labels translations
i18n = (str, def) => rl.i18n(str) || def,
@@ -105,21 +101,7 @@
},
pasteSanitizer = (event) => {
- const frag = event.detail.fragment;
- frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
- frag.querySelectorAll(removeElements).forEach(el => el.remove());
- frag.querySelectorAll('*').forEach(el => {
- if (!el.matches(allowedElements)) {
- el.replaceWith(getFragmentOfChildren(el));
- } else if (el.hasAttributes()) {
- [...el.attributes].forEach(attr => {
- let name = attr.name.toLowerCase();
- if (!allowedAttributes.includes(name)) {
- el.removeAttribute(name);
- }
- });
- }
- });
+ return rl.Utils.cleanHtml(event.detail.html).html;
},
pasteImageHandler = (e, squire) => {
@@ -327,7 +309,8 @@
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
clr.style.width = input.offsetWidth + 'px';
- clr.value = '';
+ // firefox does not call "onchange" for #000 if we use clr.value=''
+ clr.value = '#00ff0c';
clr.onchange = () => {
switch (name) {
case 'color':
From e7ecf53b4a3451ce34db2ff60f1bda1fe9149f4d Mon Sep 17 00:00:00 2001
From: Sergey Mosin
Date: Tue, 8 Oct 2024 15:01:18 -0400
Subject: [PATCH 23/45] bump version to v1.0.5
---
plugins/compact-composer/index.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/compact-composer/index.php b/plugins/compact-composer/index.php
index ae743ca79..d98b5cc95 100644
--- a/plugins/compact-composer/index.php
+++ b/plugins/compact-composer/index.php
@@ -6,8 +6,8 @@ class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Compact Composer',
AUTHOR = 'Sergey Mosin',
URL = 'https://github.com/the-djmaze/snappymail/pull/1466',
- VERSION = '1.0.4',
- RELEASE = '2024-04-23',
+ VERSION = '1.0.5',
+ RELEASE = '2024-08-08',
REQUIRED = '2.34.0',
LICENSE = 'AGPL v3',
DESCRIPTION = 'WYSIWYG editor with a compact toolbar';
From b86743de24af0fab7807c5235c08c2da81324808 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Tue, 8 Oct 2024 22:52:33 +0200
Subject: [PATCH 24/45] Resolve #1706
---
.../app/libraries/RainLoop/Actions/UserAuth.php | 14 ++++++++------
.../app/libraries/RainLoop/Providers/Domain.php | 8 +++-----
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php
index f497fedd0..be26a590e 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php
@@ -141,21 +141,23 @@ trait UserAuth
*/
public function LoginProcess(string $sEmail, SensitiveString $oPassword, bool $bMainAccount = true): Account
{
- $sCredentials = $this->resolveLoginCredentials($sEmail, $oPassword);
+ $aCredentials = $this->resolveLoginCredentials($sEmail, $oPassword);
- if (!\str_contains($sCredentials['email'], '@') || !\strlen($oPassword)) {
+ if (!\str_contains($aCredentials['email'], '@') || !\strlen($oPassword)) {
throw new ClientException(Notifications::InvalidInputArgument);
}
+ $oDomain = $this->DomainProvider()->getByEmailAddress($aCredentials['email']);
+
$oAccount = null;
try {
$oAccount = $bMainAccount ? new MainAccount : new AdditionalAccount;
$oAccount->setCredentials(
- $sCredentials['domain'],
- $sCredentials['email'],
- $sCredentials['imapUser'],
+ $aCredentials['domain'],
+ $aCredentials['email'],
+ $aCredentials['imapUser'],
$oPassword,
- $sCredentials['smtpUser']
+ $aCredentials['smtpUser']
// ,new SensitiveString($oPassword)
);
$this->Plugins()->RunHook('filter.account', array($oAccount));
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php
index 41b18a840..25ef02501 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php
@@ -2,6 +2,7 @@
namespace RainLoop\Providers;
+use RainLoop\Notifications;
use RainLoop\Exceptions\ClientException;
class Domain extends AbstractProvider
@@ -80,13 +81,10 @@ class Domain extends AbstractProvider
{
$oDomain = $this->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true);
if (!$oDomain) {
- $this->logWrite("{$sEmail} has no domain configuration", \LOG_INFO, 'domain');
- throw new ClientException(Notifications::DomainNotAllowed);
+ throw new ClientException(Notifications::DomainNotAllowed, null, "{$sEmail} has no domain configuration");
}
if (!$oDomain->ValidateWhiteList($sEmail)) {
- $this->logWrite("{$sEmail} not whitelisted in {$oDomain->Name()}", \LOG_WARNING, 'domain');
- throw new ClientException(Notifications::AccountNotAllowed);
-// throw new ClientException(Notifications::AccountNotAllowed, null, "{$sEmail} not whitelisted in {$oDomain->Name()}");
+ throw new ClientException(Notifications::AccountNotAllowed, null, "{$sEmail} not whitelisted");
}
return $oDomain;
}
From ececfae31ae64fb5f617183dfb029e809879903a Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Tue, 8 Oct 2024 23:07:58 +0200
Subject: [PATCH 25/45] v2.38.1
---
CHANGELOG.md | 53 ++++++++++++++++++-
README.md | 34 ++++++------
integrations/cloudron/DESCRIPTION.md | 2 +-
integrations/cloudron/Dockerfile | 2 +-
.../nextcloud/snappymail/appinfo/info.xml | 2 +-
integrations/virtualmin/snappymail.pl | 2 +-
package.json | 2 +-
plugins/nextcloud/index.php | 4 +-
plugins/send-save-in/index.php | 2 +-
.../templates/Views/User/MailMessageView.html | 2 +-
10 files changed, 78 insertions(+), 27 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2c804345..a165aeeb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,54 @@
+## 2.38.1 – 2024-10-08
+
+### Added
+- Admin - Extensions search filter
+- Options to unset current font family and size
+ [#1726](https://github.com/the-djmaze/snappymail/issues/1726)
+- Option to allow insecure cryptkey
+ [#1746](https://github.com/the-djmaze/snappymail/issues/1746)
+- Save vCard FN property
+ [#1761](https://github.com/the-djmaze/snappymail/issues/1761)
+- Docker Nginx should listen on IPv6 addresses in addition to IPv4
+ [#1770](https://github.com/the-djmaze/snappymail/issues/1770)
+- Full-screen Message View on Double-Click
+ [#1787](https://github.com/the-djmaze/snappymail/issues/1787)
+
+### Changed
+- Use the custom Squire as submodule vendors/squire2
+- Disallow noembed and noframes HTML elements
+- Keep 1 space between HTML elements in compressed templates
+- Squire: sanitizeToDOMFragment now uses cleanHTML()
+- Squire: improved handling of BR elements
+ [#1389](https://github.com/the-djmaze/snappymail/issues/1389)
+- Nextcloud border-radius by @gnilebein
+ [#1790](https://github.com/the-djmaze/snappymail/pull/1790)
+- Compose window sentFolder handling for
+ [#1793](https://github.com/the-djmaze/snappymail/pull/1793)
+- Update Portuguese by @ner00
+- Update Polish by @tinola
+- Update French by @hguilbert
+- Update Portuguese (Brazil) by @mstolf
+
+### Fixed
+- Composer dialog scroll got broken in v2.28
+- Composer dialog "from" triangle button wrong position due to font changes
+- Admin - Config `search` should be ko.observable() not ko.observableArray()
+- Squire: paste images
+ [#1389](https://github.com/the-djmaze/snappymail/issues/1389)
+- Domain whitelist failures on login
+ [#1706](https://github.com/the-djmaze/snappymail/issues/1706)
+- Sieve parse errors
+- Sieve support `index` was not optional for
+ [#1709](https://github.com/the-djmaze/snappymail/issues/1709)
+- Pagination problem for large mailbox after 2.36.1
+ [#1716](https://github.com/the-djmaze/snappymail/issues/1716)
+- Undefined constant "LOG_ERROR"
+ [#1754](https://github.com/the-djmaze/snappymail/issues/1754)
+- Search Filter Capital "B" not working
+ [#1780](https://github.com/the-djmaze/snappymail/issues/1780)
+- PHP 8.4: Implicitly nullable parameter declarations deprecated
+
+
## 2.38.0 – 2024-09-16
### Added
@@ -247,7 +298,7 @@
- CSS set min-width for .attachmentParent and .flagParent to line them up
- cPanel use extension login-cpanel instead of login-remote
- Improved login credentials handling
-- Speedup Knockout a bit
+- Speedup Knockout a bit and removed `with($context)` scope
- Update Belarusian by @spoooyders
- Update Chinese by @mayswind
- Update French by @hguilbert
diff --git a/README.md b/README.md
index 04e17c287..09d5abded 100644
--- a/README.md
+++ b/README.md
@@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
-|admin.js |2.170.153 | 84.203 |
-|app.js |4.207.787 | 446.887 |
+|admin.js |2.170.153 | 84.925 |
+|app.js |4.207.787 | 447.263 |
|boot.js | 868.735 | 4.343 |
-|libs.js | 658.812 | 236.059 |
-|sieve.js | 0 | 84.571 |
+|libs.js | 658.812 | 233.715 |
+|sieve.js | 0 | 91.418 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
-|TOTAL |8.240.095 | 856.348 |
+|TOTAL |8.240.095 | 861.949 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
-|admin.min.js | 256.831 | 41.256 | 73.606 | 13.940 | 60.877 | 12.493 |
-|app.min.js | 515.367 | 201.917 |139.456 | 68.450 |110.485 | 58.321 |
+|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
+|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
-|libs.min.js | 584.772 | 111.742 |180.901 | 39.882 |155.182 | 35.584 |
-|sieve.min.js | 0 | 41.288 | 0 | 10.327 | 0 | 9.318 |
+|libs.min.js | 584.772 | 110.633 |180.901 | 39.515 |155.182 | 35.246 |
+|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
-|TOTAL user |1.217.635 | 315.890 |358.761 |109.603 |299.485 | 94.972 |
-|TOTAL user+sieve |1.217.635 | 357.178 |358.761 |119.930 |299.485 |104.290 |
-|TOTAL admin | 959.099 | 155.229 |292.911 | 55.093 |249.877 | 49.144 |
+|TOTAL user |1.217.635 | 314.965 |358.761 |109.291 |299.485 | 94.794 |
+|TOTAL user+sieve |1.217.635 | 360.469 |358.761 |120.422 |299.485 |104.711 |
+|TOTAL admin | 959.099 | 154.583 |292.911 | 54.808 |249.877 | 48.880 |
-For a user it is around 67% smaller and faster than traditional RainLoop.
+For a user it is around 66% smaller and faster than traditional RainLoop.
### CSS changes
@@ -188,12 +188,12 @@ For a user it is around 67% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
-|app.css | 340.331 | 84.850 | 46.946 | 17.710 | 15.161 |
-|app.min.css | 274.947 | 68.192 | 39.647 | 15.602 | 13.626 |
+|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
+|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
-|admin.css | | 30.761 | | 7.038 | 6.118 |
-|admin.min.css | | 24.857 | | 6.360 | 5.598 |
+|admin.css | | 30.880 | | 7.045 | 6.127 |
+|admin.min.css | | 24.959 | | 6.368 | 5.615 |
### PGP
RainLoop uses the old OpenPGP.js v2
diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md
index 599a4c1cc..4c6cf6a51 100644
--- a/integrations/cloudron/DESCRIPTION.md
+++ b/integrations/cloudron/DESCRIPTION.md
@@ -1,4 +1,4 @@
-This app packages SnappyMail 2.38.0.
+This app packages SnappyMail 2.38.1.
SnappyMail is a simple, modern, lightweight & fast web-based email client.
diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile
index c95b06a13..fc54eae02 100644
--- a/integrations/cloudron/Dockerfile
+++ b/integrations/cloudron/Dockerfile
@@ -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.38.0
+VERSION=2.38.1
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 && \
diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml
index dd1346e87..8dfaf6be7 100644
--- a/integrations/nextcloud/snappymail/appinfo/info.xml
+++ b/integrations/nextcloud/snappymail/appinfo/info.xml
@@ -3,7 +3,7 @@
snappymail
SnappyMail
SnappyMail Webmail
- 2.38.0
+ 2.38.1
agpl
SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli
❯
-
+
From cb9e1b07053406430a0150b3aeefdc0f6eab0fc4 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Wed, 9 Oct 2024 21:34:00 +0200
Subject: [PATCH 26/45] Resolve #1797
---
.../0.0.0/app/templates/Views/User/SettingsAccounts.html | 2 +-
vendors/knockout/build/fragments/extern-post.js | 2 +-
vendors/knockout/build/output/knockout-latest.debug.js | 8 +++++---
vendors/knockout/build/output/knockout-latest.js | 4 ++--
vendors/knockout/src/binding/bindingProvider.js | 6 ++++--
5 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html
index 4eb5a8c87..eac1e5f07 100644
--- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html
+++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html
@@ -59,7 +59,7 @@
-
+
|
{
itemsForBeforeRemoveCallbacks.forEach(callback => callback && (callback.arrayEntry = deletedItemDummyValue));
}
})();
- window['ko'] = koExports;
+ window['ko'] = koExports;
})(this);
diff --git a/vendors/knockout/build/output/knockout-latest.js b/vendors/knockout/build/output/knockout-latest.js
index 645811e44..b9a34d87c 100644
--- a/vendors/knockout/build/output/knockout-latest.js
+++ b/vendors/knockout/build/output/knockout-latest.js
@@ -31,8 +31,8 @@ a.value=b):(c.g.l.set(a,c.i.options.Wa,b),a.__ko__hasDomDataOptionValue__=!0,a.v
l[k-1].match(b))&&!d[p[0]]&&(e=e.slice(e.indexOf(m)+1),l=e.match(a),k=-1,m="/"):40===p||123===p||91===p?++h:41===p||125===p||93===p?--h:n||f.length||34!==p&&39!==p||(m=m.slice(1,-1));f.push(m)}if(0$data");return g.join(",")},cc:(e,g)=>-1l.key==g),Ga:(e,g,l,f,h,k)=>{g&&c.W(g)?!c.vb(g)||k&&g.L()===h||g(h):(console.error(`"${f}" should be observable in ${e.outerHTML.replace(/>.+/,">")}`),l.get("$data")[f]=
h)}}})();(()=>{function a(f){return 8==f.nodeType&&e.test(f.nodeValue)}function b(f){return 8==f.nodeType&&g.test(f.nodeValue)}function d(f,h){for(var k=f,m=1,p=[];k=k.nextSibling;){if(b(k)&&(c.g.l.set(k,l,!0),!--m))return p;p.push(k);a(k)&&++m}if(!h)throw Error("Cannot find closing comment tag to match: "+f.nodeValue);return null}var e=/^\s*ko(?:\s+([\s\S]+))?\s*$/,g=/^\s*\/ko\s*$/,l="__ko_matchedEndComment__";c.m={aa:{},childNodes:f=>a(f)?d(f):f.childNodes,ja:f=>{a(f)?(f=d(f))&&[...f].forEach(h=>
c.removeNode(h)):c.g.Qa(f)},pa:(f,h)=>{a(f)?(c.m.ja(f),f.after(...h)):c.g.pa(f,h)},prepend:(f,h)=>{a(f)?f.nextSibling.before(h):f.prepend(h)},Ub:(f,h,k)=>{k?k.after(h):c.m.prepend(f,h)},firstChild:f=>{if(a(f))return f=f.nextSibling,!f||b(f)?null:f;let h=f.firstChild;if(h&&b(h))throw Error("Found invalid end comment, as the first child of "+f);return h},nextSibling:f=>{if(a(f)){var h=d(f,void 0);f=h?(h.length?h[h.length-1]:f).nextSibling:null}if((h=f.nextSibling)&&b(h)){if(b(h)&&!c.g.l.get(h,l))throw Error("Found end comment without a matching opening comment, as child of "+
-f);return null}return h},Qb:a,ac:f=>(f=f.nodeValue.match(e))?f[1]:null}})();const T=new Map;c.ob=new class{Xb(a){switch(a.nodeType){case 1:return null!=a.getAttribute("data-bind");case 8:return c.m.Qb(a)}return!1}Ob(a,b){a:{switch(a.nodeType){case 1:var d=a.getAttribute("data-bind");break a;case 8:d=c.m.ac(a);break a}d=null}if(d)try{let g=T.get(d);if(!g){var e="with($data){return{"+c.la.Yb(d)+"}}";g=new Function("$root","$parent","$data","$element",e);T.set(d,g)}return g(b.$root,b.$parent,b.$data||
-{},a)}catch(g){throw g.message="Unable to parse bindings.\nBindings value: "+d+"\nMessage: "+g.message,g;}return null}};const G=Symbol("_subscribable"),H=Symbol("_ancestorBindingInfo"),U=Symbol("_dataDependency"),V={},I=c.g.l.Z();c.i={};c.ba=class{constructor(a,b,d,e){var g=this,l=a===V,f=l?void 0:a,h="function"==typeof f&&!c.W(f),k=e?.dataDependency;a=()=>{var p=h?f():f;p=c.g.h(p);b?(c.g.extend(g,b),H in b&&(g[H]=b[H])):g.$root=p;g[G]=m;l?p=g.$data:g.$data=p;d?.(g,b,p);if(b?.[G]&&!c.u.o().Sa(b[G]))b[G]();
+f);return null}return h},Qb:a,ac:f=>(f=f.nodeValue.match(e))?f[1]:null}})();const T=new Map;c.ob=new class{Xb(a){switch(a.nodeType){case 1:return null!=a.getAttribute("data-bind");case 8:return c.m.Qb(a)}return!1}Ob(a,b){a:{switch(a.nodeType){case 1:var d=a.getAttribute("data-bind");break a;case 8:d=c.m.ac(a);break a}d=null}if(d)try{let g=T.get(d);if(!g){var e="with($data){return{"+c.la.Yb(d)+"}}";g=new Function("$context","$root","$parent","$data","$element",e);T.set(d,g)}return g(b,b.$root,b.$parent,
+b.$data||{},a)}catch(g){throw g.message="Unable to parse bindings.\nBindings value: "+d+"\nMessage: "+g.message,g;}return null}};const G=Symbol("_subscribable"),H=Symbol("_ancestorBindingInfo"),U=Symbol("_dataDependency"),V={},I=c.g.l.Z();c.i={};c.ba=class{constructor(a,b,d,e){var g=this,l=a===V,f=l?void 0:a,h="function"==typeof f&&!c.W(f),k=e?.dataDependency;a=()=>{var p=h?f():f;p=c.g.h(p);b?(c.g.extend(g,b),H in b&&(g[H]=b[H])):g.$root=p;g[G]=m;l?p=g.$data:g.$data=p;d?.(g,b,p);if(b?.[G]&&!c.u.o().Sa(b[G]))b[G]();
k&&(g[U]=k);return g.$data};if(e?.exportDependencies)a();else{var m=c.xb(a);m.L();m.isActive()?m.ka=null:g[G]=void 0}}createChildContext(a,b){return new c.ba(a,this,(d,e)=>{d.$parent=e.$data;b.extend?.(d)},b)}extend(a,b){return new c.ba(V,this,d=>c.g.extend(d,"function"==typeof a?a(d):a),b)}};const W=a=>{a=c.g.l.get(a,I);var b=a?.D;b&&(a.D=null,b.wb())};class ma{constructor(a,b,d){this.H=a;this.da=b;this.ta=new Set;this.F=!1;b.D||c.g.N.addDisposeCallback(a,W);d?.D&&(d.D.ta.add(a),this.za=d)}wb(){this.za?.D?.Mb(this.H)}Mb(a){this.ta.delete(a);
this.ta.size||this.rb?.()}rb(){this.F=!0;this.da.D&&!this.ta.size&&(this.da.D=null,c.g.N.Ya(this.H,W),c.j.notify(this.H,c.j.ca),this.wb())}}c.j={F:"childrenComplete",ca:"descendantsComplete",subscribe:(a,b,d,e,g)=>{var l=c.g.l.Ra(a,I,{});l.wa||(l.wa=new c.P);g?.notifyImmediately&&l.Va[b]&&c.u.I(d,e,[a]);return l.wa.subscribe(d,e,b)},notify:(a,b)=>{var d=c.g.l.get(a,I);if(d&&(d.Va[b]=!0,d.wa?.B(a,b),b==c.j.F))if(d.D)d.D.rb();else if(void 0===d.D&&d.wa?.na(c.j.ca))throw Error("descendantsComplete event not supported for bindings on this node");
},$a:(a,b)=>{var d=c.g.l.Ra(a,I,{});d.D||(d.D=new ma(a,d,b[H]));return b[H]==d?b:b.extend(e=>{e[H]=d})}};const Y=(a,b)=>{for(var d,e=c.m.firstChild(b);d=e;)e=c.m.nextSibling(d),X(a,d);c.j.notify(b,c.j.F)},X=(a,b)=>{var d=a;if(1===b.nodeType||c.ob.Xb(b))d=Z(b,null,a);d&&!b.matches?.("SCRIPT,TEXTAREA,TEMPLATE")&&Y(d,b)},na=a=>{var b=[],d={},e=[],g=l=>{if(!d[l]){var f=c.i[l];f&&(f.after&&(e.push(l),f.after.forEach(h=>{if(a[h]){if(e.includes(h))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+
diff --git a/vendors/knockout/src/binding/bindingProvider.js b/vendors/knockout/src/binding/bindingProvider.js
index e87b34905..05120d2e3 100644
--- a/vendors/knockout/src/binding/bindingProvider.js
+++ b/vendors/knockout/src/binding/bindingProvider.js
@@ -38,10 +38,12 @@ ko.bindingProvider = new class
// Deprecated: with is no longer recommended
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
functionBody = "with($data){return{" + rewrittenBindings + "}}";
- bindingFunction = new Function("$root", "$parent", "$data", "$element", functionBody);
+ bindingFunction = new Function("$context", "$root", "$parent", "$data", "$element", functionBody);
bindingCache.set(cacheKey, bindingFunction);
}
- return bindingFunction(bindingContext["$root"], bindingContext["$parent"], bindingContext["$data"] || {}, node);
+ return bindingFunction(bindingContext,
+ bindingContext["$root"], bindingContext["$parent"], bindingContext["$data"] || {}, node
+ );
} catch (ex) {
ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString
+ "\nMessage: " + ex.message;
From 904a353bffd0aced84c6058041ca6648069187c2 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Wed, 9 Oct 2024 21:35:34 +0200
Subject: [PATCH 27/45] Secure ko template bindings?
---
vendors/knockout/src/binding/bindingProvider.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/vendors/knockout/src/binding/bindingProvider.js b/vendors/knockout/src/binding/bindingProvider.js
index 05120d2e3..eae03acca 100644
--- a/vendors/knockout/src/binding/bindingProvider.js
+++ b/vendors/knockout/src/binding/bindingProvider.js
@@ -36,6 +36,15 @@ ko.bindingProvider = new class
// For each scope variable, add an extra level of "with" nesting
// Example result: with(sc1) { with(sc0) { return (expression) } }
// Deprecated: with is no longer recommended
+/*
+ functionBody = "$context = new Proxy(
+ $context,
+ {
+ has:()=>true,
+ get:(target,key)=>Reflect.has(target, key) ? target[key] : target['$data'][key]
+ }
+ );with($context){return{" + rewrittenBindings + "}}";
+*/
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
functionBody = "with($data){return{" + rewrittenBindings + "}}";
bindingFunction = new Function("$context", "$root", "$parent", "$data", "$element", functionBody);
From e345550c7fddfffdda9095deba64e98dc1c59815 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Wed, 9 Oct 2024 21:41:01 +0200
Subject: [PATCH 28/45] v2.38.2
---
CHANGELOG.md | 6 ++++++
README.md | 14 +++++++-------
integrations/cloudron/DESCRIPTION.md | 2 +-
integrations/cloudron/Dockerfile | 2 +-
integrations/nextcloud/snappymail/appinfo/info.xml | 2 +-
integrations/virtualmin/snappymail.pl | 2 +-
package.json | 2 +-
7 files changed, 18 insertions(+), 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a165aeeb7..d31a1b771 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.38.2 – 2024-10-09
+### Fixed
+- error '$index is not defined' in Settings > Accounts
+ [#1797](https://github.com/the-djmaze/snappymail/issues/1797)
+
+
## 2.38.1 – 2024-10-08
### Added
diff --git a/README.md b/README.md
index 09d5abded..b9736d393 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ This fork of RainLoop has the following changes:
* CRLF => LF line endings
* Embed boot.js and boot.css into index.html
* Removal of old JavaScript code (things are native these days)
-* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
+* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
* Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
@@ -143,23 +143,23 @@ RainLoop 1.17 vs SnappyMail
|admin.js |2.170.153 | 84.925 |
|app.js |4.207.787 | 447.263 |
|boot.js | 868.735 | 4.343 |
-|libs.js | 658.812 | 233.715 |
+|libs.js | 658.812 | 233.728 |
|sieve.js | 0 | 91.418 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
-|TOTAL |8.240.095 | 861.949 |
+|TOTAL |8.240.095 | 861.962 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
-|libs.min.js | 584.772 | 110.633 |180.901 | 39.515 |155.182 | 35.246 |
+|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
-|TOTAL user |1.217.635 | 314.965 |358.761 |109.291 |299.485 | 94.794 |
-|TOTAL user+sieve |1.217.635 | 360.469 |358.761 |120.422 |299.485 |104.711 |
-|TOTAL admin | 959.099 | 154.583 |292.911 | 54.808 |249.877 | 48.880 |
+|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
+|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
+|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
For a user it is around 66% smaller and faster than traditional RainLoop.
diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md
index 4c6cf6a51..c3a50a8fa 100644
--- a/integrations/cloudron/DESCRIPTION.md
+++ b/integrations/cloudron/DESCRIPTION.md
@@ -1,4 +1,4 @@
-This app packages SnappyMail 2.38.1.
+This app packages SnappyMail 2.38.2.
SnappyMail is a simple, modern, lightweight & fast web-based email client.
diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile
index fc54eae02..a3d442e96 100644
--- a/integrations/cloudron/Dockerfile
+++ b/integrations/cloudron/Dockerfile
@@ -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.38.1
+VERSION=2.38.2
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 && \
diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml
index 8dfaf6be7..1ebad7ac1 100644
--- a/integrations/nextcloud/snappymail/appinfo/info.xml
+++ b/integrations/nextcloud/snappymail/appinfo/info.xml
@@ -3,7 +3,7 @@
snappymail
SnappyMail
SnappyMail Webmail
- 2.38.1
+ 2.38.2
agpl
SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli
Date: Thu, 10 Oct 2024 00:54:19 +0200
Subject: [PATCH 29/45] Use CSS instead of $index #1797
---
dev/Styles/User/SettingsAccounts.less | 4 ++++
.../v/0.0.0/app/templates/Views/User/SettingsAccounts.html | 3 +--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/dev/Styles/User/SettingsAccounts.less b/dev/Styles/User/SettingsAccounts.less
index 54e58c288..5b7f5c91a 100644
--- a/dev/Styles/User/SettingsAccounts.less
+++ b/dev/Styles/User/SettingsAccounts.less
@@ -45,4 +45,8 @@
content: ')';
}
+ .identities-list tr + tr .identity-default {
+ display: none;
+ }
+
}
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html
index eac1e5f07..1ae6846f6 100644
--- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html
+++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsAccounts.html
@@ -58,8 +58,7 @@
|
-
-
+
|
Date: Thu, 10 Oct 2024 00:54:30 +0200
Subject: [PATCH 30/45] KnockoutJS secure bindings using Proxy
---
.../build/output/knockout-latest.debug.js | 18 +++++
.../knockout/build/output/knockout-latest.js | 69 ++++++++++---------
.../knockout/src/binding/bindingProvider.js | 27 +++++---
3 files changed, 71 insertions(+), 43 deletions(-)
diff --git a/vendors/knockout/build/output/knockout-latest.debug.js b/vendors/knockout/build/output/knockout-latest.debug.js
index 15d1130f5..92e5a8162 100644
--- a/vendors/knockout/build/output/knockout-latest.debug.js
+++ b/vendors/knockout/build/output/knockout-latest.debug.js
@@ -1618,6 +1618,7 @@ ko.bindingProvider = new class
try {
let cacheKey = bindingsString,
bindingFunction = bindingCache.get(cacheKey);
+/*
if (!bindingFunction) {
// Build the source for a function that evaluates "expression"
// For each scope variable, add an extra level of "with" nesting
@@ -1631,6 +1632,23 @@ ko.bindingProvider = new class
return bindingFunction(bindingContext,
bindingContext["$root"], bindingContext["$parent"], bindingContext["$data"] || {}, node
);
+*/
+ if (!bindingFunction) {
+ // Build the source for a function that evaluates "expression"
+ // Use one "with" that has one secure scope handling Proxy
+ // Deprecated: with is no longer recommended
+ var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
+ functionBody = "$context = new Proxy(\
+ $context,\
+ {\
+ has: () => true,\
+ get: (target, key) => target[key] || target['$data'][key]\
+ }\
+ );with($context){return{" + rewrittenBindings + "}}";
+ bindingFunction = new Function("$context", functionBody);
+ bindingCache.set(cacheKey, bindingFunction);
+ }
+ return bindingFunction(bindingContext);
} catch (ex) {
ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString
+ "\nMessage: " + ex.message;
diff --git a/vendors/knockout/build/output/knockout-latest.js b/vendors/knockout/build/output/knockout-latest.js
index b9a34d87c..69370d21c 100644
--- a/vendors/knockout/build/output/knockout-latest.js
+++ b/vendors/knockout/build/output/knockout-latest.js
@@ -31,37 +31,38 @@ a.value=b):(c.g.l.set(a,c.i.options.Wa,b),a.__ko__hasDomDataOptionValue__=!0,a.v
l[k-1].match(b))&&!d[p[0]]&&(e=e.slice(e.indexOf(m)+1),l=e.match(a),k=-1,m="/"):40===p||123===p||91===p?++h:41===p||125===p||93===p?--h:n||f.length||34!==p&&39!==p||(m=m.slice(1,-1));f.push(m)}if(0$data");return g.join(",")},cc:(e,g)=>-1l.key==g),Ga:(e,g,l,f,h,k)=>{g&&c.W(g)?!c.vb(g)||k&&g.L()===h||g(h):(console.error(`"${f}" should be observable in ${e.outerHTML.replace(/>.+/,">")}`),l.get("$data")[f]=
h)}}})();(()=>{function a(f){return 8==f.nodeType&&e.test(f.nodeValue)}function b(f){return 8==f.nodeType&&g.test(f.nodeValue)}function d(f,h){for(var k=f,m=1,p=[];k=k.nextSibling;){if(b(k)&&(c.g.l.set(k,l,!0),!--m))return p;p.push(k);a(k)&&++m}if(!h)throw Error("Cannot find closing comment tag to match: "+f.nodeValue);return null}var e=/^\s*ko(?:\s+([\s\S]+))?\s*$/,g=/^\s*\/ko\s*$/,l="__ko_matchedEndComment__";c.m={aa:{},childNodes:f=>a(f)?d(f):f.childNodes,ja:f=>{a(f)?(f=d(f))&&[...f].forEach(h=>
c.removeNode(h)):c.g.Qa(f)},pa:(f,h)=>{a(f)?(c.m.ja(f),f.after(...h)):c.g.pa(f,h)},prepend:(f,h)=>{a(f)?f.nextSibling.before(h):f.prepend(h)},Ub:(f,h,k)=>{k?k.after(h):c.m.prepend(f,h)},firstChild:f=>{if(a(f))return f=f.nextSibling,!f||b(f)?null:f;let h=f.firstChild;if(h&&b(h))throw Error("Found invalid end comment, as the first child of "+f);return h},nextSibling:f=>{if(a(f)){var h=d(f,void 0);f=h?(h.length?h[h.length-1]:f).nextSibling:null}if((h=f.nextSibling)&&b(h)){if(b(h)&&!c.g.l.get(h,l))throw Error("Found end comment without a matching opening comment, as child of "+
-f);return null}return h},Qb:a,ac:f=>(f=f.nodeValue.match(e))?f[1]:null}})();const T=new Map;c.ob=new class{Xb(a){switch(a.nodeType){case 1:return null!=a.getAttribute("data-bind");case 8:return c.m.Qb(a)}return!1}Ob(a,b){a:{switch(a.nodeType){case 1:var d=a.getAttribute("data-bind");break a;case 8:d=c.m.ac(a);break a}d=null}if(d)try{let g=T.get(d);if(!g){var e="with($data){return{"+c.la.Yb(d)+"}}";g=new Function("$context","$root","$parent","$data","$element",e);T.set(d,g)}return g(b,b.$root,b.$parent,
-b.$data||{},a)}catch(g){throw g.message="Unable to parse bindings.\nBindings value: "+d+"\nMessage: "+g.message,g;}return null}};const G=Symbol("_subscribable"),H=Symbol("_ancestorBindingInfo"),U=Symbol("_dataDependency"),V={},I=c.g.l.Z();c.i={};c.ba=class{constructor(a,b,d,e){var g=this,l=a===V,f=l?void 0:a,h="function"==typeof f&&!c.W(f),k=e?.dataDependency;a=()=>{var p=h?f():f;p=c.g.h(p);b?(c.g.extend(g,b),H in b&&(g[H]=b[H])):g.$root=p;g[G]=m;l?p=g.$data:g.$data=p;d?.(g,b,p);if(b?.[G]&&!c.u.o().Sa(b[G]))b[G]();
-k&&(g[U]=k);return g.$data};if(e?.exportDependencies)a();else{var m=c.xb(a);m.L();m.isActive()?m.ka=null:g[G]=void 0}}createChildContext(a,b){return new c.ba(a,this,(d,e)=>{d.$parent=e.$data;b.extend?.(d)},b)}extend(a,b){return new c.ba(V,this,d=>c.g.extend(d,"function"==typeof a?a(d):a),b)}};const W=a=>{a=c.g.l.get(a,I);var b=a?.D;b&&(a.D=null,b.wb())};class ma{constructor(a,b,d){this.H=a;this.da=b;this.ta=new Set;this.F=!1;b.D||c.g.N.addDisposeCallback(a,W);d?.D&&(d.D.ta.add(a),this.za=d)}wb(){this.za?.D?.Mb(this.H)}Mb(a){this.ta.delete(a);
-this.ta.size||this.rb?.()}rb(){this.F=!0;this.da.D&&!this.ta.size&&(this.da.D=null,c.g.N.Ya(this.H,W),c.j.notify(this.H,c.j.ca),this.wb())}}c.j={F:"childrenComplete",ca:"descendantsComplete",subscribe:(a,b,d,e,g)=>{var l=c.g.l.Ra(a,I,{});l.wa||(l.wa=new c.P);g?.notifyImmediately&&l.Va[b]&&c.u.I(d,e,[a]);return l.wa.subscribe(d,e,b)},notify:(a,b)=>{var d=c.g.l.get(a,I);if(d&&(d.Va[b]=!0,d.wa?.B(a,b),b==c.j.F))if(d.D)d.D.rb();else if(void 0===d.D&&d.wa?.na(c.j.ca))throw Error("descendantsComplete event not supported for bindings on this node");
-},$a:(a,b)=>{var d=c.g.l.Ra(a,I,{});d.D||(d.D=new ma(a,d,b[H]));return b[H]==d?b:b.extend(e=>{e[H]=d})}};const Y=(a,b)=>{for(var d,e=c.m.firstChild(b);d=e;)e=c.m.nextSibling(d),X(a,d);c.j.notify(b,c.j.F)},X=(a,b)=>{var d=a;if(1===b.nodeType||c.ob.Xb(b))d=Z(b,null,a);d&&!b.matches?.("SCRIPT,TEXTAREA,TEMPLATE")&&Y(d,b)},na=a=>{var b=[],d={},e=[],g=l=>{if(!d[l]){var f=c.i[l];f&&(f.after&&(e.push(l),f.after.forEach(h=>{if(a[h]){if(e.includes(h))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+
-e.join(", "));g(h)}}),e.length--),b.push({key:l,ub:f}));d[l]=!0}};c.g.K(a,g);return b},Z=(a,b,d)=>{var e=c.g.l.Ra(a,I,{}),g=e.Hb;if(!b){if(g)throw Error("You cannot apply bindings multiple times to the same element.");e.Hb=!0}g||(e.context=d);e.Va||(e.Va={});if(b&&"function"!==typeof b)var l=b;else{var f=c.o(()=>{if(l=b?b(d,a):c.ob.Ob(a,d))d[G]?.(),d[U]?.();return l},{s:a});l&&f.isActive()||(f=null)}var h=d,k;if(l){var m=f?n=>()=>f()[n]():n=>l[n],p={get:n=>l[n]&&m(n)(),has:n=>n in l};c.j.F in l&&
-c.j.subscribe(a,c.j.F,()=>{var n=l[c.j.F]();if(n){var q=c.m.childNodes(a);q.length&&n(q,c.dataFor(q[0]))}});c.j.ca in l&&(h=c.j.$a(a,d),c.j.subscribe(a,c.j.ca,()=>{var n=l[c.j.ca]();n&&c.m.firstChild(a)&&n(a)}));na(l).forEach(n=>{var q=n.ub.init,r=n.ub.update,u=n.key;if(8===a.nodeType&&!c.m.aa[u])throw Error("The binding '"+u+"' cannot be used with comment nodes");try{"function"==typeof q&&c.u.I(()=>{var t=q(a,m(u),p,h.$data,h);if(t&&t.controlsDescendantBindings){if(void 0!==k)throw Error("Multiple bindings ("+
-k+" and "+u+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");k=u}}),"function"==typeof r&&c.o(()=>r(a,m(u),p,h.$data,h),{s:a})}catch(t){throw t.message='Unable to process binding "'+u+": "+l[u]+'"\nMessage: '+t.message,t;}})}return void 0===k&&h};c.$b=a=>c.g.l.get(a,I)?.context;const P=a=>a&&a instanceof c.ba?a:new c.ba(a);c.applyBindingAccessorsToNode=(a,b,d)=>Z(a,b,P(d));c.mb=(a,b)=>{1!==b.nodeType&&8!==b.nodeType||Y(P(a),
-b)};c.Ib=(a,b)=>X(P(a),b);c.dataFor=a=>([1,8].includes(a?.nodeType)&&c.$b(a))?.$data;c.U("bindingHandlers",c.i);(()=>{var a=Object.create(null),b=new Map;c.components={get:(l,f)=>{if(b.has(l))f(b.get(l));else{var h=a[l];h?h.subscribe(f):(h=a[l]=new c.P,h.subscribe(f),g(l,k=>{b.set(l,k);delete a[l];h.B(k)}))}},register:(l,f)=>{if(!f)throw Error("Invalid configuration for "+l);if(d[l])throw Error("Component "+l+" is already registered");d[l]=f}};var d=Object.create(null),e=(l,f)=>{throw Error(`Component '${l}': ${f}`);
-},g=(l,f)=>{var h={},k=d[l]||{},m=k.template;k=k.viewModel;if(m){m.element||e(l,"Unknown template value: "+m);m=m.element;var p=J.getElementById(m);p||e(l,"Cannot find element with ID "+m);p.matches("TEMPLATE")||e(l,"Template Source Element not a ");h.template=c.g.ua(p.content.childNodes)}k&&("function"!==typeof k.createViewModel&&e(l,"Unknown viewModel value: "+k),h.createViewModel=k.createViewModel);f(h.template&&h.createViewModel?h:null)}})();(()=>{var a=0;c.i.component={init:(b,d,e,
-g,l)=>{var f,h,k,m=()=>{var p=f&&f.dispose;"function"===typeof p&&p.call(f);k&&k.dispose();h=f=k=null};c.m.ja(b);c.g.N.addDisposeCallback(b,m);c.o(()=>{var p=c.g.h(d());if("string"!==typeof p){var n=c.g.h(p.params);p=c.g.h(p.name)}if(!p)throw Error("No component name specified");var q=c.j.$a(b,l),r=h=++a;c.components.get(p,u=>{if(h===r){m();if(!u)throw Error("Unknown component '"+p+"'");var t=u.template;if(!t)throw Error("Component '"+p+"' has no template");c.m.pa(b,c.g.ua(t));f=u.createViewModel(n,
-{element:b});c.mb(q.createChildContext(f,{}),b)}})},{s:b});return{controlsDescendantBindings:!0}}};c.m.aa.component=!0})();c.i.attr={update:(a,b)=>{b=c.g.h(b())||{};c.g.K(b,function(d,e){e=c.g.h(e);var g=d.indexOf(":");g="lookupNamespaceURI"in a&&0{c.i.checked={after:["value","attr"],init:function(a,b,d){var e="checkbox"==a.type,
-g="radio"==a.type;if(e||g){const n=c.xb(()=>{if(d.has("checkedValue"))return c.g.h(d.get("checkedValue"));if(m)return d.has("value")?c.g.h(d.get("value")):a.value});var l=()=>{if(!c.u.Ca()){var q=a.checked,r=n();if(q||!g&&!c.u.ma()){var u=c.u.I(b);if(h){var t=k?u.L():u,z=p;p=r;z!==r?q&&(t.push(r),t.remove(z)):q?t.push(r):t.remove(r);k&&c.vb(u)&&u(t)}else e&&(void 0===r?r=q:q||(r=void 0)),c.la.Ga(a,u,d,"checked",r,!0)}}},f=b(),h=e&&c.g.h(f)instanceof Array,k=!(h&&f.push&&f.splice),m=g||h,p=h?n():void 0;
-c.o(l,null,{s:a});a.addEventListener("click",l);c.o(()=>{var q=c.g.h(b()),r=n();h?(a.checked=q.includes(r),p=r):a.checked=e&&void 0===r?!!q:n()===q},null,{s:a});f=void 0}}};c.i.checkedValue={update:function(a,b){a.value=c.g.h(b())}}})();var Q=(a,b,d)=>b&&b.split(/\s+/).forEach(e=>a.classList.toggle(e,d));c.i.css={update:(a,b)=>{b=c.g.h(b());"object"==typeof b?c.g.K(b,(d,e)=>{e=c.g.h(e);Q(a,d,!!e)}):(b=c.g.Bb(b),Q(a,a.__ko__cssValue,!1),a.__ko__cssValue=b,Q(a,b,!0))}};c.i.enable={update:(a,b)=>{(b=
-c.g.h(b()))&&a.disabled?a.removeAttribute("disabled"):b||a.disabled||(a.disabled=!0)}};c.i.disable={update:(a,b)=>c.i.enable.update(a,()=>!c.g.h(b()))};c.i.event={init:function(a,b,d,e,g){c.g.K(b()||{},l=>{"string"==typeof l&&a.addEventListener(l,(...f)=>{var h=b()[l];if(h)try{e=g.$data;var k=h.apply(e,[e,...f])}finally{!0!==k&&f[0].preventDefault()}})})}};const aa=a=>()=>{var b=a(),d=c.W(b)?b.L():b;if(!d||Array.isArray(d))return{foreach:b};c.g.h(b);return{foreach:d.data}};c.i.foreach={init:(a,b)=>
-c.i.template.init(a,aa(b)),update:(a,b,d,e,g)=>c.i.template.update(a,aa(b),d,e,g)};c.m.aa.foreach=!0;c.i.hasfocus={init:(a,b,d)=>{var e=l=>{a.__ko_hasfocusUpdating=!0;l=a.ownerDocument.activeElement===a;c.la.Ga(a,b(),d,"hasfocus",l,!0);a.__ko_hasfocusLastValue=l;a.__ko_hasfocusUpdating=!1},g=e.bind(null,!0);e=e.bind(null,!1);a.addEventListener("focus",g);a.addEventListener("focusin",g);a.addEventListener("blur",e);a.addEventListener("focusout",e);a.__ko_hasfocusLastValue=!1},update:(a,b)=>{b=!!c.g.h(b());
-a.__ko_hasfocusUpdating||a.__ko_hasfocusLastValue===b||(b?a.focus():a.blur())}};c.i.html={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{c.g.Qa(a);b=c.g.h(b());if(null!=b){const d=J.createElement("template");d.innerHTML="string"!=typeof b?b.toString():b;a.appendChild(d.content)}}};(()=>{function a(b,d,e){c.i[b]={init:(g,l,f,h,k)=>{var m,p={};d&&(p={exportDependencies:!0});var n=f.has(c.j.ca);c.o(()=>{var q=c.g.h(l()),r=!e!==!q,u=!m;n&&(k=c.j.$a(g,k));if(r){p.dataDependency=c.u.o();var t=
-d?k.createChildContext("function"==typeof q?q:l,p):c.u.ma()?k.extend(null,p):k}u&&c.u.ma()&&(m=c.g.ua(c.m.childNodes(g),!0));r?(u||c.m.pa(g,c.g.ua(m)),c.mb(t,g)):(c.m.ja(g),c.j.notify(g,c.j.F))},{s:g});return{controlsDescendantBindings:!0}}};c.m.aa[b]=!0}a("if");a("ifnot",!1,!0);a("with",!0)})();var ba={};c.i.options={init:a=>{if(!a.matches("SELECT"))throw Error("options binding applies only to SELECT elements");let b=a.length;for(;b--;)a.remove(b);return{controlsDescendantBindings:!0}},update:(a,
-b,d)=>{var e=a.multiple,g=0!=a.length&&e?a.scrollTop:null,l=c.g.h(b()),f=[];b=()=>Array.from(a.options).filter(n=>n.selected);var h=(n,q,r)=>{var u=typeof q;return"function"==u?q(n):"string"==u?n[q]:r},k=(n,q)=>{f.length&&(n=f.includes(c.C.M(q[0])),q[0].selected=n,p&&!n&&c.u.I(c.g.Db,null,[a,"change"]))};e?f=b().map(c.C.M):0<=a.selectedIndex&&f.push(c.C.M(a.options[a.selectedIndex]));if(l){Array.isArray(l)||(l=[l]);var m=l.filter(n=>n??1)}var p=!1;l=k;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&
-(l=(n,q)=>{k(n,q);c.u.I(d.get("optionsAfterRender"),null,[q[0],n!==ba?n:void 0])});c.g.Ab(a,m,(n,q,r)=>{r.length&&(f=r[0].selected?[c.C.M(r[0])]:[],p=!0);q=a.ownerDocument.createElement("option");n===ba?(c.g.Za(q),c.C.Fa(q,void 0)):(r=h(n,d.get("optionsValue"),n),c.C.Fa(q,c.g.h(r)),n=h(n,d.get("optionsText"),r),c.g.Za(q,n));return[q]},{},l);m=f.length;(e?m&&b().length{c.g.K(c.g.h(b()||{}),(d,e)=>{e=c.g.h(e);if(null==e||!1===e)e="";if(/^--/.test(d))a.style.setProperty(d,e);else{d=d.replace(/-(\w)/g,(l,f)=>f.toUpperCase());var g=a.style[d];a.style[d]=e;e===g||a.style[d]!=g||isNaN(e)||(a.style[d]=e+"px")}})}};c.i.submit={init:(a,b,d,e,g)=>{if("function"!=typeof b())throw Error("The value for a submit binding must be a function");a.addEventListener("submit",
-l=>{var f=b();try{var h=f.call(g.$data,a)}finally{!0!==h&&l.preventDefault()}})}};c.i.text={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{8===a.nodeType&&(a.text||a.after(a.text=J.createTextNode("")),a=a.text);c.g.Za(a,b())}};c.m.aa.text=!0;c.i.textInput={init:(a,b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;a.checkValidity()&&e!==k&&(e=k,c.la.Ga(a,b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=
-a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var n=b(),q=c.C.M(a);c.la.Ga(a,n,d,"value",q)};f&&("string"==typeof f?l.add(f):f.forEach(n=>l.add(n)),l.delete("change"));l.forEach(n=>{var q=k;(n||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},n=n.slice(5));a.addEventListener(n,
-q)});var m=g&&"file"==a.type?()=>{var n=c.g.h(b());null==n||""===n?a.value="":c.u.I(k)}:()=>{var n=c.g.h(b()),q=c.C.M(a);if(null!==h&&n===h)setTimeout(m,0);else if(n!==q||void 0===q)e?(c.C.Fa(a,n),n!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,n)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")?m():k():(a.addEventListener("change",k),p=c.o(m,{s:a}))},null,{notifyImmediately:!0})}else a.addEventListener("change",k),c.o(m,{s:a})}else c.applyBindingAccessorsToNode(a,{checkedValue:b})},update:()=>
-{}};c.i.visible={update:(a,b)=>{b=c.g.h(b());var d="none"!=a.style.display;b&&!d?a.style.display="":d&&!b&&(a.style.display="none")}};c.i.hidden={update:(a,b)=>a.hidden=!!c.g.h(b())};(function(a){c.i[a]={init:function(b,d,e,g,l){return c.i.event.init.call(this,b,()=>({[a]:d()}),e,g,l)}}})("click");(()=>{let a=c.g.l.Z();class b{constructor(e){this.Na=e}Ua(...e){let g=this.Na;if(!e.length)return c.g.l.get(g,a)||(11===this.H?g.content:1===this.H?g:void 0);c.g.l.set(g,a,e[0])}}class d extends b{constructor(e){super(e);
-e&&(this.H=e.matches("TEMPLATE")&&e.content?e.content.nodeType:1)}}c.bb={Na:d,lb:b}})();(()=>{const a=(h,k,m)=>{var p;for(k=c.m.nextSibling(k);h&&(p=h)!==k;)h=c.m.nextSibling(p),m(p,h)},b=(h,k)=>{if(h.length){var m=h[0],p=m.parentNode;a(m,h[h.length-1],n=>(1===n.nodeType||8===n.nodeType)&&c.Ib(k,n));c.g.xa(h,p)}},d=(h,k,m,p)=>{var n=(h&&(h.nodeType?h:0c.W(h)?h():"function"===typeof h?h(k,m):h,g=(h,k,m,p)=>{m=m||{};if(p){var n=p.nodeType?p:0{var q=
-k instanceof c.ba?k:new c.ba(k,null,null,{exportDependencies:!0}),r=e(h,q.$data,q);d(p,!0,r,q,m)},{ia:()=>!n||!c.g.Pa(n),s:n})}console.log("no targetNodeOrNodeArray")},l=(h,k,m,p,n)=>{var q,r=(w,y)=>{q=n.createChildContext(w,{extend:B=>B.$index=y});w=e(h,w,q);return d(p,!1,w,q,m)},u=(w,y)=>{b(y,q);q=null},t=(w,y)=>{c.u.I(c.g.Ab,null,[p,w,r,m,u,y]);c.j.notify(p,c.j.F)};if(c.isObservableArray(k)){t(k.L());var z=k.subscribe(w=>{t(k(),w)},null,"arrayChange");z.s(p);return z}return c.o(()=>{var w=c.g.h(k)||
-[];Array.isArray(w)||(w=[w]);t(w)},{s:p})},f=c.g.l.Z();c.i.template={init:(h,k)=>{k=c.g.h(k());if("string"==typeof k||"name"in k)c.m.ja(h);else if(k=c.m.childNodes(h),k.length)k=c.g.Wb(k),(new c.bb.lb(h)).Ua(k);else throw Error("Anonymous template defined, but no template content was provided");return{controlsDescendantBindings:!0}},update:(h,k,m,p,n)=>{p=k();k=c.g.h(p);m=null;"string"==typeof k?k={}:p="name"in k?k.name:h;var q=!!p;"foreach"in k?m=l(p,q&&k.foreach||[],k,h,n):q?(m=n,"data"in k&&(m=
-n.createChildContext(k.data,{exportDependencies:!0})),m=g(p,m,k,h)):c.m.ja(h);n=m;c.g.l.get(h,f)?.dispose?.();c.g.l.set(h,f,!n||n.isActive&&!n.isActive()?void 0:n)}};c.m.aa.template=!0})();c.g.tb=(a,b,d)=>{var e=0,g,l=b.length;l&&a.every(f=>{g=b.findIndex(h=>f.value===h.value);0<=g&&(f.moved=b[g].index,b[g].moved=f.index,b.splice(g,1),e=g=0,--l);e+=l;return l&&(!d||e{var a=(b,d,e,g,l)=>{for(var f=Math.min,h=Math.max,k=[],m=-1,p=b.length,n,q=d.length,r=q-p||1,u=p+q+1,t,z,w;++m<=
-p;)for(z=t,k.push(t=[]),w=f(q,m+r),n=h(0,m-1);n<=w;n++)t[n]=n?m?b[m-1]===d[n-1]?z[n-1]:f(z[n]||u,t[n-1]||u)+1:n+1:m+1;f=[];h=[];r=[];m=p;for(n=q;m||n;)q=k[m][n]-1,n&&q===k[m][n-1]?h.push(f[f.length]={status:e,value:d[--n],index:n}):m&&q===k[m-1][n]?r.push(f[f.length]={status:g,value:b[--m],index:m}):(--n,--m,l.sparse||f.push({status:"retained",value:d[n]}));c.g.tb(r,h,10*p);return f.reverse()};return(b,d,e)=>{b=b||[];d=d||[];return b.length{function a(e,g,l,f,h){var k=[],m=c.o(()=>{var p=g(l,h,c.g.xa(k,e))||[];if(0r.insertBefore(u,q));n.forEach(u=>c.removeNode(u))}f&&c.u.I(f,null,[l,p,h])}k.length=0;k.push(...p)},{s:e,ia:()=>!!k.find(c.g.Pa)});return{O:k,La:m.isActive()?m:void 0}}var b=c.g.l.Z(),d=c.g.l.Z();c.g.Ab=(e,g,l,f,h,k)=>{g=g||[];Array.isArray(g)||(g=[g]);var m=c.g.l.get(e,b),p=[],n=0,q=0,r=[],u=[],t,z=v=>{t={sa:v,Ta:c.$(q++)};
-p.push(t)},w=v=>{t=m[v];t.Ta(q++);c.g.xa(t.O,e);p.push(t)};if(m){if(!k||m&&m._countWaitingForRemove)k=c.g.qb(Array.prototype.map.call(m,C=>C.sa),g,{sparse:!0});let v,F;for(k.forEach(C=>{v=C.moved;F=C.index;switch(C.status){case "deleted":for(;n{c.m.Ub(e,v,B);B=v};k=e.ownerDocument.activeElement;if(u.length)for(;null!=(g=u.shift());){for(t=p[g];g--;)if(f=p[g].O,f?.length){var B=f[f.length-1];break}t.O.forEach(y)}p.forEach(v=>{v.O||c.g.extend(v,a(e,l,v.sa,h,v.Ta));v.O.forEach(y);!v.Tb&&h&&(h(v.sa,v.O,v.Ta),v.Tb=!0,B=v.O[v.O.length-1])});e.ownerDocument.activeElement!=k&&k?.focus();[].forEach(v=>v&&(v.sa=d))}})();R.ko=M})(this);
+f);return null}return h},Qb:a,ac:f=>(f=f.nodeValue.match(e))?f[1]:null}})();const T=new Map;c.ob=new class{Xb(a){switch(a.nodeType){case 1:return null!=a.getAttribute("data-bind");case 8:return c.m.Qb(a)}return!1}Ob(a,b){a:{switch(a.nodeType){case 1:a=a.getAttribute("data-bind");break a;case 8:a=c.m.ac(a);break a}a=null}if(a)try{let e=T.get(a);if(!e){var d="$context = new Proxy( $context, { has: () => true, get: (target, key) => target[key] || target['$data'][key] } );with($context){return{"+
+c.la.Yb(a)+"}}";e=new Function("$context",d);T.set(a,e)}return e(b)}catch(e){throw e.message="Unable to parse bindings.\nBindings value: "+a+"\nMessage: "+e.message,e;}return null}};const G=Symbol("_subscribable"),H=Symbol("_ancestorBindingInfo"),U=Symbol("_dataDependency"),V={},I=c.g.l.Z();c.i={};c.ba=class{constructor(a,b,d,e){var g=this,l=a===V,f=l?void 0:a,h="function"==typeof f&&!c.W(f),k=e?.dataDependency;a=()=>{var p=h?f():f;p=c.g.h(p);b?(c.g.extend(g,b),H in b&&(g[H]=b[H])):g.$root=p;g[G]=
+m;l?p=g.$data:g.$data=p;d?.(g,b,p);if(b?.[G]&&!c.u.o().Sa(b[G]))b[G]();k&&(g[U]=k);return g.$data};if(e?.exportDependencies)a();else{var m=c.xb(a);m.L();m.isActive()?m.ka=null:g[G]=void 0}}createChildContext(a,b){return new c.ba(a,this,(d,e)=>{d.$parent=e.$data;b.extend?.(d)},b)}extend(a,b){return new c.ba(V,this,d=>c.g.extend(d,"function"==typeof a?a(d):a),b)}};const W=a=>{a=c.g.l.get(a,I);var b=a?.D;b&&(a.D=null,b.wb())};class ma{constructor(a,b,d){this.H=a;this.da=b;this.ta=new Set;this.F=!1;b.D||
+c.g.N.addDisposeCallback(a,W);d?.D&&(d.D.ta.add(a),this.za=d)}wb(){this.za?.D?.Mb(this.H)}Mb(a){this.ta.delete(a);this.ta.size||this.rb?.()}rb(){this.F=!0;this.da.D&&!this.ta.size&&(this.da.D=null,c.g.N.Ya(this.H,W),c.j.notify(this.H,c.j.ca),this.wb())}}c.j={F:"childrenComplete",ca:"descendantsComplete",subscribe:(a,b,d,e,g)=>{var l=c.g.l.Ra(a,I,{});l.wa||(l.wa=new c.P);g?.notifyImmediately&&l.Va[b]&&c.u.I(d,e,[a]);return l.wa.subscribe(d,e,b)},notify:(a,b)=>{var d=c.g.l.get(a,I);if(d&&(d.Va[b]=!0,
+d.wa?.B(a,b),b==c.j.F))if(d.D)d.D.rb();else if(void 0===d.D&&d.wa?.na(c.j.ca))throw Error("descendantsComplete event not supported for bindings on this node");},$a:(a,b)=>{var d=c.g.l.Ra(a,I,{});d.D||(d.D=new ma(a,d,b[H]));return b[H]==d?b:b.extend(e=>{e[H]=d})}};const Y=(a,b)=>{for(var d,e=c.m.firstChild(b);d=e;)e=c.m.nextSibling(d),X(a,d);c.j.notify(b,c.j.F)},X=(a,b)=>{var d=a;if(1===b.nodeType||c.ob.Xb(b))d=Z(b,null,a);d&&!b.matches?.("SCRIPT,TEXTAREA,TEMPLATE")&&Y(d,b)},na=a=>{var b=[],d={},e=
+[],g=l=>{if(!d[l]){var f=c.i[l];f&&(f.after&&(e.push(l),f.after.forEach(h=>{if(a[h]){if(e.includes(h))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+e.join(", "));g(h)}}),e.length--),b.push({key:l,ub:f}));d[l]=!0}};c.g.K(a,g);return b},Z=(a,b,d)=>{var e=c.g.l.Ra(a,I,{}),g=e.Hb;if(!b){if(g)throw Error("You cannot apply bindings multiple times to the same element.");e.Hb=!0}g||(e.context=d);e.Va||(e.Va={});if(b&&"function"!==typeof b)var l=b;else{var f=
+c.o(()=>{if(l=b?b(d,a):c.ob.Ob(a,d))d[G]?.(),d[U]?.();return l},{s:a});l&&f.isActive()||(f=null)}var h=d,k;if(l){var m=f?n=>()=>f()[n]():n=>l[n],p={get:n=>l[n]&&m(n)(),has:n=>n in l};c.j.F in l&&c.j.subscribe(a,c.j.F,()=>{var n=l[c.j.F]();if(n){var q=c.m.childNodes(a);q.length&&n(q,c.dataFor(q[0]))}});c.j.ca in l&&(h=c.j.$a(a,d),c.j.subscribe(a,c.j.ca,()=>{var n=l[c.j.ca]();n&&c.m.firstChild(a)&&n(a)}));na(l).forEach(n=>{var q=n.ub.init,r=n.ub.update,u=n.key;if(8===a.nodeType&&!c.m.aa[u])throw Error("The binding '"+
+u+"' cannot be used with comment nodes");try{"function"==typeof q&&c.u.I(()=>{var t=q(a,m(u),p,h.$data,h);if(t&&t.controlsDescendantBindings){if(void 0!==k)throw Error("Multiple bindings ("+k+" and "+u+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");k=u}}),"function"==typeof r&&c.o(()=>r(a,m(u),p,h.$data,h),{s:a})}catch(t){throw t.message='Unable to process binding "'+u+": "+l[u]+'"\nMessage: '+t.message,t;}})}return void 0===
+k&&h};c.$b=a=>c.g.l.get(a,I)?.context;const P=a=>a&&a instanceof c.ba?a:new c.ba(a);c.applyBindingAccessorsToNode=(a,b,d)=>Z(a,b,P(d));c.mb=(a,b)=>{1!==b.nodeType&&8!==b.nodeType||Y(P(a),b)};c.Ib=(a,b)=>X(P(a),b);c.dataFor=a=>([1,8].includes(a?.nodeType)&&c.$b(a))?.$data;c.U("bindingHandlers",c.i);(()=>{var a=Object.create(null),b=new Map;c.components={get:(l,f)=>{if(b.has(l))f(b.get(l));else{var h=a[l];h?h.subscribe(f):(h=a[l]=new c.P,h.subscribe(f),g(l,k=>{b.set(l,k);delete a[l];h.B(k)}))}},register:(l,
+f)=>{if(!f)throw Error("Invalid configuration for "+l);if(d[l])throw Error("Component "+l+" is already registered");d[l]=f}};var d=Object.create(null),e=(l,f)=>{throw Error(`Component '${l}': ${f}`);},g=(l,f)=>{var h={},k=d[l]||{},m=k.template;k=k.viewModel;if(m){m.element||e(l,"Unknown template value: "+m);m=m.element;var p=J.getElementById(m);p||e(l,"Cannot find element with ID "+m);p.matches("TEMPLATE")||e(l,"Template Source Element not a ");h.template=c.g.ua(p.content.childNodes)}k&&
+("function"!==typeof k.createViewModel&&e(l,"Unknown viewModel value: "+k),h.createViewModel=k.createViewModel);f(h.template&&h.createViewModel?h:null)}})();(()=>{var a=0;c.i.component={init:(b,d,e,g,l)=>{var f,h,k,m=()=>{var p=f&&f.dispose;"function"===typeof p&&p.call(f);k&&k.dispose();h=f=k=null};c.m.ja(b);c.g.N.addDisposeCallback(b,m);c.o(()=>{var p=c.g.h(d());if("string"!==typeof p){var n=c.g.h(p.params);p=c.g.h(p.name)}if(!p)throw Error("No component name specified");var q=c.j.$a(b,l),r=h=++a;
+c.components.get(p,u=>{if(h===r){m();if(!u)throw Error("Unknown component '"+p+"'");var t=u.template;if(!t)throw Error("Component '"+p+"' has no template");c.m.pa(b,c.g.ua(t));f=u.createViewModel(n,{element:b});c.mb(q.createChildContext(f,{}),b)}})},{s:b});return{controlsDescendantBindings:!0}}};c.m.aa.component=!0})();c.i.attr={update:(a,b)=>{b=c.g.h(b())||{};c.g.K(b,function(d,e){e=c.g.h(e);var g=d.indexOf(":");g="lookupNamespaceURI"in a&&0{c.i.checked={after:["value","attr"],init:function(a,b,d){var e="checkbox"==a.type,g="radio"==a.type;if(e||g){const n=c.xb(()=>{if(d.has("checkedValue"))return c.g.h(d.get("checkedValue"));if(m)return d.has("value")?c.g.h(d.get("value")):a.value});var l=()=>{if(!c.u.Ca()){var q=a.checked,r=n();if(q||!g&&!c.u.ma()){var u=c.u.I(b);if(h){var t=k?u.L():u,z=p;p=r;z!==r?q&&(t.push(r),
+t.remove(z)):q?t.push(r):t.remove(r);k&&c.vb(u)&&u(t)}else e&&(void 0===r?r=q:q||(r=void 0)),c.la.Ga(a,u,d,"checked",r,!0)}}},f=b(),h=e&&c.g.h(f)instanceof Array,k=!(h&&f.push&&f.splice),m=g||h,p=h?n():void 0;c.o(l,null,{s:a});a.addEventListener("click",l);c.o(()=>{var q=c.g.h(b()),r=n();h?(a.checked=q.includes(r),p=r):a.checked=e&&void 0===r?!!q:n()===q},null,{s:a});f=void 0}}};c.i.checkedValue={update:function(a,b){a.value=c.g.h(b())}}})();var Q=(a,b,d)=>b&&b.split(/\s+/).forEach(e=>a.classList.toggle(e,
+d));c.i.css={update:(a,b)=>{b=c.g.h(b());"object"==typeof b?c.g.K(b,(d,e)=>{e=c.g.h(e);Q(a,d,!!e)}):(b=c.g.Bb(b),Q(a,a.__ko__cssValue,!1),a.__ko__cssValue=b,Q(a,b,!0))}};c.i.enable={update:(a,b)=>{(b=c.g.h(b()))&&a.disabled?a.removeAttribute("disabled"):b||a.disabled||(a.disabled=!0)}};c.i.disable={update:(a,b)=>c.i.enable.update(a,()=>!c.g.h(b()))};c.i.event={init:function(a,b,d,e,g){c.g.K(b()||{},l=>{"string"==typeof l&&a.addEventListener(l,(...f)=>{var h=b()[l];if(h)try{e=g.$data;var k=h.apply(e,
+[e,...f])}finally{!0!==k&&f[0].preventDefault()}})})}};const aa=a=>()=>{var b=a(),d=c.W(b)?b.L():b;if(!d||Array.isArray(d))return{foreach:b};c.g.h(b);return{foreach:d.data}};c.i.foreach={init:(a,b)=>c.i.template.init(a,aa(b)),update:(a,b,d,e,g)=>c.i.template.update(a,aa(b),d,e,g)};c.m.aa.foreach=!0;c.i.hasfocus={init:(a,b,d)=>{var e=l=>{a.__ko_hasfocusUpdating=!0;l=a.ownerDocument.activeElement===a;c.la.Ga(a,b(),d,"hasfocus",l,!0);a.__ko_hasfocusLastValue=l;a.__ko_hasfocusUpdating=!1},g=e.bind(null,
+!0);e=e.bind(null,!1);a.addEventListener("focus",g);a.addEventListener("focusin",g);a.addEventListener("blur",e);a.addEventListener("focusout",e);a.__ko_hasfocusLastValue=!1},update:(a,b)=>{b=!!c.g.h(b());a.__ko_hasfocusUpdating||a.__ko_hasfocusLastValue===b||(b?a.focus():a.blur())}};c.i.html={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{c.g.Qa(a);b=c.g.h(b());if(null!=b){const d=J.createElement("template");d.innerHTML="string"!=typeof b?b.toString():b;a.appendChild(d.content)}}};(()=>
+{function a(b,d,e){c.i[b]={init:(g,l,f,h,k)=>{var m,p={};d&&(p={exportDependencies:!0});var n=f.has(c.j.ca);c.o(()=>{var q=c.g.h(l()),r=!e!==!q,u=!m;n&&(k=c.j.$a(g,k));if(r){p.dataDependency=c.u.o();var t=d?k.createChildContext("function"==typeof q?q:l,p):c.u.ma()?k.extend(null,p):k}u&&c.u.ma()&&(m=c.g.ua(c.m.childNodes(g),!0));r?(u||c.m.pa(g,c.g.ua(m)),c.mb(t,g)):(c.m.ja(g),c.j.notify(g,c.j.F))},{s:g});return{controlsDescendantBindings:!0}}};c.m.aa[b]=!0}a("if");a("ifnot",!1,!0);a("with",!0)})();
+var ba={};c.i.options={init:a=>{if(!a.matches("SELECT"))throw Error("options binding applies only to SELECT elements");let b=a.length;for(;b--;)a.remove(b);return{controlsDescendantBindings:!0}},update:(a,b,d)=>{var e=a.multiple,g=0!=a.length&&e?a.scrollTop:null,l=c.g.h(b()),f=[];b=()=>Array.from(a.options).filter(n=>n.selected);var h=(n,q,r)=>{var u=typeof q;return"function"==u?q(n):"string"==u?n[q]:r},k=(n,q)=>{f.length&&(n=f.includes(c.C.M(q[0])),q[0].selected=n,p&&!n&&c.u.I(c.g.Db,null,[a,"change"]))};
+e?f=b().map(c.C.M):0<=a.selectedIndex&&f.push(c.C.M(a.options[a.selectedIndex]));if(l){Array.isArray(l)||(l=[l]);var m=l.filter(n=>n??1)}var p=!1;l=k;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(l=(n,q)=>{k(n,q);c.u.I(d.get("optionsAfterRender"),null,[q[0],n!==ba?n:void 0])});c.g.Ab(a,m,(n,q,r)=>{r.length&&(f=r[0].selected?[c.C.M(r[0])]:[],p=!0);q=a.ownerDocument.createElement("option");n===ba?(c.g.Za(q),c.C.Fa(q,void 0)):(r=h(n,d.get("optionsValue"),n),c.C.Fa(q,c.g.h(r)),
+n=h(n,d.get("optionsText"),r),c.g.Za(q,n));return[q]},{},l);m=f.length;(e?m&&b().length{c.g.K(c.g.h(b()||{}),(d,e)=>{e=c.g.h(e);if(null==e||!1===e)e="";if(/^--/.test(d))a.style.setProperty(d,e);else{d=d.replace(/-(\w)/g,(l,f)=>f.toUpperCase());var g=
+a.style[d];a.style[d]=e;e===g||a.style[d]!=g||isNaN(e)||(a.style[d]=e+"px")}})}};c.i.submit={init:(a,b,d,e,g)=>{if("function"!=typeof b())throw Error("The value for a submit binding must be a function");a.addEventListener("submit",l=>{var f=b();try{var h=f.call(g.$data,a)}finally{!0!==h&&l.preventDefault()}})}};c.i.text={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{8===a.nodeType&&(a.text||a.after(a.text=J.createTextNode("")),a=a.text);c.g.Za(a,b())}};c.m.aa.text=!0;c.i.textInput={init:(a,
+b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;a.checkValidity()&&e!==k&&(e=k,c.la.Ga(a,b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var n=b(),q=c.C.M(a);
+c.la.Ga(a,n,d,"value",q)};f&&("string"==typeof f?l.add(f):f.forEach(n=>l.add(n)),l.delete("change"));l.forEach(n=>{var q=k;(n||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},n=n.slice(5));a.addEventListener(n,q)});var m=g&&"file"==a.type?()=>{var n=c.g.h(b());null==n||""===n?a.value="":c.u.I(k)}:()=>{var n=c.g.h(b()),q=c.C.M(a);if(null!==h&&n===h)setTimeout(m,0);else if(n!==q||void 0===q)e?(c.C.Fa(a,n),n!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,n)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")?
+m():k():(a.addEventListener("change",k),p=c.o(m,{s:a}))},null,{notifyImmediately:!0})}else a.addEventListener("change",k),c.o(m,{s:a})}else c.applyBindingAccessorsToNode(a,{checkedValue:b})},update:()=>{}};c.i.visible={update:(a,b)=>{b=c.g.h(b());var d="none"!=a.style.display;b&&!d?a.style.display="":d&&!b&&(a.style.display="none")}};c.i.hidden={update:(a,b)=>a.hidden=!!c.g.h(b())};(function(a){c.i[a]={init:function(b,d,e,g,l){return c.i.event.init.call(this,b,()=>({[a]:d()}),e,g,l)}}})("click");
+(()=>{let a=c.g.l.Z();class b{constructor(e){this.Na=e}Ua(...e){let g=this.Na;if(!e.length)return c.g.l.get(g,a)||(11===this.H?g.content:1===this.H?g:void 0);c.g.l.set(g,a,e[0])}}class d extends b{constructor(e){super(e);e&&(this.H=e.matches("TEMPLATE")&&e.content?e.content.nodeType:1)}}c.bb={Na:d,lb:b}})();(()=>{const a=(h,k,m)=>{var p;for(k=c.m.nextSibling(k);h&&(p=h)!==k;)h=c.m.nextSibling(p),m(p,h)},b=(h,k)=>{if(h.length){var m=h[0],p=m.parentNode;a(m,h[h.length-1],n=>(1===n.nodeType||8===n.nodeType)&&
+c.Ib(k,n));c.g.xa(h,p)}},d=(h,k,m,p)=>{var n=(h&&(h.nodeType?h:0c.W(h)?h():"function"===typeof h?h(k,m):h,g=(h,k,m,p)=>{m=m||{};if(p){var n=p.nodeType?p:0{var q=k instanceof c.ba?k:new c.ba(k,null,null,{exportDependencies:!0}),r=e(h,q.$data,q);d(p,!0,r,q,m)},{ia:()=>!n||!c.g.Pa(n),s:n})}console.log("no targetNodeOrNodeArray")},l=(h,k,m,p,n)=>{var q,r=(w,y)=>{q=n.createChildContext(w,{extend:B=>B.$index=y});w=e(h,w,q);return d(p,!1,w,q,m)},u=(w,y)=>{b(y,q);q=null},
+t=(w,y)=>{c.u.I(c.g.Ab,null,[p,w,r,m,u,y]);c.j.notify(p,c.j.F)};if(c.isObservableArray(k)){t(k.L());var z=k.subscribe(w=>{t(k(),w)},null,"arrayChange");z.s(p);return z}return c.o(()=>{var w=c.g.h(k)||[];Array.isArray(w)||(w=[w]);t(w)},{s:p})},f=c.g.l.Z();c.i.template={init:(h,k)=>{k=c.g.h(k());if("string"==typeof k||"name"in k)c.m.ja(h);else if(k=c.m.childNodes(h),k.length)k=c.g.Wb(k),(new c.bb.lb(h)).Ua(k);else throw Error("Anonymous template defined, but no template content was provided");return{controlsDescendantBindings:!0}},
+update:(h,k,m,p,n)=>{p=k();k=c.g.h(p);m=null;"string"==typeof k?k={}:p="name"in k?k.name:h;var q=!!p;"foreach"in k?m=l(p,q&&k.foreach||[],k,h,n):q?(m=n,"data"in k&&(m=n.createChildContext(k.data,{exportDependencies:!0})),m=g(p,m,k,h)):c.m.ja(h);n=m;c.g.l.get(h,f)?.dispose?.();c.g.l.set(h,f,!n||n.isActive&&!n.isActive()?void 0:n)}};c.m.aa.template=!0})();c.g.tb=(a,b,d)=>{var e=0,g,l=b.length;l&&a.every(f=>{g=b.findIndex(h=>f.value===h.value);0<=g&&(f.moved=b[g].index,b[g].moved=f.index,b.splice(g,
+1),e=g=0,--l);e+=l;return l&&(!d||e{var a=(b,d,e,g,l)=>{for(var f=Math.min,h=Math.max,k=[],m=-1,p=b.length,n,q=d.length,r=q-p||1,u=p+q+1,t,z,w;++m<=p;)for(z=t,k.push(t=[]),w=f(q,m+r),n=h(0,m-1);n<=w;n++)t[n]=n?m?b[m-1]===d[n-1]?z[n-1]:f(z[n]||u,t[n-1]||u)+1:n+1:m+1;f=[];h=[];r=[];m=p;for(n=q;m||n;)q=k[m][n]-1,n&&q===k[m][n-1]?h.push(f[f.length]={status:e,value:d[--n],index:n}):m&&q===k[m-1][n]?r.push(f[f.length]={status:g,value:b[--m],index:m}):(--n,--m,l.sparse||f.push({status:"retained",
+value:d[n]}));c.g.tb(r,h,10*p);return f.reverse()};return(b,d,e)=>{b=b||[];d=d||[];return b.length{function a(e,g,l,f,h){var k=[],m=c.o(()=>{var p=g(l,h,c.g.xa(k,e))||[];if(0r.insertBefore(u,q));n.forEach(u=>c.removeNode(u))}f&&c.u.I(f,null,[l,p,h])}k.length=0;k.push(...p)},{s:e,ia:()=>!!k.find(c.g.Pa)});return{O:k,La:m.isActive()?m:void 0}}
+var b=c.g.l.Z(),d=c.g.l.Z();c.g.Ab=(e,g,l,f,h,k)=>{g=g||[];Array.isArray(g)||(g=[g]);var m=c.g.l.get(e,b),p=[],n=0,q=0,r=[],u=[],t,z=v=>{t={sa:v,Ta:c.$(q++)};p.push(t)},w=v=>{t=m[v];t.Ta(q++);c.g.xa(t.O,e);p.push(t)};if(m){if(!k||m&&m._countWaitingForRemove)k=c.g.qb(Array.prototype.map.call(m,C=>C.sa),g,{sparse:!0});let v,F;for(k.forEach(C=>{v=C.moved;F=C.index;switch(C.status){case "deleted":for(;n{c.m.Ub(e,v,B);B=v};k=e.ownerDocument.activeElement;if(u.length)for(;null!=(g=u.shift());){for(t=p[g];g--;)if(f=p[g].O,f?.length){var B=f[f.length-1];break}t.O.forEach(y)}p.forEach(v=>{v.O||c.g.extend(v,a(e,l,v.sa,h,v.Ta));v.O.forEach(y);!v.Tb&&h&&(h(v.sa,v.O,v.Ta),v.Tb=!0,B=v.O[v.O.length-1])});
+e.ownerDocument.activeElement!=k&&k?.focus();[].forEach(v=>v&&(v.sa=d))}})();R.ko=M})(this);
diff --git a/vendors/knockout/src/binding/bindingProvider.js b/vendors/knockout/src/binding/bindingProvider.js
index eae03acca..bb39d7214 100644
--- a/vendors/knockout/src/binding/bindingProvider.js
+++ b/vendors/knockout/src/binding/bindingProvider.js
@@ -31,20 +31,12 @@ ko.bindingProvider = new class
try {
let cacheKey = bindingsString,
bindingFunction = bindingCache.get(cacheKey);
+/*
if (!bindingFunction) {
// Build the source for a function that evaluates "expression"
// For each scope variable, add an extra level of "with" nesting
// Example result: with(sc1) { with(sc0) { return (expression) } }
// Deprecated: with is no longer recommended
-/*
- functionBody = "$context = new Proxy(
- $context,
- {
- has:()=>true,
- get:(target,key)=>Reflect.has(target, key) ? target[key] : target['$data'][key]
- }
- );with($context){return{" + rewrittenBindings + "}}";
-*/
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
functionBody = "with($data){return{" + rewrittenBindings + "}}";
bindingFunction = new Function("$context", "$root", "$parent", "$data", "$element", functionBody);
@@ -53,6 +45,23 @@ ko.bindingProvider = new class
return bindingFunction(bindingContext,
bindingContext["$root"], bindingContext["$parent"], bindingContext["$data"] || {}, node
);
+*/
+ if (!bindingFunction) {
+ // Build the source for a function that evaluates "expression"
+ // Use one "with" that has one secure scope handling Proxy
+ // Deprecated: with is no longer recommended
+ var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
+ functionBody = "$context = new Proxy(\
+ $context,\
+ {\
+ has: () => true,\
+ get: (target, key) => target[key] || target['$data'][key]\
+ }\
+ );with($context){return{" + rewrittenBindings + "}}";
+ bindingFunction = new Function("$context", functionBody);
+ bindingCache.set(cacheKey, bindingFunction);
+ }
+ return bindingFunction(bindingContext);
} catch (ex) {
ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString
+ "\nMessage: " + ex.message;
From 16106a28d5a4902596b490a9e31885eb142608f7 Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Thu, 10 Oct 2024 23:45:28 +0200
Subject: [PATCH 31/45] Resolve #1798
---
dev/View/User/MailBox/MessageList.js | 12 ++++++++----
snappymail/v/0.0.0/app/localization/ar/user.json | 2 ++
snappymail/v/0.0.0/app/localization/be/user.json | 2 ++
snappymail/v/0.0.0/app/localization/bg/user.json | 2 ++
snappymail/v/0.0.0/app/localization/cs/user.json | 2 ++
snappymail/v/0.0.0/app/localization/da/user.json | 2 ++
snappymail/v/0.0.0/app/localization/de/user.json | 2 ++
snappymail/v/0.0.0/app/localization/el/user.json | 2 ++
snappymail/v/0.0.0/app/localization/en/user.json | 2 ++
snappymail/v/0.0.0/app/localization/es/user.json | 2 ++
snappymail/v/0.0.0/app/localization/et/user.json | 2 ++
snappymail/v/0.0.0/app/localization/eu/user.json | 2 ++
snappymail/v/0.0.0/app/localization/fa/user.json | 2 ++
snappymail/v/0.0.0/app/localization/fi/user.json | 2 ++
snappymail/v/0.0.0/app/localization/fr/user.json | 2 ++
snappymail/v/0.0.0/app/localization/hu/user.json | 2 ++
snappymail/v/0.0.0/app/localization/id/user.json | 2 ++
snappymail/v/0.0.0/app/localization/is/user.json | 2 ++
snappymail/v/0.0.0/app/localization/it/user.json | 2 ++
snappymail/v/0.0.0/app/localization/ja/user.json | 2 ++
snappymail/v/0.0.0/app/localization/ko/user.json | 2 ++
snappymail/v/0.0.0/app/localization/lt/user.json | 2 ++
snappymail/v/0.0.0/app/localization/lv/user.json | 2 ++
snappymail/v/0.0.0/app/localization/nb/user.json | 2 ++
snappymail/v/0.0.0/app/localization/nl/user.json | 2 ++
snappymail/v/0.0.0/app/localization/pl/user.json | 2 ++
snappymail/v/0.0.0/app/localization/pt-BR/user.json | 2 ++
snappymail/v/0.0.0/app/localization/pt/user.json | 2 ++
snappymail/v/0.0.0/app/localization/ro/user.json | 2 ++
snappymail/v/0.0.0/app/localization/ru/user.json | 2 ++
snappymail/v/0.0.0/app/localization/sk/user.json | 2 ++
snappymail/v/0.0.0/app/localization/sl/user.json | 2 ++
snappymail/v/0.0.0/app/localization/sv/user.json | 2 ++
snappymail/v/0.0.0/app/localization/tr/user.json | 2 ++
snappymail/v/0.0.0/app/localization/uk/user.json | 2 ++
snappymail/v/0.0.0/app/localization/vi/user.json | 2 ++
snappymail/v/0.0.0/app/localization/zh-TW/user.json | 2 ++
snappymail/v/0.0.0/app/localization/zh/user.json | 2 ++
.../app/templates/Views/User/MailMessageList.html | 6 ++++++
39 files changed, 88 insertions(+), 4 deletions(-)
diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js
index a55062be5..2517fe903 100644
--- a/dev/View/User/MailBox/MessageList.js
+++ b/dev/View/User/MailBox/MessageList.js
@@ -213,15 +213,19 @@ export class MailMessageList extends AbstractViewRight {
sortText: () => {
let mode = FolderUserStore.sortMode(),
- desc = '' === mode || mode.includes('REVERSE');
+ has = w => mode.includes(w),
+ desc = '' === mode || has('REVERSE');
mode = mode.split(/\s+/);
- if (mode.includes('FROM')) {
+ if (has('FROM')) {
return '@' + (desc ? '⬆' : '⬇');
}
- if (mode.includes('SUBJECT')) {
+ if (has('SUBJECT')) {
return '𝐒' + (desc ? '⬆' : '⬇');
}
- return (mode.includes('SIZE') ? '✉' : '📅') + (desc ? '⬇' : '⬆');
+ if (has('SIZE')) {
+ return '✉' + (desc ? '⬇' : '⬆');
+ }
+ return (has('ARRIVAL') ? '📨' : '📅') + (desc ? '⬇' : '⬆');
},
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
diff --git a/snappymail/v/0.0.0/app/localization/ar/user.json b/snappymail/v/0.0.0/app/localization/ar/user.json
index 676857698..513560340 100644
--- a/snappymail/v/0.0.0/app/localization/ar/user.json
+++ b/snappymail/v/0.0.0/app/localization/ar/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Using %SIZE% (%PROC%%)<\/strong> of your %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/be/user.json b/snappymail/v/0.0.0/app/localization/be/user.json
index 6e2fa9f5d..c56f6f005 100644
--- a/snappymail/v/0.0.0/app/localization/be/user.json
+++ b/snappymail/v/0.0.0/app/localization/be/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Выкарыстана %SIZE% (%PROC%%)<\/strong> з %LIMIT%<\/strong>",
"SORT": "Сартаванне",
"NO_SORT": "Паштовы сервер не падтрымлівае сартаванне",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Дата па ўзрастанні",
"SORT_DATE_DESC": "Дата па спаданні",
"SORT_SIZE_ASC": "Памер па ўзрастанні",
diff --git a/snappymail/v/0.0.0/app/localization/bg/user.json b/snappymail/v/0.0.0/app/localization/bg/user.json
index dbecf23d7..a64fedd65 100644
--- a/snappymail/v/0.0.0/app/localization/bg/user.json
+++ b/snappymail/v/0.0.0/app/localization/bg/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Използване на %SIZE% (%PROC%%)<\/strong> от вашите %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/cs/user.json b/snappymail/v/0.0.0/app/localization/cs/user.json
index 724217cc7..b42c93de7 100644
--- a/snappymail/v/0.0.0/app/localization/cs/user.json
+++ b/snappymail/v/0.0.0/app/localization/cs/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Používáte %SIZE% (%PROC%%)<\/strong> z vašich %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/da/user.json b/snappymail/v/0.0.0/app/localization/da/user.json
index bc246b57b..b2d247392 100644
--- a/snappymail/v/0.0.0/app/localization/da/user.json
+++ b/snappymail/v/0.0.0/app/localization/da/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Du bruger %SIZE% (%PROC%%)<\/strong> af %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/de/user.json b/snappymail/v/0.0.0/app/localization/de/user.json
index 3869a6065..fcc16a687 100644
--- a/snappymail/v/0.0.0/app/localization/de/user.json
+++ b/snappymail/v/0.0.0/app/localization/de/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Sie nutzen %SIZE% (%PROC%%)<\/strong> von %LIMIT%<\/strong>.",
"SORT": "Sortieren",
"NO_SORT": "Ihr Mailserver unterstützt das Sortieren von E-Mails nicht",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Datum aufsteigend",
"SORT_DATE_DESC": "Datum absteigend",
"SORT_SIZE_ASC": "Größe aufsteigend",
diff --git a/snappymail/v/0.0.0/app/localization/el/user.json b/snappymail/v/0.0.0/app/localization/el/user.json
index 0223b0029..983250946 100644
--- a/snappymail/v/0.0.0/app/localization/el/user.json
+++ b/snappymail/v/0.0.0/app/localization/el/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Χρησιμοποιούνται %SIZE% (%PROC%%)<\/strong> από %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/en/user.json b/snappymail/v/0.0.0/app/localization/en/user.json
index 2439964e3..ef8da1452 100644
--- a/snappymail/v/0.0.0/app/localization/en/user.json
+++ b/snappymail/v/0.0.0/app/localization/en/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Using %SIZE% (%PROC%%)<\/strong> of your %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/es/user.json b/snappymail/v/0.0.0/app/localization/es/user.json
index ef00f08a2..34768fc30 100644
--- a/snappymail/v/0.0.0/app/localization/es/user.json
+++ b/snappymail/v/0.0.0/app/localization/es/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Usando %SIZE% (%PROC%%)<\/strong> de sus %LIMIT%<\/strong>",
"SORT": "Ordenar",
"NO_SORT": "El servidor no soporta clasificación",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Fecha ascendente",
"SORT_DATE_DESC": "Fecha descendente",
"SORT_SIZE_ASC": "Tamaño ascendente",
diff --git a/snappymail/v/0.0.0/app/localization/et/user.json b/snappymail/v/0.0.0/app/localization/et/user.json
index c66d9cbbc..bcca7ee88 100644
--- a/snappymail/v/0.0.0/app/localization/et/user.json
+++ b/snappymail/v/0.0.0/app/localization/et/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Kasutad %SIZE% (%PROC%%)<\/strong> lubatud kettamahust %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/eu/user.json b/snappymail/v/0.0.0/app/localization/eu/user.json
index dca831e9f..ab073e58a 100644
--- a/snappymail/v/0.0.0/app/localization/eu/user.json
+++ b/snappymail/v/0.0.0/app/localization/eu/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "%SIZE% (%PROC%%)<\/strong> erabiliz zure %LIMIT%<\/strong>-tik",
"SORT": "Ordenatu",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Data goraka",
"SORT_DATE_DESC": "Data beheraka",
"SORT_SIZE_ASC": "Tamaina goraka",
diff --git a/snappymail/v/0.0.0/app/localization/fa/user.json b/snappymail/v/0.0.0/app/localization/fa/user.json
index 75592f471..75bf574bd 100644
--- a/snappymail/v/0.0.0/app/localization/fa/user.json
+++ b/snappymail/v/0.0.0/app/localization/fa/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "استفاده %SIZE% (%PROC%%)<\/strong> از%LIMIT%<\/strong> برای شما",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/fi/user.json b/snappymail/v/0.0.0/app/localization/fi/user.json
index f14ed8416..0a2f84b1f 100644
--- a/snappymail/v/0.0.0/app/localization/fi/user.json
+++ b/snappymail/v/0.0.0/app/localization/fi/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Käytät %SIZE% (%PROC%%)<\/strong> sinulle varatusta %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/fr/user.json b/snappymail/v/0.0.0/app/localization/fr/user.json
index 1df10a859..2caadef51 100644
--- a/snappymail/v/0.0.0/app/localization/fr/user.json
+++ b/snappymail/v/0.0.0/app/localization/fr/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Vous utilisez %SIZE% (%PROC%%)<\/strong> sur %LIMIT%<\/strong>",
"SORT": "Trier",
"NO_SORT": "Le serveur de messagerie ne prend pas en charge le tri",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascendante",
"SORT_DATE_DESC": "Date décroissante",
"SORT_SIZE_ASC": "Taille ascendante",
diff --git a/snappymail/v/0.0.0/app/localization/hu/user.json b/snappymail/v/0.0.0/app/localization/hu/user.json
index 94e4bf9f6..15d1f3d60 100644
--- a/snappymail/v/0.0.0/app/localization/hu/user.json
+++ b/snappymail/v/0.0.0/app/localization/hu/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Felhasználva %SIZE% (%PROC%%)<\/strong> ebből: %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/id/user.json b/snappymail/v/0.0.0/app/localization/id/user.json
index 4c02611c6..f789ddf95 100644
--- a/snappymail/v/0.0.0/app/localization/id/user.json
+++ b/snappymail/v/0.0.0/app/localization/id/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Menggunakan %SIZE% (%PROC%%)<\/strong> dari %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/is/user.json b/snappymail/v/0.0.0/app/localization/is/user.json
index bbf9b8da0..33e7384db 100644
--- a/snappymail/v/0.0.0/app/localization/is/user.json
+++ b/snappymail/v/0.0.0/app/localization/is/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Notar %SIZE% (%PROC%%)<\/strong> af %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/it/user.json b/snappymail/v/0.0.0/app/localization/it/user.json
index 3829b1444..92c89fb30 100644
--- a/snappymail/v/0.0.0/app/localization/it/user.json
+++ b/snappymail/v/0.0.0/app/localization/it/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Stai usando %SIZE% (%PROC%%)<\/strong> su %LIMIT%<\/strong>",
"SORT": "Riordina",
"NO_SORT": "Il mail server non supporta l'ordinamento",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Data crescente",
"SORT_DATE_DESC": "Data decrescente",
"SORT_SIZE_ASC": "Dimensione crescente",
diff --git a/snappymail/v/0.0.0/app/localization/ja/user.json b/snappymail/v/0.0.0/app/localization/ja/user.json
index 1bcab0ebc..6eb886d48 100644
--- a/snappymail/v/0.0.0/app/localization/ja/user.json
+++ b/snappymail/v/0.0.0/app/localization/ja/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "%SIZE% (%PROC%%)<\/strong>\/%LIMIT%<\/strong>を使用中",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "日付で昇順",
"SORT_DATE_DESC": "日付で降順",
"SORT_SIZE_ASC": "サイズで昇順",
diff --git a/snappymail/v/0.0.0/app/localization/ko/user.json b/snappymail/v/0.0.0/app/localization/ko/user.json
index 02b0bb0d5..fd50e6f09 100644
--- a/snappymail/v/0.0.0/app/localization/ko/user.json
+++ b/snappymail/v/0.0.0/app/localization/ko/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "총 %LIMIT%<\/strong> 중 %PROC%%<\/strong>을(를) 사용 중입니다.",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/lt/user.json b/snappymail/v/0.0.0/app/localization/lt/user.json
index 483b0a2b6..3e94f5ac8 100644
--- a/snappymail/v/0.0.0/app/localization/lt/user.json
+++ b/snappymail/v/0.0.0/app/localization/lt/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Naudojate %SIZE% (%PROC%%)<\/strong> iš savo turimų %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/lv/user.json b/snappymail/v/0.0.0/app/localization/lv/user.json
index 20ed39cc2..3c75f0eb2 100644
--- a/snappymail/v/0.0.0/app/localization/lv/user.json
+++ b/snappymail/v/0.0.0/app/localization/lv/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Izmantots %SIZE% (%PROC%%)<\/strong> no %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/nb/user.json b/snappymail/v/0.0.0/app/localization/nb/user.json
index fa14ab14b..dd2a2347b 100644
--- a/snappymail/v/0.0.0/app/localization/nb/user.json
+++ b/snappymail/v/0.0.0/app/localization/nb/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Bruker %SIZE% ( %PROC%% ) <\/strong> av %LIMIT% <\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Dato, stigende",
"SORT_DATE_DESC": "Dato, synkende",
"SORT_SIZE_ASC": "Størrelse, stigende",
diff --git a/snappymail/v/0.0.0/app/localization/nl/user.json b/snappymail/v/0.0.0/app/localization/nl/user.json
index 5c898ef85..27db22c00 100644
--- a/snappymail/v/0.0.0/app/localization/nl/user.json
+++ b/snappymail/v/0.0.0/app/localization/nl/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "U gebruikt %SIZE% (%PROC%%)<\/strong> van %LIMIT%<\/strong>",
"SORT": "Sorteren",
"NO_SORT": "Mail server ondersteund sorteren niet",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Datum oplopend",
"SORT_DATE_DESC": "Datum aflopend",
"SORT_SIZE_ASC": "Grootte oplopend",
diff --git a/snappymail/v/0.0.0/app/localization/pl/user.json b/snappymail/v/0.0.0/app/localization/pl/user.json
index b4a11b0af..f3c9c77da 100644
--- a/snappymail/v/0.0.0/app/localization/pl/user.json
+++ b/snappymail/v/0.0.0/app/localization/pl/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Wykorzystano %SIZE% (%PROC%%)<\/strong> z %LIMIT%<\/strong>",
"SORT": "Sortowanie",
"NO_SORT": "Serwer pocztowy nie wspiera sortowania",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Data rosnąco",
"SORT_DATE_DESC": "Data malejąco",
"SORT_SIZE_ASC": "Wielkość rosnąco",
diff --git a/snappymail/v/0.0.0/app/localization/pt-BR/user.json b/snappymail/v/0.0.0/app/localization/pt-BR/user.json
index 0ac96760e..c188a135a 100644
--- a/snappymail/v/0.0.0/app/localization/pt-BR/user.json
+++ b/snappymail/v/0.0.0/app/localization/pt-BR/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Utilizando %SIZE% (%PROC%%)<\/strong> de %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/pt/user.json b/snappymail/v/0.0.0/app/localization/pt/user.json
index da1ed6de6..a32437267 100644
--- a/snappymail/v/0.0.0/app/localization/pt/user.json
+++ b/snappymail/v/0.0.0/app/localization/pt/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Está a usar %SIZE% (%PROC%%)<\/strong> dos %LIMIT%<\/strong>",
"SORT": "Ordenar",
"NO_SORT": "O servidor de correio não suporta ordenação",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Data ascendente",
"SORT_DATE_DESC": "Data descendente",
"SORT_SIZE_ASC": "Tamanho ascendente",
diff --git a/snappymail/v/0.0.0/app/localization/ro/user.json b/snappymail/v/0.0.0/app/localization/ro/user.json
index e82b3080a..64491adc0 100644
--- a/snappymail/v/0.0.0/app/localization/ro/user.json
+++ b/snappymail/v/0.0.0/app/localization/ro/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Ați folosit %SIZE% (%PROC%%)<\/strong> din %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/ru/user.json b/snappymail/v/0.0.0/app/localization/ru/user.json
index e3bd2dfad..2a0a16c18 100644
--- a/snappymail/v/0.0.0/app/localization/ru/user.json
+++ b/snappymail/v/0.0.0/app/localization/ru/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Использовано %SIZE% (%PROC%%)<\/strong> из %LIMIT%<\/strong>",
"SORT": "Сортировка",
"NO_SORT": "Почтовый сервер не поддерживает сортировку",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Дата по возрастанию",
"SORT_DATE_DESC": "Дата по убыванию",
"SORT_SIZE_ASC": "Размер по возрастанию",
diff --git a/snappymail/v/0.0.0/app/localization/sk/user.json b/snappymail/v/0.0.0/app/localization/sk/user.json
index 520cfe073..aac2e35dd 100644
--- a/snappymail/v/0.0.0/app/localization/sk/user.json
+++ b/snappymail/v/0.0.0/app/localization/sk/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Používate %SIZE% (%PROC%%)<\/strong> z vašich %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/sl/user.json b/snappymail/v/0.0.0/app/localization/sl/user.json
index e1c9116f5..805ec438b 100644
--- a/snappymail/v/0.0.0/app/localization/sl/user.json
+++ b/snappymail/v/0.0.0/app/localization/sl/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Uporabljenih %SIZE% (%PROC%%)<\/strong> od %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/sv/user.json b/snappymail/v/0.0.0/app/localization/sv/user.json
index 07e071708..da15f2e46 100644
--- a/snappymail/v/0.0.0/app/localization/sv/user.json
+++ b/snappymail/v/0.0.0/app/localization/sv/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Använder %SIZE% (%PROC%%)<\/strong> av dina %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/tr/user.json b/snappymail/v/0.0.0/app/localization/tr/user.json
index dfa6a222e..46dd4bad4 100644
--- a/snappymail/v/0.0.0/app/localization/tr/user.json
+++ b/snappymail/v/0.0.0/app/localization/tr/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Size ayrılan %LIMIT%<\/strong>'dan %SIZE% (%PROC%%)<\/strong> kullanıyorsunuz",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/uk/user.json b/snappymail/v/0.0.0/app/localization/uk/user.json
index 2e5105f5b..666b635cd 100644
--- a/snappymail/v/0.0.0/app/localization/uk/user.json
+++ b/snappymail/v/0.0.0/app/localization/uk/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Використано %SIZE% (%PROC%%)<\/strong> з %LIMIT%<\/strong>",
"SORT": "Sort",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Date ascending",
"SORT_DATE_DESC": "Date descending",
"SORT_SIZE_ASC": "Size ascending",
diff --git a/snappymail/v/0.0.0/app/localization/vi/user.json b/snappymail/v/0.0.0/app/localization/vi/user.json
index 7ccc2f891..8f5481bbe 100644
--- a/snappymail/v/0.0.0/app/localization/vi/user.json
+++ b/snappymail/v/0.0.0/app/localization/vi/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "Đang sử dụng %SIZE% (%PROC%%)<\/strong> trong mức %LIMIT%<\/strong> của bạn",
"SORT": "Sắp xếp thư",
"NO_SORT": "Mail server does not support sorting",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Thư mới hiện trước",
"SORT_DATE_DESC": "Thư cũ hiện trước",
"SORT_SIZE_ASC": "Kích cỡ thư nhỏ đến lớn",
diff --git a/snappymail/v/0.0.0/app/localization/zh-TW/user.json b/snappymail/v/0.0.0/app/localization/zh-TW/user.json
index c6ef33ce9..7b77b3c51 100644
--- a/snappymail/v/0.0.0/app/localization/zh-TW/user.json
+++ b/snappymail/v/0.0.0/app/localization/zh-TW/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "已使用 %LIMIT%<\/strong> 中的 %SIZE% (%PROC%%)<\/strong> 儲存空間",
"SORT": "排序",
"NO_SORT": "郵件伺服器不支援排序",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "日期遞增",
"SORT_DATE_DESC": "日期遞減",
"SORT_SIZE_ASC": "大小遞增",
diff --git a/snappymail/v/0.0.0/app/localization/zh/user.json b/snappymail/v/0.0.0/app/localization/zh/user.json
index cb81edb72..fd3e8ce0a 100644
--- a/snappymail/v/0.0.0/app/localization/zh/user.json
+++ b/snappymail/v/0.0.0/app/localization/zh/user.json
@@ -130,6 +130,8 @@
"QUOTA_SIZE": "使用了 %LIMIT%<\/strong> 存储空间中的%SIZE% (%PROC%%)<\/strong>",
"SORT": "排序",
"NO_SORT": "邮件服务器不支持排序",
+ "SORT_ARRIVAL_ASC": "Arrived ascending",
+ "SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "日期升序",
"SORT_DATE_DESC": "日期降序",
"SORT_SIZE_ASC": "大小升序",
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html
index 9bc226a08..42521ef43 100644
--- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html
+++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html
@@ -107,6 +107,12 @@
+
+
+
+
+
+
From 055c0d3bf4680e531e71a6e1dc57f156672bf7ec Mon Sep 17 00:00:00 2001
From: the-djmaze <>
Date: Fri, 11 Oct 2024 00:28:05 +0200
Subject: [PATCH 32/45] Added setting for default messages sort mode
---
dev/Settings/User/General.js | 49 ++++++++++++++++++-
dev/Stores/User/Folder.js | 3 +-
dev/Stores/User/Settings.js | 2 +
.../libraries/RainLoop/Actions/Accounts.php | 2 +
.../app/libraries/RainLoop/Actions/User.php | 1 +
.../v/0.0.0/app/localization/nl/user.json | 4 +-
.../templates/Views/User/SettingsGeneral.html | 5 ++
7 files changed, 61 insertions(+), 5 deletions(-)
diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js
index a05a3227d..f06349c25 100644
--- a/dev/Settings/User/General.js
+++ b/dev/Settings/User/General.js
@@ -44,6 +44,51 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.desktopNotifications = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
+ const i18nSort = s => i18n('MESSAGE_LIST/SORT_'+s);
+ this.sortSupported = FolderUserStore.hasCapability('SORT');
+ this.sortOptions = [
+ {
+ value: '',
+ text: i18nSort('DATE_DESC')
+ },
+ {
+ value: 'DATE',
+ text: i18nSort('DATE_ASC')
+ },
+ {
+ value: 'FROM',
+ text: i18nSort('FROM_ASC')
+ },
+ {
+ value: 'REVERSE FROM',
+ text: i18nSort('FROM_DESC')
+ },
+ {
+ value: 'REVERSE SIZE',
+ text: i18nSort('SIZE_DESC')
+ },
+ {
+ value: 'SIZE',
+ text: i18nSort('SIZE_ASC')
+ },
+ {
+ value: 'SUBJECT',
+ text: i18nSort('SUBJECT_ASC')
+ },
+ {
+ value: 'REVERSE SUBJECT',
+ text: i18nSort('SUBJECT_DESC')
+ },
+ {
+ value: 'REVERSE ARRIVAL',
+ text: i18nSort('ARRIVAL_DESC')
+ },
+ {
+ value: 'ARRIVAL',
+ text: i18nSort('ARRIVAL_ASC')
+ }
+ ];
+
this.threadsAllowed = AppUserStore.threadsAllowed;
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
this.threadAlgorithms = ko.observableArray();
@@ -51,9 +96,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
);
this.threadAlgorithms.sort((a, b) => a.length - b.length);
- this.threadAlgorithm = SettingsUserStore.threadAlgorithm;
- ['useThreads', 'threadAlgorithm',
+ ['defaultSort', 'useThreads', 'threadAlgorithm',
// These use addSetting()
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
@@ -117,6 +161,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('CheckMailInterval');
this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel');
+ this.addSetting('defaultSort');
this.addSettings([
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
diff --git a/dev/Stores/User/Folder.js b/dev/Stores/User/Folder.js
index 37007c768..12983b373 100644
--- a/dev/Stores/User/Folder.js
+++ b/dev/Stores/User/Folder.js
@@ -7,6 +7,7 @@ import { forEachObjectEntry } from 'Common/Utils';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
import { Settings, SettingsCapa } from 'Common/Globals';
//import Remote from 'Remote/User/Fetch'; // Circular dependency
+import { SettingsUserStore } from 'Stores/User/Settings';
export const
@@ -59,7 +60,7 @@ FolderUserStore = new class {
*/
displaySpecSetting: false,
- sortMode: '',
+ sortMode: SettingsUserStore.defaultSort(),
quotaLimit: 0,
quotaUsage: 0,
diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js
index 382c74289..211a89288 100644
--- a/dev/Stores/User/Settings.js
+++ b/dev/Stores/User/Settings.js
@@ -29,6 +29,7 @@ export const SettingsUserStore = new class {
showNextMessage: 0,
allowDraftAutosave: 1,
useThreads: 0,
+ defaultSort: '',
threadAlgorithm: '',
replySameFolder: 0,
hideUnsubscribed: 0,
@@ -103,6 +104,7 @@ export const SettingsUserStore = new class {
'showNextMessage',
'AllowDraftAutosave',
'useThreads',
+ 'defaultSort',
'threadAlgorithm',
'ReplySameFolder',
'HideUnsubscribed',
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php
index 2a4e40bb0..4e78236e9 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php
@@ -210,6 +210,7 @@ trait Accounts
'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(),
'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
'HideUnsubscribed' => false,
+ 'defaultSort' => '',
'useThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false),
'threadAlgorithm' => '',
'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false),
@@ -226,6 +227,7 @@ trait Accounts
$aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', '');
$aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', '');
$aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']);
+ $aResult['defaultSort'] = (string) $oSettingsLocal->GetConf('defaultSort', $aResult['defaultSort']);
$aResult['useThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['useThreads']);
$aResult['threadAlgorithm'] = (string) $oSettingsLocal->GetConf('threadAlgorithm', $aResult['threadAlgorithm']);
$aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);
diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
index 014f403e7..f1a19c172 100644
--- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
+++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
@@ -204,6 +204,7 @@ trait User
$this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int');
$this->setSettingsFromParams($oSettings, 'Resizer5Height', 'int');
+ $this->setSettingsFromParams($oSettingsLocal, 'defaultSort', 'string');
$this->setSettingsFromParams($oSettingsLocal, 'UseThreads', 'bool');
$this->setSettingsFromParams($oSettingsLocal, 'threadAlgorithm', 'string');
$this->setSettingsFromParams($oSettingsLocal, 'ReplySameFolder', 'bool');
diff --git a/snappymail/v/0.0.0/app/localization/nl/user.json b/snappymail/v/0.0.0/app/localization/nl/user.json
index 27db22c00..e94df562b 100644
--- a/snappymail/v/0.0.0/app/localization/nl/user.json
+++ b/snappymail/v/0.0.0/app/localization/nl/user.json
@@ -130,8 +130,8 @@
"QUOTA_SIZE": "U gebruikt %SIZE% (%PROC%%)<\/strong> van %LIMIT%<\/strong>",
"SORT": "Sorteren",
"NO_SORT": "Mail server ondersteund sorteren niet",
- "SORT_ARRIVAL_ASC": "Arrived ascending",
- "SORT_ARRIVAL_DESC": "Arrived descending",
+ "SORT_ARRIVAL_ASC": "Ontvangen oplopend",
+ "SORT_ARRIVAL_DESC": "Ontvangen aflopend",
"SORT_DATE_ASC": "Datum oplopend",
"SORT_DATE_DESC": "Datum aflopend",
"SORT_SIZE_ASC": "Grootte oplopend",
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html
index 233149db3..4a81406b9 100644
--- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html
+++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html
@@ -65,6 +65,11 @@
+
+
+
+
+
%SIZE% (%PROC%%)<\/strong> sur %LIMIT%<\/strong>",
"SORT": "Trier",
"NO_SORT": "Le serveur de messagerie ne prend pas en charge le tri",
- "SORT_ARRIVAL_ASC": "Arrived ascending",
- "SORT_ARRIVAL_DESC": "Arrived descending",
+ "SORT_ARRIVAL_ASC": "Arrivée ascendante",
+ "SORT_ARRIVAL_DESC": "Arrivée décroissante",
"SORT_DATE_ASC": "Date ascendante",
"SORT_DATE_DESC": "Date décroissante",
"SORT_SIZE_ASC": "Taille ascendante",
From 838e73fa94d1d83d9cd8c03e017c1915c3ab3897 Mon Sep 17 00:00:00 2001
From: tinola
Date: Fri, 11 Oct 2024 07:43:08 +0200
Subject: [PATCH 34/45] Update user.json
Update Polish translation
---
snappymail/v/0.0.0/app/localization/pl/user.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/snappymail/v/0.0.0/app/localization/pl/user.json b/snappymail/v/0.0.0/app/localization/pl/user.json
index f3c9c77da..47969b3d1 100644
--- a/snappymail/v/0.0.0/app/localization/pl/user.json
+++ b/snappymail/v/0.0.0/app/localization/pl/user.json
@@ -130,8 +130,8 @@
"QUOTA_SIZE": "Wykorzystano %SIZE% (%PROC%%)<\/strong> z %LIMIT%<\/strong>",
"SORT": "Sortowanie",
"NO_SORT": "Serwer pocztowy nie wspiera sortowania",
- "SORT_ARRIVAL_ASC": "Arrived ascending",
- "SORT_ARRIVAL_DESC": "Arrived descending",
+ "SORT_ARRIVAL_ASC": "Odebrano rosnąco",
+ "SORT_ARRIVAL_DESC": "Odebrano malejąco",
"SORT_DATE_ASC": "Data rosnąco",
"SORT_DATE_DESC": "Data malejąco",
"SORT_SIZE_ASC": "Wielkość rosnąco",
From 5861124c2a92d8c99383ca707ebc83233f37c037 Mon Sep 17 00:00:00 2001
From: Roman Pereviznyk <62486104+romawkafor@users.noreply.github.com>
Date: Mon, 14 Oct 2024 13:50:31 +0300
Subject: [PATCH 35/45] Update user.json
I propose changes for the Ukrainian language, taking into account syntax and untranslated phrases.
In a short time, I will try to create and translate an admin page (admin.json) and 2-Step Verification (TOTP), which I will also offer
---
.../v/0.0.0/app/localization/uk/user.json | 484 +++++++++---------
1 file changed, 242 insertions(+), 242 deletions(-)
diff --git a/snappymail/v/0.0.0/app/localization/uk/user.json b/snappymail/v/0.0.0/app/localization/uk/user.json
index 666b635cd..54ab4a8d7 100644
--- a/snappymail/v/0.0.0/app/localization/uk/user.json
+++ b/snappymail/v/0.0.0/app/localization/uk/user.json
@@ -4,14 +4,14 @@
"TO_ARCHIVE": "Архівування",
"ARE_YOU_SURE": "Ви впевнені?",
"BACK": "Назад",
- "BCC": "Прихована",
+ "BCC": "Прихована копія",
"CANCEL": "Відмінити",
"CC": "Копія",
"CLEAR": "Очистити",
"CLOSE": "Закрити (Esc)",
"CONTACTS": "Контакти",
"DELETE": "Видалити",
- "UNDELETE": "Undelete",
+ "UNDELETE": "Відновити",
"DONE": "Зроблено",
"EMAIL": "Пошта",
"FROM": "Від",
@@ -20,26 +20,26 @@
"MORE": "Ще",
"MOVE_TO": "Перемістити",
"COPY_TO": "Копіювати в",
- "NAME": "Вхідні",
- "NONE": "None",
- "TO_SPAM": "Move message(s) to Spam",
+ "NAME": "Ім'я",
+ "NONE": "Нічого",
+ "TO_SPAM": "Перемістити повідомлення у спам",
"NOT_SPAM": "Не спам",
"USERNAME": "Ім'я користувача",
"PASSWORD": "Пароль",
- "REMEMBER": "Remember",
- "REPLY_TO": "Reply-To",
+ "REMEMBER": "Запам'ятати",
+ "REPLY_TO": "Відповідь сюди",
"SAVE": "Зберегти",
- "SAVE_CHANGES": "Save changes?",
- "SEARCH": "Контакти не знайдено",
+ "SAVE_CHANGES": "Зберегти зміни?",
+ "SEARCH": "Пошук",
"SPAM": "В спам",
"SUBJECT": "Тема",
"TEST": "Тест",
"TO": "Кому",
- "SERVER_MESSAGE": "Server message",
+ "SERVER_MESSAGE": "Повідомлення сервера",
"YES": "Так",
"NO": "Ні",
- "INACTIVE": "Inactive",
- "DEFAULT": "default",
+ "INACTIVE": "Не активне",
+ "DEFAULT": "за замовчуванням",
"HELP": "Допомога",
"TEXT": "Текст",
"ATTACHMENTS": "Файли"
@@ -60,47 +60,47 @@
"UNSEEN": "Непрочитані",
"DATE": "Дата",
"DATE_ALL": "За весь час",
- "BEFORE_3_DAYS": "Older then 3 days",
- "BEFORE_7_DAYS": "Older then 1 week",
- "BEFORE_MONTH": "Older then 1 month",
- "BEFORE_3_MONTHS": "Older then 3 months",
- "BEFORE_6_MONTHS": "Older then 6 months",
- "BEFORE_YEAR": "Older then 1 year",
+ "BEFORE_3_DAYS": "Старше 3 днів",
+ "BEFORE_7_DAYS": "Старше 1 тижня",
+ "BEFORE_MONTH": "Старше 1 місяця",
+ "BEFORE_3_MONTHS": "Старше 3 місяців",
+ "BEFORE_6_MONTHS": "Старше 6 місяців",
+ "BEFORE_YEAR": "Старше 1 року",
"SINCE_3_DAYS": "За три дні",
"SINCE_7_DAYS": "За тиждень",
"SINCE_MONTH": "За місяць",
"SINCE_3_MONTHS": "За 3 місяці",
"SINCE_6_MONTHS": "За півроку",
"SINCE_YEAR": "За рік",
- "SUBFOLDERS": "Subfolders",
- "SUBFOLDERS_NONE": "None",
- "SUBFOLDERS_SUBTREE": "All",
- "SUBFOLDERS_SUBTREE_ONE": "One level",
- "REPLIED": "відповів",
- "KEYWORD": "Tag"
+ "SUBFOLDERS": "Підпапки",
+ "SUBFOLDERS_NONE": "Нічого",
+ "SUBFOLDERS_SUBTREE": "Усі",
+ "SUBFOLDERS_SUBTREE_ONE": "Один рівень",
+ "REPLIED": "Відповів",
+ "KEYWORD": "Тег"
},
"FOLDER_LIST": {
"BUTTON_COMPOSE": "Написати",
- "BUTTON_NEW_MESSAGE": "New message",
+ "BUTTON_NEW_MESSAGE": "Новий лист",
"INBOX_NAME": "Вхідні",
"SENT_NAME": "Вихідні",
- "DRAFTS_NAME": "Чорновики",
+ "DRAFTS_NAME": "Чернетки",
"TRASH_NAME": "Кошик",
"ARCHIVE_NAME": "Архів"
},
"IMAP": {
- "ACL_RIGHTS": "Rights",
- "ACL_A": "Administer",
- "ACL_E": "Expunge",
- "ACL_I": "Insert messages",
- "ACL_K": "Create sub-folders",
- "ACL_L": "Show messages",
- "ACL_P": "Send mail",
- "ACL_R": "Select folder",
- "ACL_S": "\\SEEN tag",
- "ACL_T": "\\DELETED tag",
- "ACL_W": "Other tags",
- "ACL_X": "Delete\/Rename folder"
+ "ACL_RIGHTS": "Права",
+ "ACL_A": "Адмініструвати",
+ "ACL_E": "Видалити",
+ "ACL_I": "Вставте повідомлення",
+ "ACL_K": "Створити вкладені папки",
+ "ACL_L": "Показати повідомлення",
+ "ACL_P": "Надіслати лист",
+ "ACL_R": "Виберіть папку",
+ "ACL_S": "\\ПЕРЕГЛЯНУТИ тег",
+ "ACL_T": "\\ВИДАЛЕНО тег",
+ "ACL_W": "Інші теги",
+ "ACL_X": "Видалити\/Перейменувати папку"
},
"MESSAGE_LIST": {
"BUTTON_RELOAD": "Оновити список листів",
@@ -118,7 +118,7 @@
"MENU_SELECT_SEEN": "Прочитані",
"MENU_SELECT_FLAGGED": "Відмічені",
"MENU_SELECT_UNFLAGGED": "Без поміток",
- "DOWNLOAD_ALL_ATTACHMENTS": "Download all attachments",
+ "DOWNLOAD_ALL_ATTACHMENTS": "Завантажити всі вкладення",
"EMPTY_LIST": "В папці немає листів.",
"EMPTY_SEARCH_LIST": "Листи не знайдено.",
"SEARCH_RESULT_FOR": "Результат пошуку \"%SEARCH%\"",
@@ -128,22 +128,22 @@
"YESTERDAY_AT": "вчора в %TIME%",
"NEW_MESSAGE_NOTIFICATION": "У вас %COUNT% нових повідомлень!",
"QUOTA_SIZE": "Використано %SIZE% (%PROC%%)<\/strong> з %LIMIT%<\/strong>",
- "SORT": "Sort",
- "NO_SORT": "Mail server does not support sorting",
- "SORT_ARRIVAL_ASC": "Arrived ascending",
- "SORT_ARRIVAL_DESC": "Arrived descending",
- "SORT_DATE_ASC": "Date ascending",
- "SORT_DATE_DESC": "Date descending",
- "SORT_SIZE_ASC": "Size ascending",
- "SORT_SIZE_DESC": "Size descending",
- "SORT_SUBJECT_ASC": "Subject A-Z",
- "SORT_SUBJECT_DESC": "Subject Z-A",
- "SORT_FROM_ASC": "From A-Z",
- "SORT_FROM_DESC": "From Z-A"
+ "SORT": "Сортувати",
+ "NO_SORT": "Поштовий сервер не підтримує сортування",
+ "SORT_ARRIVAL_ASC": "Доставлено спершу старі",
+ "SORT_ARRIVAL_DESC": "Доставлено спершу нові",
+ "SORT_DATE_ASC": "Дата спершу старі",
+ "SORT_DATE_DESC": "Дата спершу нові",
+ "SORT_SIZE_ASC": "Розмір за зростанням",
+ "SORT_SIZE_DESC": "Розмір за спаданням",
+ "SORT_SUBJECT_ASC": "Тема А-Я",
+ "SORT_SUBJECT_DESC": "Тема Я-А",
+ "SORT_FROM_ASC": "Відправник А-Я",
+ "SORT_FROM_DESC": "Відправник Я-A"
},
"MESSAGE": {
"BUTTON_EDIT": "Редагувати",
- "BUTTON_UNSUBSCRIBE": "Unsubscribe from this list",
+ "BUTTON_UNSUBSCRIBE": "Відписатися",
"BUTTON_REPLY": "Відповісти на Листа",
"BUTTON_REPLY_ALL": "Відповісти Всім",
"BUTTON_FORWARD": "Переслати",
@@ -158,9 +158,9 @@
"BUTTON_THREAD_MORE": "ще листи",
"MENU_HEADERS": "Подивитися заголовки",
"MENU_VIEW_ORIGINAL": "Подивитися оригінал",
- "HTML_VIEW": "View HTML message",
- "PLAIN_VIEW": "View plain text message",
- "SWAP_COLORS": "Swap default (background)color",
+ "HTML_VIEW": "Переглянути HTML версію",
+ "PLAIN_VIEW": "Переглянути текстову версію",
+ "SWAP_COLORS": "Змінити стандартний (фоновий) колір",
"MENU_DOWNLOAD_ORIGINAL": "Завантажити як .eml файл",
"MENU_FILTER_SIMILAR": "Фільтрувати схожі",
"MENU_PRINT": "Надрукувати",
@@ -170,23 +170,23 @@
"LABEL_TO_SHORT": "кому",
"PRINT_LABEL_ATTACHMENTS": "Файли",
"MESSAGE_VIEW_DESC": "Виберіть повідомлення для перегляду.",
- "MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.",
+ "MESSAGE_VIEW_MOVE_DESC": "Клікніть на папку на лівій панелі, щоб вибрати місце призначення.",
"LINK_DOWNLOAD_AS_ZIP": "Завантажити як zip",
- "SPAM_SCORE": "Spam Score",
- "HAS_VIRUS_WARNING": "WARNING: virus detected",
- "TAGS": "Tags",
- "NEW_TAG": "New tag",
- "ENABLE_TRACKING_LINKS": "Enable tracking links",
- "TRACKING_ENABLED": "Links are now tracked when clicked!"
+ "SPAM_SCORE": "Оцінка спаму",
+ "HAS_VIRUS_WARNING": "ПОПЕРЕДЖЕННЯ: виявлено вірус",
+ "TAGS": "Теги",
+ "NEW_TAG": "Новий тег",
+ "ENABLE_TRACKING_LINKS": "Увімкнути відстеження посилань",
+ "TRACKING_ENABLED": "Посилання тепер відстежуються при натисканні!"
},
"MESSAGE_TAGS": {
- "$important": "Important",
- "$todo": "To Do",
- "$label1": "Important",
- "$label2": "Work",
- "$label3": "Personal",
- "$label4": "To Do",
- "$label5": "Later"
+ "$important": "Важливе",
+ "$todo": "Зробити",
+ "$label1": "Важливе",
+ "$label2": "Робота",
+ "$label3": "Особисте",
+ "$label4": "Зробити",
+ "$label5": "Пізніше"
},
"READ_RECEIPT": {
"SUBJECT": "Сповіщення про прочитання цього повідомлення - %SUBJECT%",
@@ -196,17 +196,17 @@
"SEARCHING_DESC": "Пошук..."
},
"CONTACTS": {
- "TAB_CONTACT": "Contact",
- "TAB_LOCATIONS": "Locations",
- "TAB_BUSINESS": "Business",
- "LABEL_ORGANIZATION": "Organization",
- "LABEL_DEPARTMENT": "Department",
- "LABEL_TITLE": "Title",
- "TAB_CRYPTO": "Crypto",
- "ASK": "Ask",
- "NEVER": "Never",
- "ALWAYS": "Always",
- "ALWAYS_IF_POSSIBLE": "Always if possible",
+ "TAB_CONTACT": "Контакт",
+ "TAB_LOCATIONS": "Розташування",
+ "TAB_BUSINESS": "Організація",
+ "LABEL_ORGANIZATION": "Компанія",
+ "LABEL_DEPARTMENT": "Департамент",
+ "LABEL_TITLE": "Назва",
+ "TAB_CRYPTO": "Шифрування",
+ "ASK": "Запитувати",
+ "NEVER": "Ніколи",
+ "ALWAYS": "Завжди",
+ "ALWAYS_IF_POSSIBLE": "Завжди якщо можливо",
"BUTTON_ADD_CONTACT": "Додати контакт",
"BUTTON_CREATE_CONTACT": "Зберегти",
"BUTTON_IMPORT": "Імпорт (csv, vcf, vCard)",
@@ -221,9 +221,9 @@
"DISPLAY_NAME": "Повне ім'я",
"LAST_NAME": "Ім'я",
"FIRST_NAME": "Прізвище",
- "MIDDLE_NAME": "Middle name",
- "NAME_PREFIX": "Name prefix",
- "NAME_SUFFIX": "Name suffix",
+ "MIDDLE_NAME": "По батькові",
+ "NAME_PREFIX": "Префікс імені",
+ "NAME_SUFFIX": "Суфікс імені",
"NICK_NAME": "Нік",
"LABEL_READ_ONLY": "Тільки читання",
"ADD_MENU_LABEL": "Додати",
@@ -236,34 +236,34 @@
"ADD_MENU_TAGS": "Теги",
"BUTTON_SHARE_ALL": "Всім",
"BUTTON_SYNC": "Синхронизація (CardDAV)",
- "SEND_TO_ALL_CONTACT_EMAILS": "Send to all contact emails"
+ "SEND_TO_ALL_CONTACT_EMAILS": "Надіслати на всі контактні email"
},
"COMPOSE": {
"LINK_SHOW_INPUTS": "Показати всі поля",
"BUTTON_SEND": "Надіслати",
- "BUTTON_MINIMIZE": "Minimize",
+ "BUTTON_MINIMIZE": "Згорнути",
"SAVED_TIME": "Збережено в %TIME%",
"SAVED_ERROR_ON_SEND": "Повідомлення було надіслано, але не збережено",
- "DISCARD_UNSAVED_DATA": "Discard unsaved data?",
+ "DISCARD_UNSAVED_DATA": "Видалити незбережені дані?",
"ATTACH_FILES": "Додати файли",
"ATTACH_DROP_FILES_DESC": "Помістити файли сюди",
"REPLY_MESSAGE_TITLE": "%DATETIME%, %EMAIL% написав",
"FORWARD_MESSAGE_TOP_TITLE": "-------- Переслане повідомлення -------",
"FORWARD_MESSAGE_TOP_SENT": "Надіслано",
"EMPTY_TO_ERROR_DESC": "Вкажіть як мінімум одного отримувача",
- "NO_ATTACHMENTS_HERE_DESC": "No attachments here.",
- "ATTACHMENTS_ERROR_DESC": "Warning! Not all attachments have been uploaded.",
- "ATTACHMENTS_UPLOAD_ERROR_DESC": "Not all attachments have been uploaded yet",
- "ALLOW_SPELLCHECK": "Enable browser spelling checker",
- "ALLOW_DRAFT_AUTOSAVE": "Automatically save draft",
+ "NO_ATTACHMENTS_HERE_DESC": "Тут немає вкладень.",
+ "ATTACHMENTS_ERROR_DESC": "УВАГА! Не всі вкладення завантажено.",
+ "ATTACHMENTS_UPLOAD_ERROR_DESC": "Ще не всі вкладені файли завантажено",
+ "ALLOW_SPELLCHECK": "Увімкнути перевірку правопису в браузері",
+ "ALLOW_DRAFT_AUTOSAVE": "Автоматично зберігати чернетку",
"BUTTON_REQUEST_READ_RECEIPT": "Запит про прочитання листа",
- "BUTTON_MARK_AS_IMPORTANT": "Mark as important",
- "BUTTON_REQUEST_DSN": "Request a delivery receipt",
- "BUTTON_REQUIRE_TLS": "Require secure delivery",
- "ERROR_EMPTY_BODY": "Message body is empty"
+ "BUTTON_MARK_AS_IMPORTANT": "Позначити як важливе",
+ "BUTTON_REQUEST_DSN": "Запитати звіт про доставку",
+ "BUTTON_REQUIRE_TLS": "Вимагати захищеної доставки",
+ "ERROR_EMPTY_BODY": "Повідомлення порожнє"
},
"POPUPS_ASK": {
- "EXIT_ARE_YOU_SURE": "Are you sure you want to exit?",
+ "EXIT_ARE_YOU_SURE": "Ви впевнені, що бажаєте вийти?",
"DESC_WANT_CLOSE_THIS_WINDOW": "Ви впевнені, що хочете закрити це вікно?",
"DESC_WANT_DELETE_MESSAGES": "Ви впевнені, що хочете видалити повідомлення?"
},
@@ -273,15 +273,15 @@
"POPUPS_ADD_ACCOUNT": {
"TITLE_ADD_ACCOUNT": "Додати ще акаунт",
"BUTTON_ADD_ACCOUNT": "Додати",
- "TITLE_UPDATE_ACCOUNT": "Update Account"
+ "TITLE_UPDATE_ACCOUNT": "Оновити акаунт"
},
"POPUPS_IDENTITY": {
- "TITLE_ADD_IDENTITY": "Додати Профіль",
- "TITLE_UPDATE_IDENTITY": "Оновити Профіль",
- "LABEL": "Label",
+ "TITLE_ADD_IDENTITY": "Додати відправника",
+ "TITLE_UPDATE_IDENTITY": "Оновити відправника",
+ "LABEL": "Внутрішня назва",
"BUTTON_ADD_IDENTITY": "Додати",
- "SIGNATURE": "Signature",
- "LABEL_SIGNATURE_INSERT_BEFORE": "Insert this signature before quoted text in replies"
+ "SIGNATURE": "Підпис",
+ "LABEL_SIGNATURE_INSERT_BEFORE": "Вставляти підпис перед цитованим текстом у відповідях"
},
"POPUPS_CREATE_FOLDER": {
"TITLE_CREATE_FOLDER": "Створити теку",
@@ -300,16 +300,16 @@
"SIGN": "Підпис",
"ENCRYPT": "Шифрування",
"DECRYPT": "Дешифрувати",
- "VERIFY": "Verify",
+ "VERIFY": "Перевірити",
"GOOD_SIGNATURE": "Підтверджена сигнатура для %USER%",
"ERROR": "%TYPE% помилка: %ERROR%",
- "CREATE_SELF_SIGNED": "Create self-signed",
- "VALID_UNTIL": "Valid until",
+ "CREATE_SELF_SIGNED": "Створити самопідписаний",
+ "VALID_UNTIL": "Дійсний до",
"PRIVATE_KEY": "Private key",
- "CHANGE_PASS": "Change passphrase",
- "CURRENT_PASS": "Current passphrase",
- "NEW_PASS": "New passphrase",
- "ASK_CRYPTKEY_PASS": "Your login passphrase has changed. Please fill in your old login passphrase to decrypt data"
+ "CHANGE_PASS": "Змінити пароль",
+ "CURRENT_PASS": "Поточна пароль",
+ "NEW_PASS": "Новий пароль",
+ "ASK_CRYPTKEY_PASS": "Ваш пароль змінено. Щоб розшифрувати дані, введіть старий пароль"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Імпорт OpenPGP ключа",
@@ -321,20 +321,20 @@
"LABEL_KEY_TYPE": "Type",
"SIGNED_MESSAGE": "OpenPGP підписане повідомлення",
"ENCRYPTED_MESSAGE": "OpenPGP шифроване повідомлення",
- "STORE_IN_GNUPG": "Store on server in GnuPG",
- "STORE_PUBLIC_KEY_IN_GNUPG": "Store public key on server in GnuPG",
- "STORE_PRIVATE_KEY_IN_GNUPG": "Store private key on server in GnuPG",
- "BACKUP_ON_SERVER": "Store (encrypted) on server",
- "BACKUP_PUBLIC_KEY_ON_SERVER": "Backup public key on server",
- "BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
+ "STORE_IN_GNUPG": "Зберігати на сервері в GnuPG",
+ "STORE_PUBLIC_KEY_IN_GNUPG": "Зберігайте public key на сервері в GnuPG",
+ "STORE_PRIVATE_KEY_IN_GNUPG": "Зберігайте private key на сервері в GnuPG",
+ "BACKUP_ON_SERVER": "Зберігати (зашифроване) на сервері",
+ "BACKUP_PUBLIC_KEY_ON_SERVER": "Бекап public key на сервері",
+ "BACKUP_PRIVATE_KEY_ON_SERVER": "Бекап private key на сервері"
},
"SMIME": {
- "POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
- "CERTIFICATE": "Certificate",
- "CERTIFICATES": "S\/MIME Certificates",
+ "POPUP_IMPORT_TITLE": "Імпорт S\/MIME сертифікат",
+ "CERTIFICATE": "Сертифікат",
+ "CERTIFICATES": "S\/MIME Сертифікати",
"SIGNED_MESSAGE": "S\/MIME підписане повідомлення",
"ENCRYPTED_MESSAGE": "S\/MIME шифроване повідомлення",
- "PRIVATE_KEY_OF": "S\/MIME private key of %EMAIL%"
+ "PRIVATE_KEY_OF": "S\/MIME private key з %EMAIL%"
},
"POPUPS_FILTER": {
"TITLE_CREATE_FILTER": "Створити фільтер",
@@ -349,26 +349,26 @@
"SELECT_ACTION_DISCARD": "Відклонити",
"SELECT_FIELD_RECIPIENTS": "Отримувачі (To or CC)",
"SELECT_FIELD_HEADER": "Заголовок",
- "SELECT_FIELD_BODY": "Body",
+ "SELECT_FIELD_BODY": "Тіло",
"SELECT_FIELD_SIZE": "Розмір",
"SELECT_TYPE_CONTAINS": "Містить",
"SELECT_TYPE_NOT_CONTAINS": "Не містить",
- "SELECT_TYPE_MATCHES": "Matches (* and ? supported)",
- "SELECT_TYPE_NOT_MATCHES": "Not Matches (* and ? supported)",
- "SELECT_TYPE_REGEXP": "Regexp",
- "SELECT_TYPE_NOT_REGEXP": "Not Regexp",
+ "SELECT_TYPE_MATCHES": "Збіги (* та ? підтримуються)",
+ "SELECT_TYPE_NOT_MATCHES": "Заперечення збігів (* та ? підтримуються)",
+ "SELECT_TYPE_REGEXP": "Регулярний вираз",
+ "SELECT_TYPE_NOT_REGEXP": "Не регулярний вираз",
"SELECT_TYPE_EQUAL_TO": "Дорівнює",
"SELECT_TYPE_NOT_EQUAL_TO": "Не дорівнює",
- "SELECT_TYPE_OVER": "Over",
- "SELECT_TYPE_UNDER": "Under",
- "SELECT_TYPE_TEXT": "Text",
+ "SELECT_TYPE_OVER": "Більше",
+ "SELECT_TYPE_UNDER": "Менше",
+ "SELECT_TYPE_TEXT": "Текст",
"SELECT_TYPE_RAW": "Raw",
- "SELECT_MATCH_ANY": "Matching any of the following rules",
- "SELECT_MATCH_ALL": "Matching all of the following rules",
- "MARK_AS_READ_LABEL": "Mark as read",
- "REPLY_INTERVAL_LABEL": "Reply interval (days)",
- "KEEP_LABEL": "Keep",
- "STOP_LABEL": "Stop further processing of rules",
+ "SELECT_MATCH_ANY": "Відповідає будь-якому з наступних правил",
+ "SELECT_MATCH_ALL": "Відповідає усім наступним правилам",
+ "MARK_AS_READ_LABEL": "Позначити як прочитане",
+ "REPLY_INTERVAL_LABEL": "Інтервал відповідей (днів)",
+ "KEEP_LABEL": "Тримати",
+ "STOP_LABEL": "Зупинити подальшу обробку правил",
"VACATION_SUBJECT_LABEL": "Тема (не обов'язково)",
"VACATION_MESSAGE_LABEL": "Повідомлення",
"VACATION_RECIPIENTS_LABEL": "Отримувачі (розділяти комами)",
@@ -376,12 +376,12 @@
"ALL_INCOMING_MESSAGES_DESC": "Всі повідомлення, що надійшли"
},
"POPUPS_SIEVE_SCRIPT": {
- "TITLE_CREATE": "Create Script",
- "TITLE_EDIT": "Edit Script",
- "BUTTON_ADD_FILTER": "Add a Filter",
- "BUTTON_RAW_SCRIPT": "Use Custom User Script",
- "CAPABILITY_LABEL": "Capabilities",
- "CHANGES_NEED_TO_BE_SAVED_DESC": "These changes need to be saved to the server."
+ "TITLE_CREATE": "Створити скрипт",
+ "TITLE_EDIT": "Редагувати скрипт",
+ "BUTTON_ADD_FILTER": "Додати фільтр",
+ "BUTTON_RAW_SCRIPT": "Використати користувацький скрипт",
+ "CAPABILITY_LABEL": "Можливості",
+ "CHANGES_NEED_TO_BE_SAVED_DESC": "Ці зміни потрібно зберегти на сервері."
},
"POPUPS_SYSTEM_FOLDERS": {
"TITLE_SYSTEM_FOLDERS": "Виберіть системні теки",
@@ -407,68 +407,68 @@
"ERROR_UNKNOWN": "Помилка завантаження файлу"
},
"EDITOR": {
- "PLAIN": "HTML <-> TEXT",
- "SOURCE": "HTML <-> SOURCE",
- "FONTFAMILY": "Font family",
- "FONTSIZE": "Font size",
- "DIR": "Text direction",
- "DIR_LTR": "Left to right",
- "DIR_RTL": "Right to left",
- "DIR_AUTO": "Automatic",
- "TEXTCOLOR": "Text color",
- "BACKGROUNDCOLOR": "Background color",
- "BOLD": "Bold",
- "ITALIC": "Italic",
- "UNDERLINE": "Underline",
- "STRIKE": "Strikethrough",
- "SUB": "Subscript",
- "SUP": "Superscript",
- "OL": "Ordered list",
- "UL": "Unordered list",
- "QUOTE": "Blockquote",
- "INDENTDECREASE": "Decrease indent",
- "INDENTINCREASE": "Increase indent",
- "LINK": "Link",
- "IMAGEURL": "Image URL",
- "IMAGEUPLOAD": "Image select",
- "UNDO": "Undo",
- "REDO": "Redo",
- "REMOVESTYLE": "Remove style"
+ "PLAIN": "HTML <-> ТЕКСТ",
+ "SOURCE": "HTML <-> ДЖЕРЕЛО",
+ "FONTFAMILY": "Сімейство шрифтів",
+ "FONTSIZE": "Розмір шрифту",
+ "DIR": "Напрямок тексту",
+ "DIR_LTR": "Зліва направо",
+ "DIR_RTL": "Справа наліво",
+ "DIR_AUTO": "Автоматично",
+ "TEXTCOLOR": "Колір тексту",
+ "BACKGROUNDCOLOR": "Колір фону",
+ "BOLD": "Жирний",
+ "ITALIC": "Курсив",
+ "UNDERLINE": "Підкреслений",
+ "STRIKE": "Закреслений",
+ "SUB": "Нижній індекс",
+ "SUP": "Верхній індекс",
+ "OL": "Упорядкований список",
+ "UL": "Невпорядкований список",
+ "QUOTE": "Блок цитати",
+ "INDENTDECREASE": "Зменшити відступ",
+ "INDENTINCREASE": "Збільшити відступ",
+ "LINK": "Посилання",
+ "IMAGEURL": "URL зображення",
+ "IMAGEUPLOAD": "Вибір зображення",
+ "UNDO": "Скасувати",
+ "REDO": "Повторити",
+ "REMOVESTYLE": "Видалити стиль"
},
"SETTINGS_LABELS": {
"GENERAL": "Основні",
"CONTACTS": "Контакти",
"FOLDERS": "Теки",
"ACCOUNTS": "Акаунти",
- "FILTERS": "Фильтры",
+ "FILTERS": "Фільтри",
"SECURITY": "Безпека",
"THEMES": "Теми"
},
"SETTINGS_FILTERS": {
- "SIMPLE": "Simple",
- "ADVANCED": "Advanced",
- "BUTTON_ADD_SCRIPT": "Add a Script",
- "SUBNAME_MOVE_TO": "Move to \"%FOLDER%\"",
- "SUBNAME_FORWARD_TO": "Forward to \"%EMAIL%\"",
- "SUBNAME_REJECT": "Reject",
- "SUBNAME_VACATION_MESSAGE": "Повідомлення про відпустку",
- "SUBNAME_DISCARD": "Відклонити"
+ "SIMPLE": "Прості",
+ "ADVANCED": "Розширені",
+ "BUTTON_ADD_SCRIPT": "Додати скрипт",
+ "SUBNAME_MOVE_TO": "Перемістити до \"%FOLDER%\"",
+ "SUBNAME_FORWARD_TO": "Переслати на \"%EMAIL%\"",
+ "SUBNAME_REJECT": "Відхилити",
+ "SUBNAME_VACATION_MESSAGE": "Повідомлення автовідповідача",
+ "SUBNAME_DISCARD": "Знищити"
},
"SETTINGS_SECURITY": {
"LEGEND_SECURITY": "Безпека",
"LABEL_AUTOLOGOUT": "Автовихід",
- "FORGET_KEY_PASS": "Forget private key passphrase",
+ "FORGET_KEY_PASS": "Забути пароль private key",
"NEVER": "Ніколи"
},
"SETTINGS_GENERAL": {
"LANGUAGE": "Мова",
- "HOUR_FORMAT": "Hour format",
- "IDENTITY": "Identity",
- "LAYOUT": "Layout",
- "LAYOUT_NO_SPLIT": "No Split",
- "LAYOUT_VERTICAL_SPLIT": "Vertical Split",
- "LAYOUT_HORIZONTAL_SPLIT": "Horizontal Split",
- "SHOW_NEXT_MESSAGE": "Show next message when (re)move current message",
+ "HOUR_FORMAT": "Формат часу",
+ "IDENTITY": "Профіль",
+ "LAYOUT": "Макет",
+ "LAYOUT_NO_SPLIT": "Список листів",
+ "LAYOUT_VERTICAL_SPLIT": "Список з вертикальним переглядом",
+ "LAYOUT_HORIZONTAL_SPLIT": "Список з горизонтальним переглядом",
+ "SHOW_NEXT_MESSAGE": "Перейти до наступного повідомлення після видалення\/переміщення поточного",
"EDITOR": "Редактор тексту за змовчуванням",
"EDITOR_HTML": "Форматований",
"EDITOR_PLAIN": "Простий",
@@ -477,35 +477,35 @@
"HIDE_DELETED_MESSAGES": "Приховати повідомлення, позначені для видалення",
"USE_CHECKBOXES_IN_LIST": "Показувати чекбокси у списках",
"USE_THREADS": "Згортати листи у листуваннях",
- "THREAD_ALGORITHM": "Threads algorithm",
+ "THREAD_ALGORITHM": "У вигляді розмови",
"REPLY_SAME_FOLDER": "Поміщати відповіді в ту ж теку, де знаходиться оригінал",
- "PREFER_HTML": "Prefer HTML to plain text",
- "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
- "REMOVE_COLORS": "Remove background and text colors from message body",
- "ALLOW_STYLES": "Allow | |