Changeset View
Changeset View
Standalone View
Standalone View
web/src/script.js
| Context not available. | |||||
| // SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com> | // SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com> | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | // SPDX-License-Identifier: GPL-2.0-or-later | ||||
| import {createApp} from 'vue' | |||||
| import App from './App.vue' | |||||
| const global = (0, eval)("this"); | const global = (0, eval)("this"); | ||||
| const Office = global.Office; | const Office = global.Office; | ||||
| const messages = global.messages; | const messages = global.messages; | ||||
| Office.onReady(() => { | import {i18n, i18nc} from './services/i18n.js' | ||||
| const app = createApp(App) | import {getElement, changeElement, getIcon, spanFromHTML, divFromHTML, showElement, | ||||
| app.mount('#app') | makeButton, setMessage, setErrorMessage, haveError} from './utils.js' | ||||
| import {createNestablePublicClientApplication} from "@azure/msal-browser"; | |||||
| "use strict"; | |||||
| let mainButtons = []; | |||||
| let devicesToVerify = []; | |||||
| let verifiedNativeClients = []; | |||||
| let status = { | |||||
| encrypted: false, | |||||
| signed: false, | |||||
| drafts: [], | |||||
| fetched: false, | |||||
| fetching: false, | |||||
| folderId: '', | |||||
| features: [], | |||||
| viewerOpen: false, | |||||
| }; | |||||
| let socket = null; | |||||
| let pca = undefined; | |||||
| let ewsAccessToken = null; | |||||
| Office.onReady(async () => { | |||||
| verifiedNativeClients = localStorage.getItem("verifiedNativeClients")?.split(';') ?? [] | |||||
| if (Office.context.requirements.isSetSupported("NestedAppAuth", "1.1")) { | |||||
| await auth(); | |||||
| } | |||||
| webSocketConnect(); | |||||
| Office.context.mailbox.addHandlerAsync(Office.EventType.ItemChanged, (eventArgs) => { | |||||
| status.fetching = false; | |||||
| status.fetched = false; | |||||
| if (Office.context.mailbox.item) { | |||||
| console.log(Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0), Office.context.mailbox.item.itemId); | |||||
| info(); | |||||
| } else { | |||||
| setErrorMessage(i18n("No item selected")); | |||||
| } | |||||
| updateStatusText(); | |||||
| }); | |||||
| initUI(); | |||||
| }) | }) | ||||
| function initUI() { | |||||
| let h2 = document.createElement("h2"); | |||||
| h2.classList.add("mb-0"); | |||||
| h2.appendChild(document.createTextNode(i18n("Drafts"))); | |||||
| changeElement("draftscaption", h2); | |||||
| mainButtons = { | |||||
| "decrypt": makeButton("view", "", function() { view(); }), | |||||
| "newemail": makeButton("new", i18nc("@action:button", "New secure email"), function() { newEmail(); }), | |||||
| "reply": makeButton("reply", i18nc("@action:button", "Reply securely"), function() { reply(); }), | |||||
| "forward": makeButton("forward", i18nc("@action:button", "Forward securely"), function() { forward(); }), | |||||
| "reencrypt": makeButton("reencrypt", i18nc("@action:button", "Reencrypt folder"), function() { reencrypt(); }), | |||||
| } | |||||
| for (let [id, button] of Object.entries(mainButtons)) { | |||||
| changeElement(id, button); | |||||
| } | |||||
| let box = divFromHTML("<small>" + i18nc("@info", "Viewer already open.") + "</small>"); | |||||
| changeElement("vieweropenbox", box); | |||||
| updateStatusText(); | |||||
| } | |||||
| function updatePairing() { | |||||
| let elems = []; | |||||
| for (dev of devicesToVerify) { | |||||
| let caption = spanFromHTML(i18n("Unknown device: %1. Do you trust this device?", dev.name), "div"); | |||||
| caption.replaceChildren.apply(caption, [getIcon("warning")].concat(caption.childNodes())); | |||||
| let buttons = document.createElement("div"); | |||||
| buttons.classList.add("d-flex", "flex-row", "gap"); | |||||
| buttons.appendChild(makeButton("", i18n("Don't Trust"), ignore.bind(null, device.id))); | |||||
| buttons.appendChild(makeButton("", i18n("Trust"), trust.bind(null, device.id))); | |||||
| elems.push(caption); | |||||
| elems.push(buttons); | |||||
| } | |||||
| let box = getElement("pairingbox"); | |||||
| box.replaceChildren.apply(box, elems); | |||||
| showElement(box, elems.length > 0); | |||||
| updateStatusText(); | |||||
| } | |||||
| function updateStatusText() { | |||||
| let msg = i18n("This mail is not encrypted nor signed."); | |||||
| if (!status.fetched) { | |||||
| if (devicesToVerify.length > 0) { | |||||
| msg = i18nc("Loading placeholder", "Waiting for authorization"); | |||||
| } else { | |||||
| msg = haveError() ? "" : i18nc("Loading placeholder", "Loading…"); | |||||
| } | |||||
| for (let [id, button] of Object.entries(mainButtons)) { | |||||
| button.disabled = true; | |||||
| } | |||||
| } else { | |||||
| if (status.encrypted) { | |||||
| msg = status.signed ? i18n("This mail is encrypted and signed.") : i18n("This mail is encrypted."); | |||||
| } else if (status.signed) { | |||||
| msg = i18n("This mail is signed.") | |||||
| } | |||||
| for (let [id, button] of Object.entries(mainButtons)) { | |||||
| button.disabled = false; | |||||
| } | |||||
| } | |||||
| getElement("statusbox").replaceChildren(document.createTextNode(msg)); | |||||
| showElement(getElement("vieweropenbox"), status.viewerOpen); | |||||
| mainButtons.decrypt.setIconAndText("view", status.encrypted ? i18nc("@action:button", "Decrypt") : i18nc("@action:button", "View email")); | |||||
| mainButtons.decrypt.disabled = mainButtons.decrypt.disabled && !status.viewerOpen; | |||||
| updateDraftList(); // TODO: perhaps only on changes | |||||
| } | |||||
| function updateDraftList() { | |||||
| if (status.drafts.length > 0) { | |||||
| let draftsList = document.createElement("ul"); | |||||
| draftsList.classList.add("my-0", "list-unstyled", "gap", "d-flex"); | |||||
| for (let draft of status.drafts) { | |||||
| let li = document.createElement("li"); | |||||
| li.classList.add("d-flex", "flex-row"); | |||||
| let button = makeButton("opendraft", | |||||
| i18n("Last Modified: %1", displayDate(draft.last_modification)), | |||||
| function() { openDraft(draft.id); }, | |||||
| ["btn", "w-100", "d-flex", "flex-row", "align-items-center", "rounded-e-md"]); | |||||
| li.appendChild(button); | |||||
| button = makeButton("delete", | |||||
| '<span class="sr-only">' + i18nc("@action:button", "Delete") + '</span>', | |||||
| function() { deleteDraft(draft.id) }, | |||||
| ["btn", "btn-danger", "ms-auto", "py-1", "rounded-e-md"]); | |||||
| li.appendChild(button); | |||||
| draftsList.appendChild(li); | |||||
| } | |||||
| changeElement("draftslist", draftsList); | |||||
| } else { | |||||
| changeElement("draftslist", spanFromHTML("<p>" + i18nc("Placeholder", "No draft found") + "</p>")); | |||||
| } | |||||
| } | |||||
| function gpgolLog(message, args) { | |||||
| console.log(message, args); | |||||
| if (socket) { | |||||
| socket.send(JSON.stringify({ | |||||
| command: "log", | |||||
| arguments: { | |||||
| message, | |||||
| args: JSON.stringify(args), | |||||
| }, | |||||
| })); | |||||
| } | |||||
| } | |||||
| function genericMailAction(command) { | |||||
| socket.send(JSON.stringify({ | |||||
| command, | |||||
| arguments: { | |||||
| email: Office.context.mailbox.userProfile.emailAddress, | |||||
| displayName: Office.context.mailbox.userProfile.displayName, | |||||
| folderId: status.folderId, | |||||
| itemId: Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0), | |||||
| ewsAccessToken, | |||||
| verifiedNativeClients, | |||||
| } | |||||
| })); | |||||
| } | |||||
| function reencrypt() { | |||||
| genericMailAction('reencrypt'); | |||||
| } | |||||
| function view() { | |||||
| genericMailAction('view'); | |||||
| } | |||||
| function reply() { | |||||
| genericMailAction('reply'); | |||||
| } | |||||
| function forward() { | |||||
| genericMailAction('forward'); | |||||
| } | |||||
| function newEmail() { | |||||
| genericMailAction('composer'); | |||||
| } | |||||
| function openDraft(id) { | |||||
| socket.send(JSON.stringify({ | |||||
| command: 'open-draft', | |||||
| arguments: { | |||||
| id, | |||||
| email: Office.context.mailbox.userProfile.emailAddress, | |||||
| displayName: Office.context.mailbox.userProfile.displayName, | |||||
| ewsAccessToken, | |||||
| verifiedNativeClients, | |||||
| } | |||||
| })); | |||||
| } | |||||
| function deleteDraft(id) { | |||||
| socket.send(JSON.stringify({ | |||||
| command: 'delete-draft', | |||||
| arguments: { | |||||
| id: id, | |||||
| email: Office.context.mailbox.userProfile.emailAddress, | |||||
| displayName: Office.context.mailbox.userProfile.displayName, | |||||
| ewsAccessToken, | |||||
| verifiedNativeClients, | |||||
| } | |||||
| })); | |||||
| } | |||||
| function info() { | |||||
| if (status.fetching || verifiedNativeClients.length === 0) { | |||||
| return; | |||||
| } | |||||
| status.fetched = false; | |||||
| status.fetching = true; | |||||
| updateStatusText(); | |||||
| socket.send(JSON.stringify({ | |||||
| command: 'info', | |||||
| arguments: { | |||||
| itemId: Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0), | |||||
| email: Office.context.mailbox.userProfile.emailAddress, | |||||
| ewsAccessToken, | |||||
| verifiedNativeClients, | |||||
| } | |||||
| })); | |||||
| } | |||||
| function trust(deviceId) { | |||||
| devicesToVerify.splice(devicesToVerify.findIndex((device) => device.id === deviceId), 1); | |||||
| verifiedNativeClients.push(deviceId); | |||||
| localStorage.setItem("verifiedNativeClients", verifiedNativeClients.join(';')); | |||||
| info(); | |||||
| updatePairing(); | |||||
| } | |||||
| function ignore(deviceId) { | |||||
| devicesToVerify.splice(devicesToVerify.findIndex((device) => device.id === deviceId), 1); | |||||
| updatePairing(); | |||||
| } | |||||
| function displayDate(timestamp) { | |||||
| const date = new Date(timestamp * 1000); | |||||
| let todayDate = new Date(); | |||||
| if ((new Date(date)).setHours(0, 0, 0, 0) === todayDate.setHours(0, 0, 0, 0)) { | |||||
| return date.toLocaleTimeString([], { | |||||
| hour: 'numeric', | |||||
| minute: 'numeric', | |||||
| }); | |||||
| } else { | |||||
| return date.toLocaleDateString(); | |||||
| } | |||||
| } | |||||
| /// Only called when not using Office365 | |||||
| async function executeEws(message) { | |||||
| Office.context.mailbox.makeEwsRequestAsync(message.arguments.body, (asyncResult) => { | |||||
| if (asyncResult.error) { | |||||
| gpgolLog("Error while trying to send email via EWS", {error: asyncResult.error, value: asyncResult.value,}); | |||||
| return; | |||||
| } | |||||
| gpgolLog("Email sent", {value: asyncResult.value}); | |||||
| // let the client known that the email was sent | |||||
| socket.send(JSON.stringify({ | |||||
| command: 'ews-response', | |||||
| arguments: { | |||||
| requestId: message.arguments.requestId, | |||||
| email: Office.context.mailbox.userProfile.emailAddress, | |||||
| body: asyncResult.value, | |||||
| } | |||||
| })); | |||||
| }); | |||||
| } | |||||
| function webSocketConnect() { | |||||
| console.log("Set socket", socket) | |||||
| if (socket && socket.readyState === WebSocket.OPEN) { | |||||
| return; | |||||
| } | |||||
| console.log("Set socket") | |||||
| socket = new WebSocket("wss://" + window.location.host + '/websocket'); | |||||
| // Connection opened | |||||
| socket.addEventListener("open", (event) => { | |||||
| setErrorMessage(''); | |||||
| socket.send(JSON.stringify({ | |||||
| command: "register", | |||||
| arguments: { | |||||
| emails: [Office.context.mailbox.userProfile.emailAddress], | |||||
| type: 'webclient', | |||||
| }, | |||||
| })); | |||||
| socket.send(JSON.stringify({ | |||||
| command: 'restore-autosave', | |||||
| arguments: { | |||||
| email: Office.context.mailbox.userProfile.emailAddress, | |||||
| displayName: Office.context.mailbox.userProfile.displayName, | |||||
| ewsAccessToken, | |||||
| } | |||||
| })); | |||||
| info() | |||||
| }); | |||||
| socket.addEventListener("close", (event) => { | |||||
| setErrorMessage(i18n("Native client was disconnected, reconnecting in 1 second.")); | |||||
| console.log(event.reason) | |||||
| setTimeout(function () { | |||||
| webSocketConnect(); | |||||
| }, 1000); | |||||
| }); | |||||
| socket.addEventListener("error", (event) => { | |||||
| setErrorMessage(i18n("Native client received an error")); | |||||
| socket.close(); | |||||
| }); | |||||
| // Listen for messages | |||||
| socket.addEventListener("message", function (result) { | |||||
| const {data} = result; | |||||
| const message = JSON.parse(data); | |||||
| gpgolLog("Received message from server", {command: message.command}); | |||||
| switch (message.command) { | |||||
| case 'ews': | |||||
| executeEws(message); | |||||
| break; | |||||
| case 'error': | |||||
| setErrorMessage(message.arguments.error); | |||||
| break; | |||||
| case 'status-update': | |||||
| status.drafts = message.arguments.drafts; | |||||
| status.features = message.arguments.features; | |||||
| status.viewerOpen = message.arguments.viewerOpen; | |||||
| updateStatusText(); | |||||
| break; | |||||
| case 'disconnection': | |||||
| setErrorMessage(i18n("Native client was disconnected")); | |||||
| break; | |||||
| case 'connection': | |||||
| setErrorMessage(''); | |||||
| if (!verifiedNativeClients.includes(message.arguments.id)) { | |||||
| if (devicesToVerify.findIndex(device => device.id === message.arguments.id) >= 0) { | |||||
| break; | |||||
| } | |||||
| devicesToVerify.push({ | |||||
| id: message.arguments.id, | |||||
| name: message.arguments.name, | |||||
| }) | |||||
| updatePairing(); | |||||
| } else { | |||||
| info(); | |||||
| } | |||||
| break; | |||||
| case 'info-fetched': | |||||
| console.log(message.arguments) | |||||
| const {itemId, folderId, encrypted, signed, version} = message.arguments; | |||||
| status.fetching = false; | |||||
| if (itemId === Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0)) { | |||||
| status.fetched = true; | |||||
| status.encrypted = encrypted; | |||||
| status.signed = signed; | |||||
| status.folderId = folderId; | |||||
| if (status.viewerOpen) { | |||||
| view(); | |||||
| } | |||||
| let params = new URLSearchParams(document.location.search); | |||||
| let manifestVersion = params.get("version"); | |||||
| if (version !== manifestVersion) { | |||||
| setMessage("versionbox", i18nc("@info", "Version mismatch. Make sure you installed the last manifest.xml."), "warning"); | |||||
| } | |||||
| } else { | |||||
| status.fetched = false; | |||||
| gpgolLog("Received info for wrong email", {itemId, currentItemId: Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0) }); | |||||
| info(); | |||||
| } | |||||
| updateStatusText(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| async function auth() { | |||||
| pca = await createNestablePublicClientApplication({ | |||||
| auth: { | |||||
| clientId: "1d6f4a59-be04-4274-8793-71b4c081eb72", | |||||
| authority: "https://login.microsoftonline.com/common" | |||||
| }, | |||||
| }); | |||||
| { | |||||
| const tokenRequest = { | |||||
| scopes: ["https://outlook.office365.com/EWS.AccessAsUser.All"] | |||||
| } | |||||
| try { | |||||
| console.log("Trying to acquire token silently..."); | |||||
| const userAccount = await pca.acquireTokenSilent(tokenRequest); | |||||
| console.log("Acquired token silently."); | |||||
| ewsAccessToken = userAccount.accessToken; | |||||
| } catch (error) { | |||||
| console.log(`Unable to acquire token silently: ${error}`); | |||||
| } | |||||
| if (ewsAccessToken === null) { | |||||
| // Acquire token silent failure. Send an interactive request via popup. | |||||
| try { | |||||
| console.log("Trying to acquire token interactively..."); | |||||
| const userAccount = await pca.acquireTokenPopup(tokenRequest); | |||||
| console.log("Acquired token interactively."); | |||||
| ewsAccessToken = userAccount.accessToken; | |||||
| } catch (popupError) { | |||||
| // Acquire token interactive failure. | |||||
| console.error( `Unable to acquire token interactively: ${popupError}`); | |||||
| } | |||||
| } | |||||
| // Log error if both silent and popup requests failed. | |||||
| if (ewsAccessToken === null) { | |||||
| setErrorMessage(i18n("Unable to acquire access token.")); | |||||
| } | |||||
| } | |||||
| } | |||||
| Context not available. | |||||