import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils'; import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser'; import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache'; import { Settings, SettingsGet, fireEvent } from 'Common/Globals'; import * as Local from 'Storage/Client'; import { AppUserStore } from 'Stores/User/App'; import { FolderUserStore } from 'Stores/User/Folder'; import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { SettingsUserStore } from 'Stores/User/Settings'; import { sortFolders } from 'Common/Folders'; import { i18n, translateTrigger } from 'Common/Translator'; import { AbstractModel } from 'Knoin/AbstractModel'; import { /*koComputable,*/ addObservablesTo } from 'External/ko'; import { mailBox } from 'Common/Links'; import Remote from 'Remote/User/Fetch'; import { FileInfo } from 'Common/File'; import { FolderPopupView } from 'View/Popup/Folder'; import { showScreenPopup } from 'Knoin/Knoin'; import { isAllowedKeyword } from 'Stores/User/Folder'; const // isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()), normalizeFolder = sFolderFullName => ('' === sFolderFullName || UNUSED_OPTION_VALUE === sFolderFullName || null !== getFolderFromCacheList(sFolderFullName)) ? sFolderFullName : '', SystemFolders = { Inbox: 0, Sent: 0, Drafts: 0, Junk: 0, // Spam Trash: 0, Archive: 0 }, kolabTypes = { configuration: 'CONFIGURATION', event: 'CALENDAR', contact: 'CONTACTS', task: 'TASKS', note: 'NOTES', file: 'FILES', journal: 'JOURNAL' }, getKolabFolderName = type => kolabTypes[type] ? 'Kolab ' + i18n('SETTINGS_FOLDERS/TYPE_' + kolabTypes[type]) : '', getSystemFolderName = (type, def) => { switch (type) { case FolderType.Inbox: case FolderType.Sent: case FolderType.Drafts: case FolderType.Trash: case FolderType.Archive: return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME'); case FolderType.Junk: return i18n('GLOBAL/SPAM'); // no default } return def; }; export const /** * @param {string} sFullName * @param {boolean} bExpanded */ setExpandedFolder = (sFullName, bExpanded) => { let aExpandedList = Local.get(ClientSideKeyNameExpandedFolders); aExpandedList = new Set(isArray(aExpandedList) ? aExpandedList : []); bExpanded ? aExpandedList.add(sFullName) : aExpandedList.delete(sFullName); Local.set(ClientSideKeyNameExpandedFolders, [...aExpandedList]); }, foldersFilter = ko.observable(''), /** * @param {?Function} fCallback */ loadFolders = fCallback => { // clearTimeout(this.foldersTimeout); Remote.abort('Folders') .post('Folders', FolderUserStore.foldersLoading) .then(data => { clearCache(); FolderCollectionModel.reviveFromJson(data.Result)?.storeIt(); fCallback?.(true); // Repeat every 15 minutes? // this.foldersTimeout = setTimeout(loadFolders, 900000); }) .catch(e => fCallback && setTimeout(fCallback, 1, false, e)); }; export class FolderCollectionModel extends AbstractCollectionModel { /* constructor() { super(); this.quotaUsage; this.quotaLimit; this.namespace; this.optimized this.capabilities this.allow; // allow adding // this.exist; } */ /** * @param {?Object} json * @returns {FolderCollectionModel} */ static reviveFromJson(object) { const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders); forEachObjectEntry(SystemFolders, (key, value) => value || (SystemFolders[key] = SettingsGet(key+'Folder')) ); const result = super.reviveFromJson(object, oFolder => { let oCacheFolder = getFolderFromCacheList(oFolder.fullName); if (oCacheFolder) { // oCacheFolder.revivePropertiesFromJson(oFolder); if (oFolder.etag) { oCacheFolder.etag = oFolder.etag; } if (null != oFolder.totalEmails) { oCacheFolder.totalEmails(oFolder.totalEmails); } if (null != oFolder.unreadEmails) { oCacheFolder.unreadEmails(oFolder.unreadEmails); } } else { oCacheFolder = FolderModel.reviveFromJson(oFolder); if (!oCacheFolder) return null; setFolder(oCacheFolder); } // JMAP RFC 8621 let role = oFolder.role; /* if (!role) { // Kolab let type = oFolder.metadata[FolderMetadataKeys.KolabFolderType] || oFolder.metadata[FolderMetadataKeys.KolabFolderTypeShared]; switch (type) { case 'mail.inbox': case 'mail.drafts': role = type.replace('mail.', ''); break; // case 'mail.outbox': case 'mail.sentitems': role = 'sent'; break; case 'mail.junkemail': role = 'spam'; break; case 'mail.wastebasket': role = 'trash'; break; } // Flags if (oFolder.attributes.includes('\\sentmail')) { role = 'sent'; } if (oFolder.attributes.includes('\\spam')) { role = 'junk'; } if (oFolder.attributes.includes('\\bin')) { role = 'trash'; } if (oFolder.attributes.includes('\\important')) { role = 'important'; } if (oFolder.attributes.includes('\\starred')) { role = 'flagged'; } if (oFolder.attributes.includes('\\all') || oFolder.flags.includes('\\allmail')) { role = 'all'; } } */ if (role) { role = role[0].toUpperCase() + role.slice(1); SystemFolders[role] || (SystemFolders[role] = oFolder.fullName); } oCacheFolder.type(FolderType[getKeyByValue(SystemFolders, oFolder.fullName)] || 0); oCacheFolder.collapsed(!expandedFolders || !isArray(expandedFolders) || !expandedFolders.includes(oCacheFolder.fullName)); return oCacheFolder; }); result.CountRec = result.length; setFolderInboxName(SystemFolders.Inbox); let i = result.length; if (i) { sortFolders(result); try { while (i--) { let folder = result[i], parent = getFolderFromCacheList(folder.parentName); if (!parent) { // Create NonExistent parent folders let delimiter = folder.delimiter; if (delimiter) { let parents = folder.fullName.split(delimiter); parents.pop(); while (parents.length) { let parentName = parents.join(delimiter), name = parents.pop(), pfolder = getFolderFromCacheList(parentName); if (!pfolder) { console.log('Create nonexistent folder ' + parentName); pfolder = FolderModel.reviveFromJson({ '@Object': 'Object/Folder', name: name, fullName: parentName, delimiter: delimiter, attributes: ['\\nonexistent'] }); setFolder(pfolder); result.splice(i, 0, pfolder); ++i; } } parent = getFolderFromCacheList(folder.parentName); } } if (parent) { parent.subFolders.unshift(folder); result.splice(i,1); } } } catch (e) { console.error(e); } } return result; } visible() { return this.filter(folder => folder.visible()); } storeIt() { FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec); if (!( SettingsGet('SentFolder') + SettingsGet('DraftsFolder') + SettingsGet('JunkFolder') + SettingsGet('TrashFolder') + SettingsGet('ArchiveFolder') ) ) { FolderUserStore.saveSystemFolders(SystemFolders); } FolderUserStore.folderList(this); FolderUserStore.namespace = this.namespace; // 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT' AppUserStore.threadsAllowed(!!this.capabilities.some(capa => capa.startsWith('THREAD='))); // FolderUserStore.optimized(!!this.optimized); FolderUserStore.quotaUsage(this.quotaUsage); FolderUserStore.quotaLimit(this.quotaLimit); FolderUserStore.capabilities(this.capabilities); FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent)); FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts)); FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Junk)); FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash)); FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive)); // FolderUserStore.folderList.valueHasMutated(); } } export class FolderModel extends AbstractModel { constructor() { super(); this.fullName = ''; this.parentName = ''; this.delimiter = ''; this.deep = 0; this.expires = 0; this.metadata = {}; this.exists = true; this.etag = ''; this.id = 0; this.uidNext = 0; this.size = 0; addObservablesTo(this, { name: '', type: 0, role: null, selectable: false, focused: false, selected: false, isSubscribed: true, checkable: false, // Check for new messages askDelete: false, errorMsg: '', totalEmails: 0, unreadEmails: 0, kolabType: null, collapsed: true, tagsAllowed: false }); this.attributes = ko.observableArray(); // For messages this.permanentFlags = ko.observableArray(); this.addSubscribables({ kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue, permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')), unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread) }); this.subFolders = ko.observableArray(new FolderCollectionModel); this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 }); /* this.totalEmails = koComputable({ read: this.totalEmailsValue, write: iValue => isPosNumeric(iValue) ? this.totalEmailsValue(iValue) : this.totalEmailsValue.valueHasMutated() }) .extend({ notify: 'always' }); this.unreadEmails = koComputable({ read: this.unreadEmailsValue, write: value => isPosNumeric(value) ? this.unreadEmailsValue(value) : this.unreadEmailsValue.valueHasMutated() }) .extend({ notify: 'always' }); */ /* // https://www.rfc-editor.org/rfc/rfc8621.html#section-2 this.myRights = { 'mayAddItems': true, 'mayCreateChild': true, 'mayDelete': true, 'mayReadItems': true, 'mayRemoveItems': true, 'mayRename': true, 'maySetKeywords': true, 'maySetSeen': true, 'maySubmit': true }; */ this.addComputables({ isInbox: () => FolderType.Inbox === this.type(), isFlagged: () => FolderUserStore.currentFolder() === this && MessagelistUserStore.listSearch().includes('flagged'), // isSubscribed: () => this.attributes().includes('\\subscribed'), hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()), visibleSubfolders: () => this.subFolders().visible(), hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find( oFolder => { const subscribed = oFolder.hasSubscriptions(); return !oFolder.isSystemFolder() && subscribed; } ), isSystemFolder: () => this.type() | (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()), canBeSelected: () => this.selectable() && !this.isSystemFolder(), canBeDeleted: () => this.canBeSelected() && this.exists, canBeSubscribed: () => this.selectable() && !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()), optionalTags: () => this.permanentFlags.filter(isAllowedKeyword), /** * Folder is visible when: * - hasVisibleSubfolders() * Or when all below conditions are true: * - selectable() * - isSubscribed() OR hideUnsubscribed = false * - 0 == type() * - not kolabType() */ visible: () => { const selectable = this.canBeSelected(), name = this.name(), filter = foldersFilter(), visible = (this.isSubscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable && (!filter || name.toLowerCase().includes(filter.toLowerCase())); return this.hasVisibleSubfolders() | visible; }, unreadCount: () => this.unreadEmails() || null, /* { // TODO: make this optional in Settings // https://github.com/the-djmaze/snappymail/issues/457 // https://github.com/the-djmaze/snappymail/issues/567 const unread = this.unreadEmails(), type = this.type(); // return ((!this.isSystemFolder() || type == FolderType.Inbox) && unread) ? unread : null; }, */ localName: () => { let name = this.name(); if (this.isSystemFolder()) { translateTrigger(); name = getSystemFolderName(this.type(), name); } return name; }, nameInfo: () => { if (this.isSystemFolder()) { translateTrigger(); let suffix = getSystemFolderName(this.type(), getKolabFolderName(this.kolabType())); if (this.name() !== suffix && 'inbox' !== suffix.toLowerCase()) { return ' (' + suffix + ')'; } } return ''; }, friendlySize: () => FileInfo.friendlySize(this.size), detailedName: () => this.name() + ' ' + this.nameInfo(), icon: () => { switch (this.type()) { case 1: return '📥'; // FolderType.Inbox case 2: return '📧'; // FolderType.Sent icon-paper-plane case 3: return '🗎'; // FolderType.Drafts case 4: return '⚠'; // FolderType.Junk case 5: return '🗑'; // FolderType.Trash case 6: return '🗄'; // FolderType.Archive } return null; }, hasUnreadInSub: () => this.subFolders().some( folder => folder.unreadEmails() | folder.hasUnreadInSub() ), href: () => this.canBeSelected() && mailBox(this.fullNameHash) }); } edit() { showScreenPopup(FolderPopupView, [this]); } /** * For url safe '/#/mailbox/...' path */ get fullNameHash() { return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe); // return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName); } /** * @static * @param {FetchJsonFolder} json * @returns {?FolderModel} */ static reviveFromJson(json) { const folder = super.reviveFromJson(json); if (folder) { const path = folder.fullName.split(folder.delimiter), attr = name => folder.attributes.includes(name), type = (folder.metadata[FolderMetadataKeys.KolabFolderType] || folder.metadata[FolderMetadataKeys.KolabFolderTypeShared] || '' ).split('.')[0]; folder.deep = path.length - 1; path.pop(); folder.parentName = path.join(folder.delimiter); folder.isSubscribed(attr('\\subscribed')); folder.exists = !attr('\\nonexistent'); folder.subFolders.allow = !attr('\\noinferiors'); // folder.subFolders.exist = attr('\\haschildren') || !attr('\\hasnochildren'); folder.selectable(folder.exists && !attr('\\noselect')); type && 'mail' != type && folder.kolabType(type); } return folder; } /** * @returns {string} */ collapsedCss() { return 'e-collapsed-sign ' + (this.hasVisibleSubfolders() ? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini') : 'icon-none' ); } }