Merge branch 'master' of https://github.com/Murena-SAS/snappymail into dev/sync-nextcloud-addressbook

This commit is contained in:
Akhil 2023-09-04 15:30:50 +05:30
commit ae7e00859c
No known key found for this signature in database
GPG key ID: A8AB680DBB7F3D45
41 changed files with 3127 additions and 347 deletions

View file

@ -61,9 +61,7 @@ import { AskPopupView } from 'View/Popup/Ask';
import {
folderInformation,
folderInformationMultiply,
setRefreshFoldersInterval,
messagesMoveHelper,
messagesDeleteHelper
setRefreshFoldersInterval
} from 'Common/Folders';
import { loadFolders } from 'Model/FolderCollection';
@ -133,13 +131,11 @@ export class AppUser extends AbstractApp {
showScreenPopup(AskPopupView, [
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
() => {
messagesDeleteHelper(sFromFolderFullName, oUids);
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids);
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids);
}
]);
} else if (oMoveFolder) {
messagesMoveHelper(sFromFolderFullName, oMoveFolder.fullName, oUids);
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids, oMoveFolder.fullName);
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids, oMoveFolder.fullName);
}
}

View file

@ -1,13 +1,11 @@
import { isArray, arrayLength } from 'Common/Utils';
import {
setFolderETag,
getFolderInboxName,
getFolderFromCacheList
} from 'Common/Cache';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { getNotification } from 'Common/Translator';
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) => {
let count = files.length;
for (const file of files) {

View file

@ -50,7 +50,7 @@ const
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
*/
// 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,
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
decode = decodeURIComponent,
@ -99,7 +99,7 @@ const
},
cleanCSS = source =>
source.trim().replace(/-(ms|webkit)-[^;]+(;|$)/g, '')
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+(;|$)/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
@ -346,7 +346,7 @@ export const
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName)) {
if (!allowedAttributes.includes(sAttrName) && ('class' !== sAttrName || 'mail-body' !== className)) {
delAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
@ -542,7 +542,7 @@ export const
oStyle.removeProperty('color');
}
oStyle.cssText = cleanCSS(oStyle.cssText);
oStyle.cssText && (oStyle.cssText = cleanCSS(oStyle.cssText));
}
if (debug && aAttrsForRemove.length) {

View file

@ -65,8 +65,8 @@ export class Selector {
this.listChecked.subscribe(items => {
if (items.length) {
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
} else if (this.autoSelect()) {
koSelectedItem(koFocusedItem());
} else {
this.autoSelect();
}
});
@ -174,9 +174,7 @@ export class Selector {
this.iFocusedNextHelper = 0;
}
if (this.autoSelect() && !isChecked && !koSelectedItem()) {
koSelectedItem(koFocusedItem());
}
!isChecked && !koSelectedItem() && this.autoSelect();
}
aCheckedCache = [];
@ -250,8 +248,10 @@ export class Selector {
/**
* @returns {boolean}
*/
autoSelect() {
return (this.oCallbacks.AutoSelect || (()=>1))() && this.focusedItem();
autoSelect(bForce) {
(bForce || (this.oCallbacks.AutoSelect || (()=>1))())
&& this.focusedItem()
&& this.selectedItem(this.focusedItem());
}
/**
@ -327,9 +327,7 @@ export class Selector {
if (result) {
this.focusedItem(result);
if ((this.autoSelect() || bForceSelect) && !this.list.hasChecked()) {
this.selectedItem(result);
}
!this.list.hasChecked() && this.autoSelect(bForceSelect);
this.scrollToFocused();
}
}

View file

@ -69,6 +69,8 @@ class SquireUI
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);
},
@ -279,6 +281,27 @@ class SquireUI
clr.type = 'color';
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.accept = 'image/*';

View file

@ -6,7 +6,7 @@ import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } fr
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme';
import { moveMessagesToFolder, dropFilesInFolder } from 'Common/Folders';
import { dropFilesInFolder } from 'Common/Folders';
import { setExpandedFolder } from 'Model/FolderCollection';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -63,7 +63,9 @@ const rlContentType = 'snappymail/action',
dragDrop = (e, element, folder, dragData) => {
dragStop(e, element);
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')) {
dropFilesInFolder(folder.fullName, e.dataTransfer.files);
}

View file

@ -45,6 +45,9 @@ body > * {
border: 1px solid rgba(125,128,128,0.5);
padding: 0.25em;
margin-right: 1em;
}
#attachments > *::before {
content: '📎 ';
}
</style>
</head>

View file

@ -1,7 +1,7 @@
import ko from '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 { SaveSettingStatus } from 'Common/Enums';
@ -95,7 +95,6 @@ export class AbstractViewLeft extends AbstractView
constructor(templateID)
{
super(templateID, 'left');
this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel;
}
}

View file

@ -464,7 +464,7 @@ export class MessageModel extends AbstractModel {
clone.querySelectorAll('.sm-bq-switcher').forEach(
node => node.replaceWith(node.lastElementChild)
);
return clone.innerHTML;
return (clone.querySelector('.mail-body') || clone).innerHTML;
}
let result = msgHtml(this);
return result.html || plainToHtml(this.plain());

View file

@ -36,7 +36,7 @@ abort = (sAction, sReason, bClearOnly) => {
oRequests[sAction] = null;
if (controller) {
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 => {
clearTimeout(controller.timeoutId);
err.aborted = signal.aborted;
err.reason = signal.reason;
return Promise.reject(err);
});
};
@ -165,7 +164,7 @@ export class AbstractFetchRemote
.catch(err => {
console.error({fetchError:err});
fCallback && fCallback(
'TimeoutError' == err.reason ? 3 : (err.name == 'AbortError' ? 2 : 1),
'TimeoutError' == err.name ? 3 : (err.name == 'AbortError' ? 2 : 1),
err
);
});

View file

@ -10,6 +10,8 @@ let __themeTimer = 0;
export const
// Also see Styles/_Values.less @maxMobileWidth
isMobile = matchMedia('(max-width: 799px)'),
// https://github.com/the-djmaze/snappymail/issues/1150
// isSmall = matchMedia('(max-width: 1400px)'),
ThemeStore = {
theme: ko.observable(''),
@ -104,6 +106,9 @@ addSubscribablesTo(ThemeStore, {
isMobile.onchange = e => {
ThemeStore.isMobile(e.matches);
$htmlCL.toggle('rl-mobile', e.matches);
leftPanelDisabled(e.matches);
/*$htmlCL.contains('sm-msgView-side') || */leftPanelDisabled(e.matches);
};
isMobile.onchange(isMobile);
//isSmall.onchange = e => $htmlCL.contains('sm-msgView-side') && leftPanelDisabled(e.matches);
//isSmall.onchange(isSmall);

View file

@ -166,9 +166,9 @@ MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
}
MessagelistUserStore.canAutoSelect = () =>
!/is:unseen/.test(MessagelistUserStore.mainSearch())
&& !disableAutoSelect()
!disableAutoSelect()
&& SettingsUserStore.usePreviewPane();
// && !SettingsUserStore.showNextMessage();
let prevFolderName;
@ -222,7 +222,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
fCallback = (iError, oData, bCached) => {
let error = '';
if (iError) {
if ('reload' != oData?.reason) {
if ('reload' != oData?.name) {
error = getNotification(iError);
MessagelistUserStore.loading(false);
// if (Notifications.RequestAborted !== iError) {
@ -399,53 +399,92 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
* @param {string=} toFolderFullName = ''
* @param {boolean=} copy = false
*/
MessagelistUserStore.removeMessagesFromList = (
MessagelistUserStore.moveMessages = (
fromFolderFullName, oUids, toFolderFullName = '', copy = false
) => {
const fromFolder = getFolderFromCacheList(fromFolderFullName);
if (!fromFolder || !oUids?.size) return;
let unseenCount = 0,
setPage = 0,
currentMessage = MessageUserStore.message();
const trashFolder = FolderUserStore.trashFolder(),
const toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
trashFolder = FolderUserStore.trashFolder(),
spamFolder = FolderUserStore.spamFolder(),
fromFolder = getFolderFromCacheList(fromFolderFullName),
toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
page = MessagelistUserStore.page(),
messages =
FolderUserStore.currentFolderFullName() === fromFolderFullName
? 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);
if (fromFolder) {
if (!copy) {
fromFolder.etag = '';
if (!copy) {
fromFolder.totalEmails(
0 <= fromFolder.totalEmails() - oUids.size ? fromFolder.totalEmails() - oUids.size : 0
);
if (0 < unseenCount) {
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
}
}
fromFolder.totalEmails(Math.max(0, fromFolder.totalEmails() - oUids.size));
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
}
if (toFolder) {
toFolder.etag = '';
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
unseenCount = 0;
}
toFolder.totalEmails(toFolder.totalEmails() + oUids.size);
if (0 < unseenCount) {
if (trashFolder !== toFolder.fullName && spamFolder !== toFolder.fullName) {
toFolder.unreadEmails(toFolder.unreadEmails() + unseenCount);
}
toFolder.actionBlink(true);
}
if (messages.length) {
disableAutoSelect(true);
if (copy) {
messages.forEach(item => item.checked(false));
} else {
@ -468,49 +507,37 @@ MessagelistUserStore.removeMessagesFromList = (
currentMessage = null;
MessageUserStore.message(null);
}
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()
&& MessagelistUserStore.length
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
) {
const message = MessagelistUserStore.find(item => item && !item.deleted());
if (!message) {
if (1 < MessagelistUserStore.page()) {
setPage = MessagelistUserStore.page() - 1;
if (toFolderFullName) {
if (toFolder && fromFolderFullName != toFolderFullName) {
const params = {
fromFolder: fromFolderFullName,
toFolder: toFolderFullName,
uids: [...oUids].join(',')
};
if (copy) {
Remote.request('MessageCopy', null, params);
} else {
MessagelistUserStore.threadUid(0);
setPage = MessagelistUserStore.pageBeforeThread();
const
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();
}
}
if (setPage) {
MessagelistUserStore.page(setPage);
replaceHash(
mailBox(
FolderUserStore.currentFolderFullNameHash(),
setPage,
MessagelistUserStore.listSearch(),
MessagelistUserStore.threadUid()
)
} else {
Remote.abort('MessageList', 'reload').request('MessageDelete',
moveOrDeleteResponseHelper,
{
folder: fromFolderFullName,
uids: [...oUids].join(',')
}
);
}
};

View file

@ -4,6 +4,13 @@ html.list-loading body {
cursor: progress;
}
.toggleLeft::before {
content: '';
}
html.rl-left-panel-disabled .toggleLeft::before {
content: '';
}
@media screen and (min-width: @maxMobileWidth + 1px) {
#rl-app {
background-image: var(--main-bg-image);

View file

@ -15,6 +15,7 @@
.b-toolbar {
padding: 10px 0 10px @rlLowMargin;
white-space: nowrap;
}
}
@ -189,7 +190,7 @@ html.rl-left-panel-disabled {
/* desktop */
@media screen and (min-width: @maxMobileWidth + 1px) {
.toggleLeft,
#rl-right .toggleLeft,
#V-MailMessageList .buttonCompose {
display: none !important;
}

View file

@ -2,12 +2,11 @@ import Remote from 'Remote/Admin/Fetch';
import { AbstractViewRight } from 'Knoin/AbstractViews';
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
import { toggleLeftPanel } from 'Common/Globals';
export class PaneSettingsAdminView extends AbstractViewRight {
constructor() {
super('AdminPane');
this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel;
}

View file

@ -15,7 +15,7 @@ import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { folderInformation, messagesDeleteHelper } from 'Common/Folders';
import { folderInformation } from 'Common/Folders';
import { serverRequest } from 'Common/Links';
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
import { setFolderETag } from 'Common/Cache';
@ -586,8 +586,7 @@ export class ComposePopupView extends AbstractViewPopup {
const
sFromFolderFullName = this.draftsFolder(),
oUids = new Set([this.draftUid()]);
messagesDeleteHelper(sFromFolderFullName, oUids);
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids);
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids);
this.close();
}
]);

View file

@ -20,8 +20,6 @@ import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { ContactsPopupView } from 'View/Popup/Contacts';
import { ComposePopupView } from 'View/Popup/Compose';
import { moveMessagesToFolder } from 'Common/Folders';
import { setExpandedFolder, foldersFilter } from 'Model/FolderCollection';
export class MailFolderList extends AbstractViewLeft {
@ -92,7 +90,7 @@ export class MailFolderList extends AbstractViewLeft {
if (folder) {
if (moveAction()) {
moveAction(false);
moveMessagesToFolder(
MessagelistUserStore.moveMessages(
FolderUserStore.currentFolderFullName(),
MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails(),
folder.fullName,

View file

@ -88,8 +88,6 @@ export class MailMessageList extends AbstractViewRight {
this.composeInEdit = ComposePopupView.inEdit;
this.isMobile = ThemeStore.isMobile; // Obsolete
this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel;
this.popupVisibility = arePopupsVisible;
@ -582,7 +580,11 @@ export class MailMessageList extends AbstractViewRight {
addEventsListeners(dom, {
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()) {
AppUserStore.focusedState(ScopeMessageList);

View file

@ -8,9 +8,6 @@ import { AbstractViewRight } from 'Knoin/AbstractViews';
export class SettingsPaneUserView extends AbstractViewRight {
constructor() {
super();
this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel;
}
onShow() {
@ -18,8 +15,12 @@ export class SettingsPaneUserView extends AbstractViewRight {
}
onBuild(dom) {
dom.addEventListener('click', () =>
ThemeStore.isMobile() && !event.target.closestWithin('.toggleLeft', dom) && leftPanelDisabled(true)
);
dom.addEventListener('click', () => {
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

View file

@ -1,7 +1,7 @@
/**
* 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
*/
@ -10,11 +10,47 @@
document.onreadystatechange = () => {
if (document.readyState === 'complete') {
watchIFrameTitle();
passThemesToIFrame();
let form = document.querySelector('form.snappymail');
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
// 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

20
plugins/view-ics/LICENSE Normal file
View 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.

View 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
View 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);

View file

@ -384,14 +384,17 @@ trait Messages
* @throws \MailSo\Net\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->sCriterias = $sSearchCriterias;
$oESearch->aReturn = $aSearchReturn;
$oESearch->bUid = $bReturnUid;
$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();
return $this->getSimpleESearchOrESortResult($bReturnUid);
}
@ -420,12 +423,13 @@ trait Messages
* @throws \MailSo\Net\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();
if (\strlen($sCharset)) {
// if (!$this->UTF8 && !\mb_check_encoding($sSearchCriterias, 'UTF-8')) {
if (!$this->UTF8 && !\MailSo\Base\Utils::IsAscii($sSearchCriterias)) {
$aRequest[] = 'CHARSET';
$aRequest[] = \strtoupper($sCharset);
$aRequest[] = 'UTF-8';
}
$aRequest[] = !\strlen($sSearchCriterias) || '*' === $sSearchCriterias ? 'ALL' : $sSearchCriterias;

View file

@ -49,6 +49,9 @@ class ImapClient extends \MailSo\Net\NetClient
private bool $bIsLoggined = false;
/**
* RFC 6855 UTF8 mode
*/
private bool $UTF8 = false;
public function Hash() : string
@ -685,5 +688,4 @@ class ImapClient extends \MailSo\Net\NetClient
{
return $this->UTF8 ? $sText : \MailSo\Base\Utils::Utf7ModifiedToUtf8($sText);
}
}

View file

@ -77,7 +77,8 @@ class MailClient
MimeHeader::AUTOCRYPT,
// SPAM
MimeHeader::X_SPAM_STATUS,
// MimeHeader::X_SPAM_FLAG,
MimeHeader::X_SPAM_FLAG,
MimeHeader::X_SPAM_INFO,
MimeHeader::X_SPAMD_RESULT,
MimeHeader::X_BOGOSITY,
// Virus
@ -606,8 +607,8 @@ class MailClient
$aResultUids = $this->oImapClient->MessageSimpleSort($aSortTypes, $sSearchCriterias, $bReturnUid);
} else {
// $this->oImapClient->hasCapability('ESEARCH')
// $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, $bReturnUid, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8')
$aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, $bReturnUid, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8');
// $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, $bReturnUid)
$aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, $bReturnUid);
}
if ($bUseCacheAfterSearch) {

View file

@ -13,6 +13,7 @@ namespace MailSo\Mail;
use MailSo\Base\Utils;
use MailSo\Imap\Enumerations\FetchType;
use MailSo\Mime\Enumerations\Header as MimeHeader;
/**
* @category MailSo
@ -160,7 +161,7 @@ class Message implements \JsonSerializable
$oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null;
if ($oHeaders) {
$sContentTypeCharset = $oHeaders->ParameterValue(
\MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
MimeHeader::CONTENT_TYPE,
\MailSo\Mime\Enumerations\Parameter::CHARSET
);
@ -174,31 +175,31 @@ class Message implements \JsonSerializable
$bCharsetAutoDetect = !\strlen($sCharset);
$oMessage->sSubject = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::SUBJECT, $bCharsetAutoDetect);
$oMessage->sMessageId = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::MESSAGE_ID);
$oMessage->sContentType = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE);
$oMessage->sSubject = $oHeaders->ValueByName(MimeHeader::SUBJECT, $bCharsetAutoDetect);
$oMessage->sMessageId = $oHeaders->ValueByName(MimeHeader::MESSAGE_ID);
$oMessage->sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE);
$oMessage->oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, $bCharsetAutoDetect);
$oMessage->oTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::TO_, $bCharsetAutoDetect);
$oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect);
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect);
$oMessage->oFrom = $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, $bCharsetAutoDetect);
$oMessage->oTo = $oHeaders->GetAsEmailCollection(MimeHeader::TO_, $bCharsetAutoDetect);
$oMessage->oCc = $oHeaders->GetAsEmailCollection(MimeHeader::CC, $bCharsetAutoDetect);
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(MimeHeader::BCC, $bCharsetAutoDetect);
$oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect);
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect);
$oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect);
$oMessage->oSender = $oHeaders->GetAsEmailCollection(MimeHeader::SENDER, $bCharsetAutoDetect);
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(MimeHeader::REPLY_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(
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES));
$oHeaders->ValueByName(MimeHeader::REFERENCES));
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString(
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE)
$oHeaders->ValueByName(MimeHeader::DATE)
);
// Priority
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY)
?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE)
?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY);
$sPriority = $oHeaders->ValueByName(MimeHeader::X_MSMAIL_PRIORITY)
?: $oHeaders->ValueByName(MimeHeader::IMPORTANCE)
?: $oHeaders->ValueByName(MimeHeader::X_PRIORITY);
if (\strlen($sPriority)) {
switch (\substr(\trim($sPriority), 0, 1))
{
@ -217,16 +218,16 @@ class Message implements \JsonSerializable
}
// 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
$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)) {
$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
$UnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
$UnsubsribeLinks = $oHeaders->ValueByName(MimeHeader::LIST_UNSUBSCRIBE);
if ($UnsubsribeLinks) {
$oMessage->UnsubsribeLinks = \array_map(
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 ($threshold = \floatval($match[2])) {
$oMessage->setSpamScore(100 * \floatval($match[1]) / $threshold);
@ -244,13 +245,13 @@ class Message implements \JsonSerializable
}
}
$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->bIsSpam = !\str_contains($spam, 'Ham');
if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) {
$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;
if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value)
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
@ -259,26 +260,33 @@ class Message implements \JsonSerializable
$oMessage->sSpamResult = "{$value[1]} / {$required[1]}";
}
}
$oMessage->bIsSpam = 'Yes' === \substr($spam, 0, 3);
// $spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_FLAG);
// $oMessage->bIsSpam = false !== \stripos($spam, 'YES');
// https://github.com/the-djmaze/snappymail/issues/1228
else if (\preg_match('@([\\d\\.]+)/([\\d\\.]+)@', $spam, $value)
|| \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;
}
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS)) {
if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS_STATUS)) {
if (false !== \stripos($spam, 'infected')) {
$oMessage->bHasVirus = true;
} else if (false !== \stripos($spam, 'clean')) {
$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;
}
$sDraftInfo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_DRAFT_INFO);
$sDraftInfo = $oHeaders->ValueByName(MimeHeader::X_DRAFT_INFO);
if (\strlen($sDraftInfo)) {
$sType = '';
$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())
{
@ -360,7 +368,7 @@ class Message implements \JsonSerializable
// /?/Raw/&q[]=/0/View/&q[]=/...
'bodyPartId' => $oPart->SubParts()[0]->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.

View file

@ -60,6 +60,7 @@ abstract class Header
X_SPAM_STATUS = 'X-Spam-Status', // Yes|No
X_SPAM_BAR = 'X-Spam-Bar', // ++ | --
X_SPAM_REPORT = 'X-Spam-Report',
X_SPAM_INFO = 'X-Spam-Info', // v4.0.0
// Rspamd
X_SPAMD_RESULT = 'X-Spamd-Result', // default: False [7.13 / 9.00],
X_SPAMD_BAR = 'X-Spamd-Bar', // +++++++

View file

@ -225,7 +225,7 @@ abstract class Parser
$oSubPart = new Part;
$oSubPart->parseStreamRecursive($rStreamHandle,
static::parseStreamRecursive($oSubPart, $rStreamHandle,
$iOffset, $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef, true);
$oPart->SubParts->append($oSubPart);

View file

@ -4,17 +4,45 @@ namespace RainLoop\Model;
use RainLoop\Utils;
use RainLoop\Exceptions\ClientException;
use RainLoop\Providers\Storage\Enumerations\StorageType;
class MainAccount extends Account
{
/**
* @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
{
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());
}
return $this->sCryptKey;

View file

@ -66,6 +66,9 @@ class Storage extends \RainLoop\Providers\AbstractProvider
return $this->oDriver->DeleteStorage($mAccount);
}
/**
* @param \RainLoop\Model\Account|string|null $mAccount
*/
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
{
return $this->oDriver->GenerateFilePath($mAccount, $iStorageType, $bMkDir);

View file

@ -85,6 +85,9 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
return $this->bLocal;
}
/**
* @param \RainLoop\Model\Account|string|null $mAccount
*/
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
{
$sEmail = $sSubFolder = '';

View file

@ -1,5 +1,5 @@
<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>
<a class="btn btn-logout fontastic" data-bind="click: logoutClick"></a>
</div>

View file

@ -29,7 +29,7 @@
</div>
<div class="b-footer btn-toolbar">
<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 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>

View file

@ -1,5 +1,5 @@
<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">
<i class="icon-paper-plane"></i>
</a>

View file

@ -1,5 +1,5 @@
<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>
</div>
<nav data-bind="foreach: menu">

View file

@ -1,3 +1,3 @@
<div class="btn-toolbar" data-bind="visible: leftPanelDisabled">
<a class="btn btn-thin fontastic toggleLeft" data-bind="click: toggleLeftPanel"></a>
<div class="btn-toolbar">
<a class="btn btn-thin fontastic toggleLeft"></a>
</div>

View file

@ -1,59 +1,59 @@
<?php
if (defined('APP_VERSION')) {
if (PHP_VERSION_ID < 70400) {
echo '<p style="color: red">';
echo '[301] Your PHP version ('.PHP_VERSION.') is lower than the minimal required 7.4.0!';
echo '</p>';
exit(301);
}
if (PHP_VERSION_ID < 70400) {
echo '<p style="color: red">';
echo '[301] Your PHP version ('.PHP_VERSION.') is lower than the minimal required 7.4.0!';
echo '</p>';
exit(301);
}
$aOptional = array(
'cURL' => extension_loaded('curl'),
'exif' => extension_loaded('exif'),
'finfo' => class_exists('finfo'),
'gd' => extension_loaded('gd'),
'gnupg' => extension_loaded('gnupg'),
'gmagick' => extension_loaded('gmagick'),
'imagick' => extension_loaded('imagick'),
'iconv' => function_exists('iconv'),
'intl' => function_exists('idn_to_ascii'),
'ldap' => extension_loaded('ldap'),
'OpenSSL' => extension_loaded('openssl'),
'mysql' => extension_loaded('pdo_mysql'),
'pgsql' => extension_loaded('pdo_pgsql'),
'redis' => extension_loaded('redis'),
'Sodium' => extension_loaded('sodium'),
'sqlite' => extension_loaded('pdo_sqlite'),
'tidy' => extension_loaded('tidy'),
'uuid' => extension_loaded('uuid'),
'xxtea' => extension_loaded('xxtea'),
'zip' => extension_loaded('zip')
);
$aOptional = array(
'cURL' => extension_loaded('curl'),
'exif' => extension_loaded('exif'),
'finfo' => class_exists('finfo'),
'gd' => extension_loaded('gd'),
'gnupg' => extension_loaded('gnupg'),
'gmagick' => extension_loaded('gmagick'),
'imagick' => extension_loaded('imagick'),
'iconv' => function_exists('iconv'),
'intl' => function_exists('idn_to_ascii'),
'ldap' => extension_loaded('ldap'),
'OpenSSL' => extension_loaded('openssl'),
'mysql' => extension_loaded('pdo_mysql'),
'pgsql' => extension_loaded('pdo_pgsql'),
'redis' => extension_loaded('redis'),
'Sodium' => extension_loaded('sodium'),
'sqlite' => extension_loaded('pdo_sqlite'),
'tidy' => extension_loaded('tidy'),
'uuid' => extension_loaded('uuid'),
'xxtea' => extension_loaded('xxtea'),
'zip' => extension_loaded('zip')
);
$aRequirements = array(
'mbstring' => extension_loaded('mbstring'),
'Zlib' => extension_loaded('zlib'),
// enabled by default:
'json' => function_exists('json_decode'),
'libxml' => function_exists('libxml_use_internal_errors'),
'dom' => class_exists('DOMDocument')
// https://github.com/the-djmaze/snappymail/issues/392
// 'phar' => class_exists('PharData')
);
$aRequirements = array(
'mbstring' => extension_loaded('mbstring'),
'Zlib' => extension_loaded('zlib'),
// enabled by default:
'json' => function_exists('json_decode'),
'libxml' => function_exists('libxml_use_internal_errors'),
'dom' => class_exists('DOMDocument')
// https://github.com/the-djmaze/snappymail/issues/392
// 'phar' => class_exists('PharData')
);
if (in_array(false, $aRequirements)) {
echo '<p>[302] The following PHP extensions are not available in your PHP configuration!</p>';
echo '<ul>';
foreach ($aRequirements as $sKey => $bValue) {
if (!$bValue) {
echo '<li>'.$sKey.'</li>';
}
if (in_array(false, $aRequirements)) {
echo '<p>[302] The following PHP extensions are not available in your PHP configuration!</p>';
echo '<ul>';
foreach ($aRequirements as $sKey => $bValue) {
if (!$bValue) {
echo '<li>'.$sKey.'</li>';
}
echo '</ul>';
exit(302);
}
echo '</ul>';
exit(302);
}
if (defined('APP_VERSION')) {
$sCheckName = 'delete_if_you_see_it_after_install';
$sCheckFolder = APP_DATA_FOLDER_PATH.$sCheckName;
$sCheckFilePath = APP_DATA_FOLDER_PATH.$sCheckName.'/'.$sCheckName.'.file';

View file

@ -220,52 +220,36 @@ const
},
fixCursor = (node, root) => {
// In Webkit and Gecko, block level elements are collapsed and
// unfocusable if they have no content (:empty). To remedy this, a <BR> must be
// inserted. In Opera and IE, we just need a textnode in order for the
// 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;
}
let fixer = null;
if (node instanceof Text) {
return node;
}
if (node.nodeType === TEXT_NODE) {
return originalNode;
}
if (isInline(node)) {
child = node.firstChild;
while (isWebKit && child?.nodeType === TEXT_NODE && !child.data) {
child.remove();
child = node.firstChild;
let child = node.firstChild;
if (cantFocusEmptyTextNodes) {
while (child && child instanceof Text && !child.data) {
node.removeChild(child);
child = node.firstChild;
}
}
if (!child) {
fixer = self._addZWS();
if (cantFocusEmptyTextNodes) {
fixer = document.createTextNode(ZWS);
} else {
fixer = document.createTextNode("");
}
}
// } else if (!node.querySelector('BR')) {
// } else if (!node.innerText.trim().length) {
} else if (node.matches(':empty')) {
fixer = createElement('BR');
while ((child = node.lastElementChild) && !isInline(child)) {
node = child;
} else if (node instanceof Element && !node.querySelector("BR")) {
fixer = createElement("BR");
let parent = node;
let child;
while ((child = parent.lastElementChild) && !isInline(child)) {
parent = child;
}
}
if (fixer) {
try {
node.append(fixer);
node.appendChild(fixer);
} catch (error) {
didError({
name: 'Squire: fixCursor ' + error,
@ -274,8 +258,7 @@ const
});
}
}
return originalNode;
return node;
},
// Recursively examine container nodes and wrap any inline children.
@ -959,36 +942,30 @@ const
let startBlock = getStartBlockOfRange(range, root);
let nodeAfterCursor;
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) {
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) {
if (startBlock) {
// 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);
// 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) => {
@ -1823,7 +1800,8 @@ function onKey(event) {
let key = event.key.toLowerCase(),
modifiers = '',
range = this.getSelection();
range = this.getSelection(),
root = this._root;
// We need to apply the backspace/delete handlers regardless of
// control key modifiers.
@ -1842,10 +1820,37 @@ function onKey(event) {
// Record undo checkpoint.
this.saveUndoState(range);
// Delete the selection
deleteContentsOfRange(range, this._root);
deleteContentsOfRange(range, root);
this._ensureBottomLine();
this.setSelection(range);
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;
}
}
}