snappymail/dev/Model/Message.js

647 lines
16 KiB
JavaScript
Raw Normal View History

2016-07-07 05:03:30 +08:00
import ko from 'ko';
2021-01-25 05:58:06 +08:00
import { MessagePriority } from 'Common/EnumsUser';
2019-07-05 03:19:24 +08:00
import { i18n } from 'Common/Translator';
2016-07-07 05:03:30 +08:00
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';
2016-07-07 05:03:30 +08:00
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';
2019-07-05 03:19:24 +08:00
import { AbstractModel } from 'Knoin/AbstractModel';
2016-07-07 05:03:30 +08:00
import PreviewHTML from 'Html/PreviewMessage.html';
//import { MessageFlagsCache } from 'Common/Cache';
2022-06-03 19:47:04 +08:00
import Remote from 'Remote/User/Fetch';
const
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
2022-01-20 23:38:27 +08:00
// 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,
// rfc3966
tel = /(^|[\s\n]|\/?>)(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
getRealHeight = el => {
hcont.innerHTML = el.outerHTML;
const result = hcont.clientHeight;
hcont.innerHTML = '';
return result;
},
ignoredTags = [
// rfc5788
'$forwarded',
2022-06-10 06:35:13 +08:00
'$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'
],
2022-06-03 19:47:04 +08:00
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());
2022-06-03 19:47:04 +08:00
}
}, {
Folder: message.folder,
Uids: message.uid,
Keyword: keyword,
SetAction: isSet ? 0 : 1
})
},
2022-08-25 20:08:19 +08:00
/**
* @param {EmailCollectionModel} emails
* @param {Object} unic
* @param {Map} localEmails
*/
replyHelper = (emails, unic, localEmails) => {
emails.forEach(email => {
2022-08-25 20:08:19 +08:00
if (!unic[email.email] && !localEmails.has(email.email)) {
localEmails.set(email.email, email);
}
});
};
doc.body.append(hcont);
2021-01-22 23:32:08 +08:00
export class MessageModel extends AbstractModel {
2016-07-16 05:29:42 +08:00
constructor() {
2020-10-19 01:19:45 +08:00
super();
2016-07-07 05:03:30 +08:00
this._reset();
2020-10-25 18:46:58 +08:00
this.addObservables({
subject: '',
plain: '',
html: '',
2020-10-25 18:46:58 +08:00
size: 0,
spamScore: 0,
2021-04-09 15:01:48 +08:00
spamResult: '',
isSpam: false,
2021-09-02 18:09:16 +08:00
hasVirus: null, // or boolean when scanned
2020-10-25 18:46:58 +08:00
dateTimeStampInUTC: 0,
priority: MessagePriority.Normal,
senderEmailsString: '',
senderClearEmailsString: '',
deleted: false,
// Also used by Selector
2020-10-25 18:46:58 +08:00
focused: false,
selected: false,
checked: false,
isHtml: false,
hasImages: false,
hasExternals: false,
2020-10-25 18:46:58 +08:00
pgpSigned: null,
pgpVerified: null,
2020-10-25 18:46:58 +08:00
pgpEncrypted: null,
pgpDecrypted: false,
2020-10-25 18:46:58 +08:00
readReceipt: '',
hasUnseenSubMessage: false,
hasFlaggedSubMessage: false
});
2016-07-07 05:03:30 +08:00
2020-10-25 18:46:58 +08:00
this.attachments = ko.observableArray(new AttachmentCollectionModel);
this.threads = ko.observableArray();
2021-09-01 22:10:44 +08:00
this.unsubsribeLinks = ko.observableArray();
this.flags = ko.observableArray();
2016-07-07 05:03:30 +08:00
2020-10-25 21:14:14 +08:00
this.addComputables({
attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
threadsLen: () => this.threads().length,
2022-02-15 19:08:52 +08:00
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'),
2022-06-03 05:13:20 +08:00
tagsToHTML: () => this.flags().map(value =>
('\\' == value[0] || ignoredTags.includes(value))
? ''
2022-06-03 05:13:20 +08:00
: '<span class="focused msgflag-'+value+'">' + i18n('MESSAGE_TAGS/'+value,0,value) + '</span>'
2022-06-03 19:47:04 +08:00
).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
}
2020-10-25 21:14:14 +08:00
});
2016-07-07 05:03:30 +08:00
}
_reset() {
2020-10-23 21:15:54 +08:00
this.folder = '';
2021-09-10 22:28:29 +08:00
this.uid = 0;
2016-07-07 05:03:30 +08:00
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;
2020-10-23 21:15:54 +08:00
this.draftInfo = [];
this.messageId = '';
this.inReplyTo = '';
this.references = '';
}
clear() {
this._reset();
2016-07-07 05:03:30 +08:00
this.subject('');
this.html('');
this.plain('');
2016-07-07 05:03:30 +08:00
this.size(0);
this.spamScore(0);
2021-04-09 15:01:48 +08:00
this.spamResult('');
this.isSpam(false);
2021-09-02 18:09:16 +08:00
this.hasVirus(null);
2016-07-07 05:03:30 +08:00
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);
2016-07-07 05:03:30 +08:00
this.pgpSigned(null);
this.pgpVerified(null);
2016-07-07 05:03:30 +08:00
this.pgpEncrypted(null);
this.pgpDecrypted(false);
2016-07-07 05:03:30 +08:00
this.priority(MessagePriority.Normal);
this.readReceipt('');
this.threads([]);
2021-09-01 22:10:44 +08:00
this.unsubsribeLinks([]);
2016-07-07 05:03:30 +08:00
this.hasUnseenSubMessage(false);
this.hasFlaggedSubMessage(false);
}
spamStatus() {
let spam = this.spamResult();
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
}
2016-07-07 05:03:30 +08:00
/**
* @param {Array} properties
* @returns {Array}
*/
getEmails(properties) {
return properties.reduce((carry, property) => carry.concat(this[property]), []).map(
oItem => oItem ? oItem.email : ''
).validUnique();
2016-07-07 05:03:30 +08:00
}
/**
* @returns {string}
*/
friendlySize() {
return FileInfo.friendlySize(this.size());
2016-07-07 05:03:30 +08:00
}
computeSenderEmail() {
2021-12-01 20:54:35 +08:00
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from';
2020-10-23 21:15:54 +08:00
this.senderEmailsString(this[list].toString(true));
this.senderClearEmailsString(this[list].toStringClear());
2016-07-07 05:03:30 +08:00
}
/**
* @param {FetchJsonMessage} json
2016-07-07 05:03:30 +08:00
* @returns {boolean}
*/
2020-10-23 21:15:54 +08:00
revivePropertiesFromJson(json) {
2021-09-10 22:28:29 +08:00
if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
json.Priority = MessagePriority.Normal;
2020-10-23 21:15:54 +08:00
}
if (super.revivePropertiesFromJson(json)) {
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
2016-07-07 05:03:30 +08:00
this.computeSenderEmail();
}
}
2016-07-17 23:03:38 +08:00
/**
* @returns {boolean}
*/
hasUnsubsribeLinks() {
2021-09-01 22:10:44 +08:00
return this.unsubsribeLinks().length;
2016-07-17 23:03:38 +08:00
}
/**
* @returns {string}
*/
getFirstUnsubsribeLink() {
2021-09-01 22:10:44 +08:00
return this.unsubsribeLinks()[0] || '';
2016-07-17 23:03:38 +08:00
}
2016-07-07 05:03:30 +08:00
/**
* @param {boolean} friendlyView
* @param {boolean=} wrapWithLink
2016-07-07 05:03:30 +08:00
* @returns {string}
*/
fromToLine(friendlyView, wrapWithLink) {
return this.from.toString(friendlyView, wrapWithLink);
2016-07-07 05:03:30 +08:00
}
/**
* @returns {string}
*/
fromDkimData() {
let result = ['none', ''];
2022-09-02 17:52:07 +08:00
if (1 === arrayLength(this.from) && this.from[0]?.dkimStatus) {
2016-07-07 05:03:30 +08:00
result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
}
return result;
}
/**
* @param {boolean} friendlyView
* @param {boolean=} wrapWithLink
2016-07-07 05:03:30 +08:00
* @returns {string}
*/
toToLine(friendlyView, wrapWithLink) {
return this.to.toString(friendlyView, wrapWithLink);
2016-07-07 05:03:30 +08:00
}
/**
* @param {boolean} friendlyView
* @param {boolean=} wrapWithLink
2016-07-07 05:03:30 +08:00
* @returns {string}
*/
ccToLine(friendlyView, wrapWithLink) {
return this.cc.toString(friendlyView, wrapWithLink);
2016-07-07 05:03:30 +08:00
}
/**
* @param {boolean} friendlyView
* @param {boolean=} wrapWithLink
2016-07-07 05:03:30 +08:00
* @returns {string}
*/
bccToLine(friendlyView, wrapWithLink) {
return this.bcc.toString(friendlyView, wrapWithLink);
2016-07-07 05:03:30 +08:00
}
/**
* @param {boolean} friendlyView
* @param {boolean=} wrapWithLink
2016-07-07 05:03:30 +08:00
* @returns {string}
*/
replyToToLine(friendlyView, wrapWithLink) {
return this.replyTo.toString(friendlyView, wrapWithLink);
2016-07-07 05:03:30 +08:00
}
/**
* @return string
*/
2022-09-13 05:13:04 +08:00
lineAsCss(flags=1) {
2020-08-12 07:47:24 +08:00
let classes = [];
forEachObjectEntry({
deleted: this.deleted(),
selected: this.selected(),
checked: this.checked(),
unseen: this.isUnseen(),
focused: this.focused(),
2022-09-13 05:13:04 +08:00
priorityHigh: this.priority() === MessagePriority.High,
withAttachments: !!this.attachments().length,
// hasChildrenMessage: 1 < this.threadsLen(),
hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
}, (key, value) => value && classes.push(key));
2022-09-13 05:13:04 +08:00
flags && this.flags().forEach(value => classes.push('msgflag-'+value));
2020-08-12 07:47:24 +08:00
return classes.join(' ');
2016-07-07 05:03:30 +08:00
}
/**
* @return array
* https://datatracker.ietf.org/doc/html/rfc5788
*/
keywords() {
return this.flags().filter(value => '\\' !== value[0]);
}
2016-07-07 05:03:30 +08:00
/**
* @returns {string}
*/
fromAsSingleEmail() {
2022-09-02 17:52:07 +08:00
return (isArray(this.from) && this.from[0]?.email) || '';
2016-07-07 05:03:30 +08:00
}
/**
* @returns {string}
*/
viewLink() {
2021-02-04 18:25:00 +08:00
return serverRequestRaw('ViewAsPlain', this.requestHash);
2016-07-07 05:03:30 +08:00
}
/**
* @returns {string}
*/
downloadLink() {
2021-02-04 18:25:00 +08:00
return serverRequestRaw('Download', this.requestHash);
2016-07-07 05:03:30 +08:00
}
/**
* @param {Object} excludeEmails
* @returns {Array}
*/
2022-08-25 20:08:19 +08:00
replyEmails(excludeEmails) {
const
result = new Map(),
unic = excludeEmails || {};
2016-07-07 05:03:30 +08:00
replyHelper(this.replyTo, unic, result);
2022-08-25 20:08:19 +08:00
result.size || replyHelper(this.from, unic, result);
return result.size ? [...result.values()] : [this.to[0]];
2016-07-07 05:03:30 +08:00
}
/**
* @param {Object} excludeEmails
* @returns {Array.<Array>}
*/
2022-08-25 20:08:19 +08:00
replyAllEmails(excludeEmails) {
const
toResult = new Map(),
ccResult = new Map(),
unic = excludeEmails || {};
2016-07-07 05:03:30 +08:00
replyHelper(this.replyTo, unic, toResult);
2022-08-25 20:08:19 +08:00
if (!toResult.size) {
2016-07-07 05:03:30 +08:00
replyHelper(this.from, unic, toResult);
}
replyHelper(this.to, unic, toResult);
2022-08-25 20:08:19 +08:00
replyHelper(this.cc, unic, ccResult);
2016-07-07 05:03:30 +08:00
2022-08-31 03:42:05 +08:00
return [[...toResult.values()], [...ccResult.values()]];
2016-07-07 05:03:30 +08:00
}
viewHtml() {
const body = this.body;
if (body && this.html()) {
2022-05-12 05:13:24 +08:00
let result = cleanHtml(this.html(), this.attachments(), SettingsUserStore.removeColors());
this.hasExternals(result.hasExternals);
this.hasImages(body.rlHasImages = !!result.hasExternals);
body.innerHTML = result.html;
body.classList.toggle('html', 1);
body.classList.toggle('plain', 0);
if (SettingsUserStore.showImages()) {
this.showExternalImages();
}
this.isHtml(true);
this.initView();
return true;
}
}
viewPlain() {
const body = this.body;
if (body && this.plain()) {
body.classList.toggle('html', 0);
body.classList.toggle('plain', 1);
body.innerHTML = plainToHtml(
this.plain()
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
.trim()
)
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
.replace(email, '$1<a href="mailto:$2">$2</a>')
.replace(tel, '$1<a href="$2">$2</a>');
this.isHtml(false);
this.hasImages(false);
this.initView();
return true;
}
}
initView() {
// init BlockquoteSwitcher
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
node.removeAttribute('style')
2022-09-13 05:16:59 +08:00
if (node.textContent.trim() && !node.parentNode.closest?.('blockquote')) {
let h = node.clientHeight || getRealHeight(node);
if (0 === h || 100 < h) {
const el = Element.fromHTML('<span class="rlBlockquoteSwitcher">•••</span>');
node.classList.add('rl-bq-switcher','hidden-bq');
node.before(el);
el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
}
}
});
}
viewPopupMessage(print) {
2019-07-05 03:19:24 +08:00
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.ccToLine(false),
2020-08-27 21:45:47 +08:00
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
win = open(''),
sdoc = win.document;
let subject = encodeHtml(this.subject()),
mode = this.isHtml() ? 'div' : 'pre',
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
style = getComputedStyle(doc.querySelector('.messageView')),
prop = property => style.getPropertyValue(property);
sdoc.write(PreviewHTML
.replace('<title>', '<title>'+subject)
// eslint-disable-next-line max-len
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine(false))}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine(false))}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
2019-07-05 03:19:24 +08:00
);
sdoc.close();
2020-08-27 21:45:47 +08:00
if (print) {
setTimeout(() => win.print(), 100);
}
2016-07-07 05:03:30 +08:00
}
/**
* @param {boolean=} print = false
*/
popupMessage() {
this.viewPopupMessage(false);
}
2016-07-07 05:03:30 +08:00
printMessage() {
this.viewPopupMessage(true);
}
/**
* @returns {string}
*/
generateUid() {
2020-10-23 21:15:54 +08:00
return this.folder + '/' + this.uid;
2016-07-07 05:03:30 +08:00
}
/**
* @param {MessageModel} message
* @returns {MessageModel}
*/
static fromMessageListItem(message) {
let self = new MessageModel();
2019-07-05 03:19:24 +08:00
if (message) {
self.folder = message.folder;
self.uid = message.uid;
self.hash = message.hash;
self.requestHash = message.requestHash;
self.subject(message.subject());
self.plain(message.plain());
self.html(message.html());
self.size(message.size());
self.spamScore(message.spamScore());
self.spamResult(message.spamResult());
self.isSpam(message.isSpam());
self.hasVirus(message.hasVirus());
self.dateTimeStampInUTC(message.dateTimeStampInUTC());
self.priority(message.priority());
self.hasExternals(message.hasExternals());
self.emails = message.emails;
self.from = message.from;
self.to = message.to;
self.cc = message.cc;
self.bcc = message.bcc;
self.replyTo = message.replyTo;
self.deliveredTo = message.deliveredTo;
self.unsubsribeLinks(message.unsubsribeLinks);
self.flags(message.flags());
self.priority(message.priority());
self.selected(message.selected());
self.checked(message.checked());
self.attachments(message.attachments());
self.threads(message.threads());
2016-07-07 05:03:30 +08:00
}
self.computeSenderEmail();
2016-07-07 05:03:30 +08:00
return self;
2016-07-07 05:03:30 +08:00
}
2020-08-27 21:45:47 +08:00
showExternalImages() {
const body = this.body;
if (body && this.hasImages()) {
2016-07-07 05:03:30 +08:00
this.hasImages(false);
body.rlHasImages = false;
2016-07-07 05:03:30 +08:00
let attr = 'data-x-src',
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => {
node.loading = 'lazy';
src = node.getAttribute(attr);
node.src = useProxy ? proxy(src) : src;
2016-07-07 05:03:30 +08:00
});
body.querySelectorAll('[data-x-style-url]').forEach(node => {
JSON.parse(node.dataset.xStyleUrl).forEach(data =>
node.style[data[0]] = "url('" + (useProxy ? proxy(data[1]) : data[1]) + "')"
);
2016-07-07 05:03:30 +08:00
});
}
}
2020-08-27 21:45:47 +08:00
/**
* @returns {string}
*/
bodyAsHTML() {
// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
if (this.body && !this.body.querySelector('iframe')) {
let clone = this.body.cloneNode(true);
2020-08-27 21:45:47 +08:00
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
node => node.classList.remove('rl-bq-switcher','hidden-bq')
);
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
node => node.remove()
);
return clone.innerHTML;
2016-07-07 05:03:30 +08:00
}
2022-05-12 05:13:24 +08:00
let result = cleanHtml(this.html(), this.attachments(), SettingsUserStore.removeColors())
return result.html || plainToHtml(this.plain());
2016-07-07 05:03:30 +08:00
}
}