mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-07 08:27:03 +03:00
Merge branch 'master' of https://github.com/Murena-SAS/snappymail into dev/sync-nextcloud-addressbook
This commit is contained in:
commit
ae7e00859c
41 changed files with 3127 additions and 347 deletions
|
|
@ -61,9 +61,7 @@ import { AskPopupView } from 'View/Popup/Ask';
|
||||||
import {
|
import {
|
||||||
folderInformation,
|
folderInformation,
|
||||||
folderInformationMultiply,
|
folderInformationMultiply,
|
||||||
setRefreshFoldersInterval,
|
setRefreshFoldersInterval
|
||||||
messagesMoveHelper,
|
|
||||||
messagesDeleteHelper
|
|
||||||
} from 'Common/Folders';
|
} from 'Common/Folders';
|
||||||
import { loadFolders } from 'Model/FolderCollection';
|
import { loadFolders } from 'Model/FolderCollection';
|
||||||
|
|
||||||
|
|
@ -133,13 +131,11 @@ export class AppUser extends AbstractApp {
|
||||||
showScreenPopup(AskPopupView, [
|
showScreenPopup(AskPopupView, [
|
||||||
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
|
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
|
||||||
() => {
|
() => {
|
||||||
messagesDeleteHelper(sFromFolderFullName, oUids);
|
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids);
|
||||||
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids);
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
} else if (oMoveFolder) {
|
} else if (oMoveFolder) {
|
||||||
messagesMoveHelper(sFromFolderFullName, oMoveFolder.fullName, oUids);
|
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids, oMoveFolder.fullName);
|
||||||
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids, oMoveFolder.fullName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { isArray, arrayLength } from 'Common/Utils';
|
import { isArray, arrayLength } from 'Common/Utils';
|
||||||
import {
|
import {
|
||||||
setFolderETag,
|
|
||||||
getFolderInboxName,
|
getFolderInboxName,
|
||||||
getFolderFromCacheList
|
getFolderFromCacheList
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { getNotification } from 'Common/Translator';
|
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
|
@ -197,76 +195,6 @@ folderInformationMultiply = (boot = false) => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
moveOrDeleteResponseHelper = (iError, oData) => {
|
|
||||||
if (iError) {
|
|
||||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
|
||||||
alert(getNotification(iError));
|
|
||||||
} else if (FolderUserStore.currentFolder()) {
|
|
||||||
if (2 === arrayLength(oData.Result)) {
|
|
||||||
setFolderETag(oData.Result[0], oData.Result[1]);
|
|
||||||
} else {
|
|
||||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
|
||||||
}
|
|
||||||
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
|
|
||||||
const
|
|
||||||
sSpamFolder = FolderUserStore.spamFolder(),
|
|
||||||
isSpam = sSpamFolder === toFolderFullName,
|
|
||||||
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
|
||||||
|
|
||||||
Remote.abort('MessageList', 'reload').request('MessageMove',
|
|
||||||
moveOrDeleteResponseHelper,
|
|
||||||
{
|
|
||||||
fromFolder: fromFolderFullName,
|
|
||||||
toFolder: toFolderFullName,
|
|
||||||
uids: [...uidsForMove].join(','),
|
|
||||||
markAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
|
||||||
learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
|
||||||
Remote.abort('MessageList', 'reload').request('MessageDelete',
|
|
||||||
moveOrDeleteResponseHelper,
|
|
||||||
{
|
|
||||||
folder: sFromFolderFullName,
|
|
||||||
uids: [...aUidForRemove].join(',')
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} sFromFolderFullName
|
|
||||||
* @param {Set} oUids
|
|
||||||
* @param {string} sToFolderFullName
|
|
||||||
* @param {boolean=} bCopy = false
|
|
||||||
*/
|
|
||||||
moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) => {
|
|
||||||
if (sFromFolderFullName !== sToFolderFullName && oUids?.size) {
|
|
||||||
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
|
|
||||||
oToFolder = getFolderFromCacheList(sToFolderFullName);
|
|
||||||
|
|
||||||
if (oFromFolder && oToFolder) {
|
|
||||||
bCopy
|
|
||||||
? Remote.request('MessageCopy', null, {
|
|
||||||
fromFolder: oFromFolder.fullName,
|
|
||||||
toFolder: oToFolder.fullName,
|
|
||||||
uids: [...oUids].join(',')
|
|
||||||
})
|
|
||||||
: messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, oUids);
|
|
||||||
|
|
||||||
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, oUids, oToFolder.fullName, bCopy);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
dropFilesInFolder = (sFolderFullName, files) => {
|
dropFilesInFolder = (sFolderFullName, files) => {
|
||||||
let count = files.length;
|
let count = files.length;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const
|
||||||
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
|
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)/i,
|
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)$/i,
|
||||||
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
|
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
|
||||||
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
|
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
|
||||||
decode = decodeURIComponent,
|
decode = decodeURIComponent,
|
||||||
|
|
@ -99,7 +99,7 @@ const
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanCSS = source =>
|
cleanCSS = source =>
|
||||||
source.trim().replace(/-(ms|webkit)-[^;]+(;|$)/g, '')
|
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
|
||||||
.replace(/white-space[^;]+(;|$)/g, '')
|
.replace(/white-space[^;]+(;|$)/g, '')
|
||||||
// Drop Microsoft Office style properties
|
// Drop Microsoft Office style properties
|
||||||
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
||||||
|
|
@ -346,7 +346,7 @@ export const
|
||||||
let i = oElement.attributes.length;
|
let i = oElement.attributes.length;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
let sAttrName = oElement.attributes[i].name.toLowerCase();
|
let sAttrName = oElement.attributes[i].name.toLowerCase();
|
||||||
if (!allowedAttributes.includes(sAttrName)) {
|
if (!allowedAttributes.includes(sAttrName) && ('class' !== sAttrName || 'mail-body' !== className)) {
|
||||||
delAttribute(sAttrName);
|
delAttribute(sAttrName);
|
||||||
aAttrsForRemove.push(sAttrName);
|
aAttrsForRemove.push(sAttrName);
|
||||||
}
|
}
|
||||||
|
|
@ -542,7 +542,7 @@ export const
|
||||||
oStyle.removeProperty('color');
|
oStyle.removeProperty('color');
|
||||||
}
|
}
|
||||||
|
|
||||||
oStyle.cssText = cleanCSS(oStyle.cssText);
|
oStyle.cssText && (oStyle.cssText = cleanCSS(oStyle.cssText));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug && aAttrsForRemove.length) {
|
if (debug && aAttrsForRemove.length) {
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ export class Selector {
|
||||||
this.listChecked.subscribe(items => {
|
this.listChecked.subscribe(items => {
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
|
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
|
||||||
} else if (this.autoSelect()) {
|
} else {
|
||||||
koSelectedItem(koFocusedItem());
|
this.autoSelect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -174,9 +174,7 @@ export class Selector {
|
||||||
this.iFocusedNextHelper = 0;
|
this.iFocusedNextHelper = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.autoSelect() && !isChecked && !koSelectedItem()) {
|
!isChecked && !koSelectedItem() && this.autoSelect();
|
||||||
koSelectedItem(koFocusedItem());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aCheckedCache = [];
|
aCheckedCache = [];
|
||||||
|
|
@ -250,8 +248,10 @@ export class Selector {
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
autoSelect() {
|
autoSelect(bForce) {
|
||||||
return (this.oCallbacks.AutoSelect || (()=>1))() && this.focusedItem();
|
(bForce || (this.oCallbacks.AutoSelect || (()=>1))())
|
||||||
|
&& this.focusedItem()
|
||||||
|
&& this.selectedItem(this.focusedItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -327,9 +327,7 @@ export class Selector {
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this.focusedItem(result);
|
this.focusedItem(result);
|
||||||
if ((this.autoSelect() || bForceSelect) && !this.list.hasChecked()) {
|
!this.list.hasChecked() && this.autoSelect(bForceSelect);
|
||||||
this.selectedItem(result);
|
|
||||||
}
|
|
||||||
this.scrollToFocused();
|
this.scrollToFocused();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
dev/External/SquireUI.js
vendored
23
dev/External/SquireUI.js
vendored
|
|
@ -69,6 +69,8 @@ class SquireUI
|
||||||
|
|
||||||
clr.value = '';
|
clr.value = '';
|
||||||
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
||||||
|
// Chrome 110+ https://github.com/the-djmaze/snappymail/issues/1199
|
||||||
|
// clr.oninput = () => squire.setStyle({[name]:clr.value});
|
||||||
setTimeout(()=>clr.click(),1);
|
setTimeout(()=>clr.click(),1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -279,6 +281,27 @@ class SquireUI
|
||||||
|
|
||||||
clr.type = 'color';
|
clr.type = 'color';
|
||||||
toolbar.append(clr);
|
toolbar.append(clr);
|
||||||
|
// Chrome https://github.com/the-djmaze/snappymail/issues/1199
|
||||||
|
let clrid = 'squire-colors',
|
||||||
|
colorlist = doc.getElementById(clrid),
|
||||||
|
add = hex => colorlist.append(new Option(hex));
|
||||||
|
if (!colorlist) {
|
||||||
|
colorlist = createElement('datalist');
|
||||||
|
colorlist.id = clrid;
|
||||||
|
// Color blind safe Tableau 10 by Maureen Stone
|
||||||
|
add('#4E79A7');
|
||||||
|
add('#F28E2B');
|
||||||
|
add('#E15759');
|
||||||
|
add('#76B7B2');
|
||||||
|
add('#59A14F');
|
||||||
|
add('#EDC948');
|
||||||
|
add('#B07AA1');
|
||||||
|
add('#FF9DA7');
|
||||||
|
add('#9C755F');
|
||||||
|
add('#BAB0AC');
|
||||||
|
doc.body.append(colorlist);
|
||||||
|
}
|
||||||
|
clr.setAttribute('list', clrid);
|
||||||
|
|
||||||
browseImage.type = 'file';
|
browseImage.type = 'file';
|
||||||
browseImage.accept = 'image/*';
|
browseImage.accept = 'image/*';
|
||||||
|
|
|
||||||
6
dev/External/User/ko.js
vendored
6
dev/External/User/ko.js
vendored
|
|
@ -6,7 +6,7 @@ import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } fr
|
||||||
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
import { moveMessagesToFolder, dropFilesInFolder } from 'Common/Folders';
|
import { dropFilesInFolder } from 'Common/Folders';
|
||||||
import { setExpandedFolder } from 'Model/FolderCollection';
|
import { setExpandedFolder } from 'Model/FolderCollection';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
@ -63,7 +63,9 @@ const rlContentType = 'snappymail/action',
|
||||||
dragDrop = (e, element, folder, dragData) => {
|
dragDrop = (e, element, folder, dragData) => {
|
||||||
dragStop(e, element);
|
dragStop(e, element);
|
||||||
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
|
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
|
||||||
moveMessagesToFolder(FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey);
|
MessagelistUserStore.moveMessages(
|
||||||
|
FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey
|
||||||
|
);
|
||||||
} else if (e.dataTransfer.types.includes('Files')) {
|
} else if (e.dataTransfer.types.includes('Files')) {
|
||||||
dropFilesInFolder(folder.fullName, e.dataTransfer.files);
|
dropFilesInFolder(folder.fullName, e.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ body > * {
|
||||||
border: 1px solid rgba(125,128,128,0.5);
|
border: 1px solid rgba(125,128,128,0.5);
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
#attachments > *::before {
|
||||||
|
content: '📎 ';
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
import { keyScope, addShortcut, SettingsGet, leftPanelDisabled, toggleLeftPanel, elementById } from 'Common/Globals';
|
import { keyScope, addShortcut, SettingsGet, toggleLeftPanel, elementById } from 'Common/Globals';
|
||||||
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
|
@ -95,7 +95,6 @@ export class AbstractViewLeft extends AbstractView
|
||||||
constructor(templateID)
|
constructor(templateID)
|
||||||
{
|
{
|
||||||
super(templateID, 'left');
|
super(templateID, 'left');
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
|
||||||
this.toggleLeftPanel = toggleLeftPanel;
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ export class MessageModel extends AbstractModel {
|
||||||
clone.querySelectorAll('.sm-bq-switcher').forEach(
|
clone.querySelectorAll('.sm-bq-switcher').forEach(
|
||||||
node => node.replaceWith(node.lastElementChild)
|
node => node.replaceWith(node.lastElementChild)
|
||||||
);
|
);
|
||||||
return clone.innerHTML;
|
return (clone.querySelector('.mail-body') || clone).innerHTML;
|
||||||
}
|
}
|
||||||
let result = msgHtml(this);
|
let result = msgHtml(this);
|
||||||
return result.html || plainToHtml(this.plain());
|
return result.html || plainToHtml(this.plain());
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ abort = (sAction, sReason, bClearOnly) => {
|
||||||
oRequests[sAction] = null;
|
oRequests[sAction] = null;
|
||||||
if (controller) {
|
if (controller) {
|
||||||
clearTimeout(controller.timeoutId);
|
clearTimeout(controller.timeoutId);
|
||||||
bClearOnly || controller.abort(sReason || 'AbortError');
|
bClearOnly || controller.abort(new DOMException(sAction, sReason || 'AbortError'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -61,7 +61,6 @@ fetchJSON = (action, sUrl, params, timeout, jsonCallback) => {
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
clearTimeout(controller.timeoutId);
|
clearTimeout(controller.timeoutId);
|
||||||
err.aborted = signal.aborted;
|
err.aborted = signal.aborted;
|
||||||
err.reason = signal.reason;
|
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -165,7 +164,7 @@ export class AbstractFetchRemote
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error({fetchError:err});
|
console.error({fetchError:err});
|
||||||
fCallback && fCallback(
|
fCallback && fCallback(
|
||||||
'TimeoutError' == err.reason ? 3 : (err.name == 'AbortError' ? 2 : 1),
|
'TimeoutError' == err.name ? 3 : (err.name == 'AbortError' ? 2 : 1),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ let __themeTimer = 0;
|
||||||
export const
|
export const
|
||||||
// Also see Styles/_Values.less @maxMobileWidth
|
// Also see Styles/_Values.less @maxMobileWidth
|
||||||
isMobile = matchMedia('(max-width: 799px)'),
|
isMobile = matchMedia('(max-width: 799px)'),
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1150
|
||||||
|
// isSmall = matchMedia('(max-width: 1400px)'),
|
||||||
|
|
||||||
ThemeStore = {
|
ThemeStore = {
|
||||||
theme: ko.observable(''),
|
theme: ko.observable(''),
|
||||||
|
|
@ -104,6 +106,9 @@ addSubscribablesTo(ThemeStore, {
|
||||||
isMobile.onchange = e => {
|
isMobile.onchange = e => {
|
||||||
ThemeStore.isMobile(e.matches);
|
ThemeStore.isMobile(e.matches);
|
||||||
$htmlCL.toggle('rl-mobile', e.matches);
|
$htmlCL.toggle('rl-mobile', e.matches);
|
||||||
leftPanelDisabled(e.matches);
|
/*$htmlCL.contains('sm-msgView-side') || */leftPanelDisabled(e.matches);
|
||||||
};
|
};
|
||||||
isMobile.onchange(isMobile);
|
isMobile.onchange(isMobile);
|
||||||
|
|
||||||
|
//isSmall.onchange = e => $htmlCL.contains('sm-msgView-side') && leftPanelDisabled(e.matches);
|
||||||
|
//isSmall.onchange(isSmall);
|
||||||
|
|
|
||||||
|
|
@ -166,9 +166,9 @@ MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
MessagelistUserStore.canAutoSelect = () =>
|
MessagelistUserStore.canAutoSelect = () =>
|
||||||
!/is:unseen/.test(MessagelistUserStore.mainSearch())
|
!disableAutoSelect()
|
||||||
&& !disableAutoSelect()
|
|
||||||
&& SettingsUserStore.usePreviewPane();
|
&& SettingsUserStore.usePreviewPane();
|
||||||
|
// && !SettingsUserStore.showNextMessage();
|
||||||
|
|
||||||
let prevFolderName;
|
let prevFolderName;
|
||||||
|
|
||||||
|
|
@ -222,7 +222,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
||||||
fCallback = (iError, oData, bCached) => {
|
fCallback = (iError, oData, bCached) => {
|
||||||
let error = '';
|
let error = '';
|
||||||
if (iError) {
|
if (iError) {
|
||||||
if ('reload' != oData?.reason) {
|
if ('reload' != oData?.name) {
|
||||||
error = getNotification(iError);
|
error = getNotification(iError);
|
||||||
MessagelistUserStore.loading(false);
|
MessagelistUserStore.loading(false);
|
||||||
// if (Notifications.RequestAborted !== iError) {
|
// if (Notifications.RequestAborted !== iError) {
|
||||||
|
|
@ -399,53 +399,92 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
||||||
* @param {string=} toFolderFullName = ''
|
* @param {string=} toFolderFullName = ''
|
||||||
* @param {boolean=} copy = false
|
* @param {boolean=} copy = false
|
||||||
*/
|
*/
|
||||||
MessagelistUserStore.removeMessagesFromList = (
|
MessagelistUserStore.moveMessages = (
|
||||||
fromFolderFullName, oUids, toFolderFullName = '', copy = false
|
fromFolderFullName, oUids, toFolderFullName = '', copy = false
|
||||||
) => {
|
) => {
|
||||||
|
const fromFolder = getFolderFromCacheList(fromFolderFullName);
|
||||||
|
|
||||||
|
if (!fromFolder || !oUids?.size) return;
|
||||||
|
|
||||||
let unseenCount = 0,
|
let unseenCount = 0,
|
||||||
setPage = 0,
|
setPage = 0,
|
||||||
currentMessage = MessageUserStore.message();
|
currentMessage = MessageUserStore.message();
|
||||||
|
|
||||||
const trashFolder = FolderUserStore.trashFolder(),
|
const toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
|
||||||
|
trashFolder = FolderUserStore.trashFolder(),
|
||||||
spamFolder = FolderUserStore.spamFolder(),
|
spamFolder = FolderUserStore.spamFolder(),
|
||||||
fromFolder = getFolderFromCacheList(fromFolderFullName),
|
page = MessagelistUserStore.page(),
|
||||||
toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
|
|
||||||
messages =
|
messages =
|
||||||
FolderUserStore.currentFolderFullName() === fromFolderFullName
|
FolderUserStore.currentFolderFullName() === fromFolderFullName
|
||||||
? MessagelistUserStore.filter(item => item && oUids.has(item.uid))
|
? MessagelistUserStore.filter(item => item && oUids.has(item.uid))
|
||||||
: [];
|
: [],
|
||||||
|
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||||
|
if (iError) {
|
||||||
|
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||||
|
alert(getNotification(iError));
|
||||||
|
} else if (FolderUserStore.currentFolder()) {
|
||||||
|
if (2 === arrayLength(oData.Result)) {
|
||||||
|
setFolderETag(oData.Result[0], oData.Result[1]);
|
||||||
|
} else {
|
||||||
|
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
MessagelistUserStore.count(MessagelistUserStore.count() - oUids.size);
|
||||||
|
if (page > MessagelistUserStore.pageCount()) {
|
||||||
|
setPage = MessagelistUserStore.pageCount();
|
||||||
|
}
|
||||||
|
if (MessagelistUserStore.threadUid()
|
||||||
|
&& MessagelistUserStore.length
|
||||||
|
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
||||||
|
) {
|
||||||
|
const message = MessagelistUserStore.find(item => item && !item.deleted());
|
||||||
|
if (!message) {
|
||||||
|
if (1 < page) {
|
||||||
|
setPage = page - 1;
|
||||||
|
} else {
|
||||||
|
MessagelistUserStore.threadUid(0);
|
||||||
|
setPage = MessagelistUserStore.pageBeforeThread();
|
||||||
|
}
|
||||||
|
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
||||||
|
MessagelistUserStore.threadUid(message.uid);
|
||||||
|
setPage = page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setPage) {
|
||||||
|
MessagelistUserStore.page(setPage);
|
||||||
|
replaceHash(
|
||||||
|
mailBox(
|
||||||
|
FolderUserStore.currentFolderFullNameHash(),
|
||||||
|
setPage,
|
||||||
|
MessagelistUserStore.listSearch(),
|
||||||
|
MessagelistUserStore.threadUid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessagelistUserStore.reload(!MessagelistUserStore.count());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
messages.forEach(item => item?.isUnseen() && ++unseenCount);
|
messages.forEach(item => item?.isUnseen() && ++unseenCount);
|
||||||
|
|
||||||
if (fromFolder) {
|
if (!copy) {
|
||||||
fromFolder.etag = '';
|
fromFolder.etag = '';
|
||||||
if (!copy) {
|
fromFolder.totalEmails(Math.max(0, fromFolder.totalEmails() - oUids.size));
|
||||||
fromFolder.totalEmails(
|
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
|
||||||
0 <= fromFolder.totalEmails() - oUids.size ? fromFolder.totalEmails() - oUids.size : 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (0 < unseenCount) {
|
|
||||||
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toFolder) {
|
if (toFolder) {
|
||||||
toFolder.etag = '';
|
toFolder.etag = '';
|
||||||
|
|
||||||
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
|
|
||||||
unseenCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
toFolder.totalEmails(toFolder.totalEmails() + oUids.size);
|
toFolder.totalEmails(toFolder.totalEmails() + oUids.size);
|
||||||
if (0 < unseenCount) {
|
if (trashFolder !== toFolder.fullName && spamFolder !== toFolder.fullName) {
|
||||||
toFolder.unreadEmails(toFolder.unreadEmails() + unseenCount);
|
toFolder.unreadEmails(toFolder.unreadEmails() + unseenCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
toFolder.actionBlink(true);
|
toFolder.actionBlink(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length) {
|
if (messages.length) {
|
||||||
|
disableAutoSelect(true);
|
||||||
if (copy) {
|
if (copy) {
|
||||||
messages.forEach(item => item.checked(false));
|
messages.forEach(item => item.checked(false));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -468,49 +507,37 @@ MessagelistUserStore.removeMessagesFromList = (
|
||||||
currentMessage = null;
|
currentMessage = null;
|
||||||
MessageUserStore.message(null);
|
MessageUserStore.message(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
item.deleted(true);
|
item.deleted(true);
|
||||||
|
MessagelistUserStore.remove(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => messages.forEach(item => MessagelistUserStore.remove(item)), 350);
|
|
||||||
|
|
||||||
const
|
|
||||||
count = MessagelistUserStore.count() - messages.length,
|
|
||||||
page = MessagelistUserStore.page();
|
|
||||||
MessagelistUserStore.count(count);
|
|
||||||
if (page > MessagelistUserStore.pageCount()) {
|
|
||||||
setPage = MessagelistUserStore.pageCount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MessagelistUserStore.threadUid()
|
if (toFolderFullName) {
|
||||||
&& MessagelistUserStore.length
|
if (toFolder && fromFolderFullName != toFolderFullName) {
|
||||||
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
const params = {
|
||||||
) {
|
fromFolder: fromFolderFullName,
|
||||||
const message = MessagelistUserStore.find(item => item && !item.deleted());
|
toFolder: toFolderFullName,
|
||||||
if (!message) {
|
uids: [...oUids].join(',')
|
||||||
if (1 < MessagelistUserStore.page()) {
|
};
|
||||||
setPage = MessagelistUserStore.page() - 1;
|
if (copy) {
|
||||||
|
Remote.request('MessageCopy', null, params);
|
||||||
} else {
|
} else {
|
||||||
MessagelistUserStore.threadUid(0);
|
const
|
||||||
setPage = MessagelistUserStore.pageBeforeThread();
|
isSpam = spamFolder === toFolderFullName,
|
||||||
|
isHam = !isSpam && spamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
||||||
|
params.markAsRead = (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0;
|
||||||
|
params.learning = isSpam ? 'SPAM' : isHam ? 'HAM' : '';
|
||||||
|
Remote.abort('MessageList', 'reload').request('MessageMove', moveOrDeleteResponseHelper, params);
|
||||||
}
|
}
|
||||||
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
|
||||||
MessagelistUserStore.threadUid(message.uid);
|
|
||||||
setPage = MessagelistUserStore.page();
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
Remote.abort('MessageList', 'reload').request('MessageDelete',
|
||||||
if (setPage) {
|
moveOrDeleteResponseHelper,
|
||||||
MessagelistUserStore.page(setPage);
|
{
|
||||||
replaceHash(
|
folder: fromFolderFullName,
|
||||||
mailBox(
|
uids: [...oUids].join(',')
|
||||||
FolderUserStore.currentFolderFullNameHash(),
|
}
|
||||||
setPage,
|
|
||||||
MessagelistUserStore.listSearch(),
|
|
||||||
MessagelistUserStore.threadUid()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ html.list-loading body {
|
||||||
cursor: progress;
|
cursor: progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleLeft::before {
|
||||||
|
content: '❮';
|
||||||
|
}
|
||||||
|
html.rl-left-panel-disabled .toggleLeft::before {
|
||||||
|
content: '❯';
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||||
#rl-app {
|
#rl-app {
|
||||||
background-image: var(--main-bg-image);
|
background-image: var(--main-bg-image);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
.b-toolbar {
|
.b-toolbar {
|
||||||
padding: 10px 0 10px @rlLowMargin;
|
padding: 10px 0 10px @rlLowMargin;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +190,7 @@ html.rl-left-panel-disabled {
|
||||||
|
|
||||||
/* desktop */
|
/* desktop */
|
||||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||||
.toggleLeft,
|
#rl-right .toggleLeft,
|
||||||
#V-MailMessageList .buttonCompose {
|
#V-MailMessageList .buttonCompose {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { AbstractViewRight } from 'Knoin/AbstractViews';
|
import { AbstractViewRight } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
|
import { toggleLeftPanel } from 'Common/Globals';
|
||||||
|
|
||||||
export class PaneSettingsAdminView extends AbstractViewRight {
|
export class PaneSettingsAdminView extends AbstractViewRight {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('AdminPane');
|
super('AdminPane');
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
|
||||||
this.toggleLeftPanel = toggleLeftPanel;
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
|
||||||
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
import { folderInformation, messagesDeleteHelper } from 'Common/Folders';
|
import { folderInformation } from 'Common/Folders';
|
||||||
import { serverRequest } from 'Common/Links';
|
import { serverRequest } from 'Common/Links';
|
||||||
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
|
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
|
||||||
import { setFolderETag } from 'Common/Cache';
|
import { setFolderETag } from 'Common/Cache';
|
||||||
|
|
@ -586,8 +586,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
const
|
const
|
||||||
sFromFolderFullName = this.draftsFolder(),
|
sFromFolderFullName = this.draftsFolder(),
|
||||||
oUids = new Set([this.draftUid()]);
|
oUids = new Set([this.draftUid()]);
|
||||||
messagesDeleteHelper(sFromFolderFullName, oUids);
|
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids);
|
||||||
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids);
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||||
import { ComposePopupView } from 'View/Popup/Compose';
|
import { ComposePopupView } from 'View/Popup/Compose';
|
||||||
|
|
||||||
import { moveMessagesToFolder } from 'Common/Folders';
|
|
||||||
|
|
||||||
import { setExpandedFolder, foldersFilter } from 'Model/FolderCollection';
|
import { setExpandedFolder, foldersFilter } from 'Model/FolderCollection';
|
||||||
|
|
||||||
export class MailFolderList extends AbstractViewLeft {
|
export class MailFolderList extends AbstractViewLeft {
|
||||||
|
|
@ -92,7 +90,7 @@ export class MailFolderList extends AbstractViewLeft {
|
||||||
if (folder) {
|
if (folder) {
|
||||||
if (moveAction()) {
|
if (moveAction()) {
|
||||||
moveAction(false);
|
moveAction(false);
|
||||||
moveMessagesToFolder(
|
MessagelistUserStore.moveMessages(
|
||||||
FolderUserStore.currentFolderFullName(),
|
FolderUserStore.currentFolderFullName(),
|
||||||
MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
|
MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
|
||||||
folder.fullName,
|
folder.fullName,
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,6 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
this.composeInEdit = ComposePopupView.inEdit;
|
this.composeInEdit = ComposePopupView.inEdit;
|
||||||
|
|
||||||
this.isMobile = ThemeStore.isMobile; // Obsolete
|
this.isMobile = ThemeStore.isMobile; // Obsolete
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
|
||||||
this.toggleLeftPanel = toggleLeftPanel;
|
|
||||||
|
|
||||||
this.popupVisibility = arePopupsVisible;
|
this.popupVisibility = arePopupsVisible;
|
||||||
|
|
||||||
|
|
@ -582,7 +580,11 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
|
|
||||||
addEventsListeners(dom, {
|
addEventsListeners(dom, {
|
||||||
click: event => {
|
click: event => {
|
||||||
ThemeStore.isMobile() && !eqs(event, '.toggleLeft') && leftPanelDisabled(true);
|
if (eqs(event, '.toggleLeft')) {
|
||||||
|
toggleLeftPanel();
|
||||||
|
} else {
|
||||||
|
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (eqs(event, '.messageList') && ScopeMessageView === AppUserStore.focusedState()) {
|
if (eqs(event, '.messageList') && ScopeMessageView === AppUserStore.focusedState()) {
|
||||||
AppUserStore.focusedState(ScopeMessageList);
|
AppUserStore.focusedState(ScopeMessageList);
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ import { AbstractViewRight } from 'Knoin/AbstractViews';
|
||||||
export class SettingsPaneUserView extends AbstractViewRight {
|
export class SettingsPaneUserView extends AbstractViewRight {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
|
||||||
this.toggleLeftPanel = toggleLeftPanel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
@ -18,8 +15,12 @@ export class SettingsPaneUserView extends AbstractViewRight {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild(dom) {
|
onBuild(dom) {
|
||||||
dom.addEventListener('click', () =>
|
dom.addEventListener('click', () => {
|
||||||
ThemeStore.isMobile() && !event.target.closestWithin('.toggleLeft', dom) && leftPanelDisabled(true)
|
if (event.target.closestWithin('.toggleLeft', dom)) {
|
||||||
);
|
toggleLeftPanel();
|
||||||
|
} else {
|
||||||
|
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Nextcloud - SnappyMail mail plugin
|
* Nextcloud - SnappyMail mail plugin
|
||||||
*
|
*
|
||||||
* @author RainLoop Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b), SnappyMail
|
* @author RainLoop Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b), SnappyMail, Rene Hampölz (@hampoelz)
|
||||||
*
|
*
|
||||||
* Based initially on https://github.com/RainLoop/rainloop-webmail/tree/master/build/owncloud/rainloop-app
|
* Based initially on https://github.com/RainLoop/rainloop-webmail/tree/master/build/owncloud/rainloop-app
|
||||||
*/
|
*/
|
||||||
|
|
@ -10,11 +10,47 @@
|
||||||
document.onreadystatechange = () => {
|
document.onreadystatechange = () => {
|
||||||
if (document.readyState === 'complete') {
|
if (document.readyState === 'complete') {
|
||||||
watchIFrameTitle();
|
watchIFrameTitle();
|
||||||
|
passThemesToIFrame();
|
||||||
let form = document.querySelector('form.snappymail');
|
let form = document.querySelector('form.snappymail');
|
||||||
form && SnappyMailFormHelper(form);
|
form && SnappyMailFormHelper(form);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pass Nextcloud themes and theme attributes to SnappyMail on
|
||||||
|
// first load and when the SnappyMail iframe is reloaded.
|
||||||
|
function passThemesToIFrame() {
|
||||||
|
const iframe = document.getElementById('rliframe');
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
let firstLoad = true;
|
||||||
|
|
||||||
|
iframe.addEventListener('load', event => {
|
||||||
|
// repass theme styles when iframe is reloaded
|
||||||
|
if (!firstLoad) {
|
||||||
|
passThemes(event.target);
|
||||||
|
}
|
||||||
|
firstLoad = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
passThemes(iframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass Nextcloud themes and theme attributes to SnappyMail.
|
||||||
|
function passThemes(iframe) {
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
const target = iframe.contentWindow.document;
|
||||||
|
|
||||||
|
const ncStylesheets = [...document.querySelectorAll('link.theme')];
|
||||||
|
ncStylesheets.forEach(ncSheet => {
|
||||||
|
const smSheet = target.importNode(ncSheet, true);
|
||||||
|
target.head.appendChild(smSheet);
|
||||||
|
});
|
||||||
|
|
||||||
|
const themes = [...document.body.attributes].filter(att => att.name.startsWith('data-theme'));
|
||||||
|
themes.forEach(theme => target.body.setAttribute(theme.name, theme.value));
|
||||||
|
}
|
||||||
|
|
||||||
// The SnappyMail application is already configured to modify the <title> element
|
// The SnappyMail application is already configured to modify the <title> element
|
||||||
// of its root document with the number of unread messages in the inbox.
|
// of its root document with the number of unread messages in the inbox.
|
||||||
// However, its document is the SnappyMail iframe. This function sets up a
|
// However, its document is the SnappyMail iframe. This function sets up a
|
||||||
|
|
|
||||||
20
plugins/view-ics/LICENSE
Normal file
20
plugins/view-ics/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 SnappyMail
|
||||||
|
|
||||||
|
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.
|
||||||
18
plugins/view-ics/index.php
Normal file
18
plugins/view-ics/index.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class ViewICSPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'View ICS',
|
||||||
|
VERSION = '2.0',
|
||||||
|
RELEASE = '2023-08-28',
|
||||||
|
CATEGORY = 'Messages',
|
||||||
|
DESCRIPTION = 'Display ICS attachment details',
|
||||||
|
REQUIRED = '2.27.0';
|
||||||
|
|
||||||
|
public function Init() : void
|
||||||
|
{
|
||||||
|
// $this->UseLangs(true);
|
||||||
|
$this->addJs('message.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
113
plugins/view-ics/message.js
Normal file
113
plugins/view-ics/message.js
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
(rl => {
|
||||||
|
const templateId = 'MailMessageView';
|
||||||
|
|
||||||
|
addEventListener('rl-view-model.create', e => {
|
||||||
|
if (templateId === e.detail.viewModelTemplateID) {
|
||||||
|
|
||||||
|
const
|
||||||
|
template = document.getElementById(templateId),
|
||||||
|
view = e.detail,
|
||||||
|
attachmentsPlace = template.content.querySelector('.attachmentsPlace'),
|
||||||
|
dateRegEx = /(TZID=(?<tz>[^:]+):)?(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})T(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})(?<utc>Z?)/;
|
||||||
|
|
||||||
|
attachmentsPlace.after(Element.fromHTML(`
|
||||||
|
<details data-bind="if: viewICS, visible: viewICS">
|
||||||
|
<summary data-icon="📅" data-bind="text: viewICS().SUMMARY"></summary>
|
||||||
|
<table><tbody style="white-space:pre">
|
||||||
|
<tr data-bind="visible: viewICS().ORGANIZER"><td>Organizer</td><td data-bind="text: viewICS().ORGANIZER"></td></tr>
|
||||||
|
<tr><td>Start</td><td data-bind="text: viewICS().DTSTART"></td></tr>
|
||||||
|
<tr><td>End</td><td data-bind="text: viewICS().DTEND"></td></tr>
|
||||||
|
<!-- <tr><td>Transparency</td><td data-bind="text: viewICS().TRANSP"></td></tr>-->
|
||||||
|
<tr data-bind="foreach: viewICS().ATTENDEE">
|
||||||
|
<td></td><td data-bind="text: $data.replace(/;/g,';\\n')"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</details>`));
|
||||||
|
|
||||||
|
view.viewICS = ko.observable(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*/
|
||||||
|
view.message.subscribe(msg => {
|
||||||
|
view.viewICS(null);
|
||||||
|
if (msg) {
|
||||||
|
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
||||||
|
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
||||||
|
if (ics && ics.download) {
|
||||||
|
// fetch it and parse the VEVENT
|
||||||
|
rl.fetch(ics.linkDownload())
|
||||||
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
|
.then(text => {
|
||||||
|
let VEVENT,
|
||||||
|
VALARM,
|
||||||
|
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
||||||
|
'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'],
|
||||||
|
lines = text.split(/\r?\n/),
|
||||||
|
i = lines.length;
|
||||||
|
while (i--) {
|
||||||
|
let line = lines[i];
|
||||||
|
if (VEVENT) {
|
||||||
|
while (line.startsWith(' ') && i--) {
|
||||||
|
line = lines[i] + line.slice(1);
|
||||||
|
}
|
||||||
|
if (line.startsWith('END:VALARM')) {
|
||||||
|
VALARM = {};
|
||||||
|
continue;
|
||||||
|
} else if (line.startsWith('BEGIN:VALARM')) {
|
||||||
|
VEVENT.VALARM || (VEVENT.VALARM = []);
|
||||||
|
VEVENT.VALARM.push(VALARM);
|
||||||
|
VALARM = null;
|
||||||
|
continue;
|
||||||
|
} else if (line.startsWith('BEGIN:VEVENT')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line = line.match(/^([^:;]+)[:;](.+)$/);
|
||||||
|
if (line) {
|
||||||
|
if (VALARM) {
|
||||||
|
VALARM[line[1]] = line[2];
|
||||||
|
} else if (multiple.includes(line[1]) || 'X-' == line[1].slice(0,2)) {
|
||||||
|
VEVENT[line[1]] || (VEVENT[line[1]] = []);
|
||||||
|
VEVENT[line[1]].push(line[2]);
|
||||||
|
} else {
|
||||||
|
if ('DTSTART' === line[1] || 'DTEND' === line[1]) {
|
||||||
|
let parts = dateRegEx.exec(line[2])?.groups,
|
||||||
|
options = {dateStyle: 'long', timeStyle: 'short'};
|
||||||
|
parts.tz && (options.timeZone = parts.tz);
|
||||||
|
line[2] = new Date(
|
||||||
|
parseInt(parts.year, 10),
|
||||||
|
parseInt(parts.month, 10) - 1,
|
||||||
|
parseInt(parts.day, 10),
|
||||||
|
parseInt(parts.hour, 10),
|
||||||
|
parseInt(parts.minute, 10),
|
||||||
|
parseInt(parts.second, 10)
|
||||||
|
).format(options);
|
||||||
|
}
|
||||||
|
VEVENT[line[1]] = line[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (line.startsWith('END:VEVENT')) {
|
||||||
|
VEVENT = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// METHOD:REPLY || METHOD:REQUEST
|
||||||
|
// console.dir({VEVENT:VEVENT});
|
||||||
|
if (VEVENT) {
|
||||||
|
VEVENT.rawText = text;
|
||||||
|
VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED');
|
||||||
|
VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED');
|
||||||
|
VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY');
|
||||||
|
console.dir({
|
||||||
|
isCancelled: VEVENT.isCancelled(),
|
||||||
|
shouldReply: VEVENT.shouldReply()
|
||||||
|
});
|
||||||
|
view.viewICS(VEVENT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window.rl);
|
||||||
|
|
@ -384,14 +384,17 @@ trait Messages
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function MessageSimpleESearch(string $sSearchCriterias = 'ALL', array $aSearchReturn = null, bool $bReturnUid = true, string $sCharset = '', string $sLimit = '') : array
|
public function MessageSimpleESearch(string $sSearchCriterias = 'ALL', array $aSearchReturn = null, bool $bReturnUid = true, string $sLimit = '') : array
|
||||||
{
|
{
|
||||||
$oESearch = new \MailSo\Imap\Requests\ESEARCH($this);
|
$oESearch = new \MailSo\Imap\Requests\ESEARCH($this);
|
||||||
$oESearch->sCriterias = $sSearchCriterias;
|
$oESearch->sCriterias = $sSearchCriterias;
|
||||||
$oESearch->aReturn = $aSearchReturn;
|
$oESearch->aReturn = $aSearchReturn;
|
||||||
$oESearch->bUid = $bReturnUid;
|
$oESearch->bUid = $bReturnUid;
|
||||||
$oESearch->sLimit = $sLimit;
|
$oESearch->sLimit = $sLimit;
|
||||||
$oESearch->sCharset = $sCharset;
|
// if (!$this->UTF8 && !\mb_check_encoding($sSearchCriterias, 'UTF-8')) {
|
||||||
|
if (!$this->UTF8 && !\MailSo\Base\Utils::IsAscii($sSearchCriterias)) {
|
||||||
|
$oESearch->sCharset = 'UTF-8';
|
||||||
|
}
|
||||||
$oESearch->SendRequest();
|
$oESearch->SendRequest();
|
||||||
return $this->getSimpleESearchOrESortResult($bReturnUid);
|
return $this->getSimpleESearchOrESortResult($bReturnUid);
|
||||||
}
|
}
|
||||||
|
|
@ -420,12 +423,13 @@ trait Messages
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function MessageSimpleSearch(string $sSearchCriterias = 'ALL', bool $bReturnUid = true, string $sCharset = '') : array
|
public function MessageSimpleSearch(string $sSearchCriterias = 'ALL', bool $bReturnUid = true) : array
|
||||||
{
|
{
|
||||||
$aRequest = array();
|
$aRequest = array();
|
||||||
if (\strlen($sCharset)) {
|
// if (!$this->UTF8 && !\mb_check_encoding($sSearchCriterias, 'UTF-8')) {
|
||||||
|
if (!$this->UTF8 && !\MailSo\Base\Utils::IsAscii($sSearchCriterias)) {
|
||||||
$aRequest[] = 'CHARSET';
|
$aRequest[] = 'CHARSET';
|
||||||
$aRequest[] = \strtoupper($sCharset);
|
$aRequest[] = 'UTF-8';
|
||||||
}
|
}
|
||||||
|
|
||||||
$aRequest[] = !\strlen($sSearchCriterias) || '*' === $sSearchCriterias ? 'ALL' : $sSearchCriterias;
|
$aRequest[] = !\strlen($sSearchCriterias) || '*' === $sSearchCriterias ? 'ALL' : $sSearchCriterias;
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
|
|
||||||
private bool $bIsLoggined = false;
|
private bool $bIsLoggined = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC 6855 UTF8 mode
|
||||||
|
*/
|
||||||
private bool $UTF8 = false;
|
private bool $UTF8 = false;
|
||||||
|
|
||||||
public function Hash() : string
|
public function Hash() : string
|
||||||
|
|
@ -685,5 +688,4 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
{
|
{
|
||||||
return $this->UTF8 ? $sText : \MailSo\Base\Utils::Utf7ModifiedToUtf8($sText);
|
return $this->UTF8 ? $sText : \MailSo\Base\Utils::Utf7ModifiedToUtf8($sText);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@ class MailClient
|
||||||
MimeHeader::AUTOCRYPT,
|
MimeHeader::AUTOCRYPT,
|
||||||
// SPAM
|
// SPAM
|
||||||
MimeHeader::X_SPAM_STATUS,
|
MimeHeader::X_SPAM_STATUS,
|
||||||
// MimeHeader::X_SPAM_FLAG,
|
MimeHeader::X_SPAM_FLAG,
|
||||||
|
MimeHeader::X_SPAM_INFO,
|
||||||
MimeHeader::X_SPAMD_RESULT,
|
MimeHeader::X_SPAMD_RESULT,
|
||||||
MimeHeader::X_BOGOSITY,
|
MimeHeader::X_BOGOSITY,
|
||||||
// Virus
|
// Virus
|
||||||
|
|
@ -606,8 +607,8 @@ class MailClient
|
||||||
$aResultUids = $this->oImapClient->MessageSimpleSort($aSortTypes, $sSearchCriterias, $bReturnUid);
|
$aResultUids = $this->oImapClient->MessageSimpleSort($aSortTypes, $sSearchCriterias, $bReturnUid);
|
||||||
} else {
|
} else {
|
||||||
// $this->oImapClient->hasCapability('ESEARCH')
|
// $this->oImapClient->hasCapability('ESEARCH')
|
||||||
// $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, $bReturnUid, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8')
|
// $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, $bReturnUid)
|
||||||
$aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, $bReturnUid, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8');
|
$aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, $bReturnUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($bUseCacheAfterSearch) {
|
if ($bUseCacheAfterSearch) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ namespace MailSo\Mail;
|
||||||
|
|
||||||
use MailSo\Base\Utils;
|
use MailSo\Base\Utils;
|
||||||
use MailSo\Imap\Enumerations\FetchType;
|
use MailSo\Imap\Enumerations\FetchType;
|
||||||
|
use MailSo\Mime\Enumerations\Header as MimeHeader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @category MailSo
|
* @category MailSo
|
||||||
|
|
@ -160,7 +161,7 @@ class Message implements \JsonSerializable
|
||||||
$oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null;
|
$oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null;
|
||||||
if ($oHeaders) {
|
if ($oHeaders) {
|
||||||
$sContentTypeCharset = $oHeaders->ParameterValue(
|
$sContentTypeCharset = $oHeaders->ParameterValue(
|
||||||
\MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
|
MimeHeader::CONTENT_TYPE,
|
||||||
\MailSo\Mime\Enumerations\Parameter::CHARSET
|
\MailSo\Mime\Enumerations\Parameter::CHARSET
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -174,31 +175,31 @@ class Message implements \JsonSerializable
|
||||||
|
|
||||||
$bCharsetAutoDetect = !\strlen($sCharset);
|
$bCharsetAutoDetect = !\strlen($sCharset);
|
||||||
|
|
||||||
$oMessage->sSubject = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::SUBJECT, $bCharsetAutoDetect);
|
$oMessage->sSubject = $oHeaders->ValueByName(MimeHeader::SUBJECT, $bCharsetAutoDetect);
|
||||||
$oMessage->sMessageId = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::MESSAGE_ID);
|
$oMessage->sMessageId = $oHeaders->ValueByName(MimeHeader::MESSAGE_ID);
|
||||||
$oMessage->sContentType = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE);
|
$oMessage->sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE);
|
||||||
|
|
||||||
$oMessage->oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, $bCharsetAutoDetect);
|
$oMessage->oFrom = $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, $bCharsetAutoDetect);
|
||||||
$oMessage->oTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::TO_, $bCharsetAutoDetect);
|
$oMessage->oTo = $oHeaders->GetAsEmailCollection(MimeHeader::TO_, $bCharsetAutoDetect);
|
||||||
$oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect);
|
$oMessage->oCc = $oHeaders->GetAsEmailCollection(MimeHeader::CC, $bCharsetAutoDetect);
|
||||||
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect);
|
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(MimeHeader::BCC, $bCharsetAutoDetect);
|
||||||
|
|
||||||
$oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect);
|
$oMessage->oSender = $oHeaders->GetAsEmailCollection(MimeHeader::SENDER, $bCharsetAutoDetect);
|
||||||
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect);
|
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(MimeHeader::REPLY_TO, $bCharsetAutoDetect);
|
||||||
$oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect);
|
$oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(MimeHeader::DELIVERED_TO, $bCharsetAutoDetect);
|
||||||
|
|
||||||
$oMessage->InReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO);
|
$oMessage->InReplyTo = $oHeaders->ValueByName(MimeHeader::IN_REPLY_TO);
|
||||||
$oMessage->References = Utils::StripSpaces(
|
$oMessage->References = Utils::StripSpaces(
|
||||||
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES));
|
$oHeaders->ValueByName(MimeHeader::REFERENCES));
|
||||||
|
|
||||||
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString(
|
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString(
|
||||||
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE)
|
$oHeaders->ValueByName(MimeHeader::DATE)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Priority
|
// Priority
|
||||||
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY)
|
$sPriority = $oHeaders->ValueByName(MimeHeader::X_MSMAIL_PRIORITY)
|
||||||
?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE)
|
?: $oHeaders->ValueByName(MimeHeader::IMPORTANCE)
|
||||||
?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY);
|
?: $oHeaders->ValueByName(MimeHeader::X_PRIORITY);
|
||||||
if (\strlen($sPriority)) {
|
if (\strlen($sPriority)) {
|
||||||
switch (\substr(\trim($sPriority), 0, 1))
|
switch (\substr(\trim($sPriority), 0, 1))
|
||||||
{
|
{
|
||||||
|
|
@ -217,16 +218,16 @@ class Message implements \JsonSerializable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery Receipt
|
// Delivery Receipt
|
||||||
$oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::RETURN_RECEIPT_TO));
|
$oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(MimeHeader::RETURN_RECEIPT_TO));
|
||||||
|
|
||||||
// Read Receipt
|
// Read Receipt
|
||||||
$oMessage->ReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO));
|
$oMessage->ReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::DISPOSITION_NOTIFICATION_TO));
|
||||||
if (empty($oMessage->ReadReceipt)) {
|
if (empty($oMessage->ReadReceipt)) {
|
||||||
$oMessage->ReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO));
|
$oMessage->ReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::X_CONFIRM_READING_TO));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsubscribe links
|
// Unsubscribe links
|
||||||
$UnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
|
$UnsubsribeLinks = $oHeaders->ValueByName(MimeHeader::LIST_UNSUBSCRIBE);
|
||||||
if ($UnsubsribeLinks) {
|
if ($UnsubsribeLinks) {
|
||||||
$oMessage->UnsubsribeLinks = \array_map(
|
$oMessage->UnsubsribeLinks = \array_map(
|
||||||
function ($link) {
|
function ($link) {
|
||||||
|
|
@ -236,7 +237,7 @@ class Message implements \JsonSerializable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAMD_RESULT)) {
|
if ($spam = $oHeaders->ValueByName(MimeHeader::X_SPAMD_RESULT)) {
|
||||||
if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) {
|
if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) {
|
||||||
if ($threshold = \floatval($match[2])) {
|
if ($threshold = \floatval($match[2])) {
|
||||||
$oMessage->setSpamScore(100 * \floatval($match[1]) / $threshold);
|
$oMessage->setSpamScore(100 * \floatval($match[1]) / $threshold);
|
||||||
|
|
@ -244,13 +245,13 @@ class Message implements \JsonSerializable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$oMessage->bIsSpam = false !== \stripos($oMessage->sSubject, '*** SPAM ***');
|
$oMessage->bIsSpam = false !== \stripos($oMessage->sSubject, '*** SPAM ***');
|
||||||
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_BOGOSITY)) {
|
} else if ($spam = $oHeaders->ValueByName(MimeHeader::X_BOGOSITY)) {
|
||||||
$oMessage->sSpamResult = $spam;
|
$oMessage->sSpamResult = $spam;
|
||||||
$oMessage->bIsSpam = !\str_contains($spam, 'Ham');
|
$oMessage->bIsSpam = !\str_contains($spam, 'Ham');
|
||||||
if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) {
|
if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) {
|
||||||
$oMessage->setSpamScore(100 * \floatval($spamicity[1]));
|
$oMessage->setSpamScore(100 * \floatval($spamicity[1]));
|
||||||
}
|
}
|
||||||
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_STATUS)) {
|
} else if ($spam = $oHeaders->ValueByName(MimeHeader::X_SPAM_STATUS)) {
|
||||||
$oMessage->sSpamResult = $spam;
|
$oMessage->sSpamResult = $spam;
|
||||||
if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value)
|
if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value)
|
||||||
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
|
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
|
||||||
|
|
@ -259,26 +260,33 @@ class Message implements \JsonSerializable
|
||||||
$oMessage->sSpamResult = "{$value[1]} / {$required[1]}";
|
$oMessage->sSpamResult = "{$value[1]} / {$required[1]}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$oMessage->bIsSpam = 'Yes' === \substr($spam, 0, 3);
|
// https://github.com/the-djmaze/snappymail/issues/1228
|
||||||
// $spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_FLAG);
|
else if (\preg_match('@([\\d\\.]+)/([\\d\\.]+)@', $spam, $value)
|
||||||
// $oMessage->bIsSpam = false !== \stripos($spam, 'YES');
|
|| \preg_match('@([\\d\\.]+)/([\\d\\.]+)@', $oHeaders->ValueByName(MimeHeader::X_SPAM_INFO), $value)
|
||||||
|
) {
|
||||||
|
$oMessage->sSpamResult = "{$value[1]} / {$value[2]}";
|
||||||
|
$oMessage->setSpamScore(100 * \floatval($value[1]) / \floatval($value[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$oMessage->bIsSpam = 'Yes' === \substr($spam, 0, 3)
|
||||||
|
|| false !== \stripos($oHeaders->ValueByName(MimeHeader::X_SPAM_FLAG), 'YES');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS)) {
|
if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS)) {
|
||||||
$oMessage->bHasVirus = true;
|
$oMessage->bHasVirus = true;
|
||||||
}
|
}
|
||||||
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS)) {
|
if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS_STATUS)) {
|
||||||
if (false !== \stripos($spam, 'infected')) {
|
if (false !== \stripos($spam, 'infected')) {
|
||||||
$oMessage->bHasVirus = true;
|
$oMessage->bHasVirus = true;
|
||||||
} else if (false !== \stripos($spam, 'clean')) {
|
} else if (false !== \stripos($spam, 'clean')) {
|
||||||
$oMessage->bHasVirus = false;
|
$oMessage->bHasVirus = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_SCANNED)) {
|
if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS_SCANNED)) {
|
||||||
$oMessage->sVirusScanned = $virus;
|
$oMessage->sVirusScanned = $virus;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sDraftInfo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_DRAFT_INFO);
|
$sDraftInfo = $oHeaders->ValueByName(MimeHeader::X_DRAFT_INFO);
|
||||||
if (\strlen($sDraftInfo)) {
|
if (\strlen($sDraftInfo)) {
|
||||||
$sType = '';
|
$sType = '';
|
||||||
$sFolder = '';
|
$sFolder = '';
|
||||||
|
|
@ -320,7 +328,7 @@ class Message implements \JsonSerializable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$oMessage->sAutocrypt = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::AUTOCRYPT);
|
$oMessage->sAutocrypt = $oHeaders->ValueByName(MimeHeader::AUTOCRYPT);
|
||||||
}
|
}
|
||||||
else if ($oFetchResponse->GetEnvelope())
|
else if ($oFetchResponse->GetEnvelope())
|
||||||
{
|
{
|
||||||
|
|
@ -360,7 +368,7 @@ class Message implements \JsonSerializable
|
||||||
// /?/Raw/&q[]=/0/View/&q[]=/...
|
// /?/Raw/&q[]=/0/View/&q[]=/...
|
||||||
'bodyPartId' => $oPart->SubParts()[0]->PartID(),
|
'bodyPartId' => $oPart->SubParts()[0]->PartID(),
|
||||||
'sigPartId' => $oPgpSignaturePart->PartID(),
|
'sigPartId' => $oPgpSignaturePart->PartID(),
|
||||||
'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') : ''
|
'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : ''
|
||||||
];
|
];
|
||||||
/*
|
/*
|
||||||
// An empty section specification refers to the entire message, including the header.
|
// An empty section specification refers to the entire message, including the header.
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ abstract class Header
|
||||||
X_SPAM_STATUS = 'X-Spam-Status', // Yes|No
|
X_SPAM_STATUS = 'X-Spam-Status', // Yes|No
|
||||||
X_SPAM_BAR = 'X-Spam-Bar', // ++ | --
|
X_SPAM_BAR = 'X-Spam-Bar', // ++ | --
|
||||||
X_SPAM_REPORT = 'X-Spam-Report',
|
X_SPAM_REPORT = 'X-Spam-Report',
|
||||||
|
X_SPAM_INFO = 'X-Spam-Info', // v4.0.0
|
||||||
// Rspamd
|
// Rspamd
|
||||||
X_SPAMD_RESULT = 'X-Spamd-Result', // default: False [7.13 / 9.00],
|
X_SPAMD_RESULT = 'X-Spamd-Result', // default: False [7.13 / 9.00],
|
||||||
X_SPAMD_BAR = 'X-Spamd-Bar', // +++++++
|
X_SPAMD_BAR = 'X-Spamd-Bar', // +++++++
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ abstract class Parser
|
||||||
|
|
||||||
$oSubPart = new Part;
|
$oSubPart = new Part;
|
||||||
|
|
||||||
$oSubPart->parseStreamRecursive($rStreamHandle,
|
static::parseStreamRecursive($oSubPart, $rStreamHandle,
|
||||||
$iOffset, $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef, true);
|
$iOffset, $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef, true);
|
||||||
|
|
||||||
$oPart->SubParts->append($oSubPart);
|
$oPart->SubParts->append($oSubPart);
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,45 @@ namespace RainLoop\Model;
|
||||||
|
|
||||||
use RainLoop\Utils;
|
use RainLoop\Utils;
|
||||||
use RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
use RainLoop\Providers\Storage\Enumerations\StorageType;
|
||||||
|
|
||||||
class MainAccount extends Account
|
class MainAccount extends Account
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private $sCryptKey;
|
private string $sCryptKey = '';
|
||||||
|
/*
|
||||||
|
public function resealCryptKey(string $sOldPass, string $sNewPass) : string
|
||||||
|
{
|
||||||
|
$oStorage = \RainLoop\Api::Actions()->StorageProvider();
|
||||||
|
$sKey = $oStorage->Get($this, StorageType::ROOT, 'cryptkey');
|
||||||
|
if ($sKey) {
|
||||||
|
$sKey = \SnappyMail\Crypt::DecryptUrlSafe($sKey, $sOldPass);
|
||||||
|
$sKey = \SnappyMail\Crypt::EncryptUrlSafe($sKey, $sNewPass);
|
||||||
|
$oStorage->Put($this, StorageType::ROOT, 'cryptkey', $sKey);
|
||||||
|
$sKey = \SnappyMail\Crypt::DecryptUrlSafe($sKey, $sNewPass);
|
||||||
|
$this->SetCryptKey($sKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
public function CryptKey() : string
|
public function CryptKey() : string
|
||||||
{
|
{
|
||||||
if (!$this->sCryptKey) {
|
if (!$this->sCryptKey) {
|
||||||
|
/*
|
||||||
|
// Seal the cryptkey so that people who change their login password
|
||||||
|
// can use the old password to re-seal the cryptkey
|
||||||
|
$oStorage = \RainLoop\Api::Actions()->StorageProvider();
|
||||||
|
$sKey = $oStorage->Get($this, StorageType::ROOT, 'cryptkey');
|
||||||
|
if (!$sKey) {
|
||||||
|
$sKey = $this->IncPassword();
|
||||||
|
// $sKey = \random_bytes(32);
|
||||||
|
$sKey = \SnappyMail\Crypt::EncryptUrlSafe($sKey, $this->IncPassword());
|
||||||
|
$oStorage->Put($this, StorageType::ROOT, 'cryptkey', $sKey);
|
||||||
|
}
|
||||||
|
$sKey = \SnappyMail\Crypt::DecryptUrlSafe($sKey, $this->IncPassword());
|
||||||
|
$this->SetCryptKey($sKey);
|
||||||
|
*/
|
||||||
$this->SetCryptKey($this->IncPassword());
|
$this->SetCryptKey($this->IncPassword());
|
||||||
}
|
}
|
||||||
return $this->sCryptKey;
|
return $this->sCryptKey;
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ class Storage extends \RainLoop\Providers\AbstractProvider
|
||||||
return $this->oDriver->DeleteStorage($mAccount);
|
return $this->oDriver->DeleteStorage($mAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \RainLoop\Model\Account|string|null $mAccount
|
||||||
|
*/
|
||||||
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
|
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
|
||||||
{
|
{
|
||||||
return $this->oDriver->GenerateFilePath($mAccount, $iStorageType, $bMkDir);
|
return $this->oDriver->GenerateFilePath($mAccount, $iStorageType, $bMkDir);
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,9 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
return $this->bLocal;
|
return $this->bLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \RainLoop\Model\Account|string|null $mAccount
|
||||||
|
*/
|
||||||
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
|
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
|
||||||
{
|
{
|
||||||
$sEmail = $sSubFolder = '';
|
$sEmail = $sSubFolder = '';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="b-toolbar g-ui-user-select-none">
|
<div class="b-toolbar g-ui-user-select-none">
|
||||||
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel, text: leftPanelDisabled() ? '❯' : '❮'">❮</a>
|
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel"></a>
|
||||||
<h4>SnappyMail - <span data-i18n="TOP_PANEL/LABEL_ADMIN_PANEL"></span></h4>
|
<h4>SnappyMail - <span data-i18n="TOP_PANEL/LABEL_ADMIN_PANEL"></span></h4>
|
||||||
<a class="btn btn-logout fontastic" data-bind="click: logoutClick">⏻</a>
|
<a class="btn btn-logout fontastic" data-bind="click: logoutClick">⏻</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="b-footer btn-toolbar">
|
<div class="b-footer btn-toolbar">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a class="btn fontastic" data-bind="click: toggleLeftPanel, text: leftPanelDisabled() ? '❯' : '❮'"></a>
|
<a class="btn fontastic toggleLeft" data-bind="click: toggleLeftPanel"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group hide-on-panel-disabled">
|
<div class="btn-group hide-on-panel-disabled">
|
||||||
<a class="btn icon-folder-add" data-bind="click: createFolder" data-i18n="[title]POPUPS_CREATE_FOLDER/TITLE_CREATE_FOLDER"></a>
|
<a class="btn icon-folder-add" data-bind="click: createFolder" data-i18n="[title]POPUPS_CREATE_FOLDER/TITLE_CREATE_FOLDER"></a>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="btn-toolbar" data-bind="css: {'hasChecked':checkAll}">
|
<div class="btn-toolbar" data-bind="css: {'hasChecked':checkAll}">
|
||||||
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel, text: leftPanelDisabled() ? '❯' : '❮'">❮</a>
|
<a class="btn btn-thin fontastic toggleLeft"></a>
|
||||||
<a class="btn buttonCompose onCheckedHide" data-bind="click: composeClick, css: {'btn-warning': composeInEdit, 'btn-success': !composeInEdit()}" data-i18n="[title]FOLDER_LIST/BUTTON_NEW_MESSAGE">
|
<a class="btn buttonCompose onCheckedHide" data-bind="click: composeClick, css: {'btn-warning': composeInEdit, 'btn-success': !composeInEdit()}" data-i18n="[title]FOLDER_LIST/BUTTON_NEW_MESSAGE">
|
||||||
<i class="icon-paper-plane"></i>
|
<i class="icon-paper-plane"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="b-toolbar btn-toolbar">
|
<div class="b-toolbar btn-toolbar">
|
||||||
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel, text: leftPanelDisabled() ? '❯' : '❮'"></a>
|
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel"></a>
|
||||||
<a class="btn" data-bind="click: backToInbox" data-icon="⬅" data-i18n="GLOBAL/BACK"></a>
|
<a class="btn" data-bind="click: backToInbox" data-icon="⬅" data-i18n="GLOBAL/BACK"></a>
|
||||||
</div>
|
</div>
|
||||||
<nav data-bind="foreach: menu">
|
<nav data-bind="foreach: menu">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<div class="btn-toolbar" data-bind="visible: leftPanelDisabled">
|
<div class="btn-toolbar">
|
||||||
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel">❯</a>
|
<a class="btn btn-thin fontastic toggleLeft"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,59 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (defined('APP_VERSION')) {
|
if (PHP_VERSION_ID < 70400) {
|
||||||
if (PHP_VERSION_ID < 70400) {
|
echo '<p style="color: red">';
|
||||||
echo '<p style="color: red">';
|
echo '[301] Your PHP version ('.PHP_VERSION.') is lower than the minimal required 7.4.0!';
|
||||||
echo '[301] Your PHP version ('.PHP_VERSION.') is lower than the minimal required 7.4.0!';
|
echo '</p>';
|
||||||
echo '</p>';
|
exit(301);
|
||||||
exit(301);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$aOptional = array(
|
$aOptional = array(
|
||||||
'cURL' => extension_loaded('curl'),
|
'cURL' => extension_loaded('curl'),
|
||||||
'exif' => extension_loaded('exif'),
|
'exif' => extension_loaded('exif'),
|
||||||
'finfo' => class_exists('finfo'),
|
'finfo' => class_exists('finfo'),
|
||||||
'gd' => extension_loaded('gd'),
|
'gd' => extension_loaded('gd'),
|
||||||
'gnupg' => extension_loaded('gnupg'),
|
'gnupg' => extension_loaded('gnupg'),
|
||||||
'gmagick' => extension_loaded('gmagick'),
|
'gmagick' => extension_loaded('gmagick'),
|
||||||
'imagick' => extension_loaded('imagick'),
|
'imagick' => extension_loaded('imagick'),
|
||||||
'iconv' => function_exists('iconv'),
|
'iconv' => function_exists('iconv'),
|
||||||
'intl' => function_exists('idn_to_ascii'),
|
'intl' => function_exists('idn_to_ascii'),
|
||||||
'ldap' => extension_loaded('ldap'),
|
'ldap' => extension_loaded('ldap'),
|
||||||
'OpenSSL' => extension_loaded('openssl'),
|
'OpenSSL' => extension_loaded('openssl'),
|
||||||
'mysql' => extension_loaded('pdo_mysql'),
|
'mysql' => extension_loaded('pdo_mysql'),
|
||||||
'pgsql' => extension_loaded('pdo_pgsql'),
|
'pgsql' => extension_loaded('pdo_pgsql'),
|
||||||
'redis' => extension_loaded('redis'),
|
'redis' => extension_loaded('redis'),
|
||||||
'Sodium' => extension_loaded('sodium'),
|
'Sodium' => extension_loaded('sodium'),
|
||||||
'sqlite' => extension_loaded('pdo_sqlite'),
|
'sqlite' => extension_loaded('pdo_sqlite'),
|
||||||
'tidy' => extension_loaded('tidy'),
|
'tidy' => extension_loaded('tidy'),
|
||||||
'uuid' => extension_loaded('uuid'),
|
'uuid' => extension_loaded('uuid'),
|
||||||
'xxtea' => extension_loaded('xxtea'),
|
'xxtea' => extension_loaded('xxtea'),
|
||||||
'zip' => extension_loaded('zip')
|
'zip' => extension_loaded('zip')
|
||||||
);
|
);
|
||||||
|
|
||||||
$aRequirements = array(
|
$aRequirements = array(
|
||||||
'mbstring' => extension_loaded('mbstring'),
|
'mbstring' => extension_loaded('mbstring'),
|
||||||
'Zlib' => extension_loaded('zlib'),
|
'Zlib' => extension_loaded('zlib'),
|
||||||
// enabled by default:
|
// enabled by default:
|
||||||
'json' => function_exists('json_decode'),
|
'json' => function_exists('json_decode'),
|
||||||
'libxml' => function_exists('libxml_use_internal_errors'),
|
'libxml' => function_exists('libxml_use_internal_errors'),
|
||||||
'dom' => class_exists('DOMDocument')
|
'dom' => class_exists('DOMDocument')
|
||||||
// https://github.com/the-djmaze/snappymail/issues/392
|
// https://github.com/the-djmaze/snappymail/issues/392
|
||||||
// 'phar' => class_exists('PharData')
|
// 'phar' => class_exists('PharData')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (in_array(false, $aRequirements)) {
|
if (in_array(false, $aRequirements)) {
|
||||||
echo '<p>[302] The following PHP extensions are not available in your PHP configuration!</p>';
|
echo '<p>[302] The following PHP extensions are not available in your PHP configuration!</p>';
|
||||||
echo '<ul>';
|
echo '<ul>';
|
||||||
foreach ($aRequirements as $sKey => $bValue) {
|
foreach ($aRequirements as $sKey => $bValue) {
|
||||||
if (!$bValue) {
|
if (!$bValue) {
|
||||||
echo '<li>'.$sKey.'</li>';
|
echo '<li>'.$sKey.'</li>';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
echo '</ul>';
|
|
||||||
exit(302);
|
|
||||||
}
|
}
|
||||||
|
echo '</ul>';
|
||||||
|
exit(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined('APP_VERSION')) {
|
||||||
$sCheckName = 'delete_if_you_see_it_after_install';
|
$sCheckName = 'delete_if_you_see_it_after_install';
|
||||||
$sCheckFolder = APP_DATA_FOLDER_PATH.$sCheckName;
|
$sCheckFolder = APP_DATA_FOLDER_PATH.$sCheckName;
|
||||||
$sCheckFilePath = APP_DATA_FOLDER_PATH.$sCheckName.'/'.$sCheckName.'.file';
|
$sCheckFilePath = APP_DATA_FOLDER_PATH.$sCheckName.'/'.$sCheckName.'.file';
|
||||||
|
|
|
||||||
139
vendors/squire/build/squire-raw.js
vendored
139
vendors/squire/build/squire-raw.js
vendored
|
|
@ -220,52 +220,36 @@ const
|
||||||
},
|
},
|
||||||
|
|
||||||
fixCursor = (node, root) => {
|
fixCursor = (node, root) => {
|
||||||
// In Webkit and Gecko, block level elements are collapsed and
|
let fixer = null;
|
||||||
// unfocusable if they have no content (:empty). To remedy this, a <BR> must be
|
if (node instanceof Text) {
|
||||||
// inserted. In Opera and IE, we just need a textnode in order for the
|
return node;
|
||||||
// cursor to appear.
|
|
||||||
let self = root.__squire__;
|
|
||||||
let originalNode = node;
|
|
||||||
let fixer, child;
|
|
||||||
|
|
||||||
if (node === root) {
|
|
||||||
if (!(child = node.firstChild) || child.nodeName === 'BR') {
|
|
||||||
fixer = self.createDefaultBlock();
|
|
||||||
if (child) {
|
|
||||||
child.replaceWith(fixer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.append(fixer);
|
|
||||||
}
|
|
||||||
node = fixer;
|
|
||||||
fixer = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.nodeType === TEXT_NODE) {
|
|
||||||
return originalNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInline(node)) {
|
if (isInline(node)) {
|
||||||
child = node.firstChild;
|
let child = node.firstChild;
|
||||||
while (isWebKit && child?.nodeType === TEXT_NODE && !child.data) {
|
if (cantFocusEmptyTextNodes) {
|
||||||
child.remove();
|
while (child && child instanceof Text && !child.data) {
|
||||||
child = node.firstChild;
|
node.removeChild(child);
|
||||||
|
child = node.firstChild;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!child) {
|
if (!child) {
|
||||||
fixer = self._addZWS();
|
if (cantFocusEmptyTextNodes) {
|
||||||
|
fixer = document.createTextNode(ZWS);
|
||||||
|
} else {
|
||||||
|
fixer = document.createTextNode("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// } else if (!node.querySelector('BR')) {
|
} else if (node instanceof Element && !node.querySelector("BR")) {
|
||||||
// } else if (!node.innerText.trim().length) {
|
fixer = createElement("BR");
|
||||||
} else if (node.matches(':empty')) {
|
let parent = node;
|
||||||
fixer = createElement('BR');
|
let child;
|
||||||
while ((child = node.lastElementChild) && !isInline(child)) {
|
while ((child = parent.lastElementChild) && !isInline(child)) {
|
||||||
node = child;
|
parent = child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fixer) {
|
if (fixer) {
|
||||||
try {
|
try {
|
||||||
node.append(fixer);
|
node.appendChild(fixer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
didError({
|
didError({
|
||||||
name: 'Squire: fixCursor – ' + error,
|
name: 'Squire: fixCursor – ' + error,
|
||||||
|
|
@ -274,8 +258,7 @@ const
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return node;
|
||||||
return originalNode;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Recursively examine container nodes and wrap any inline children.
|
// Recursively examine container nodes and wrap any inline children.
|
||||||
|
|
@ -959,36 +942,30 @@ const
|
||||||
let startBlock = getStartBlockOfRange(range, root);
|
let startBlock = getStartBlockOfRange(range, root);
|
||||||
let nodeAfterCursor;
|
let nodeAfterCursor;
|
||||||
|
|
||||||
if (!startBlock) {
|
if (startBlock) {
|
||||||
return false;
|
// If in the middle or end of a text node, we're not at the boundary.
|
||||||
}
|
if (startContainer.nodeType === TEXT_NODE) {
|
||||||
|
if (startOffset) {
|
||||||
// If in the middle or end of a text node, we're not at the boundary.
|
|
||||||
if (startContainer.nodeType === TEXT_NODE) {
|
|
||||||
if (startOffset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
nodeAfterCursor = startContainer;
|
|
||||||
} else {
|
|
||||||
nodeAfterCursor = getNodeAfter(startContainer, startOffset);
|
|
||||||
if (nodeAfterCursor && !root.contains(nodeAfterCursor)) {
|
|
||||||
nodeAfterCursor = null;
|
|
||||||
}
|
|
||||||
// The cursor was right at the end of the document
|
|
||||||
if (!nodeAfterCursor) {
|
|
||||||
nodeAfterCursor = getNodeBefore(startContainer, startOffset);
|
|
||||||
if (nodeAfterCursor.nodeType === TEXT_NODE &&
|
|
||||||
nodeAfterCursor.length) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
nodeAfterCursor = startContainer;
|
||||||
|
} else {
|
||||||
|
nodeAfterCursor = getNodeAfter(startContainer, startOffset);
|
||||||
|
// The cursor was right at the end of the document
|
||||||
|
if (!nodeAfterCursor || !root.contains(nodeAfterCursor)) {
|
||||||
|
nodeAfterCursor = getNodeBefore(startContainer, startOffset);
|
||||||
|
if (nodeAfterCursor.nodeType === TEXT_NODE && nodeAfterCursor.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, look for any previous content in the same block.
|
||||||
|
contentWalker = newContentWalker(getStartBlockOfRange(range, root));
|
||||||
|
contentWalker.currentNode = nodeAfterCursor;
|
||||||
|
|
||||||
|
return !contentWalker.previousNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, look for any previous content in the same block.
|
|
||||||
contentWalker = newContentWalker(getStartBlockOfRange(range, root));
|
|
||||||
contentWalker.currentNode = nodeAfterCursor;
|
|
||||||
|
|
||||||
return !contentWalker.previousNode();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
rangeDoesEndAtBlockBoundary = (range, root) => {
|
rangeDoesEndAtBlockBoundary = (range, root) => {
|
||||||
|
|
@ -1823,7 +1800,8 @@ function onKey(event) {
|
||||||
|
|
||||||
let key = event.key.toLowerCase(),
|
let key = event.key.toLowerCase(),
|
||||||
modifiers = '',
|
modifiers = '',
|
||||||
range = this.getSelection();
|
range = this.getSelection(),
|
||||||
|
root = this._root;
|
||||||
|
|
||||||
// We need to apply the backspace/delete handlers regardless of
|
// We need to apply the backspace/delete handlers regardless of
|
||||||
// control key modifiers.
|
// control key modifiers.
|
||||||
|
|
@ -1842,10 +1820,37 @@ function onKey(event) {
|
||||||
// Record undo checkpoint.
|
// Record undo checkpoint.
|
||||||
this.saveUndoState(range);
|
this.saveUndoState(range);
|
||||||
// Delete the selection
|
// Delete the selection
|
||||||
deleteContentsOfRange(range, this._root);
|
deleteContentsOfRange(range, root);
|
||||||
this._ensureBottomLine();
|
this._ensureBottomLine();
|
||||||
this.setSelection(range);
|
this.setSelection(range);
|
||||||
this._updatePath(range, true);
|
this._updatePath(range, true);
|
||||||
|
} else if (range.collapsed
|
||||||
|
&& range.startContainer === root
|
||||||
|
&& root.children.length > 0) {
|
||||||
|
|
||||||
|
// Under certain conditions, cursor/range can be positioned directly
|
||||||
|
// under this._root (not wrapped) and when this happens, an inline(TEXT)
|
||||||
|
// element is attached directly to this._root. There might be other
|
||||||
|
// issues, but squire.makeUnorderedList(), squire.makeOrderedList() and
|
||||||
|
// maybe other functions that call squire.modifyBlocks() do NOT work
|
||||||
|
// on inline(TEXT) nodes that are direct children of this._root.
|
||||||
|
// Therefor, we try to detect this case here and wrap the cursor/range
|
||||||
|
// before the text is inserted.
|
||||||
|
|
||||||
|
const nextElement = root.children[range.startOffset];
|
||||||
|
if (nextElement && !isBlock(nextElement)) {
|
||||||
|
// create a new wrapper
|
||||||
|
range = createRange(root.insertBefore(
|
||||||
|
this.createDefaultBlock(), nextElement
|
||||||
|
), 0);
|
||||||
|
if (nextElement.tagName === 'BR') {
|
||||||
|
// delete it because a new <br> is created by createDefaultBlock()
|
||||||
|
root.removeChild(nextElement);
|
||||||
|
}
|
||||||
|
const restore = this._restoreSelection;
|
||||||
|
this.setSelection(range);
|
||||||
|
this._restoreSelection = restore;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue