diff --git a/i18n.py b/i18n.py new file mode 100644 index 0000000..3e31390 --- /dev/null +++ b/i18n.py @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: 2024 Carl Schwan +# SPDX-License-Identifier: LGPL-2.0-or-later + +import argparse +import polib +from lxml import etree +import os +import os.path +import subprocess +import glob +import json + +def extract_strings(output_dir): + print("Extracting") + + subprocess.run(["xgettext", "-C", "--from-code=UTF-8", "-ci18n", "-ki18n:1", "-ki18nc:1c,2", "server/web/assets/script.js", "server/web/index.html", "-o", output_dir + "/gpgol-js-web.pot"]) + + cpp_files = glob.glob("client/**/*.cpp") + glob.glob("client/**/*.h") + subprocess.run(["xgettext", "-C", "--from-code=UTF-8", "-kde", "-ci18n", "-ki18n:1", "-ki18nc:1c,2", + "-ki18ncp:1c,2,3", "-ki18nd:2", "-ki18ndc:2c,3", "-ki18ndp:2,3", "-ki18ndcp:2c,3,4", + "-kki18n:1", "-kki18nc:1c,2", "-kki18np:1,2", "-kki18ncp:1c,2,3", + "-kki18nd:2", "-kki18ndc:2c,3", "-kki18ndp:2,3", "-kki18ndcp:2c,3,4", + "-kxi18n:1", "-kxi18nc:1c,2", "-kxi18np:1,2", "-kxi18ncp:1c,2,3", + "-kxi18nd:2", "-kxi18ndc:2c,3", "-kxi18ndp:2,3", "-kxi18ndcp:2c,3,4", + "-kkxi18n:1", "-kkxi18nc:1c,2", "-kkxi18np:1,2", "-kkxi18ncp:1c,2,3", + "-kkxi18nd:2", "-kkxi18ndc:2c,3", "-kkxi18ndp:2,3", "-kkxi18ndcp:2c,3,4", + "-kkli18n:1", "-kkli18nc:1c,2", "-kkli18np:1,2", "-kkli18ncp:1c,2,3", + "-kklxi18n:1", "-kklxi18nc:1c,2", "-kklxi18np:1,2", "-kklxi18ncp:1c,2,3", + "-kI18N_NOOP:1", "-kI18NC_NOOP:1c,2", + "-kI18N_NOOP2:1c,2", "-kI18N_NOOP2_NOSTRIP:1c,2", + "-ktr2i18n:1", "-ktr2xi18n:1"] + cpp_files + ["-o", output_dir + "/gpgol-js-native.pot"]) + + tree = etree.parse('server/manifest.xml') + root = tree.getroot() + + xml_version1 = "{http://schemas.microsoft.com/office/mailappversionoverrides}"; + xml_version = "{http://schemas.microsoft.com/office/mailappversionoverrides/1.1}"; + xml_bt = "{http://schemas.microsoft.com/office/officeappbasictypes/1.0}" + + resources = root.find(xml_version1 + "VersionOverrides").find(xml_version + "VersionOverrides").find(xml_version + "Resources") + + short_strings = resources.find(xml_bt + "ShortStrings") + long_strings = resources.find(xml_bt + "LongStrings") + + po = polib.POFile() + po.metadata = { + 'Project-Id-Version': '1.0', + 'Report-Msgid-Bugs-To': 'you@example.com', + 'POT-Creation-Date': '2007-10-18 14:00+0100', + 'PO-Revision-Date': '2007-10-18 14:00+0100', + 'Last-Translator': 'you ', + 'Language-Team': 'English ', + 'MIME-Version': '1.0', + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Transfer-Encoding': '8bit', + } + + for child in short_strings: + entry = polib.POEntry( + msgid=child.get("DefaultValue"), + msgstr=u'', + occurrences=[('server/manigest.xml', child.sourceline)] + ) + po.append(entry) + + for child in long_strings: + entry = polib.POEntry( + msgid=child.get("DefaultValue"), + msgstr=u'', + occurrences=[('server/manigest.xml', child.sourceline)] + ) + po.append(entry) + + po.save(output_dir + '/manifest.pot') + + +def import_strings(input_dir): + print("Importing") + + ## Import string for manifest.py + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse('server/manifest.xml', parser) + root = tree.getroot() + + xml_version1 = "{http://schemas.microsoft.com/office/mailappversionoverrides}"; + xml_version = "{http://schemas.microsoft.com/office/mailappversionoverrides/1.1}"; + xml_bt = "{http://schemas.microsoft.com/office/officeappbasictypes/1.0}" + + resources = root.find(xml_version1 + "VersionOverrides").find(xml_version + "VersionOverrides").find(xml_version + "Resources") + + short_strings = resources.find(xml_bt + "ShortStrings") + long_strings = resources.find(xml_bt + "LongStrings") + + for lang in next(os.walk(input_dir))[1]: + manifest_po_path = input_dir + '/' + lang + '/manifest.po' + + if not os.path.isfile(manifest_po_path): + continue + + po = polib.pofile(manifest_po_path) + + def fill_string(child): + for entry in po.translated_entries(): + if entry.msgid == child.get("DefaultValue"): + lang_xml = lang + '-' + lang.upper() # HACK to use same format as outlook wants + override_found = False + for override in child: + if override.get('Locale') == lang_xml: + override.set("Value", entry.msgstr) + override_found = True + + if not override_found: + override = etree.Element(xml_bt + "Override") + override.set("Locale", lang_xml) + override.set("Value", entry.msgstr) + child.append(override) + + for string in short_strings: + fill_string(string) + + for string in long_strings: + fill_string(string) + + tree.write('server/manifest.xml', pretty_print=True) + + ## Import strings for web vue + + lang_obj = {} + + for lang in next(os.walk(input_dir))[1]: + web_po_path = input_dir + '/' + lang + '/gpgol-js-web.po' + + if not os.path.isfile(web_po_path): + continue + + po = polib.pofile(web_po_path) + obj = {} + + for entry in po.translated_entries(): + obj[entry.msgid] = entry.msgstr + + lang_obj[lang + '-' + lang.upper()] = obj + + f = open("server/web/assets/translation.js", "w") + f.write("const messages = " + json.dumps(lang_obj)) + f.close() + + +def main(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="subcommands", + description='valid subcommands', + dest='command') + + extract_parser = subparsers.add_parser('extract', help="Extract strings from source code") + extract_parser.add_argument("output_dir", default="pot_dir") + + import_parser = subparsers.add_parser('import', help="Import strings to source code") + import_parser.add_argument("input_dir", default="po") + + args = parser.parse_args() + + if args.command == 'extract': + try: + os.mkdir(args.output_dir) + except FileExistsError: + pass + extract_strings(args.output_dir) + elif args.command == 'import': + import_strings(args.input_dir) + +if __name__ == "__main__": + main() diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0fda7e6..09283e8 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,42 +1,44 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_executable(gpgol-server) target_sources(gpgol-server PRIVATE # Controllers controllers/abstractcontroller.cpp controllers/abstractcontroller.h controllers/registrationcontroller.cpp controllers/registrationcontroller.h controllers/staticcontroller.h controllers/staticcontroller.cpp controllers/emailcontroller.cpp controllers/emailcontroller.h # State model/serverstate.cpp model/serverstate.h # web sever webserver.cpp webserver.h main.cpp ) qt_add_resources(gpgol-server PREFIX "/" FILES - assets/document-decrypt-16.png - assets/document-decrypt-32.png - assets/document-decrypt-64.png - assets/document-decrypt-80.png - - assets/script.js + web/assets/document-decrypt-16.png + web/assets/document-decrypt-32.png + web/assets/document-decrypt-64.png + web/assets/document-decrypt-80.png + web/assets/script.js + web/assets/translation.js + web/assets/vue.global.v3.4.21.js + web/assets/main.css web/index.html ) target_link_libraries(gpgol-server PRIVATE Qt6::HttpServer Qt6::Core common) diff --git a/server/assets/script.js b/server/assets/script.js deleted file mode 100644 index 082a1f1..0000000 --- a/server/assets/script.js +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-FileCopyrightText: 2023 g10 code GmbH -// SPDX-Contributor: Carl Schwan -// SPDX-License-Identifier: GPL-2.0-or-later - -function downloadViaRest(callback) { - const request = - '' + - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' IdOnly' + - ' true' + - ' ' + - ' ' + - ' ' + - ' ' + - ''; - - Office.context.mailbox.makeEwsRequestAsync(request, (asyncResult) => { - const parser = new DOMParser(); - xmlDoc = parser.parseFromString(asyncResult.value, "text/xml"); - - const mimeContent = xmlDoc.getElementsByTagName('t:MimeContent')[0].innerHTML; - callback(atob(mimeContent)); - }); -} - -async function view(content) { - const response = await fetch('https://localhost:5656/view', { - method: 'POST', - body: content, - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -async function info(content) { - const response = await fetch('https://localhost:5656/info', { - method: 'POST', - body: content, - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -async function newEmail(content) { - const response = await fetch('https://localhost:5656/new', { - method: 'POST', - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -async function reply(content) { - const response = await fetch('https://localhost:5656/reply', { - method: 'POST', - body: content, - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -async function forward(content) { - const response = await fetch('https://localhost:5656/forward', { - method: 'POST', - body: content, - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -async function openDraft(id) { - const response = await fetch(`https://localhost:5656/draft/${id}`, { - method: 'POST', - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -async function deleteDraft(id) { - const response = await fetch(`https://localhost:5656/draft/${id}`, { - method: 'DELETE', - headers: { - 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, - 'X-NAME': Office.context.mailbox.userProfile.displayName, - }, - }); - const json = await response.json(); - return json; -} - -function showError(errorMessage) { - const errorElement = document.getElementById('error'); - errorElement.innerHTML = errorMessage; - errorElement.classList.remove('d-none'); -} - -function hideError() { - const errorElement = document.getElementById('error'); - errorElement.classList.add('d-none'); -} - -Office.onReady(). - then(()=> { - downloadViaRest(async (content) => { - const status = await info(content) - const statusText = document.getElementById('status-text'); - if (status.encrypted || status.signed) { - const decryptButton = document.getElementById('decrypt-button'); - decryptButton.classList.remove('d-none'); - if (status.encrypted) { - decryptButton.innerText = "Decrypt"; - statusText.innerText = status.signed ? "This mail is encrypted and signed." : "This mail is encrypted."; - } else if (status.signed) { - decryptButton.innerText = "Show signature"; - statusText.innerText = "This mail is signed"; - } - decryptButton.addEventListener('click', (event) => { - view(content); - }); - } else { - statusText.innerText = "This mail is not encrypted nor signed."; - } - - document.getElementById('reply-button').addEventListener('click', (event) => { - reply(content); - }); - - document.getElementById('forward-button').addEventListener('click', (event) => { - forward(content); - }); - - document.getElementById('new-button').addEventListener('click', (event) => { - newEmail(); - }); - - if (status.drafts.length === 0) { - document.getElementById('no-draft').classList.remove('d-none'); - } else { - const draftsContainer = document.getElementById('drafts'); - status.drafts.forEach(draft => { - const draftElementContainer = document.createElement('li'); - const draftElement = document.createElement('button'); - draftElement.classList.add('btn', 'w-100', 'd-flex', 'flex-row', 'align-items-center'); - draftElement.addEventListener('click', (event) => { - openDraft(draft.id); - }); - const date = new Date(draft.last_modification * 1000); - let todaysDate = new Date(); - let lastModification = ''; - if ((new Date(date)).setHours(0, 0, 0, 0) == todaysDate.setHours(0, 0, 0, 0)) { - lastModification = date.toLocaleTimeString([], { - hour: 'numeric', - minute: 'numeric', - }); - } else { - lastModification = date.toLocaleDateString(); - } - - const content = document.createTextNode('Last Modified: ' + lastModification); - draftElement.appendChild(content); - - const deleteDraftButton = document.createElement('button'); - deleteDraftButton.classList.add('btn', 'btn-danger', 'ms-auto', 'py-1'); - deleteDraftButton.addEventListener('click', (event) => { - deleteDraft(draft.id); - draftElement.remove(); - }); - const deleteDraftButtonContent = document.createTextNode('X'); - deleteDraftButton.appendChild(deleteDraftButtonContent); - draftElement.appendChild(deleteDraftButton); - - draftElementContainer.appendChild(draftElement); - draftsContainer.appendChild(draftElementContainer); - }); - } - - function webSocketConnect() { - // Create WebSocket connection. - const socket = new WebSocket("wss://localhost:5657"); - - // Connection opened - socket.addEventListener("open", (event) => { - hideError(); - socket.send(JSON.stringify({ - command: "register", - arguments: { - emails: [Office.context.mailbox.userProfile.emailAddress], - type: 'webclient', - }, - })); - }); - - socket.addEventListener("close", (event) => { - showError('Native client was disconnected'); - setTimeout(function() { - webSocketConnect(); - }, 1000); - }); - - socket.addEventListener("error", (event) => { - showError('Native client received an error'); - setTimeout(function() { - webSocketConnect(); - }, 1000); - }); - - // Listen for messages - socket.addEventListener("message", ({ data }) => { - const message = JSON.parse(data); - console.log("Message from server ", message); - switch (message.type) { - case 'ews': - Office.context.mailbox.makeEwsRequestAsync(message.payload, (asyncResult) => { - console.log('Email sent') - // let the client known that the email was sent - socket.send(JSON.stringify({ - command: 'email-sent', - arguments: { - id: message.id, - email: Office.context.mailbox.userProfile.emailAddress, - } - })); - }); - break; - case 'disconnection': - showError('Native client was disconnected'); - break; - case 'connection': - hideError(); - break; - } - }); - } - - webSocketConnect(); - }); - }); diff --git a/server/controllers/staticcontroller.cpp b/server/controllers/staticcontroller.cpp index fb513ab..8f1cfb3 100644 --- a/server/controllers/staticcontroller.cpp +++ b/server/controllers/staticcontroller.cpp @@ -1,31 +1,38 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "staticcontroller.h" #include #include using namespace Qt::Literals::StringLiterals; QHttpServerResponse StaticController::homeAction(const QHttpServerRequest &request) { QFile file(u":/web/index.html"_s); if (!file.open(QIODeviceBase::ReadOnly)) { qWarning() << file.errorString(); } return QHttpServerResponse("text/html", file.readAll()); } QHttpServerResponse StaticController::assetsAction(QString fileName, const QHttpServerRequest &request) { - QFile file(u":/assets/"_s + fileName); + QFile file(u":/web/assets/"_s + fileName); if (!file.open(QIODeviceBase::ReadOnly)) { qWarning() << file.errorString(); return badRequest(); } - return QHttpServerResponse(fileName.endsWith(u".png"_s) ? "image/png" : "text/javascript", file.readAll()); + if (fileName.endsWith(u".png"_s)) { + return QHttpServerResponse("image/png", file.readAll()); + } else if (fileName.endsWith(u".js"_s)) { + return QHttpServerResponse("text/javascript", file.readAll()); + } else if (fileName.endsWith(u".css"_s)) { + return QHttpServerResponse("text/css", file.readAll()); + } + return QHttpServerResponse("text/plain", file.readAll()); } diff --git a/server/manifest.xml b/server/manifest.xml index e83b503..09e76ee 100644 --- a/server/manifest.xml +++ b/server/manifest.xml @@ -1,112 +1,117 @@ - 95b7e9a9-1ce6-49c2-adf8-48aa704f156d 1.0.0.0 g10code de-DE https://www.gnupg.org
250
ReadWriteMailbox false - - - - - - - - - - - - - - - - - - - - -