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 = ""; - 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