diff --git a/dev/Model/Message.js b/dev/Model/Message.js
index 79b4149cd..23066f687 100644
--- a/dev/Model/Message.js
+++ b/dev/Model/Message.js
@@ -22,6 +22,9 @@ import { LanguageStore } from 'Stores/Language';
import Remote from 'Remote/User/Fetch';
+import { showScreenPopup } from 'Knoin/Knoin';
+import { MessagePopupView } from 'View/Popup/Message';
+
const
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
@@ -347,34 +350,38 @@ export class MessageModel extends AbstractModel {
* @param {boolean=} print = false
*/
popupMessage(print) {
- const
- timeStampInUTC = this.dateTimestamp() || 0,
- ccLine = this.cc.toString(),
- bccLine = this.bcc.toString(),
- m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
- win = open('', 'sm-msg-'+this.requestHash
- ,SettingsUserStore.messageNewWindow() ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''
- ),
- sdoc = win.document,
- subject = encodeHtml(this.subject()),
- mode = this.isHtml() ? 'div' : 'pre',
- to = `
${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.to)}
`
- + (ccLine ? `${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}
` : '')
- + (bccLine ? `${encodeHtml(i18n('GLOBAL/BCC'))}: ${encodeHtml(bccLine)}
` : ''),
- style = getComputedStyle(doc.querySelector('.messageView')),
- prop = property => style.getPropertyValue(property);
- let attachments = '';
- this.attachments.forEach(attachment => {
- attachments += `${attachment.fileName}`;
- });
- sdoc.write(PreviewHTML
- .replace('', ''+subject)
- // eslint-disable-next-line max-len
- .replace('', `${subject}
${encodeHtml(this.from)}
${to}<${mode}>${this.bodyAsHTML()}${mode}>`)
- .replace('', `${attachments}
`)
- );
- sdoc.close();
- (true === print) && setTimeout(() => win.print(), 100);
+ if (print) {
+ const
+ timeStampInUTC = this.dateTimestamp() || 0,
+ ccLine = this.cc.toString(),
+ bccLine = this.bcc.toString(),
+ m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
+ win = open('', 'sm-msg-'+this.requestHash
+ ,SettingsUserStore.messageNewWindow() ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''
+ ),
+ sdoc = win.document,
+ subject = encodeHtml(this.subject()),
+ mode = this.isHtml() ? 'div' : 'pre',
+ to = `${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.to)}
`
+ + (ccLine ? `${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}
` : '')
+ + (bccLine ? `${encodeHtml(i18n('GLOBAL/BCC'))}: ${encodeHtml(bccLine)}
` : ''),
+ style = getComputedStyle(doc.querySelector('.messageView')),
+ prop = property => style.getPropertyValue(property);
+ let attachments = '';
+ this.attachments.forEach(attachment => {
+ attachments += `${attachment.fileName}`;
+ });
+ sdoc.write(PreviewHTML
+ .replace('', ''+subject)
+ // eslint-disable-next-line max-len
+ .replace('', `${subject}
${encodeHtml(this.from)}
${to}<${mode}>${this.bodyAsHTML()}${mode}>`)
+ .replace('', `${attachments}
`)
+ );
+ sdoc.close();
+ (true === print) && setTimeout(() => win.print(), 100);
+ } else {
+ showScreenPopup(MessagePopupView, [this]);
+ }
}
printMessage() {
diff --git a/dev/View/Popup/Message.js b/dev/View/Popup/Message.js
new file mode 100644
index 000000000..030d632db
--- /dev/null
+++ b/dev/View/Popup/Message.js
@@ -0,0 +1,453 @@
+import ko from 'ko';
+import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
+
+import { ScopeMessageList, ScopeMessageView } from 'Common/Enums';
+
+import {
+ ComposeType,
+ ClientSideKeyNameMessageHeaderFullInfo,
+ ClientSideKeyNameMessageAttachmentControls
+} from 'Common/EnumsUser';
+import { RFC822 } from 'Common/File';
+import {
+ keyScopeReal,
+ Settings,
+ SettingsCapa,
+ stopEvent,
+ addShortcut,
+ registerShortcut
+} from 'Common/Globals';
+
+import { arrayLength } from 'Common/Utils';
+import { download, downloadZip, mailToHelper, showMessageComposer, moveAction } from 'Common/UtilsUser';
+import { isFullscreen, exitFullscreen, toggleFullscreen } from 'Common/Fullscreen';
+
+import { SMAudio } from 'Common/Audio';
+
+import { i18n } from 'Common/Translator';
+
+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 * as Local from 'Storage/Client';
+
+import Remote from 'Remote/User/Fetch';
+
+import { AbstractViewPopup } from 'Knoin/AbstractViews';
+
+import { PgpUserStore } from 'Stores/User/Pgp';
+
+import { MimeToMessage } from 'Mime/Utils';
+
+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
+ fetchRaw = url => rl.fetch(url).then(response => response.ok && response.text());
+
+export class MessagePopupView extends AbstractViewPopup {
+ constructor() {
+ super('Message');
+
+ const
+ /**
+ * @param {Function} fExecute
+ * @param {Function} fCanExecute = true
+ * @returns {Function}
+ */
+ createCommand = (fExecute, fCanExecute) => {
+ let fResult = () => {
+ fCanExecute() && fExecute.call(null);
+ return false;
+ };
+ fResult.canExecute = fCanExecute;
+ return fResult;
+ },
+
+ createCommandReplyHelper = type =>
+ createCommand(() => this.replyOrforward(type), ()=>1);
+
+ this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
+ this.simpleAttachmentsList = SettingsUserStore.simpleAttachmentsList;
+
+ addObservablesTo(this, {
+ message: null,
+ showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
+ downloadAsZipLoading: false,
+ showFullInfo: '1' === Local.get(ClientSideKeyNameMessageHeaderFullInfo),
+ // viewer
+ viewFromShort: '',
+ dkimData: ['none', '', '']
+ });
+
+ this.moveAction = moveAction;
+
+ const attachmentsActions = Settings.app('attachmentsActions');
+ this.attachmentsActions = ko.observableArray(arrayLength(attachmentsActions) ? attachmentsActions : []);
+
+ this.fullScreenMode = isFullscreen;
+ this.toggleFullScreen = toggleFullscreen;
+
+ this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
+
+ this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
+
+ // viewer
+ this.viewHash = '';
+
+ addComputablesTo(this, {
+ allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'),
+
+ downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
+ && (this.message()?.attachments || [])
+ .filter(item => item?.download /*&& !item?.isLinked()*/ && item?.checked())
+ .length,
+
+ tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
+
+ tagsToHTML: () => this.message()?.flags().map(value =>
+ isAllowedKeyword(value)
+ ? '' + i18n('MESSAGE_TAGS/'+value,0,value) + ''
+ : ''
+ ).join(' '),
+
+ listAttachments: () => this.message()?.attachments()
+ .filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
+ hasAttachments: () => this.listAttachments()?.length,
+
+ viewDkimIcon: () => 'none' !== this.dkimData()[0],
+
+ dkimIconClass:() => {
+ switch (this.dkimData()[0]) {
+ case 'none':
+ return '';
+ case 'pass':
+ return 'icon-ok iconcolor-green'; // ✔️
+ default:
+ return 'icon-cross iconcolor-red'; // ✖ ❌
+ }
+ },
+
+ dkimTitle:() => {
+ const dkim = this.dkimData();
+ return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : '';
+ },
+
+ showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(),
+
+ firstUnsubsribeLink: () => this.message()?.unsubsribeLinks()[0] || '',
+
+ pgpSupported: () => this.message() && PgpUserStore.isSupported()
+ });
+
+ addSubscribablesTo(this, {
+ message: message => {
+ if (message) {
+ this.viewHash = message.hash;
+ // TODO: make first param a user setting #683
+ this.viewFromShort(message.from.toString(false, true));
+ this.dkimData(message.dkim[0] || ['none', '', '']);
+ } else {
+ this.viewHash = '';
+ }
+ },
+
+ 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);
+ }
+
+ toggleFullInfo() {
+ this.showFullInfo(!this.showFullInfo());
+ }
+
+ /**
+ * @param {string} sType
+ * @returns {void}
+ */
+ replyOrforward(sType) {
+ showMessageComposer([sType, this.message()]);
+ }
+
+ onBuild(dom) {
+ const eqs = (ev, s) => ev.target.closestWithin(s, dom);
+ dom.addEventListener('click', event => {
+ let el = eqs(event, 'a');
+ if (el && 0 === event.button && mailToHelper(el.href)) {
+ stopEvent(event);
+ return;
+ }
+
+ el = eqs(event, '.attachmentsPlace .showPreview');
+ if (el) {
+ const attachment = ko.dataFor(el), url = attachment?.linkDownload();
+// if (url && FileType.Eml === attachment.fileType) {
+ if (url && RFC822 == attachment.mimeType) {
+ stopEvent(event);
+ fetchRaw(url).then(text => {
+ const oMessage = new MessageModel();
+ MimeToMessage(text, oMessage);
+ // cleanHTML
+ oMessage.popupMessage();
+ });
+ }
+ return;
+ }
+
+ el = eqs(event, '.attachmentsPlace .showPreplay');
+ if (el) {
+ stopEvent(event);
+ const attachment = ko.dataFor(el);
+ if (attachment && SMAudio.supported) {
+ 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
+ }
+ }
+ return;
+ }
+
+ 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 {
+ download(url, attachment.fileName);
+ }
+ }
+ }
+ });
+
+ keyScopeReal.subscribe(value => this.messageDomFocused(ScopeMessageView === value));
+
+ // initShortcuts
+
+ // exit fullscreen, back
+ addShortcut('escape', '', ScopeMessageView, () => {
+ if (!this.viewModelDom.hidden && this.message()) {
+ const preview = SettingsUserStore.usePreviewPane();
+ if (isFullscreen()) {
+ exitFullscreen();
+ if (preview) {
+ AppUserStore.focusedState(ScopeMessageList);
+ }
+ } else if (!preview) {
+ this.message(null);
+ } else {
+ AppUserStore.focusedState(ScopeMessageList);
+ }
+
+ return false;
+ }
+ });
+
+ // fullscreen
+ addShortcut('enter,open', '', ScopeMessageView, () => {
+ isFullscreen() || toggleFullscreen();
+ return false;
+ });
+
+ // reply
+ registerShortcut('r,mailreply', '', [ScopeMessageList, ScopeMessageView], () => {
+ if (this.message()) {
+ this.replyCommand();
+ return false;
+ }
+ return true;
+ });
+
+ // replyAll
+ registerShortcut('a', '', [ScopeMessageList, ScopeMessageView], () => {
+ if (this.message()) {
+ this.replyAllCommand();
+ return false;
+ }
+ });
+ registerShortcut('mailreply', 'shift', [ScopeMessageList, ScopeMessageView], () => {
+ if (this.message()) {
+ this.replyAllCommand();
+ return false;
+ }
+ });
+
+ // forward
+ registerShortcut('f,mailforward', '', [ScopeMessageList, ScopeMessageView], () => {
+ if (this.message()) {
+ this.forwardCommand();
+ return false;
+ }
+ });
+
+ // message information
+ registerShortcut('i', 'meta', [ScopeMessageList, ScopeMessageView], () => {
+ this.message() && this.toggleFullInfo();
+ return false;
+ });
+
+ // toggle message blockquotes
+ registerShortcut('b', '', [ScopeMessageList, ScopeMessageView], () => {
+ const message = this.message();
+ if (message?.body) {
+ message.body.querySelectorAll('details').forEach(node => node.open = !node.open);
+ return false;
+ }
+ });
+
+ this.body = dom.querySelector('.bodyText');
+ }
+
+ onShow(message) {
+ this.message(message);
+ message.body = this.body;
+ message.viewBody(message.html());
+ }
+
+ toggleAttachmentControls() {
+ const b = !this.showAttachmentControls();
+ this.showAttachmentControls(b);
+ Local.set(ClientSideKeyNameMessageAttachmentControls, b);
+ }
+
+ downloadAsZip() {
+ const hashes = (this.message()?.attachments || [])
+ .map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '')
+ .filter(v => v);
+ downloadZip(
+ this.message().subject(),
+ hashes,
+ () => this.downloadAsZipError(true),
+ this.downloadAsZipLoading
+ );
+ }
+
+ /**
+ * @param {MessageModel} oMessage
+ * @returns {void}
+ */
+ showImages() {
+ this.message().showExternalImages();
+ }
+
+ whitelistText(txt) {
+ let value = (SettingsUserStore.viewImagesWhitelist().trim() + '\n' + txt).trim();
+/*
+ if ('pass' === this.message().spf[0]?.[0]) value += '+spf';
+ if ('pass' === this.message().dkim[0]?.[0]) value += '+dkim';
+ if ('pass' === this.message().dmarc[0]?.[0]) value += '+dmarc';
+*/
+ SettingsUserStore.viewImagesWhitelist(value);
+ Remote.saveSetting('ViewImagesWhitelist', value);
+ this.message().showExternalImages(1);
+ }
+
+ /**
+ * @param {MessageModel} oMessage
+ * @returns {void}
+ */
+ readReceipt() {
+ let oMessage = this.message()
+ if (oMessage.readReceipt()) {
+ Remote.request('SendReadReceiptMessage', iError => {
+ if (!iError) {
+ oMessage.flags.push('$mdnsent');
+// oMessage.flags.valueHasMutated();
+ }
+ }, {
+ messageFolder: oMessage.folder,
+ messageUid: oMessage.uid,
+ readReceipt: oMessage.readReceipt(),
+ subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
+ plain: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
+ });
+ }
+ }
+
+ newTag() {
+ let message = this.message();
+ 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 = this.message();
+ PgpUserStore.decrypt(oMessage).then(result => {
+ if (result) {
+ oMessage.pgpDecrypted(true);
+ 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
+ });
+ }
+ }
+ } else {
+ // TODO: translate
+ alert('Decryption failed, canceled or not possible');
+ }
+ })
+ .catch(e => console.error(e));
+ }
+
+ pgpVerify(/*self, event*/) {
+ const oMessage = this.message()/*, ctrl = event.target.closest('.openpgp-control')*/;
+ PgpUserStore.verify(oMessage).then(result => {
+ if (result) {
+ oMessage.pgpVerified(result);
+ } else {
+ alert('Verification failed or no valid public key found');
+ }
+/*
+ if (result?.success) {
+ i18n('OPENPGP/GOOD_SIGNATURE', {
+ USER: validKey.user + ' (' + validKey.id + ')'
+ });
+ message.getText()
+ } else {
+ const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
+ additional = keyIds
+ ? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
+ : '';
+
+ i18n('OPENPGP/ERROR', {
+ ERROR: 'message'
+ }) + (additional ? ' (' + additional + ')' : '');
+ }
+*/
+ });
+ }
+
+}
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsMessage.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsMessage.html
new file mode 100644
index 000000000..a67f6c298
--- /dev/null
+++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsMessage.html
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚙
+
+
+ ✖
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+