mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-12-29 11:01:34 +08:00
553 lines
16 KiB
JavaScript
553 lines
16 KiB
JavaScript
import { koComputable, addObservablesTo, addComputablesTo } from 'External/ko';
|
|
|
|
import { SMAudio } from 'Common/Audio';
|
|
import { Notifications } from 'Common/Enums';
|
|
import { MessageSetAction } from 'Common/EnumsUser';
|
|
import { $htmlCL, fireEvent } from 'Common/Globals';
|
|
import { arrayLength, pString } from 'Common/Utils';
|
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
|
|
|
import {
|
|
getFolderInboxName,
|
|
getFolderFromCacheList,
|
|
setFolderETag
|
|
} from 'Common/Cache';
|
|
|
|
import { mailBox } from 'Common/Links';
|
|
import { i18n, getNotification } from 'Common/Translator';
|
|
|
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
|
import { MessageCollectionModel } from 'Model/MessageCollection';
|
|
|
|
import { AccountUserStore } from 'Stores/User/Account';
|
|
import { FolderUserStore } from 'Stores/User/Folder';
|
|
import { MessageUserStore } from 'Stores/User/Message';
|
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
|
|
|
import Remote from 'Remote/User/Fetch';
|
|
|
|
import { b64EncodeJSONSafe } from 'Common/Utils';
|
|
import { SettingsGet } from 'Common/Globals';
|
|
import { SUB_QUERY_PREFIX } from 'Common/Links';
|
|
import { AppUserStore } from 'Stores/User/App';
|
|
|
|
const
|
|
isChecked = item => item.checked(),
|
|
replaceHash = hash => {
|
|
rl.route.off();
|
|
hasher.replaceHash(hash);
|
|
rl.route.on();
|
|
},
|
|
disableAutoSelect = ko.observable(false).extend({ falseTimeout: 500 });
|
|
|
|
export const MessagelistUserStore = ko.observableArray().extend({ debounce: 0 });
|
|
|
|
addObservablesTo(MessagelistUserStore, {
|
|
count: 0,
|
|
listSearch: '',
|
|
listLimited: 0,
|
|
threadUid: 0,
|
|
page: 1,
|
|
pageBeforeThread: 1,
|
|
error: '',
|
|
// folder: '',
|
|
|
|
endHash: '',
|
|
endThreadUid: 0,
|
|
|
|
loading: false,
|
|
// Happens when message(s) removed from list
|
|
isIncomplete: false,
|
|
|
|
selectedMessage: null,
|
|
focusedMessage: null
|
|
});
|
|
|
|
// Computed Observables
|
|
|
|
addComputablesTo(MessagelistUserStore, {
|
|
isLoading: () => {
|
|
const value = MessagelistUserStore.loading() | MessagelistUserStore.isIncomplete();
|
|
$htmlCL.toggle('list-loading', value);
|
|
return value;
|
|
},
|
|
|
|
isArchiveFolder: () => FolderUserStore.archiveFolder() === MessagelistUserStore().folder,
|
|
|
|
isDraftFolder: () => FolderUserStore.draftsFolder() === MessagelistUserStore().folder,
|
|
|
|
isSentFolder: () => FolderUserStore.sentFolder() === MessagelistUserStore().folder,
|
|
|
|
isSpamFolder: () => FolderUserStore.spamFolder() === MessagelistUserStore().folder,
|
|
|
|
isTrashFolder: () => FolderUserStore.trashFolder() === MessagelistUserStore().folder,
|
|
|
|
archiveAllowed: () => ![UNUSED_OPTION_VALUE, MessagelistUserStore().folder].includes(FolderUserStore.archiveFolder())
|
|
&& !MessagelistUserStore.isDraftFolder(),
|
|
|
|
canMarkAsSpam: () => !(UNUSED_OPTION_VALUE === FolderUserStore.spamFolder()
|
|
// | MessagelistUserStore.isArchiveFolder()
|
|
| MessagelistUserStore.isSentFolder()
|
|
| MessagelistUserStore.isDraftFolder()
|
|
| MessagelistUserStore.isSpamFolder()),
|
|
|
|
pageCount: () => Math.max(1, Math.ceil(MessagelistUserStore.count() / SettingsUserStore.messagesPerPage())),
|
|
|
|
mainSearch: {
|
|
read: MessagelistUserStore.listSearch,
|
|
write: value => hasher.setHash(
|
|
mailBox(FolderUserStore.currentFolderFullNameHash(), 1,
|
|
value.toString().trim(), MessagelistUserStore.threadUid())
|
|
)
|
|
},
|
|
|
|
listCheckedOrSelected: () => {
|
|
const
|
|
selectedMessage = MessagelistUserStore.selectedMessage(),
|
|
focusedMessage = MessagelistUserStore.focusedMessage(),
|
|
checked = MessagelistUserStore.filter(item => isChecked(item));
|
|
return checked.length ? checked : (selectedMessage || focusedMessage ? [selectedMessage || focusedMessage] : []);
|
|
},
|
|
|
|
listCheckedOrSelectedUidsWithSubMails: () => {
|
|
let result = new Set;
|
|
MessagelistUserStore.listCheckedOrSelected().forEach(message => {
|
|
result.add(message.uid);
|
|
if (1 < message.threadsLen()) {
|
|
message.threads().forEach(result.add, result);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
});
|
|
|
|
MessagelistUserStore.listChecked = koComputable(
|
|
() => MessagelistUserStore.filter(isChecked)
|
|
).extend({ rateLimit: 0 });
|
|
|
|
// Also used by Selector
|
|
MessagelistUserStore.hasChecked = koComputable(
|
|
// Issue: not all are observed?
|
|
() => !!MessagelistUserStore.find(isChecked)
|
|
).extend({ rateLimit: 0 });
|
|
|
|
MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
|
|
!!MessagelistUserStore.selectedMessage()
|
|
| !!MessagelistUserStore.focusedMessage()
|
|
// Issue: not all are observed?
|
|
| !!MessagelistUserStore.find(isChecked)
|
|
).extend({ rateLimit: 50 });
|
|
|
|
MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
|
|
if (getFolderInboxName() === folder && arrayLength(newMessages)) {
|
|
|
|
SMAudio.playNotification();
|
|
|
|
const len = newMessages.length;
|
|
if (3 < len) {
|
|
NotificationUserStore.display(
|
|
AccountUserStore.email(),
|
|
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
|
|
COUNT: len
|
|
}),
|
|
{ Url: mailBox(newMessages[0].folder) }
|
|
);
|
|
} else {
|
|
newMessages.forEach(item => {
|
|
NotificationUserStore.display(
|
|
EmailCollectionModel.reviveFromJson(item.from).toString(),
|
|
item.subject,
|
|
{ folder: item.folder, uid: item.uid }
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
MessagelistUserStore.canSelect = () =>
|
|
!disableAutoSelect()
|
|
&& SettingsUserStore.usePreviewPane();
|
|
// && !SettingsUserStore.showNextMessage();
|
|
|
|
let prevFolderName;
|
|
|
|
/**
|
|
* @param {boolean=} bDropPagePosition = false
|
|
* @param {boolean=} bDropCurrentFolderCache = false
|
|
*/
|
|
MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCache = false) => {
|
|
let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage(),
|
|
folderName = FolderUserStore.currentFolderFullName();
|
|
// folderName = FolderUserStore.currentFolder() ? self.currentFolder().fullName : '');
|
|
|
|
if (bDropCurrentFolderCache) {
|
|
setFolderETag(folderName, '');
|
|
}
|
|
|
|
if (bDropPagePosition) {
|
|
MessagelistUserStore.page(1);
|
|
MessagelistUserStore.pageBeforeThread(1);
|
|
iOffset = 0;
|
|
|
|
replaceHash(
|
|
mailBox(
|
|
FolderUserStore.currentFolderFullNameHash(),
|
|
MessagelistUserStore.page(),
|
|
MessagelistUserStore.listSearch(),
|
|
MessagelistUserStore.threadUid()
|
|
)
|
|
);
|
|
}
|
|
|
|
if (prevFolderName != folderName) {
|
|
prevFolderName = folderName;
|
|
MessagelistUserStore([]);
|
|
}
|
|
|
|
MessagelistUserStore.loading(true);
|
|
|
|
let sGetAdd = '',
|
|
// folder = getFolderFromCacheList(folderName.fullName),
|
|
folder = getFolderFromCacheList(folderName),
|
|
folderETag = folder?.etag || '',
|
|
params = {
|
|
folder: folderName,
|
|
offset: iOffset,
|
|
limit: SettingsUserStore.messagesPerPage(),
|
|
uidNext: folder?.uidNext || 0, // Used to check for new messages
|
|
sort: FolderUserStore.sortMode(),
|
|
search: MessagelistUserStore.listSearch()
|
|
},
|
|
fCallback = (iError, oData, bCached) => {
|
|
let error = '';
|
|
if (iError) {
|
|
if ('reload' != oData?.name) {
|
|
error = getNotification(iError);
|
|
MessagelistUserStore.loading(false);
|
|
// if (Notifications.RequestAborted !== iError) {
|
|
// MessagelistUserStore([]);
|
|
// }
|
|
// if (oData.message) { error = oData.message + error; }
|
|
}
|
|
} else {
|
|
const collection = MessageCollectionModel.reviveFromJson(oData.Result, bCached);
|
|
if (collection) {
|
|
const
|
|
folderInfo = collection.folder,
|
|
folder = getFolderFromCacheList(folderInfo.name);
|
|
collection.folder = folderInfo.name;
|
|
if (folder && !bCached) {
|
|
// folder.revivePropertiesFromJson(result);
|
|
folder.expires = Date.now();
|
|
folder.uidNext = folderInfo.uidNext;
|
|
folder.etag = folderInfo.etag;
|
|
|
|
if (null != folderInfo.totalEmails) {
|
|
folder.totalEmails(folderInfo.totalEmails);
|
|
}
|
|
|
|
if (null != folderInfo.unreadEmails) {
|
|
folder.unreadEmails(folderInfo.unreadEmails);
|
|
}
|
|
|
|
let flags = folderInfo.permanentFlags || [];
|
|
if (flags.includes('\\*')) {
|
|
let i = 6;
|
|
while (--i) {
|
|
flags.includes('$label'+i) || flags.push('$label'+i);
|
|
}
|
|
}
|
|
flags.sort((a, b) => {
|
|
a = a.toUpperCase();
|
|
b = b.toUpperCase();
|
|
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
|
});
|
|
folder.permanentFlags(flags);
|
|
|
|
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
|
|
}
|
|
|
|
MessagelistUserStore.count(collection.totalEmails);
|
|
MessagelistUserStore.listSearch(pString(collection.search));
|
|
MessagelistUserStore.listLimited(!!collection.limited);
|
|
MessagelistUserStore.page(Math.ceil(collection.offset / SettingsUserStore.messagesPerPage() + 1));
|
|
MessagelistUserStore.threadUid(collection.threadUid);
|
|
|
|
MessagelistUserStore.endHash(
|
|
folderInfo.name +
|
|
'|' + collection.search +
|
|
'|' + MessagelistUserStore.threadUid() +
|
|
'|' + MessagelistUserStore.page()
|
|
);
|
|
MessagelistUserStore.endThreadUid(collection.threadUid);
|
|
const message = MessageUserStore.message();
|
|
if (message && folderInfo.name !== message.folder) {
|
|
MessageUserStore.message(null);
|
|
}
|
|
|
|
disableAutoSelect(true);
|
|
|
|
if (collection.threadUid) {
|
|
let refs = {};
|
|
collection.forEach(msg => {
|
|
msg.level = 0;
|
|
if (msg.inReplyTo && refs[msg.inReplyTo]) {
|
|
msg.level = 1 + refs[msg.inReplyTo].level;
|
|
}
|
|
refs[msg.messageId] = msg;
|
|
});
|
|
}
|
|
|
|
MessagelistUserStore(collection);
|
|
MessagelistUserStore.isIncomplete(false);
|
|
} else {
|
|
MessagelistUserStore.count(0);
|
|
MessagelistUserStore([]);
|
|
error = getNotification(Notifications.CantGetMessageList);
|
|
}
|
|
MessagelistUserStore.loading(false);
|
|
}
|
|
MessagelistUserStore.error(error);
|
|
};
|
|
|
|
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
|
params.useThreads = 1;
|
|
params.threadUid = MessagelistUserStore.threadUid();
|
|
} else {
|
|
params.threadUid = 0;
|
|
}
|
|
if (folderETag) {
|
|
params.hash = folderETag + '-' + SettingsGet('accountHash');
|
|
sGetAdd = 'MessageList/' + SUB_QUERY_PREFIX + '/' + b64EncodeJSONSafe(params);
|
|
params = {};
|
|
}
|
|
|
|
Remote.abort('MessageList', 'reload').request('MessageList',
|
|
fCallback,
|
|
params,
|
|
60000, // 60 seconds before aborting
|
|
sGetAdd
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @param {string} sFolderFullName
|
|
* @param {number} iSetAction
|
|
* @param {Array=} messages = null
|
|
*/
|
|
MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
|
messages = messages || MessagelistUserStore.listChecked();
|
|
|
|
let folder,
|
|
rootUids = [],
|
|
length;
|
|
|
|
if (iSetAction == MessageSetAction.SetSeen) {
|
|
messages.forEach(oMessage => {
|
|
if (oMessage.isUnseen() && rootUids.push(oMessage.uid)) {
|
|
oMessage.flags.push('\\seen');
|
|
if (oMessage.threads().length > 0 && oMessage.threadUnseen().includes(oMessage.uid)) {
|
|
oMessage.threadUnseen.remove(oMessage.uid);
|
|
}
|
|
}
|
|
});
|
|
} else if (iSetAction == MessageSetAction.UnsetSeen) {
|
|
messages.forEach(oMessage => {
|
|
if (!oMessage.isUnseen() && rootUids.push(oMessage.uid)) {
|
|
oMessage.flags.remove('\\seen');
|
|
if (oMessage.threads().length > 0 && !oMessage.threadUnseen().includes(oMessage.uid)) {
|
|
oMessage.threadUnseen.push(oMessage.uid);
|
|
}
|
|
}
|
|
});
|
|
} else if (iSetAction == MessageSetAction.SetFlag) {
|
|
messages.forEach(oMessage =>
|
|
!oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\flagged')
|
|
);
|
|
} else if (iSetAction == MessageSetAction.UnsetFlag) {
|
|
messages.forEach(oMessage =>
|
|
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged')
|
|
);
|
|
}
|
|
rootUids = rootUids.validUnique();
|
|
length = rootUids.length;
|
|
|
|
if (sFolderFullName && length) {
|
|
switch (iSetAction) {
|
|
case MessageSetAction.SetSeen:
|
|
length = -length;
|
|
// fallthrough is intentionally
|
|
case MessageSetAction.UnsetSeen:
|
|
folder = getFolderFromCacheList(sFolderFullName);
|
|
if (folder) {
|
|
folder.unreadEmails(Math.max(0, folder.unreadEmails() + length));
|
|
}
|
|
Remote.request('MessageSetSeen', null, {
|
|
folder: sFolderFullName,
|
|
uids: rootUids.join(','),
|
|
setAction: iSetAction == MessageSetAction.SetSeen ? 1 : 0
|
|
});
|
|
break;
|
|
|
|
case MessageSetAction.SetFlag:
|
|
case MessageSetAction.UnsetFlag:
|
|
Remote.request('MessageSetFlagged', null, {
|
|
folder: sFolderFullName,
|
|
uids: rootUids.join(','),
|
|
setAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
|
|
});
|
|
break;
|
|
// no default
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} fromFolderFullName
|
|
* @param {Set} oUids
|
|
* @param {string=} toFolderFullName = ''
|
|
* @param {boolean=} copy = false
|
|
*/
|
|
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 toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
|
|
trashFolder = FolderUserStore.trashFolder(),
|
|
spamFolder = FolderUserStore.spamFolder(),
|
|
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 (!copy) {
|
|
fromFolder.etag = '';
|
|
fromFolder.totalEmails(Math.max(0, fromFolder.totalEmails() - oUids.size));
|
|
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
|
|
}
|
|
|
|
if (toFolder) {
|
|
toFolder.etag = '';
|
|
toFolder.totalEmails(toFolder.totalEmails() + oUids.size);
|
|
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 {
|
|
MessagelistUserStore.isIncomplete(true);
|
|
|
|
// Select next email https://github.com/the-djmaze/snappymail/issues/968
|
|
if (currentMessage && 1 == messages.length && SettingsUserStore.showNextMessage()) {
|
|
let next = MessagelistUserStore.indexOf(currentMessage) + 1;
|
|
if (0 < next && (next = MessagelistUserStore()[next])) {
|
|
currentMessage = null;
|
|
fireEvent('mailbox.message.show', {
|
|
folder: next.folder,
|
|
uid: next.uid
|
|
});
|
|
}
|
|
}
|
|
|
|
messages.forEach(item => {
|
|
if (currentMessage && currentMessage.hash === item.hash) {
|
|
currentMessage = null;
|
|
MessageUserStore.message(null);
|
|
}
|
|
item.deleted(true);
|
|
MessagelistUserStore.remove(item);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (toFolderFullName) {
|
|
if (toFolder && fromFolderFullName != toFolderFullName) {
|
|
const params = {
|
|
fromFolder: fromFolderFullName,
|
|
toFolder: toFolderFullName,
|
|
uids: [...oUids].join(',')
|
|
};
|
|
if (copy) {
|
|
Remote.request('MessageCopy', null, params);
|
|
} else {
|
|
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 {
|
|
Remote.abort('MessageList', 'reload').request('MessageDelete',
|
|
moveOrDeleteResponseHelper,
|
|
{
|
|
folder: fromFolderFullName,
|
|
uids: [...oUids].join(',')
|
|
}
|
|
);
|
|
}
|
|
};
|