1 /* Copyright (C) 2006 - 2016 Jan Kundrát <jkt@kde.org>
2 Copyright (C) 2014 Luke Dashjr <luke+trojita@dashjr.org>
3 Copyright (C) 2014 - 2015 Stephan Platz <trojita@paalsteek.de>
5 This file is part of the Trojita Qt IMAP e-mail client,
6 http://trojita.flaska.net/
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of
11 the License or (at your option) version 3 or any later version
12 accepted by the membership of KDE e.V. (or its successor approved
13 by the membership of KDE e.V.), which shall act as a proxy
14 defined in Section 14 of version 3 of the license.
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program. If not, see <http://www.gnu.org/licenses/>.
28 #include <QStackedLayout>
30 #include "Common/InvokeMethod.h"
31 #include "Common/SettingsNames.h"
32 #include "Composer/QuoteText.h"
33 #include "Composer/SubjectMangling.h"
34 #include "Cryptography/MessageModel.h"
35 #include "Gui/MessageView.h"
36 #include "Gui/ComposeWidget.h"
37 #include "Gui/EnvelopeView.h"
38 #include "Gui/ExternalElementsWidget.h"
39 #include "Gui/OverlayWidget.h"
40 #include "Gui/PartWidgetFactoryVisitor.h"
41 #include "Gui/ShortcutHandler/ShortcutHandler.h"
42 #include "Gui/SimplePartWidget.h"
43 #include "Gui/Spinner.h"
44 #include "Gui/TagListWidget.h"
45 #include "Gui/UserAgentWebPage.h"
46 #include "Gui/Window.h"
47 #include "Imap/Model/MailboxTree.h"
48 #include "Imap/Model/MsgListModel.h"
49 #include "Imap/Model/NetworkWatcher.h"
50 #include "Imap/Model/Utils.h"
51 #include "Imap/Network/MsgPartNetAccessManager.h"
52 #include "Plugins/PluginManager.h"
53 #include "UiUtils/IconLoader.h"
58 MessageView::MessageView(QWidget
*parent
, QSettings
*settings
, Plugins::PluginManager
*pluginManager
,
59 Imap::Mailbox::FavoriteTagsModel
*m_favoriteTags
)
61 , m_stack(new QStackedLayout(this))
63 , netAccess(new Imap::Network::MsgPartNetAccessManager(this))
64 , factory(new PartWidgetFactory(netAccess
, this,
65 std::unique_ptr
<PartWidgetFactoryVisitor
>(new PartWidgetFactoryVisitor())))
66 , m_settings(settings
)
67 , m_pluginManager(pluginManager
)
69 connect(netAccess
, &Imap::Network::MsgPartNetAccessManager::requestingExternal
, this, &MessageView::externalsRequested
);
72 setBackgroundRole(QPalette::Base
);
73 setForegroundRole(QPalette::Text
);
74 setAutoFillBackground(true);
75 setFocusPolicy(Qt::StrongFocus
); // not by the wheel
78 m_zoomIn
= ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_in"), this);
80 connect(m_zoomIn
, &QAction::triggered
, this, &MessageView::zoomIn
);
82 m_zoomOut
= ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_out"), this);
84 connect(m_zoomOut
, &QAction::triggered
, this, &MessageView::zoomOut
);
86 m_zoomOriginal
= ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_original"), this);
87 addAction(m_zoomOriginal
);
88 connect(m_zoomOriginal
, &QAction::triggered
, this, &MessageView::zoomOriginal
);
90 m_findAction
= new QAction(UiUtils::loadIcon(QStringLiteral("edit-find")), tr("Search..."), this);
91 m_findAction
->setShortcut(tr("Ctrl+F"));
92 // We can search only in one part of the message at a time. If searching is initiated by shortcut,
93 // there is no better solution than ask Part Widgets in visible order to handle this request. A
94 // callback method called MessageView::triggerSearchDialogBy is provided for this purpose.
95 // Other parts are searchable via the context menu only.
96 connect(m_findAction
, &QAction::triggered
, this, &MessageView::searchDialogRequested
);
97 addAction(m_findAction
);
99 // The homepage widget -- our poor man's splashscreen
100 m_homePage
= new EmbeddedWebView(this, new QNetworkAccessManager(this), settings
);
101 m_homePage
->setFixedSize(450,300);
102 CALL_LATER_NOARG(m_homePage
, handlePageLoadFinished
);
103 m_homePage
->setPage(new UserAgentWebPage(m_homePage
));
104 m_homePage
->installEventFilter(this);
105 m_homePage
->setAutoFillBackground(false);
106 m_stack
->addWidget(m_homePage
);
109 // The actual widget for the actual message
110 m_messageWidget
= new QWidget(this);
111 auto fullMsgLayout
= new QVBoxLayout(m_messageWidget
);
112 m_stack
->addWidget(m_messageWidget
);
114 m_envelope
= new EnvelopeView(m_messageWidget
, this);
115 fullMsgLayout
->addWidget(m_envelope
, 1);
117 tags
= new TagListWidget(m_messageWidget
, m_favoriteTags
);
118 connect(tags
, &TagListWidget::tagAdded
, this, &MessageView::newLabelAction
);
119 connect(tags
, &TagListWidget::tagRemoved
, this, &MessageView::deleteLabelAction
);
120 fullMsgLayout
->addWidget(tags
, 3);
122 externalElements
= new ExternalElementsWidget(this);
123 externalElements
->hide();
124 connect(externalElements
, &ExternalElementsWidget::loadingEnabled
, this, &MessageView::enableExternalData
);
125 fullMsgLayout
->addWidget(externalElements
, 1);
127 // put the actual messages into an extra horizontal view
128 // this allows us easy usage of the trailing stretch and also to indent the message a bit
129 m_msgLayout
= new QHBoxLayout
;
130 m_msgLayout
->setContentsMargins(6,6,6,0);
131 fullMsgLayout
->addLayout(m_msgLayout
, 1);
132 // add a strong stretch to squeeze header and message to the top
133 // possibly passing a large stretch factor to the message could be enough...
134 fullMsgLayout
->addStretch(1000);
137 markAsReadTimer
= new QTimer(this);
138 markAsReadTimer
->setSingleShot(true);
139 connect(markAsReadTimer
, &QTimer::timeout
, this, &MessageView::markAsRead
);
141 m_loadingSpinner
= new Spinner(this);
142 m_loadingSpinner
->setText(tr("Fetching\nMessage"));
143 m_loadingSpinner
->setType(Spinner::Sun
);
146 MessageView::~MessageView()
148 // Redmine #496 -- the default order of destruction starts with our QNAM subclass which in turn takes care of all pending
149 // QNetworkReply instances created by that manager. When the destruction goes to the WebKit objects, they try to disconnect
150 // from the network replies which are however gone already. We can mitigate that by simply making sure that the destruction
151 // starts with the QWebView subclasses and only after that proceeds to the QNAM. Qt's default order leads to segfaults here.
152 unsetPreviousMessage();
155 void MessageView::unsetPreviousMessage()
158 m_loadingItems
.clear();
159 message
= QModelIndex();
160 markAsReadTimer
->stop();
161 if (auto w
= bodyWidget()) {
162 m_stack
->removeWidget(dynamic_cast<QWidget
*>(w
));
165 m_envelope
->setMessage(QModelIndex());
167 messageModel
= nullptr;
170 void MessageView::setEmpty()
172 unsetPreviousMessage();
173 m_loadingSpinner
->stop();
174 m_stack
->setCurrentWidget(m_homePage
);
175 emit
messageChanged();
178 AbstractPartWidget
*MessageView::bodyWidget() const
180 if (m_msgLayout
->itemAt(0) && m_msgLayout
->itemAt(0)->widget()) {
181 return dynamic_cast<AbstractPartWidget
*>(m_msgLayout
->itemAt(0)->widget());
187 void MessageView::setMessage(const QModelIndex
&index
)
189 Q_ASSERT(index
.isValid());
190 QModelIndex messageIndex
= Imap::deproxifiedIndex(index
);
191 Q_ASSERT(messageIndex
.isValid());
193 if (message
== messageIndex
) {
194 // This is a duplicate call, let's do nothing.
195 // It might not be our fat-fingered user, but also just a side-effect of our duplicate invocation through
196 // QAbstractItemView::clicked() and activated().
200 unsetPreviousMessage();
202 message
= messageIndex
;
203 messageModel
= new Cryptography::MessageModel(this, message
);
204 messageModel
->setObjectName(QStringLiteral("cryptoMessageModel-%1-%2")
205 .arg(message
.data(Imap::Mailbox::RoleMailboxName
).toString(),
206 message
.data(Imap::Mailbox::RoleMessageUid
).toString()));
207 for (const auto &module
: m_pluginManager
->mimePartReplacers()) {
208 messageModel
->registerPartHandler(module
);
210 emit
messageModelChanged(messageModel
);
212 // The data might be available from the local cache, so let's try to save a possible roundtrip here
213 // by explicitly requesting the data
214 message
.data(Imap::Mailbox::RolePartData
);
216 if (!message
.data(Imap::Mailbox::RoleIsFetched
).toBool()) {
217 // This happens when the message placeholder is already available in the GUI, but the actual message data haven't been
218 // loaded yet. This is especially common with the threading model, but also with bigger unsynced mailboxes.
219 // Note that the data might be already available in the cache, it's just that it isn't in the mailbox tree yet.
220 m_waitingMessageConns
.emplace_back(
221 connect(messageModel
, &QAbstractItemModel::dataChanged
, this, [this](const QModelIndex
&topLeft
){
222 if (topLeft
.data(Imap::Mailbox::RoleIsFetched
).toBool()) {
223 // OK, message is fully fetched now
227 m_loadingSpinner
->setText(tr("Waiting\nfor\nMessage..."));
228 m_loadingSpinner
->start();
234 /** @short Implementation of the "hey, let's really display the message, its BODYSTRUCTURE is available now" */
235 void MessageView::showMessageNow()
237 Q_ASSERT(message
.data(Imap::Mailbox::RoleIsFetched
).toBool());
241 QModelIndex rootPartIndex
= messageModel
->index(0,0);
242 Q_ASSERT(rootPartIndex
.child(0,0).isValid());
244 netAccess
->setExternalsEnabled(false);
245 externalElements
->hide();
247 netAccess
->setModelMessage(rootPartIndex
);
249 m_loadingItems
.clear();
250 m_loadingSpinner
->stop();
252 m_envelope
->setMessage(message
);
254 auto updateTagList
= [this]() {
255 tags
->setTagList(message
.data(Imap::Mailbox::RoleMessageFlags
).toStringList());
257 connect(messageModel
, &QAbstractItemModel::dataChanged
, this, updateTagList
);
260 UiUtils::PartLoadingOptions loadingMode
;
261 if (m_settings
->value(Common::SettingsNames::guiPreferPlaintextRendering
, QVariant(true)).toBool())
262 loadingMode
|= UiUtils::PART_PREFER_PLAINTEXT_OVER_HTML
;
263 auto viewer
= factory
->walk(rootPartIndex
.child(0,0), 0, loadingMode
);
264 viewer
->setParent(this);
265 m_msgLayout
->addWidget(viewer
);
266 m_msgLayout
->setAlignment(viewer
, Qt::AlignTop
|Qt::AlignLeft
);
268 // We want to propagate the QWheelEvent to upper layers
269 viewer
->installEventFilter(this);
270 m_stack
->setCurrentWidget(m_messageWidget
);
272 if (m_netWatcher
&& m_netWatcher
->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE
273 && m_settings
->value(Common::SettingsNames::autoMarkReadEnabled
, QVariant(true)).toBool()) {
274 // No additional delay is needed here because the MsgListView won't open a message while the user keeps scrolling,
275 // which was AFAIK the original intention
276 markAsReadTimer
->start(m_settings
->value(Common::SettingsNames::autoMarkReadSeconds
, QVariant(0)).toUInt() * 1000);
279 emit
messageChanged();
282 /** @short There's no point in waiting for the message to appear */
283 void MessageView::clearWaitingConns()
285 for (auto &conn
: m_waitingMessageConns
) {
288 m_waitingMessageConns
.clear();
291 void MessageView::markAsRead()
293 if (!message
.isValid())
295 Imap::Mailbox::Model
*model
= const_cast<Imap::Mailbox::Model
*>(dynamic_cast<const Imap::Mailbox::Model
*>(message
.model()));
297 if (!model
->isNetworkAvailable())
299 if (!message
.data(Imap::Mailbox::RoleMessageIsMarkedRead
).toBool())
300 model
->markMessagesRead(QModelIndexList() << message
, Imap::Mailbox::FLAG_ADD
);
303 /** @short Inhibit the automatic marking of the current message as already read
305 The user might have e.g. explicitly marked a previously read message as unread again immediately after navigating back to it
306 in the message listing. In that situation, the message viewer shall respect this decision and inhibit the helper which would
307 otherwise mark the current message as read after a short timeout.
309 void MessageView::stopAutoMarkAsRead()
311 markAsReadTimer
->stop();
314 bool MessageView::eventFilter(QObject
*object
, QEvent
*event
)
316 if (event
->type() == QEvent::Wheel
) {
317 if (static_cast<QWheelEvent
*>(event
)->modifiers() == Qt::ControlModifier
) {
318 if (static_cast<QWheelEvent
*>(event
)->delta() > 0) {
324 // while the containing scrollview has Qt::StrongFocus, the event forwarding breaks that
325 // -> completely disable focus for the following wheel event ...
326 parentWidget()->setFocusPolicy(Qt::NoFocus
);
327 MessageView::event(event
);
329 parentWidget()->setFocusPolicy(Qt::StrongFocus
);
332 } else if (event
->type() == QEvent::KeyPress
|| event
->type() == QEvent::KeyRelease
) {
333 QKeyEvent
*keyEvent
= static_cast<QKeyEvent
*>(event
);
334 switch (keyEvent
->key()) {
340 case Qt::Key_PageDown
:
341 MessageView::event(event
);
347 return QObject::eventFilter(object
, event
);
350 return QObject::eventFilter(object
, event
);
354 QString
MessageView::quoteText() const
356 if (auto w
= bodyWidget()) {
357 const Imap::Message::Envelope
&e
= message
.data(Imap::Mailbox::RoleMessageEnvelope
).value
<Imap::Message::Envelope
>();
359 if (!e
.from
.isEmpty())
360 sender
= e
.from
[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME
);
361 if (e
.from
.isEmpty())
364 if (messageModel
->index(0, 0) /* fake message root */.child(0, 0) /* first MIME part */.data(Imap::Mailbox::RolePartDecryptionSupported
).toBool()) {
365 // This is just an UX improvement shortcut: real filtering for CVE-2019-10734 is in
366 // MultipartSignedEncryptedWidget::quoteMe().
367 // That is required because the encrypted part might not be the root part of the message.
368 return tr("On %1, %2 sent an encrypted message:\n> ...\n\n").arg(e
.date
.toLocalTime().toString(Qt::SystemLocaleLongDate
), sender
);
371 QStringList quote
= Composer::quoteText(w
->quoteMe().split(QLatin1Char('\n')));
372 // One extra newline at the end of the quoted text to separate the response
375 return tr("On %1, %2 wrote:\n").arg(e
.date
.toLocalTime().toString(Qt::SystemLocaleLongDate
), sender
) + quote
.join(QStringLiteral("\n"));
380 #define FORWARD_METHOD(METHOD) \
381 void MessageView::METHOD() \
383 if (auto w = bodyWidget()) { \
387 FORWARD_METHOD(zoomIn
)
388 FORWARD_METHOD(zoomOut
)
389 FORWARD_METHOD(zoomOriginal
)
390 FORWARD_METHOD(searchDialogRequested
)
392 void MessageView::setNetworkWatcher(Imap::Mailbox::NetworkWatcher
*netWatcher
)
394 m_netWatcher
= netWatcher
;
395 factory
->setNetworkWatcher(netWatcher
);
398 void MessageView::reply(MainWindow
*mainWindow
, Composer::ReplyMode mode
)
400 if (!message
.isValid())
403 // The Message-Id of the original message might have been empty; be sure we can handle that
404 QByteArray messageId
= message
.data(Imap::Mailbox::RoleMessageMessageId
).toByteArray();
405 QList
<QByteArray
> messageIdList
;
406 if (!messageId
.isEmpty()) {
407 messageIdList
.append(messageId
);
410 ComposeWidget::warnIfMsaNotConfigured(
411 ComposeWidget::createReply(mainWindow
, mode
, message
, QList
<QPair
<Composer::RecipientKind
,QString
> >(),
412 Composer::Util::replySubject(message
.data(Imap::Mailbox::RoleMessageSubject
).toString()),
413 quoteText(), messageIdList
,
414 message
.data(Imap::Mailbox::RoleMessageHeaderReferences
).value
<QList
<QByteArray
> >() + messageIdList
),
418 void MessageView::forward(MainWindow
*mainWindow
, const Composer::ForwardMode mode
)
420 if (!message
.isValid())
423 // The Message-Id of the original message might have been empty; be sure we can handle that
424 QByteArray messageId
= message
.data(Imap::Mailbox::RoleMessageMessageId
).toByteArray();
425 QList
<QByteArray
> messageIdList
;
426 if (!messageId
.isEmpty()) {
427 messageIdList
.append(messageId
);
430 ComposeWidget::warnIfMsaNotConfigured(
431 ComposeWidget::createForward(mainWindow
, mode
, message
, Composer::Util::forwardSubject(message
.data(Imap::Mailbox::RoleMessageSubject
).toString()),
432 messageIdList
, message
.data(Imap::Mailbox::RoleMessageHeaderReferences
).value
<QList
<QByteArray
>>() + messageIdList
),
436 void MessageView::externalsRequested(const QUrl
&url
)
439 externalElements
->show();
442 void MessageView::enableExternalData()
444 netAccess
->setExternalsEnabled(true);
445 externalElements
->hide();
446 if (auto w
= bodyWidget()) {
451 void MessageView::newLabelAction(const QString
&tag
)
453 if (!message
.isValid())
456 Imap::Mailbox::Model
*model
= dynamic_cast<Imap::Mailbox::Model
*>(const_cast<QAbstractItemModel
*>(message
.model()));
457 model
->setMessageFlags(QModelIndexList() << message
, tag
, Imap::Mailbox::FLAG_ADD
);
460 void MessageView::deleteLabelAction(const QString
&tag
)
462 if (!message
.isValid())
465 Imap::Mailbox::Model
*model
= dynamic_cast<Imap::Mailbox::Model
*>(const_cast<QAbstractItemModel
*>(message
.model()));
466 model
->setMessageFlags(QModelIndexList() << message
, tag
, Imap::Mailbox::FLAG_REMOVE
);
469 void MessageView::setHomepageUrl(const QUrl
&homepage
)
471 m_homePage
->load(homepage
);
474 void MessageView::showEvent(QShowEvent
*se
)
476 QWidget::showEvent(se
);
477 // The Oxygen style reset the attribute - since we're gonna cause an update() here anyway, it's
478 // a good moment to stress that "we know better, Hugo ;-)" -- Thomas
479 setAutoFillBackground(true);
482 void MessageView::partContextMenuRequested(const QPoint
&point
)
484 if (SimplePartWidget
*w
= qobject_cast
<SimplePartWidget
*>(sender())) {
486 w
->buildContextMenu(point
, menu
);
487 menu
.exec(w
->mapToGlobal(point
));
491 void MessageView::partLinkHovered(const QString
&link
, const QString
&title
, const QString
&textContent
)
494 Q_UNUSED(textContent
);
495 emit
linkHovered(link
);
498 QSettings
* MessageView::profileSettings() const
503 QModelIndex
MessageView::currentMessage() const
508 void MessageView::onWebViewLoadStarted()
510 QWebView
*wv
= qobject_cast
<QWebView
*>(sender());
513 if (m_netWatcher
&& m_netWatcher
->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE
) {
514 m_loadingItems
<< wv
;
515 m_loadingSpinner
->start(250);
519 void MessageView::onWebViewLoadFinished()
521 QWebView
*wv
= qobject_cast
<QWebView
*>(sender());
523 m_loadingItems
.remove(wv
);
524 if (m_loadingItems
.isEmpty())
525 m_loadingSpinner
->stop();
528 Plugins::PluginManager
*MessageView::pluginManager() const
530 return m_pluginManager
;
533 /** @short Callback for AbstractPartWidget */
534 void MessageView::triggerSearchDialogBy(EmbeddedWebView
*w
)
536 emit
searchRequestedBy(w
);