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("\"/> "); 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"