Page MenuHome GnuPG

No OneTemporary

diff --git a/web/src/App.vue b/web/src/App.vue
index 33c09cd..17d3ca2 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -1,475 +1,475 @@
<!--
SPDX-FileCopyrightText: 2025 g10 code GmbH
SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
SPDX-License-Identifier: GPL-2.0-or-later
-->
<script setup>
import {computed, onMounted, ref} from "vue"
import {i18n, i18nc} from './services/i18n.js'
import {createNestablePublicClientApplication} from "@azure/msal-browser";
/* global Office */
const error = ref('')
const versionWarning = ref('')
const hasSelection = ref(false)
const viewerOpen = ref(false)
const nativeClientId = ref('')
const proxyconnected = ref(false)
const clientconnected = ref(false)
const status = ref({
encrypted: false,
signed: false,
drafts: [],
fetched: false,
fetching: false,
folderId: '',
features: [],
})
const socket = ref(null)
let pca = undefined;
let ewsAccessToken = null;
const loaded = computed(() => {
return status.value.fetched;
})
const statusText = computed(() => {
if (!loaded.value) {
return error.value.length > 0 ? error.value : i18nc("Loading placeholder", "Loading…");
}
if (status.value.encrypted) {
return status.value.signed ? i18n("This mail is encrypted and signed.") : i18n("This mail is encrypted.");
}
if (status.value.signed) {
return i18n("This mail is signed.")
}
return i18n("This mail is not encrypted nor signed.");
})
const decryptButtonText = computed(() => {
if (!loaded.value) {
return '';
}
if (status.value.encrypted) {
return i18nc("@action:button", "Decrypt")
}
return i18nc("@action:button", "View email")
})
function sendCommand(command, args) {
if (!socket.value) {
console.log("socket not connected");
return;
}
socket.value.send(JSON.stringify({
command: command,
arguments: args,
id: nativeClientId.value,
}));
}
function gpgolLog(message, args) {
console.log(message, args);
sendCommand("log", {
message,
args: JSON.stringify(args),
},
);
}
function genericMailAction(command) {
sendCommand(command, {
email: Office.context.mailbox.userProfile.emailAddress,
displayName: Office.context.mailbox.userProfile.displayName,
folderId: status.value.folderId,
itemId: Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0),
ewsAccessToken,
});
}
function reencrypt() {
genericMailAction('reencrypt');
}
function view() {
genericMailAction('view');
}
function reply() {
genericMailAction('reply');
}
function forward() {
genericMailAction('forward');
}
function newEmail() {
genericMailAction('composer');
}
function openDraft(id) {
sendCommand('open-draft',
{
draftId: id,
email: Office.context.mailbox.userProfile.emailAddress,
displayName: Office.context.mailbox.userProfile.displayName,
ewsAccessToken,
}
);
}
function deleteDraft(id) {
sendCommand('delete-draft',
{
draftId: id,
email: Office.context.mailbox.userProfile.emailAddress,
displayName: Office.context.mailbox.userProfile.displayName,
ewsAccessToken,
}
);
// TODO this.status.drafts.splice(this.status.drafts.findIndex((draft) => draft.id === id), 1);
}
function info() {
if (status.value.fetching || nativeClientId.value.length === 0) {
return;
}
status.value.fetched = false;
status.value.fetching = true;
sendCommand('info',
{
itemId: Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0),
email: Office.context.mailbox.userProfile.emailAddress,
ewsAccessToken,
}
);
}
function pairingRequest(token) {
sendCommand("pairing",
{
type: "web",
token: token,
}
);
}
function pairDevice(deviceId) {
nativeClientId.value = deviceId;
localStorage.setItem("nativeClientId", deviceId);
}
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
sendCommand('ews-response',
{
requestId: message.arguments.requestId,
email: Office.context.mailbox.userProfile.emailAddress,
body: asyncResult.value,
}
);
});
}
function webSocketConnect() {
console.log("Set socket", socket.value)
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
return;
}
console.log("Set socket")
socket.value = new WebSocket("wss://" + window.location.host + '/websocket');
// Connection opened
socket.value.addEventListener("open", (event) => {
error.value = '';
proxyconnected.value = true;
clientconnected.value = false;
sendCommand("register",
{
email: [Office.context.mailbox.userProfile.emailAddress],
type: 'webclient',
},
);
sendCommand('restore-autosave',
{
email: Office.context.mailbox.userProfile.emailAddress,
displayName: Office.context.mailbox.userProfile.displayName,
ewsAccessToken,
}
);
info();
});
socket.value.addEventListener("close", (event) => {
error.value = i18n("Native client was disconnected, reconnecting in 1 second.");
console.log(event.reason)
proxyconnected.value = false;
setTimeout(function () {
webSocketConnect();
}, 1000);
});
socket.value.addEventListener("error", (event) => {
error.value = i18n("Native client received an error");
socket.value.close();
proxyconnected.value = false;
});
// Listen for messages
socket.value.addEventListener("message", function (result) {
// TODO: verify id of connection?
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':
error.value = message.arguments.error;
break;
case 'status-update':
viewerOpen.value = message.arguments.viewerOpen;
status.value.drafts = message.arguments.drafts;
status.value.features = message.arguments.features;
clientconnected.value = true;
break;
case 'disconnection':
error.value = i18n("Native client was disconnected")
clientconnected.value = false;
break;
case 'connection':
error.value = '';
if (nativeClientId.value.length) {
if (message.id != nativeClientId.value) {
error.value = 'Connection attempt from invalid client.';
break;
}
} else {
pairDevice(message.id);
}
break;
case 'info-fetched':
console.log(message.arguments)
const {itemId, folderId, encrypted, signed, version} = message.arguments;
status.value.fetching = false;
if (itemId === Office.context.mailbox.convertToEwsId(Office.context.mailbox.item.itemId, Office.MailboxEnums.RestVersion.v2_0)) {
status.value.fetched = true;
status.value.encrypted = encrypted;
status.value.signed = signed;
status.value.folderId = folderId;
if (viewerOpen.value) {
view();
}
let params = new URLSearchParams(document.location.search);
let manifestVersion = params.get("version");
if (version !== manifestVersion) {
versionWarning.value = i18nc("@info", "Version mismatch. Make sure you installed the last manifest.xml.")
}
} else {
status.value.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();
}
}
});
}
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) {
error.value = i18n("Unable to acquire access token.");
}
}
}
onMounted(async () => {
let storedId = localStorage.getItem("nativeClientId");
nativeClientId.value = storedId ? storedId : '';
if (Office.context.requirements.isSetSupported("NestedAppAuth", "1.1")) {
await auth();
}
webSocketConnect();
Office.context.mailbox.addHandlerAsync(Office.EventType.ItemChanged, (eventArgs) => {
status.value.fetching = false;
status.value.fetched = false;
if (Office.context.mailbox.item) {
info();
} else {
content.value = '';
hasSelection.value = false;
}
});
})
</script>
<template>
<div class="d-flex gap" id="app">
<div class="alert alert-error rounded-md p-2 d-flex flex-row gap" v-if="error.length > 0">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="h-32 w-32"
style="color: rgb(33, 33, 33);"><!----> <!---->
<path
d="M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333Zm-.001 10.835a.999.999 0 1 1 0 1.998.999.999 0 0 1 0-1.998ZM11.994 7a.75.75 0 0 1 .744.648l.007.101.004 4.502a.75.75 0 0 1-1.493.103l-.007-.102-.004-4.501a.75.75 0 0 1 .75-.751Z"
fill="currentColor" fill-opacity="1"></path>
</svg>
<div>{{ error }}</div>
</div>
<div class="alert alert-warning rounded-md p-2 d-flex flex-row gap" v-if="versionWarning.length > 0">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="h-32 w-32" style="color: rgb(33, 33, 33);">
<path
d="M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z"
fill="currentColor" fill-opacity="1"></path>
</svg>
<div>{{ versionWarning }}</div>
</div>
<div class="alert alert-warning rounded-md p-2 d-flex flex-column gap" v-if="nativeClientId.length == 0">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="h-32 w-32" style="color: rgb(33, 33, 33);">
<path
d="M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z"
fill="currentColor" fill-opacity="1"></path>
</svg>
- <div>{{ i18n("Not paired to native client. Please ensure GPGOL/Web app is running and in pairing mode, then paste or enter pairing code, below:" ) }}
+ <div>{{ i18n("Not paired to native client. Please ensure GPGOL/Web app is running and in pairing mode, then paste or enter pairing code below:" ) }}
<input type="text" placeholder="1234" @input="event => pairingRequest(event.target.value)"/>
</div>
</div>
<div>
<div class="mt-3">
{{ statusText }}
</div>
<button v-if="loaded" class="w-100 btn rounded-md fa mt-3" @click="view" :disabled="viewerOpen">
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 20 20">
<path fill="currentColor"
d="M18 5.95a2.5 2.5 0 1 0-1.002-4.9A2.5 2.5 0 0 0 18 5.95M4.5 3h9.535a3.5 3.5 0 0 0 0 1H4.5A1.5 1.5 0 0 0 3 5.5v.302l7 4.118l5.754-3.386c.375.217.795.365 1.241.43l-6.741 3.967a.5.5 0 0 1-.426.038l-.082-.038L3 6.963V13.5A1.5 1.5 0 0 0 4.5 15h11a1.5 1.5 0 0 0 1.5-1.5V6.965a3.5 3.5 0 0 0 1 0V13.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 13.5v-8A2.5 2.5 0 0 1 4.5 3"/>
</svg>
{{ decryptButtonText }}
</button>
<div v-if="viewerOpen"><small>{{ i18nc("@info", "Viewer already open.") }}</small></div>
</div>
<hr class="w-100 my-0"/>
<button class="w-100 btn rounded-md" @click="newEmail()" :disabled="!loaded">
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 20 20">
<path fill="currentColor"
d="M15.5 4A2.5 2.5 0 0 1 18 6.5v8a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 14.5v-8A2.5 2.5 0 0 1 4.5 4zM17 7.961l-6.746 3.97a.5.5 0 0 1-.426.038l-.082-.038L3 7.963V14.5A1.5 1.5 0 0 0 4.5 16h11a1.5 1.5 0 0 0 1.5-1.5zM15.5 5h-11A1.5 1.5 0 0 0 3 6.5v.302l7 4.118l7-4.12v-.3A1.5 1.5 0 0 0 15.5 5"/>
</svg>
{{ i18nc("@action:button", "New secure email") }}
</button>
<button class="w-100 btn rounded-md" @click="reply" :disabled="!loaded">
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 20 20">
<path fill="currentColor"
d="M7.354 3.646a.5.5 0 0 1 0 .708L3.707 8H10.5a7.5 7.5 0 0 1 7.5 7.5a.5.5 0 0 1-1 0A6.5 6.5 0 0 0 10.5 9H3.707l3.647 3.646a.5.5 0 0 1-.708.708l-4.5-4.5a.5.5 0 0 1 0-.708l4.5-4.5a.5.5 0 0 1 .708 0"/>
</svg>
{{ i18nc("@action:button", "Reply securely") }}
</button>
<button class="w-100 btn rounded-md" @click="forward" :disabled="!loaded">
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 20 20">
<path fill="currentColor"
d="m16.293 9l-3.39 3.39a.5.5 0 0 0 .639.765l.069-.058l4.243-4.243a.5.5 0 0 0 .057-.638l-.057-.07l-4.243-4.242a.5.5 0 0 0-.765.638l.058.07L16.293 8H10a7.5 7.5 0 0 0-7.496 7.258L2.5 15.5a.5.5 0 0 0 1 0a6.5 6.5 0 0 1 6.267-6.496L10 9z"/>
</svg>
{{ i18nc("@action:button", "Forward securely") }}
</button>
<button v-if="status.features.includes('reencrypt')" class="w-100 btn rounded-md d-none" @click="reencrypt" :disabled="!loaded" >
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 20 20">
<path fill="currentColor"
d="m16.293 9l-3.39 3.39a.5.5 0 0 0 .639.765l.069-.058l4.243-4.243a.5.5 0 0 0 .057-.638l-.057-.07l-4.243-4.242a.5.5 0 0 0-.765.638l.058.07L16.293 8H10a7.5 7.5 0 0 0-7.496 7.258L2.5 15.5a.5.5 0 0 0 1 0a6.5 6.5 0 0 1 6.267-6.496L10 9z"/>
</svg>
{{ i18nc("@action:button", "Reencrypt") }}
</button>
<div class="draft-container" v-if="loaded">
<h2 class="mb-0">{{ i18n("Drafts") }}</h2>
<ul v-if="status.drafts.length > 0" id="drafts" class="my-0 list-unstyled gap d-flex">
<li v-for="draft in status.drafts" :key="draft.id" class="d-flex flex-row">
<button class="btn w-100 d-flex flex-row align-items-center rounded-e-md" @click="openDraft(draft.id)">
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 20 20">
<path fill="currentColor"
d="M15.5 3.001a2.5 2.5 0 0 1 2.5 2.5v3.633a2.9 2.9 0 0 0-1-.131V6.962l-6.746 3.97a.5.5 0 0 1-.426.038l-.082-.038L3 6.964v6.537a1.5 1.5 0 0 0 1.5 1.5h5.484c-.227.3-.4.639-.51 1H4.5a2.5 2.5 0 0 1-2.5-2.5v-8a2.5 2.5 0 0 1 2.5-2.5zm0 1h-11a1.5 1.5 0 0 0-1.5 1.5v.302l7 4.118l7-4.119v-.301a1.5 1.5 0 0 0-1.5-1.5m-4.52 11.376l4.83-4.83a1.87 1.87 0 1 1 2.644 2.646l-4.83 4.829a2.2 2.2 0 0 1-1.02.578l-1.498.374a.89.89 0 0 1-1.079-1.078l.375-1.498a2.2 2.2 0 0 1 .578-1.02"/>
</svg>
{{ i18n("Last Modified: %1", displayDate(draft.last_modification)) }}
</button>
<button class="btn btn-danger ms-auto py-1 rounded-e-md" @click="deleteDraft(draft.id)">
<span class="sr-only">{{ i18nc("@action:button", "Delete") }}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M10 5h4a2 2 0 1 0-4 0M8.5 5a3.5 3.5 0 1 1 7 0h5.75a.75.75 0 0 1 0 1.5h-1.32l-1.17 12.111A3.75 3.75 0 0 1 15.026 22H8.974a3.75 3.75 0 0 1-3.733-3.389L4.07 6.5H2.75a.75.75 0 0 1 0-1.5zm2 4.75a.75.75 0 0 0-1.5 0v7.5a.75.75 0 0 0 1.5 0zM14.25 9a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-1.5 0v-7.5a.75.75 0 0 1 .75-.75m-7.516 9.467a2.25 2.25 0 0 0 2.24 2.033h6.052a2.25 2.25 0 0 0 2.24-2.033L18.424 6.5H5.576z"/>
</svg>
</button>
</li>
</ul>
<p v-else>{{ i18nc("Placeholder", "No draft found") }}</p>
</div>
<div class="rounded-md p-2 d-flex flex-column">
<h3 class="mb-0"><span v-if="proxyconnected && clientconnected">&#x2705;</span><span v-else="proxyconnected && clientconnected">&#x26A0;</span>{{ i18nc("Short heading", "Connection status") }}</h3>
<p>
<span v-if="proxyconnected">{{ i18nc("Status", "Connected to proxy server") }}</span><span v-else>{{ i18nc("Status", "Not connected to proxy server") }}</span><br/>
<span v-if="proxyconnected && clientconnected">{{ i18nc("Status", "Connected to native client") }}</span><span v-else>{{ i18nc("Status", "Not connected to native client") }}</span>
</p>
<button @click="pairDevice('')">Pair new native client</button>
</div>
</div>
</template>
<style scoped>
</style>

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jan 16, 12:48 AM (4 h, 35 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f6/c2/e80e8ce05e262bf40c9504b83d72

Event Timeline