snappymail/dev/Model/Message.js

466 lines
12 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, htmlToPlain, cleanHtml } from 'Common/Html';
import { forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw, proxy } from 'Common/Links';
import { addObservablesTo, addComputablesTo } from 'External/ko';
2016-07-07 05:03:30 +08:00
import { FolderUserStore, isAllowedKeyword } 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';
2022-12-12 20:03:41 +08:00
import { LanguageStore } from 'Stores/Language';
//import { MessageFlagsCache } from 'Common/Cache';
2022-06-03 19:47:04 +08:00
import Remote from 'Remote/User/Fetch';
const
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
2023-02-14 21:07:38 +08:00
2022-06-03 19:47:04 +08:00
toggleTag = (message, keyword) => {
const lower = keyword.toLowerCase(),
2022-10-10 19:52:56 +08:00
flags = message.flags,
isSet = flags.includes(lower);
2022-06-03 19:47:04 +08:00
Remote.request('MessageSetKeyword', iError => {
if (!iError) {
2022-10-10 19:52:56 +08:00
isSet ? flags.remove(lower) : flags.push(lower);
// MessageFlagsCache.setFor(message.folder, message.uid, flags());
2022-06-03 19:47:04 +08:00
}
}, {
folder: message.folder,
uids: message.uid,
keyword: keyword,
setAction: isSet ? 0 : 1
2022-06-03 19:47:04 +08:00
})
},
2022-08-25 20:08:19 +08:00
/**
* @param {EmailCollectionModel} emails
* @param {Object} unic
* @param {Map} localEmails
*/
2022-09-27 15:45:35 +08:00
replyHelper = (emails, unic, localEmails) =>
2022-10-10 19:52:56 +08:00
emails.forEach(email =>
unic[email.email] || localEmails.has(email.email) || localEmails.set(email.email, email)
);
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.folder = '';
this.uid = 0;
this.hash = '';
this.requestHash = '';
this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel;
this.cc = new EmailCollectionModel;
this.bcc = new EmailCollectionModel;
2023-01-25 16:41:15 +08:00
this.sender = new EmailCollectionModel;
this.replyTo = new EmailCollectionModel;
this.deliveredTo = new EmailCollectionModel;
this.body = null;
this.draftInfo = [];
this.dkim = [];
this.spf = [];
this.dmarc = [];
this.messageId = '';
this.inReplyTo = '';
this.references = '';
2023-02-07 22:25:28 +08:00
this.autocrypt = {};
addObservablesTo(this, {
2020-10-25 18:46:58 +08:00
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
encrypted: false,
pgpEncrypted: null,
pgpDecrypted: false,
2020-10-25 18:46:58 +08:00
readReceipt: '',
2023-01-25 16:41:15 +08:00
// rfc8621
id: '',
// threadId: '',
2020-10-25 18:46:58 +08:00
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
addComputablesTo(this, {
attachmentIconClass: () =>
this.encrypted() ? 'icon-lock' : FileInfo.getAttachmentsIconClass(this.attachments()),
threadsLen: () => this.threads().length,
isUnseen: () => !this.flags().includes('\\seen'),
isFlagged: () => this.flags().includes('\\flagged'),
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing'),
2022-06-03 19:47:04 +08:00
tagOptions: () => {
const tagOptions = [];
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
if (isAllowedKeyword(value)) {
let lower = value.toLowerCase();
2022-06-03 19:47:04 +08:00
tagOptions.push({
css: 'msgflag-' + lower,
value: value,
checked: this.flags().includes(lower),
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
2022-06-03 19:47:04 +08:00
toggle: (/*obj*/) => toggleTag(this, value)
});
}
});
return tagOptions
},
2022-06-03 19:47:04 +08:00
whitelistOptions: () => {
let options = [];
if ('match' === SettingsUserStore.viewImages()) {
let from = this.from[0],
list = SettingsUserStore.viewImagesWhitelist(),
counts = {};
this.html().match(/src=["'][^"']+/g)?.forEach(m => {
m = m.replace(/^.+(:\/\/[^/]+).+$/, '$1');
if (counts[m]) {
++counts[m];
} else {
counts[m] = 1;
options.push(m);
}
});
options = options.filter(txt => !list.includes(txt)).sort((a,b) => (counts[a] < counts[b])
? 1
: (counts[a] > counts[b] ? -1 : a.localeCompare(b))
);
from && options.unshift(from.email);
}
return options;
}
2020-10-25 21:14:14 +08:00
});
2016-07-07 05:03:30 +08:00
}
toggleTag(keyword) {
toggleTag(this, keyword);
}
spamStatus() {
let spam = this.spamResult();
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
}
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() {
2022-10-10 19:52:56 +08:00
const list = this[
[FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from'
];
this.senderEmailsString(list.toString(true));
this.senderClearEmailsString(list.map(email => email?.email).filter(email => email).join(', '));
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) {
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();
return true;
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
}
indent() {
return this.level ? 'margin-left:'+this.level+'em' : null;
}
2016-07-07 05:03:30 +08:00
/**
* @returns {string}
*/
viewRaw() {
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-09-27 15:45:35 +08:00
toResult.size || replyHelper(this.from, unic, toResult);
2016-07-07 05:03:30 +08:00
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
}
viewBody(html) {
const body = this.body;
if (body) {
if (html) {
let result = msgHtml(this);
this.hasExternals(result.hasExternals);
this.hasImages(!!result.hasExternals);
body.innerHTML = result.html;
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
if ('always' === SettingsUserStore.viewImages()) {
this.showExternalImages();
}
if ('match' === SettingsUserStore.viewImages()) {
this.showExternalImages(1);
}
2023-01-31 21:45:50 +08:00
}
} else {
body.innerHTML = plainToHtml(
(this.plain()
? this.plain()
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
.trim()
: htmlToPlain(body.innerHTML)
)
);
this.hasImages(false);
}
body.classList.toggle('html', html);
body.classList.toggle('plain', !html);
this.isHtml(html);
return true;
}
}
viewHtml() {
return this.html() && this.viewBody(true);
}
viewPlain() {
return this.viewBody(false);
}
viewPopupMessage(print) {
2023-02-14 17:21:26 +08:00
const
timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.cc.toString(),
bccLine = this.bcc.toString(),
2020-08-27 21:45:47 +08:00
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
2023-02-14 17:21:26 +08:00
win = open('', 'sm-msg-'+this.requestHash
/*,newWindow ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''*/
),
sdoc = win.document,
subject = encodeHtml(this.subject()),
mode = this.isHtml() ? 'div' : 'pre',
to = `<div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.to)}</div>`
+ (ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '')
+ (bccLine ? `<div>${encodeHtml(i18n('GLOBAL/BCC'))}: ${encodeHtml(bccLine)}</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',0,LanguageStore.hourCycle()) : '')}</time><div>${encodeHtml(this.from)}</div>${to}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
2019-07-05 03:19:24 +08:00
);
sdoc.close();
2020-08-27 21:45:47 +08:00
print && setTimeout(() => win.print(), 100);
2016-07-07 05:03:30 +08:00
}
/**
* @param {boolean=} print = false
*/
popupMessage() {
2022-10-10 19:52:56 +08:00
this.viewPopupMessage();
}
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
}
/**
* @returns {MessageModel}
*//*
clone() {
let self = new MessageModel();
// Clone message values
forEachObjectEntry(this, (key, value) => {
if (ko.isObservable(value)) {
ko.isComputed(value) || self[key](value());
} else if (!isFunction(value)) {
self[key] = value;
}
});
self.computeSenderEmail();
return self;
}*/
2016-07-07 05:03:30 +08:00
2023-01-31 21:45:50 +08:00
showExternalImages(regex) {
const body = this.body;
if (body && this.hasImages()) {
if (regex) {
2023-02-10 16:55:52 +08:00
regex = [];
SettingsUserStore.viewImagesWhitelist().trim().split(/[\s\r\n,;]+/g).forEach(rule => {
rule = rule.split('+');
rule[0] = rule[0].trim();
if (rule[0]
&& (!rule.includes('spf') || 'pass' === this.spf[0]?.[0])
&& (!rule.includes('dkim') || 'pass' === this.dkim[0]?.[0])
&& (!rule.includes('dmarc') || 'pass' === this.dmarc[0]?.[0])
) {
regex.push(rule[0].replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'));
}
});
regex = regex.join('|').replace(/\|+/g, '|');
if (regex) {
console.log('whitelist images = '+regex);
regex = new RegExp(regex);
if (this.from[0]?.email.match(regex)) {
regex = null;
}
}
}
let hasImages = false,
isValid = src => {
if (null == regex || (regex && src.match(regex))) {
return true;
}
hasImages = true;
},
attr = 'data-x-src',
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => {
src = node.getAttribute(attr);
if (isValid(src)) {
2023-01-31 21:45:50 +08:00
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 => {
if (isValid(data[1])) {
node.style[data[0]] = "url('" + (useProxy ? proxy(data[1]) : data[1]) + "')"
}
});
2016-07-07 05:03:30 +08:00
});
this.hasImages(hasImages);
2016-07-07 05:03:30 +08:00
}
}
2020-08-27 21:45:47 +08:00
/**
* @returns {string}
*/
bodyAsHTML() {
if (this.body) {
let clone = this.body.cloneNode(true);
clone.querySelectorAll('.sm-bq-switcher').forEach(
node => node.replaceWith(node.lastElementChild)
2020-08-27 21:45:47 +08:00
);
return clone.innerHTML;
2016-07-07 05:03:30 +08:00
}
2023-02-14 21:07:38 +08:00
let result = msgHtml(this);
2022-05-12 05:13:24 +08:00
return result.html || plainToHtml(this.plain());
2016-07-07 05:03:30 +08:00
}
}