import ko from 'ko'; import { MessagePriority } from 'Common/EnumsUser'; import { i18n } from 'Common/Translator'; import { doc, SettingsGet } from 'Common/Globals'; import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html'; import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils'; import { serverRequestRaw, proxy } from 'Common/Links'; import { FolderUserStore } from 'Stores/User/Folder'; import { SettingsUserStore } from 'Stores/User/Settings'; import { FileInfo } from 'Common/File'; import { AttachmentCollectionModel } from 'Model/AttachmentCollection'; import { EmailCollectionModel } from 'Model/EmailCollection'; import { AbstractModel } from 'Knoin/AbstractModel'; import PreviewHTML from 'Html/PreviewMessage.html'; //import { MessageFlagsCache } from 'Common/Cache'; import Remote from 'Remote/User/Fetch'; const // eslint-disable-next-line max-len url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi, // eslint-disable-next-line max-len email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi, hcont = Element.fromHTML('
'), getRealHeight = el => { hcont.innerHTML = el.outerHTML; const result = hcont.clientHeight; hcont.innerHTML = ''; return result; }, ignoredTags = [ // rfc5788 '$forwarded', '$mdnsent', '$submitpending', '$submitted', // rfc9051 '$junk', '$notjunk', '$phishing', // Mailo 'sent', // KMail '$attachment', '$encrypted', '$error', '$ignored', '$invitation', '$queued', '$replied', '$sent', '$signed', '$todo', '$watched', // GMail '$replied', '$attachment', '$notphishing', 'junk', 'nonjunk', // Others '$readreceipt' ], toggleTag = (message, keyword) => { const lower = keyword.toLowerCase(), isSet = message.flags().includes(lower); Remote.request('MessageSetKeyword', iError => { if (!iError) { if (isSet) { message.flags.remove(lower); } else { message.flags.push(lower); } // MessageFlagsCache.setFor(message.folder, message.uid, message.flags()); } }, { Folder: message.folder, Uids: message.uid, Keyword: keyword, SetAction: isSet ? 0 : 1 }) }, /** * @param {EmailCollectionModel} emails * @param {Object} unic * @param {Map} localEmails */ replyHelper = (emails, unic, localEmails) => { emails.forEach(email => { if (!unic[email.email] && !localEmails.has(email.email)) { localEmails.set(email.email, email); } }); }; doc.body.append(hcont); export class MessageModel extends AbstractModel { constructor() { super(); this._reset(); this.addObservables({ subject: '', plain: '', html: '', size: 0, spamScore: 0, spamResult: '', isSpam: false, hasVirus: null, // or boolean when scanned dateTimeStampInUTC: 0, priority: MessagePriority.Normal, senderEmailsString: '', senderClearEmailsString: '', deleted: false, // Also used by Selector focused: false, selected: false, checked: false, isHtml: false, hasImages: false, hasExternals: false, pgpSigned: null, pgpVerified: null, pgpEncrypted: null, pgpDecrypted: false, readReceipt: '', hasUnseenSubMessage: false, hasFlaggedSubMessage: false }); this.attachments = ko.observableArray(new AttachmentCollectionModel); this.threads = ko.observableArray(); this.unsubsribeLinks = ko.observableArray(); this.flags = ko.observableArray(); this.addComputables({ attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()), threadsLen: () => this.threads().length, isImportant: () => MessagePriority.High === this.priority(), hasAttachments: () => this.attachments().hasVisible(), isUnseen: () => !this.flags().includes('\\seen'), isFlagged: () => this.flags().includes('\\flagged'), isReadReceipt: () => this.flags().includes('$mdnsent'), // isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'), // isPhishing: () => this.flags().includes('$phishing'), tagsToHTML: () => this.flags().map(value => ('\\' == value[0] || ignoredTags.includes(value)) ? '' : '' + i18n('MESSAGE_TAGS/'+value,0,value) + '' ).join(' '), tagOptions: () => { const tagOptions = []; FolderUserStore.currentFolder().permanentFlags.forEach(value => { let lower = value.toLowerCase(); if ('\\' != value[0] && !ignoredTags.includes(lower)) { tagOptions.push({ css: 'msgflag-' + lower, value: value, checked: this.flags().includes(lower), label: i18n('MESSAGE_TAGS/'+lower, 0, lower), toggle: (/*obj*/) => toggleTag(this, value) }); } }); return tagOptions } }); } _reset() { this.folder = ''; this.uid = 0; this.hash = ''; this.requestHash = ''; this.emails = []; this.from = new EmailCollectionModel; this.to = new EmailCollectionModel; this.cc = new EmailCollectionModel; this.bcc = new EmailCollectionModel; this.replyTo = new EmailCollectionModel; this.deliveredTo = new EmailCollectionModel; this.body = null; this.draftInfo = []; this.messageId = ''; this.inReplyTo = ''; this.references = ''; } clear() { this._reset(); this.subject(''); this.html(''); this.plain(''); this.size(0); this.spamScore(0); this.spamResult(''); this.isSpam(false); this.hasVirus(null); this.dateTimeStampInUTC(0); this.priority(MessagePriority.Normal); this.senderEmailsString(''); this.senderClearEmailsString(''); this.deleted(false); this.selected(false); this.checked(false); this.isHtml(false); this.hasImages(false); this.hasExternals(false); this.attachments(new AttachmentCollectionModel); this.pgpSigned(null); this.pgpVerified(null); this.pgpEncrypted(null); this.pgpDecrypted(false); this.priority(MessagePriority.Normal); this.readReceipt(''); this.threads([]); this.unsubsribeLinks([]); this.hasUnseenSubMessage(false); this.hasFlaggedSubMessage(false); } spamStatus() { let spam = this.spamResult(); return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : ''; } /** * @param {Array} properties * @returns {Array} */ getEmails(properties) { return properties.reduce((carry, property) => carry.concat(this[property]), []).map( oItem => oItem ? oItem.email : '' ).validUnique(); } /** * @returns {string} */ friendlySize() { return FileInfo.friendlySize(this.size()); } computeSenderEmail() { const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from'; this.senderEmailsString(this[list].toString(true)); this.senderClearEmailsString(this[list].toStringClear()); } /** * @param {FetchJsonMessage} json * @returns {boolean} */ revivePropertiesFromJson(json) { if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) { json.Priority = MessagePriority.Normal; } if (super.revivePropertiesFromJson(json)) { // this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : []; // this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs)); this.computeSenderEmail(); } } /** * @returns {boolean} */ hasUnsubsribeLinks() { return this.unsubsribeLinks().length; } /** * @returns {string} */ getFirstUnsubsribeLink() { return this.unsubsribeLinks()[0] || ''; } /** * @param {boolean} friendlyView * @param {boolean=} wrapWithLink * @returns {string} */ fromToLine(friendlyView, wrapWithLink) { return this.from.toString(friendlyView, wrapWithLink); } /** * @returns {string} */ fromDkimData() { let result = ['none', '']; if (1 === arrayLength(this.from) && this.from[0]?.dkimStatus) { result = [this.from[0].dkimStatus, this.from[0].dkimValue || '']; } return result; } /** * @param {boolean} friendlyView * @param {boolean=} wrapWithLink * @returns {string} */ toToLine(friendlyView, wrapWithLink) { return this.to.toString(friendlyView, wrapWithLink); } /** * @param {boolean} friendlyView * @param {boolean=} wrapWithLink * @returns {string} */ ccToLine(friendlyView, wrapWithLink) { return this.cc.toString(friendlyView, wrapWithLink); } /** * @param {boolean} friendlyView * @param {boolean=} wrapWithLink * @returns {string} */ bccToLine(friendlyView, wrapWithLink) { return this.bcc.toString(friendlyView, wrapWithLink); } /** * @param {boolean} friendlyView * @param {boolean=} wrapWithLink * @returns {string} */ replyToToLine(friendlyView, wrapWithLink) { return this.replyTo.toString(friendlyView, wrapWithLink); } /** * @return string */ lineAsCss() { let classes = []; forEachObjectEntry({ deleted: this.deleted(), selected: this.selected(), checked: this.checked(), unseen: this.isUnseen(), focused: this.focused(), important: this.isImportant(), withAttachments: !!this.attachments().length, // hasChildrenMessage: 1 < this.threadsLen(), hasUnseenSubMessage: this.hasUnseenSubMessage(), hasFlaggedSubMessage: this.hasFlaggedSubMessage() }, (key, value) => value && classes.push(key)); this.flags().forEach(value => classes.push('msgflag-'+value)); return classes.join(' '); } /** * @return array * https://datatracker.ietf.org/doc/html/rfc5788 */ keywords() { return this.flags().filter(value => '\\' !== value[0]); } /** * @returns {string} */ fromAsSingleEmail() { return (isArray(this.from) && this.from[0]?.email) || ''; } /** * @returns {string} */ viewLink() { return serverRequestRaw('ViewAsPlain', this.requestHash); } /** * @returns {string} */ downloadLink() { return serverRequestRaw('Download', this.requestHash); } /** * @param {Object} excludeEmails * @returns {Array} */ replyEmails(excludeEmails) { const result = new Map(), unic = excludeEmails || {}; replyHelper(this.replyTo, unic, result); result.size || replyHelper(this.from, unic, result); return result.size ? [...result.values()] : [this.to[0]]; } /** * @param {Object} excludeEmails * @returns {Array.