snappymail/dev/View/User/MailBox/MessageView.js

595 lines
16 KiB
JavaScript
Raw Normal View History

import ko from 'ko';
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
2022-02-24 19:22:27 +08:00
import { Scope } from 'Common/Enums';
2021-01-25 05:58:06 +08:00
import {
2019-07-05 03:19:24 +08:00
ComposeType,
2022-03-14 22:42:05 +08:00
ClientSideKeyNameMessageHeaderFullInfo,
ClientSideKeyNameMessageAttachmentControls,
2019-07-05 03:19:24 +08:00
FolderType,
MessageSetAction
2021-01-25 05:58:06 +08:00
} from 'Common/EnumsUser';
import {
elementById,
leftPanelDisabled,
keyScopeReal,
Settings,
2022-02-08 21:13:06 +08:00
SettingsCapa,
fireEvent,
addShortcut,
2022-03-22 23:24:58 +08:00
registerShortcut
} from 'Common/Globals';
import { arrayLength } from 'Common/Utils';
import { download, mailToHelper, showMessageComposer, moveAction } from 'Common/UtilsUser';
2022-03-22 23:24:58 +08:00
import { isFullscreen, exitFullscreen, toggleFullscreen } from 'Common/Fullscreen';
import { SMAudio } from 'Common/Audio';
2019-07-05 03:19:24 +08:00
import { i18n } from 'Common/Translator';
import { attachmentDownload } from 'Common/Links';
import { MessageFlagsCache } from 'Common/Cache';
import { AppUserStore } from 'Stores/User/App';
import { SettingsUserStore } from 'Stores/User/Settings';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import * as Local from 'Storage/Client';
import Remote from 'Remote/User/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewRight } from 'Knoin/AbstractViews';
import { PgpUserStore } from 'Stores/User/Pgp';
import { MimeToMessage } from 'Mime/Utils';
2022-05-12 05:13:24 +08:00
import { MessageModel } from 'Model/Message';
import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
const
oMessageScrollerDom = () => elementById('messageItem') || {},
currentMessage = MessageUserStore.message,
fetchRaw = url => rl.fetch(url).then(response => response.ok && response.text());
export class MailMessageView extends AbstractViewRight {
constructor() {
2022-03-08 19:28:16 +08:00
super();
2014-08-20 23:03:12 +08:00
2021-03-13 00:08:58 +08:00
const
/**
* @param {Function} fExecute
2022-03-04 02:22:17 +08:00
* @param {Function} fCanExecute = true
* @returns {Function}
*/
createCommand = (fExecute, fCanExecute) => {
let fResult = () => {
fCanExecute() && fExecute.call(null);
return false;
};
fResult.canExecute = fCanExecute;
return fResult;
},
2021-03-13 00:08:58 +08:00
createCommandReplyHelper = type =>
createCommand(() => this.replyOrforward(type), this.canBeRepliedOrForwarded),
2021-03-13 00:08:58 +08:00
createCommandActionHelper = (folderType, bDelete) =>
2021-03-13 00:08:58 +08:00
createCommand(() => {
const message = currentMessage();
if (message) {
2022-09-09 23:04:52 +08:00
currentMessage(null);
rl.app.moveMessagesToFolderType(folderType, message.folder, [message.uid], bDelete);
2021-03-13 00:08:58 +08:00
}
}, this.messageVisibility);
2014-08-20 23:03:12 +08:00
2022-08-31 23:31:08 +08:00
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
this.simpleAttachmentsList = SettingsUserStore.simpleAttachmentsList;
2022-08-31 23:31:08 +08:00
addObservablesTo(this, {
2022-04-25 15:40:38 +08:00
showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
downloadAsZipLoading: false,
2022-03-14 22:42:05 +08:00
showFullInfo: '1' === Local.get(ClientSideKeyNameMessageHeaderFullInfo),
moreDropdownTrigger: false,
// viewer
viewFromShort: '',
viewFromDkimData: ['none', ''],
viewToShort: ''
});
2014-08-20 23:03:12 +08:00
2017-03-02 02:38:18 +08:00
this.moveAction = moveAction;
2022-02-24 19:22:27 +08:00
this.allowMessageActions = SettingsCapa('MessageActions');
const attachmentsActions = Settings.app('attachmentsActions');
2021-07-22 03:34:17 +08:00
this.attachmentsActions = ko.observableArray(arrayLength(attachmentsActions) ? attachmentsActions : []);
2016-05-01 09:07:10 +08:00
this.hasCheckedMessages = MessagelistUserStore.hasChecked;
this.archiveAllowed = MessagelistUserStore.archiveAllowed;
this.canMarkAsSpam = MessagelistUserStore.canMarkAsSpam;
this.isDraftFolder = MessagelistUserStore.isDraftFolder;
this.isSpamFolder = MessagelistUserStore.isSpamFolder;
this.message = MessageUserStore.message;
this.messageLoadingThrottle = MessageUserStore.loading;
this.messagesBodiesDom = MessageUserStore.bodiesDom;
this.messageError = MessageUserStore.error;
2015-04-25 06:15:11 +08:00
2022-03-22 18:47:17 +08:00
this.fullScreenMode = isFullscreen;
this.toggleFullScreen = toggleFullscreen;
2014-08-22 23:08:56 +08:00
2019-07-05 03:19:24 +08:00
this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
2015-04-25 06:15:11 +08:00
2019-07-05 03:19:24 +08:00
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
// viewer
this.viewHash = '';
2014-08-20 23:03:12 +08:00
addComputablesTo(this, {
2022-04-25 15:40:38 +08:00
allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'),
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
2022-09-02 17:52:07 +08:00
&& (currentMessage()?.attachments || [])
2022-10-03 22:04:39 +08:00
.filter(item => item?.download /*&& !item?.isLinked()*/ && item?.checked())
.length,
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
2022-06-03 20:46:05 +08:00
messageVisibility: () => !MessageUserStore.loading() && !!currentMessage(),
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(),
viewFromDkimVisibility: () => 'none' !== this.viewFromDkimData()[0],
viewFromDkimStatusIconClass:() => {
switch (this.viewFromDkimData()[0]) {
case 'none':
return '';
case 'pass':
return 'icon-ok iconcolor-green'; // ✔️
default:
return 'icon-cross iconcolor-red'; // ✖ ❌
}
},
viewFromDkimStatusTitle:() => {
const status = this.viewFromDkimData();
2021-07-22 03:34:17 +08:00
if (arrayLength(status) && status[0]) {
return status[1] || 'DKIM: ' + status[0];
}
return '';
},
pgpSupported: () => currentMessage() && PgpUserStore.isSupported(),
2021-11-30 17:19:43 +08:00
messageListOrViewLoading:
() => MessagelistUserStore.isLoading() | MessageUserStore.loading()
});
addSubscribablesTo(this, {
message: message => {
if (message) {
if (this.viewHash !== message.hash) {
this.scrollMessageToTop();
}
this.viewHash = message.hash;
// TODO: make first param a user setting #683
this.viewFromShort(message.fromToLine(false, true));
this.viewFromDkimData(message.fromDkimData());
this.viewToShort(message.toToLine(true, true));
} else {
MessagelistUserStore.selectedMessage(null);
this.viewHash = '';
this.scrollMessageToTop();
}
},
2016-06-30 08:02:45 +08:00
2022-03-14 22:42:05 +08:00
showFullInfo: value => Local.set(ClientSideKeyNameMessageHeaderFullInfo, value ? '1' : '0')
});
// commands
this.replyCommand = createCommandReplyHelper(ComposeType.Reply);
this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll);
this.forwardCommand = createCommandReplyHelper(ComposeType.Forward);
this.forwardAsAttachmentCommand = createCommandReplyHelper(ComposeType.ForwardAsAttachment);
this.editAsNewCommand = createCommandReplyHelper(ComposeType.EditAsNew);
this.deleteCommand = createCommandActionHelper(FolderType.Trash);
this.deleteWithoutMoveCommand = createCommandActionHelper(FolderType.Trash, true);
this.archiveCommand = createCommandActionHelper(FolderType.Archive);
this.spamCommand = createCommandActionHelper(FolderType.Spam);
this.notSpamCommand = createCommandActionHelper(FolderType.NotSpam);
decorateKoCommands(this, {
messageEditCommand: self => self.messageVisibility(),
2021-11-30 17:19:43 +08:00
goUpCommand: self => !self.messageListOrViewLoading(),
goDownCommand: self => !self.messageListOrViewLoading()
});
2016-06-30 08:02:45 +08:00
}
toggleFullInfo() {
this.showFullInfo(!this.showFullInfo());
}
2022-02-21 22:36:34 +08:00
closeMessage() {
2022-09-09 23:04:52 +08:00
currentMessage(null);
}
messageEditCommand() {
2022-09-09 23:04:52 +08:00
currentMessage() && showMessageComposer([ComposeType.Draft, currentMessage()]);
}
goUpCommand() {
fireEvent('mailbox.message-list.selector.go-up',
2022-09-09 23:04:52 +08:00
!!currentMessage() // bForceSelect
);
}
goDownCommand() {
fireEvent('mailbox.message-list.selector.go-down',
2022-09-09 23:04:52 +08:00
!!currentMessage() // bForceSelect
);
}
/**
* @param {string} sType
* @returns {void}
*/
replyOrforward(sType) {
showMessageComposer([sType, currentMessage()]);
2016-06-30 08:02:45 +08:00
}
2016-05-01 09:07:10 +08:00
onBuild(dom) {
const eqs = (ev, s) => ev.target.closestWithin(s, dom);
dom.addEventListener('click', event => {
ThemeStore.isMobile() && leftPanelDisabled(true);
let el = eqs(event, 'a');
2022-09-30 17:38:51 +08:00
if (el && 0 === event.button && mailToHelper(el.href)) {
event.preventDefault();
event.stopPropagation();
return;
}
2022-11-23 18:22:20 +08:00
if (eqs(event, '.attachmentsPlace .showPreview')) {
event.stopPropagation();
2022-11-23 18:22:20 +08:00
return;
}
el = eqs(event, '.attachmentsPlace .showPreplay');
if (el) {
event.stopPropagation();
2021-03-13 00:08:58 +08:00
const attachment = ko.dataFor(el);
if (attachment && SMAudio.supported) {
2019-07-05 03:19:24 +08:00
switch (true) {
case SMAudio.supportedMp3 && attachment.isMp3():
SMAudio.playMp3(attachment.linkDownload(), attachment.fileName);
break;
case SMAudio.supportedOgg && attachment.isOgg():
SMAudio.playOgg(attachment.linkDownload(), attachment.fileName);
break;
case SMAudio.supportedWav && attachment.isWav():
SMAudio.playWav(attachment.linkDownload(), attachment.fileName);
break;
// no default
}
}
2022-11-23 18:22:20 +08:00
return;
}
2022-11-23 18:22:20 +08:00
el = eqs(event, '.attachmentItem');
if (el) {
const attachment = ko.dataFor(el), url = attachment?.linkDownload();
if (url) {
if ('application/pgp-keys' == attachment.mimeType
&& (OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported())) {
fetchRaw(url).then(text =>
showScreenPopup(OpenPgpImportPopupView, [text])
);
} else if ('message/rfc822' == attachment.mimeType) {
2022-05-12 05:13:24 +08:00
// TODO
fetchRaw(url).then(text => {
const oMessage = new MessageModel();
MimeToMessage(text, oMessage);
// cleanHTML
oMessage.viewPopupMessage();
2022-05-12 05:13:24 +08:00
});
} else {
download(url, attachment.fileName);
2022-05-12 05:13:24 +08:00
}
}
}
if (eqs(event, '.messageItemHeader .subjectParent .flagParent')) {
const message = currentMessage();
message && MessagelistUserStore.setAction(
2020-10-23 21:15:54 +08:00
message.folder,
message.isFlagged() ? MessageSetAction.UnsetFlag : MessageSetAction.SetFlag,
[message]
);
}
});
2022-02-17 16:36:29 +08:00
AppUserStore.focusedState.subscribe(value => {
if (Scope.MessageView !== value) {
this.scrollMessageToTop();
this.scrollMessageToLeft();
}
});
keyScopeReal.subscribe(value => this.messageDomFocused(Scope.MessageView === value));
2021-08-13 02:17:37 +08:00
// initShortcuts
2021-04-23 16:47:24 +08:00
// exit fullscreen, back
addShortcut('escape', '', Scope.MessageView, () => {
if (!this.viewModelDom.hidden && currentMessage()) {
2021-04-23 16:47:24 +08:00
const preview = SettingsUserStore.usePreviewPane();
2022-03-22 18:47:17 +08:00
if (isFullscreen()) {
exitFullscreen();
2021-04-23 16:47:24 +08:00
if (preview) {
AppUserStore.focusedState(Scope.MessageList);
}
} else if (!preview) {
2022-09-09 23:04:52 +08:00
currentMessage(null);
2021-04-23 16:47:24 +08:00
} else {
AppUserStore.focusedState(Scope.MessageList);
}
2016-07-01 06:50:11 +08:00
2021-04-23 16:47:24 +08:00
return false;
}
});
// fullscreen
addShortcut('enter,open', '', Scope.MessageView, () => {
2022-03-22 18:47:17 +08:00
isFullscreen() || toggleFullscreen();
2016-06-30 08:02:45 +08:00
return false;
});
2016-07-01 06:50:11 +08:00
// reply
registerShortcut('r,mailreply', '', [Scope.MessageList, Scope.MessageView], () => {
if (currentMessage()) {
this.replyCommand();
return false;
}
return true;
});
2016-06-30 08:02:45 +08:00
2021-08-13 02:17:37 +08:00
// replyAll
registerShortcut('a', '', [Scope.MessageList, Scope.MessageView], () => {
if (currentMessage()) {
this.replyAllCommand();
return false;
}
});
registerShortcut('mailreply', 'shift', [Scope.MessageList, Scope.MessageView], () => {
if (currentMessage()) {
this.replyAllCommand();
return false;
}
});
// forward
registerShortcut('f,mailforward', '', [Scope.MessageList, Scope.MessageView], () => {
if (currentMessage()) {
this.forwardCommand();
return false;
}
});
// message information
registerShortcut('i', 'meta', [Scope.MessageList, Scope.MessageView], () => {
currentMessage() && this.toggleFullInfo();
return false;
});
// toggle message blockquotes
registerShortcut('b', '', [Scope.MessageList, Scope.MessageView], () => {
const message = currentMessage();
2022-09-02 17:52:07 +08:00
if (message?.body) {
message.body.querySelectorAll('.sm-bq-switcher > summary').forEach(node => node.click());
return false;
2014-11-20 07:25:39 +08:00
}
});
addShortcut('arrowup,arrowleft', 'meta', [Scope.MessageList, Scope.MessageView], () => {
this.goUpCommand();
2016-06-30 08:02:45 +08:00
return false;
});
2016-07-01 06:50:11 +08:00
addShortcut('arrowdown,arrowright', 'meta', [Scope.MessageList, Scope.MessageView], () => {
this.goDownCommand();
return false;
});
2015-04-14 02:45:09 +08:00
// delete
addShortcut('delete', '', Scope.MessageView, () => {
this.deleteCommand();
return false;
});
addShortcut('delete', 'shift', Scope.MessageView, () => {
this.deleteWithoutMoveCommand();
return false;
});
2015-04-14 02:45:09 +08:00
// change focused state
addShortcut('arrowleft', '', Scope.MessageView, () => {
2022-03-22 18:47:17 +08:00
if (!isFullscreen() && currentMessage() && SettingsUserStore.usePreviewPane()
&& !oMessageScrollerDom().scrollLeft) {
AppUserStore.focusedState(Scope.MessageList);
return false;
}
});
addShortcut('tab', 'shift', Scope.MessageView, () => {
2022-03-22 18:47:17 +08:00
if (!isFullscreen() && currentMessage() && SettingsUserStore.usePreviewPane()) {
AppUserStore.focusedState(Scope.MessageList);
}
return false;
});
}
2016-06-30 08:02:45 +08:00
/**
* @returns {boolean}
*/
isDraftOrSentFolder() {
return MessagelistUserStore.isDraftFolder() || MessagelistUserStore.isSentFolder();
2016-06-30 08:02:45 +08:00
}
scrollMessageToTop() {
oMessageScrollerDom().scrollTop = (50 < oMessageScrollerDom().scrollTop) ? 50 : 0;
2016-06-30 08:02:45 +08:00
}
scrollMessageToLeft() {
oMessageScrollerDom().scrollLeft = 0;
2016-06-30 08:02:45 +08:00
}
2015-04-25 06:15:11 +08:00
2022-04-25 15:40:38 +08:00
toggleAttachmentControls() {
const b = !this.showAttachmentControls();
this.showAttachmentControls(b);
Local.set(ClientSideKeyNameMessageAttachmentControls, b);
}
downloadAsZip() {
2022-10-13 05:08:28 +08:00
const hashes = (currentMessage()?.attachments || [])
2022-10-03 22:04:39 +08:00
.map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '')
.filter(v => v);
if (hashes.length) {
2022-02-24 06:11:12 +08:00
Remote.post('AttachmentsActions', this.downloadAsZipLoading, {
Do: 'Zip',
Hashes: hashes
})
.then(result => {
2022-09-02 17:52:07 +08:00
let hash = result?.Result?.FileHash;
2022-02-24 06:11:12 +08:00
if (hash) {
download(attachmentDownload(hash), hash+'.zip');
} else {
this.downloadAsZipError(true);
}
})
.catch(() => this.downloadAsZipError(true));
}
2016-06-30 08:02:45 +08:00
}
2015-04-14 02:45:09 +08:00
/**
* @param {MessageModel} oMessage
* @returns {void}
*/
2021-08-13 02:17:37 +08:00
showImages() {
currentMessage().showExternalImages();
2016-06-30 08:02:45 +08:00
}
/**
* @returns {string}
*/
printableCheckedMessageCount() {
const cnt = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails().size;
2021-03-13 00:08:58 +08:00
return 0 < cnt ? (100 > cnt ? cnt : '99+') : '';
2016-06-30 08:02:45 +08:00
}
/**
* @param {MessageModel} oMessage
* @returns {void}
*/
2021-08-13 02:17:37 +08:00
readReceipt() {
let oMessage = currentMessage()
if (oMessage.readReceipt()) {
Remote.request('SendReadReceiptMessage', iError => {
if (!iError) {
oMessage.flags.push('$mdnsent');
// oMessage.flags.valueHasMutated();
MessageFlagsCache.store(oMessage);
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
}, {
MessageFolder: oMessage.folder,
MessageUid: oMessage.uid,
ReadReceipt: oMessage.readReceipt(),
subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
Text: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
});
}
2016-06-30 08:02:45 +08:00
}
newTag() {
let message = currentMessage();
if (message) {
let keyword = prompt(i18n('MESSAGE/NEW_TAG'), '')?.replace(/[\s\\]+/g, '');
if (keyword.length && isAllowedKeyword(keyword)) {
message.toggleTag(keyword);
FolderUserStore.currentFolder().permanentFlags.push(keyword);
}
}
}
pgpDecrypt() {
const oMessage = currentMessage();
PgpUserStore.decrypt(oMessage).then(result => {
2022-11-02 02:14:08 +08:00
if (result) {
oMessage.pgpDecrypted(true);
2022-11-02 02:14:08 +08:00
if (result.data) {
MimeToMessage(result.data, oMessage);
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
if (result.signatures?.length) {
oMessage.pgpSigned(true);
oMessage.pgpVerified({
signatures: result.signatures,
success: !!result.signatures.length
});
}
2022-02-03 17:17:18 +08:00
}
2022-11-01 19:05:44 +08:00
} else {
// TODO: translate
alert('Decryption failed, canceled or not possible');
}
2022-11-01 19:05:44 +08:00
})
.catch(e => console.error(e));
}
pgpVerify(/*self, event*/) {
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
PgpUserStore.verify(oMessage).then(result => {
if (result) {
oMessage.pgpVerified(result);
2022-11-02 02:14:08 +08:00
} else {
alert('Verification failed or no valid public key found');
}
/*
2022-09-02 17:52:07 +08:00
if (result?.success) {
2022-02-09 22:43:14 +08:00
i18n('OPENPGP/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
});
message.getText()
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
2022-09-02 17:52:07 +08:00
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
: '';
2022-02-09 22:43:14 +08:00
i18n('OPENPGP/ERROR', {
ERROR: 'message'
}) + (additional ? ' (' + additional + ')' : '');
}
*/
});
}
2022-01-20 23:38:27 +08:00
}