Page MenuHome GnuPG

No OneTemporary

diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
index 8075659..7e487f1 100644
--- a/client/CMakeLists.txt
+++ b/client/CMakeLists.txt
@@ -1,391 +1,386 @@
# SPDX-FileCopyrightText: 2023 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_definitions(-DDATAROUTDIR="${KDE_INSTALL_FULL_DATAROOTDIR}")
add_definitions(-DTRANSLATION_DOMAIN=\"gpgol-js-native\")
add_subdirectory(rootcagenerator)
add_library(gpgol-client-static STATIC)
target_sources(gpgol-client-static PRIVATE
emailviewer.cpp
emailviewer.h
websocketclient.cpp
websocketclient.h
messagedispatcher.cpp
messagedispatcher.h
- firsttimedialog.cpp
- firsttimedialog.h
mailapi.cpp
securitylevelinfo.cpp
+ setupdialogs.cpp
+ statusdialog.cpp
+ connectioncontroller.cpp
# Identity
identity/addressvalidationjob.cpp
identity/addressvalidationjob.h
identity/identitymanager.cpp
identity/identitymanager.h
identity/identitydialog.cpp
identity/identitydialog.h
identity/identity.cpp
identity/identity.h
identity/signature.h
identity/signature.cpp
identity/signatureconfigurator.cpp
identity/signatureconfigurator.h
identity/signaturerichtexteditor.cpp
identity/signaturerichtexteditor_p.h
# Draft
draft/draft.cpp
draft/draft.h
draft/draftmanager.cpp
draft/draftmanager.h
# EWS integration
ews/ewsattachment.cpp
ews/ewsattachment.h
ews/ewsattendee.cpp
ews/ewsattendee.h
ews/ewsclient_debug.cpp
ews/ewsclient_debug.h
ews/ewsid.cpp
ews/ewsid.h
ews/ewsitem.cpp
ews/ewsitem.h
ews/ewsitemshape.cpp
ews/ewsitemshape.h
ews/ewsitembase.cpp
ews/ewsitembase.h
ews/ewsitembase_p.h
ews/ewsmailbox.cpp
ews/ewsmailbox.h
ews/ewsmessagedispatcher.cpp
ews/ewsmessagedispatcher.h
ews/ewsoccurrence.cpp
ews/ewsoccurrence.h
ews/ewspropertyfield.cpp
ews/ewspropertyfield.h
ews/ewsrecurrence.cpp
ews/ewsrecurrence.h
ews/ewsserverversion.cpp
ews/ewsserverversion.h
ews/ewstypes.cpp
ews/ewstypes.h
ews/ewsxml.cpp
ews/ewsxml.h
ews/ewscreateitemrequest.cpp
ews/ewscreateitemrequest.h
ews/ewsgetfolderrequest.cpp
ews/ewsgetfolderrequest.h
ews/ewsupdateitemrequest.cpp
ews/ewsupdateitemrequest.h
ews/ewsrequest.cpp
ews/ewsrequest.h
ews/ewsjob.cpp
ews/ewsjob.h
ews/ewsclient.cpp
ews/ewsclient.h
ews/ewsfinditemrequest.cpp
ews/ewsfinditemrequest.h
ews/ewsgetitemrequest.cpp
ews/ewsgetitemrequest.h
ews/ewsgetfolderrequest.cpp
ews/ewsgetfolderrequest.h
ews/ewsfolder.cpp
ews/ewsfolder.h
ews/ewscreatefolderrequest.cpp
ews/ewscreatefolderrequest.h
ews/ewseffectiverights.h
ews/ewseffectiverights.cpp
ews/ewsfoldershape.cpp
ews/ewsfoldershape.h
ews/ewscopyitemrequest.cpp
ews/ewscopyitemrequest.h
ews/ewsgetfoldercontentrequest.cpp
ews/ewsgetfoldercontentrequest.h
reencrypt/reencryptjob.cpp
reencrypt/reencryptjob.h
reencrypt/choosekeydialog.cpp
reencrypt/choosekeydialog.h
reencrypt/certificatelineedit.cpp
reencrypt/certificatelineedit.h
reencrypt/reencryptprogressdialog.cpp
# Editor
editor/addresseelineedit.cpp
editor/addresseelineedit.h
editor/addresseelineeditmanager.cpp
editor/addresseelineeditmanager.h
editor/bodytexteditor.cpp
editor/bodytexteditor.h
editor/composerviewbase.cpp
editor/composerviewbase.h
editor/composerwindow.cpp
editor/composerwindow.h
editor/composerwindowfactory.cpp
editor/composerwindowfactory.h
editor/cryptostateindicatorwidget.cpp
editor/cryptostateindicatorwidget.h
editor/kmcomposerglobalaction.cpp
editor/kmcomposerglobalaction.h
editor/nearexpirywarning.cpp
editor/nearexpirywarning.h
editor/mailtemplates.cpp
editor/mailtemplates.h
editor/recipient.cpp
editor/recipient.h
editor/recipientline.cpp
editor/recipientline.h
editor/recipientseditor.cpp
editor/recipientseditor.h
editor/util.h
editor/util.cpp
editor/kmailcompletion.cpp
editor/kmailcompletion.h
editor/composersignatures.cpp
editor/composersignatures.h
editor/nodehelper.cpp
editor/nodehelper.h
editor/signaturecontroller.cpp
editor/signaturecontroller.h
editor/spellcheckerconfigdialog.cpp
editor/spellcheckerconfigdialog.h
# Editor job
editor/job/abstractencryptjob.h
editor/job/autocryptheadersjob.h
editor/job/contentjobbase.h
editor/job/contentjobbase_p.h
editor/job/composerjob.cpp
editor/job/composerjob.h
editor/job/encryptjob.h
editor/job/inserttextfilejob.h
editor/job/itipjob.h
editor/job/jobbase.h
editor/job/jobbase_p.h
editor/job/maintextjob.h
editor/job/multipartjob.h
editor/job/protectedheadersjob.h
editor/job/signencryptjob.h
editor/job/signjob.h
editor/job/singlepartjob.h
editor/job/skeletonmessagejob.h
editor/job/transparentjob.h
editor/job/autocryptheadersjob.cpp
editor/job/contentjobbase.cpp
editor/job/encryptjob.cpp
editor/job/inserttextfilejob.cpp
editor/job/itipjob.cpp
editor/job/jobbase.cpp
editor/job/maintextjob.cpp
editor/job/multipartjob.cpp
editor/job/protectedheadersjob.cpp
editor/job/saveasfilejob.cpp
editor/job/saveasfilejob.h
editor/job/signencryptjob.cpp
editor/job/signjob.cpp
editor/job/singlepartjob.cpp
editor/job/skeletonmessagejob.cpp
editor/job/transparentjob.cpp
## Editor Part
editor/part/globalpart.h
editor/part/infopart.h
editor/part/itippart.h
editor/part/messagepart.h
editor/part/textpart.h
editor/part/globalpart.cpp
editor/part/infopart.cpp
editor/part/itippart.cpp
editor/part/messagepart.cpp
editor/part/textpart.cpp
## Attachment
editor/attachment/attachmentjob.cpp
editor/attachment/attachmentjob.h
editor/attachment/attachmentclipboardjob.cpp
editor/attachment/attachmentclipboardjob.h
editor/attachment/attachmentcompressjob.cpp
editor/attachment/attachmentcompressjob.h
editor/attachment/attachmentcontroller.cpp
editor/attachment/attachmentcontroller.h
editor/attachment/attachmentcontrollerbase.cpp
editor/attachment/attachmentcontrollerbase.h
editor/attachment/attachmentfromfolderjob.cpp
editor/attachment/attachmentfromfolderjob.h
editor/attachment/attachmentfrommimecontentjob.cpp
editor/attachment/attachmentfrommimecontentjob.h
editor/attachment/attachmentfromurlbasejob.cpp
editor/attachment/attachmentfromurlbasejob.h
editor/attachment/attachmentfromurljob.cpp
editor/attachment/attachmentfromurljob.h
editor/attachment/attachmentfromurlutils.cpp
editor/attachment/attachmentfromurlutils.h
editor/attachment/attachmentfrompublickeyjob.cpp
editor/attachment/attachmentfrompublickeyjob.h
editor/attachment/attachmentloadjob.cpp
editor/attachment/attachmentloadjob.h
editor/attachment/attachmentmodel.cpp
editor/attachment/attachmentmodel.h
editor/attachment/attachmentpart.cpp
editor/attachment/attachmentpart.h
editor/attachment/attachmentpropertiesdialog.cpp
editor/attachment/attachmentpropertiesdialog.h
editor/attachment/attachmentupdatejob.cpp
editor/attachment/attachmentupdatejob.h
editor/attachment/attachmentview.cpp
editor/attachment/attachmentview.h
# Multiplyingline
multiplyingline/multiplyingline.cpp
multiplyingline/multiplyingline.h
multiplyingline/multiplyinglineeditor.cpp
multiplyingline/multiplyinglineeditor.h
multiplyingline/multiplyinglineview_p.cpp
multiplyingline/multiplyinglineview_p.h
# Utils
utils/kuniqueservice.h
utils/kuniqueservice.cpp
utils/systemtrayicon.h
utils/systemtrayicon.cpp
)
if (WIN32)
add_definitions(-DHAVE_QDBUS=false)
target_sources(gpgol-client-static PRIVATE utils/kuniqueservice_win.cpp)
else()
add_definitions(-DHAVE_QDBUS=true)
target_sources(gpgol-client-static PRIVATE utils/kuniqueservice_dbus.cpp)
endif()
ki18n_wrap_ui(gpgol-client-static
editor/attachment/ui/attachmentpropertiesdialog.ui
editor/attachment/ui/attachmentpropertiesdialog_readonly.ui
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER gpgol_client_debug.h
IDENTIFIER GPGOL_CLIENT_LOG
CATEGORY_NAME org.gpgol.client
DESCRIPTION "General client log"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER websocket_debug.h
IDENTIFIER WEBSOCKET_LOG
CATEGORY_NAME org.gpgol.client.websocket
DESCRIPTION "Websocket connection in the client"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER ewsresource_debug.h
IDENTIFIER EWSRES_LOG
CATEGORY_NAME org.gpgol.ews
DESCRIPTION "Ews mail client"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER ewscli_debug.h
IDENTIFIER EWSCLI_LOG
CATEGORY_NAME org.gpgol.ews.client
DESCRIPTION "ews client (gpgol-client)"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER editor_debug.h
IDENTIFIER EDITOR_LOG
CATEGORY_NAME org.gpgol.editor
DESCRIPTION "mail composer"
EXPORT GPGOL
)
set(WARN_TOOMANY_RECIPIENTS_DEFAULT true)
set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true)
configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg)
kconfig_add_kcfg_files(gpgol-client-static editor/settings/messagecomposersettings.kcfgc config.kcfgc)
install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-client)
target_sources(gpgol-client-static PUBLIC ${gpgol-client-static_SRCS})
-ki18n_wrap_ui(gpgol-client-static firsttimedialog.ui)
-ki18n_wrap_ui(gpgol-client-static confpagewelcome.ui)
-ki18n_wrap_ui(gpgol-client-static confpageinstalladdin.ui)
-ki18n_wrap_ui(gpgol-client-static confpageproxyoptions.ui)
-ki18n_wrap_ui(gpgol-client-static confpagetlscertificate.ui)
-
ki18n_wrap_ui(gpgol-client-static reencrypt/choosekeydialog.ui)
target_link_libraries(gpgol-client-static PUBLIC
common
rootcagenerator
LibGpgError::LibGpgError
Qt6::HttpServer
Qt6::Widgets
Qt6::PrintSupport
Qt6::WebSockets
KF6::I18n
KF6::JobWidgets
KF6::CalendarCore
KF6::ConfigCore
KF6::ConfigGui
KF6::Contacts
KF6::Completion
KF6::CoreAddons
KF6::ColorScheme
KF6::Codecs
KF6::GuiAddons
KF6::SonnetUi
KF6::SonnetCore
KF6::WidgetsAddons
KF6::XmlGui
KF6::Archive
KF6::KIOWidgets
KPim6::MimeTreeParserCore
KPim6::MimeTreeParserWidgets
KPim6::Libkleo
${_gpgol_dbusaddons_libs}
)
if(Gpgmepp_VERSION VERSION_GREATER_EQUAL "2.0.0")
target_compile_definitions(gpgol-client-static PUBLIC GPGME2)
endif()
set(GPGOLWEB_ICON_DIR "${CMAKE_CURRENT_SOURCE_DIR}/icons")
file(GLOB ICONS_PNGS "${GPGOLWEB_ICON_DIR}/*gpgolweb.png")
file(GLOB ICONS_SVGS "${GPGOLWEB_ICON_DIR}/*gpgolweb.svg")
ecm_add_app_icon(_gpgol-client_SRCS ICONS ${ICONS_PNGS} ${ICONS_SVGS})
ecm_install_icons(ICONS ${ICONS_PNGS} ${ICONS_SVGS} DESTINATION ${KDE_INSTALL_ICONDIR})
if (ICONS_SVGS)
list(GET ICONS_SVGS 0 app_icon_svg)
configure_file(icons/icons.qrc.in icons.qrc @ONLY)
set(_gpgol-client_SRCS ${_gpgol-client_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/icons.qrc)
endif()
add_executable(gpgol-client main.cpp ${_gpgol-client_SRCS})
qt_add_resources(gpgol-client "manifest"
PREFIX "/gpgol-client"
FILES manifest.xml.in
)
target_link_libraries(gpgol-client PRIVATE gpgol-client-static)
if (BUILD_TESTING)
add_subdirectory(autotests)
endif()
install(TARGETS gpgol-client ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES com.gnupg.gpgolweb.desktop DESTINATION ${KDE_INSTALL_APPDIR})
diff --git a/client/confpageinstalladdin.ui b/client/confpageinstalladdin.ui
deleted file mode 100644
index 24a6b61..0000000
--- a/client/confpageinstalladdin.ui
+++ /dev/null
@@ -1,228 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>ConfPageInstallAddin</class>
- <widget class="QWidget" name="ConfPageInstallAddin">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>615</width>
- <height>528</height>
- </rect>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="windowTitle">
- <string>Form</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>&lt;h3&gt;Activating the Add-In&lt;/h3&gt;
-&lt;p&gt;Before the first use, the add-in has to be activated in Outlook:&lt;/p&gt;&lt;p&gt;
-&lt;ul&gt;&lt;li&gt;1. Go to the &lt;a href=&quot;https://outlook.office.com/mail/jsmvvmdeeplink/?path=/options/manageapps&amp;amp;bO=4&quot;&gt;Outlook Extension Manager&lt;/a&gt; (you may be prompted to log in).&lt;/li&gt;
-&lt;li&gt;2. Copy the file-name below, register it via&lt;br&gt;&lt;tt&gt;My Add-Ins -&amp;gt; Custom Add-Ins -&amp;gt; Add a custom Add-In&lt;/tt&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;/p&gt;</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- <property name="openExternalLinks">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout">
- <property name="sizeConstraint">
- <enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
- </property>
- <item>
- <widget class="QLineEdit" name="manifestPath">
- <property name="readOnly">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="manifestPathOpenFolder">
- <property name="toolTip">
- <string>Open Folder</string>
- </property>
- <property name="icon">
- <iconset theme="document-open-folder-symbolic"/>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="manifestPathCopy">
- <property name="toolTip">
- <string>Copy</string>
- </property>
- <property name="icon">
- <iconset theme="edit-copy-symbolic"/>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>&lt;p&gt;&lt;ul&gt;
-&lt;li&gt;3. In your mail account, click on any e-mail and then activate the add-in by clicking on the GnuPG icon shown about the email header. (Icon not visible? See troubleshooting below)&lt;/li&gt;
-&lt;li&gt;4. You will be prompted for a pairing code. Click the button below to obtain the code, and paste it to the input field.&lt;/li&gt;&lt;/ul&gt;&lt;/p&gt;
-</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout">
- <item>
- <widget class="QPushButton" name="pairWebClientButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Show pairing code</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <spacer name="verticalSpacer_3">
- <property name="orientation">
- <enum>Qt::Orientation::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>60</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>&lt;h3&gt;Troubleshooting&lt;/h3&gt;
-If extension isn't connected, please verify your browser can communicate with the GpgOL/Web service by opening this test page:</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item alignment="Qt::AlignmentFlag::AlignHCenter">
- <widget class="QPushButton" name="testPageButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Open test page</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer_2">
- <property name="orientation">
- <enum>Qt::Orientation::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item alignment="Qt::AlignmentFlag::AlignLeft">
- <widget class="QCheckBox" name="showOnStartup">
- <property name="text">
- <string>Show this configuration window on startup</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0">
- <item>
- <widget class="QPushButton" name="backButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Back</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Orientation::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="minimizeButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Minimize to tray</string>
- </property>
- <property name="default">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>manifestPath</tabstop>
- <tabstop>manifestPathOpenFolder</tabstop>
- <tabstop>manifestPathCopy</tabstop>
- <tabstop>testPageButton</tabstop>
- <tabstop>showOnStartup</tabstop>
- <tabstop>minimizeButton</tabstop>
- <tabstop>backButton</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/client/confpageproxyoptions.ui b/client/confpageproxyoptions.ui
deleted file mode 100644
index 671b2d9..0000000
--- a/client/confpageproxyoptions.ui
+++ /dev/null
@@ -1,208 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>ConfPageProxyOptions</class>
- <widget class="QWidget" name="ConfPageProxyOptions">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>481</width>
- <height>300</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Form</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QLabel" name="label">
- <property name="toolTip">
- <string/>
- </property>
- <property name="text">
- <string>Choose your configuration for the proxy component:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_3">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="font">
- <font>
- <italic>true</italic>
- </font>
- </property>
- <property name="text">
- <string>Note: Changes to this setting only take effect after uploading the adjusted manifest file to Outlook on the next page!</string>
- </property>
- <property name="scaledContents">
- <bool>false</bool>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QRadioButton" name="localOption">
- <property name="text">
- <string>Run a local proxy on this machine</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QRadioButton" name="remoteOption">
- <property name="text">
- <string>Use proxy from a remote server (EXPERIMENTAL)</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="remoteServerLayout">
- <item>
- <widget class="QLabel" name="remoteLabel">
- <property name="text">
- <string>Remote proxy server</string>
- </property>
- <property name="margin">
- <number>0</number>
- </property>
- <property name="indent">
- <number>-1</number>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="remoteServer">
- <property name="text">
- <string/>
- </property>
- <property name="placeholderText">
- <string>internal.company.com</string>
- </property>
- <property name="clearButtonEnabled">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Orientation::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>Optional features:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="reencryptOption">
- <property name="text">
- <string>Reencrypt email folders with new keys</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="spacer">
- <property name="orientation">
- <enum>Qt::Orientation::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QPushButton" name="backButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Back</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Orientation::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="continueButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <bold>false</bold>
- <hintingpreference>PreferDefaultHinting</hintingpreference>
- </font>
- </property>
- <property name="autoFillBackground">
- <bool>false</bool>
- </property>
- <property name="text">
- <string>Continue</string>
- </property>
- <property name="autoDefault">
- <bool>false</bool>
- </property>
- <property name="default">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>localOption</tabstop>
- <tabstop>remoteOption</tabstop>
- <tabstop>remoteServer</tabstop>
- <tabstop>reencryptOption</tabstop>
- <tabstop>continueButton</tabstop>
- <tabstop>backButton</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/client/confpagetlscertificate.ui b/client/confpagetlscertificate.ui
deleted file mode 100644
index 19be31d..0000000
--- a/client/confpagetlscertificate.ui
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>ConfPageTLSCertificate</class>
- <widget class="QWidget" name="ConfPageTLSCertificate">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>400</width>
- <height>300</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Form</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QPlainTextEdit" name="plainTextEdit"/>
- </item>
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>TextLabel</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QPushButton" name="backButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Back</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Orientation::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="installButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Install certificate</string>
- </property>
- <property name="default">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="continueButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Continue</string>
- </property>
- <property name="default">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>plainTextEdit</tabstop>
- <tabstop>continueButton</tabstop>
- <tabstop>installButton</tabstop>
- <tabstop>backButton</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/client/confpagewelcome.ui b/client/confpagewelcome.ui
deleted file mode 100644
index 98e50c0..0000000
--- a/client/confpagewelcome.ui
+++ /dev/null
@@ -1,120 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>ConfPageWelcome</class>
- <widget class="QWidget" name="ConfPageWelcome">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>400</width>
- <height>300</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Form</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Orientation::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="logo">
- <property name="alignment">
- <set>Qt::AlignmentFlag::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item alignment="Qt::AlignmentFlag::AlignHCenter">
- <widget class="KTitleWidget" name="titleWelcome">
- <property name="text">
- <string>GpgOL/Web</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>The GnuPG Add-in for Outlook</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignmentFlag::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer2">
- <property name="orientation">
- <enum>Qt::Orientation::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QCheckBox" name="autostartBox">
- <property name="minimumSize">
- <size>
- <width>388</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string>Autostart</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="configureButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Configure</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <customwidgets>
- <customwidget>
- <class>KTitleWidget</class>
- <extends>QWidget</extends>
- <header>ktitlewidget.h</header>
- </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
diff --git a/client/connectioncontroller.cpp b/client/connectioncontroller.cpp
new file mode 100644
index 0000000..91deeb1
--- /dev/null
+++ b/client/connectioncontroller.cpp
@@ -0,0 +1,83 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "connectioncontroller.h"
+
+#include "config.h"
+#include "websocketclient.h"
+
+#include <QUuid>
+#include <QUrl>
+#include <QTimer>
+
+using namespace Qt::StringLiterals;
+
+ConnectionController* ConnectionController::m_instance = nullptr;
+ConnectionController::ConnectionController()
+ : QObject()
+ , m_state(LocalServerStarting)
+{
+ Q_ASSERT(m_instance == nullptr);
+ m_instance = this;
+
+ connect(&m_serverProcess, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
+ if (state == QProcess::Running) {
+ m_state = LocalServerRunning;
+ startWebsocketClient();
+ } else if (state == QProcess::NotRunning) {
+ m_state = LocalServerFailed;
+ QTimer::singleShot(1000, this, [this]() {
+ startStopLocalServer();
+ });
+ } else if (m_state != LocalServerFailed) {
+ m_state = LocalServerStarting;
+ }
+ Q_EMIT serverProcessStatusChanged();
+ });
+ connect(&m_serverProcess, &QProcess::readyReadStandardError, this, [this]() {
+ qWarning().noquote() << m_serverProcess.readAllStandardError();
+ });
+
+ connect(&m_serverProcess, &QProcess::readyReadStandardOutput, this, [this]() {
+ qWarning().noquote() << m_serverProcess.readAllStandardOutput();
+ });
+}
+
+ConnectionController* ConnectionController::instance()
+{
+ return m_instance;
+}
+
+ConnectionController::ServerProcessState ConnectionController::serverProcessState() const
+{
+ if (!Config::self()->isLocalServer()) {
+ return RemoteServer;
+ }
+ return m_state;
+}
+
+void ConnectionController::startStopLocalServer()
+{
+ if (Config::self()->isLocalServer()) {
+ if (m_serverProcess.state() == QProcess::NotRunning) {
+ WebsocketClient::self().disconnectFromProxy();
+ m_clientSecret = QUuid::createUuid().toString(QUuid::WithoutBraces);
+ const auto secretHash = QString::fromLatin1(QCryptographicHash::hash(m_clientSecret.toUtf8().data(), QCryptographicHash::Sha256).toHex());
+ m_serverProcess.start(u"gpgol-server"_s, QStringList{u"--singleclient="_s + secretHash});
+ }
+ } else if (m_serverProcess.state() != QProcess::NotRunning) {
+ m_serverProcess.kill();
+ }
+ // NOTE: state change gets signaled via the stateChanged lambda
+}
+
+void ConnectionController::startWebsocketClient()
+{
+ WebsocketClient::self().connectToProxy(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s), m_clientSecret);
+}
+
+QString ConnectionController::serverDomain()
+{
+ return (Config::self()->isLocalServer() ? u"localhost:5656"_s : Config::self()->remoteAddress().toString());
+}
diff --git a/client/connectioncontroller.h b/client/connectioncontroller.h
new file mode 100644
index 0000000..1c61dc0
--- /dev/null
+++ b/client/connectioncontroller.h
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QProcess>
+
+class ConnectionController : public QObject
+{
+ Q_OBJECT
+public:
+ explicit ConnectionController();
+
+ static QString serverDomain();
+ static ConnectionController* instance();
+
+ void startStopLocalServer();
+ void startWebsocketClient();
+
+ enum ServerProcessState {
+ RemoteServer,
+ LocalServerStarting,
+ LocalServerRunning,
+ LocalServerFailed
+ };
+ ServerProcessState serverProcessState() const;
+Q_SIGNALS:
+ void serverProcessStatusChanged();
+private:
+ QProcess m_serverProcess;
+ static ConnectionController* m_instance;
+ ServerProcessState m_state;
+ QString m_clientSecret;
+};
diff --git a/client/editor/composerwindowfactory.cpp b/client/editor/composerwindowfactory.cpp
index 459cd1c..fdb59eb 100644
--- a/client/editor/composerwindowfactory.cpp
+++ b/client/editor/composerwindowfactory.cpp
@@ -1,65 +1,65 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "composerwindowfactory.h"
#include "draft/draftmanager.h"
#include "editor/composerwindow.h"
-#include "../firsttimedialog.h" // for strongActivateWindow
+#include "setupdialogs.h"
ComposerWindowFactory::ComposerWindowFactory() = default;
ComposerWindowFactory &ComposerWindowFactory::self()
{
static ComposerWindowFactory instance;
return instance;
}
ComposerWindow *ComposerWindowFactory::create(const QString &fromAddress, const QString &name)
{
if (m_inactiveWindow) {
auto window = m_inactiveWindow;
m_inactiveWindow = nullptr;
window->reset(fromAddress, name);
return window;
}
auto window = new ComposerWindow(fromAddress, name);
QObject::connect(window, &ComposerWindow::initialized, window, [window, this]() {
const auto key = m_activeWindows.key(window);
if (!key.isEmpty()) {
m_activeWindows.remove(key);
}
m_activeWindows[window->mailId()] = window;
});
return window;
}
void ComposerWindowFactory::restoreAutosave(const QString &email, const QString &displayName)
{
const auto &autosaves = DraftManager::self().autosaves();
for (const auto &autosave : autosaves) {
if (!m_activeWindows.contains(autosave.id())) {
auto dialog = create(email, displayName);
dialog->setMessage(autosave.mime());
dialog->setMailId(autosave.id());
- FirstTimeDialog::strongActivateWindow(dialog);
+ DialogController::strongActivateWindow(dialog);
}
}
}
void ComposerWindowFactory::clear(ComposerWindow *composerWindow)
{
auto close = composerWindow->queryClose();
if (!close) {
return;
}
if (m_inactiveWindow) {
composerWindow->deleteLater();
return;
}
m_inactiveWindow = composerWindow;
m_inactiveWindow->hide();
}
diff --git a/client/emailviewer.cpp b/client/emailviewer.cpp
index 5a68f53..74b71c1 100644
--- a/client/emailviewer.cpp
+++ b/client/emailviewer.cpp
@@ -1,153 +1,153 @@
// SPDX-FileCopyrightText: 2024 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "emailviewer.h"
#include "editor/composerwindow.h"
#include "editor/composerwindowfactory.h"
-#include "firsttimedialog.h" // for strongActivateWindow
#include "securitylevelinfo.h"
+#include "setupdialogs.h"
#include "websocketclient.h"
#include <Libkleo/Compliance>
#include <KColorScheme>
#include <KLocalizedString>
#include <KMessageDialog>
#include <mimetreeparser_widgets_version.h>
#include <QApplication>
#include <QLabel>
#include <QMenuBar>
#include <QStatusBar>
#include <QToolBar>
#include <QToolButton>
#include <QToolTip>
#include <QVBoxLayout>
using namespace Qt::Literals::StringLiterals;
static KMime::Message::Ptr toMessage(const QString &content)
{
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(content.toUtf8()));
message->parse();
return message;
}
EmailViewer::EmailViewer(const QString &content, const QString &accountEmail, const QString &displayName)
: MimeTreeParser::Widgets::MessageViewerWindow()
, m_email(accountEmail)
, m_displayName(displayName)
, m_secLevelButton(nullptr)
{
setMessages({toMessage(content)});
auto message = messages().at(0);
toolBar()->show();
// spacer
QWidget *spacer = new QWidget();
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
toolBar()->addWidget(spacer);
auto messageMenu = menuBar()->findChild<QMenu *>("messageMenu");
Q_ASSERT(messageMenu);
messageMenu->addSeparator();
m_secLevelButtonContainer = new QWidget();
auto l = new QVBoxLayout(m_secLevelButtonContainer);
l->setContentsMargins(0, 0, 0, 0);
toolBar()->addWidget(m_secLevelButtonContainer);
updateSecLevelButton();
// reply
auto replyAction = new QAction(QIcon::fromTheme(u"mail-reply-sender-symbolic"_s), i18nc("@action:button", "Reply"), toolBar());
connect(replyAction, &QAction::triggered, this, [this](bool) {
auto dialog = ComposerWindowFactory::self().create(m_email, m_displayName);
dialog->reply(messages().at(0));
- FirstTimeDialog::strongActivateWindow(dialog);
+ DialogController::strongActivateWindow(dialog);
});
toolBar()->addAction(replyAction);
messageMenu->addAction(replyAction);
auto widget = qobject_cast<QToolButton *>(toolBar()->widgetForAction(replyAction));
widget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
// forward
auto forwardAction = new QAction(QIcon::fromTheme(u"mail-forward-symbolic"_s), i18nc("@action:button", "Forward"), toolBar());
connect(forwardAction, &QAction::triggered, this, [this](bool) {
auto dialog = ComposerWindowFactory::self().create(m_email, m_displayName);
dialog->reply(messages().at(0));
- FirstTimeDialog::strongActivateWindow(dialog);
+ DialogController::strongActivateWindow(dialog);
});
toolBar()->addAction(forwardAction);
messageMenu->addAction(forwardAction);
widget = qobject_cast<QToolButton *>(toolBar()->widgetForAction(forwardAction));
widget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
#if MIMETREEPARSER_WIDGETS_VERSION >= QT_VERSION_CHECK(6, 5, 0)
if (Kleo::DeVSCompliance::isActive()) {
auto statusBar = new QStatusBar(this);
auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
{
auto statusPalette = qApp->palette();
KColorScheme::adjustForeground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
statusLbl->foregroundRole(),
KColorScheme::View);
statusLbl->setAutoFillBackground(true);
KColorScheme::adjustBackground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
QPalette::Window,
KColorScheme::View);
statusLbl->setPalette(statusPalette);
}
statusBar->addPermanentWidget(statusLbl.release());
setStatusBar(statusBar);
}
#endif
}
void EmailViewer::closeEvent(QCloseEvent *event)
{
MimeTreeParser::Widgets::MessageViewerWindow::closeEvent(event);
WebsocketClient::self().sendStatusUpdate(true);
}
void EmailViewer::showEvent(QShowEvent *event)
{
MimeTreeParser::Widgets::MessageViewerWindow::showEvent(event);
WebsocketClient::self().sendStatusUpdate();
}
void EmailViewer::view(const QString &content, const QString &accountEmail, const QString &displayName)
{
m_email = accountEmail;
m_displayName = displayName;
setMessages({toMessage(content)});
updateSecLevelButton();
toolBar()->show();
}
void EmailViewer::updateSecLevelButton()
{
delete m_secLevelButton;
m_secLevelButton = new QToolButton();
SecurityLevelInfo secInfo(messages().at(0));
m_secLevelButton->setText(secInfo.shortText());
m_secLevelButton->setIcon(secInfo.icon());
m_secLevelButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_secLevelButton->setToolTip(secInfo.details());
connect(m_secLevelButton, &QToolButton::clicked, m_secLevelButton, [this, secInfo]() {
KMessageDialog dialog(KMessageDialog::Information, secInfo.details(), m_secLevelButton);
dialog.setWindowTitle(secInfo.shortText());
dialog.setIcon(secInfo.icon());
dialog.exec();
});
m_secLevelButtonContainer->layout()->addWidget(m_secLevelButton);
}
diff --git a/client/firsttimedialog.cpp b/client/firsttimedialog.cpp
deleted file mode 100644
index 539b50f..0000000
--- a/client/firsttimedialog.cpp
+++ /dev/null
@@ -1,474 +0,0 @@
-// SPDX-FileCopyrightText: 2024 g10 code Gmbh
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "firsttimedialog.h"
-#include "config.h"
-#include "gpgolweb_version.h"
-#include "rootcagenerator/controller.h"
-#include "ui_confpageinstalladdin.h"
-#include "ui_confpageproxyoptions.h"
-#include "ui_confpagetlscertificate.h"
-#include "ui_confpagewelcome.h"
-#include "ui_firsttimedialog.h"
-#include "websocketclient.h"
-
-#include <QCheckBox>
-#include <QClipboard>
-#include <QCloseEvent>
-#include <QDesktopServices>
-#include <QFile>
-#include <QMargins>
-#include <QDialog>
-#include <QDialogButtonBox>
-#include <QSaveFile>
-#include <QSettings>
-#include <QStandardPaths>
-#include <QStatusBar>
-#include <QStyle>
-#include <QTemporaryDir>
-#include <QToolBar>
-
-#include <Libkleo/Compliance>
-
-#include <KColorScheme>
-#include <KIO/OpenFileManagerWindowJob>
-#include <KTitleWidget>
-
-using namespace Qt::StringLiterals;
-
-class PairingDialog : public QDialog {
-public:
- PairingDialog(QWidget *parent) : QDialog(parent) {
- setWindowTitle(i18n("Pairing mode active"));
- auto l = new QVBoxLayout(this);
- auto lab = new QLabel(i18n("<p>Copy and paste the code shown below into the input field shown at the top of the Add-In:</p>"));
- lab->setWordWrap(true);
- l->addWidget(lab);
- m_pairingTokenLabel = new QLabel(i18nc("status message", "<p align='center'><b>Obtaining token...</b></p>"));
- l->addWidget(m_pairingTokenLabel);
- auto bb = new QDialogButtonBox;
- auto endButton = bb->addButton(i18nc("@button", "End pairing mode"), QDialogButtonBox::RejectRole);
- connect(endButton, &QAbstractButton::clicked, this, &QDialog::reject);
- m_copyButton = bb->addButton(i18nc("@button", "Copy code to clipboard"), QDialogButtonBox::ActionRole);
- m_copyButton->setEnabled(false);
- l->addWidget(bb);
- setFixedSize(sizeHint());
-
- connect(&WebsocketClient::self(), &WebsocketClient::pairingStatusChanged, this, &PairingDialog::pairingStatusChanged);
- WebsocketClient::self().enterPairingMode();
- }
- void pairingStatusChanged(const QString& token, bool pairingActive) {
- if (!pairingActive) {
- reject();
- return;
- }
- m_pairingTokenLabel->setText(QString(u"<p align='center'><b>%1</b></p>").arg(token));
- m_copyButton->setEnabled(true);
- connect(m_copyButton, &QAbstractButton::clicked, [this, token]() {
- qApp->clipboard()->setText(token);
- });
- }
-private:
- QLabel* m_pairingTokenLabel;
- QAbstractButton* m_copyButton;
-};
-
-FirstTimeDialog::FirstTimeDialog(QWidget *parent)
- : QMainWindow(parent)
- , ui(new Ui::FirstTimeDialog)
- , confPageWelcome(new Ui::ConfPageWelcome)
- , confPageInstallAddin(new Ui::ConfPageInstallAddin)
- , confPageProxyOptions(new Ui::ConfPageProxyOptions)
- , confPageTLSCertificate(new Ui::ConfPageTLSCertificate)
- , m_systemTrayIcon(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s))
-{
- m_reconnectTimer.setSingleShot(true);
- connect(&m_reconnectTimer, &QTimer::timeout, this, [this]() { startConnectionNow(); });
-
- ui->setupUi(this);
- confPageWelcome->setupUi(ui->confPageWelcome);
- ui->confPageWelcome->setProperty("title", i18nc("@title", "Welcome to GpgOL/Web"));
- confPageProxyOptions->setupUi(ui->confPageProxyOptions);
- ui->confPageProxyOptions->setProperty("title", i18nc("@title", "Configure Proxy and Optional Features"));
- confPageInstallAddin->setupUi(ui->confPageInstallAddin);
- ui->confPageInstallAddin->setProperty("title", i18nc("@title", "Install Outlook Add-In"));
- confPageTLSCertificate->setupUi(ui->confPageTLSCertificate);
- ui->confPageTLSCertificate->setProperty("title", i18nc("@title", "Setting Up TLS Certificate for Local Proxy"));
-
- if (ui->stack->indexOf(ui->confPageWelcome) != ConfPageWelcome) {
- qFatal("Welcome page misplaced");
- }
- if (ui->stack->indexOf(ui->confPageTLSCertificate) != ConfPageTLSCertificate) {
- qFatal("Tls certification page misplaced");
- }
- if (ui->stack->indexOf(ui->confPageProxyOptions) != ConfPageProxyOptions) {
- qFatal("Proxy options page misplaced");
- }
- if (ui->stack->indexOf(ui->confPageInstallAddin) != ManifestPage) {
- qFatal("Manifest install page misplaced");
- }
-
- confPageProxyOptions->reencryptOption->setChecked(Config::self()->reencrypt());
- connect(confPageProxyOptions->reencryptOption, &QCheckBox::stateChanged, this, [](int state) {
- Config::self()->setReencrypt(state == Qt::Checked);
- Config::self()->save();
- });
-
- auto margins = confPageProxyOptions->remoteServerLayout->contentsMargins();
- margins.setLeft(margins.left() + style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing) + style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth));
- confPageProxyOptions->remoteServerLayout->setContentsMargins(margins);
-
- m_systemTrayIcon.setMainWindow(this);
- m_systemTrayIcon.show();
-
- m_backAction = new QAction(this);
- connect(m_backAction, &QAction::triggered, this, [this]() {
- if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
- ui->stack->setCurrentIndex(ui->stack->currentIndex() > 1 ? ConfPageProxyOptions : ConfPageWelcome);
- } else {
- ui->stack->setCurrentIndex(ui->stack->currentIndex() == ConfPageProxyOptions ? ConfPageWelcome : ConfPageProxyOptions);
- }
- });
- connect(confPageTLSCertificate->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
- connect(confPageInstallAddin->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
- connect(confPageProxyOptions->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
- auto toolbar = new QToolBar(this);
- toolbar->setMovable(false);
- auto titleWidget = new KTitleWidget(this);
- toolbar->addWidget(titleWidget);
- addToolBar(Qt::TopToolBarArea, toolbar);
-
- titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
- connect(ui->stack, &QStackedWidget::currentChanged, this, [titleWidget, this]() {
- titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
- });
-
- QPixmap logo = QIcon::fromTheme(u"com.gnupg.gpgolweb"_s).pixmap(64, 64);
- confPageWelcome->logo->setPixmap(logo);
- confPageWelcome->titleWelcome->setText(i18nc("@info", "GpgOL/Web %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
-
- auto statusBar = new QStatusBar(this);
-
- confPageInstallAddin->showOnStartup->setChecked(Config::self()->showLauncher());
- connect(confPageInstallAddin->showOnStartup, &QCheckBox::toggled, this, [](bool checked) {
- Config::self()->setShowLauncher(checked);
- Config::self()->save();
- });
-
- m_status = new QLabel;
- statusBar->addPermanentWidget(m_status);
-
- auto version = new QLabel(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
- statusBar->addPermanentWidget(version);
-
- if (Kleo::DeVSCompliance::isActive()) {
- auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
- {
- auto statusPalette = qApp->palette();
- KColorScheme::adjustForeground(statusPalette,
- Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
- statusLbl->foregroundRole(),
- KColorScheme::View);
- statusLbl->setAutoFillBackground(true);
- KColorScheme::adjustBackground(statusPalette,
- Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
- QPalette::Window,
- KColorScheme::View);
- statusLbl->setPalette(statusPalette);
- }
- statusBar->addPermanentWidget(statusLbl.release());
- }
-
- setStatusBar(statusBar);
-
- connect(confPageProxyOptions->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
- connect(confPageWelcome->configureButton, &QPushButton::clicked, this, [this]() {
- ui->stack->setCurrentIndex(ConfPageProxyOptions);
- });
-
- connect(confPageTLSCertificate->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
- confPageTLSCertificate->continueButton->setEnabled(false);
- confPageTLSCertificate->installButton->setVisible(false);
- confPageTLSCertificate->label->setVisible(false);
- connect(confPageTLSCertificate->installButton, &QPushButton::clicked, this, [this]() {
- if (m_controller) {
- m_controller->install();
- }
- });
-
- confPageInstallAddin->manifestPath->setText(QLatin1StringView(DATAROUTDIR) + u"/gpgol/manifest.xml"_s);
-
- connect(confPageInstallAddin->testPageButton, &QPushButton::clicked, this, [this]() {
- QDesktopServices::openUrl(QUrl(u"https://"_s + serverDomain() + u"/test"_s));
- });
- connect(confPageInstallAddin->pairWebClientButton, &QPushButton::clicked, this, [this]() {
- PairingDialog d(this);
- d.exec();
- WebsocketClient::self().quitPairingMode();
- });
- connect(confPageInstallAddin->minimizeButton, &QPushButton::clicked, this, &QWidget::hide);
-
- connect(confPageInstallAddin->manifestPathCopy, &QPushButton::clicked, this, [this]() {
- QGuiApplication::clipboard()->setText(confPageInstallAddin->manifestPath->text());
- });
-
- connect(confPageInstallAddin->manifestPathOpenFolder, &QPushButton::clicked, this, [this]() {
- auto job = new KIO::OpenFileManagerWindowJob();
- job->setHighlightUrls({QUrl::fromUserInput(confPageInstallAddin->manifestPath->text())});
- if (!qEnvironmentVariableIsEmpty("XDG_ACTIVATION_TOKEN")) {
- job->setStartupId(qgetenv("XDG_ACTIVATION_TOKEN"));
- }
- job->start();
- });
-
- confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
- confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
- confPageProxyOptions->remoteServer->setText(Config::self()->remoteAddress().toString());
- confPageProxyOptions->remoteOption->setChecked(!Config::self()->isLocalServer());
-
- connect(confPageProxyOptions->remoteOption, &QRadioButton::toggled, this, [this](bool checked) {
- Config::self()->setIsLocalServer(!checked);
- Config::self()->save();
-
- confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
- confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
- });
-
- connect(confPageProxyOptions->remoteServer, &QLineEdit::textChanged, this, [this]() {
- Config::self()->setRemoteAddress(QUrl::fromUserInput(confPageProxyOptions->remoteServer->text()));
- Config::self()->save();
- });
-
- if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
- ui->stack->setCurrentIndex(ConfPageWelcome);
- startConnection();
- } else {
- ui->stack->setCurrentIndex(ConfPageProxyOptions);
- }
-
- connect(&m_serverProcess, &QProcess::readyReadStandardError, this, [this]() {
- qWarning().noquote() << m_serverProcess.readAllStandardError();
- });
-
- connect(&m_serverProcess, &QProcess::readyReadStandardOutput, this, [this]() {
- qWarning().noquote() << m_serverProcess.readAllStandardOutput();
- });
- connect(&m_serverProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) {
- qWarning() << "Process error" << err;
- qWarning().noquote() << m_serverProcess.readAllStandardError();
- });
- connect(&m_serverProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
- qWarning() << "Process finished" << exitCode << status;
- if (status == QProcess::NormalExit) {
- qWarning() << "Status code" << m_serverProcess.exitCode();
- }
- qWarning().noquote() << m_serverProcess.readAllStandardError();
- });
-
- connect(&m_serverProcess, &QProcess::finished, this, [this](int error, QProcess::ExitStatus exitstatus) {
- if (error) {
- qWarning().noquote() << "proxy process exited with error" << error << "status" << exitstatus;
- // TODO: show status message
- // this usually means, another server process is already running, blocking the port
- }
- startConnection();
- });
-
-#ifdef Q_OS_WIN
- // We intentionally don't use our own config for this: If users disable autostart via
- // the Windows settings menu, we want to respect that, too.
- {
- QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
- confPageWelcome->autostartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
- }
- connect(confPageWelcome->autostartBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) {
- QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
- if (state) {
- winreg.setValue(QCoreApplication::applicationName(),
- QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
- } else {
- winreg.remove(QCoreApplication::applicationName());
- }
- });
-#else
- confPageWelcome->autostartBox->setVisible(false);
-#endif
-}
-
-FirstTimeDialog::~FirstTimeDialog() = default;
-
-void FirstTimeDialog::slotStateChanged(const QString &stateDisplay)
-{
- m_status->setText(stateDisplay);
- m_systemTrayIcon.stateChanged(stateDisplay, WebsocketClient::self().state());
- if (!WebsocketClient::self().connectedToProxy()) {
- QTimer::singleShot(1000, this, [this]() { startConnection(); });
- }
-}
-
-void FirstTimeDialog::closeEvent(QCloseEvent *e)
-{
- e->ignore();
- hide();
-}
-
-void FirstTimeDialog::slotSetup()
-{
- if (confPageProxyOptions->localOption->isChecked()) {
- if (!Controller::certificateAlreadyGenerated()) {
- delete m_controller;
- m_controller = new Controller(this);
- confPageTLSCertificate->plainTextEdit->clear();
- connect(m_controller, &Controller::generationDone, this, [this]() {
- confPageTLSCertificate->installButton->setVisible(true);
- confPageTLSCertificate->installButton->setEnabled(true);
- confPageTLSCertificate->label->setText(
- i18nc("@info", "About to install certificate with fingerprint: %1 ", m_controller->rootFingerprint()));
- confPageTLSCertificate->label->setVisible(true);
- confPageTLSCertificate->continueButton->setVisible(false);
- });
- ui->stack->setCurrentIndex(ConfPageTLSCertificate);
-
- connect(m_controller, &Controller::result, this, [this](KJob *) {
- if (m_controller->error()) {
- confPageTLSCertificate->plainTextEdit->appendPlainText(m_controller->errorText());
- return;
- }
- confPageTLSCertificate->installButton->setVisible(false);
- confPageTLSCertificate->continueButton->setVisible(true);
- confPageTLSCertificate->label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
-
- confPageTLSCertificate->continueButton->setEnabled(true);
-
- });
- connect(m_controller, &Controller::debutOutput, this, &FirstTimeDialog::slotTlsDebutOutput);
- m_controller->start();
- } else {
- startConnection();
- generateManifest();
- }
- } else {
- generateManifest();
- }
-}
-
-void FirstTimeDialog::startConnection()
-{
- m_reconnectTimer.start(1000);
-}
-
-void FirstTimeDialog::startConnectionNow()
-{
- auto &websocketClient = WebsocketClient::self(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s));
- QObject::disconnect(&websocketClient);
- connect(&websocketClient, &WebsocketClient::stateChanged, this, &FirstTimeDialog::slotStateChanged);
-
- if (Config::self()->isLocalServer()) {
- if (m_serverProcess.state() == QProcess::NotRunning) {
- WebsocketClient::self().disconnectFromProxy();
- m_serverProcess.disconnect();
- const auto clientSecret = QUuid::createUuid().toString(QUuid::WithoutBraces);
- const auto secretHash = QString::fromLatin1(QCryptographicHash::hash(clientSecret.toUtf8().data(), QCryptographicHash::Sha256).toHex());
- connect(&m_serverProcess, &QProcess::started, this, [this, clientSecret]() {
- WebsocketClient::self().connectToProxy(clientSecret);
- });
- m_serverProcess.start(u"gpgol-server"_s, QStringList{u"--singleclient="_s + secretHash});
- return;
- }
- }
-
- slotStateChanged(websocketClient.stateDisplay());
- if (!WebsocketClient::self().connectedToProxy()) {
- WebsocketClient::self().connectToProxy();
- }
-}
-
-void FirstTimeDialog::slotTlsDebutOutput(const QString &output)
-{
- confPageTLSCertificate->plainTextEdit->appendPlainText(output);
-}
-
-static QByteArray ampersandEncode(const QString &input)
-{
- QByteArray encoded;
- for(int i = 0; i < input.size(); ++i) {
- QChar ch = input.at(i);
- if(ch.unicode() > 127) {
- encoded += QString(u"&#%1;"_s).arg(static_cast<int>(ch.unicode())).toLatin1();
- } else {
- encoded += ch.toLatin1();
- }
- }
- return encoded;
-}
-
-void FirstTimeDialog::generateManifest()
-{
- QFile file(u":/gpgol-client/manifest.xml.in"_s);
- if (!file.open(QIODeviceBase::ReadOnly)) {
- Q_ASSERT(false);
- return;
- }
-
- ui->stack->setCurrentIndex(ManifestPage);
-
- QByteArray manifest = file.readAll();
- manifest.replace("%HOST%", serverDomain().toUtf8());
- manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
- // HACK: For a manifest loaded from local file - as single users will do - MS does not apply translations.
- // They claim it's a feature, not a bug. To work around this, we localize the default value, here, instead.
- // At the same time, we also have to keep translations in the manifest, so as not to break translations
- // for origanization-installed manifests.
- int offset = 0;
- const auto attrib = QByteArray("DefaultValue=\"");
- while ((offset = manifest.indexOf("GPGOLI18N=\"true\"", offset)) > -1) {
- int strBegin = manifest.indexOf(attrib, offset) + attrib.length();
- Q_ASSERT(strBegin > attrib.length());
- int strEnd = manifest.indexOf("\"", strBegin);
- Q_ASSERT(strEnd > 0);
- const auto translation = ki18nd("gpgol-js-manifest", manifest.mid(strBegin, strEnd-strBegin).constData()).toString();
- manifest.replace(offset, strEnd-offset, QByteArray(attrib + ampersandEncode(translation).constData()));
- }
- const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
-
- QSaveFile saveFile(saveFilePath);
- if (!saveFile.open(QIODeviceBase::WriteOnly)) {
- Q_ASSERT(false);
- return;
- }
- saveFile.write(manifest);
- saveFile.commit();
-
- confPageInstallAddin->manifestPath->setText(QDir::toNativeSeparators(saveFilePath));
-}
-
-QString FirstTimeDialog::serverDomain() const
-{
- return confPageProxyOptions->localOption->isChecked() ? u"localhost:5656"_s : QUrl(confPageProxyOptions->remoteServer->text()).authority();
-}
-
-#ifdef Q_OS_WIN
-#include <windows.h>
-#endif
-
-void FirstTimeDialog::strongActivateWindow(QWidget* window)
-{
- window->show();
- window->activateWindow();
- window->raise();
-#ifdef Q_OS_WIN
- // HACK: Simulate Alt-keyPress while bringing the window to the front.
- // This helps when our app does not currently have focus - and
- // frequently it does not, because we have just clicked in browser/outlook.
- // https://stackoverflow.com/questions/72620538/whats-the-correct-way-to-bring-a-window-to-the-front
- keybd_event(VK_MENU, 0, 0, 0);
- auto hwnd = HWND(window->winId());
- SetForegroundWindow(hwnd);
- keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
- // Sometimes Qt may act as if Alt was still pressed, after the above sequence.
- // Send it another key release event, explicitly.
- auto e = new QKeyEvent(QEvent::KeyRelease, Qt::Key_Alt, Qt::AltModifier);
- qApp->postEvent(window, e);
-#endif
-}
diff --git a/client/firsttimedialog.h b/client/firsttimedialog.h
deleted file mode 100644
index f313700..0000000
--- a/client/firsttimedialog.h
+++ /dev/null
@@ -1,67 +0,0 @@
-// SPDX-FileCopyrightText: 2024 g10 code Gmbh
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "utils/systemtrayicon.h"
-#include <QMainWindow>
-#include <QPointer>
-#include <QProcess>
-#include <QTimer>
-#include <memory>
-
-class QLabel;
-class Controller;
-
-namespace Ui
-{
-class FirstTimeDialog;
-class ConfPageWelcome;
-class ConfPageInstallAddin;
-class ConfPageProxyOptions;
-class ConfPageTLSCertificate;
-}
-
-class FirstTimeDialog : public QMainWindow
-{
- Q_OBJECT
-public:
- explicit FirstTimeDialog(QWidget *parent = nullptr);
- ~FirstTimeDialog();
-
- QString serverDomain() const;
- // TODO: move somewhere appropriate. But FirstTimeDialog needs to be split / renamed, anyway
- static void strongActivateWindow(QWidget* window);
-public Q_SLOTS:
- void slotStateChanged(const QString &stateDisplay);
- void slotTlsDebutOutput(const QString &output);
-
-protected:
- void closeEvent(QCloseEvent *e) override;
- void generateManifest();
-
-private:
- void slotSetup();
- void startConnectionNow();
- void startConnection();
-
- enum Page {
- ConfPageWelcome = 0,
- ConfPageProxyOptions = 1,
- ManifestPage = 2,
- ConfPageTLSCertificate = 3,
- };
-
- std::unique_ptr<Ui::FirstTimeDialog> ui;
- std::unique_ptr<Ui::ConfPageWelcome> confPageWelcome;
- std::unique_ptr<Ui::ConfPageInstallAddin> confPageInstallAddin;
- std::unique_ptr<Ui::ConfPageProxyOptions> confPageProxyOptions;
- std::unique_ptr<Ui::ConfPageTLSCertificate> confPageTLSCertificate;
- QAction *m_backAction;
- QLabel *m_status;
- QPointer<Controller> m_controller;
- SystemTrayIcon m_systemTrayIcon;
- QProcess m_serverProcess;
- QTimer m_reconnectTimer;
-};
diff --git a/client/firsttimedialog.ui b/client/firsttimedialog.ui
deleted file mode 100644
index a5cbaae..0000000
--- a/client/firsttimedialog.ui
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
-<!--
-SPDX-FileCopyrightText: 2024 g10 Code GmbH
-SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-SPDX-License-Identifier: GPL-2.0-or-later
--->
- <class>FirstTimeDialog</class>
- <widget class="QMainWindow" name="FirstTimeDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>629</width>
- <height>390</height>
- </rect>
- </property>
- <widget class="QStackedWidget" name="stack">
- <property name="currentIndex">
- <number>3</number>
- </property>
- <widget class="QWidget" name="confPageWelcome">
- </widget>
- <widget class="QWidget" name="confPageProxyOptions">
- </widget>
- <widget class="QWidget" name="confPageInstallAddin">
- </widget>
- <widget class="QWidget" name="confPageTLSCertificate">
- </widget>
- </widget>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/client/icons/icons.qrc.in b/client/icons/icons.qrc.in
index 60d8014..94fbc5b 100644
--- a/client/icons/icons.qrc.in
+++ b/client/icons/icons.qrc.in
@@ -1,18 +1,19 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/icons/breeze">
<file alias="apps/48/com.gnupg.gpgolweb.svg">@app_icon_svg@</file>
</qresource>
<qresource prefix="/">
+ <file alias="/icons/addin_logo.png">@CMAKE_SOURCE_DIR@/web/assets/logo.png</file>
<file alias="/icons/level-0.svg">@CMAKE_SOURCE_DIR@/client/icons/level-0.svg</file>
<file alias="/icons/level-1.svg">@CMAKE_SOURCE_DIR@/client/icons/level-1.svg</file>
<file alias="/icons/level-2.svg">@CMAKE_SOURCE_DIR@/client/icons/level-2.svg</file>
<file alias="/icons/level-3.svg">@CMAKE_SOURCE_DIR@/client/icons/level-3.svg</file>
<file alias="/icons/level-4.svg">@CMAKE_SOURCE_DIR@/client/icons/level-4.svg</file>
<file alias="/icons/level-0-enc.svg">@CMAKE_SOURCE_DIR@/client/icons/level-0-enc.svg</file>
<file alias="/icons/level-1-enc.svg">@CMAKE_SOURCE_DIR@/client/icons/level-1-enc.svg</file>
<file alias="/icons/level-2-enc.svg">@CMAKE_SOURCE_DIR@/client/icons/level-2-enc.svg</file>
<file alias="/icons/level-3-enc.svg">@CMAKE_SOURCE_DIR@/client/icons/level-3-enc.svg</file>
<file alias="/icons/level-4-enc.svg">@CMAKE_SOURCE_DIR@/client/icons/level-4-enc.svg</file>
</qresource>
</RCC>
diff --git a/client/main.cpp b/client/main.cpp
index fa9cbdf..e76397e 100644
--- a/client/main.cpp
+++ b/client/main.cpp
@@ -1,136 +1,149 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QCommandLineParser>
#include <QElapsedTimer>
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPointer>
#include <QTimer>
#include <QUuid>
#include <Libkleo/FileSystemWatcher>
#include <Libkleo/GnuPG>
#include <Libkleo/KeyCache>
#include <KAboutData>
#include <KJob>
#include <KLocalizedString>
#include "../common/log.h"
#include "config.h"
-#include "firsttimedialog.h"
+#include "connectioncontroller.h"
+#include "controller.h"
#include "gpgol_client_debug.h"
#include "gpgolweb_version.h"
+#include "setupdialogs.h"
+#include "statusdialog.h"
#include "utils/kuniqueservice.h"
+#include "utils/systemtrayicon.h"
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono;
#ifdef Q_OS_WINDOWS
#include <windows.h>
#endif
#define STARTUP_TIMING qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:"
#define STARTUP_TRACE qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:" << SRCNAME << __func__ << __LINE__;
static void setupLogging(std::shared_ptr<Kleo::Log> log)
{
const QByteArray dirNative = qgetenv("GPGOL_CLIENT_LOGDIR");
if (dirNative.isEmpty()) {
return;
}
log->setOutputDirectory(QFile::decodeName(dirNative));
qInstallMessageHandler(Kleo::Log::messageHandler);
}
int main(int argc, char *argv[])
{
#ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
// On Windows, broken heuristics appear to be in place WRT whether our app should exit
// when the last QEventLocker goes out of scope, resulting in sudden quits, after handling a
// websocket request while minimized to systray (the specific conditions appear to include that no
// other window besides the FirstTimeDialog had been visible before minimzing).
// We prefer to quit on our own terms, thank you!
app.setQuitLockEnabled(false);
KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol-js-native"));
KAboutData about(QStringLiteral("gpgol-client"),
i18nc("@title:window", "GnuPG Outlook Add-in"),
QStringLiteral(GPGOLWEB_VERSION_STRING),
i18nc("@info", "GPG Outlook add-in"),
KAboutLicense::GPL,
i18nc("@info:credit", "© 2023-2025 g10 Code GmbH"));
about.setDesktopFileName(u"com.gnupg.gpgolweb"_s);
about.setProgramLogo(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s));
about.addAuthor(i18nc("@info:credit", "Carl Schwan"), i18nc("@info:credit", "Maintainer"), u"carl.schwan@gnupg.com"_s, u"https://carlschwan.eu"_s);
about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails"));
about.setOrganizationDomain("gnupg.com");
about.setBugAddress("https://dev.gnupg.org/maniphest/task/edit/form/1/");
QCommandLineParser parser;
+ parser.addOption({u"setup"_s, u"Run first time setup assistant."_s});
KAboutData::setApplicationData(about);
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
QElapsedTimer startupTimer;
startupTimer.start();
STARTUP_TIMING << "Application created";
/* Create the unique service ASAP to prevent double starts if
* the application is started twice very quickly. */
KUniqueService service;
QObject::connect(&service, &KUniqueService::activateRequested, &service, [&service](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(arguments);
Q_UNUSED(workingDirectory);
service.setExitValue(0);
});
auto log = Kleo::Log::mutableInstance();
setupLogging(log);
STARTUP_TIMING << "Service created";
- QPointer<FirstTimeDialog> launcher = new FirstTimeDialog;
-
+ ConnectionController connection;
+ connection.startStopLocalServer();
+ connection.startWebsocketClient();
+ bool setup = parser.isSet(u"setup"_s) || (Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
+ if (setup) {
+ DialogController::doFirstTimeAssistant();
+ }
+ SystemTrayIcon icon(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s));
+ icon.show();
if (Config::self()->showLauncher()) {
- launcher->show();
+ auto dialog = StatusDialog::getOrCreate();
+ dialog->show();
}
STARTUP_TIMING << "KeyCache creation";
auto keyCache = Kleo::KeyCache::mutableInstance();
auto fsWatcher = std::make_shared<Kleo::FileSystemWatcher>();
fsWatcher->whitelistFiles(Kleo::gnupgFileWhitelist());
fsWatcher->addPaths(Kleo::gnupgFolderWhitelist());
fsWatcher->setDelay(1000);
keyCache->addFileSystemWatcher(fsWatcher);
keyCache->startKeyListing();
return app.exec();
}
diff --git a/client/reencrypt/reencryptjob.cpp b/client/reencrypt/reencryptjob.cpp
index 66eaa7a..67902d3 100644
--- a/client/reencrypt/reencryptjob.cpp
+++ b/client/reencrypt/reencryptjob.cpp
@@ -1,351 +1,351 @@
// SPDX-FileCopyrightText: 2025 g10 code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "reencryptjob.h"
#include "choosekeydialog.h"
-#include "../firsttimedialog.h" // for strongActivateWindow
#include "reencryptprogressdialog.h"
#include "../mailapi.h"
+#include "../setupdialogs.h"
#include <QGpgME/DecryptJob>
#include <QGpgME/EncryptJob>
#include <QGpgME/Protocol>
#include <gpgme++/context.h>
#include <gpgme++/decryptionresult.h>
#include <gpgme++/encryptionresult.h>
#include <QFile>
#include <QLocale>
#include <QSaveFile>
#include <QTimer>
#include <KLocalizedString>
#include <KMime/Message>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
using namespace Qt::StringLiterals;
using namespace std::chrono_literals;
ReencryptJob::ReencryptJob(const QString &folderId)
: KJob()
, m_folderId(folderId)
{
}
bool ReencryptJob::doKill()
{
Q_EMIT wasKilled();
return true;
}
void ReencryptJob::tryRaiseDialog()
{
if (m_progress_dialog) {
- FirstTimeDialog::strongActivateWindow(m_progress_dialog);
+ DialogController::strongActivateWindow(m_progress_dialog);
}
}
bool ReencryptJob::hasStarted() const
{
return m_progress_dialog;
}
bool ReencryptJob::checkPropagateError(KJob *job)
{
if (job->error() != KJob::NoError) {
if (!error()) {
setError(job->error());
setErrorText(job->errorText());
emitResult();
}
return true;
}
return false;
}
void ReencryptJob::start()
{
auto request = MailApiController::self().getFolderInfo(m_folderId);
connect(request, &MailApiJob::finished, this, [this, request](KJob *) {
if (checkPropagateError(request)) {
return;
}
const auto &responses = request->takeResponses();
if (responses.isEmpty()) {
setError(KJob::UserDefinedError);
setErrorText(u"Could not read folder contents"_s);
emitResult();
return;
}
const auto sourceFolderName = GetFolderInfoJob::folderName(responses.value(0));
m_folderName = i18nc("SourceFolderName - reencrypted - DateTime", "%1 - reencrypted %2", sourceFolderName, QLocale().toString(QDateTime::currentDateTime(), u"yyyy-MM-dd hh-mm-ss"_s));
m_parentFolderId = GetFolderInfoJob::parentFolderId(responses.value(0));
int messageCount = GetFolderInfoJob::messageCount(responses.value(0));
auto chooseKeyDialog = new ChooseKeyDialog(sourceFolderName, m_folderName);
- FirstTimeDialog::strongActivateWindow(chooseKeyDialog);
+ DialogController::strongActivateWindow(chooseKeyDialog);
chooseKeyDialog->setAttribute(Qt::WA_DeleteOnClose);
// must also go, if we get cancelled
connect(this, &QObject::destroyed, chooseKeyDialog, &QObject::deleteLater);
connect(chooseKeyDialog, &QDialog::accepted, this, [this, chooseKeyDialog, messageCount] {
m_currentKeys = chooseKeyDialog->currentKeys();
m_unencryptedMode = chooseKeyDialog->unencryptedMode();
m_progress_dialog = new ReencryptProgressDialog(m_unencryptedMode, this);
m_progress_dialog->setTotalMessageCount(messageCount);
// sync job lifetime with lifetime of the progress dialog from here on
setAutoDelete(false);
m_progress_dialog->setAttribute(Qt::WA_DeleteOnClose);
connect(m_progress_dialog.data(), &QDialog::destroyed, this, &QObject::deleteLater);
createNewFolder();
tryRaiseDialog();
});
connect(chooseKeyDialog, &QDialog::rejected, this, [this] {
emitResult();
});
});
request->start();
}
void ReencryptJob::createNewFolder()
{
auto request = MailApiController::self().createFolder(
m_parentFolderId,
m_folderName);
connect(request, &CreateFolderJob::finished, this, [this, request](KJob *) {
if (checkPropagateError(request)) {
return;
}
m_newFolderId = CreateFolderJob::createdFolderId(request->takeResponses().value(0));
if (m_newFolderId.isEmpty()) {
// This can happen in EWS API, if folder already exists
setError(1);
setErrorText(i18n("Could not create folder %1"));
emitResult();
return;
}
auto listFolderRequest = MailApiController::self().listFolder(m_folderId);
ensureSubJobReceivesCancel(listFolderRequest);
connect(listFolderRequest, &ListFolderJob::responseAvailable, this, [this](const MailApiJob::Response &response) {
const auto mailIds = ListFolderJob::mailIds(response);
for (const auto &mailId : mailIds) {
m_itemsToFetch.append(mailId);
}
auto getJob = MailApiController::self().getMails(mailIds, GetMailsJob::GetMimeContent);
connect(getJob, &GetMailsJob::responseAvailable, this, [this](const MailApiJob::Response &response) {
Message msg;
msg.itemId = GetMailsJob::mailId(response);
msg.mimeContent = GetMailsJob::mimeContent(response);
m_itemsToReencrypt.append(msg);
m_itemsToFetch.removeAll(msg.itemId);
reencrypt();
});
connect(getJob, &GetMailsJob::finished, this, [this](KJob *job) {
checkPropagateError(job);
});
ensureSubJobReceivesCancel(getJob);
getJob->start();
});
connect(listFolderRequest, &ListFolderJob::result, this, [this](KJob *) {
m_listFetchFinished = true;
reencrypt();
});
listFolderRequest->start();
});
request->start();
}
template<typename T>
const T *findHeader(KMime::Content *content)
{
auto header = content->header<T>();
if (header || !content->parent()) {
return header;
}
return findHeader<T>(content->parent());
}
void ReencryptJob::reencrypt()
{
if (m_reencrypting) {
return;
}
if (m_itemsToReencrypt.isEmpty()) {
if (m_listFetchFinished && m_itemsToFetch.isEmpty()) {
emitResult();
}
return;
}
const auto item = m_itemsToReencrypt.dequeue();
const auto itemId = item.itemId;
if (item.mimeContent.isEmpty()) {
m_progress_dialog->setMessageError(itemId, i18n("Message is empty"));
reencrypt();
return;
}
m_reencrypting = true;
const auto mailData = KMime::CRLFtoLF(item.mimeContent);
const auto msg = KMime::Message::Ptr(new KMime::Message);
msg->setContent(mailData);
msg->parse();
auto subJobs = std::make_shared<ReencryptSubJobs>();
auto subject = findHeader<KMime::Headers::Subject>(msg.get());
subJobs->message = msg;
subJobs->itemId = itemId;
m_progress_dialog->addMessage(itemId, subject ? subject->asUnicodeString() : QString());
parseParts(msg.get(), subJobs);
const bool isEncrypted = !subJobs->encryptJobs.isEmpty();
if (!isEncrypted) {
saveCopy(itemId);
} else {
QTimer::singleShot(10s, this, [this, subJobs, itemId]() {
// Check if reencryption was finished
if (!subJobs->encryptJobs.isEmpty()) {
m_progress_dialog->setMessageError(itemId, i18n("Unable to reencrypt \"%1\". Timing out in GPG."));
saveCopy(itemId);
}
});
}
}
void ReencryptJob::saveCopy(const QString &itemId)
{
if (m_unencryptedMode == UnencryptedMode::Copy) {
auto copyItemRequest = MailApiController::self().copyMail(itemId, m_newFolderId);
connect(copyItemRequest, &CopyMailJob::finished, this, [this, itemId, copyItemRequest]() {
m_reencrypting = false;
reencrypt();
if (copyItemRequest->error() != KJob::NoError) {
m_progress_dialog->setMessageError(itemId, copyItemRequest->errorText());
return;
}
m_progress_dialog->setMessageSkipped(itemId);
});
copyItemRequest->start();
} else {
m_progress_dialog->setMessageSkipped(itemId);
m_reencrypting = false;
reencrypt();
}
}
void ReencryptJob::parseParts(KMime::Content *content, std::shared_ptr<ReencryptSubJobs> subJobs)
{
if (const auto contentType = content->contentType(); contentType) {
const auto mimeType = contentType->mimeType();
if (mimeType == "application/pgp-encrypted") {
const auto parent = content->parent();
if (!parent) {
return;
}
const auto siblings = parent->contents();
for (const auto sibling : siblings) {
if (const auto contentType = sibling->contentType(); contentType) {
const auto mimeType = contentType->mimeType();
if (mimeType == "application/octet-stream") {
const auto encryptedContent = sibling->decodedContent();
auto job = QGpgME::openpgp()->encryptJob(true, true);
connect(this, &ReencryptJob::wasKilled, job, &QGpgME::EncryptJob::slotCancel);
#ifdef GPGME2
job->setEncryptionFlags(GpgME::Context::AddRecipient);
#endif
job->setInputEncoding(GpgME::Data::ArmorEncoding);
qInfo() << "Started reencrypting" << subJobs->itemId << "size" << encryptedContent.size();
connect(job,
&QGpgME::EncryptJob::result,
this,
[this, sibling, job, subJobs](const GpgME::EncryptionResult &result,
const QByteArray &cipherText,
const QString &,
const GpgME::Error &) {
subJobs->encryptJobs.removeAll(job);
if (result.error()) {
// TODO: If we ever want to process things we can't decrypt other than skip item
// code is needed here somehow to deal with it
qWarning() << "Finished reencrypting" << subJobs->itemId << "with error"
<< Kleo::Formatting::errorAsString(result.error());
m_progress_dialog->setMessageError(subJobs->itemId, Kleo::Formatting::errorAsString(result.error()));
if (subJobs->encryptJobs.isEmpty()) {
m_reencrypting = false;
reencrypt();
}
return;
}
qInfo() << "Finished reencrypting" << subJobs->itemId;
sibling->setBody(cipherText);
if (subJobs->encryptJobs.isEmpty()) {
subJobs->message->assemble();
const auto newMessage = subJobs->message->encodedContent();
saveAsNew(subJobs->itemId, newMessage);
}
});
auto error = job->start(m_currentKeys, encryptedContent);
if (error) {
qInfo() << "Could not start reencrypting" << subJobs->itemId << "with error"
<< Kleo::Formatting::errorAsString(error);
setError(KJob::UserDefinedError);
setErrorText(errorText() + Kleo::Formatting::errorAsString(error) + u'\n');
m_progress_dialog->setMessageError(subJobs->itemId, Kleo::Formatting::errorAsString(error));
continue;
}
subJobs->encryptJobs.append(job);
}
}
}
}
}
for (const auto &content : content->contents()) {
parseParts(content, subJobs);
}
}
void ReencryptJob::ensureSubJobReceivesCancel(KJob *job)
{
connect(this, &ReencryptJob::wasKilled, job, [job]() {
job->kill(KJob::EmitResult);
});
}
void ReencryptJob::saveAsNew(const QString &itemId, const QByteArray &newContent)
{
auto copyAndChangeRequest = MailApiController::self().copyAndChangeMail(itemId, m_newFolderId, newContent);
ensureSubJobReceivesCancel(copyAndChangeRequest);
connect(copyAndChangeRequest, &CopyAndChangeMailJob::finished, this, [this, itemId, newContent, copyAndChangeRequest]() {
if (copyAndChangeRequest->error() != KJob::NoError) {
m_progress_dialog->setMessageError(itemId, copyAndChangeRequest->errorText());
return;
}
m_progress_dialog->setMessageFinished(itemId);
m_reencrypting = false;
reencrypt();
});
copyAndChangeRequest->start();
}
diff --git a/client/rootcagenerator/controller.cpp b/client/rootcagenerator/controller.cpp
index b134e28..5e90f5a 100644
--- a/client/rootcagenerator/controller.cpp
+++ b/client/rootcagenerator/controller.cpp
@@ -1,410 +1,404 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "controller.h"
#include "truststore.h"
#include "../utils/formatter.h"
#include <QDate>
#include <QDir>
#include <QProcess>
#include <QSaveFile>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QGpgME/ExportJob>
#include <QGpgME/ImportJob>
#include <QGpgME/KeyGenerationJob>
#include <QGpgME/KeyListJob>
#include <QGpgME/Protocol>
#include <gpgme++/context.h>
#include <Libkleo/Formatting>
#include <Libkleo/KeyParameters>
#include <Libkleo/KeyUsage>
#include <KLocalizedString>
using namespace Qt::StringLiterals;
using namespace Kleo;
using namespace GpgME;
static KeyParameters createRootCaParms()
{
KeyParameters keyParameters(KeyParameters::CMS);
keyParameters.setKeyType(GpgME::Subkey::PubkeyAlgo::AlgoRSA);
keyParameters.setKeyUsage(KeyUsage{KeyUsage::Sign | KeyUsage::Certify});
keyParameters.setDN(u"CN=GnuPG Outlook Add-in Local Root CA"_s);
keyParameters.setEmail(u"localroot@gpgoljs.local"_s);
keyParameters.setKeyLength(3072);
keyParameters.setExpirationDate(QDate(2060, 10, 10));
keyParameters.setUseRandomSerial();
keyParameters.setControlStatements({u"%no-protection"_s});
return keyParameters;
}
static KeyParameters createTlsCertParms(QLatin1StringView keyGrip)
{
KeyParameters keyParameters(KeyParameters::CMS);
keyParameters.setKeyType(GpgME::Subkey::PubkeyAlgo::AlgoRSA);
keyParameters.setKeyUsage(KeyUsage{KeyUsage::Sign | KeyUsage::Encrypt});
keyParameters.setDN(u"CN=GnuPG Outlook Add-in Local Server Certificate"_s);
keyParameters.setEmail(u"local@gpgoljs.local"_s);
keyParameters.setKeyLength(3072);
keyParameters.setExpirationDate(QDate(2060, 10, 10));
keyParameters.setIssuerDN(u"CN=GnuPG Outlook Add-in Local Root CA"_s);
keyParameters.setSigningKey(keyGrip);
keyParameters.setUseRandomSerial();
keyParameters.addDomainName(u"localhost"_s);
keyParameters.setControlStatements({u"%no-protection"_s});
return keyParameters;
}
Controller::Controller(QObject *parent)
: KJob(parent)
{
}
Controller::~Controller()
{
if (m_tmpdir) {
QProcess p;
p.start(u"gpgconf"_s, {u"--homedir"_s, m_tmpdir->path(), u"--kill"_s, u"all"_s});
p.waitForFinished();
}
}
QByteArray Controller::caCert() const
{
return m_publicCA;
}
bool Controller::certificateAlreadyGenerated()
{
auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
-
return !certPath.isEmpty();
}
void Controller::setHomeDirForJob(QGpgME::Job *job)
{
auto context = QGpgME::Job::context(job);
context->setEngineHomeDirectory(m_tmpdir->path().toUtf8().constData());
}
void Controller::start()
{
- if (certificateAlreadyGenerated()) {
- emitResult();
- return;
- }
-
m_tmpdir = std::make_unique<QTemporaryDir>();
auto keyGenerationJob = QGpgME::smime()->keyGenerationJob();
setHomeDirForJob(keyGenerationJob);
connect(keyGenerationJob, &QGpgME::KeyGenerationJob::result, this, &Controller::slotRootCaCreatedSlot);
keyGenerationJob->start(createRootCaParms().toString());
}
void Controller::slotRootCaCreatedSlot(const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData, const QString &auditLog)
{
Q_UNUSED(auditLog)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto importJob = QGpgME::smime()->importJob();
setHomeDirForJob(importJob);
connect(importJob, &QGpgME::ImportJob::result, this, &Controller::slotRootCaImportedSlot);
importJob->start(pubKeyData);
}
void Controller::slotRootCaImportedSlot(const GpgME::ImportResult &result, const QString &auditLogAsHtml, const GpgME::Error &auditLogError)
{
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
Q_EMIT debutOutput(i18nc("Debug message", "Imported root CA"));
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
// Get the keygrip
auto keyListJob = QGpgME::smime()->keyListJob();
setHomeDirForJob(keyListJob);
connect(keyListJob, &QGpgME::KeyListJob::result, this, &Controller::slotKeyGripOptained);
keyListJob->start({u"GnuPG Outlook Add-in Local Root CA"_s}, true);
// Export public key
auto exportJob = QGpgME::smime()->publicKeyExportJob(true);
setHomeDirForJob(exportJob);
const auto imports = result.imports();
const auto fingerprint = imports[0].fingerprint();
m_fingerPrint = Formatter::formatX509Fingerprint(QByteArray(fingerprint));
Q_EMIT debutOutput(i18nc("Debug message, %1 is fingerprint", "Root CA created: %1", m_fingerPrint));
exportJob->start({QString::fromLatin1(fingerprint)});
connect(exportJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export public key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_publicCA = keyData;
checkFinished();
});
// Export private key
auto exportSecretJob = QGpgME::smime()->secretKeyExportJob(true);
setHomeDirForJob(exportSecretJob);
exportSecretJob->start({QString::fromLatin1(fingerprint)});
connect(exportSecretJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export secret key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_secretCA = keyData;
checkFinished();
});
}
void Controller::slotKeyGripOptained(const GpgME::KeyListResult &result,
const std::vector<GpgME::Key> &keys,
const QString &auditLogAsHtml,
const GpgME::Error &auditLogError)
{
Q_EMIT debutOutput(i18nc("Debug message", "Got the key grip of Root CA"));
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not get keygrip : %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
if (keys.size() != 1) {
setErrorText(i18n("More than one root certificate found"));
setError(UserDefinedError);
emitResult();
return;
}
m_ca = keys[0];
auto keyGenerationJob = QGpgME::smime()->keyGenerationJob();
setHomeDirForJob(keyGenerationJob);
connect(keyGenerationJob, &QGpgME::KeyGenerationJob::result, this, &Controller::slotCertCreatedSlot);
keyGenerationJob->start(createTlsCertParms(QLatin1StringView(keys[0].subkey(0).keyGrip())).toString());
}
void Controller::slotCertCreatedSlot(const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData, const QString &auditLog)
{
Q_EMIT debutOutput(i18nc("Debug message", "TLS certificate created"));
Q_UNUSED(auditLog)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair for cert: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto importJob = QGpgME::smime()->importJob();
setHomeDirForJob(importJob);
connect(importJob, &QGpgME::ImportJob::result, this, &Controller::slotCertImportedSlot);
importJob->start(pubKeyData);
}
void Controller::slotCertImportedSlot(const GpgME::ImportResult &result, const QString &auditLogAsHtml, const GpgME::Error &auditLogError)
{
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not import cert: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto keyListJob = QGpgME::smime()->keyListJob();
setHomeDirForJob(keyListJob);
connect(keyListJob,
&QGpgME::KeyListJob::result,
this,
[this](const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) {
Q_UNUSED(result);
Q_UNUSED(auditLogAsHtml);
Q_UNUSED(auditLogError);
m_tls = keys[0];
checkFinished();
});
keyListJob->start({u"GnuPG Outlook Add-in Local Root CA"_s}, true);
// Export public key
auto exportJob = QGpgME::smime()->publicKeyExportJob(true);
setHomeDirForJob(exportJob);
const auto imports = result.imports();
const auto fingerprint = imports[0].fingerprint();
exportJob->start({QString::fromLatin1(fingerprint)});
connect(exportJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export public key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_publicTLS = keyData;
checkFinished();
});
// Export private key
auto exportSecretJob = QGpgME::smime()->secretKeyExportJob(true);
setHomeDirForJob(exportSecretJob);
exportSecretJob->start({QString::fromLatin1(fingerprint)});
connect(exportSecretJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export secret key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_secretTLS = keyData;
checkFinished();
});
}
void Controller::checkFinished()
{
if (!m_secretCA.isEmpty() && !m_publicCA.isEmpty() && !m_publicTLS.isEmpty() && !m_secretTLS.isEmpty() && !m_ca.isNull() && !m_tls.isNull()) {
Q_EMIT generationDone();
}
}
void Controller::install()
{
auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem"));
// Install for gpgol-client
{
auto certPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
Q_EMIT debutOutput(i18nc("Debug message, %1 is a path", "Installing certificate for gpgol-client in %1", certPath));
QDir dir;
if (!dir.mkpath(certPath)) {
Q_EMIT debutOutput(i18nc("Debug message, %1 is a path", "Unable to create the following path: ", certPath));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostPub(certPath + u"/certificate.pem"_s);
if (localhostPub.open(QIODeviceBase::WriteOnly)) {
localhostPub.write(m_publicTLS);
localhostPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", localhostPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile rootCaPub(certPath + u"/root-ca.pem"_s);
if (rootCaPub.open(QIODeviceBase::WriteOnly)) {
rootCaPub.write(m_publicCA);
rootCaPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", rootCaPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
}
// Install for gpgol-server
{
auto certPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation).chopped(QStringLiteral(u"gpgol-client").length()).append(u"gpgol-server");
Q_EMIT debutOutput(i18nc("Debug message. %1 is a path", "Installing certificate for gpgol-server in %1", certPath));
QDir dir;
if (!dir.mkpath(certPath)) {
Q_EMIT debutOutput(i18nc("Debug message. %1 is a path", "Unable to create the following path: %1", certPath));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostPub(certPath + u"/certificate.pem"_s);
if (localhostPub.open(QIODeviceBase::WriteOnly)) {
localhostPub.write(m_publicTLS);
localhostPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message, %1 is a filename. %2 is a path", "No permission to write: %1 in %2", localhostPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostKey(certPath + u"/certificate-key.pem"_s);
if (localhostKey.open(QIODeviceBase::WriteOnly)) {
localhostKey.write(m_secretTLS);
localhostKey.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path.", "No permission to write: %1 in %2", localhostKey.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile rootCaPub(certPath + u"/root-ca.pem"_s);
if (rootCaPub.open(QIODeviceBase::WriteOnly)) {
rootCaPub.write(m_publicCA);
rootCaPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", rootCaPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
}
auto trustStore = TrustStore();
if (!trustStore.install(*this)) {
Q_EMIT debutOutput(i18nc("Debug message", "Installing certificate to browser failed"));
}
emitResult();
}
QString Controller::rootFingerprint() const
{
return m_fingerPrint;
}
diff --git a/client/setupdialogs.cpp b/client/setupdialogs.cpp
new file mode 100644
index 0000000..a4109d6
--- /dev/null
+++ b/client/setupdialogs.cpp
@@ -0,0 +1,476 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "setupdialogs.h"
+
+#include "config.h"
+#include "connectioncontroller.h"
+#include "gpgolweb_version.h"
+#include "rootcagenerator/controller.h"
+#include "websocketclient.h"
+
+#include <KAssistantDialog>
+#include <KLocalizedString>
+#include <KTitleWidget>
+
+#include <QApplication>
+#include <QButtonGroup>
+#include <QCheckBox>
+#include <QClipboard>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QDir>
+#include <QDesktopServices>
+#include <QFile>
+#include <QGroupBox>
+#include <QLabel>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QSaveFile>
+#include <QSettings>
+#include <QToolTip>
+#include <QVBoxLayout>
+
+using namespace Qt::StringLiterals;
+
+PairingDialog::PairingDialog(QWidget *parent)
+ : QDialog(parent)
+{
+ setWindowTitle(i18n("Pairing mode active"));
+ auto l = new QVBoxLayout(this);
+ auto lab = new QLabel(i18n("<p>Copy and paste the code shown below into the input field shown at the top of the Add-In:</p>"));
+ lab->setWordWrap(true);
+ l->addWidget(lab);
+ m_pairingTokenLabel = new QLabel(i18nc("status message", "<p align='center'><b>Obtaining token...</b></p>"));
+ l->addWidget(m_pairingTokenLabel);
+ auto bb = new QDialogButtonBox;
+ auto endButton = bb->addButton(i18nc("@button", "End pairing mode"), QDialogButtonBox::RejectRole);
+ connect(endButton, &QAbstractButton::clicked, this, &QDialog::reject);
+ m_copyButton = bb->addButton(i18nc("@button", "Copy code to clipboard"), QDialogButtonBox::ActionRole);
+ m_copyButton->setEnabled(false);
+ l->addWidget(bb);
+ setFixedSize(sizeHint());
+
+ connect(&WebsocketClient::self(), &WebsocketClient::pairingStatusChanged, this, &PairingDialog::pairingStatusChanged);
+ WebsocketClient::self().enterPairingMode();
+}
+
+void PairingDialog::pairingStatusChanged(const QString& token, bool pairingActive) {
+ if (!pairingActive) {
+ reject();
+ return;
+ }
+ m_pairingTokenLabel->setText(QString(u"<p align='center'><b>%1</b></p>").arg(token));
+ m_copyButton->setEnabled(true);
+ connect(m_copyButton, &QAbstractButton::clicked, [this, token]() {
+ qApp->clipboard()->setText(token);
+ });
+}
+
+
+class GenerateCertificateWidget : public QWidget {
+ Q_OBJECT
+public:
+ GenerateCertificateWidget(bool assistant, QWidget *parent)
+ : QWidget(parent)
+ , m_controller(nullptr)
+ , m_installed(false)
+ , m_dialog(nullptr)
+ {
+ auto hbox = new QHBoxLayout(this);
+ hbox->setContentsMargins(0, 0, 0, 0);
+ m_label = new QLabel();
+ hbox->addWidget(m_label);
+ m_generateButton = new QPushButton();
+ m_generateButton->setVisible(!assistant);
+ if (!assistant) {
+ connect(m_generateButton, &QPushButton::clicked, this, [this]() {
+ KAssistantDialog dialog;
+ addAssistantPages(&dialog);
+ auto cancelButton = dialog.button(QDialogButtonBox::Cancel);
+ if (cancelButton) {
+ cancelButton->hide(); // it does not really have defined behavior
+ }
+ startGenerate();
+ dialog.exec();
+ });
+ }
+ hbox->addWidget(m_generateButton);
+
+ updateStatus();
+ }
+
+ void addAssistantPages(KAssistantDialog *dialog) {
+ m_dialog = dialog;
+
+ auto genPage = new QWidget();
+ auto vbox = new QVBoxLayout(genPage);
+ m_genProgress = new QPlainTextEdit();
+ vbox->addWidget(m_genProgress);
+ m_genDoneLabel = new QLabel();
+ vbox->addWidget(m_genDoneLabel);
+ m_genPageItem = new KPageWidgetItem(genPage);
+ m_genPageItem->setHeader(i18n("Generating TLS certificate"));
+ m_dialog->addPage(m_genPageItem);
+
+ auto installPage = new QWidget();
+ vbox = new QVBoxLayout(installPage);
+ m_installProgress = new QPlainTextEdit();
+ vbox->addWidget(m_installProgress);
+ m_installDoneLabel = new QLabel();
+ vbox->addWidget(m_installDoneLabel);
+ m_installPageItem = new KPageWidgetItem(installPage);
+ m_installPageItem->setHeader(i18n("Installing certificate"));
+ m_dialog->addPage(m_installPageItem);
+
+ connect(dialog, &KPageDialog::currentPageChanged, this,
+ [this](KPageWidgetItem *current, KPageWidgetItem *) {
+ if (current == m_genPageItem) {
+ startGenerate();
+ } else if (current == m_installPageItem) {
+ if (!m_installed) {
+ m_controller->install();
+ }
+ }
+ });
+ }
+
+ void updateStatus() {
+ if (Config::self()->isLocalServer()) {
+ setVisible(true);
+ if (Controller::certificateAlreadyGenerated()) {
+ m_label->setText(i18n("A TLS certificate has already been generated."));
+ m_generateButton->setText(i18n("Re-generate certificate"));
+ m_generateButton->setFixedSize(m_generateButton->minimumSizeHint());
+ //m_generateButton->setToolTip(i18n("Re-generates and installs a TLS certificate for a secure connection to the local proxy server. It is not usually necessary to repeat this step."));
+ } else {
+ m_label->setText(i18n("A TLS certificate is needed for the secure connection to the proxy."));
+ m_generateButton->setText(i18n("Generate and install certificate"));
+ if (m_dialog) {
+ m_label->setText(i18n("A TLS certificate is needed for the secure connection to the proxy. This will be generated in the next step."));
+ }
+ }
+ } else {
+ setVisible(false);
+ }
+
+ if (m_dialog) {
+ m_dialog->setAppropriate(m_genPageItem, Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
+ m_dialog->setAppropriate(m_installPageItem, Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
+ }
+ }
+private:
+ QLabel *m_label;
+ QPushButton *m_generateButton;
+ QPlainTextEdit *m_genProgress, *m_installProgress;
+ QLabel *m_genDoneLabel, *m_installDoneLabel;
+ KPageWidgetItem *m_genPageItem, *m_installPageItem;
+ QPointer<Controller> m_controller; // it's a KJob and will auto-delete
+ bool m_installed;
+ KAssistantDialog *m_dialog;
+
+ void startGenerate() {
+ if (m_controller || m_installed) {
+ return;
+ }
+
+ m_dialog->setValid(m_genPageItem, false);
+ m_dialog->setValid(m_installPageItem, false);
+ m_controller = new Controller(this);
+ m_dialog->setValid(m_genPageItem, false);
+ connect(m_controller, &Controller::generationDone, this, [this]() {
+ m_genDoneLabel->setText(i18nc("@info", "Successfully generated certificate with fingerprint %1", m_controller->rootFingerprint()));
+ m_dialog->setValid(m_genPageItem, true);
+ });
+ connect(m_controller, &Controller::debutOutput, this, [this](const QString &output) {
+ m_genProgress->appendPlainText(output);
+ m_installProgress->appendPlainText(output);
+ });
+ connect(m_controller, &Controller::result, this, [this](KJob *) {
+ if (m_controller->error()) {
+ m_genProgress->appendPlainText(m_controller->errorText());
+ m_installProgress->appendPlainText(m_controller->errorText());
+ return;
+ }
+ m_label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
+ m_dialog->setValid(m_installPageItem, true);
+ m_installed = true;
+ });
+ m_controller->start();
+ }
+};
+
+static QByteArray ampersandEncode(const QString &input)
+{
+ QByteArray encoded;
+ for(int i = 0; i < input.size(); ++i) {
+ QChar ch = input.at(i);
+ if(ch.unicode() > 127) {
+ encoded += QString(u"&#%1;"_s).arg(static_cast<int>(ch.unicode())).toLatin1();
+ } else {
+ encoded += ch.toLatin1();
+ }
+ }
+ return encoded;
+}
+
+void DialogController::doDialog(const QList<PageID> &pageIds, const bool assistant, QWidget *parent) {
+ KPageDialog *dialog = assistant ? new KAssistantDialog(parent) : new KPageDialog(parent);
+ auto cancelButton = dialog->button(QDialogButtonBox::Cancel);
+ if (cancelButton) {
+ cancelButton->hide(); // it does not really have defined behavior
+ }
+
+ if (pageIds.contains(PageProxy)) {
+ auto widget = new QWidget();
+ auto vbox = new QVBoxLayout(widget);
+ auto item = new KPageWidgetItem(widget, i18n("Proxy"));
+ item->setHeader(i18nc("@title", "Configure Proxy"));
+ dialog->addPage(item);
+
+ auto label = new QLabel(i18n("Choose your configuration for the proxy component:"));
+ vbox->addWidget(label);
+ label = new QLabel(i18n("Note: Changes to this setting only take effect after uploading the adjusted manifest file to Outlook on the next page!"));
+ auto font = label->font();
+ font.setItalic(true);
+ label->setFont(font);
+ label->setWordWrap(true);
+ vbox->addWidget(label);
+
+ auto certcontrol = new GenerateCertificateWidget(assistant, nullptr);
+ if (assistant) {
+ certcontrol->addAssistantPages(static_cast<KAssistantDialog*>(dialog));
+ }
+
+ auto grp = new QButtonGroup(widget);
+ auto localOption = new QRadioButton(i18n("Run a local proxy on this machine"));
+ localOption->setChecked(Config::self()->isLocalServer());
+ auto remoteOption = new QRadioButton(i18n("Use proxy from a remote server (EXPERIMENTAL)"));
+ remoteOption->setChecked(!Config::self()->isLocalServer());
+ grp->addButton(localOption);
+ vbox->addWidget(localOption);
+ grp->addButton(remoteOption);
+ vbox->addWidget(remoteOption);
+
+ auto hbox = new QHBoxLayout();
+ auto remoteLabel = new QLabel(i18n("Remote proxy server"));
+ hbox->addWidget(remoteLabel);
+ auto remoteServer = new QLineEdit();
+ remoteServer->setPlaceholderText(u"internal.company.com"_s);
+ remoteServer->setClearButtonEnabled(true);
+ hbox->addWidget(remoteServer);
+ vbox->addLayout(hbox);
+
+ QObject::connect(remoteOption, &QRadioButton::toggled, dialog, [remoteLabel, remoteServer, certcontrol](bool checked) {
+ Config::self()->setIsLocalServer(!checked);
+ Config::self()->save();
+
+ remoteLabel->setEnabled(!Config::self()->isLocalServer());
+ remoteServer->setEnabled(!Config::self()->isLocalServer());
+ certcontrol->updateStatus();
+ ConnectionController::instance()->startStopLocalServer();
+ });
+ QObject::connect(remoteServer, &QLineEdit::textChanged, dialog, [remoteServer]() {
+ Config::self()->setRemoteAddress(QUrl::fromUserInput(remoteServer->text()));
+ Config::self()->save();
+ ConnectionController::instance()->startWebsocketClient();
+ });
+ vbox->addWidget(certcontrol);
+ vbox->addStretch();
+ }
+
+ if (pageIds.contains(PageInstallAddin)) {
+ auto widget = new QWidget();
+ auto vbox = new QVBoxLayout(widget);
+ auto item = new KPageWidgetItem(widget);
+ item->setHeader(i18nc("@title", "Install Outlook Add-In"));
+ dialog->addPage(item);
+
+ auto grid = new QGridLayout();
+ vbox->addLayout(grid);
+ auto addGridRow = [](QGridLayout *grid, bool number, const QString &label, QWidget *button) {
+ const int row = grid->rowCount();
+ if (number) {
+ auto num = new QLabel(u"%1."_s.arg(row-1));
+ grid->addWidget(num, row, 0, Qt::AlignTop);
+ }
+ auto lbl = new QLabel(label);
+ lbl->setWordWrap(true);
+ grid->addWidget(lbl, row, 0 + number, 1, 3 - number - (button != nullptr), Qt::AlignTop);
+ if (button) {
+ grid->addWidget(button, row, 2, Qt::AlignTop);
+ }
+ };
+
+ addGridRow(grid, false, i18n("Before the first use, the add-in has to be activated in Outlook:"), nullptr);
+ auto extMgrButton = new QPushButton(i18nc("@button", "Outlook Extension Manager"));
+ extMgrButton->setToolTip(i18n("Click to open the Outlook Extension Manager in your web browser. You may be prompted to log in. Please allow a few seconds for the page to load."));
+ extMgrButton->setIcon(QIcon::fromTheme(u"external-link-symbolic"_s));
+ addGridRow(grid, true, i18n("Open the Outlook Extension Manager (you may be prompted to log in):"), extMgrButton);
+ auto generateManifestButton = new QPushButton(i18nc("@button", "Generate Manifest"));
+ addGridRow(grid, true, i18n("Generate a manifest file (the filename will be copied to the clipboard):"), generateManifestButton);
+ addGridRow(grid, true, i18n("In Outlook, register this via <tt>My Add-Ins -> Custom Add-Ins -> Add a custom Add-In</tt>"), nullptr);
+ addGridRow(grid, true, i18n("In Outlook, select any e-mail, and activate the add-in by clicking the GnuPG icon <nobr>( %1 )</nobr> shown above the email header.",
+ u"<img src=':/icons/addin_logo.png' height='%1'>"_s.arg(QFontMetrics(dialog->font()).ascent())), nullptr);
+
+ QObject::connect(extMgrButton, &QPushButton::clicked, dialog, []() {
+ QDesktopServices::openUrl(QUrl(u"https://outlook.office.com/mail/jsmvvmdeeplink/?path=/options/manageapps&amp;bO=4"_s));
+ });
+
+ QObject::connect(generateManifestButton, &QPushButton::clicked, dialog, [generateManifestButton]() {
+ QFile file(u":/gpgol-client/manifest.xml.in"_s);
+ if (!file.open(QIODeviceBase::ReadOnly)) {
+ Q_ASSERT(false);
+ return;
+ }
+ QByteArray manifest = file.readAll();
+ manifest.replace("%HOST%", ConnectionController::serverDomain().toUtf8());
+ manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
+ // HACK: For a manifest loaded from local file - as single users will do - MS does not apply translations.
+ // They claim it's a feature, not a bug. To work around this, we localize the default value, here, instead.
+ // At the same time, we also have to keep translations in the manifest, so as not to break translations
+ // for origanization-installed manifests.
+ int offset = 0;
+ const auto attrib = QByteArray("DefaultValue=\"");
+ while ((offset = manifest.indexOf("GPGOLI18N=\"true\"", offset)) > -1) {
+ int strBegin = manifest.indexOf(attrib, offset) + attrib.length();
+ Q_ASSERT(strBegin > attrib.length());
+ int strEnd = manifest.indexOf("\"", strBegin);
+ Q_ASSERT(strEnd > 0);
+ const auto translation = ki18nd("gpgol-js-manifest", manifest.mid(strBegin, strEnd-strBegin).constData()).toString();
+ manifest.replace(offset, strEnd-offset, QByteArray(attrib + ampersandEncode(translation).constData()));
+ }
+ const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
+
+ QSaveFile saveFile(saveFilePath);
+ if (!saveFile.open(QIODeviceBase::WriteOnly)) {
+ Q_ASSERT(false);
+ return;
+ }
+ saveFile.write(manifest);
+ saveFile.commit();
+
+ QGuiApplication::clipboard()->setText(saveFilePath);
+ QToolTip::showText(generateManifestButton->mapToGlobal(QPoint(10, 10)), i18n("Copied to clipboard."), generateManifestButton, QRect(), 2000);
+ });
+
+ //if (!Config::isLocalServer()) {
+ auto pairingButton = new QPushButton(i18nc("@button", "Enter Pairing Mode"));
+ pairingButton->setIcon(QIcon::fromTheme(u"network-connect"_s));
+ addGridRow(grid, true, i18n("When prompted for a pairing code, click here to enter pairing mode:"), pairingButton);
+ QObject::connect(pairingButton, &QPushButton::clicked, dialog, [dialog]() {
+ PairingDialog d(dialog);
+ d.exec();
+ WebsocketClient::self().quitPairingMode();
+ });
+ //}
+
+ auto title = new KTitleWidget();
+ title->setText(i18n("Troubleshooting"));
+ grid->addWidget(title, grid->rowCount(), 0, 1, 3);
+
+ //grid->addWidget(makeLabel(i18n("If the extension is not connected:")), 0, 0, 1, 2);
+ auto testPageButton = new QPushButton(i18nc("@button", "Open Test Page"));
+ testPageButton->setIcon(QIcon::fromTheme(u"external-link-symbolic"_s));
+ addGridRow(grid, false, i18n("Test for problems with the TLS-certificate installation, by opening this test page in your browser:"), testPageButton);
+/* grid->addWidget(makeLabel(i18n("Sometimes the add-in icon is not immediately visible in Outlook's menu ribbon. Make sure to select an existing message in Outlook. "
+ "You may also have to click on the \"Apps\" icon.")), 2, 0, 1, 2);
+ grid->addWidget(makeLabel(i18n("Once you see the add-in, you may want to \"pin\" it for easier access.")), 3, 0, 1, 2);
+ grid->addWidget(makeLabel(i18n("If you have just added the manifest, it may be necessary to reload / restart Outlook.")), 4, 0, 1, 2);
+ grid->addWidget(makeLabel(i18n("If your account is organization managed, your administrator may have to allow usage of the GPGOL/Web add-in, manually.")), 5, 0, 1, 2); */
+ QObject::connect(testPageButton, &QPushButton::clicked, dialog, []() {
+ QDesktopServices::openUrl(QUrl(u"https://"_s + ConnectionController::serverDomain() + u"/test"_s));
+ });
+
+ grid->setColumnStretch(1, 2);
+ }
+
+ if (pageIds.contains(PageSettings)) {
+ auto widget = new QWidget();
+ auto vbox = new QVBoxLayout(widget);
+ auto featuresbox = new QGroupBox(i18n("Optional features"));
+ auto boxlayout = new QVBoxLayout(featuresbox);
+ auto reencrypt = new QCheckBox(i18n("Reencrypt email folders with new keys"), featuresbox);
+ reencrypt->setChecked(Config::self()->reencrypt());
+ QObject::connect(reencrypt, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
+ Config::self()->setReencrypt(state == Qt::Checked);
+ Config::self()->save();
+ });
+ boxlayout->addWidget(reencrypt);
+ vbox->addWidget(featuresbox);
+
+ auto startupbox = new QGroupBox(i18n("Startup behavior"));
+ boxlayout = new QVBoxLayout(startupbox);
+
+#ifdef Q_OS_WIN
+ auto autoStartBox = new QCheckBox(i18n("Start GPGOL/Web automatically"));
+ // We intentionally don't use our own config for this: If users disable autostart via
+ // the Windows settings menu, we want to respect that, too.
+ {
+ QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
+ autoStartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
+ }
+ QObject::connect(autoStartBox, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
+ QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
+ if (state) {
+ winreg.setValue(QCoreApplication::applicationName(),
+ QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
+ } else {
+ winreg.remove(QCoreApplication::applicationName());
+ }
+ });
+ boxlayout->addWidget(autoStartBox);
+#endif
+
+ auto showOnStartup = new QCheckBox(i18n("Show status dialog when starting"));
+ showOnStartup->setChecked(Config::self()->showLauncher());
+ QObject::connect(showOnStartup, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
+ Config::self()->setShowLauncher(state == Qt::Checked);
+ Config::self()->save();
+ });
+ boxlayout->addWidget(showOnStartup);
+ vbox->addWidget(startupbox);
+
+ vbox->addStretch();
+ auto item = new KPageWidgetItem(widget);
+ item->setHeader(i18nc("@title", "Options"));
+ dialog->addPage(item);
+ }
+
+ dialog->exec();
+}
+
+void DialogController::doFirstTimeAssistant()
+{
+ doDialog(QList{PageProxy, PageInstallAddin, PageSettings}, true);
+}
+
+#ifdef Q_OS_WIN
+#include <windows.h>
+#endif
+
+void DialogController::strongActivateWindow(QWidget *window)
+{
+ window->show();
+ window->activateWindow();
+ window->raise();
+#ifdef Q_OS_WIN
+ // HACK: Simulate Alt-keyPress while bringing the window to the front.
+ // This helps when our app does not currently have focus - and
+ // frequently it does not, because we have just clicked in browser/outlook.
+ // https://stackoverflow.com/questions/72620538/whats-the-correct-way-to-bring-a-window-to-the-front
+ keybd_event(VK_MENU, 0, 0, 0);
+ auto hwnd = HWND(window->winId());
+ SetForegroundWindow(hwnd);
+ keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
+ // Sometimes Qt may act as if Alt was still pressed, after the above sequence.
+ // Send it another key release event, explicitly.
+ auto e = new QKeyEvent(QEvent::KeyRelease, Qt::Key_Alt, Qt::AltModifier);
+ qApp->postEvent(window, e);
+#endif
+}
+
+#include "setupdialogs.moc"
diff --git a/client/setupdialogs.h b/client/setupdialogs.h
new file mode 100644
index 0000000..c9d8ab0
--- /dev/null
+++ b/client/setupdialogs.h
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QDialog>
+
+class QLabel;
+class QAbstractButton;
+
+class DialogController {
+public:
+ enum PageID {
+ PageProxy,
+ PageInstallAddin,
+ PageSettings,
+ };
+ // Encapsulate the various setup/config pages. These may be shown one by one
+ // or sequentailly (in an assistant dialog).
+ static void doDialog(const QList<PageID> &pageIds, const bool assistant=false);
+ static void doFirstTimeAssistant();
+ static void strongActivateWindow(QWidget *window);
+};
+
+class PairingDialog : public QDialog {
+public:
+ PairingDialog(QWidget *parent);
+ void pairingStatusChanged(const QString& token, bool pairingActive);
+private:
+ QLabel* m_pairingTokenLabel;
+ QAbstractButton* m_copyButton;
+};
diff --git a/client/statusdialog.cpp b/client/statusdialog.cpp
new file mode 100644
index 0000000..d5f1ed6
--- /dev/null
+++ b/client/statusdialog.cpp
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "statusdialog.h"
+
+#include "config.h"
+#include "connectioncontroller.h"
+#include "gpgolweb_version.h"
+#include "setupdialogs.h"
+#include "websocketclient.h"
+
+#include <KColorScheme>
+#include <KLocalizedString>
+#include <KTitleWidget>
+
+#include <Libkleo/Compliance>
+
+#include <QApplication>
+#include <QFrame>
+#include <QGroupBox>
+#include <QIcon>
+#include <QLabel>
+#include <QPushButton>
+#include <QStatusBar>
+#include <QVBoxLayout>
+
+using namespace Qt::StringLiterals;
+
+QPointer<StatusDialog> StatusDialog::instance;
+
+StatusDialog *StatusDialog::getOrCreate(QWidget *parent)
+{
+ if (!instance) {
+ instance = new StatusDialog(parent);
+ }
+ return instance;
+}
+
+StatusDialog::StatusDialog(QWidget *parent)
+ : QMainWindow(parent)
+{
+ setAttribute(Qt::WA_DeleteOnClose);
+ auto central = new QWidget();
+ setCentralWidget(central);
+
+ auto vbox = new QVBoxLayout(central);
+ auto appicon = new QLabel();
+ appicon->setPixmap(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s).pixmap(64, 64));
+ vbox->addWidget(appicon);
+ vbox->setAlignment(appicon, Qt::AlignCenter);
+ auto title = new KTitleWidget();
+ title->setText(i18nc("@info", "GpgOL/Web %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
+ vbox->addWidget(title);
+ vbox->setAlignment(title, Qt::AlignCenter);
+ auto label = new QLabel(i18n("The GnuPG Add-in for Outlook"));
+ vbox->addWidget(label);
+ vbox->setAlignment(label, Qt::AlignCenter);
+
+ auto statusGroup = new QGroupBox(i18n("Connection status:"));
+ auto grid = new QGridLayout(statusGroup);
+ m_proxyProcessLabel = new QLabel();
+ grid->addWidget(m_proxyProcessLabel, 0, 0);
+ m_proxyConnectionLabel = new QLabel();
+ grid->addWidget(m_proxyConnectionLabel, 1, 0);
+ auto connectionButton = new QPushButton(i18n("Setup connection"));
+ connectionButton->setIcon(QIcon::fromTheme(u"applications-network"_s));
+ grid->addWidget(connectionButton, 1, 1);
+ m_clientConnectionLabel = new QLabel();
+ grid->addWidget(m_clientConnectionLabel, 2, 0);
+ auto extensionButton = new QPushButton(i18n("Setup extension"));
+ extensionButton->setIcon(QIcon::fromTheme(u"extension-symbolic"_s));
+ grid->addWidget(extensionButton, 2, 1);
+ grid->setColumnStretch(0, 2);
+
+ vbox->addStretch();
+ vbox->addWidget(statusGroup);
+ vbox->addStretch();
+
+ auto hbox = new QHBoxLayout();
+ auto settingsButton = new QPushButton(i18n("General Settings"));
+ settingsButton->setIcon(QIcon::fromTheme(u"configure-symbolic"_s));
+ hbox->addWidget(settingsButton);
+ hbox->addStretch();
+ auto minimizeButton = new QPushButton(i18n("Minimize to tray"));
+ connect(minimizeButton, &QPushButton::clicked, this, &QObject::deleteLater);
+ hbox->addWidget(minimizeButton);
+ vbox->addLayout(hbox);
+
+ auto statusBar = new QStatusBar(this);
+ m_status = new QLabel;
+ statusBar->addPermanentWidget(m_status);
+
+ auto version = new QLabel(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
+ statusBar->addPermanentWidget(version);
+ if (Kleo::DeVSCompliance::isActive()) {
+ auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
+ {
+ auto statusPalette = qApp->palette();
+ KColorScheme::adjustForeground(statusPalette,
+ Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
+ statusLbl->foregroundRole(),
+ KColorScheme::View);
+ statusLbl->setAutoFillBackground(true);
+ KColorScheme::adjustBackground(statusPalette,
+ Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
+ QPalette::Window,
+ KColorScheme::View);
+ statusLbl->setPalette(statusPalette);
+ }
+ statusBar->addPermanentWidget(statusLbl.release());
+ }
+ setStatusBar(statusBar);
+
+ connect(&WebsocketClient::self(), &WebsocketClient::stateChanged, this, &StatusDialog::stateChanged);
+ connect(ConnectionController::instance(), &ConnectionController::serverProcessStatusChanged, this, &StatusDialog::stateChanged);
+ stateChanged();
+
+ connect(connectionButton, &QPushButton::clicked, this, [this]() {
+ DialogController::doDialog(QList{ DialogController::PageProxy });
+ });
+ connect(extensionButton, &QPushButton::clicked, this, [this]() {
+ DialogController::doDialog(QList{ DialogController::PageInstallAddin });
+ });
+ connect(settingsButton, &QPushButton::clicked, this, [this]() {
+ DialogController::doDialog(QList{ DialogController::PageSettings });
+ });
+}
+
+#define ICON_OK u"<span style=\"color:green\">&#10004;</span> "_s
+#define ICON_WARN u"<span style=\"color:orange\">&#9888;</span> "_s
+#define ICON_ERROR u"<span style=\"color:red\">&#10060;</span> "_s
+void StatusDialog::stateChanged()
+{
+ auto serverState = ConnectionController::instance()->serverProcessState();
+ if (serverState == ConnectionController::RemoteServer) {
+ m_proxyProcessLabel->setText(i18n("Configured to connect to remote proxy %1", ConnectionController::serverDomain()));
+ } else if (serverState == ConnectionController::LocalServerRunning) {
+ m_proxyProcessLabel->setText(ICON_OK + i18n("Proxy process is running"));
+ } else if (serverState == ConnectionController::LocalServerStarting) {
+ m_proxyProcessLabel->setText(ICON_WARN + i18n("Proxy process is starting"));
+ } else {
+ m_proxyProcessLabel->setText(ICON_ERROR + i18n("Proxy process failed. Attempting restart..."));
+ }
+
+ auto state = WebsocketClient::self().state();
+ if (state == WebsocketClient::ConnectedToWebclient || state == WebsocketClient::ConnectedToProxy) {
+ m_proxyConnectionLabel->setText(ICON_OK + i18n("Proxy is connected"));
+ } else {
+ m_proxyConnectionLabel->setText(ICON_ERROR + i18n("Proxy is not connected"));
+ }
+ if (state == WebsocketClient::ConnectedToWebclient) {
+ m_clientConnectionLabel->setText(ICON_OK + i18n("Web extension is connected, and opened"));
+ } else {
+ m_clientConnectionLabel->setText(ICON_WARN + i18n("Web extension is not connected, or not opened"));
+ }
+}
diff --git a/client/statusdialog.h b/client/statusdialog.h
new file mode 100644
index 0000000..c2d6b59
--- /dev/null
+++ b/client/statusdialog.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2026 g10 code Gmbh
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QMainWindow>
+
+class QLabel;
+
+class StatusDialog : public QMainWindow {
+public:
+ static StatusDialog *getOrCreate(QWidget *parent = nullptr);
+private:
+ StatusDialog(QWidget *parent = nullptr);
+ void stateChanged();
+ QLabel* m_proxyProcessLabel;
+ QLabel* m_proxyConnectionLabel;
+ QLabel* m_clientConnectionLabel;
+ QLabel* m_status;
+ static QPointer<StatusDialog> instance;
+};
diff --git a/client/utils/systemtrayicon.cpp b/client/utils/systemtrayicon.cpp
index ee6f8ea..f4eb470 100644
--- a/client/utils/systemtrayicon.cpp
+++ b/client/utils/systemtrayicon.cpp
@@ -1,102 +1,99 @@
// SPDX-FileCopyrightText: 2007, 2009 Klarälvdalens Datakonsult AB
// SPDX-License-Identifier: GPL-2.0-or-later
#include "systemtrayicon.h"
#include "gpgol_client_debug.h"
+#include "../connectioncontroller.h"
+#include "../statusdialog.h"
#include "../websocketclient.h"
#include <QCoreApplication>
#include <QEvent>
#include <QMenu>
#include <QPointer>
#include <QWidget>
#include <KAboutApplicationDialog>
#include <KAboutData>
#include <KIconUtils>
#include <KLocalizedString>
using namespace Qt::StringLiterals;
SystemTrayIcon::SystemTrayIcon(const QIcon &baseicon, QObject *p)
: QSystemTrayIcon(baseicon, p)
, m_aboutAction(QIcon::fromTheme(QStringLiteral("com.gnupg.gpgolweb")),
xi18nc("@action:inmenu", "&About <application>%1</application>...", KAboutData::applicationData().displayName()),
this)
, m_quitAction(QIcon::fromTheme(QStringLiteral("application-exit")),
xi18nc("@action:inmenu", "&Shutdown <application>%1</application>", KAboutData::applicationData().displayName()),
this)
, m_baseIcon(baseicon)
, m_previousState(-1)
{
connect(&m_aboutAction, &QAction::triggered, this, [this]() {
if (!m_aboutDialog) {
m_aboutDialog = new KAboutApplicationDialog(KAboutData::applicationData());
m_aboutDialog->setAttribute(Qt::WA_DeleteOnClose);
}
if (m_aboutDialog->isVisible()) {
m_aboutDialog->raise();
} else {
m_aboutDialog->show();
}
});
connect(&m_quitAction, &QAction::triggered, QCoreApplication::instance(), &QCoreApplication::quit);
connect(this, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
- Q_ASSERT(m_mainWindow);
if (reason == QSystemTrayIcon::Context) {
return;
}
- if (m_mainWindow->isVisible()) {
- m_mainWindow->hide();
- } else {
- m_mainWindow->show();
- m_mainWindow->raise();
- }
+ auto dialog = StatusDialog::getOrCreate();
+ dialog->show();
+ dialog->raise();
});
m_menu.addAction(&m_aboutAction);
m_menu.addAction(&m_quitAction);
setContextMenu(&m_menu);
m_attentionIcon = KIconUtils::addOverlay(m_baseIcon, QIcon::fromTheme(u"emblem-warning"_s), Qt::BottomLeftCorner);
m_errorIcon = KIconUtils::addOverlay(m_baseIcon, QIcon::fromTheme(u"emblem-error"_s), Qt::BottomLeftCorner);
// add a short timeout, when showing status change messages, such as not to spam the user with "not connnected", "connected to proxy", "connected to client"
m_messageTimer.setSingleShot(true);
connect(&m_messageTimer, &QTimer::timeout, this, [this]() {
showMessage(KAboutData::applicationData().displayName(), toolTip(), icon(), 3000);
});
+
+ connect(&WebsocketClient::self(), &WebsocketClient::stateChanged, this, [this](const QString &msg) {
+ stateChanged(msg, WebsocketClient::self().state());
+ });
}
SystemTrayIcon::~SystemTrayIcon() = default;
-void SystemTrayIcon::setMainWindow(QWidget *mw)
-{
- m_mainWindow = mw;
-}
-
void SystemTrayIcon::stateChanged(const QString &msg, int state)
{
setToolTip(msg);
if (state != m_previousState) {
auto icon = m_baseIcon;
if (state == WebsocketClient::NotConnected) {
icon = m_errorIcon;
} else if (state != WebsocketClient::ConnectedToWebclient) {
icon = m_attentionIcon;
}
setIcon(icon);
- if (state == WebsocketClient::ConnectedToProxy && m_previousState == WebsocketClient::ConnectedToWebclient ||
- m_previousState == WebsocketClient::ConnectedToProxy && state == WebsocketClient::ConnectedToWebclient) {
+ if ((state == WebsocketClient::ConnectedToProxy && m_previousState == WebsocketClient::ConnectedToWebclient) ||
+ (m_previousState == WebsocketClient::ConnectedToProxy && state == WebsocketClient::ConnectedToWebclient)) {
// Losing connection to web client is not an error per se. The user may have simply closed the pane.
// So don't generate a popup for this. (And neither for the reverse)
} else {
m_messageTimer.start(500);
}
m_previousState = state;
}
}
#include "moc_systemtrayicon.cpp"
diff --git a/client/utils/systemtrayicon.h b/client/utils/systemtrayicon.h
index e51756e..dfe8480 100644
--- a/client/utils/systemtrayicon.h
+++ b/client/utils/systemtrayicon.h
@@ -1,35 +1,33 @@
// SPDX-FileCopyrightText: 2007, 2009 Klarälvdalens Datakonsult AB
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAction>
#include <QMenu>
#include <QPointer>
#include <QSystemTrayIcon>
#include <QTimer>
class KAboutApplicationDialog;
class SystemTrayIcon : public QSystemTrayIcon
{
Q_OBJECT
public:
explicit SystemTrayIcon(const QIcon &icon, QObject *parent = nullptr);
~SystemTrayIcon() override;
- void setMainWindow(QWidget *w);
void stateChanged(const QString &msg, int state);
private:
QAction m_aboutAction;
QAction m_quitAction;
QIcon m_baseIcon;
QIcon m_attentionIcon;
QIcon m_errorIcon;
QMenu m_menu;
KAboutApplicationDialog *m_aboutDialog = nullptr;
- QPointer<QWidget> m_mainWindow;
QTimer m_messageTimer;
int m_previousState;
};
diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp
index 9e88a0c..7d78799 100644
--- a/client/websocketclient.cpp
+++ b/client/websocketclient.cpp
@@ -1,467 +1,480 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "websocketclient.h"
// Qt headers
#include <QFile>
#include <QHostInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
+#include <QTimer>
#include <QUuid>
// KDE headers
#include <KConfigGroup>
#include <KLocalizedString>
#include <KMime/Message>
#include <KSharedConfig>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <MimeTreeParserCore/ObjectTreeParser>
// gpgme headers
#include <QGpgME/KeyListJob>
#include <QGpgME/Protocol>
#include <gpgme++/global.h>
#include <gpgme++/key.h>
#include <gpgme++/keylistresult.h>
#include "config.h"
#include "draft/draftmanager.h"
#include "editor/composerwindow.h"
#include "editor/composerwindowfactory.h"
#include "emailviewer.h"
-#include "firsttimedialog.h"
#include "gpgol_client_debug.h"
#include "gpgolweb_version.h"
#include "mailapi.h"
#include "protocol.h"
#include "reencrypt/reencryptjob.h"
+#include "setupdialogs.h"
#include "websocket_debug.h"
using namespace Qt::Literals::StringLiterals;
-WebsocketClient &WebsocketClient::self(const QUrl &url)
+WebsocketClient &WebsocketClient::self()
{
static WebsocketClient *client = nullptr;
- if (!client && url.isEmpty()) {
- qFatal() << "Unable to create a client without an url";
- } else if (!client) {
- client = new WebsocketClient(url);
+ if (!client) {
+ client = new WebsocketClient();
}
return *client;
};
-WebsocketClient::WebsocketClient(const QUrl &url)
+WebsocketClient::WebsocketClient()
: QObject(nullptr)
, m_webSocket(QWebSocket(QStringLiteral("Client")))
- , m_url(url)
, m_state(NotConnected)
, m_stateDisplay(i18nc("@info", "Loading..."))
{
auto job = QGpgME::openpgp()->keyListJob();
connect(job, &QGpgME::KeyListJob::result, this, &WebsocketClient::slotKeyListingDone);
job->start({}, true);
qCWarning(GPGOL_CLIENT_LOG) << "Found the following trusted emails" << m_emails;
connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected);
connect(&m_webSocket, &QWebSocket::disconnected, this, [this] {
m_state = NotConnected;
m_stateDisplay = i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.");
Q_EMIT stateChanged(m_stateDisplay);
});
connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred);
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived);
connect(&m_webSocket, QOverload<const QList<QSslError> &>::of(&QWebSocket::sslErrors), this, [this](const QList<QSslError> &errors) {
// TODO remove
m_webSocket.ignoreSslErrors(errors);
});
QSslConfiguration sslConfiguration;
auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
Q_ASSERT(!certPath.isEmpty());
QFile certFile(certPath);
if (!certFile.open(QIODevice::ReadOnly)) {
qFatal() << "Couldn't read certificate" << certPath;
}
QSslCertificate certificate(&certFile, QSsl::Pem);
certFile.close();
sslConfiguration.addCaCertificate(certificate);
m_webSocket.setSslConfiguration(sslConfiguration);
}
void WebsocketClient::slotKeyListingDone(const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys, const QString &, const GpgME::Error &error)
{
Q_UNUSED(result);
Q_UNUSED(error);
if (error) {
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged(m_stateDisplay);
return;
}
QStringList oldEmails = m_emails;
for (const auto &key : keys) {
for (const auto &userId : key.userIDs()) {
const auto email = QString::fromLatin1(userId.email()).toLower();
if (!m_emails.contains(email)) {
m_emails << email;
}
}
}
if (m_emails == oldEmails) {
return;
}
qCWarning(GPGOL_CLIENT_LOG) << "Found the following trusted emails" << m_emails;
if (m_webSocket.state() == QAbstractSocket::ConnectedState) {
slotConnected();
}
}
-void WebsocketClient::connectToProxy(const QString &clientSecret)
-{
- if (!clientSecret.isEmpty()) {
- m_clientSecret = clientSecret;
- }
- if (m_webSocket.state() != QAbstractSocket::ConnectedState && m_webSocket.state() != QAbstractSocket::ConnectingState) {
- m_webSocket.open(m_url);
- }
-}
-
void WebsocketClient::disconnectFromProxy()
{
- m_webSocket.close();
+ m_webSocket.abort();
}
bool WebsocketClient::connectedToProxy() const
{
return m_webSocket.state() != QAbstractSocket::UnconnectedState;
}
void WebsocketClient::slotConnected()
{
qCInfo(WEBSOCKET_LOG) << "websocket connected";
sendCommand(Protocol::Register, QJsonObject{
{"emails"_L1, QJsonArray::fromStringList(m_emails)}, // TODO: keep this?
{"type"_L1, "native"_L1},
{"name"_L1, QString(QHostInfo::localHostName() + u" - GpgOL/Web ("_s + QStringLiteral(GPGOLWEB_VERSION_STRING) + u')') }, // TODO: unused
{"secret"_L1, m_clientSecret},
});
sendStatusUpdate(); // in case web client was started before native client
m_state = ConnectedToProxy; /// We still need to connect to the web client
m_stateDisplay = i18nc("@info", "Waiting for web client.");
Q_EMIT stateChanged(m_stateDisplay);
}
void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error)
{
qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString();
m_state = (m_webSocket.state() == QAbstractSocket::ConnectedState) ? ConnectedToProxy : NotConnected;
m_stateDisplay = i18nc("@info", "Could not reach the Outlook extension.");
Q_EMIT stateChanged(m_stateDisplay);
+ QTimer::singleShot(1000, this, [this]() {
+ if (m_webSocket.state() != QAbstractSocket::ConnectedState) {
+ connectToProxy(m_url);
+ }
+ });
}
void WebsocketClient::enterPairingMode()
{
sendCommand(Protocol::PairingRequest, QJsonObject{
{"type"_L1, "native-start-pairing"_L1},
});
}
void WebsocketClient::quitPairingMode()
{
sendCommand(Protocol::PairingRequest, QJsonObject{
{"type"_L1, "native-end-pairing"_L1},
});
}
bool WebsocketClient::sendEWSRequest(const QString &fromEmail, const QString &requestId, const QString &requestBody)
{
KMime::Types::Mailbox mailbox;
mailbox.fromUnicodeString(fromEmail);
sendCommand(Protocol::Ews, QJsonObject{
{"body"_L1, requestBody},
{"email"_L1, QString::fromUtf8(mailbox.address())},
{"requestId"_L1, requestId}
});
return true;
}
// TODO: We should really centralize all calls to this as a single call in the connection stage. Afterwards
// the webclient will not send a new token, anyway.
// However, fixing this is currently on hold pending changes in the pairing process,
// so for now, calls to this are littered all around various requests, but at least we can indentify them
// easily as calls to this function.
void WebsocketClient::initMailApiFromArgs(const QJsonObject &args)
{
MailApiController::init(
(args[u"api"_s].toString() == u"ews"_s) ? MailApiController::EWSApi : MailApiController::GraphApi,
args[u"apiendpoint"_s].toString(),
args[u"ewsAccessToken"_s].toString()
);
}
void WebsocketClient::slotTextMessageReceived(QString message)
{
const auto doc = QJsonDocument::fromJson(message.toUtf8());
if (!doc.isObject()) {
qCWarning(WEBSOCKET_LOG) << "invalid text message received" << message;
return;
}
const auto object = doc.object();
const auto command = Protocol::commandFromString(object["command"_L1].toString());
const auto args = object["arguments"_L1].toObject();
switch (command) {
case Protocol::Disconnection:
// disconnection of the web client
m_state = ConnectedToProxy;
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged(m_stateDisplay);
// TODO: handle multiple clients
return;
case Protocol::PairingRequest: {
const auto token = args["token"_L1].toString();
if (token.isEmpty()) {
Q_EMIT pairingStatusChanged(QString(), false);
qCWarning(GPGOL_CLIENT_LOG) << "Pairing complete";
} else {
Q_EMIT pairingStatusChanged(token, true);
}
return;
}
case Protocol::Connection:
// reconnection of the web client
m_state = ConnectedToWebclient;
m_stateDisplay = i18nc("@info", "Connected.");
Q_EMIT stateChanged(m_stateDisplay);
sendStatusUpdate();
return;
case Protocol::View: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto id = args["itemId"_L1].toString();
const auto content = m_cachedMime[id];
if (content.isEmpty()) {
return;
}
initMailApiFromArgs(args);
if (!m_emailViewer) {
m_emailViewer = new EmailViewer(QString::fromUtf8(content), email, displayName);
m_emailViewer->setAttribute(Qt::WA_DeleteOnClose);
} else {
m_emailViewer->view(QString::fromUtf8(content), email, displayName);
}
- FirstTimeDialog::strongActivateWindow(m_emailViewer);
+ DialogController::strongActivateWindow(m_emailViewer);
return;
}
case Protocol::RestoreAutosave: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
initMailApiFromArgs(args);
ComposerWindowFactory::self().restoreAutosave(email, displayName);
return;
}
case Protocol::EwsResponse: {
// confirmation that the email was sent
const auto args = object["arguments"_L1].toObject();
Q_EMIT ewsResponseReceived(args["requestId"_L1].toString(), args["body"_L1].toString());
return;
}
case Protocol::Composer:
case Protocol::Reply:
case Protocol::Forward:
case Protocol::OpenDraft: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
initMailApiFromArgs(args);
auto dialog = ComposerWindowFactory::self().create(email, displayName);
if (command == Protocol::Reply || command == Protocol::Forward) {
const auto id = args["itemId"_L1].toString();
const auto content = m_cachedMime[id];
if (content.isEmpty()) {
return;
}
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(content));
message->parse();
if (command == Protocol::Reply) {
dialog->reply(message);
} else {
dialog->forward(message);
}
} else if (command == Protocol::OpenDraft) {
const auto draftId = args["draftId"_L1].toString();
if (draftId.isEmpty()) {
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
dialog->setMessage(draft.mime());
}
- FirstTimeDialog::strongActivateWindow(dialog);
+ DialogController::strongActivateWindow(dialog);
return;
}
case Protocol::DeleteDraft: {
const auto draftId = args["draftId"_L1].toString();
if (draftId.isEmpty()) {
qWarning() << "Draft not valid";
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
if (!draft.isValid()) {
qWarning() << "Draft not valid";
return;
}
if (!DraftManager::self().remove(draft)) {
qCWarning(GPGOL_CLIENT_LOG) << "Could not delete draft";
return;
}
sendStatusUpdate();
return;
}
case Protocol::Reencrypt: {
initMailApiFromArgs(args);
reencrypt(args);
return;
}
case Protocol::Info: {
initMailApiFromArgs(args);
info(args);
return;
}
default:
qCWarning(WEBSOCKET_LOG) << "Unhandled command" << command;
}
}
void WebsocketClient::reencrypt(const QJsonObject &args)
{
if (m_reencryptJob) {
if (m_reencryptJob->hasStarted()) {
m_reencryptJob->tryRaiseDialog();
return;
}
m_reencryptJob->deleteLater();
}
// TODO: Looking up the folderId by itemId would allow some simplification in the web.js
m_reencryptJob = new ReencryptJob(args["folderId"_L1].toString());
m_reencryptJob->start();
}
+void WebsocketClient::connectToProxy(const QUrl &url, const QString &clientSecret)
+{
+ if (!clientSecret.isEmpty()) {
+ if (clientSecret != m_clientSecret) {
+ m_url.clear();
+ }
+ m_clientSecret = clientSecret;
+ }
+ if (m_url != url) {
+ m_url = url;
+ if (m_webSocket.state() != QAbstractSocket::UnconnectedState) {
+ disconnectFromProxy();
+ }
+ }
+ if ((m_webSocket.state() != QAbstractSocket::ConnectedState) &&
+ (m_webSocket.state() != QAbstractSocket::ConnectingState)) {
+ m_webSocket.open(m_url);
+ }
+}
+
WebsocketClient::State WebsocketClient::state() const
{
return m_state;
}
QString WebsocketClient::stateDisplay() const
{
return m_stateDisplay;
}
void WebsocketClient::sendCommand(Protocol::Command command, const QJsonObject &arguments)
{
const auto json = Protocol::makeCommand(command, arguments, getId());
m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
}
void WebsocketClient::sendStatusUpdate(bool viewerJustClosed)
{
QJsonArray features;
if (Config::self()->reencrypt()) {
features << u"reencrypt"_s;
}
sendCommand(Protocol::StatusUpdate, QJsonObject{
{"drafts"_L1, DraftManager::self().toJson()},
{"viewerOpen"_L1, !viewerJustClosed && !m_emailViewer.isNull()},
{"features"_L1, features}
});
}
void WebsocketClient::info(const QJsonObject &args)
{
const auto email = args["email"_L1].toString();
sendStatusUpdate(false); // web client expects to know that info before info-fetched
const QString id(args["itemId"_L1].toString());
qCWarning(GPGOL_CLIENT_LOG) << "Info for" << id << "requested";
if (m_cachedInfo.contains(id)) {
sendCommand(Protocol::InfoFetched, m_cachedInfo[id]);
return;
}
MailApiController::self().setAccessToken(args["ewsAccessToken"_L1].toString());
auto request = MailApiController::self().getMails({id}, GetMailsJob::GetMimeContent | GetMailsJob::GetParentFolderId);
connect(request, &GetMailsJob::finished, this, [this, id, args, request]() {
if (request->error() != KJob::NoError) {
sendCommand(Protocol::Error, QJsonObject{{"error"_L1, request->errorString()}});
qCWarning(GPGOL_CLIENT_LOG) << "Failure to get mail:" << request->errorText();
return;
}
qCWarning(GPGOL_CLIENT_LOG) << "Info for" << id << "fetched";
const auto responses = request->takeResponses();
if (responses.isEmpty()) {
return;
}
const auto item = responses.first();
const auto mimeContent = GetMailsJob::mimeContent(item);
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(mimeContent));
message->parse();
MimeTreeParser::ObjectTreeParser treeParser;
treeParser.parseObjectTree(message.get());
const auto data = QJsonObject{
{"itemId"_L1, args["itemId"_L1]},
{"folderId"_L1, GetMailsJob::parentFolderId(item)},
{"email"_L1, args["email"_L1]},
{"encrypted"_L1, treeParser.hasEncryptedParts()},
{"signed"_L1, treeParser.hasSignedParts()},
{"version"_L1, QStringLiteral(GPGOLWEB_VERSION_STRING)},
};
m_cachedInfo[id] = data;
m_cachedMime[id] = mimeContent;
sendCommand(Protocol::InfoFetched, data);
});
request->start();
}
QString WebsocketClient::getId() const
{
auto config = KSharedConfig::openStateConfig();
auto machineGroup = config->group(u"Machine"_s);
if (machineGroup.exists() && machineGroup.hasKey(u"Id"_s)) {
return machineGroup.readEntry(u"Id"_s);
}
const auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
machineGroup.writeEntry("Id", id);
config->sync();
return id;
}
diff --git a/client/websocketclient.h b/client/websocketclient.h
index a0f7dbe..6730c01 100644
--- a/client/websocketclient.h
+++ b/client/websocketclient.h
@@ -1,100 +1,100 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "../common/protocol.h"
#include <Libkleo/KeyCache>
#include <QPointer>
#include <QWebSocket>
#include <QJsonObject>
#include <chrono>
class EmailViewer;
class ReencryptJob;
class KJob;
namespace GpgME
{
class KeyListResult;
class Key;
class Error;
}
using namespace std::chrono;
class WebsocketClient : public QObject
{
Q_OBJECT
Q_PROPERTY(State state READ state NOTIFY stateChanged)
Q_PROPERTY(QString stateDisplay READ stateDisplay NOTIFY stateChanged)
public:
enum State {
NotConnected,
ConnectedToProxy,
ConnectedToWebclient,
};
- static WebsocketClient &self(const QUrl &url = {});
+ static WebsocketClient &self();
State state() const; // this one is about the web client
bool connectedToProxy() const;
QString stateDisplay() const;
/// \params fromEmail The email address who should send this EWS request.
/// \params requestId Identifier of the request, will be send back in ewsResponseReceived
/// \params requestBody The SOAP request body.
bool sendEWSRequest(const QString &fromEmail, const QString &requestId, const QString &requestBody);
void sendStatusUpdate(bool viewerJustClosed=false);
void enterPairingMode();
void quitPairingMode();
void disconnectFromProxy();
- void connectToProxy(const QString &clientSecret = QString());
-
+ void connectToProxy(const QUrl &url, const QString &clientSecret = QString());
Q_SIGNALS:
void stateChanged(const QString &state);
void pairingStatusChanged(const QString& token, bool pairingActive);
void ewsResponseReceived(const QString &requestId, const QString &responseBody);
void emailSent(const QString &requestId, const QString &error);
private Q_SLOTS:
void slotConnected();
void slotErrorOccurred(QAbstractSocket::SocketError error);
void slotTextMessageReceived(QString message);
void slotKeyListingDone(const GpgME::KeyListResult &result,
const std::vector<GpgME::Key> &keys,
const QString &auditLogAsHtml,
const GpgME::Error &auditLogError);
QString getId() const;
private:
void sendCommand(Protocol::Command command, const QJsonObject &arguments);
- explicit WebsocketClient(const QUrl &url);
+ WebsocketClient();
void reencrypt(const QJsonObject &args);
void info(const QJsonObject &args);
void initMailApiFromArgs(const QJsonObject &args);
bool m_wasConnected = false;
QWebSocket m_webSocket;
QUrl m_url;
QStringList m_emails;
QString m_clientSecret;
+
std::chrono::milliseconds m_delay = 2000ms;
State m_state = NotConnected;
QString m_stateDisplay;
QPointer<EmailViewer> m_emailViewer;
QPointer<ReencryptJob> m_reencryptJob;
QHash<QString, QJsonObject> m_cachedInfo;
QHash<QString, QByteArray> m_cachedMime;
};

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 13, 9:44 AM (1 d, 21 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
21/6d/d2279dff7451c2784151fec082d5

Event Timeline