File indexing completed on 2025-03-09 04:54:43

0001 /*
0002   SPDX-FileCopyrightText: 1997 Markus Wuebben <markus.wuebben@kde.org>
0003   SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
0004   SPDX-FileCopyrightText: 2009 Andras Mantia <andras@kdab.net>
0005   SPDX-FileCopyrightText: 2010 Torgny Nyblom <nyblom@kde.org>
0006   SPDX-FileCopyrightText: 2011-2024 Laurent Montel <montel@kde.org>
0007 
0008   SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include "viewer_p.h"
0011 #include "printmessage.h"
0012 #include "viewerpurposemenuwidget.h"
0013 
0014 #include "mdn/mdnwarningwidget.h"
0015 #include "messagedisplayformatattribute.h"
0016 #include "messageviewer_debug.h"
0017 #include "scamdetection/scamattribute.h"
0018 #include "scamdetection/scamdetectionwarningwidget.h"
0019 #include "utils/mimetype.h"
0020 #include "viewer/mimeparttree/mimeparttreeview.h"
0021 #include "viewer/objecttreeemptysource.h"
0022 #include "viewer/objecttreeviewersource.h"
0023 #include "viewer/renderer/messageviewerrenderer.h"
0024 
0025 #include "messageviewer/headerstrategy.h"
0026 #include "messageviewer/headerstyle.h"
0027 #include "openurlwith/openurlwithmanager.h"
0028 #include <TextAddonsWidgets/SlideContainer>
0029 
0030 #include "job/modifymessagedisplayformatjob.h"
0031 
0032 #include "htmlwriter/webengineembedpart.h"
0033 #include "viewerplugins/viewerplugintoolmanager.h"
0034 #include <KContacts/VCardConverter>
0035 
0036 #include <KActionCollection>
0037 #include <KActionMenu>
0038 #include <QAction>
0039 #include <QHBoxLayout>
0040 #include <QPrintPreviewDialog>
0041 #include <QVBoxLayout>
0042 
0043 #include <Akonadi/ErrorAttribute>
0044 #include <Akonadi/ItemCreateJob>
0045 #include <Akonadi/ItemModifyJob>
0046 #include <Akonadi/MessageFlags>
0047 #include <Akonadi/SpecialMailCollections>
0048 #include <KApplicationTrader>
0049 #include <KEmailAddress>
0050 #include <KFileItemActions>
0051 #include <KIO/ApplicationLauncherJob>
0052 #include <KIO/JobUiDelegateFactory>
0053 #include <KIO/OpenUrlJob>
0054 #include <KLocalizedString>
0055 #include <KMessageBox>
0056 #include <KMimeTypeChooser>
0057 #include <KSelectAction>
0058 #include <KSharedConfig>
0059 #include <KStandardGuiItem>
0060 #include <KToggleAction>
0061 #include <MessageCore/Util>
0062 #include <QIcon>
0063 #include <QKeyCombination>
0064 #include <QMenu>
0065 #include <QMimeData>
0066 #include <QTemporaryDir>
0067 
0068 // Qt includes
0069 #include <QActionGroup>
0070 #include <QClipboard>
0071 #include <QItemSelectionModel>
0072 #include <QMimeDatabase>
0073 #include <QPrintDialog>
0074 #include <QPrinter>
0075 #include <QSplitter>
0076 #include <QTreeView>
0077 #include <QWheelEvent>
0078 #include <WebEngineViewer/WebEngineExportHtmlPageJob>
0079 // libkdepim
0080 #include <MessageCore/AttachmentPropertiesDialog>
0081 #include <PimCommon/BroadcastStatus>
0082 
0083 #include <Akonadi/AttributeFactory>
0084 #include <Akonadi/Collection>
0085 #include <Akonadi/ItemFetchJob>
0086 #include <Akonadi/ItemFetchScope>
0087 #include <Akonadi/MessageParts>
0088 #include <Akonadi/MessageStatus>
0089 
0090 #include <KIdentityManagementCore/Identity>
0091 #include <KIdentityManagementCore/IdentityManager>
0092 #include <MessageCore/AutocryptUtils>
0093 
0094 // own includes
0095 #include "messageviewer/messageviewerutil.h"
0096 #include "openurlwith/openurlwithjob.h"
0097 #include "settings/messageviewersettings.h"
0098 #include "utils/messageviewerutil_p.h"
0099 #include "viewer/attachmentstrategy.h"
0100 #include "viewer/mimeparttree/mimetreemodel.h"
0101 #include "viewer/urlhandlermanager.h"
0102 #include "widgets/attachmentdialog.h"
0103 #include "widgets/htmlstatusbar.h"
0104 #include "widgets/shownextmessagewidget.h"
0105 
0106 #include "header/headerstylemenumanager.h"
0107 #include "htmlwriter/webengineparthtmlwriter.h"
0108 #include "viewer/webengine/mailwebengineview.h"
0109 #include "widgets/mailsourcewebengineviewer.h"
0110 #include <WebEngineViewer/FindBarWebEngineView>
0111 #include <WebEngineViewer/LocalDataBaseManager>
0112 #include <WebEngineViewer/SubmittedFormWarningWidget>
0113 #include <WebEngineViewer/WebEngineExportPdfPageJob>
0114 #include <WebEngineViewer/WebHitTestResult>
0115 
0116 #include "interfaces/htmlwriter.h"
0117 #include <MimeTreeParser/BodyPart>
0118 #include <MimeTreeParser/NodeHelper>
0119 #include <MimeTreeParser/ObjectTreeParser>
0120 
0121 #include <MessageCore/StringUtil>
0122 
0123 #include <MessageCore/MessageCoreSettings>
0124 #include <MessageCore/NodeHelper>
0125 
0126 #include <Akonadi/AgentInstance>
0127 #include <Akonadi/AgentManager>
0128 #include <Akonadi/CollectionFetchJob>
0129 #include <Akonadi/CollectionFetchScope>
0130 
0131 #include <PimCommon/PurposeMenuMessageWidget>
0132 
0133 #ifdef HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
0134 #include <TextEditTextToSpeech/TextToSpeechContainerWidget>
0135 #endif
0136 #include "header/headerstyleplugin.h"
0137 #include "viewerplugins/viewerplugininterface.h"
0138 #include <Akonadi/MDNStateAttribute>
0139 #include <QApplication>
0140 #include <QStandardPaths>
0141 #include <QWebEngineSettings>
0142 #include <WebEngineViewer/DeveloperToolDialog>
0143 #include <WebEngineViewer/TrackingWarningWidget>
0144 #include <WebEngineViewer/ZoomActionMenu>
0145 
0146 #include <GrantleeTheme/GrantleeTheme>
0147 #include <GrantleeTheme/GrantleeThemeManager>
0148 
0149 #include "dkim-verify/dkimmanager.h"
0150 #include "dkim-verify/dkimmanagerulesdialog.h"
0151 #include "dkim-verify/dkimresultattribute.h"
0152 #include "dkim-verify/dkimviewermenu.h"
0153 #include "dkim-verify/dkimwidgetinfo.h"
0154 
0155 #include "remote-content/remotecontentmenu.h"
0156 #include <chrono>
0157 
0158 using namespace std::chrono_literals;
0159 using namespace MessageViewer;
0160 using namespace MessageCore;
0161 
0162 static QAtomicInt _k_attributeInitialized;
0163 
0164 template<typename Arg, typename R, typename C>
0165 struct InvokeWrapper {
0166     R *receiver;
0167     void (C::*memberFun)(Arg);
0168     void operator()(Arg result)
0169     {
0170         (receiver->*memberFun)(result);
0171     }
0172 };
0173 
0174 template<typename Arg, typename R, typename C>
0175 InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFun)(Arg))
0176 {
0177     InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFun};
0178     return wrapper;
0179 }
0180 
0181 ViewerPrivate::ViewerPrivate(Viewer *aParent, QWidget *mainWindow, KActionCollection *actionCollection)
0182     : QObject(aParent)
0183     , mNodeHelper(new MimeTreeParser::NodeHelper)
0184     , mOldGlobalOverrideEncoding(QStringLiteral("---"))
0185     , mMainWindow(mainWindow)
0186     , mActionCollection(actionCollection)
0187     , q(aParent)
0188     , mSession(new Akonadi::Session("MessageViewer-" + QByteArray::number(reinterpret_cast<quintptr>(this)), this))
0189 {
0190     if (!mainWindow) {
0191         mMainWindow = aParent;
0192     }
0193     mMessageViewerRenderer = new MessageViewerRenderer;
0194 
0195     mRemoteContentMenu = new MessageViewer::RemoteContentMenu(mMainWindow);
0196     connect(mRemoteContentMenu, &MessageViewer::RemoteContentMenu::updateEmail, this, &ViewerPrivate::updateReaderWin);
0197 
0198     mDkimWidgetInfo = new MessageViewer::DKIMWidgetInfo(mMainWindow);
0199     if (_k_attributeInitialized.testAndSetAcquire(0, 1)) {
0200         Akonadi::AttributeFactory::registerAttribute<MessageViewer::MessageDisplayFormatAttribute>();
0201         Akonadi::AttributeFactory::registerAttribute<MessageViewer::ScamAttribute>();
0202     }
0203     mPhishingDatabase = new WebEngineViewer::LocalDataBaseManager(this);
0204     mPhishingDatabase->initialize();
0205     connect(mPhishingDatabase, &WebEngineViewer::LocalDataBaseManager::checkUrlFinished, this, &ViewerPrivate::slotCheckedUrlFinished);
0206 
0207     mShareServiceManager = new PimCommon::ShareServiceUrlManager(this);
0208 
0209     mDisplayFormatMessageOverwrite = MessageViewer::Viewer::UseGlobalSetting;
0210 
0211     mUpdateReaderWinTimer.setObjectName(QLatin1StringView("mUpdateReaderWinTimer"));
0212     mResizeTimer.setObjectName(QLatin1StringView("mResizeTimer"));
0213 
0214     createWidgets();
0215     createActions();
0216     initHtmlWidget();
0217     readConfig();
0218 
0219     mLevelQuote = MessageViewer::MessageViewerSettings::self()->collapseQuoteLevelSpin() - 1;
0220 
0221     mResizeTimer.setSingleShot(true);
0222     connect(&mResizeTimer, &QTimer::timeout, this, &ViewerPrivate::slotDelayedResize);
0223 
0224     mUpdateReaderWinTimer.setSingleShot(true);
0225     connect(&mUpdateReaderWinTimer, &QTimer::timeout, this, &ViewerPrivate::updateReaderWin);
0226 
0227     connect(mNodeHelper, &MimeTreeParser::NodeHelper::update, this, &ViewerPrivate::update);
0228 
0229     connect(mColorBar, &HtmlStatusBar::clicked, this, &ViewerPrivate::slotToggleHtmlMode);
0230 
0231     // FIXME: Don't use the full payload here when attachment loading on demand is used, just
0232     //        like in KMMainWidget::slotMessageActivated().
0233     mMonitor.setObjectName(QLatin1StringView("MessageViewerMonitor"));
0234     mMonitor.setSession(mSession);
0235     Akonadi::ItemFetchScope fs;
0236     fs.fetchFullPayload();
0237     fs.fetchAttribute<Akonadi::ErrorAttribute>();
0238     fs.fetchAttribute<MessageViewer::MessageDisplayFormatAttribute>();
0239     fs.fetchAttribute<MessageViewer::ScamAttribute>();
0240     fs.fetchAttribute<MessageViewer::DKIMResultAttribute>();
0241     fs.fetchAttribute<Akonadi::MDNStateAttribute>();
0242     mMonitor.setItemFetchScope(fs);
0243     connect(&mMonitor, &Akonadi::Monitor::itemChanged, this, &ViewerPrivate::slotItemChanged);
0244     connect(&mMonitor, &Akonadi::Monitor::itemRemoved, this, &ViewerPrivate::slotClear);
0245     connect(&mMonitor, &Akonadi::Monitor::itemMoved, this, &ViewerPrivate::slotItemMoved);
0246 }
0247 
0248 ViewerPrivate::~ViewerPrivate()
0249 {
0250     delete mDeveloperToolDialog;
0251     delete mMessageViewerRenderer;
0252     MessageViewer::MessageViewerSettings::self()->save();
0253     delete mHtmlWriter;
0254     mHtmlWriter = nullptr;
0255     delete mViewer;
0256     mViewer = nullptr;
0257     mNodeHelper->forceCleanTempFiles();
0258     qDeleteAll(mListMailSourceViewer);
0259     mMessage.clear();
0260     delete mNodeHelper;
0261 }
0262 
0263 //-----------------------------------------------------------------------------
0264 KMime::Content *ViewerPrivate::nodeFromUrl(const QUrl &url) const
0265 {
0266     return mNodeHelper->fromHREF(mMessage, url);
0267 }
0268 
0269 void ViewerPrivate::openAttachment(KMime::Content *node, const QUrl &url)
0270 {
0271     if (!node) {
0272         return;
0273     }
0274 
0275     if (auto ct = node->contentType(false)) {
0276         if (ct->mimeType() == "text/x-moz-deleted") {
0277             return;
0278         }
0279         if (ct->mimeType() == "message/external-body") {
0280             if (ct->hasParameter(QStringLiteral("url"))) {
0281                 auto job = new KIO::OpenUrlJob(url, QStringLiteral("text/html"));
0282                 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, q));
0283                 job->start();
0284                 return;
0285             }
0286         }
0287     }
0288 
0289     const bool isEncapsulatedMessage = node->parent() && node->parent()->bodyIsMessage();
0290     if (isEncapsulatedMessage) {
0291         // the viewer/urlhandlermanager expects that the message (mMessage) it is passed is the root when doing index calculation
0292         // in urls. Simply passing the result of bodyAsMessage() does not cut it as the resulting pointer is a child in its tree.
0293         KMime::Message::Ptr m(new KMime::Message);
0294         m->setContent(node->parent()->bodyAsMessage()->encodedContent());
0295         m->parse();
0296         attachmentViewMessage(m);
0297         return;
0298     }
0299     // determine the MIME type of the attachment
0300     // prefer the value of the Content-Type header
0301     QMimeDatabase mimeDb;
0302     auto mimetype = mimeDb.mimeTypeForName(QString::fromLatin1(node->contentType()->mimeType().toLower()));
0303 
0304     // special case treatment on mac and windows
0305     QUrl atmUrl = url;
0306     if (url.isEmpty()) {
0307         atmUrl = mNodeHelper->tempFileUrlFromNode(node);
0308     }
0309     if (Util::handleUrlWithQDesktopServices(atmUrl)) {
0310         return;
0311     }
0312 
0313     if (!mimetype.isValid() || mimetype.name() == QLatin1StringView("application/octet-stream")) {
0314         mimetype = MimeTreeParser::Util::mimetype(url.isLocalFile() ? url.toLocalFile() : url.fileName());
0315     }
0316     KService::Ptr offer = KApplicationTrader::preferredService(mimetype.name());
0317 
0318     const QString filenameText = MimeTreeParser::NodeHelper::fileName(node);
0319 
0320     QPointer<AttachmentDialog> dialog = new AttachmentDialog(mMainWindow, filenameText, offer, QLatin1StringView("askSave_") + mimetype.name());
0321     const int choice = dialog->exec();
0322     delete dialog;
0323     if (choice == AttachmentDialog::Save) {
0324         QList<QUrl> urlList;
0325         if (Util::saveContents(mMainWindow, KMime::Content::List() << node, urlList)) {
0326             showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
0327         }
0328     } else if (choice == AttachmentDialog::Open) { // Open
0329         if (offer) {
0330             attachmentOpenWith(node, offer);
0331         } else {
0332             attachmentOpen(node);
0333         }
0334     } else if (choice == AttachmentDialog::OpenWith) {
0335         attachmentOpenWith(node);
0336     } else { // Cancel
0337         qCDebug(MESSAGEVIEWER_LOG) << "Canceled opening attachment";
0338     }
0339 }
0340 
0341 static bool confirmAttachmentDeletion(QWidget *parent)
0342 {
0343     return KMessageBox::warningContinueCancel(parent,
0344                                               i18n("Deleting an attachment might invalidate any digital signature on this message."),
0345                                               i18nc("@title:window", "Delete Attachment"),
0346                                               KStandardGuiItem::del(),
0347                                               KStandardGuiItem::cancel(),
0348                                               QStringLiteral("DeleteAttachmentSignatureWarning"))
0349         == KMessageBox::Continue;
0350 }
0351 
0352 void ViewerPrivate::updateMessageAfterDeletingAttachments(KMime::Message::Ptr &message)
0353 {
0354     KMime::Message *modifiedMessage = mNodeHelper->messageWithExtraContent(message.data());
0355     mMimePartTree->mimePartModel()->setRoot(modifiedMessage);
0356     mMessageItem.setPayloadFromData(message->encodedContent());
0357     // Modifying the payload might change the remote id (e.g. for IMAP) of the item, so don't try to force on it
0358     // a potentially old remote id. Without clearing the remote id, deleting multiple attachments from a message
0359     // stored on an IMAP server will likely fail with "Invalid attempt to modify the remoteID for item [...]".
0360     mMessageItem.setRemoteId({});
0361     auto job = new Akonadi::ItemModifyJob(mMessageItem, mSession);
0362     job->disableRevisionCheck();
0363     connect(job, &KJob::result, this, &ViewerPrivate::itemModifiedResult);
0364 }
0365 
0366 bool ViewerPrivate::deleteAttachment(KMime::Content *node, bool showWarning)
0367 {
0368     if (!node) {
0369         return true;
0370     }
0371     KMime::Content *parent = node->parent();
0372     if (!parent) {
0373         return true;
0374     }
0375 
0376     const QList<KMime::Content *> extraNodes = mNodeHelper->extraContents(mMessage.data());
0377     if (extraNodes.contains(node->topLevel())) {
0378         KMessageBox::error(mMainWindow,
0379                            i18n("Deleting an attachment from an encrypted or old-style mailman message is not supported."),
0380                            i18nc("@title:window", "Delete Attachment"));
0381         return true; // cancelled
0382     }
0383 
0384     if (showWarning && !confirmAttachmentDeletion(mMainWindow)) {
0385         return false; // cancelled
0386     }
0387 
0388     // don't confuse the model
0389     mMimePartTree->clearModel();
0390 
0391     if (!Util::deleteAttachment(node)) {
0392         return false;
0393     }
0394 
0395     updateMessageAfterDeletingAttachments(mMessage);
0396 
0397     return true;
0398 }
0399 
0400 void ViewerPrivate::itemModifiedResult(KJob *job)
0401 {
0402     if (job->error()) {
0403         qCDebug(MESSAGEVIEWER_LOG) << "Item update failed:" << job->errorString();
0404     } else {
0405         setMessageItem(mMessageItem, MimeTreeParser::Force);
0406     }
0407 }
0408 
0409 void ViewerPrivate::scrollToAnchor(const QString &anchor)
0410 {
0411     mViewer->scrollToAnchor(anchor);
0412 }
0413 
0414 void ViewerPrivate::createOpenWithMenu(QMenu *topMenu, const QString &contentTypeStr, bool fromCurrentContent)
0415 {
0416     const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr);
0417     if (!offers.isEmpty()) {
0418         QMenu *menu = topMenu;
0419         auto actionGroup = new QActionGroup(menu);
0420 
0421         if (fromCurrentContent) {
0422             connect(actionGroup, &QActionGroup::triggered, this, &ViewerPrivate::slotOpenWithActionCurrentContent);
0423         } else {
0424             connect(actionGroup, &QActionGroup::triggered, this, &ViewerPrivate::slotOpenWithAction);
0425         }
0426 
0427         if (offers.count() > 1) { // submenu 'open with'
0428             menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu);
0429             menu->menuAction()->setObjectName(QLatin1StringView("openWith_submenu")); // for the unittest
0430             topMenu->addMenu(menu);
0431         }
0432         // qCDebug(MESSAGEVIEWER_LOG) << offers.count() << "offers" << topMenu << menu;
0433 
0434         for (const KService::Ptr &ser : offers) {
0435             QAction *act = MessageViewer::Util::createAppAction(ser,
0436                                                                 // no submenu -> prefix single offer
0437                                                                 menu == topMenu,
0438                                                                 actionGroup,
0439                                                                 menu);
0440             menu->addAction(act);
0441         }
0442 
0443         QString openWithActionName;
0444         if (menu != topMenu) { // submenu
0445             menu->addSeparator();
0446             openWithActionName = i18nc("@action:inmenu Open With", "&Other...");
0447         } else {
0448             openWithActionName = i18nc("@title:menu", "&Open With...");
0449         }
0450         auto openWithAct = new QAction(menu);
0451         openWithAct->setText(openWithActionName);
0452         if (fromCurrentContent) {
0453             connect(openWithAct, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialogCurrentContent);
0454         } else {
0455             connect(openWithAct, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialog);
0456         }
0457 
0458         menu->addAction(openWithAct);
0459     } else { // no app offers -> Open With...
0460         auto act = new QAction(topMenu);
0461         act->setText(i18nc("@title:menu", "&Open With..."));
0462         if (fromCurrentContent) {
0463             connect(act, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialogCurrentContent);
0464         } else {
0465             connect(act, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialog);
0466         }
0467         topMenu->addAction(act);
0468     }
0469 }
0470 
0471 void ViewerPrivate::slotOpenWithDialogCurrentContent()
0472 {
0473     if (!mCurrentContent) {
0474         return;
0475     }
0476     attachmentOpenWith(mCurrentContent);
0477 }
0478 
0479 void ViewerPrivate::slotOpenWithDialog()
0480 {
0481     const auto contents = selectedContents();
0482     if (contents.count() == 1) {
0483         attachmentOpenWith(contents.first());
0484     }
0485 }
0486 
0487 void ViewerPrivate::slotOpenWithActionCurrentContent(QAction *act)
0488 {
0489     if (!mCurrentContent) {
0490         return;
0491     }
0492     const auto app = act->data().value<KService::Ptr>();
0493     attachmentOpenWith(mCurrentContent, app);
0494 }
0495 
0496 void ViewerPrivate::slotOpenWithAction(QAction *act)
0497 {
0498     const auto app = act->data().value<KService::Ptr>();
0499     const auto contents = selectedContents();
0500     if (contents.count() == 1) {
0501         attachmentOpenWith(contents.first(), app);
0502     }
0503 }
0504 
0505 void ViewerPrivate::showAttachmentPopup(KMime::Content *node, const QString &name, const QPoint &globalPos)
0506 {
0507     Q_UNUSED(name)
0508     prepareHandleAttachment(node);
0509     bool deletedAttachment = false;
0510     QString contentTypeStr;
0511     if (auto contentType = node->contentType(false)) {
0512         contentTypeStr = QLatin1StringView(contentType->mimeType());
0513     }
0514     if (contentTypeStr == QLatin1StringView("message/global")) { // Not registered in mimetype => it's a message/rfc822
0515         contentTypeStr = QStringLiteral("message/rfc822");
0516     }
0517     deletedAttachment = (contentTypeStr == QLatin1StringView("text/x-moz-deleted"));
0518     // Not necessary to show popup menu when attachment was removed
0519     if (deletedAttachment) {
0520         return;
0521     }
0522 
0523     QMenu menu;
0524 
0525     QAction *action = menu.addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("to open", "Open"));
0526     action->setEnabled(!deletedAttachment);
0527     connect(action, &QAction::triggered, this, [this]() {
0528         slotHandleAttachment(Viewer::Open);
0529     });
0530     createOpenWithMenu(&menu, contentTypeStr, true);
0531 
0532     QMimeDatabase mimeDb;
0533     auto mimetype = mimeDb.mimeTypeForName(contentTypeStr);
0534     if (mimetype.isValid()) {
0535         const QStringList parentMimeType = mimetype.parentMimeTypes();
0536         if ((contentTypeStr == QLatin1StringView("text/plain")) || (contentTypeStr == QLatin1StringView("image/png"))
0537             || (contentTypeStr == QLatin1StringView("image/jpeg")) || parentMimeType.contains(QLatin1StringView("text/plain"))
0538             || parentMimeType.contains(QLatin1StringView("image/png")) || parentMimeType.contains(QLatin1StringView("image/jpeg"))) {
0539             action = menu.addAction(i18nc("to view something", "View"));
0540             action->setEnabled(!deletedAttachment);
0541             connect(action, &QAction::triggered, this, [this]() {
0542                 slotHandleAttachment(Viewer::View);
0543             });
0544         }
0545     }
0546 
0547     action = menu.addAction(i18n("Scroll To"));
0548     connect(action, &QAction::triggered, this, [this]() {
0549         slotHandleAttachment(Viewer::ScrollTo);
0550     });
0551 
0552     action = menu.addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."));
0553     action->setEnabled(!deletedAttachment);
0554     connect(action, &QAction::triggered, this, [this]() {
0555         slotHandleAttachment(Viewer::Save);
0556     });
0557 
0558     action = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"));
0559     action->setEnabled(!deletedAttachment);
0560     connect(action, &QAction::triggered, this, [this]() {
0561         slotHandleAttachment(Viewer::Copy);
0562     });
0563 
0564     const bool isEncapsulatedMessage = node->parent() && node->parent()->bodyIsMessage();
0565     const bool canChange = mMessageItem.isValid() && mMessageItem.parentCollection().isValid()
0566         && (mMessageItem.parentCollection().rights() != Akonadi::Collection::ReadOnly) && !isEncapsulatedMessage;
0567 
0568     menu.addSeparator();
0569     action = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Attachment"));
0570     connect(action, &QAction::triggered, this, [this]() {
0571         slotHandleAttachment(Viewer::Delete);
0572     });
0573 
0574     action->setEnabled(canChange && !deletedAttachment);
0575 #if 0
0576     menu->addSeparator();
0577 
0578     action
0579         = menu->addAction(QIcon::fromTheme(QStringLiteral("mail-reply-sender")),
0580                           i18n("Reply To Author"));
0581     connect(action, &QAction::triggered, this, [this]() {
0582         slotHandleAttachment(Viewer::ReplyMessageToAuthor);
0583     });
0584 
0585     menu->addSeparator();
0586 
0587     action = menu->addAction(QIcon::fromTheme(QStringLiteral("mail-reply-all")), i18n(
0588                                  "Reply To All"));
0589     connect(action, &QAction::triggered, this, [this]() {
0590         slotHandleAttachment(Viewer::ReplyMessageToAll);
0591     });
0592 #endif
0593     menu.addSeparator();
0594     action = menu.addAction(i18n("Properties"));
0595     connect(action, &QAction::triggered, this, [this]() {
0596         slotHandleAttachment(Viewer::Properties);
0597     });
0598     menu.exec(globalPos);
0599 }
0600 
0601 void ViewerPrivate::prepareHandleAttachment(KMime::Content *node)
0602 {
0603     mCurrentContent = node;
0604 }
0605 
0606 KService::Ptr ViewerPrivate::getServiceOffer(KMime::Content *content)
0607 {
0608     const QString fileName = mNodeHelper->writeNodeToTempFile(content);
0609 
0610     const QString contentTypeStr = QLatin1StringView(content->contentType()->mimeType());
0611 
0612     // determine the MIME type of the attachment
0613     // prefer the value of the Content-Type header
0614     QMimeDatabase mimeDb;
0615     auto mimetype = mimeDb.mimeTypeForName(contentTypeStr);
0616 
0617     if (mimetype.isValid() && mimetype.inherits(KContacts::Addressee::mimeType())) {
0618         attachmentView(content);
0619         return KService::Ptr(nullptr);
0620     }
0621 
0622     if (!mimetype.isValid() || mimetype.name() == QLatin1StringView("application/octet-stream")) {
0623         /*TODO(Andris) port when on-demand loading is done   && msgPart.isComplete() */
0624         mimetype = MimeTreeParser::Util::mimetype(fileName);
0625     }
0626     return KApplicationTrader::preferredService(mimetype.name());
0627 }
0628 
0629 KMime::Content::List ViewerPrivate::selectedContents() const
0630 {
0631     return mMimePartTree->selectedContents();
0632 }
0633 
0634 void ViewerPrivate::attachmentOpenWith(KMime::Content *node, const KService::Ptr &offer)
0635 {
0636     QString name = mNodeHelper->writeNodeToTempFile(node);
0637 
0638     // Make sure that it will not deleted when we switch from message.
0639     auto tmpDir = new QTemporaryDir(QDir::tempPath() + QLatin1StringView("/messageviewer_attachment_XXXXXX"));
0640     if (tmpDir->isValid()) {
0641         tmpDir->setAutoRemove(false);
0642         const QString path = tmpDir->path();
0643         delete tmpDir;
0644         QFile f(name);
0645         const QUrl tmpFileName = QUrl::fromLocalFile(name);
0646         const QString newPath = path + QLatin1Char('/') + tmpFileName.fileName();
0647 
0648         if (!f.copy(newPath)) {
0649             qCDebug(MESSAGEVIEWER_LOG) << " File was not able to copy: filename: " << name << " to " << path;
0650         } else {
0651             name = newPath;
0652         }
0653         f.close();
0654     } else {
0655         delete tmpDir;
0656     }
0657 
0658     const QFileDevice::Permissions perms = QFile::permissions(name);
0659     QFile::setPermissions(name, perms | QFileDevice::ReadUser | QFileDevice::WriteUser);
0660     const QUrl url = QUrl::fromLocalFile(name);
0661 
0662     auto job = new KIO::ApplicationLauncherJob(offer);
0663     job->setUrls({url});
0664     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, mMainWindow));
0665     job->start();
0666     connect(job, &KJob::result, this, [url, job]() {
0667         if (job->error()) {
0668             QFile::remove(url.toLocalFile());
0669         }
0670     });
0671 }
0672 
0673 void ViewerPrivate::attachmentOpen(KMime::Content *node)
0674 {
0675     const KService::Ptr offer = getServiceOffer(node);
0676     if (!offer) {
0677         qCDebug(MESSAGEVIEWER_LOG) << "got no offer";
0678         return;
0679     }
0680     attachmentOpenWith(node, offer);
0681 }
0682 
0683 bool ViewerPrivate::showEmoticons() const
0684 {
0685     return mForceEmoticons;
0686 }
0687 
0688 HtmlWriter *ViewerPrivate::htmlWriter() const
0689 {
0690     return mHtmlWriter;
0691 }
0692 
0693 CSSHelper *ViewerPrivate::cssHelper() const
0694 {
0695     return mMessageViewerRenderer->cssHelper();
0696 }
0697 
0698 MimeTreeParser::NodeHelper *ViewerPrivate::nodeHelper() const
0699 {
0700     return mNodeHelper;
0701 }
0702 
0703 Viewer *ViewerPrivate::viewer() const
0704 {
0705     return q;
0706 }
0707 
0708 Akonadi::Item ViewerPrivate::messageItem() const
0709 {
0710     return mMessageItem;
0711 }
0712 
0713 KMime::Message::Ptr ViewerPrivate::message() const
0714 {
0715     return mMessage;
0716 }
0717 
0718 bool ViewerPrivate::decryptMessage() const
0719 {
0720     if (MessageViewer::MessageViewerSettings::self()->alwaysDecrypt()) {
0721         return true;
0722     } else {
0723         return mDecrytMessageOverwrite;
0724     }
0725 }
0726 
0727 void ViewerPrivate::displaySplashPage(const QString &message)
0728 {
0729     displaySplashPage(QStringLiteral("status.html"),
0730                       {{QStringLiteral("icon"), QStringLiteral("kmail")},
0731                        {QStringLiteral("name"), i18n("KMail")},
0732                        {QStringLiteral("subtitle"), i18n("The KDE Mail Client")},
0733                        {QStringLiteral("message"), message}});
0734 }
0735 
0736 void ViewerPrivate::displaySplashPage(const QString &templateName, const QVariantHash &data, const QByteArray &domain)
0737 {
0738     if (mViewer) {
0739         mMsgDisplay = false;
0740         adjustLayout();
0741 
0742         GrantleeTheme::ThemeManager manager(QStringLiteral("splashPage"), QStringLiteral("splash.theme"), nullptr, QStringLiteral("messageviewer/about/"));
0743         GrantleeTheme::Theme theme = manager.theme(QStringLiteral("default"));
0744         if (theme.isValid()) {
0745             mViewer->setHtml(theme.render(templateName, data, domain), QUrl::fromLocalFile(theme.absolutePath() + QLatin1Char('/')));
0746         } else {
0747             qCDebug(MESSAGEVIEWER_LOG) << "Theme error: failed to find splash theme";
0748         }
0749         mViewer->show();
0750     }
0751 }
0752 
0753 void ViewerPrivate::enableMessageDisplay()
0754 {
0755     if (mMsgDisplay) {
0756         return;
0757     }
0758     mMsgDisplay = true;
0759     adjustLayout();
0760 }
0761 
0762 void ViewerPrivate::displayMessage()
0763 {
0764     showHideMimeTree();
0765 
0766     mNodeHelper->setOverrideCodec(mMessage.data(), overrideCodecName());
0767 
0768     if (mMessageItem.hasAttribute<MessageViewer::MessageDisplayFormatAttribute>()) {
0769         const MessageViewer::MessageDisplayFormatAttribute *const attr = mMessageItem.attribute<MessageViewer::MessageDisplayFormatAttribute>();
0770         setHtmlLoadExtOverride(attr->remoteContent());
0771         setDisplayFormatMessageOverwrite(attr->messageFormat());
0772     }
0773 
0774     adaptHtmlHeadSettings();
0775     htmlWriter()->begin();
0776     htmlWriter()->write(cssHelper()->htmlHead(mHtmlHeadSettings));
0777 
0778     if (!mMainWindow) {
0779         q->setWindowTitle(mMessage->subject()->asUnicodeString());
0780     }
0781 
0782     // Don't update here, parseMsg() can overwrite the HTML mode, which would lead to flicker.
0783     // It is updated right after parseMsg() instead.
0784     mColorBar->setMode(MimeTreeParser::Util::Normal, HtmlStatusBar::NoUpdate);
0785 
0786     if (mMessageItem.hasAttribute<Akonadi::ErrorAttribute>()) {
0787         // TODO: Insert link to clear error so that message might be resent
0788         const auto *const attr = mMessageItem.attribute<Akonadi::ErrorAttribute>();
0789         Q_ASSERT(attr);
0790         initializeColorFromScheme();
0791 
0792         htmlWriter()->write(QStringLiteral("<div style=\"background:%1;color:%2;border:1px solid %2\">%3</div>")
0793                                 .arg(mBackgroundError.name(), mForegroundError.name(), attr->message().toHtmlEscaped()));
0794         htmlWriter()->write(QStringLiteral("<p></p>"));
0795     }
0796 
0797     parseContent(mMessage.data());
0798     mMimePartTree->setRoot(mNodeHelper->messageWithExtraContent(mMessage.data()));
0799     mColorBar->update();
0800 
0801     htmlWriter()->write(cssHelper()->endBodyHtml());
0802     connect(mViewer, &MailWebEngineView::loadFinished, this, &ViewerPrivate::executeCustomScriptsAfterLoading, Qt::UniqueConnection);
0803     connect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotMessageRendered, Qt::UniqueConnection);
0804 
0805     htmlWriter()->end();
0806 }
0807 
0808 void ViewerPrivate::parseContent(KMime::Content *content)
0809 {
0810     Q_ASSERT(content != nullptr);
0811     mNodeHelper->removeTempFiles();
0812 
0813     // Check if any part of this message is a v-card
0814     // v-cards can be either text/x-vcard or text/directory, so we need to check
0815     // both.
0816     KMime::Content *vCardContent = findContentByType(content, "text/x-vcard");
0817     if (!vCardContent) {
0818         vCardContent = findContentByType(content, "text/directory");
0819     }
0820     bool hasVCard = false;
0821     if (vCardContent) {
0822         // ### FIXME: We should only do this if the vCard belongs to the sender,
0823         // ### i.e. if the sender's email address is contained in the vCard.
0824         const QByteArray vCard = vCardContent->decodedContent();
0825         KContacts::VCardConverter t;
0826         if (!t.parseVCards(vCard).isEmpty()) {
0827             hasVCard = true;
0828             mNodeHelper->writeNodeToTempFile(vCardContent);
0829         }
0830     }
0831 
0832     auto message = dynamic_cast<KMime::Message *>(content);
0833     bool onlySingleNode = mMessage.data() != content;
0834 
0835     // Pass control to the OTP now, which does the real work
0836     mNodeHelper->setNodeUnprocessed(mMessage.data(), true);
0837     MailViewerSource otpSource(this);
0838     MimeTreeParser::ObjectTreeParser otp(&otpSource, mNodeHelper);
0839 
0840     otp.setAllowAsync(!mPrinting);
0841     otp.parseObjectTree(content, onlySingleNode);
0842     htmlWriter()->setCodec(otp.plainTextContentCharset());
0843     if (message) {
0844         htmlWriter()->write(writeMessageHeader(message, hasVCard ? vCardContent : nullptr, true));
0845     }
0846 
0847     otpSource.render(otp.parsedPart(), onlySingleNode);
0848 
0849     // TODO: Setting the signature state to nodehelper is not enough, it should actually
0850     // be added to the store, so that the message list correctly displays the signature state
0851     // of messages that were parsed at least once
0852     // store encrypted/signed status information in the KMMessage
0853     //  - this can only be done *after* calling parseObjectTree()
0854     const MimeTreeParser::KMMsgEncryptionState encryptionState = mNodeHelper->overallEncryptionState(content);
0855     const MimeTreeParser::KMMsgSignatureState signatureState = mNodeHelper->overallSignatureState(content);
0856     mNodeHelper->setEncryptionState(content, encryptionState);
0857     // Don't reset the signature state to "not signed" (e.g. if one canceled the
0858     // decryption of a signed messages which has already been decrypted before).
0859     if (signatureState != MimeTreeParser::KMMsgNotSigned || mNodeHelper->signatureState(content) == MimeTreeParser::KMMsgSignatureStateUnknown) {
0860         mNodeHelper->setSignatureState(content, signatureState);
0861     }
0862 
0863     if (!onlySingleNode && isAutocryptEnabled(message)) {
0864         auto mixup = HeaderMixupNodeHelper(mNodeHelper, message);
0865         processAutocryptfromMail(mixup);
0866     }
0867 
0868     showHideMimeTree();
0869 }
0870 
0871 QString ViewerPrivate::writeMessageHeader(KMime::Message *aMsg, KMime::Content *vCardNode, bool topLevel)
0872 {
0873     if (!headerStylePlugin()) {
0874         qCCritical(MESSAGEVIEWER_LOG) << "trying to writeMessageHeader() without a header style set!";
0875         return {};
0876     }
0877     HeaderStyle *style = headerStylePlugin()->headerStyle();
0878     if (vCardNode) {
0879         style->setVCardName(mNodeHelper->asHREF(vCardNode, QStringLiteral("body")));
0880     } else {
0881         style->setVCardName(QString());
0882     }
0883     style->setHeaderStrategy(headerStylePlugin()->headerStrategy());
0884     style->setPrinting(mPrinting);
0885     style->setTopLevel(topLevel);
0886     style->setAllowAsync(true);
0887     style->setSourceObject(this);
0888     style->setNodeHelper(mNodeHelper);
0889     style->setAttachmentHtml(attachmentHtml());
0890     if (mMessageItem.isValid()) {
0891         Akonadi::MessageStatus status;
0892         status.setStatusFromFlags(mMessageItem.flags());
0893 
0894         style->setMessageStatus(status);
0895     } else {
0896         style->setReadOnlyMessage(true);
0897     }
0898 
0899     return style->format(aMsg);
0900 }
0901 
0902 void ViewerPrivate::initHtmlWidget()
0903 {
0904     if (!htmlWriter()) {
0905         mPartHtmlWriter = new WebEnginePartHtmlWriter(mViewer, nullptr);
0906         mHtmlWriter = mPartHtmlWriter;
0907     }
0908     connect(mViewer->page(), &QWebEnginePage::linkHovered, this, &ViewerPrivate::slotUrlOn);
0909     connect(mViewer, &MailWebEngineView::openUrl, this, &ViewerPrivate::slotUrlOpen, Qt::QueuedConnection);
0910     connect(mViewer, &MailWebEngineView::popupMenu, this, &ViewerPrivate::slotUrlPopup);
0911     connect(mViewer, &MailWebEngineView::wheelZoomChanged, this, &ViewerPrivate::slotWheelZoomChanged);
0912     connect(mViewer, &MailWebEngineView::messageMayBeAScam, this, &ViewerPrivate::slotMessageMayBeAScam);
0913     connect(mViewer, &MailWebEngineView::formSubmittedForbidden, [this]() {
0914         if (!mSubmittedFormWarning) {
0915             createSubmittedFormWarning();
0916         }
0917         mSubmittedFormWarning->showWarning();
0918     });
0919     connect(mViewer, &MailWebEngineView::mailTrackingFound, this, [this](const WebEngineViewer::BlockTrackingUrlInterceptor::TrackerBlackList &lst) {
0920         if (!mMailTrackingWarning) {
0921             createTrackingWarningWidget();
0922         }
0923         mMailTrackingWarning->addTracker(lst);
0924     });
0925     connect(mViewer, &MailWebEngineView::pageIsScrolledToBottom, this, &ViewerPrivate::pageIsScrolledToBottom);
0926     connect(mViewer, &MailWebEngineView::urlBlocked, this, &ViewerPrivate::slotUrlBlocked);
0927 }
0928 
0929 void ViewerPrivate::slotUrlBlocked(const QUrl &url)
0930 {
0931     mRemoteContentMenu->appendUrl(url.adjusted(QUrl::RemovePath | QUrl::RemovePort | QUrl::RemoveQuery).toString());
0932 }
0933 
0934 RemoteContentMenu *ViewerPrivate::remoteContentMenu() const
0935 {
0936     return mRemoteContentMenu;
0937 }
0938 
0939 void ViewerPrivate::applyZoomValue(qreal factor, bool saveConfig)
0940 {
0941     if (mZoomActionMenu) {
0942         if (factor >= 10 && factor <= 300) {
0943             if (!qFuzzyCompare(mZoomActionMenu->zoomFactor(), factor)) {
0944                 mZoomActionMenu->setZoomFactor(factor);
0945                 mZoomActionMenu->setWebViewerZoomFactor(factor / 100.0);
0946                 if (saveConfig) {
0947                     MessageViewer::MessageViewerSettings::self()->setZoomFactor(factor);
0948                 }
0949             }
0950         }
0951     }
0952 }
0953 
0954 void ViewerPrivate::setWebViewZoomFactor(qreal factor)
0955 {
0956     applyZoomValue(factor, false);
0957 }
0958 
0959 qreal ViewerPrivate::webViewZoomFactor() const
0960 {
0961     qreal zoomFactor = -1;
0962     if (mZoomActionMenu) {
0963         zoomFactor = mZoomActionMenu->zoomFactor();
0964     }
0965     return zoomFactor;
0966 }
0967 
0968 void ViewerPrivate::slotWheelZoomChanged(int numSteps)
0969 {
0970     const qreal factor = mZoomActionMenu->zoomFactor() + numSteps * 10;
0971     applyZoomValue(factor);
0972 }
0973 
0974 void ViewerPrivate::readConfig()
0975 {
0976     mMessageViewerRenderer->setCurrentWidget(mViewer);
0977     recreateCssHelper();
0978 
0979     mForceEmoticons = MessageViewer::MessageViewerSettings::self()->showEmoticons();
0980     if (mDisableEmoticonAction) {
0981         mDisableEmoticonAction->setChecked(!mForceEmoticons);
0982     }
0983     if (headerStylePlugin()) {
0984         headerStylePlugin()->headerStyle()->setShowEmoticons(mForceEmoticons);
0985     }
0986 
0987     mHtmlHeadSettings.fixedFont = MessageViewer::MessageViewerSettings::self()->useFixedFont();
0988     if (mToggleFixFontAction) {
0989         mToggleFixFontAction->setChecked(mHtmlHeadSettings.fixedFont);
0990     }
0991 
0992     mHtmlMailGlobalSetting = MessageViewer::MessageViewerSettings::self()->htmlMail();
0993 
0994     MessageViewer::Util::readGravatarConfig();
0995     if (mHeaderStyleMenuManager) {
0996         mHeaderStyleMenuManager->readConfig();
0997     }
0998 
0999     setAttachmentStrategy(AttachmentStrategy::create(MessageViewer::MessageViewerSettings::self()->attachmentStrategy()));
1000     KToggleAction *raction = actionForAttachmentStrategy(attachmentStrategy());
1001     if (raction) {
1002         raction->setChecked(true);
1003     }
1004 
1005     adjustLayout();
1006 
1007     readGlobalOverrideCodec();
1008     mViewer->readConfig();
1009     mViewer->settings()->setFontSize(QWebEngineSettings::MinimumFontSize, MessageViewer::MessageViewerSettings::self()->minimumFontSize());
1010     mViewer->settings()->setFontSize(QWebEngineSettings::MinimumLogicalFontSize, MessageViewer::MessageViewerSettings::self()->minimumFontSize());
1011     if (mMessage) {
1012         update();
1013     }
1014     mColorBar->update();
1015     applyZoomValue(MessageViewer::MessageViewerSettings::self()->zoomFactor(), false);
1016 }
1017 
1018 void ViewerPrivate::recreateCssHelper()
1019 {
1020     mMessageViewerRenderer->recreateCssHelper();
1021 }
1022 
1023 void ViewerPrivate::hasMultiMessages(bool messages)
1024 {
1025     if (!mShowNextMessageWidget) {
1026         createShowNextMessageWidget();
1027         mShowNextMessageWidget->setVisible(messages);
1028     }
1029 }
1030 
1031 void ViewerPrivate::slotGeneralFontChanged()
1032 {
1033     recreateCssHelper();
1034     if (mMessage) {
1035         update();
1036     }
1037 }
1038 
1039 void ViewerPrivate::writeConfig(bool sync)
1040 {
1041     MessageViewer::MessageViewerSettings::self()->setShowEmoticons(mForceEmoticons);
1042     MessageViewer::MessageViewerSettings::self()->setUseFixedFont(mHtmlHeadSettings.fixedFont);
1043     if (attachmentStrategy()) {
1044         MessageViewer::MessageViewerSettings::self()->setAttachmentStrategy(QLatin1StringView(attachmentStrategy()->name()));
1045     }
1046     saveSplitterSizes();
1047     if (sync) {
1048         Q_EMIT requestConfigSync();
1049     }
1050 }
1051 
1052 const AttachmentStrategy *ViewerPrivate::attachmentStrategy() const
1053 {
1054     return mAttachmentStrategy;
1055 }
1056 
1057 void ViewerPrivate::setAttachmentStrategy(const AttachmentStrategy *strategy)
1058 {
1059     if (mAttachmentStrategy == strategy) {
1060         return;
1061     }
1062     mAttachmentStrategy = strategy ? strategy : AttachmentStrategy::smart();
1063     update(MimeTreeParser::Force);
1064 }
1065 
1066 QString ViewerPrivate::overrideEncoding() const
1067 {
1068     return mOverrideEncoding;
1069 }
1070 
1071 void ViewerPrivate::setOverrideEncoding(const QString &encoding)
1072 {
1073     if (encoding == mOverrideEncoding) {
1074         return;
1075     }
1076 
1077     mOverrideEncoding = encoding;
1078     if (mSelectEncodingAction) {
1079         if (encoding.isEmpty()) {
1080             mSelectEncodingAction->setCurrentItem(0);
1081         } else {
1082             const QStringList encodings = mSelectEncodingAction->items();
1083             int i = 0;
1084             for (QStringList::const_iterator it = encodings.constBegin(), end = encodings.constEnd(); it != end; ++it, ++i) {
1085                 if (MimeTreeParser::NodeHelper::encodingForName(*it) == encoding) {
1086                     mSelectEncodingAction->setCurrentItem(i);
1087                     break;
1088                 }
1089             }
1090             if (i == encodings.size()) {
1091                 // the value of encoding is unknown => use Auto
1092                 qCWarning(MESSAGEVIEWER_LOG) << "Unknown override character encoding" << encoding << ". Using Auto instead.";
1093                 mSelectEncodingAction->setCurrentItem(0);
1094                 mOverrideEncoding.clear();
1095             }
1096         }
1097     }
1098     update(MimeTreeParser::Force);
1099 }
1100 
1101 void ViewerPrivate::setPrinting(bool enable)
1102 {
1103     mPrinting = enable;
1104 }
1105 
1106 bool ViewerPrivate::printingMode() const
1107 {
1108     return mPrinting;
1109 }
1110 
1111 void ViewerPrivate::printMessage(const Akonadi::Item &message)
1112 {
1113     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintMessage);
1114     connect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintMessage);
1115     // need to set htmlLoadExtOverride() when we set Item otherwise this settings is reset
1116     setMessageItem(message, MimeTreeParser::Force, htmlLoadExtOverride());
1117 }
1118 
1119 void ViewerPrivate::printPreviewMessage(const Akonadi::Item &message)
1120 {
1121     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintPreview);
1122     connect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintPreview);
1123     setMessageItem(message, MimeTreeParser::Force, htmlLoadExtOverride());
1124 }
1125 
1126 void ViewerPrivate::resetStateForNewMessage()
1127 {
1128     mDkimWidgetInfo->clear();
1129     mHtmlLoadExtOverride = false;
1130     mClickedUrl.clear();
1131     mImageUrl.clear();
1132     enableMessageDisplay(); // just to make sure it's on
1133     mMessage.reset();
1134     mNodeHelper->clear();
1135     mMessagePartNode = nullptr;
1136     mMimePartTree->clearModel();
1137     if (mViewer) {
1138         mViewer->clearRelativePosition();
1139         mViewer->hideAccessKeys();
1140     }
1141     if (!mPrinting) {
1142         setShowSignatureDetails(false);
1143     }
1144     mViewerPluginToolManager->closeAllTools();
1145     if (mScamDetectionWarning) {
1146         mScamDetectionWarning->setVisible(false);
1147     }
1148     if (mOpenSavedFileFolderWidget) {
1149         mOpenSavedFileFolderWidget->setVisible(false);
1150     }
1151     if (mSubmittedFormWarning) {
1152         mSubmittedFormWarning->setVisible(false);
1153     }
1154     if (mMailTrackingWarning) {
1155         mMailTrackingWarning->hideAndClear();
1156     }
1157     mRemoteContentMenu->clearUrls();
1158 
1159     if (mPrinting) {
1160         if (MessageViewer::MessageViewerSettings::self()->respectExpandCollapseSettings()) {
1161             if (MessageViewer::MessageViewerSettings::self()->showExpandQuotesMark()) {
1162                 mLevelQuote = MessageViewer::MessageViewerSettings::self()->collapseQuoteLevelSpin() - 1;
1163             } else {
1164                 mLevelQuote = -1;
1165             }
1166         } else {
1167             mLevelQuote = -1;
1168         }
1169     } else {
1170         //        mDisplayFormatMessageOverwrite
1171         //            = (mDisplayFormatMessageOverwrite
1172         //               == MessageViewer::Viewer::UseGlobalSetting) ? MessageViewer::Viewer::UseGlobalSetting
1173         //              :
1174         //              MessageViewer::Viewer::Unknown;
1175     }
1176 }
1177 
1178 void ViewerPrivate::setMessageInternal(const KMime::Message::Ptr &message, MimeTreeParser::UpdateMode updateMode)
1179 {
1180     mViewerPluginToolManager->updateActions(mMessageItem);
1181     mMessage = message;
1182     if (message) {
1183         mNodeHelper->setOverrideCodec(mMessage.data(), overrideCodecName());
1184     }
1185 
1186     mMimePartTree->setRoot(mNodeHelper->messageWithExtraContent(message.data()));
1187     update(updateMode);
1188 }
1189 
1190 void ViewerPrivate::assignMessageItem(const Akonadi::Item &item)
1191 {
1192     mMessageItem = item;
1193 }
1194 
1195 void ViewerPrivate::setMessageItem(const Akonadi::Item &item, MimeTreeParser::UpdateMode updateMode, bool forceHtmlLoadExtOverride)
1196 {
1197     resetStateForNewMessage();
1198     if (forceHtmlLoadExtOverride) {
1199         setHtmlLoadExtOverride(true);
1200     }
1201 
1202     const auto itemsMonitoredEx = mMonitor.itemsMonitoredEx();
1203 
1204     for (const Akonadi::Item::Id monitoredId : itemsMonitoredEx) {
1205         mMonitor.setItemMonitored(Akonadi::Item(monitoredId), false);
1206     }
1207     Q_ASSERT(mMonitor.itemsMonitoredEx().isEmpty());
1208 
1209     assignMessageItem(item);
1210     if (mMessageItem.isValid()) {
1211         mMonitor.setItemMonitored(mMessageItem, true);
1212     }
1213 
1214     if (!mMessageItem.hasPayload<KMime::Message::Ptr>()) {
1215         if (mMessageItem.isValid()) {
1216             qCWarning(MESSAGEVIEWER_LOG) << "Payload is not a MessagePtr!";
1217         }
1218         return;
1219     }
1220     if (!mPrinting) {
1221         if (MessageViewer::MessageViewerSettings::self()->enabledDkim()) {
1222             if (messageIsInSpecialFolder()) {
1223                 mDkimWidgetInfo->clear();
1224             } else {
1225                 mDkimWidgetInfo->setCurrentItemId(mMessageItem.id());
1226                 MessageViewer::DKIMManager::self()->checkDKim(mMessageItem);
1227             }
1228         }
1229     }
1230 
1231     setMessageInternal(mMessageItem.payload<KMime::Message::Ptr>(), updateMode);
1232 }
1233 
1234 bool ViewerPrivate::messageIsInSpecialFolder() const
1235 {
1236     const Akonadi::Collection parentCollection = mMessageItem.parentCollection();
1237     if ((Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::SentMail) != parentCollection)
1238         && (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Outbox) != parentCollection)
1239         && (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Templates) != parentCollection)
1240         && (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Drafts) != parentCollection)) {
1241         return false;
1242     } else {
1243         return true;
1244     }
1245 }
1246 
1247 void ViewerPrivate::setMessage(const KMime::Message::Ptr &aMsg, MimeTreeParser::UpdateMode updateMode)
1248 {
1249     resetStateForNewMessage();
1250 
1251     Akonadi::Item item;
1252     item.setMimeType(KMime::Message::mimeType());
1253     item.setPayload(aMsg);
1254     assignMessageItem(item);
1255 
1256     setMessageInternal(aMsg, updateMode);
1257 }
1258 
1259 void ViewerPrivate::setMessagePart(KMime::Content *node)
1260 {
1261     // Cancel scheduled updates of the reader window, as that would stop the
1262     // timer of the HTML writer, which would make viewing attachment not work
1263     // anymore as not all HTML is written to the HTML part.
1264     // We're updating the reader window here ourselves anyway.
1265     mUpdateReaderWinTimer.stop();
1266 
1267     if (node) {
1268         mMessagePartNode = node;
1269         if (node->bodyIsMessage()) {
1270             mMainWindow->setWindowTitle(node->bodyAsMessage()->subject()->asUnicodeString());
1271         } else {
1272             QString windowTitle = MimeTreeParser::NodeHelper::fileName(node);
1273             if (windowTitle.isEmpty()) {
1274                 windowTitle = node->contentDescription()->asUnicodeString();
1275             }
1276             if (!windowTitle.isEmpty()) {
1277                 mMainWindow->setWindowTitle(i18nc("@title:window", "View Attachment: %1", windowTitle));
1278             }
1279         }
1280 
1281         htmlWriter()->begin();
1282         adaptHtmlHeadSettings();
1283         htmlWriter()->write(cssHelper()->htmlHead(mHtmlHeadSettings));
1284 
1285         parseContent(node);
1286 
1287         htmlWriter()->write(cssHelper()->endBodyHtml());
1288         htmlWriter()->end();
1289     }
1290 }
1291 
1292 void ViewerPrivate::adaptHtmlHeadSettings()
1293 {
1294     mHtmlHeadSettings.htmlFormat = htmlMail();
1295 }
1296 
1297 void ViewerPrivate::showHideMimeTree()
1298 {
1299     if (mimePartTreeIsEmpty()) {
1300         mMimePartTree->hide();
1301         return;
1302     }
1303     bool showMimeTree = false;
1304     if (MessageViewer::MessageViewerSettings::self()->mimeTreeMode2() == MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Always) {
1305         mMimePartTree->show();
1306         showMimeTree = true;
1307     } else {
1308         // don't rely on QSplitter maintaining sizes for hidden widgets:
1309         saveSplitterSizes();
1310         mMimePartTree->hide();
1311         showMimeTree = false;
1312     }
1313     if (mToggleMimePartTreeAction && (mToggleMimePartTreeAction->isChecked() != showMimeTree)) {
1314         mToggleMimePartTreeAction->setChecked(showMimeTree);
1315     }
1316 }
1317 
1318 void ViewerPrivate::attachmentViewMessage(const KMime::Message::Ptr &message)
1319 {
1320     Q_ASSERT(message);
1321     Q_EMIT showMessage(message, overrideEncoding());
1322 }
1323 
1324 void ViewerPrivate::adjustLayout()
1325 {
1326     const int mimeH = MessageViewer::MessageViewerSettings::self()->mimePaneHeight();
1327     const int messageH = MessageViewer::MessageViewerSettings::self()->messagePaneHeight();
1328     const QList<int> splitterSizes{messageH, mimeH};
1329 
1330     mSplitter->addWidget(mMimePartTree);
1331     mSplitter->setSizes(splitterSizes);
1332 
1333     if (MessageViewer::MessageViewerSettings::self()->mimeTreeMode2() == MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Always && mMsgDisplay) {
1334         mMimePartTree->show();
1335     } else {
1336         mMimePartTree->hide();
1337     }
1338 
1339     if (mMsgDisplay) {
1340         mColorBar->show();
1341     } else {
1342         mColorBar->hide();
1343     }
1344 }
1345 
1346 void ViewerPrivate::saveSplitterSizes() const
1347 {
1348     if (!mSplitter || !mMimePartTree) {
1349         return;
1350     }
1351     if (mMimePartTree->isHidden()) {
1352         return; // don't rely on QSplitter maintaining sizes for hidden widgets.
1353     }
1354     MessageViewer::MessageViewerSettings::self()->setMimePaneHeight(mSplitter->sizes().at(1));
1355     MessageViewer::MessageViewerSettings::self()->setMessagePaneHeight(mSplitter->sizes().at(0));
1356 }
1357 
1358 void ViewerPrivate::createWidgets()
1359 {
1360     // TODO: Make a MDN bar similar to Mozillas password bar and show MDNs here as soon as a
1361     //      MDN enabled message is shown.
1362     auto vlay = new QVBoxLayout(q);
1363     vlay->setContentsMargins({});
1364     mSplitter = new QSplitter(Qt::Vertical, q);
1365     connect(mSplitter, &QSplitter::splitterMoved, this, &ViewerPrivate::saveSplitterSizes);
1366     mSplitter->setObjectName(QLatin1StringView("mSplitter"));
1367     mSplitter->setChildrenCollapsible(false);
1368     vlay->addWidget(mSplitter);
1369     mMimePartTree = new MimePartTreeView(mSplitter);
1370     mMimePartTree->setMinimumHeight(10);
1371     connect(mMimePartTree, &QAbstractItemView::activated, this, &ViewerPrivate::slotMimePartSelected);
1372     connect(mMimePartTree, &QWidget::customContextMenuRequested, this, &ViewerPrivate::slotMimeTreeContextMenuRequested);
1373 
1374     mBox = new QWidget(mSplitter);
1375     auto mBoxHBoxLayout = new QHBoxLayout(mBox);
1376     mBoxHBoxLayout->setContentsMargins({});
1377     mBoxHBoxLayout->setSpacing(0);
1378 
1379     mColorBar = new HtmlStatusBar(mBox);
1380     mBoxHBoxLayout->addWidget(mColorBar);
1381     mReaderBox = new QWidget(mBox);
1382     mReaderBoxVBoxLayout = new QVBoxLayout(mReaderBox);
1383     mReaderBoxVBoxLayout->setContentsMargins({});
1384     mReaderBoxVBoxLayout->setSpacing(0);
1385     mBoxHBoxLayout->addWidget(mReaderBox);
1386 
1387     mColorBar->setObjectName(QLatin1StringView("mColorBar"));
1388     mColorBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
1389 
1390 #ifdef HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
1391     mTextToSpeechContainerWidget = new TextEditTextToSpeech::TextToSpeechContainerWidget(mReaderBox);
1392     mTextToSpeechContainerWidget->setObjectName(QLatin1StringView("TextToSpeechContainerWidget"));
1393     mReaderBoxVBoxLayout->addWidget(mTextToSpeechContainerWidget);
1394 #endif
1395     mViewer = new MailWebEngineView(mActionCollection, mReaderBox);
1396     mViewer->setViewer(this);
1397     mReaderBoxVBoxLayout->addWidget(mViewer);
1398     mViewer->setObjectName(QLatin1StringView("mViewer"));
1399 
1400     mViewerPluginToolManager = new MessageViewer::ViewerPluginToolManager(mReaderBox, this);
1401     mViewerPluginToolManager->setActionCollection(mActionCollection);
1402     mViewerPluginToolManager->setPluginName(QStringLiteral("messageviewer"));
1403     mViewerPluginToolManager->setPluginDirectory(QStringLiteral("pim6/messageviewer/viewerplugin"));
1404     if (!mViewerPluginToolManager->initializePluginList()) {
1405         qCWarning(MESSAGEVIEWER_LOG) << " Impossible to initialize plugins";
1406     }
1407     mViewerPluginToolManager->createView();
1408     connect(mViewerPluginToolManager, &MessageViewer::ViewerPluginToolManager::activatePlugin, this, &ViewerPrivate::slotActivatePlugin);
1409 
1410     mSliderContainer = new TextAddonsWidgets::SlideContainer(mReaderBox);
1411     mSliderContainer->setObjectName(QLatin1StringView("slidercontainer"));
1412     mReaderBoxVBoxLayout->addWidget(mSliderContainer);
1413     mFindBar = new WebEngineViewer::FindBarWebEngineView(mViewer, q);
1414     connect(mFindBar, &WebEngineViewer::FindBarWebEngineView::hideFindBar, mSliderContainer, &TextAddonsWidgets::SlideContainer::slideOut);
1415     mSliderContainer->setContent(mFindBar);
1416 
1417     mSplitter->setStretchFactor(mSplitter->indexOf(mMimePartTree), 0);
1418 }
1419 
1420 void ViewerPrivate::createScamDetectionWarningWidget()
1421 {
1422     mScamDetectionWarning = new ScamDetectionWarningWidget(mReaderBox);
1423     mScamDetectionWarning->setObjectName(QLatin1StringView("scandetectionwarning"));
1424     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::showDetails, mViewer, &MailWebEngineView::slotShowDetails);
1425     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::moveMessageToTrash, this, &ViewerPrivate::moveMessageToTrash);
1426     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::messageIsNotAScam, this, &ViewerPrivate::slotMessageIsNotAScam);
1427     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::addToWhiteList, this, &ViewerPrivate::slotAddToWhiteList);
1428     mReaderBoxVBoxLayout->insertWidget(0, mScamDetectionWarning);
1429 }
1430 
1431 void ViewerPrivate::createTrackingWarningWidget()
1432 {
1433     mMailTrackingWarning = new WebEngineViewer::TrackingWarningWidget(mReaderBox);
1434     mMailTrackingWarning->setObjectName(QLatin1StringView("mailtrackingwarning"));
1435     mReaderBoxVBoxLayout->insertWidget(0, mMailTrackingWarning);
1436 }
1437 
1438 void ViewerPrivate::createOpenSavedFileFolderWidget()
1439 {
1440     mOpenSavedFileFolderWidget = new OpenSavedFileFolderWidget(mReaderBox);
1441     mOpenSavedFileFolderWidget->setObjectName(QLatin1StringView("opensavefilefolderwidget"));
1442     mReaderBoxVBoxLayout->insertWidget(0, mOpenSavedFileFolderWidget);
1443 }
1444 
1445 void ViewerPrivate::createSubmittedFormWarning()
1446 {
1447     mSubmittedFormWarning = new WebEngineViewer::SubmittedFormWarningWidget(mReaderBox);
1448     mSubmittedFormWarning->setObjectName(QLatin1StringView("submittedformwarning"));
1449     mReaderBoxVBoxLayout->insertWidget(0, mSubmittedFormWarning);
1450 }
1451 
1452 void ViewerPrivate::slotStyleChanged(MessageViewer::HeaderStylePlugin *plugin)
1453 {
1454     cssHelper()->setHeaderPlugin(plugin);
1455     mHeaderStylePlugin = plugin;
1456     update(MimeTreeParser::Force);
1457 }
1458 
1459 void ViewerPrivate::slotStyleUpdated()
1460 {
1461     update(MimeTreeParser::Force);
1462 }
1463 
1464 void ViewerPrivate::createActions()
1465 {
1466     KActionCollection *ac = mActionCollection;
1467     mHeaderStyleMenuManager = new MessageViewer::HeaderStyleMenuManager(ac, this);
1468     connect(mHeaderStyleMenuManager, &MessageViewer::HeaderStyleMenuManager::styleChanged, this, &ViewerPrivate::slotStyleChanged);
1469     connect(mHeaderStyleMenuManager, &MessageViewer::HeaderStyleMenuManager::styleUpdated, this, &ViewerPrivate::slotStyleUpdated);
1470     if (!ac) {
1471         return;
1472     }
1473     mZoomActionMenu = new WebEngineViewer::ZoomActionMenu(this);
1474     connect(mZoomActionMenu, &WebEngineViewer::ZoomActionMenu::zoomChanged, this, &ViewerPrivate::slotZoomChanged);
1475     mZoomActionMenu->setActionCollection(ac);
1476     mZoomActionMenu->createZoomActions();
1477 
1478     // attachment style
1479     auto attachmentMenu = new KActionMenu(i18nc("View->", "&Attachments"), this);
1480     ac->addAction(QStringLiteral("view_attachments"), attachmentMenu);
1481     MessageViewer::Util::addHelpTextAction(attachmentMenu, i18n("Choose display style of attachments"));
1482 
1483     auto group = new QActionGroup(this);
1484     auto raction = new KToggleAction(i18nc("View->attachments->", "&As Icons"), this);
1485     ac->addAction(QStringLiteral("view_attachments_as_icons"), raction);
1486     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotIconicAttachments);
1487     MessageViewer::Util::addHelpTextAction(raction, i18n("Show all attachments as icons. Click to see them."));
1488     group->addAction(raction);
1489     attachmentMenu->addAction(raction);
1490 
1491     raction = new KToggleAction(i18nc("View->attachments->", "&Smart"), this);
1492     ac->addAction(QStringLiteral("view_attachments_smart"), raction);
1493     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotSmartAttachments);
1494     MessageViewer::Util::addHelpTextAction(raction, i18n("Show attachments as suggested by sender."));
1495     group->addAction(raction);
1496     attachmentMenu->addAction(raction);
1497 
1498     raction = new KToggleAction(i18nc("View->attachments->", "&Inline"), this);
1499     ac->addAction(QStringLiteral("view_attachments_inline"), raction);
1500     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotInlineAttachments);
1501     MessageViewer::Util::addHelpTextAction(raction, i18n("Show all attachments inline (if possible)"));
1502     group->addAction(raction);
1503     attachmentMenu->addAction(raction);
1504 
1505     raction = new KToggleAction(i18nc("View->attachments->", "&Hide"), this);
1506     ac->addAction(QStringLiteral("view_attachments_hide"), raction);
1507     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotHideAttachments);
1508     MessageViewer::Util::addHelpTextAction(raction, i18n("Do not show attachments in the message viewer"));
1509     group->addAction(raction);
1510     attachmentMenu->addAction(raction);
1511 
1512     mHeaderOnlyAttachmentsAction = new KToggleAction(i18nc("View->attachments->", "In Header Only"), this);
1513     ac->addAction(QStringLiteral("view_attachments_headeronly"), mHeaderOnlyAttachmentsAction);
1514     connect(mHeaderOnlyAttachmentsAction, &QAction::triggered, this, &ViewerPrivate::slotHeaderOnlyAttachments);
1515     MessageViewer::Util::addHelpTextAction(mHeaderOnlyAttachmentsAction, i18n("Show Attachments only in the header of the mail"));
1516     group->addAction(mHeaderOnlyAttachmentsAction);
1517     attachmentMenu->addAction(mHeaderOnlyAttachmentsAction);
1518 
1519     // Set Encoding submenu
1520     mSelectEncodingAction = new KSelectAction(QIcon::fromTheme(QStringLiteral("character-set")), i18n("&Set Encoding"), this);
1521     mSelectEncodingAction->setToolBarMode(KSelectAction::MenuMode);
1522     ac->addAction(QStringLiteral("encoding"), mSelectEncodingAction);
1523     connect(mSelectEncodingAction, &KSelectAction::indexTriggered, this, &ViewerPrivate::slotSetEncoding);
1524     QStringList encodings = MimeTreeParser::NodeHelper::supportedEncodings(false);
1525     encodings.prepend(i18n("Auto"));
1526     mSelectEncodingAction->setItems(encodings);
1527     mSelectEncodingAction->setCurrentItem(0);
1528 
1529     //
1530     // Message Menu
1531     //
1532 
1533     // copy selected text to clipboard
1534     mCopyAction = ac->addAction(KStandardAction::Copy, QStringLiteral("kmail_copy"));
1535     mCopyAction->setText(i18n("Copy Text"));
1536     connect(mCopyAction, &QAction::triggered, this, &ViewerPrivate::slotCopySelectedText);
1537 
1538     connect(mViewer, &MailWebEngineView::selectionChanged, this, &ViewerPrivate::viewerSelectionChanged);
1539     viewerSelectionChanged();
1540 
1541     // copy all text to clipboard
1542     mSelectAllAction = new QAction(i18n("Select All Text"), this);
1543     ac->addAction(QStringLiteral("mark_all_text"), mSelectAllAction);
1544     connect(mSelectAllAction, &QAction::triggered, this, &ViewerPrivate::selectAll);
1545     ac->setDefaultShortcut(mSelectAllAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
1546 
1547     // copy Email address to clipboard
1548     mCopyURLAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Link Address"), this);
1549     ac->addAction(QStringLiteral("copy_url"), mCopyURLAction);
1550     connect(mCopyURLAction, &QAction::triggered, this, &ViewerPrivate::slotUrlCopy);
1551 
1552     // open URL
1553     mUrlOpenAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open URL"), this);
1554     ac->addAction(QStringLiteral("open_url"), mUrlOpenAction);
1555     connect(mUrlOpenAction, &QAction::triggered, this, &ViewerPrivate::slotOpenUrl);
1556 
1557     // use fixed font
1558     mToggleFixFontAction = new KToggleAction(i18n("Use Fi&xed Font"), this);
1559     ac->addAction(QStringLiteral("toggle_fixedfont"), mToggleFixFontAction);
1560     connect(mToggleFixFontAction, &QAction::triggered, this, &ViewerPrivate::slotToggleFixedFont);
1561     ac->setDefaultShortcut(mToggleFixFontAction, QKeySequence(Qt::Key_X));
1562 
1563     // Show message structure viewer
1564     mToggleMimePartTreeAction = new KToggleAction(i18n("Show Message Structure"), this);
1565     ac->addAction(QStringLiteral("toggle_mimeparttree"), mToggleMimePartTreeAction);
1566     connect(mToggleMimePartTreeAction, &QAction::toggled, this, &ViewerPrivate::slotToggleMimePartTree);
1567     QKeyCombination combinationKeys(Qt::CTRL | Qt::ALT, Qt::Key_D);
1568     ac->setDefaultShortcut(mToggleMimePartTreeAction, combinationKeys);
1569     mViewSourceAction = new QAction(i18n("&View Source"), this);
1570     ac->addAction(QStringLiteral("view_source"), mViewSourceAction);
1571     connect(mViewSourceAction, &QAction::triggered, this, &ViewerPrivate::slotShowMessageSource);
1572     ac->setDefaultShortcut(mViewSourceAction, QKeySequence(Qt::Key_V));
1573 
1574     mSaveMessageAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Message..."), this);
1575     ac->addAction(QStringLiteral("save_message"), mSaveMessageAction);
1576     connect(mSaveMessageAction, &QAction::triggered, this, &ViewerPrivate::slotSaveMessage);
1577     // Laurent: conflict with kmail shortcut
1578     // mSaveMessageAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
1579 
1580     mSaveMessageDisplayFormat = new QAction(i18n("&Save Display Format"), this);
1581     ac->addAction(QStringLiteral("save_message_display_format"), mSaveMessageDisplayFormat);
1582     connect(mSaveMessageDisplayFormat, &QAction::triggered, this, &ViewerPrivate::slotSaveMessageDisplayFormat);
1583 
1584     mResetMessageDisplayFormat = new QAction(i18n("&Reset Display Format"), this);
1585     ac->addAction(QStringLiteral("reset_message_display_format"), mResetMessageDisplayFormat);
1586     connect(mResetMessageDisplayFormat, &QAction::triggered, this, &ViewerPrivate::slotResetMessageDisplayFormat);
1587 
1588     //
1589     // Scroll actions
1590     //
1591     mScrollUpAction = new QAction(i18n("Scroll Message Up"), this);
1592     ac->setDefaultShortcut(mScrollUpAction, QKeySequence(Qt::Key_Up));
1593     ac->addAction(QStringLiteral("scroll_up"), mScrollUpAction);
1594     connect(mScrollUpAction, &QAction::triggered, q, &Viewer::slotScrollUp);
1595 
1596     mScrollDownAction = new QAction(i18n("Scroll Message Down"), this);
1597     ac->setDefaultShortcut(mScrollDownAction, QKeySequence(Qt::Key_Down));
1598     ac->addAction(QStringLiteral("scroll_down"), mScrollDownAction);
1599     connect(mScrollDownAction, &QAction::triggered, q, &Viewer::slotScrollDown);
1600 
1601     mScrollUpMoreAction = new QAction(i18n("Scroll Message Up (More)"), this);
1602     ac->setDefaultShortcut(mScrollUpMoreAction, QKeySequence(Qt::Key_PageUp));
1603     ac->addAction(QStringLiteral("scroll_up_more"), mScrollUpMoreAction);
1604     connect(mScrollUpMoreAction, &QAction::triggered, q, &Viewer::slotScrollPrior);
1605 
1606     mScrollDownMoreAction = new QAction(i18n("Scroll Message Down (More)"), this);
1607     ac->setDefaultShortcut(mScrollDownMoreAction, QKeySequence(Qt::Key_PageDown));
1608     ac->addAction(QStringLiteral("scroll_down_more"), mScrollDownMoreAction);
1609     connect(mScrollDownMoreAction, &QAction::triggered, q, &Viewer::slotScrollNext);
1610 
1611     //
1612     // Actions not in menu
1613     //
1614 
1615     // Toggle HTML display mode.
1616     mToggleDisplayModeAction = new KToggleAction(i18n("Toggle HTML Display Mode"), this);
1617     ac->addAction(QStringLiteral("toggle_html_display_mode"), mToggleDisplayModeAction);
1618     ac->setDefaultShortcut(mToggleDisplayModeAction, QKeySequence(Qt::SHIFT | Qt::Key_H));
1619     connect(mToggleDisplayModeAction, &QAction::triggered, this, &ViewerPrivate::slotToggleHtmlMode);
1620     MessageViewer::Util::addHelpTextAction(mToggleDisplayModeAction, i18n("Toggle display mode between HTML and plain text"));
1621 
1622     // Load external reference
1623     auto loadExternalReferenceAction = new QAction(i18n("Load external references"), this);
1624     ac->addAction(QStringLiteral("load_external_reference"), loadExternalReferenceAction);
1625     ac->setDefaultShortcut(loadExternalReferenceAction, QKeySequence(Qt::SHIFT | Qt::CTRL | Qt::Key_R));
1626     connect(loadExternalReferenceAction, &QAction::triggered, this, &ViewerPrivate::slotLoadExternalReference);
1627     MessageViewer::Util::addHelpTextAction(loadExternalReferenceAction, i18n("Load external references from the Internet for this message."));
1628 #ifdef HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
1629     mSpeakTextAction = new QAction(i18n("Speak Text"), this);
1630     mSpeakTextAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
1631     ac->addAction(QStringLiteral("speak_text"), mSpeakTextAction);
1632     connect(mSpeakTextAction, &QAction::triggered, this, &ViewerPrivate::slotSpeakText);
1633 #endif
1634     auto purposeMenuWidget = new ViewerPurposeMenuWidget(mViewer, this);
1635     mShareTextAction = new QAction(i18n("Share Text..."), this);
1636     mShareTextAction->setMenu(purposeMenuWidget->menu());
1637     mShareTextAction->setIcon(QIcon::fromTheme(QStringLiteral("document-share")));
1638     ac->addAction(QStringLiteral("purpose_share_text_menu"), mShareTextAction);
1639     purposeMenuWidget->setViewer(mViewer);
1640     connect(purposeMenuWidget, &ViewerPurposeMenuWidget::shareError, this, [this](const QString &message) {
1641         if (!mPurposeMenuMessageWidget) {
1642             createPurposeMenuMessageWidget();
1643         }
1644         mPurposeMenuMessageWidget->slotShareError(message);
1645     });
1646     connect(purposeMenuWidget, &ViewerPurposeMenuWidget::shareSuccess, this, [this](const QString &message) {
1647         if (!mPurposeMenuMessageWidget) {
1648             createPurposeMenuMessageWidget();
1649         }
1650         mPurposeMenuMessageWidget->slotShareSuccess(message);
1651     });
1652 
1653     mCopyImageLocation = new QAction(i18n("Copy Image Location"), this);
1654     mCopyImageLocation->setIcon(QIcon::fromTheme(QStringLiteral("view-media-visualization")));
1655     ac->addAction(QStringLiteral("copy_image_location"), mCopyImageLocation);
1656     ac->setShortcutsConfigurable(mCopyImageLocation, false);
1657     connect(mCopyImageLocation, &QAction::triggered, this, &ViewerPrivate::slotCopyImageLocation);
1658 
1659     mFindInMessageAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("&Find in Message..."), this);
1660     ac->addAction(QStringLiteral("find_in_messages"), mFindInMessageAction);
1661     connect(mFindInMessageAction, &QAction::triggered, this, &ViewerPrivate::slotFind);
1662     ac->setDefaultShortcut(mFindInMessageAction, KStandardShortcut::find().first());
1663 
1664     mShareServiceUrlMenu = mShareServiceManager->menu();
1665     ac->addAction(QStringLiteral("shareservice_menu"), mShareServiceUrlMenu);
1666     connect(mShareServiceManager, &PimCommon::ShareServiceUrlManager::serviceUrlSelected, this, &ViewerPrivate::slotServiceUrlSelected);
1667 
1668     mDisableEmoticonAction = new KToggleAction(i18n("Disable Emoticon"), this);
1669     ac->addAction(QStringLiteral("disable_emoticon"), mDisableEmoticonAction);
1670     connect(mDisableEmoticonAction, &QAction::triggered, this, &ViewerPrivate::slotToggleEmoticons);
1671 
1672     // Don't translate it.
1673     mDevelopmentToolsAction = new QAction(QStringLiteral("Development Tools"), this);
1674     ac->addAction(QStringLiteral("development_tools"), mDevelopmentToolsAction);
1675     ac->setDefaultShortcut(mDevelopmentToolsAction, QKeySequence(Qt::SHIFT | Qt::CTRL | Qt::Key_I));
1676 
1677     connect(mDevelopmentToolsAction, &QAction::triggered, this, &ViewerPrivate::slotShowDevelopmentTools);
1678 }
1679 
1680 void ViewerPrivate::createShowNextMessageWidget()
1681 {
1682     mShowNextMessageWidget = new MessageViewer::ShowNextMessageWidget(mReaderBox);
1683     mShowNextMessageWidget->setObjectName(QLatin1StringView("shownextmessagewidget"));
1684     mReaderBoxVBoxLayout->insertWidget(0, mShowNextMessageWidget);
1685     connect(mShowNextMessageWidget, &ShowNextMessageWidget::showPreviousMessage, this, &ViewerPrivate::showPreviousMessage);
1686     connect(mShowNextMessageWidget, &ShowNextMessageWidget::showNextMessage, this, &ViewerPrivate::showNextMessage);
1687 }
1688 
1689 void ViewerPrivate::createPurposeMenuMessageWidget()
1690 {
1691     mPurposeMenuMessageWidget = new PimCommon::PurposeMenuMessageWidget(mReaderBox);
1692     mPurposeMenuMessageWidget->setPosition(KMessageWidget::Header);
1693     mPurposeMenuMessageWidget->setObjectName(QLatin1StringView("mPurposeMenuMessageWidget"));
1694     mReaderBoxVBoxLayout->insertWidget(0, mPurposeMenuMessageWidget);
1695 }
1696 
1697 void ViewerPrivate::slotShowDevelopmentTools()
1698 {
1699     if (!mDeveloperToolDialog) {
1700         mDeveloperToolDialog = new WebEngineViewer::DeveloperToolDialog(nullptr);
1701         mViewer->page()->setDevToolsPage(mDeveloperToolDialog->enginePage());
1702         mViewer->page()->triggerAction(QWebEnginePage::InspectElement);
1703         connect(mDeveloperToolDialog,
1704                 &WebEngineViewer::DeveloperToolDialog::rejected,
1705                 mDeveloperToolDialog,
1706                 &WebEngineViewer::DeveloperToolDialog::deleteLater);
1707     }
1708     if (mDeveloperToolDialog->isHidden()) {
1709         mDeveloperToolDialog->show();
1710     }
1711 
1712     mDeveloperToolDialog->raise();
1713     mDeveloperToolDialog->activateWindow();
1714 }
1715 
1716 void ViewerPrivate::showContextMenu(KMime::Content *content, const QPoint &pos)
1717 {
1718     if (!content) {
1719         return;
1720     }
1721 
1722     if (auto ct = content->contentType(false)) {
1723         if (ct->mimeType() == "text/x-moz-deleted") {
1724             return;
1725         }
1726     }
1727     const bool isAttachment = !content->contentType()->isMultipart() && !content->isTopLevel();
1728     const bool isExtraContent = !mMessage->content(content->index());
1729     const auto hasAttachments = KMime::hasAttachment(mMessage.data());
1730 
1731     QMenu popup;
1732 
1733     if (!content->isTopLevel()) {
1734         popup.addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save &As..."), this, &ViewerPrivate::slotAttachmentSaveAs);
1735 
1736         if (isAttachment) {
1737             popup.addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("to open", "Open"), this, &ViewerPrivate::slotAttachmentOpen);
1738 
1739             if (selectedContents().count() == 1) {
1740                 createOpenWithMenu(&popup, QLatin1StringView(content->contentType()->mimeType()), false);
1741             } else {
1742                 popup.addAction(i18n("Open With..."), this, &ViewerPrivate::slotAttachmentOpenWith);
1743             }
1744             popup.addAction(i18nc("to view something", "View"), this, &ViewerPrivate::slotAttachmentView);
1745         }
1746     }
1747 
1748     if (hasAttachments) {
1749         popup.addAction(i18n("Save All Attachments..."), this, &ViewerPrivate::slotAttachmentSaveAll);
1750     }
1751 
1752     if (!content->isTopLevel()) {
1753         if (isAttachment) {
1754             popup.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"), this, &ViewerPrivate::slotAttachmentCopy);
1755         }
1756 
1757         popup.addSeparator();
1758         auto deleteAction =
1759             popup.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Attachment"), this, &ViewerPrivate::slotAttachmentDelete);
1760         // body parts can only be deleted one at a time, and extra content cannot be delete
1761         deleteAction->setEnabled(selectedContents().size() == 1 && !isExtraContent);
1762 
1763         popup.addSeparator();
1764         popup.addAction(i18n("Properties"), this, &ViewerPrivate::slotAttachmentProperties);
1765     }
1766     popup.exec(mMimePartTree->viewport()->mapToGlobal(pos));
1767 }
1768 
1769 KToggleAction *ViewerPrivate::actionForAttachmentStrategy(const AttachmentStrategy *as)
1770 {
1771     if (!mActionCollection) {
1772         return nullptr;
1773     }
1774     QString actionName;
1775     if (as == AttachmentStrategy::iconic()) {
1776         actionName = QStringLiteral("view_attachments_as_icons");
1777     } else if (as == AttachmentStrategy::smart()) {
1778         actionName = QStringLiteral("view_attachments_smart");
1779     } else if (as == AttachmentStrategy::inlined()) {
1780         actionName = QStringLiteral("view_attachments_inline");
1781     } else if (as == AttachmentStrategy::hidden()) {
1782         actionName = QStringLiteral("view_attachments_hide");
1783     } else if (as == AttachmentStrategy::headerOnly()) {
1784         actionName = QStringLiteral("view_attachments_headeronly");
1785     } else {
1786         qCWarning(MESSAGEVIEWER_LOG) << "actionForAttachmentStrategy invalid attachment type";
1787     }
1788 
1789     if (actionName.isEmpty()) {
1790         return nullptr;
1791     } else {
1792         return static_cast<KToggleAction *>(mActionCollection->action(actionName));
1793     }
1794 }
1795 
1796 void ViewerPrivate::readGlobalOverrideCodec()
1797 {
1798     // if the global character encoding wasn't changed then there's nothing to do
1799     if (MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding() == mOldGlobalOverrideEncoding) {
1800         return;
1801     }
1802 
1803     setOverrideEncoding(MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding());
1804     mOldGlobalOverrideEncoding = MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding();
1805 }
1806 
1807 QByteArray ViewerPrivate::overrideCodecName() const
1808 {
1809     if (!mOverrideEncoding.isEmpty() && mOverrideEncoding != QLatin1StringView("Auto")) { // Auto
1810         QStringDecoder codec(mOverrideEncoding.toUtf8().constData());
1811         if (!codec.isValid()) {
1812             return "UTF-8";
1813         }
1814         return mOverrideEncoding.toUtf8();
1815     }
1816     return {};
1817 }
1818 
1819 static QColor nextColor(const QColor &c)
1820 {
1821     int h;
1822     int s;
1823     int v;
1824     c.getHsv(&h, &s, &v);
1825     return QColor::fromHsv((h + 50) % 360, qMax(s, 64), v);
1826 }
1827 
1828 QString ViewerPrivate::renderAttachments(KMime::Content *node, const QColor &bgColor) const
1829 {
1830     if (!node) {
1831         return {};
1832     }
1833 
1834     QString html;
1835     KMime::Content *child = MessageCore::NodeHelper::firstChild(node);
1836 
1837     if (child) {
1838         const QString subHtml = renderAttachments(child, nextColor(bgColor));
1839         if (!subHtml.isEmpty()) {
1840             QString margin;
1841             if (node != mMessage.data() || headerStylePlugin()->hasMargin()) {
1842                 margin = QStringLiteral("padding:2px; margin:2px; ");
1843             }
1844             const QString align = headerStylePlugin()->alignment();
1845             const QByteArray mediaTypeLower = node->contentType()->mediaType().toLower();
1846             const bool result = (mediaTypeLower == "message" || mediaTypeLower == "multipart" || node == mMessage.data());
1847             if (result) {
1848                 html += QStringLiteral(
1849                             "<div style=\"background:%1; %2"
1850                             "vertical-align:middle; float:%3;\">")
1851                             .arg(bgColor.name())
1852                             .arg(margin, align);
1853             }
1854             html += subHtml;
1855             if (result) {
1856                 html += QLatin1StringView("</div>");
1857             }
1858         }
1859     } else {
1860         Util::AttachmentDisplayInfo info = Util::attachmentDisplayInfo(node);
1861         if (info.displayInHeader) {
1862             html += QLatin1StringView("<div style=\"float:left;\">");
1863             html += QStringLiteral(
1864                         "<span style=\"white-space:nowrap; border-width: 0px; border-left-width: 5px; border-color: %1; 2px; border-left-style: solid;\">")
1865                         .arg(bgColor.name());
1866             mNodeHelper->writeNodeToTempFile(node);
1867             const QString href = mNodeHelper->asHREF(node, QStringLiteral("header"));
1868             html += QLatin1StringView("<a href=\"") + href + QLatin1StringView("\">");
1869             const QString imageMaxSize = QStringLiteral("width=\"16\" height=\"16\"");
1870 #if 0
1871             if (!info.icon.isEmpty()) {
1872                 QImage tmpImg(info.icon);
1873                 if (tmpImg.width() > 48 || tmpImg.height() > 48) {
1874                     imageMaxSize = QStringLiteral("width=\"48\" height=\"48\"");
1875                 }
1876             }
1877 #endif
1878             html += QStringLiteral("<img %1 style=\"vertical-align:middle;\" src=\"").arg(imageMaxSize) + info.icon + QLatin1StringView("\"/>&nbsp;");
1879             const int elidedTextSize = headerStylePlugin()->elidedTextSize();
1880             if (elidedTextSize == -1) {
1881                 html += info.label;
1882             } else {
1883                 QFont bodyFont = cssHelper()->bodyFont(mHtmlHeadSettings.fixedFont);
1884                 QFontMetrics fm(bodyFont);
1885                 html += fm.elidedText(info.label, Qt::ElideRight, elidedTextSize);
1886             }
1887             html += QLatin1StringView("</a></span></div> ");
1888         }
1889     }
1890 
1891     for (KMime::Content *extraNode : mNodeHelper->extraContents(node)) {
1892         html += renderAttachments(extraNode, bgColor);
1893     }
1894 
1895     KMime::Content *next = MessageCore::NodeHelper::nextSibling(node);
1896     if (next) {
1897         html += renderAttachments(next, nextColor(bgColor));
1898     }
1899 
1900     return html;
1901 }
1902 
1903 KMime::Content *ViewerPrivate::findContentByType(KMime::Content *content, const QByteArray &type)
1904 {
1905     const auto list = content->contents();
1906     for (KMime::Content *c : list) {
1907         if (c->contentType()->mimeType() == type) {
1908             return c;
1909         }
1910     }
1911     return nullptr;
1912 }
1913 
1914 //-----------------------------------------------------------------------------
1915 void ViewerPrivate::update(MimeTreeParser::UpdateMode updateMode)
1916 {
1917     // Avoid flicker, somewhat of a cludge
1918     if (updateMode == MimeTreeParser::Force) {
1919         // stop the timer to avoid calling updateReaderWin twice
1920         mUpdateReaderWinTimer.stop();
1921         saveRelativePosition();
1922         updateReaderWin();
1923     } else if (mUpdateReaderWinTimer.isActive()) {
1924         mUpdateReaderWinTimer.setInterval(150ms);
1925     } else {
1926         mUpdateReaderWinTimer.start(0);
1927     }
1928 }
1929 
1930 void ViewerPrivate::slotOpenUrl()
1931 {
1932     slotUrlOpen();
1933 }
1934 
1935 void ViewerPrivate::slotUrlOpen(const QUrl &url)
1936 {
1937     if (!url.isEmpty()) {
1938         mClickedUrl = url;
1939     }
1940     const OpenWithUrlInfo openWithInfo = OpenUrlWithManager::self()->openWith(url);
1941     if (openWithInfo.isValid()) {
1942         auto job = new OpenUrlWithJob(this);
1943         job->setInfo(openWithInfo);
1944         job->setUrl(mClickedUrl);
1945         job->start();
1946         return;
1947     }
1948     // First, let's see if the URL handler manager can handle the URL. If not, try KRun for some
1949     // known URLs, otherwise fallback to emitting a signal.
1950     // That signal is caught by KMail, and in case of mailto URLs, a composer is shown.
1951 
1952     if (URLHandlerManager::instance()->handleClick(mClickedUrl, this)) {
1953         return;
1954     }
1955     Q_EMIT urlClicked(mMessageItem, mClickedUrl);
1956 }
1957 
1958 void ViewerPrivate::checkPhishingUrl()
1959 {
1960     if (MessageViewer::MessageViewerSettings::self()->checkPhishingUrl() && (mClickedUrl.scheme() != QLatin1StringView("mailto"))) {
1961         mPhishingDatabase->checkUrl(mClickedUrl);
1962     } else {
1963         executeRunner(mClickedUrl);
1964     }
1965 }
1966 
1967 void ViewerPrivate::executeRunner(const QUrl &url)
1968 {
1969     if (!MessageViewer::Util::handleUrlWithQDesktopServices(url)) {
1970         auto job = new KIO::OpenUrlJob(url);
1971         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, viewer()));
1972         job->setRunExecutables(false);
1973         job->start();
1974     }
1975 }
1976 
1977 void ViewerPrivate::slotCheckedUrlFinished(const QUrl &url, WebEngineViewer::CheckPhishingUrlUtil::UrlStatus status)
1978 {
1979     switch (status) {
1980     case WebEngineViewer::CheckPhishingUrlUtil::BrokenNetwork:
1981         KMessageBox::error(mMainWindow, i18n("The network is broken."), i18n("Check Phishing URL"));
1982         break;
1983     case WebEngineViewer::CheckPhishingUrlUtil::InvalidUrl:
1984         KMessageBox::error(mMainWindow, i18n("The URL %1 is not valid.", url.toString()), i18nc("@title:window", "Check Phishing URL"));
1985         break;
1986     case WebEngineViewer::CheckPhishingUrlUtil::Ok:
1987         break;
1988     case WebEngineViewer::CheckPhishingUrlUtil::MalWare:
1989         if (!urlIsAMalwareButContinue()) {
1990             return;
1991         }
1992         break;
1993     case WebEngineViewer::CheckPhishingUrlUtil::Unknown:
1994         qCWarning(MESSAGEVIEWER_LOG) << "WebEngineViewer::slotCheckedUrlFinished unknown error ";
1995         break;
1996     }
1997     executeRunner(url);
1998 }
1999 
2000 bool ViewerPrivate::urlIsAMalwareButContinue()
2001 {
2002     if (KMessageBox::ButtonCode::SecondaryAction
2003         == KMessageBox::warningTwoActions(mMainWindow,
2004                                           i18n("This web site is a malware, do you want to continue to show it?"),
2005                                           i18nc("@title:window", "Malware"),
2006                                           KStandardGuiItem::cont(),
2007                                           KStandardGuiItem::cancel())) {
2008         return false;
2009     }
2010     return true;
2011 }
2012 
2013 void ViewerPrivate::slotUrlOn(const QString &link)
2014 {
2015     // The "link" we get here is not URL-encoded, and therefore there is no way QUrl could
2016     // parse it correctly. To workaround that, we use QWebFrame::hitTestContent() on the mouse position
2017     // to get the URL before WebKit managed to mangle it.
2018     QUrl url(link);
2019     const QString protocol = url.scheme();
2020     if (protocol == QLatin1StringView("kmail") || protocol == QLatin1StringView("x-kmail") || protocol == QLatin1StringView("attachment")
2021         || (protocol.isEmpty() && url.path().isEmpty())) {
2022         mViewer->setAcceptDrops(false);
2023     } else {
2024         mViewer->setAcceptDrops(true);
2025     }
2026 
2027     mViewer->setLinkHovered(url);
2028     if (link.trimmed().isEmpty()) {
2029         PimCommon::BroadcastStatus::instance()->reset();
2030         Q_EMIT showStatusBarMessage(QString());
2031         return;
2032     }
2033 
2034     QString msg = URLHandlerManager::instance()->statusBarMessage(url, this);
2035     if (msg.isEmpty()) {
2036         msg = link;
2037     }
2038 
2039     Q_EMIT showStatusBarMessage(msg);
2040 }
2041 
2042 void ViewerPrivate::slotUrlPopup(const WebEngineViewer::WebHitTestResult &result)
2043 {
2044     if (!mMsgDisplay) {
2045         return;
2046     }
2047     mClickedUrl = result.linkUrl();
2048     mImageUrl = result.imageUrl();
2049     const QPoint aPos = mViewer->mapToGlobal(result.pos());
2050     if (URLHandlerManager::instance()->handleContextMenuRequest(mClickedUrl, aPos, this)) {
2051         return;
2052     }
2053 
2054     if (!mActionCollection) {
2055         return;
2056     }
2057 
2058     if (mClickedUrl.scheme() == QLatin1StringView("mailto")) {
2059         mCopyURLAction->setText(i18n("Copy Email Address"));
2060     } else {
2061         mCopyURLAction->setText(i18n("Copy Link Address"));
2062     }
2063     Q_EMIT displayPopupMenu(mMessageItem, result, aPos);
2064     Q_EMIT popupMenu(mMessageItem, mClickedUrl, mImageUrl, aPos);
2065 }
2066 
2067 void ViewerPrivate::slotLoadExternalReference()
2068 {
2069     if (mColorBar->isNormal() || htmlLoadExtOverride()) {
2070         return;
2071     }
2072     setHtmlLoadExtOverride(true);
2073     update(MimeTreeParser::Force);
2074 }
2075 
2076 Viewer::DisplayFormatMessage translateToDisplayFormat(MimeTreeParser::Util::HtmlMode mode)
2077 {
2078     switch (mode) {
2079     case MimeTreeParser::Util::Normal:
2080         return Viewer::Unknown;
2081     case MimeTreeParser::Util::Html:
2082         return Viewer::Html;
2083     case MimeTreeParser::Util::MultipartPlain:
2084         return Viewer::Text;
2085     case MimeTreeParser::Util::MultipartHtml:
2086         return Viewer::Html;
2087     case MimeTreeParser::Util::MultipartIcal:
2088         return Viewer::ICal;
2089     }
2090     return Viewer::Unknown;
2091 }
2092 
2093 void ViewerPrivate::slotToggleHtmlMode()
2094 {
2095     const auto availableModes = mColorBar->availableModes();
2096     const int availableModeSize(availableModes.size());
2097     if (mColorBar->isNormal() || availableModeSize < 2) {
2098         return;
2099     }
2100     if (mScamDetectionWarning) {
2101         mScamDetectionWarning->setVisible(false);
2102     }
2103     const MimeTreeParser::Util::HtmlMode mode = mColorBar->mode();
2104     const int pos = (availableModes.indexOf(mode) + 1) % availableModeSize;
2105     setDisplayFormatMessageOverwrite(translateToDisplayFormat(availableModes[pos]));
2106     update(MimeTreeParser::Force);
2107     mColorBar->setAvailableModes(availableModes);
2108 }
2109 
2110 void ViewerPrivate::slotFind()
2111 {
2112     if (mViewer->hasSelection()) {
2113         mFindBar->setText(mViewer->selectedText());
2114     }
2115     mSliderContainer->slideIn();
2116     mFindBar->focusAndSetCursor();
2117 }
2118 
2119 void ViewerPrivate::slotToggleFixedFont()
2120 {
2121     mHtmlHeadSettings.fixedFont = !mHtmlHeadSettings.fixedFont;
2122     update(MimeTreeParser::Force);
2123 }
2124 
2125 void ViewerPrivate::slotToggleMimePartTree()
2126 {
2127     if (mToggleMimePartTreeAction->isChecked()) {
2128         MessageViewer::MessageViewerSettings::self()->setMimeTreeMode2(MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Always);
2129     } else {
2130         MessageViewer::MessageViewerSettings::self()->setMimeTreeMode2(MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Never);
2131     }
2132     showHideMimeTree();
2133 }
2134 
2135 void ViewerPrivate::slotShowMessageSource()
2136 {
2137     if (!mMessage) {
2138         return;
2139     }
2140     QPointer<MailSourceWebEngineViewer> viewer = new MailSourceWebEngineViewer; // deletes itself upon close
2141     mListMailSourceViewer.append(viewer);
2142     viewer->setWindowTitle(i18nc("@title:window", "Message as Plain Text"));
2143     const QString rawMessage = QString::fromLatin1(mMessage->encodedContent());
2144     viewer->setRawSource(rawMessage);
2145     viewer->setDisplayedSource(mViewer->page());
2146     if (mHtmlHeadSettings.fixedFont) {
2147         viewer->setFixedFont();
2148     }
2149     viewer->show();
2150 }
2151 
2152 void ViewerPrivate::updateReaderWin()
2153 {
2154     if (!mMsgDisplay) {
2155         return;
2156     }
2157 
2158     if (mRecursionCountForDisplayMessage + 1 > 1) {
2159         // This recursion here can happen because the ObjectTreeParser in parseMsg() can exec() an
2160         // eventloop.
2161         // This happens in two cases:
2162         //   1) The ContactSearchJob started by FancyHeaderStyle::format
2163         //   2) Various modal passphrase dialogs for decryption of a message (bug 96498)
2164         //
2165         // While the exec() eventloop is running, it is possible that a timer calls updateReaderWin(),
2166         // and not aborting here would confuse the state terribly.
2167         qCWarning(MESSAGEVIEWER_LOG) << "Danger, recursion while displaying a message!";
2168         return;
2169     }
2170     mRecursionCountForDisplayMessage++;
2171 
2172     if (mViewer) {
2173         mViewer->setAllowExternalContent(htmlLoadExternal());
2174         htmlWriter()->reset();
2175         // TODO: if the item doesn't have the payload fetched, try to fetch it? Maybe not here, but in setMessageItem.
2176         if (mMessage) {
2177             mColorBar->show();
2178             displayMessage();
2179         } else if (mMessagePartNode) {
2180             setMessagePart(mMessagePartNode);
2181         } else {
2182             mColorBar->hide();
2183             mMimePartTree->hide();
2184             htmlWriter()->begin();
2185             htmlWriter()->write(cssHelper()->htmlHead(mHtmlHeadSettings) + cssHelper()->endBodyHtml());
2186             htmlWriter()->end();
2187         }
2188     }
2189     mRecursionCountForDisplayMessage--;
2190 }
2191 
2192 void ViewerPrivate::slotMimePartSelected(const QModelIndex &index)
2193 {
2194     auto content = static_cast<KMime::Content *>(index.internalPointer());
2195     if (!mMimePartTree->mimePartModel()->parent(index).isValid() && index.row() == 0) {
2196         update(MimeTreeParser::Force);
2197     } else {
2198         setMessagePart(content);
2199     }
2200 }
2201 
2202 void ViewerPrivate::slotIconicAttachments()
2203 {
2204     setAttachmentStrategy(AttachmentStrategy::iconic());
2205 }
2206 
2207 void ViewerPrivate::slotSmartAttachments()
2208 {
2209     setAttachmentStrategy(AttachmentStrategy::smart());
2210 }
2211 
2212 void ViewerPrivate::slotInlineAttachments()
2213 {
2214     setAttachmentStrategy(AttachmentStrategy::inlined());
2215 }
2216 
2217 void ViewerPrivate::slotHideAttachments()
2218 {
2219     setAttachmentStrategy(AttachmentStrategy::hidden());
2220 }
2221 
2222 void ViewerPrivate::slotHeaderOnlyAttachments()
2223 {
2224     setAttachmentStrategy(AttachmentStrategy::headerOnly());
2225 }
2226 
2227 void ViewerPrivate::attachmentView(KMime::Content *atmNode)
2228 {
2229     if (atmNode) {
2230         const bool isEncapsulatedMessage = atmNode->parent() && atmNode->parent()->bodyIsMessage();
2231         if (isEncapsulatedMessage) {
2232             attachmentViewMessage(atmNode->parent()->bodyAsMessage());
2233         } else if ((qstricmp(atmNode->contentType()->mediaType().constData(), "text") == 0)
2234                    && ((qstricmp(atmNode->contentType()->subType().constData(), "x-vcard") == 0)
2235                        || (qstricmp(atmNode->contentType()->subType().constData(), "directory") == 0))) {
2236             setMessagePart(atmNode);
2237         } else {
2238             Q_EMIT showReader(atmNode, htmlMail(), overrideEncoding());
2239         }
2240     }
2241 }
2242 
2243 void ViewerPrivate::slotDelayedResize()
2244 {
2245     mSplitter->setGeometry(0, 0, q->width(), q->height());
2246 }
2247 
2248 void ViewerPrivate::slotPrintPreview()
2249 {
2250     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintPreview);
2251     if (!mMessage) {
2252         return;
2253     }
2254     // Need to delay
2255     QTimer::singleShot(1s, this, &ViewerPrivate::slotDelayPrintPreview); // 1 second
2256 }
2257 
2258 void ViewerPrivate::slotDelayPrintPreview()
2259 {
2260     auto printMessage = new PrintMessage(this);
2261     printMessage->setParentWidget(q);
2262     printMessage->setView(mViewer);
2263     printMessage->printPreview();
2264     connect(printMessage, &PrintMessage::printingFinished, this, &ViewerPrivate::printingFinished);
2265 }
2266 
2267 void ViewerPrivate::exportToPdf(const QString &fileName)
2268 {
2269     auto job = new WebEngineViewer::WebEngineExportPdfPageJob(this);
2270     connect(job, &WebEngineViewer::WebEngineExportPdfPageJob::exportToPdfSuccess, this, [this, fileName]() {
2271         showSavedFileFolderWidget({QUrl::fromLocalFile(fileName)}, MessageViewer::OpenSavedFileFolderWidget::FileType::Pdf);
2272     });
2273     job->setEngineView(mViewer);
2274     job->setPdfPath(fileName);
2275     job->start();
2276 }
2277 
2278 void ViewerPrivate::slotOpenInBrowser()
2279 {
2280     auto job = new WebEngineViewer::WebEngineExportHtmlPageJob(this);
2281     job->setEngineView(mViewer);
2282     connect(job, &WebEngineViewer::WebEngineExportHtmlPageJob::failed, this, &ViewerPrivate::slotExportHtmlPageFailed);
2283     connect(job, &WebEngineViewer::WebEngineExportHtmlPageJob::success, this, &ViewerPrivate::slotExportHtmlPageSuccess);
2284     job->start();
2285 }
2286 
2287 void ViewerPrivate::slotExportHtmlPageSuccess(const QString &filename)
2288 {
2289     const QUrl url(QUrl::fromLocalFile(filename));
2290     auto job = new KIO::OpenUrlJob(url, QStringLiteral("text/html"), q);
2291     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, q));
2292     job->setDeleteTemporaryFile(true);
2293     job->start();
2294 
2295     Q_EMIT printingFinished();
2296 }
2297 
2298 void ViewerPrivate::slotExportHtmlPageFailed()
2299 {
2300     qCDebug(MESSAGEVIEWER_LOG) << " Export HTML failed";
2301     Q_EMIT printingFinished();
2302 }
2303 
2304 static QString filterCharsFromFilename(const QString &name)
2305 {
2306     QString value = name;
2307 
2308     value.replace(QLatin1Char('/'), QLatin1Char('-'));
2309     value.remove(QLatin1Char('\\'));
2310     value.remove(QLatin1Char(':'));
2311     value.remove(QLatin1Char('*'));
2312     value.remove(QLatin1Char('?'));
2313     value.remove(QLatin1Char('"'));
2314     value.remove(QLatin1Char('<'));
2315     value.remove(QLatin1Char('>'));
2316     value.remove(QLatin1Char('|'));
2317 
2318     return value;
2319 }
2320 
2321 void ViewerPrivate::slotPrintMessage()
2322 {
2323     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintMessage);
2324 
2325     if (!mMessage) {
2326         return;
2327     }
2328     auto printMessage = new PrintMessage(this);
2329     connect(printMessage, &PrintMessage::printingFinished, this, &ViewerPrivate::printingFinished);
2330     printMessage->setParentWidget(q);
2331     printMessage->setView(mViewer);
2332     printMessage->setDocumentName(filterCharsFromFilename(mMessage->subject()->asUnicodeString()));
2333     printMessage->print();
2334 }
2335 
2336 void ViewerPrivate::slotSetEncoding()
2337 {
2338     if (mSelectEncodingAction) {
2339         if (mSelectEncodingAction->currentItem() == 0) { // Auto
2340             mOverrideEncoding.clear();
2341         } else {
2342             mOverrideEncoding = MimeTreeParser::NodeHelper::encodingForName(mSelectEncodingAction->currentText());
2343         }
2344         update(MimeTreeParser::Force);
2345     }
2346 }
2347 
2348 HeaderStylePlugin *ViewerPrivate::headerStylePlugin() const
2349 {
2350     return mHeaderStylePlugin;
2351 }
2352 
2353 void ViewerPrivate::updatePalette()
2354 {
2355     updateColorFromScheme();
2356     cssHelper()->updateColor();
2357     recreateCssHelper();
2358     update(MimeTreeParser::Force);
2359 }
2360 
2361 void ViewerPrivate::updateColorFromScheme()
2362 {
2363     const KColorScheme scheme = KColorScheme(QPalette::Active, KColorScheme::View);
2364     mForegroundError = scheme.foreground(KColorScheme::NegativeText).color();
2365     mBackgroundError = scheme.background(KColorScheme::NegativeBackground).color();
2366     mBackgroundAttachment = scheme.background().color();
2367 }
2368 
2369 void ViewerPrivate::createMdnWarningWidget()
2370 {
2371     mMdnWarning = new MDNWarningWidget(mReaderBox);
2372     mMdnWarning->setObjectName(QLatin1StringView("mMdnWarning"));
2373     mReaderBoxVBoxLayout->insertWidget(0, mMdnWarning);
2374 }
2375 
2376 void ViewerPrivate::mdnWarningAnimatedHide()
2377 {
2378     if (!mMdnWarning) {
2379         createMdnWarningWidget();
2380     }
2381     mMdnWarning->animatedHide();
2382 }
2383 
2384 void ViewerPrivate::showMdnInformations(const QPair<QString, bool> &mdnInfo)
2385 {
2386     if (!mMdnWarning) {
2387         createMdnWarningWidget();
2388     }
2389     if (!mdnInfo.first.isEmpty()) {
2390         mMdnWarning->setCanDeny(mdnInfo.second);
2391         mMdnWarning->setInformation(mdnInfo.first);
2392     } else {
2393         mMdnWarning->animatedHide();
2394     }
2395 }
2396 
2397 void ViewerPrivate::initializeColorFromScheme()
2398 {
2399     if (!mForegroundError.isValid()) {
2400         updateColorFromScheme();
2401     }
2402 }
2403 
2404 QString ViewerPrivate::attachmentHtml()
2405 {
2406     initializeColorFromScheme();
2407     QString html = renderAttachments(mMessage.data(), mBackgroundAttachment);
2408     if (!html.isEmpty()) {
2409         html.prepend(headerStylePlugin()->attachmentHtml());
2410     }
2411     return html;
2412 }
2413 
2414 void ViewerPrivate::executeCustomScriptsAfterLoading()
2415 {
2416     disconnect(mViewer, &MailWebEngineView::loadFinished, this, &ViewerPrivate::executeCustomScriptsAfterLoading);
2417     // inject attachments in header view
2418     // we have to do that after the otp has run so we also see encrypted parts
2419 
2420     mViewer->scrollToRelativePosition(mViewer->relativePosition());
2421     mViewer->clearRelativePosition();
2422 }
2423 
2424 void ViewerPrivate::slotSettingsChanged()
2425 {
2426     update(MimeTreeParser::Force);
2427 }
2428 
2429 void ViewerPrivate::slotMimeTreeContextMenuRequested(const QPoint &pos)
2430 {
2431     const QModelIndex index = mMimePartTree->indexAt(pos);
2432     if (index.isValid()) {
2433         auto content = static_cast<KMime::Content *>(index.internalPointer());
2434         showContextMenu(content, pos);
2435     }
2436 }
2437 
2438 void ViewerPrivate::slotAttachmentOpenWith()
2439 {
2440     QItemSelectionModel *selectionModel = mMimePartTree->selectionModel();
2441     const QModelIndexList selectedRows = selectionModel->selectedRows();
2442 
2443     for (const QModelIndex &index : selectedRows) {
2444         auto content = static_cast<KMime::Content *>(index.internalPointer());
2445         attachmentOpenWith(content);
2446     }
2447 }
2448 
2449 void ViewerPrivate::slotAttachmentOpen()
2450 {
2451     QItemSelectionModel *selectionModel = mMimePartTree->selectionModel();
2452     const QModelIndexList selectedRows = selectionModel->selectedRows();
2453 
2454     for (const QModelIndex &index : selectedRows) {
2455         auto content = static_cast<KMime::Content *>(index.internalPointer());
2456         attachmentOpen(content);
2457     }
2458 }
2459 
2460 void ViewerPrivate::showSavedFileFolderWidget(const QList<QUrl> &urls, OpenSavedFileFolderWidget::FileType fileType)
2461 {
2462     if (!mOpenSavedFileFolderWidget) {
2463         createOpenSavedFileFolderWidget();
2464     }
2465     mOpenSavedFileFolderWidget->setUrls(urls, fileType);
2466     mOpenSavedFileFolderWidget->slotShowWarning();
2467 }
2468 
2469 bool ViewerPrivate::mimePartTreeIsEmpty() const
2470 {
2471     return mMimePartTree->model()->rowCount() == 0;
2472 }
2473 
2474 void ViewerPrivate::setPluginName(const QString &pluginName)
2475 {
2476     mHeaderStyleMenuManager->setPluginName(pluginName);
2477 }
2478 
2479 QList<QAction *> ViewerPrivate::viewerPluginActionList(ViewerPluginInterface::SpecificFeatureTypes features)
2480 {
2481     if (mViewerPluginToolManager) {
2482         return mViewerPluginToolManager->viewerPluginActionList(features);
2483     }
2484     return {};
2485 }
2486 
2487 void ViewerPrivate::slotActivatePlugin(ViewerPluginInterface *interface)
2488 {
2489     interface->setMessage(mMessage);
2490     interface->setMessageItem(mMessageItem);
2491     interface->setUrl(mClickedUrl);
2492     interface->setCurrentCollection(mMessageItem.parentCollection());
2493     const QString text = mViewer->selectedText();
2494     if (!text.isEmpty()) {
2495         interface->setText(text);
2496     }
2497     interface->execute();
2498 }
2499 
2500 void ViewerPrivate::slotAttachmentSaveAs()
2501 {
2502     const auto contents = selectedContents();
2503     QList<QUrl> urlList;
2504     if (Util::saveAttachments(contents, mMainWindow, urlList)) {
2505         showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2506     }
2507 }
2508 
2509 void ViewerPrivate::slotAttachmentSaveAll()
2510 {
2511     const auto contents = mMessage->attachments();
2512     QList<QUrl> urlList;
2513     if (Util::saveAttachments(contents, mMainWindow, urlList)) {
2514         showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2515     }
2516 }
2517 
2518 void ViewerPrivate::slotAttachmentView()
2519 {
2520     const auto contents = selectedContents();
2521 
2522     for (KMime::Content *content : contents) {
2523         attachmentView(content);
2524     }
2525 }
2526 
2527 void ViewerPrivate::slotAttachmentProperties()
2528 {
2529     const auto contents = selectedContents();
2530 
2531     for (KMime::Content *content : contents) {
2532         attachmentProperties(content);
2533     }
2534 }
2535 
2536 void ViewerPrivate::attachmentProperties(KMime::Content *content)
2537 {
2538     auto dialog = new MessageCore::AttachmentPropertiesDialog(content, mMainWindow);
2539     dialog->setAttribute(Qt::WA_DeleteOnClose);
2540     dialog->show();
2541 }
2542 
2543 void ViewerPrivate::slotAttachmentCopy()
2544 {
2545 #ifndef QT_NO_CLIPBOARD
2546     attachmentCopy(selectedContents());
2547 #endif
2548 }
2549 
2550 void ViewerPrivate::attachmentCopy(const KMime::Content::List &contents)
2551 {
2552 #ifndef QT_NO_CLIPBOARD
2553     if (contents.isEmpty()) {
2554         return;
2555     }
2556 
2557     QList<QUrl> urls;
2558     for (KMime::Content *content : contents) {
2559         auto url = QUrl::fromLocalFile(mNodeHelper->writeNodeToTempFile(content));
2560         if (!url.isValid()) {
2561             continue;
2562         }
2563         urls.append(url);
2564     }
2565 
2566     if (urls.isEmpty()) {
2567         return;
2568     }
2569 
2570     auto mimeData = new QMimeData;
2571     mimeData->setUrls(urls);
2572     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
2573 #endif
2574 }
2575 
2576 void ViewerPrivate::slotAttachmentDelete()
2577 {
2578     const auto contents = selectedContents();
2579     if (contents.size() != 1) {
2580         return;
2581     }
2582     // look up the selected content node of the mime part tree in the node tree of the original message;
2583     // since deleting extra content (e.g. attachments inside encrypted message parts) is not supported,
2584     // we do not need to consider the extra content in the lookup
2585     const auto contentIndex = contents[0]->index();
2586     const auto contentInOriginalMessage = mMessage->content(contentIndex);
2587     if (contentInOriginalMessage) {
2588         (void)deleteAttachment(contentInOriginalMessage);
2589     }
2590 }
2591 
2592 void ViewerPrivate::slotLevelQuote(int l)
2593 {
2594     if (mLevelQuote != l) {
2595         mLevelQuote = l;
2596         update(MimeTreeParser::Force);
2597     }
2598 }
2599 
2600 void ViewerPrivate::slotHandleAttachment(int choice)
2601 {
2602     if (!mCurrentContent) {
2603         return;
2604     }
2605     switch (choice) {
2606     case Viewer::Delete:
2607         if (!deleteAttachment(mCurrentContent)) {
2608             qCWarning(MESSAGEVIEWER_LOG) << "Impossible to delete attachment";
2609         }
2610         break;
2611     case Viewer::Properties:
2612         attachmentProperties(mCurrentContent);
2613         break;
2614     case Viewer::Save: {
2615         const bool isEncapsulatedMessage = mCurrentContent->parent() && mCurrentContent->parent()->bodyIsMessage();
2616         if (isEncapsulatedMessage) {
2617             KMime::Message::Ptr message(new KMime::Message);
2618             message->setContent(mCurrentContent->parent()->bodyAsMessage()->encodedContent());
2619             message->parse();
2620             Akonadi::Item item;
2621             item.setPayload<KMime::Message::Ptr>(message);
2622             Akonadi::MessageFlags::copyMessageFlags(*message, item);
2623             item.setMimeType(KMime::Message::mimeType());
2624             QUrl url;
2625             if (MessageViewer::Util::saveMessageInMboxAndGetUrl(url, Akonadi::Item::List() << item, mMainWindow)) {
2626                 showSavedFileFolderWidget({url}, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2627             }
2628         } else {
2629             QList<QUrl> urlList;
2630             if (Util::saveContents(mMainWindow, KMime::Content::List() << mCurrentContent, urlList)) {
2631                 showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2632             }
2633         }
2634         break;
2635     }
2636     case Viewer::OpenWith:
2637         attachmentOpenWith(mCurrentContent);
2638         break;
2639     case Viewer::Open:
2640         attachmentOpen(mCurrentContent);
2641         break;
2642     case Viewer::View:
2643         attachmentView(mCurrentContent);
2644         break;
2645     case Viewer::Copy:
2646         attachmentCopy(KMime::Content::List() << mCurrentContent);
2647         break;
2648     case Viewer::ScrollTo:
2649         scrollToAttachment(mCurrentContent);
2650         break;
2651     case Viewer::ReplyMessageToAuthor:
2652         replyMessageToAuthor(mCurrentContent);
2653         break;
2654     case Viewer::ReplyMessageToAll:
2655         replyMessageToAll(mCurrentContent);
2656         break;
2657     }
2658 }
2659 
2660 void ViewerPrivate::replyMessageToAuthor(KMime::Content *atmNode)
2661 {
2662     replyMessage(atmNode, false);
2663 }
2664 
2665 void ViewerPrivate::replyMessageToAll(KMime::Content *atmNode)
2666 {
2667     replyMessage(atmNode, true);
2668 }
2669 
2670 void ViewerPrivate::replyMessage(KMime::Content *atmNode, bool replyToAll)
2671 {
2672     if (atmNode) {
2673         const bool isEncapsulatedMessage = atmNode->parent() && atmNode->parent()->bodyIsMessage();
2674         if (isEncapsulatedMessage) {
2675             Q_EMIT replyMessageTo(atmNode->parent()->bodyAsMessage(), replyToAll);
2676         }
2677     }
2678 }
2679 
2680 void ViewerPrivate::slotSpeakText()
2681 {
2682     const QString text = mViewer->selectedText();
2683     if (!text.isEmpty()) {
2684 #ifdef HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
2685         mTextToSpeechContainerWidget->say(text);
2686 #endif
2687     }
2688 }
2689 
2690 QUrl ViewerPrivate::imageUrl() const
2691 {
2692     QUrl url;
2693     if (mImageUrl.scheme() == QLatin1StringView("cid")) {
2694         url = QUrl(MessageViewer::WebEngineEmbedPart::self()->contentUrl(mImageUrl.path()));
2695     } else {
2696         url = mImageUrl;
2697     }
2698     return url;
2699 }
2700 
2701 void ViewerPrivate::slotCopyImageLocation()
2702 {
2703 #ifndef QT_NO_CLIPBOARD
2704     QApplication::clipboard()->setText(imageUrl().url());
2705 #endif
2706 }
2707 
2708 void ViewerPrivate::slotCopySelectedText()
2709 {
2710     mViewer->triggerPageAction(QWebEnginePage::Copy);
2711 }
2712 
2713 void ViewerPrivate::viewerSelectionChanged()
2714 {
2715     mActionCollection->action(QStringLiteral("kmail_copy"))->setEnabled(!mViewer->selectedText().isEmpty());
2716 }
2717 
2718 void ViewerPrivate::selectAll()
2719 {
2720     mViewer->selectAll();
2721 }
2722 
2723 void ViewerPrivate::slotUrlCopy()
2724 {
2725 #ifndef QT_NO_CLIPBOARD
2726     QClipboard *clip = QApplication::clipboard();
2727     if (mClickedUrl.scheme() == QLatin1StringView("mailto")) {
2728         // put the url into the mouse selection and the clipboard
2729         const QString address = KEmailAddress::decodeMailtoUrl(mClickedUrl);
2730         clip->setText(address, QClipboard::Clipboard);
2731         clip->setText(address, QClipboard::Selection);
2732         PimCommon::BroadcastStatus::instance()->setStatusMsg(i18n("Address copied to clipboard."));
2733     } else {
2734         // put the url into the mouse selection and the clipboard
2735         const QString clickedUrl = mClickedUrl.url();
2736         clip->setText(clickedUrl, QClipboard::Clipboard);
2737         clip->setText(clickedUrl, QClipboard::Selection);
2738         PimCommon::BroadcastStatus::instance()->setStatusMsg(i18n("URL copied to clipboard."));
2739     }
2740 #endif
2741 }
2742 
2743 void ViewerPrivate::slotSaveMessage()
2744 {
2745     if (!mMessageItem.hasPayload<KMime::Message::Ptr>()) {
2746         if (mMessageItem.isValid()) {
2747             qCWarning(MESSAGEVIEWER_LOG) << "Payload is not a MessagePtr!";
2748         }
2749         return;
2750     }
2751 
2752     if (!Util::saveMessageInMbox(Akonadi::Item::List() << mMessageItem, mMainWindow)) {
2753         qCWarning(MESSAGEVIEWER_LOG) << "Impossible to save as mbox";
2754     }
2755 }
2756 
2757 void ViewerPrivate::saveRelativePosition()
2758 {
2759     if (mViewer) {
2760         mViewer->saveRelativePosition();
2761     }
2762 }
2763 
2764 bool ViewerPrivate::htmlMail() const
2765 {
2766     if (mDisplayFormatMessageOverwrite == Viewer::UseGlobalSetting) {
2767         return mHtmlMailGlobalSetting;
2768     } else {
2769         return mDisplayFormatMessageOverwrite == Viewer::Html;
2770     }
2771 }
2772 
2773 bool ViewerPrivate::htmlLoadExternal() const
2774 {
2775     if (!mNodeHelper || !mMessage) {
2776         return mHtmlLoadExtOverride;
2777     }
2778 
2779     // when displaying an encrypted message, only load external resources on explicit request
2780     if (mNodeHelper->overallEncryptionState(mMessage.data()) != MimeTreeParser::KMMsgNotEncrypted) {
2781         return mHtmlLoadExtOverride;
2782     }
2783 
2784     const bool loadExternal = (mHtmlLoadExternalDefaultSetting && !mHtmlLoadExtOverride) || (!mHtmlLoadExternalDefaultSetting && mHtmlLoadExtOverride);
2785 
2786     return loadExternal;
2787 }
2788 
2789 void ViewerPrivate::setDisplayFormatMessageOverwrite(Viewer::DisplayFormatMessage format)
2790 {
2791     if (mDisplayFormatMessageOverwrite != format) {
2792         mDisplayFormatMessageOverwrite = format;
2793         // keep toggle display mode action state in sync.
2794         if (mToggleDisplayModeAction) {
2795             mToggleDisplayModeAction->setChecked(htmlMail());
2796         }
2797     }
2798 }
2799 
2800 bool ViewerPrivate::htmlMailGlobalSetting() const
2801 {
2802     return mHtmlMailGlobalSetting;
2803 }
2804 
2805 Viewer::DisplayFormatMessage ViewerPrivate::displayFormatMessageOverwrite() const
2806 {
2807     return mDisplayFormatMessageOverwrite;
2808 }
2809 
2810 void ViewerPrivate::setHtmlLoadExtDefault(bool loadExtDefault)
2811 {
2812     mHtmlLoadExternalDefaultSetting = loadExtDefault;
2813 }
2814 
2815 void ViewerPrivate::setHtmlLoadExtOverride(bool loadExtOverride)
2816 {
2817     mHtmlLoadExtOverride = loadExtOverride;
2818 }
2819 
2820 bool ViewerPrivate::htmlLoadExtOverride() const
2821 {
2822     return mHtmlLoadExtOverride;
2823 }
2824 
2825 void ViewerPrivate::setDecryptMessageOverwrite(bool overwrite)
2826 {
2827     mDecrytMessageOverwrite = overwrite;
2828 }
2829 
2830 bool ViewerPrivate::showSignatureDetails() const
2831 {
2832     return mShowSignatureDetails;
2833 }
2834 
2835 void ViewerPrivate::setShowSignatureDetails(bool showDetails)
2836 {
2837     mShowSignatureDetails = showDetails;
2838 }
2839 
2840 void ViewerPrivate::setShowEncryptionDetails(bool showEncDetails)
2841 {
2842     mShowEncryptionDetails = showEncDetails;
2843 }
2844 
2845 bool ViewerPrivate::showEncryptionDetails() const
2846 {
2847     return mShowEncryptionDetails;
2848 }
2849 
2850 void ViewerPrivate::scrollToAttachment(KMime::Content *node)
2851 {
2852     const QString indexStr = node->index().toString();
2853     // The anchors for this are created in ObjectTreeParser::parseObjectTree()
2854     mViewer->scrollToAnchor(QLatin1StringView("attachmentDiv") + indexStr);
2855 
2856     // Remove any old color markings which might be there
2857     const KMime::Content *root = node->topLevel();
2858     const int totalChildCount = Util::allContents(root).size();
2859     for (int i = 0; i < totalChildCount + 1; ++i) {
2860         // Not optimal I need to optimize it. But for the moment it removes yellow mark
2861         mViewer->removeAttachmentMarking(QStringLiteral("attachmentDiv%1").arg(i + 1));
2862         mViewer->removeAttachmentMarking(QStringLiteral("attachmentDiv1.%1").arg(i + 1));
2863         mViewer->removeAttachmentMarking(QStringLiteral("attachmentDiv2.%1").arg(i + 1));
2864     }
2865 
2866     // Don't mark hidden nodes, that would just produce a strange yellow line
2867     if (mNodeHelper->isNodeDisplayedHidden(node)) {
2868         return;
2869     }
2870 
2871     // Now, color the div of the attachment in yellow, so that the user sees what happened.
2872     // We created a special marked div for this in writeAttachmentMarkHeader() in ObjectTreeParser,
2873     // find and modify that now.
2874     mViewer->markAttachment(QLatin1StringView("attachmentDiv") + indexStr, QStringLiteral("border:2px solid %1").arg(cssHelper()->pgpWarnColor().name()));
2875 }
2876 
2877 void ViewerPrivate::setUseFixedFont(bool useFixedFont)
2878 {
2879     if (mHtmlHeadSettings.fixedFont != useFixedFont) {
2880         mHtmlHeadSettings.fixedFont = useFixedFont;
2881         if (mToggleFixFontAction) {
2882             mToggleFixFontAction->setChecked(mHtmlHeadSettings.fixedFont);
2883         }
2884     }
2885 }
2886 
2887 void ViewerPrivate::itemFetchResult(KJob *job)
2888 {
2889     if (job->error()) {
2890         displaySplashPage(i18n("Message loading failed: %1.", job->errorText()));
2891     } else {
2892         auto fetch = qobject_cast<Akonadi::ItemFetchJob *>(job);
2893         Q_ASSERT(fetch);
2894         if (fetch->items().isEmpty()) {
2895             displaySplashPage(i18n("Message not found."));
2896         } else {
2897             setMessageItem(fetch->items().constFirst());
2898         }
2899     }
2900 }
2901 
2902 void ViewerPrivate::slotItemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts)
2903 {
2904     if (item.id() != messageItem().id()) {
2905         qCDebug(MESSAGEVIEWER_LOG) << "Update for an already forgotten item. Weird.";
2906         return;
2907     }
2908     if (parts.contains("PLD:RFC822")) {
2909         setMessageItem(item, MimeTreeParser::Force);
2910     }
2911 }
2912 
2913 void ViewerPrivate::slotItemMoved(const Akonadi::Item &item, const Akonadi::Collection &, const Akonadi::Collection &)
2914 {
2915     // clear the view after the current item has been moved somewhere else (e.g. to trash)
2916     if (item.id() == messageItem().id()) {
2917         slotClear();
2918     }
2919 }
2920 
2921 void ViewerPrivate::slotClear()
2922 {
2923     q->clear(MimeTreeParser::Force);
2924     Q_EMIT itemRemoved();
2925 }
2926 
2927 void ViewerPrivate::slotMessageRendered()
2928 {
2929     if (!mMessageItem.isValid()) {
2930         return;
2931     }
2932 
2933     /**
2934      * This slot might be called multiple times for the same message if
2935      * some asynchronous mementos are involved in rendering. Therefore we
2936      * have to make sure we execute the MessageLoadedHandlers only once.
2937      */
2938     if (mMessageItem.id() == mPreviouslyViewedItemId) {
2939         return;
2940     }
2941 
2942     mPreviouslyViewedItemId = mMessageItem.id();
2943 
2944     for (AbstractMessageLoadedHandler *handler : std::as_const(mMessageLoadedHandlers)) {
2945         handler->setItem(mMessageItem);
2946     }
2947 }
2948 
2949 void ViewerPrivate::setZoomFactor(qreal zoomFactor)
2950 {
2951     mZoomActionMenu->setWebViewerZoomFactor(zoomFactor);
2952 }
2953 
2954 void ViewerPrivate::goOnline()
2955 {
2956     Q_EMIT makeResourceOnline(Viewer::AllResources);
2957 }
2958 
2959 void ViewerPrivate::goResourceOnline()
2960 {
2961     Q_EMIT makeResourceOnline(Viewer::SelectedResource);
2962 }
2963 
2964 void ViewerPrivate::slotSaveMessageDisplayFormat()
2965 {
2966     if (mMessageItem.isValid()) {
2967         auto job = new MessageViewer::ModifyMessageDisplayFormatJob(mSession, this);
2968         job->setMessageFormat(displayFormatMessageOverwrite());
2969         job->setMessageItem(mMessageItem);
2970         job->setRemoteContent(htmlLoadExtOverride());
2971         job->start();
2972     }
2973 }
2974 
2975 void ViewerPrivate::slotResetMessageDisplayFormat()
2976 {
2977     if (mMessageItem.isValid()) {
2978         if (mMessageItem.hasAttribute<MessageViewer::MessageDisplayFormatAttribute>()) {
2979             auto job = new MessageViewer::ModifyMessageDisplayFormatJob(mSession, this);
2980             job->setMessageItem(mMessageItem);
2981             job->setResetFormat(true);
2982             job->start();
2983         }
2984     }
2985 }
2986 
2987 void ViewerPrivate::slotMessageMayBeAScam()
2988 {
2989     if (mMessageItem.isValid()) {
2990         if (mMessageItem.hasAttribute<MessageViewer::ScamAttribute>()) {
2991             const MessageViewer::ScamAttribute *const attr = mMessageItem.attribute<MessageViewer::ScamAttribute>();
2992             if (attr && !attr->isAScam()) {
2993                 return;
2994             }
2995         }
2996         if (mMessageItem.hasPayload<KMime::Message::Ptr>()) {
2997             auto message = mMessageItem.payload<KMime::Message::Ptr>();
2998             const QString email = QLatin1StringView(KEmailAddress::firstEmailAddress(message->from()->as7BitString(false)));
2999             const QStringList lst = MessageViewer::MessageViewerSettings::self()->scamDetectionWhiteList();
3000             if (lst.contains(email)) {
3001                 return;
3002             }
3003         }
3004     }
3005     if (!mScamDetectionWarning) {
3006         createScamDetectionWarningWidget();
3007     }
3008     mScamDetectionWarning->slotShowWarning();
3009 }
3010 
3011 void ViewerPrivate::slotMessageIsNotAScam()
3012 {
3013     if (mMessageItem.isValid()) {
3014         auto attr = mMessageItem.attribute<MessageViewer::ScamAttribute>(Akonadi::Item::AddIfMissing);
3015         attr->setIsAScam(false);
3016         auto modify = new Akonadi::ItemModifyJob(mMessageItem, mSession);
3017         modify->setIgnorePayload(true);
3018         modify->disableRevisionCheck();
3019         connect(modify, &KJob::result, this, &ViewerPrivate::slotModifyItemDone);
3020     }
3021 }
3022 
3023 void ViewerPrivate::slotModifyItemDone(KJob *job)
3024 {
3025     if (job && job->error()) {
3026         qCWarning(MESSAGEVIEWER_LOG) << " Error trying to change attribute:" << job->errorText();
3027     }
3028 }
3029 
3030 void ViewerPrivate::saveMainFrameScreenshotInFile(const QString &filename)
3031 {
3032     mViewer->saveMainFrameScreenshotInFile(filename);
3033 }
3034 
3035 void ViewerPrivate::slotAddToWhiteList()
3036 {
3037     if (mMessageItem.isValid()) {
3038         if (mMessageItem.hasPayload<KMime::Message::Ptr>()) {
3039             auto message = mMessageItem.payload<KMime::Message::Ptr>();
3040             const QString email = QLatin1StringView(KEmailAddress::firstEmailAddress(message->from()->as7BitString(false)));
3041             QStringList lst = MessageViewer::MessageViewerSettings::self()->scamDetectionWhiteList();
3042             if (lst.contains(email)) {
3043                 return;
3044             }
3045             lst << email;
3046             MessageViewer::MessageViewerSettings::self()->setScamDetectionWhiteList(lst);
3047             MessageViewer::MessageViewerSettings::self()->save();
3048         }
3049     }
3050 }
3051 
3052 void ViewerPrivate::slotRefreshMessage(const Akonadi::Item &item)
3053 {
3054     if (item.id() == mMessageItem.id()) {
3055         setMessageItem(item, MimeTreeParser::Force);
3056     }
3057 }
3058 
3059 void ViewerPrivate::slotServiceUrlSelected(PimCommon::ShareServiceUrlManager::ServiceType serviceType)
3060 {
3061     const QUrl url = mShareServiceManager->generateServiceUrl(mClickedUrl.toString(), QString(), serviceType);
3062     mShareServiceManager->openUrl(url);
3063 }
3064 
3065 QList<QAction *> ViewerPrivate::interceptorUrlActions(const WebEngineViewer::WebHitTestResult &result) const
3066 {
3067     return mViewer->interceptorUrlActions(result);
3068 }
3069 
3070 void ViewerPrivate::setPrintElementBackground(bool printElementBackground)
3071 {
3072     mViewer->setPrintElementBackground(printElementBackground);
3073 }
3074 
3075 void ViewerPrivate::slotToggleEmoticons()
3076 {
3077     mForceEmoticons = !mForceEmoticons;
3078     // Save value
3079     MessageViewer::MessageViewerSettings::self()->setShowEmoticons(mForceEmoticons);
3080     headerStylePlugin()->headerStyle()->setShowEmoticons(mForceEmoticons);
3081     update(MimeTreeParser::Force);
3082 }
3083 
3084 void ViewerPrivate::slotZoomChanged(qreal zoom)
3085 {
3086     mViewer->slotZoomChanged(zoom);
3087     const qreal zoomFactor = zoom * 100;
3088     MessageViewer::MessageViewerSettings::self()->setZoomFactor(zoomFactor);
3089     Q_EMIT zoomChanged(zoomFactor);
3090 }
3091 
3092 void ViewerPrivate::updateShowMultiMessagesButton(bool enablePreviousButton, bool enableNextButton)
3093 {
3094     mShowNextMessageWidget->updateButton(enablePreviousButton, enableNextButton);
3095 }
3096 
3097 DKIMViewerMenu *ViewerPrivate::dkimViewerMenu()
3098 {
3099     if (MessageViewer::MessageViewerSettings::self()->enabledDkim()) {
3100         if (!messageIsInSpecialFolder()) {
3101             if (!mDkimViewerMenu) {
3102                 mDkimViewerMenu = new MessageViewer::DKIMViewerMenu(this);
3103                 connect(mDkimViewerMenu, &DKIMViewerMenu::recheckSignature, this, [this]() {
3104                     MessageViewer::DKIMManager::self()->checkDKim(mMessageItem);
3105                 });
3106                 connect(mDkimViewerMenu, &DKIMViewerMenu::updateDkimKey, this, []() {
3107                     qWarning() << " Unimplemented yet updateDkimKey";
3108                 });
3109                 connect(mDkimViewerMenu, &DKIMViewerMenu::showDkimRules, this, [this]() {
3110                     DKIMManageRulesDialog dlg(viewer());
3111                     dlg.exec();
3112                 });
3113             }
3114             mDkimViewerMenu->setEnableUpdateDkimKeyMenu(MessageViewer::MessageViewerSettings::saveKey()
3115                                                         == MessageViewer::MessageViewerSettings::EnumSaveKey::Save);
3116             return mDkimViewerMenu;
3117         }
3118     }
3119     return nullptr;
3120 }
3121 
3122 bool ViewerPrivate::isAutocryptEnabled(KMime::Message *message)
3123 {
3124     if (!mIdentityManager) {
3125         return false;
3126     }
3127 
3128     const auto id = MessageCore::Util::identityForMessage(message, mIdentityManager, mFolderIdentity);
3129     return id.autocryptEnabled();
3130 }
3131 
3132 void ViewerPrivate::setIdentityManager(KIdentityManagementCore::IdentityManager *ident)
3133 {
3134     mIdentityManager = ident;
3135 }
3136 
3137 void MessageViewer::ViewerPrivate::setFolderIdentity(uint folderIdentity)
3138 {
3139     mFolderIdentity = folderIdentity;
3140 }
3141 
3142 #include "moc_viewer_p.cpp"