1 /* Copyright (C) 2006 - 2015 Jan Kundrát <jkt@flaska.net>
2    Copyright (C) 2013 - 2015 Pali Rohár <pali.rohar@gmail.com>
3 
4    This file is part of the Trojita Qt IMAP e-mail client,
5    http://trojita.flaska.net/
6 
7    This program is free software; you can redistribute it and/or
8    modify it under the terms of the GNU General Public License as
9    published by the Free Software Foundation; either version 2 of
10    the License or (at your option) version 3 or any later version
11    accepted by the membership of KDE e.V. (or its successor approved
12    by the membership of KDE e.V.), which shall act as a proxy
13    defined in Section 14 of version 3 of the license.
14 
15    This program is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19 
20    You should have received a copy of the GNU General Public License
21    along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 */
23 
24 #include <QAuthenticator>
25 #include <QDesktopServices>
26 #include <QDesktopWidget>
27 #include <QDir>
28 #include <QDockWidget>
29 #include <QFileDialog>
30 #include <QHeaderView>
31 #include <QItemSelectionModel>
32 #include <QKeyEvent>
33 #include <QMenuBar>
34 #include <QMessageBox>
35 #include <QProgressBar>
36 #include <QScrollBar>
37 #include <QSplitter>
38 #include <QSslError>
39 #include <QSslKey>
40 #include <QStackedWidget>
41 #include <QStatusBar>
42 #include <QTextDocument>
43 #include <QToolBar>
44 #include <QToolButton>
45 #include <QToolTip>
46 #include <QUrl>
47 #include <QWheelEvent>
48 #include <QPainterPath>
49 
50 #include "configure.cmake.h"
51 #include "Common/Application.h"
52 #include "Common/Paths.h"
53 #include "Common/PortNumbers.h"
54 #include "Common/SettingsNames.h"
55 #include "Composer/Mailto.h"
56 #include "Composer/SenderIdentitiesModel.h"
57 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
58 #  ifdef TROJITA_HAVE_GPGMEPP
59 #    include "Cryptography/GpgMe++.h"
60 #  endif
61 #endif
62 #include "Imap/Model/ImapAccess.h"
63 #include "Imap/Model/MailboxTree.h"
64 #include "Imap/Model/Model.h"
65 #include "Imap/Model/ModelWatcher.h"
66 #include "Imap/Model/MsgListModel.h"
67 #include "Imap/Model/NetworkWatcher.h"
68 #include "Imap/Model/PrettyMailboxModel.h"
69 #include "Imap/Model/PrettyMsgListModel.h"
70 #include "Imap/Model/SpecialFlagNames.h"
71 #include "Imap/Model/ThreadingMsgListModel.h"
72 #include "Imap/Model/Utils.h"
73 #include "Imap/Network/FileDownloadManager.h"
74 #include "MSA/ImapSubmit.h"
75 #include "MSA/Sendmail.h"
76 #include "MSA/SMTP.h"
77 #include "Plugins/AddressbookPlugin.h"
78 #include "Plugins/PasswordPlugin.h"
79 #include "Plugins/PluginManager.h"
80 #include "CompleteMessageWidget.h"
81 #include "ComposeWidget.h"
82 #include "MailBoxTreeView.h"
83 #include "MessageListWidget.h"
84 #include "MessageView.h"
85 #include "MessageSourceWidget.h"
86 #include "Gui/MessageHeadersWidget.h"
87 #include "MsgListView.h"
88 #include "OnePanelAtTimeWidget.h"
89 #include "PasswordDialog.h"
90 #include "ProtocolLoggerWidget.h"
91 #include "SettingsDialog.h"
92 #include "SimplePartWidget.h"
93 #include "Streams/SocketFactory.h"
94 #include "TaskProgressIndicator.h"
95 #include "Util.h"
96 #include "Window.h"
97 #include "ShortcutHandler/ShortcutHandler.h"
98 
99 #include "ui_CreateMailboxDialog.h"
100 #include "ui_AboutDialog.h"
101 
102 #include "Imap/Model/ModelTest/modeltest.h"
103 #include "UiUtils/IconLoader.h"
104 
105 /** @short All user-facing widgets and related classes */
106 namespace Gui
107 {
108 
109     static const char * const netErrorUnseen = "net_error_unseen";
110 
MainWindow(QSettings * settings)111 MainWindow::MainWindow(QSettings *settings): QMainWindow(), m_imapAccess(0), m_mainHSplitter(0), m_mainVSplitter(0),
112     m_mainStack(0), m_layoutMode(LAYOUT_COMPACT), m_skipSavingOfUI(true), m_delayedStateSaving(0), m_actionSortNone(0),
113     m_ignoreStoredPassword(false), m_settings(settings), m_pluginManager(0), m_networkErrorMessageBox(0), m_trayIcon(0)
114 {
115     setAttribute(Qt::WA_AlwaysShowToolTips);
116     // m_pluginManager must be created before calling createWidgets
117     m_pluginManager = new Plugins::PluginManager(this, m_settings,
118                                                  Common::SettingsNames::addressbookPlugin, Common::SettingsNames::passwordPlugin);
119     connect(m_pluginManager, &Plugins::PluginManager::pluginsChanged, this, &MainWindow::slotPluginsChanged);
120     connect(m_pluginManager, &Plugins::PluginManager::pluginError, this, [this](const QString &errorMessage) {
121         QMessageBox::warning(this, tr("Plugin Error"),
122                              //: The %1 placeholder is a full error message as provided by Qt, ready for human consumption.
123                              trUtf8("A plugin failed to load, therefore some functionality might be lost. "
124                                     "You might want to update your system or report a bug to your vendor."
125                                     "\n\n%1").arg(errorMessage));
126     });
127 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
128     Plugins::PluginManager::MimePartReplacers replacers;
129 #ifdef TROJITA_HAVE_GPGMEPP
130     replacers.emplace_back(std::make_shared<Cryptography::GpgMeReplacer>());
131 #endif
132     m_pluginManager->setMimePartReplacers(replacers);
133 #endif
134 
135     // ImapAccess contains a wrapper for retrieving passwords through some plugin.
136     // That PasswordWatcher is used by the SettingsDialog's widgets *and* by this class,
137     // which means that ImapAccess has to be constructed before we go and open the settings dialog.
138 
139     // FIXME: use another account-id at some point in future
140     //        we are now using the profile to avoid overwriting passwords of
141     //        other profiles in secure storage
142     QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
143     m_imapAccess = new Imap::ImapAccess(this, m_settings, m_pluginManager, profileName);
144     connect(m_imapAccess, &Imap::ImapAccess::cacheError, this, &MainWindow::cacheError);
145     connect(m_imapAccess, &Imap::ImapAccess::checkSslPolicy, this, &MainWindow::checkSslPolicy, Qt::QueuedConnection);
146 
147     ShortcutHandler *shortcutHandler = new ShortcutHandler(this);
148     shortcutHandler->setSettingsObject(m_settings);
149     defineActions();
150     shortcutHandler->readSettings(); // must happen after defineActions()
151 
152     createWidgets();
153 
154     Imap::migrateSettings(m_settings);
155 
156     m_senderIdentities = new Composer::SenderIdentitiesModel(this);
157     m_senderIdentities->loadFromSettings(*m_settings);
158 
159     if (! m_settings->contains(Common::SettingsNames::imapMethodKey)) {
160         QTimer::singleShot(0, this, SLOT(slotShowSettings()));
161     }
162 
163 
164     setupModels();
165     createActions();
166     createMenus();
167     slotToggleSysTray();
168     slotPluginsChanged();
169 
170     // Please note that Qt 4.6.1 really requires passing the method signature this way, *not* using the SLOT() macro
171     QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "slotComposeMailUrl");
172     QDesktopServices::setUrlHandler(QStringLiteral("x-trojita-manage-contact"), this, "slotManageContact");
173 
174     slotUpdateWindowTitle();
175 
176     recoverDrafts();
177 
178     if (m_actionLayoutWide->isEnabled() &&
179             m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutWide) {
180         m_actionLayoutWide->trigger();
181     } else if (m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutOneAtTime) {
182         m_actionLayoutOneAtTime->trigger();
183     } else {
184         m_actionLayoutCompact->trigger();
185     }
186 
187     connect(qApp, &QGuiApplication::applicationStateChanged, this,
188             [&](Qt::ApplicationState state) {
189                 if (state == Qt::ApplicationActive && m_networkErrorMessageBox && m_networkErrorMessageBox->property(netErrorUnseen).toBool()) {
190                     m_networkErrorMessageBox->setProperty(netErrorUnseen, false);
191                     m_networkErrorMessageBox->show();
192                 }
193             });
194 
195     // Don't listen to QDesktopWidget::resized; that is emitted too early (when it gets fired, the screen size has changed, but
196     // the workspace area is still the old one). Instead, listen to workAreaResized which gets emitted at an appropriate time.
197     // The delay is still there to guarantee some smoothing; on jkt's box there are typically three events in a rapid sequence
198     // (some of them most likely due to the fact that at first, the actual desktop gets resized, the plasma panel reacts
199     // to that and only after the panel gets resized, the available size of "the rest" is correct again).
200     // Which is why it makes sense to introduce some delay in there. The 0.5s delay is my best guess and "should work" (especially
201     // because every change bumps the timer anyway, as Thomas pointed out).
202     QTimer *delayedResize = new QTimer(this);
203     delayedResize->setSingleShot(true);
204     delayedResize->setInterval(500);
205     connect(delayedResize, &QTimer::timeout, this, &MainWindow::desktopGeometryChanged);
206     connect(qApp->desktop(), &QDesktopWidget::workAreaResized, delayedResize, static_cast<void (QTimer::*)()>(&QTimer::start));
207     m_skipSavingOfUI = false;
208 }
209 
defineActions()210 void MainWindow::defineActions()
211 {
212     ShortcutHandler *shortcutHandler = ShortcutHandler::instance();
213     shortcutHandler->defineAction(QStringLiteral("action_application_exit"), QStringLiteral("application-exit"), tr("E&xit"), QKeySequence::Quit);
214     shortcutHandler->defineAction(QStringLiteral("action_compose_mail"), QStringLiteral("document-edit"), tr("&New Message..."), QKeySequence::New);
215     shortcutHandler->defineAction(QStringLiteral("action_compose_draft"), QStringLiteral("document-open-recent"), tr("&Edit Draft..."));
216     shortcutHandler->defineAction(QStringLiteral("action_show_menubar"), QStringLiteral("view-list-text"), tr("Show Main Menu &Bar"), tr("Ctrl+M"));
217     shortcutHandler->defineAction(QStringLiteral("action_expunge"), QStringLiteral("trash-empty"), tr("Exp&unge"), tr("Ctrl+E"));
218     shortcutHandler->defineAction(QStringLiteral("action_mark_as_read"), QStringLiteral("mail-mark-read"), tr("Mark as &Read"), QStringLiteral("M"));
219     shortcutHandler->defineAction(QStringLiteral("action_go_to_next_unread"), QStringLiteral("arrow-right"), tr("&Next Unread Message"), QStringLiteral("N"));
220     shortcutHandler->defineAction(QStringLiteral("action_go_to_previous_unread"), QStringLiteral("arrow-left"), tr("&Previous Unread Message"), QStringLiteral("P"));
221     shortcutHandler->defineAction(QStringLiteral("action_mark_as_deleted"), QStringLiteral("list-remove"), tr("Mark as &Deleted"), QKeySequence(Qt::Key_Delete).toString());
222     shortcutHandler->defineAction(QStringLiteral("action_mark_as_flagged"), QStringLiteral("mail-flagged"), tr("Mark as &Flagged"), QStringLiteral("S"));
223     shortcutHandler->defineAction(QStringLiteral("action_mark_as_junk"), QStringLiteral("mail-mark-junk"), tr("Mark as &Junk"), QStringLiteral("J"));
224     shortcutHandler->defineAction(QStringLiteral("action_mark_as_notjunk"), QStringLiteral("mail-mark-notjunk"), tr("Mark as Not &junk"), QStringLiteral("Shift+J"));
225     shortcutHandler->defineAction(QStringLiteral("action_save_message_as"), QStringLiteral("document-save"), tr("&Save Message..."));
226     shortcutHandler->defineAction(QStringLiteral("action_view_message_source"), QString(), tr("View Message &Source..."));
227     shortcutHandler->defineAction(QStringLiteral("action_view_message_headers"), QString(), tr("View Message &Headers..."), tr("Ctrl+U"));
228     shortcutHandler->defineAction(QStringLiteral("action_reply_private"), QStringLiteral("mail-reply-sender"), tr("&Private Reply"), tr("Ctrl+Shift+A"));
229     shortcutHandler->defineAction(QStringLiteral("action_reply_all_but_me"), QStringLiteral("mail-reply-all"), tr("Reply to All &but Me"), tr("Ctrl+Shift+R"));
230     shortcutHandler->defineAction(QStringLiteral("action_reply_all"), QStringLiteral("mail-reply-all"), tr("Reply to &All"), tr("Ctrl+Alt+Shift+R"));
231     shortcutHandler->defineAction(QStringLiteral("action_reply_list"), QStringLiteral("mail-reply-list"), tr("Reply to &Mailing List"), tr("Ctrl+L"));
232     shortcutHandler->defineAction(QStringLiteral("action_reply_guess"), QString(), tr("Reply by &Guess"), tr("Ctrl+R"));
233     shortcutHandler->defineAction(QStringLiteral("action_forward_attachment"), QStringLiteral("mail-forward"), tr("&Forward"), tr("Ctrl+Shift+F"));
234     shortcutHandler->defineAction(QStringLiteral("action_contact_editor"), QStringLiteral("contact-unknown"), tr("Address Book..."));
235     shortcutHandler->defineAction(QStringLiteral("action_network_offline"), QStringLiteral("network-disconnect"), tr("&Offline"));
236     shortcutHandler->defineAction(QStringLiteral("action_network_expensive"), QStringLiteral("network-wireless"), tr("&Expensive Connection"));
237     shortcutHandler->defineAction(QStringLiteral("action_network_online"), QStringLiteral("network-connect"), tr("&Free Access"));
238     shortcutHandler->defineAction(QStringLiteral("action_messagewindow_close"), QStringLiteral("window-close"), tr("Close Standalone Message Window"));
239     shortcutHandler->defineAction(QStringLiteral("action_oneattime_go_back"), QStringLiteral("go-previous"), tr("Navigate Back"), QKeySequence(QKeySequence::Back).toString());
240     shortcutHandler->defineAction(QStringLiteral("action_zoom_in"), QStringLiteral("zoom-in"), tr("Zoom In"), QKeySequence::ZoomIn);
241     shortcutHandler->defineAction(QStringLiteral("action_zoom_out"), QStringLiteral("zoom-out"), tr("Zoom Out"), QKeySequence::ZoomOut);
242     shortcutHandler->defineAction(QStringLiteral("action_zoom_original"), QStringLiteral("zoom-original"), tr("Original Size"));
243 }
244 
createActions()245 void MainWindow::createActions()
246 {
247     // The shortcuts are a little bit complicated, unfortunately. This is what the other applications use by default:
248     //
249     // Thunderbird:
250     // private: Ctrl+R
251     // all: Ctrl+Shift+R
252     // list: Ctrl+Shift+L
253     // forward: Ctrl+L
254     // (no shortcuts for type of forwarding)
255     // bounce: ctrl+B
256     // new message: Ctrl+N
257     //
258     // KMail:
259     // "reply": R
260     // private: Shift+A
261     // all: A
262     // list: L
263     // forward as attachment: F
264     // forward inline: Shift+F
265     // bounce: E
266     // new: Ctrl+N
267 
268     m_actionContactEditor = ShortcutHandler::instance()->createAction(QStringLiteral("action_contact_editor"), this, SLOT(invokeContactEditor()), this);
269 
270     m_mainToolbar = addToolBar(tr("Navigation"));
271     m_mainToolbar->setObjectName(QStringLiteral("mainToolbar"));
272 
273     reloadMboxList = new QAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("&Update List of Child Mailboxes"), this);
274     connect(reloadMboxList, &QAction::triggered, this, &MainWindow::slotReloadMboxList);
275 
276     resyncMbox = new QAction(UiUtils::loadIcon(QStringLiteral("view-refresh")), tr("Check for &New Messages"), this);
277     connect(resyncMbox, &QAction::triggered, this, &MainWindow::slotResyncMbox);
278 
279     reloadAllMailboxes = new QAction(tr("&Reload Everything"), this);
280     // connect later
281 
282     exitAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_application_exit"), qApp, SLOT(quit()), this);
283     exitAction->setStatusTip(tr("Exit the application"));
284 
285     netOffline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_offline"));
286     netOffline->setCheckable(true);
287     // connect later
288     netExpensive = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_expensive"));
289     netExpensive->setCheckable(true);
290     // connect later
291     netOnline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_online"));
292     netOnline->setCheckable(true);
293     // connect later
294 
295     QActionGroup *netPolicyGroup = new QActionGroup(this);
296     netPolicyGroup->setExclusive(true);
297     netPolicyGroup->addAction(netOffline);
298     netPolicyGroup->addAction(netExpensive);
299     netPolicyGroup->addAction(netOnline);
300 
301     //: a debugging tool showing the full contents of the whole IMAP server; all folders, messages and their parts
302     showFullView = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find-mail")), tr("Show Full &Tree Window"), this);
303     showFullView->setCheckable(true);
304     connect(showFullView, &QAction::triggered, allDock, &QWidget::setVisible);
305     connect(allDock, &QDockWidget::visibilityChanged, showFullView, &QAction::setChecked);
306 
307     //: list of active "tasks", entities which are performing certain action like downloading a message or syncing a mailbox
308     showTaskView = new QAction(tr("Show ImapTask t&ree"), this);
309     showTaskView->setCheckable(true);
310     connect(showTaskView, &QAction::triggered, taskDock, &QWidget::setVisible);
311     connect(taskDock, &QDockWidget::visibilityChanged, showTaskView, &QAction::setChecked);
312 
313     //: a debugging tool showing the mime tree of the current message
314     showMimeView = new QAction(tr("Show &MIME tree"), this);
315     showMimeView->setCheckable(true);
316     connect(showMimeView, &QAction::triggered, mailMimeDock, &QWidget::setVisible);
317     connect(mailMimeDock, &QDockWidget::visibilityChanged, showMimeView, &QAction::setChecked);
318 
319     showImapLogger = new QAction(tr("Show IMAP protocol &log"), this);
320     showImapLogger->setCheckable(true);
321     connect(showImapLogger, &QAction::toggled, imapLoggerDock, &QWidget::setVisible);
322     connect(imapLoggerDock, &QDockWidget::visibilityChanged, showImapLogger, &QAction::setChecked);
323 
324     //: file to save the debug log into
325     logPersistent = new QAction(tr("Log &into %1").arg(Imap::Mailbox::persistentLogFileName()), this);
326     logPersistent->setCheckable(true);
327     connect(logPersistent, &QAction::triggered, imapLogger, &ProtocolLoggerWidget::slotSetPersistentLogging);
328     connect(imapLogger, &ProtocolLoggerWidget::persistentLoggingChanged, logPersistent, &QAction::setChecked);
329 
330     showImapCapabilities = new QAction(tr("IMAP Server In&formation..."), this);
331     connect(showImapCapabilities, &QAction::triggered, this, &MainWindow::slotShowImapInfo);
332 
333     showMenuBar = ShortcutHandler::instance()->createAction(QStringLiteral("action_show_menubar"), this);
334     showMenuBar->setCheckable(true);
335     showMenuBar->setChecked(true);
336     connect(showMenuBar, &QAction::triggered, menuBar(), &QMenuBar::setVisible);
337     connect(showMenuBar, &QAction::triggered, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
338 
339     showToolBar = new QAction(tr("Show &Toolbar"), this);
340     showToolBar->setCheckable(true);
341     connect(showToolBar, &QAction::triggered, m_mainToolbar, &QWidget::setVisible);
342     connect(m_mainToolbar, &QToolBar::visibilityChanged, showToolBar, &QAction::setChecked);
343     connect(m_mainToolbar, &QToolBar::visibilityChanged, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
344 
345     configSettings = new QAction(UiUtils::loadIcon(QStringLiteral("configure")),  tr("&Settings..."), this);
346     connect(configSettings, &QAction::triggered, this, &MainWindow::slotShowSettings);
347 
348     QAction *triggerSearch = new QAction(this);
349     addAction(triggerSearch);
350     triggerSearch->setShortcut(QKeySequence(QStringLiteral(":, =")));
351     connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusRawSearch);
352 
353     triggerSearch = new QAction(this);
354     addAction(triggerSearch);
355     triggerSearch->setShortcut(QKeySequence(QStringLiteral("/")));
356     connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusSearch);
357 
358     m_oneAtTimeGoBack = ShortcutHandler::instance()->createAction(QStringLiteral("action_oneattime_go_back"), this);
359     m_oneAtTimeGoBack->setEnabled(false);
360 
361     composeMail = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_mail"), this, SLOT(slotComposeMail()), this);
362     m_editDraft = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_draft"), this, SLOT(slotEditDraft()), this);
363 
364     expunge = ShortcutHandler::instance()->createAction(QStringLiteral("action_expunge"), this, SLOT(slotExpunge()), this);
365 
366     m_forwardAsAttachment = ShortcutHandler::instance()->createAction(QStringLiteral("action_forward_attachment"), this, SLOT(slotForwardAsAttachment()), this);
367     markAsRead = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_read"), this);
368     markAsRead->setCheckable(true);
369     msgListWidget->tree->addAction(markAsRead);
370     connect(markAsRead, &QAction::triggered, this, &MainWindow::handleMarkAsRead);
371 
372     m_nextMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_next_unread"), this, SLOT(slotNextUnread()), this);
373     msgListWidget->tree->addAction(m_nextMessage);
374     m_messageWidget->messageView->addAction(m_nextMessage);
375 
376     m_previousMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_previous_unread"), this, SLOT(slotPreviousUnread()), this);
377     msgListWidget->tree->addAction(m_previousMessage);
378     m_messageWidget->messageView->addAction(m_previousMessage);
379 
380     markAsDeleted = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_deleted"), this);
381     markAsDeleted->setCheckable(true);
382     msgListWidget->tree->addAction(markAsDeleted);
383     connect(markAsDeleted, &QAction::triggered, this, &MainWindow::handleMarkAsDeleted);
384 
385     markAsFlagged = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_flagged"), this);
386     markAsFlagged->setCheckable(true);
387     connect(markAsFlagged, &QAction::triggered, this, &MainWindow::handleMarkAsFlagged);
388 
389     markAsJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_junk"), this);
390     markAsJunk->setCheckable(true);
391     connect(markAsJunk, &QAction::triggered, this, &MainWindow::handleMarkAsJunk);
392 
393     markAsNotJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_notjunk"), this);
394     markAsNotJunk->setCheckable(true);
395     connect(markAsNotJunk, &QAction::triggered, this, &MainWindow::handleMarkAsNotJunk);
396 
397     saveWholeMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_save_message_as"), this, SLOT(slotSaveCurrentMessageBody()), this);
398     msgListWidget->tree->addAction(saveWholeMessage);
399 
400     viewMsgSource = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_source"), this, SLOT(slotViewMsgSource()), this);
401     msgListWidget->tree->addAction(viewMsgSource);
402 
403     viewMsgHeaders = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_headers"), this, SLOT(slotViewMsgHeaders()), this);
404     msgListWidget->tree->addAction(viewMsgHeaders);
405 
406     //: "mailbox" as a "folder of messages", not as a "mail account"
407     createChildMailbox = new QAction(tr("Create &Child Mailbox..."), this);
408     connect(createChildMailbox, &QAction::triggered, this, &MainWindow::slotCreateMailboxBelowCurrent);
409 
410     //: "mailbox" as a "folder of messages", not as a "mail account"
411     createTopMailbox = new QAction(tr("Create &New Mailbox..."), this);
412     connect(createTopMailbox, &QAction::triggered, this, &MainWindow::slotCreateTopMailbox);
413 
414     m_actionMarkMailboxAsRead = new QAction(tr("&Mark Mailbox as Read"), this);
415     connect(m_actionMarkMailboxAsRead, &QAction::triggered, this, &MainWindow::slotMarkCurrentMailboxRead);
416 
417     //: "mailbox" as a "folder of messages", not as a "mail account"
418     deleteCurrentMailbox = new QAction(tr("&Remove Mailbox"), this);
419     connect(deleteCurrentMailbox, &QAction::triggered, this, &MainWindow::slotDeleteCurrentMailbox);
420 
421 #ifdef XTUPLE_CONNECT
422     xtIncludeMailboxInSync = new QAction(tr("&Synchronize with xTuple"), this);
423     xtIncludeMailboxInSync->setCheckable(true);
424     connect(xtIncludeMailboxInSync, SIGNAL(triggered()), this, SLOT(slotXtSyncCurrentMailbox()));
425 #endif
426 
427     m_replyPrivate = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_private"), this, SLOT(slotReplyTo()), this);
428     m_replyPrivate->setEnabled(false);
429 
430     m_replyAllButMe = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all_but_me"), this, SLOT(slotReplyAllButMe()), this);
431     m_replyAllButMe->setEnabled(false);
432 
433     m_replyAll = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all"), this, SLOT(slotReplyAll()), this);
434     m_replyAll->setEnabled(false);
435 
436     m_replyList = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_list"), this, SLOT(slotReplyList()), this);
437     m_replyList->setEnabled(false);
438 
439     m_replyGuess = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_guess"), this, SLOT(slotReplyGuess()), this);
440     m_replyGuess->setEnabled(true);
441 
442     actionThreadMsgList = new QAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Show Messages in &Threads"), this);
443     actionThreadMsgList->setCheckable(true);
444     // This action is enabled/disabled by model's capabilities
445     actionThreadMsgList->setEnabled(false);
446     if (m_settings->value(Common::SettingsNames::guiMsgListShowThreading).toBool()) {
447         actionThreadMsgList->setChecked(true);
448         // The actual threading will be performed only when model updates its capabilities
449     }
450     connect(actionThreadMsgList, &QAction::triggered, this, &MainWindow::slotThreadMsgList);
451 
452     QActionGroup *sortOrderGroup = new QActionGroup(this);
453     m_actionSortAscending = new QAction(tr("&Ascending"), sortOrderGroup);
454     m_actionSortAscending->setCheckable(true);
455     m_actionSortAscending->setChecked(true);
456     m_actionSortDescending = new QAction(tr("&Descending"), sortOrderGroup);
457     m_actionSortDescending->setCheckable(true);
458     // QActionGroup has no toggle signal, but connecting descending will implicitly catch the acscending complement ;-)
459     connect(m_actionSortDescending, &QAction::toggled, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
460     connect(m_actionSortDescending, &QAction::toggled, this, &MainWindow::slotScrollToCurrent);
461     connect(sortOrderGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged);
462 
463     QActionGroup *sortColumnGroup = new QActionGroup(this);
464     m_actionSortNone = new QAction(tr("&No sorting"), sortColumnGroup);
465     m_actionSortNone->setCheckable(true);
466     m_actionSortThreading = new QAction(tr("Sorted by &Threading"), sortColumnGroup);
467     m_actionSortThreading->setCheckable(true);
468     m_actionSortByArrival = new QAction(tr("A&rrival"), sortColumnGroup);
469     m_actionSortByArrival->setCheckable(true);
470     m_actionSortByCc = new QAction(tr("&Cc (Carbon Copy)"), sortColumnGroup);
471     m_actionSortByCc->setCheckable(true);
472     m_actionSortByDate = new QAction(tr("Date from &Message Headers"), sortColumnGroup);
473     m_actionSortByDate->setCheckable(true);
474     m_actionSortByFrom = new QAction(tr("&From Address"), sortColumnGroup);
475     m_actionSortByFrom->setCheckable(true);
476     m_actionSortBySize = new QAction(tr("&Size"), sortColumnGroup);
477     m_actionSortBySize->setCheckable(true);
478     m_actionSortBySubject = new QAction(tr("S&ubject"), sortColumnGroup);
479     m_actionSortBySubject->setCheckable(true);
480     m_actionSortByTo = new QAction(tr("T&o Address"), sortColumnGroup);
481     m_actionSortByTo->setCheckable(true);
482     connect(sortColumnGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged);
483     slotSortingConfirmed(-1, Qt::AscendingOrder);
484 
485     actionHideRead = new QAction(tr("&Hide Read Messages"), this);
486     actionHideRead->setCheckable(true);
487     if (m_settings->value(Common::SettingsNames::guiMsgListHideRead).toBool()) {
488         actionHideRead->setChecked(true);
489         prettyMsgListModel->setHideRead(true);
490     }
491     connect(actionHideRead, &QAction::triggered, this, &MainWindow::slotHideRead);
492 
493     QActionGroup *layoutGroup = new QActionGroup(this);
494     m_actionLayoutCompact = new QAction(tr("&Compact"), layoutGroup);
495     m_actionLayoutCompact->setCheckable(true);
496     m_actionLayoutCompact->setChecked(true);
497     connect(m_actionLayoutCompact, &QAction::triggered, this, &MainWindow::slotLayoutCompact);
498     m_actionLayoutWide = new QAction(tr("&Wide"), layoutGroup);
499     m_actionLayoutWide->setCheckable(true);
500     connect(m_actionLayoutWide, &QAction::triggered, this, &MainWindow::slotLayoutWide);
501     m_actionLayoutOneAtTime = new QAction(tr("&One At Time"), layoutGroup);
502     m_actionLayoutOneAtTime->setCheckable(true);
503     connect(m_actionLayoutOneAtTime, &QAction::triggered, this, &MainWindow::slotLayoutOneAtTime);
504 
505 
506     m_actionShowOnlySubscribed = new QAction(tr("Show Only S&ubscribed Folders"), this);
507     m_actionShowOnlySubscribed->setCheckable(true);
508     m_actionShowOnlySubscribed->setEnabled(false);
509     connect(m_actionShowOnlySubscribed, &QAction::toggled, this, &MainWindow::slotShowOnlySubscribed);
510     m_actionSubscribeMailbox = new QAction(tr("Su&bscribed"), this);
511     m_actionSubscribeMailbox->setCheckable(true);
512     m_actionSubscribeMailbox->setEnabled(false);
513     connect(m_actionSubscribeMailbox, &QAction::triggered, this, &MainWindow::slotSubscribeCurrentMailbox);
514 
515     aboutTrojita = new QAction(trUtf8("&About Trojitá..."), this);
516     connect(aboutTrojita, &QAction::triggered, this, &MainWindow::slotShowAboutTrojita);
517 
518     donateToTrojita = new QAction(tr("&Donate to the project"), this);
519     connect(donateToTrojita, &QAction::triggered, this, &MainWindow::slotDonateToTrojita);
520 
521     connectModelActions();
522 
523     m_composeMenu = new QMenu(tr("Compose Mail"), this);
524     m_composeMenu->addAction(composeMail);
525     m_composeMenu->addAction(m_editDraft);
526     m_composeButton = new QToolButton(this);
527     m_composeButton->setPopupMode(QToolButton::MenuButtonPopup);
528     m_composeButton->setMenu(m_composeMenu);
529     m_composeButton->setDefaultAction(composeMail);
530 
531     m_replyButton = new QToolButton(this);
532     m_replyButton->setPopupMode(QToolButton::MenuButtonPopup);
533     m_replyMenu = new QMenu(m_replyButton);
534     m_replyMenu->addAction(m_replyPrivate);
535     m_replyMenu->addAction(m_replyAllButMe);
536     m_replyMenu->addAction(m_replyAll);
537     m_replyMenu->addAction(m_replyList);
538     m_replyButton->setMenu(m_replyMenu);
539     m_replyButton->setDefaultAction(m_replyPrivate);
540 
541     m_mainToolbar->addWidget(m_composeButton);
542     m_mainToolbar->addWidget(m_replyButton);
543     m_mainToolbar->addAction(m_forwardAsAttachment);
544     m_mainToolbar->addAction(expunge);
545     m_mainToolbar->addSeparator();
546     m_mainToolbar->addAction(markAsRead);
547     m_mainToolbar->addAction(markAsDeleted);
548     m_mainToolbar->addAction(markAsFlagged);
549     m_mainToolbar->addAction(markAsJunk);
550     m_mainToolbar->addAction(markAsNotJunk);
551 
552     // Push the status indicators all the way to the other side of the toolbar -- either to the far right, or far bottom.
553     QWidget *toolbarSpacer = new QWidget(m_mainToolbar);
554     toolbarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
555     m_mainToolbar->addWidget(toolbarSpacer);
556 
557     m_mainToolbar->addSeparator();
558     m_mainToolbar->addWidget(busyParsersIndicator);
559 
560     networkIndicator = new QToolButton(this);
561     // This is deliberate; we want to show this button in the same style as the other ones in the toolbar
562     networkIndicator->setPopupMode(QToolButton::MenuButtonPopup);
563     m_mainToolbar->addWidget(networkIndicator);
564 
565     m_menuFromToolBar = new QToolButton(this);
566     m_menuFromToolBar->setIcon(UiUtils::loadIcon(QStringLiteral("menu_new")));
567     m_menuFromToolBar->setText(QChar(0x205d)); // Unicode 'TRICOLON'
568     m_menuFromToolBar->setPopupMode(QToolButton::MenuButtonPopup);
569     connect(m_menuFromToolBar, &QAbstractButton::clicked, m_menuFromToolBar, &QToolButton::showMenu);
570     m_mainToolbar->addWidget(m_menuFromToolBar);
571     connect(showMenuBar, &QAction::toggled, [this](const bool menuBarVisible) {
572         // https://bugreports.qt.io/browse/QTBUG-35768 , we have to work on the QAction, not QToolButton
573         m_mainToolbar->actions().last()->setVisible(!menuBarVisible);
574     });
575     m_mainToolbar->actions().last()->setVisible(false); // initial state to complement the default of the QMenuBar's visibility
576 
577     busyParsersIndicator->setFixedSize(m_mainToolbar->iconSize());
578 
579     {
580         // Custom widgets which are added into a QToolBar are by default aligned to the left, while QActions are justified.
581         // That sucks, because some of our widgets use multiple actions with an expanding arrow at right.
582         // Make sure everything is aligned to the left, so that the actual buttons are aligned properly and the extra arrows
583         // are, well, at right.
584         // I have no idea how this works on RTL layouts.
585         QLayout *lay = m_mainToolbar->layout();
586         for (int i = 0; i < lay->count(); ++i) {
587             QLayoutItem *it = lay->itemAt(i);
588             if (it->widget() == toolbarSpacer) {
589                 // Don't align this one, otherwise it won't push stuff when in horizontal direction
590                 continue;
591             }
592             if (it->widget() == busyParsersIndicator) {
593                 // It looks much better when centered
594                 it->setAlignment(Qt::AlignJustify);
595                 continue;
596             }
597             it->setAlignment(Qt::AlignLeft);
598         }
599     }
600 
601     updateMessageFlags();
602 }
603 
connectModelActions()604 void MainWindow::connectModelActions()
605 {
606     connect(reloadAllMailboxes, &QAction::triggered, imapModel(), &Imap::Mailbox::Model::reloadMailboxList);
607     connect(netOffline, &QAction::triggered,
608             qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOffline);
609     connect(netExpensive, &QAction::triggered,
610             qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkExpensive);
611     connect(netOnline, &QAction::triggered,
612             qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOnline);
613     netExpensive->setEnabled(imapAccess()->isConfigured());
614     netOnline->setEnabled(imapAccess()->isConfigured());
615 }
616 
createMenus()617 void MainWindow::createMenus()
618 {
619 #define ADD_ACTION(MENU, ACTION) \
620     MENU->addAction(ACTION); \
621     addAction(ACTION);
622 
623     QMenu *imapMenu = menuBar()->addMenu(tr("&IMAP"));
624     imapMenu->addMenu(m_composeMenu);
625     ADD_ACTION(imapMenu, m_actionContactEditor);
626     ADD_ACTION(imapMenu, m_replyGuess);
627     ADD_ACTION(imapMenu, m_replyPrivate);
628     ADD_ACTION(imapMenu, m_replyAll);
629     ADD_ACTION(imapMenu, m_replyAllButMe);
630     ADD_ACTION(imapMenu, m_replyList);
631     imapMenu->addSeparator();
632     ADD_ACTION(imapMenu, m_forwardAsAttachment);
633     imapMenu->addSeparator();
634     ADD_ACTION(imapMenu, expunge);
635     imapMenu->addSeparator()->setText(tr("Network Access"));
636     QMenu *netPolicyMenu = imapMenu->addMenu(tr("&Network Access"));
637     ADD_ACTION(netPolicyMenu, netOffline);
638     ADD_ACTION(netPolicyMenu, netExpensive);
639     ADD_ACTION(netPolicyMenu, netOnline);
640     QMenu *debugMenu = imapMenu->addMenu(tr("&Debugging"));
641     ADD_ACTION(debugMenu, showFullView);
642     ADD_ACTION(debugMenu, showTaskView);
643     ADD_ACTION(debugMenu, showMimeView);
644     ADD_ACTION(debugMenu, showImapLogger);
645     ADD_ACTION(debugMenu, logPersistent);
646     ADD_ACTION(debugMenu, showImapCapabilities);
647     imapMenu->addSeparator();
648     ADD_ACTION(imapMenu, configSettings);
649     ADD_ACTION(imapMenu, ShortcutHandler::instance()->shortcutConfigAction());
650     imapMenu->addSeparator();
651     ADD_ACTION(imapMenu, exitAction);
652 
653     QMenu *viewMenu = menuBar()->addMenu(tr("&View"));
654     ADD_ACTION(viewMenu, showMenuBar);
655     ADD_ACTION(viewMenu, showToolBar);
656     QMenu *layoutMenu = viewMenu->addMenu(tr("&Layout"));
657     ADD_ACTION(layoutMenu, m_actionLayoutCompact);
658     ADD_ACTION(layoutMenu, m_actionLayoutWide);
659     ADD_ACTION(layoutMenu, m_actionLayoutOneAtTime);
660     viewMenu->addSeparator();
661     ADD_ACTION(viewMenu, m_previousMessage);
662     ADD_ACTION(viewMenu, m_nextMessage);
663     viewMenu->addSeparator();
664     QMenu *sortMenu = viewMenu->addMenu(tr("S&orting"));
665     ADD_ACTION(sortMenu, m_actionSortNone);
666     ADD_ACTION(sortMenu, m_actionSortThreading);
667     ADD_ACTION(sortMenu, m_actionSortByArrival);
668     ADD_ACTION(sortMenu, m_actionSortByCc);
669     ADD_ACTION(sortMenu, m_actionSortByDate);
670     ADD_ACTION(sortMenu, m_actionSortByFrom);
671     ADD_ACTION(sortMenu, m_actionSortBySize);
672     ADD_ACTION(sortMenu, m_actionSortBySubject);
673     ADD_ACTION(sortMenu, m_actionSortByTo);
674     sortMenu->addSeparator();
675     ADD_ACTION(sortMenu, m_actionSortAscending);
676     ADD_ACTION(sortMenu, m_actionSortDescending);
677 
678     ADD_ACTION(viewMenu, actionThreadMsgList);
679     ADD_ACTION(viewMenu, actionHideRead);
680     ADD_ACTION(viewMenu, m_actionShowOnlySubscribed);
681 
682     QMenu *mailboxMenu = menuBar()->addMenu(tr("&Mailbox"));
683     ADD_ACTION(mailboxMenu, resyncMbox);
684     mailboxMenu->addSeparator();
685     ADD_ACTION(mailboxMenu, reloadAllMailboxes);
686 
687     QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
688     ADD_ACTION(helpMenu, donateToTrojita);
689     helpMenu->addSeparator();
690     ADD_ACTION(helpMenu, aboutTrojita);
691 
692     QMenu *mainMenuBehindToolBar = new QMenu(this);
693     m_menuFromToolBar->setMenu(mainMenuBehindToolBar);
694     m_menuFromToolBar->menu()->addMenu(imapMenu);
695     m_menuFromToolBar->menu()->addMenu(viewMenu);
696     m_menuFromToolBar->menu()->addMenu(mailboxMenu);
697     m_menuFromToolBar->menu()->addMenu(helpMenu);
698     m_menuFromToolBar->menu()->addSeparator();
699     m_menuFromToolBar->menu()->addAction(showMenuBar);
700 
701     networkIndicator->setMenu(netPolicyMenu);
702     m_netToolbarDefaultAction = new QAction(this);
703     networkIndicator->setDefaultAction(m_netToolbarDefaultAction);
704     connect(m_netToolbarDefaultAction, &QAction::triggered, networkIndicator, &QToolButton::showMenu);
705     connect(netOffline, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
706     connect(netExpensive, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
707     connect(netOnline, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
708 
709 #undef ADD_ACTION
710 }
711 
createWidgets()712 void MainWindow::createWidgets()
713 {
714     // The state of the GUI is only saved after a certain time has passed. This is just an optimization to make sure
715     // we do not hit the disk continually when e.g. resizing some random widget.
716     m_delayedStateSaving = new QTimer(this);
717     m_delayedStateSaving->setInterval(1000);
718     m_delayedStateSaving->setSingleShot(true);
719     connect(m_delayedStateSaving, &QTimer::timeout, this, &MainWindow::saveSizesAndState);
720 
721     mboxTree = new MailBoxTreeView();
722     mboxTree->setDesiredExpansion(m_settings->value(Common::SettingsNames::guiExpandedMailboxes).toStringList());
723     connect(mboxTree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMboxTree);
724     connect(mboxTree, &MailBoxTreeView::mailboxExpansionChanged, this, [this](const QStringList &mailboxNames) {
725         m_settings->setValue(Common::SettingsNames::guiExpandedMailboxes, mailboxNames);
726     });
727 
728     msgListWidget = new MessageListWidget();
729     msgListWidget->tree->setContextMenuPolicy(Qt::CustomContextMenu);
730     msgListWidget->tree->setAlternatingRowColors(true);
731     msgListWidget->setRawSearchEnabled(m_settings->value(Common::SettingsNames::guiAllowRawSearch).toBool());
732     connect (msgListWidget, &MessageListWidget::rawSearchSettingChanged, this, &MainWindow::saveRawStateSetting);
733 
734     connect(msgListWidget->tree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMsgListTree);
735     connect(msgListWidget->tree, &QAbstractItemView::activated, this, &MainWindow::msgListClicked);
736     connect(msgListWidget->tree, &QAbstractItemView::clicked, this, &MainWindow::msgListClicked);
737     connect(msgListWidget->tree, &QAbstractItemView::doubleClicked, this, &MainWindow::msgListDoubleClicked);
738     connect(msgListWidget, &MessageListWidget::requestingSearch, this, &MainWindow::slotSearchRequested);
739     connect(msgListWidget->tree->header(), &QHeaderView::sectionMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
740     connect(msgListWidget->tree->header(), &QHeaderView::sectionResized, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
741 
742     msgListWidget->tree->installEventFilter(this);
743 
744     m_messageWidget = new CompleteMessageWidget(this, m_settings, m_pluginManager);
745     connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::scrollMessageUp);
746     connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::slotUpdateMessageActions);
747 #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
748     connect(m_messageWidget->messageView, &MessageView::linkHovered, [](const QString &url) {
749         if (url.isEmpty()) {
750             QToolTip::hideText();
751         } else {
752             // indirection due to https://bugs.kde.org/show_bug.cgi?id=363783
753             QTimer::singleShot(250, [url]() {
754                 QToolTip::showText(QCursor::pos(), QObject::tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(url)));
755             });
756         }
757     });
758 #endif
759     connect(m_messageWidget->messageView, &MessageView::transferError, this, &MainWindow::slotDownloadTransferError);
760     // Do not try to get onto the homepage when we are on EXPENSIVE connection
761     if (m_settings->value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool() &&
762         m_imapAccess->preferredNetworkPolicy() == Imap::Mailbox::NETWORK_ONLINE) {
763         m_messageWidget->messageView->setHomepageUrl(QUrl(QStringLiteral("http://welcome.trojita.flaska.net/%1").arg(Common::Application::version)));
764     }
765 
766     allDock = new QDockWidget(tr("Everything"), this);
767     allDock->setObjectName(QStringLiteral("allDock"));
768     allTree = new QTreeView(allDock);
769     allDock->hide();
770     allTree->setUniformRowHeights(true);
771     allTree->setHeaderHidden(true);
772     allDock->setWidget(allTree);
773     addDockWidget(Qt::LeftDockWidgetArea, allDock);
774     taskDock = new QDockWidget(tr("IMAP Tasks"), this);
775     taskDock->setObjectName(QStringLiteral("taskDock"));
776     taskTree = new QTreeView(taskDock);
777     taskDock->hide();
778     taskTree->setHeaderHidden(true);
779     taskDock->setWidget(taskTree);
780     addDockWidget(Qt::LeftDockWidgetArea, taskDock);
781     mailMimeDock = new QDockWidget(tr("MIME Tree"), this);
782     mailMimeDock->setObjectName(QStringLiteral("mailMimeDock"));
783     mailMimeTree = new QTreeView(mailMimeDock);
784     mailMimeDock->hide();
785     mailMimeTree->setUniformRowHeights(true);
786     mailMimeTree->setHeaderHidden(true);
787     mailMimeDock->setWidget(mailMimeTree);
788     addDockWidget(Qt::RightDockWidgetArea, mailMimeDock);
789     connect(m_messageWidget->messageView, &MessageView::messageModelChanged, this, &MainWindow::slotMessageModelChanged);
790 
791     imapLoggerDock = new QDockWidget(tr("IMAP Protocol"), this);
792     imapLoggerDock->setObjectName(QStringLiteral("imapLoggerDock"));
793     imapLogger = new ProtocolLoggerWidget(imapLoggerDock);
794     imapLoggerDock->hide();
795     imapLoggerDock->setWidget(imapLogger);
796     addDockWidget(Qt::BottomDockWidgetArea, imapLoggerDock);
797 
798     busyParsersIndicator = new TaskProgressIndicator(this);
799 }
800 
setupModels()801 void MainWindow::setupModels()
802 {
803     m_imapAccess->reloadConfiguration();
804     m_imapAccess->doConnect();
805 
806     m_messageWidget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()));
807 
808     //setProperty( "trojita-sqlcache-commit-period", QVariant(5000) );
809     //setProperty( "trojita-sqlcache-commit-delay", QVariant(1000) );
810 
811     auto realThreadingModel = qobject_cast<Imap::Mailbox::ThreadingMsgListModel*>(m_imapAccess->threadingMsgListModel());
812     Q_ASSERT(realThreadingModel);
813     auto realMsgListModel = qobject_cast<Imap::Mailbox::MsgListModel*>(m_imapAccess->msgListModel());
814     Q_ASSERT(realMsgListModel);
815 
816     prettyMboxModel = new Imap::Mailbox::PrettyMailboxModel(this, qobject_cast<QAbstractItemModel *>(m_imapAccess->mailboxModel()));
817     prettyMboxModel->setObjectName(QStringLiteral("prettyMboxModel"));
818     connect(realThreadingModel, &Imap::Mailbox::ThreadingMsgListModel::sortingFailed,
819             msgListWidget, &MessageListWidget::slotSortingFailed);
820     prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this);
821     prettyMsgListModel->setSourceModel(m_imapAccess->threadingMsgListModel());
822     prettyMsgListModel->setObjectName(QStringLiteral("prettyMsgListModel"));
823 
824     connect(mboxTree, &MailBoxTreeView::clicked,
825             realMsgListModel,
826             static_cast<void (Imap::Mailbox::MsgListModel::*)(const QModelIndex &)>(&Imap::Mailbox::MsgListModel::setMailbox));
827     connect(mboxTree, &MailBoxTreeView::activated,
828             realMsgListModel,
829             static_cast<void (Imap::Mailbox::MsgListModel::*)(const QModelIndex &)>(&Imap::Mailbox::MsgListModel::setMailbox));
830     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::dataChanged, this, &MainWindow::updateMessageFlags);
831     connect(qobject_cast<Imap::Mailbox::MsgListModel*>(m_imapAccess->msgListModel()), &Imap::Mailbox::MsgListModel::messagesAvailable,
832             this, &MainWindow::slotScrollToUnseenMessage);
833     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsInserted, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
834     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
835     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateMessageFlags);
836     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
837     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, this, &MainWindow::updateMessageFlags);
838     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
839     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::updateMessageFlags);
840     connect(realMsgListModel, &Imap::Mailbox::MsgListModel::mailboxChanged, this, &MainWindow::slotMailboxChanged);
841 
842     connect(imapModel(), &Imap::Mailbox::Model::alertReceived, this, &MainWindow::alertReceived);
843     connect(imapModel(), &Imap::Mailbox::Model::imapError, this, &MainWindow::imapError);
844     connect(imapModel(), &Imap::Mailbox::Model::networkError, this, &MainWindow::networkError);
845     connect(imapModel(), &Imap::Mailbox::Model::authRequested, this, &MainWindow::authenticationRequested, Qt::QueuedConnection);
846 
847     connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOffline, this, &MainWindow::networkPolicyOffline);
848     connect(imapModel(), &Imap::Mailbox::Model::networkPolicyExpensive, this, &MainWindow::networkPolicyExpensive);
849     connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOnline, this, &MainWindow::networkPolicyOnline);
850     connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, &MainWindow::showConnectionStatus);
851 
852     connect(imapModel(), &Imap::Mailbox::Model::mailboxDeletionFailed, this, &MainWindow::slotMailboxDeleteFailed);
853     connect(imapModel(), &Imap::Mailbox::Model::mailboxCreationFailed, this, &MainWindow::slotMailboxCreateFailed);
854     connect(imapModel(), &Imap::Mailbox::Model::mailboxSyncFailed, this, &MainWindow::slotMailboxSyncFailed);
855 
856     connect(imapModel(), &Imap::Mailbox::Model::logged, imapLogger, &ProtocolLoggerWidget::slotImapLogged);
857     connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, imapLogger, &ProtocolLoggerWidget::onConnectionClosed);
858 
859     auto nw = qobject_cast<Imap::Mailbox::NetworkWatcher *>(m_imapAccess->networkWatcher());
860     Q_ASSERT(nw);
861     connect(nw, &Imap::Mailbox::NetworkWatcher::reconnectAttemptScheduled,
862             this, [this](const int timeout) {
863             showStatusMessage(tr("Attempting to reconnect in %n seconds..", 0, timeout/1000));
864             });
865     connect(nw, &Imap::Mailbox::NetworkWatcher::resetReconnectState, this, &MainWindow::slotResetReconnectState);
866 
867     connect(imapModel(), &Imap::Mailbox::Model::mailboxFirstUnseenMessage, this, &MainWindow::slotScrollToUnseenMessage);
868 
869     connect(imapModel(), &Imap::Mailbox::Model::capabilitiesUpdated, this, &MainWindow::slotCapabilitiesUpdated);
870 
871     connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::slotUpdateWindowTitle);
872     connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::slotUpdateWindowTitle);
873 
874     connect(prettyMsgListModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MainWindow::slotSortingConfirmed);
875 
876     //Imap::Mailbox::ModelWatcher* w = new Imap::Mailbox::ModelWatcher( this );
877     //w->setModel( imapModel() );
878 
879     //ModelTest* tester = new ModelTest( prettyMboxModel, this ); // when testing, test just one model at time
880 
881     mboxTree->setModel(prettyMboxModel);
882     msgListWidget->tree->setModel(prettyMsgListModel);
883     connect(msgListWidget->tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateMessageFlags);
884 
885     allTree->setModel(imapModel());
886     taskTree->setModel(imapModel()->taskModel());
887     connect(imapModel()->taskModel(), &QAbstractItemModel::layoutChanged, taskTree, &QTreeView::expandAll);
888     connect(imapModel()->taskModel(), &QAbstractItemModel::modelReset, taskTree, &QTreeView::expandAll);
889     connect(imapModel()->taskModel(), &QAbstractItemModel::rowsInserted, taskTree, &QTreeView::expandAll);
890     connect(imapModel()->taskModel(), &QAbstractItemModel::rowsRemoved, taskTree, &QTreeView::expandAll);
891     connect(imapModel()->taskModel(), &QAbstractItemModel::rowsMoved, taskTree, &QTreeView::expandAll);
892 
893     busyParsersIndicator->setImapModel(imapModel());
894 }
895 
createSysTray()896 void MainWindow::createSysTray()
897 {
898     if (m_trayIcon)
899         return;
900 
901     qApp->setQuitOnLastWindowClosed(false);
902 
903     m_trayIcon = new QSystemTrayIcon(this);
904     handleTrayIconChange();
905 
906     QAction* quitAction = new QAction(tr("&Quit"), m_trayIcon);
907     connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
908 
909     QMenu *trayIconMenu = new QMenu(this);
910     trayIconMenu->addAction(quitAction);
911     m_trayIcon->setContextMenu(trayIconMenu);
912 
913     // QMenu cannot be a child of QSystemTrayIcon, and we don't want the QMenu in MainWindow scope.
914     connect(m_trayIcon, &QObject::destroyed, trayIconMenu, &QObject::deleteLater);
915 
916     connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::slotIconActivated);
917     connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::handleTrayIconChange);
918     m_trayIcon->setVisible(true);
919     m_trayIcon->show();
920 }
921 
removeSysTray()922 void MainWindow::removeSysTray()
923 {
924     delete m_trayIcon;
925     m_trayIcon = 0;
926 
927     qApp->setQuitOnLastWindowClosed(true);
928 }
929 
slotToggleSysTray()930 void MainWindow::slotToggleSysTray()
931 {
932     bool showSystray = m_settings->value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool();
933     if (showSystray && !m_trayIcon && QSystemTrayIcon::isSystemTrayAvailable()) {
934         createSysTray();
935     } else if (!showSystray && m_trayIcon) {
936         removeSysTray();
937     }
938 }
939 
handleTrayIconChange()940 void MainWindow::handleTrayIconChange()
941 {
942     if (!m_trayIcon)
943         return;
944 
945     QModelIndex mailbox = imapModel()->index(1, 0, QModelIndex());
946 
947     const bool isOffline = qobject_cast<Imap::Mailbox::NetworkWatcher *>(m_imapAccess->networkWatcher())->effectiveNetworkPolicy()
948             == Imap::Mailbox::NETWORK_OFFLINE;
949     auto pixmap = UiUtils::loadIcon(QStringLiteral("trojita"))
950                 .pixmap(QSize(32, 32), isOffline ? QIcon::Disabled : QIcon::Normal);
951     QString tooltip;
952     auto profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
953     if (profileName.isEmpty()) {
954         tooltip = QStringLiteral("Trojitá");
955     } else {
956         tooltip = QStringLiteral("Trojitá [%1]").arg(profileName);
957     }
958 
959     if (mailbox.isValid() && mailbox.data(Imap::Mailbox::RoleMailboxName).toString() == QLatin1String("INBOX")) {
960         if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt() > 0) {
961             QFont f;
962             f.setPixelSize(pixmap.height() * 0.59);
963             f.setWeight(QFont::Bold);
964 
965             QString text = mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toString();
966             QFontMetrics fm(f);
967             if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toUInt() > 666) {
968                 // You just have too many messages.
969                 text = QStringLiteral("��");
970                 fm = QFontMetrics(f);
971             } else if (fm.width(text) > pixmap.width()) {
972                 f.setPixelSize(f.pixelSize() * pixmap.width() / fm.width(text));
973                 fm = QFontMetrics(f);
974             }
975 
976             QRect boundingRect = fm.tightBoundingRect(text);
977             boundingRect.setWidth(boundingRect.width() + 2);
978             boundingRect.setHeight(boundingRect.height() + 2);
979             boundingRect.moveCenter(QPoint(pixmap.width() / 2, pixmap.height() / 2));
980             boundingRect = boundingRect.intersected(pixmap.rect());
981 
982             QPainterPath path;
983             path.addText(boundingRect.bottomLeft(), f, text);
984 
985             QPainter painter(&pixmap);
986             painter.setRenderHint(QPainter::Antialiasing);
987             painter.setPen(QColor(255,255,255, 180));
988             painter.setBrush(isOffline ? Qt::red : Qt::black);
989             painter.drawPath(path);
990 
991             //: This is a tooltip for the tray icon. It will be prefixed by something like "Trojita" or "Trojita [work]"
992             tooltip += trUtf8(" - %n unread message(s)", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt());
993         }
994     } else if (isOffline) {
995         //: A tooltip suffix when offline. The prefix is something like "Trojita" or "Trojita [work]"
996         tooltip += tr(" - offline");
997     }
998     m_trayIcon->setToolTip(tooltip);
999     m_trayIcon->setIcon(QIcon(pixmap));
1000 }
1001 
closeEvent(QCloseEvent * event)1002 void MainWindow::closeEvent(QCloseEvent *event)
1003 {
1004     if (m_trayIcon && m_trayIcon->isVisible()) {
1005         Util::askForSomethingUnlessTold(trUtf8("Trojitá"),
1006                                         tr("The application will continue in systray. This can be disabled within the settings."),
1007                                         Common::SettingsNames::guiOnSystrayClose, QMessageBox::Ok, this, m_settings);
1008         hide();
1009         event->ignore();
1010     }
1011 }
1012 
eventFilter(QObject * o,QEvent * e)1013 bool MainWindow::eventFilter(QObject *o, QEvent *e)
1014 {
1015     if (msgListWidget && o == msgListWidget->tree && m_messageWidget->messageView) {
1016         if (e->type() == QEvent::KeyPress) {
1017             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
1018             if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace) {
1019                 QCoreApplication::sendEvent(m_messageWidget, keyEvent);
1020                 return true;
1021             }
1022             return false;
1023         }
1024         return false;
1025     }
1026     if (msgListWidget && msgListWidget->tree && o == msgListWidget->tree->header()->viewport()) {
1027         // installed if sorting is not really possible.
1028         QWidget *header = static_cast<QWidget*>(o);
1029         QMouseEvent *mouse = static_cast<QMouseEvent*>(e);
1030         if (e->type() == QEvent::MouseButtonPress) {
1031             if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor) {
1032                 m_headerDragStart = mouse->pos();
1033             }
1034             return false;
1035         }
1036         if (e->type() == QEvent::MouseButtonRelease) {
1037             if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor &&
1038                (m_headerDragStart - mouse->pos()).manhattanLength() < QApplication::startDragDistance()) {
1039                     m_actionSortDescending->toggle();
1040                     Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
1041                     msgListWidget->tree->header()->setSortIndicator(-1, order);
1042                     return true; // prevent regular click
1043             }
1044         }
1045     }
1046     return false;
1047 }
1048 
slotIconActivated(const QSystemTrayIcon::ActivationReason reason)1049 void MainWindow::slotIconActivated(const QSystemTrayIcon::ActivationReason reason)
1050 {
1051     if (reason == QSystemTrayIcon::Trigger) {
1052         setVisible(!isVisible());
1053         if (isVisible())
1054             showMainWindow();
1055     }
1056 }
1057 
showMainWindow()1058 void MainWindow::showMainWindow()
1059 {
1060     setVisible(true);
1061     activateWindow();
1062     raise();
1063 }
1064 
msgListClicked(const QModelIndex & index)1065 void MainWindow::msgListClicked(const QModelIndex &index)
1066 {
1067     Q_ASSERT(index.isValid());
1068 
1069     if (qApp->keyboardModifiers() & Qt::ShiftModifier || qApp->keyboardModifiers() & Qt::ControlModifier)
1070         return;
1071 
1072     if (! index.data(Imap::Mailbox::RoleMessageUid).isValid())
1073         return;
1074 
1075     // Because it's quite possible that we have switched into another mailbox, make sure that we're in the "current" one so that
1076     // user will be notified about new arrivals, etc.
1077     QModelIndex translated = Imap::deproxifiedIndex(index);
1078     imapModel()->switchToMailbox(translated.parent().parent());
1079 
1080     if (index.column() == Imap::Mailbox::MsgListModel::SEEN) {
1081         if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool())
1082             return;
1083         Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() ?
1084                                                Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD;
1085         imapModel()->markMessagesRead(QModelIndexList() << translated, flagOp);
1086 
1087         if (translated == m_messageWidget->messageView->currentMessage()) {
1088             m_messageWidget->messageView->stopAutoMarkAsRead();
1089         }
1090     } else if (index.column() == Imap::Mailbox::MsgListModel::FLAGGED) {
1091         if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool())
1092             return;
1093 
1094         Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedFlagged).toBool() ?
1095                                                Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD;
1096         imapModel()->setMessageFlags(QModelIndexList() << translated, Imap::Mailbox::FlagNames::flagged, flagOp);
1097     } else {
1098         if ((m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) || m_layoutMode == LAYOUT_ONE_AT_TIME) {
1099             // isVisible() won't work, the splitter manipulates width, not the visibility state
1100             m_messageWidget->messageView->setMessage(index);
1101         }
1102         msgListWidget->tree->setCurrentIndex(index);
1103     }
1104 }
1105 
msgListDoubleClicked(const QModelIndex & index)1106 void MainWindow::msgListDoubleClicked(const QModelIndex &index)
1107 {
1108     Q_ASSERT(index.isValid());
1109 
1110     if (! index.data(Imap::Mailbox::RoleMessageUid).isValid())
1111         return;
1112 
1113     CompleteMessageWidget *widget = new CompleteMessageWidget(0, m_settings, m_pluginManager);
1114     widget->messageView->setMessage(index);
1115     widget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()));
1116     widget->setFocusPolicy(Qt::StrongFocus);
1117     widget->setWindowTitle(index.data(Imap::Mailbox::RoleMessageSubject).toString());
1118     widget->setAttribute(Qt::WA_DeleteOnClose);
1119     widget->resize(800, 600);
1120     QAction *closeAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_messagewindow_close"), widget, SLOT(close()), widget);
1121     widget->addAction(closeAction);
1122     widget->show();
1123 }
1124 
showContextMenuMboxTree(const QPoint & position)1125 void MainWindow::showContextMenuMboxTree(const QPoint &position)
1126 {
1127     QList<QAction *> actionList;
1128     if (mboxTree->indexAt(position).isValid()) {
1129         actionList.append(createChildMailbox);
1130         actionList.append(deleteCurrentMailbox);
1131         actionList.append(m_actionMarkMailboxAsRead);
1132         actionList.append(resyncMbox);
1133         actionList.append(reloadMboxList);
1134 
1135         actionList.append(m_actionSubscribeMailbox);
1136         m_actionSubscribeMailbox->setChecked(mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool());
1137 
1138 #ifdef XTUPLE_CONNECT
1139         actionList.append(xtIncludeMailboxInSync);
1140         xtIncludeMailboxInSync->setChecked(
1141             m_settings->value(Common::SettingsNames::xtSyncMailboxList).toStringList().contains(
1142                 mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxName).toString()));
1143 #endif
1144     } else {
1145         actionList.append(createTopMailbox);
1146     }
1147     actionList.append(reloadAllMailboxes);
1148     actionList.append(m_actionShowOnlySubscribed);
1149     QMenu::exec(actionList, mboxTree->mapToGlobal(position));
1150 }
1151 
showContextMenuMsgListTree(const QPoint & position)1152 void MainWindow::showContextMenuMsgListTree(const QPoint &position)
1153 {
1154     QList<QAction *> actionList;
1155     QModelIndex index = msgListWidget->tree->indexAt(position);
1156     if (index.isValid()) {
1157         updateMessageFlagsOf(index);
1158         actionList.append(markAsRead);
1159         actionList.append(markAsDeleted);
1160         actionList.append(markAsFlagged);
1161         actionList.append(markAsJunk);
1162         actionList.append(markAsNotJunk);
1163         actionList.append(m_actionMarkMailboxAsRead);
1164         actionList.append(saveWholeMessage);
1165         actionList.append(viewMsgSource);
1166         actionList.append(viewMsgHeaders);
1167     }
1168     if (! actionList.isEmpty())
1169         QMenu::exec(actionList, msgListWidget->tree->mapToGlobal(position));
1170 }
1171 
1172 /** @short Ask for an updated list of mailboxes situated below the selected one
1173 
1174 */
slotReloadMboxList()1175 void MainWindow::slotReloadMboxList()
1176 {
1177     Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) {
1178         Q_ASSERT(item.isValid());
1179         if (item.column() != 0)
1180             continue;
1181         Imap::Mailbox::TreeItemMailbox *mbox = dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
1182                 Imap::Mailbox::Model::realTreeItem(item)
1183                                                );
1184         Q_ASSERT(mbox);
1185         mbox->rescanForChildMailboxes(imapModel());
1186     }
1187 }
1188 
1189 /** @short Request a check for new messages in selected mailbox */
slotResyncMbox()1190 void MainWindow::slotResyncMbox()
1191 {
1192     if (! imapModel()->isNetworkAvailable())
1193         return;
1194 
1195     Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) {
1196         Q_ASSERT(item.isValid());
1197         if (item.column() != 0)
1198             continue;
1199         imapModel()->resyncMailbox(item);
1200     }
1201 }
1202 
alertReceived(const QString & message)1203 void MainWindow::alertReceived(const QString &message)
1204 {
1205     //: "ALERT" is a special warning which we're required to show to the user
1206     QMessageBox::warning(this, tr("IMAP Alert"), message);
1207 }
1208 
imapError(const QString & message)1209 void MainWindow::imapError(const QString &message)
1210 {
1211     QMessageBox::critical(this, tr("IMAP Protocol Error"), message);
1212     // Show the IMAP logger -- maybe some user will take that as a hint that they shall include it in the bug report.
1213     // </joke>
1214     showImapLogger->setChecked(true);
1215 }
1216 
networkError(const QString & message)1217 void MainWindow::networkError(const QString &message)
1218 {
1219     const QString title = tr("Network Error");
1220     if (!m_networkErrorMessageBox) {
1221         m_networkErrorMessageBox = new QMessageBox(QMessageBox::Critical, title,
1222                                                    QString(), QMessageBox::Ok, this);
1223     }
1224     // User must be informed about a new (but not recurring) error
1225     if (message != m_networkErrorMessageBox->text()) {
1226         m_networkErrorMessageBox->setText(message);
1227         if (qApp->applicationState() == Qt::ApplicationActive) {
1228             m_networkErrorMessageBox->setProperty(netErrorUnseen, false);
1229             m_networkErrorMessageBox->show();
1230         } else {
1231             m_networkErrorMessageBox->setProperty(netErrorUnseen, true);
1232             if (m_trayIcon && m_trayIcon->isVisible())
1233                 m_trayIcon->showMessage(title, message, QSystemTrayIcon::Warning, 3333);
1234         }
1235     }
1236 }
1237 
cacheError(const QString & message)1238 void MainWindow::cacheError(const QString &message)
1239 {
1240     QMessageBox::critical(this, tr("IMAP Cache Error"),
1241                           tr("The caching subsystem managing a cache of the data already "
1242                              "downloaded from the IMAP server is having troubles. "
1243                              "All caching will be disabled.\n\n%1").arg(message));
1244 }
1245 
networkPolicyOffline()1246 void MainWindow::networkPolicyOffline()
1247 {
1248     netExpensive->setChecked(false);
1249     netOnline->setChecked(false);
1250     netOffline->setChecked(true);
1251     updateActionsOnlineOffline(false);
1252     showStatusMessage(tr("Offline"));
1253     handleTrayIconChange();
1254 }
1255 
networkPolicyExpensive()1256 void MainWindow::networkPolicyExpensive()
1257 {
1258     netOffline->setChecked(false);
1259     netOnline->setChecked(false);
1260     netExpensive->setChecked(true);
1261     updateActionsOnlineOffline(true);
1262     handleTrayIconChange();
1263 }
1264 
networkPolicyOnline()1265 void MainWindow::networkPolicyOnline()
1266 {
1267     netOffline->setChecked(false);
1268     netExpensive->setChecked(false);
1269     netOnline->setChecked(true);
1270     updateActionsOnlineOffline(true);
1271     handleTrayIconChange();
1272 }
1273 
1274 /** @short Deletes a network error message box instance upon resetting of reconnect state */
slotResetReconnectState()1275 void MainWindow::slotResetReconnectState()
1276 {
1277     if (m_networkErrorMessageBox) {
1278         delete m_networkErrorMessageBox;
1279         m_networkErrorMessageBox = 0;
1280     }
1281 }
1282 
slotShowSettings()1283 void MainWindow::slotShowSettings()
1284 {
1285     SettingsDialog *dialog = new SettingsDialog(this, m_senderIdentities, m_settings);
1286     if (dialog->exec() == QDialog::Accepted) {
1287         // FIXME: wipe cache in case we're moving between servers
1288         nukeModels();
1289         setupModels();
1290         connectModelActions();
1291         // The systray is still connected to the old model -- got to make sure it's getting updated
1292         removeSysTray();
1293         slotToggleSysTray();
1294     }
1295     QString method = m_settings->value(Common::SettingsNames::imapMethodKey).toString();
1296     if (method != Common::SettingsNames::methodTCP && method != Common::SettingsNames::methodSSL &&
1297             method != Common::SettingsNames::methodProcess ) {
1298         QMessageBox::critical(this, tr("No Configuration"),
1299                               trUtf8("No IMAP account is configured. Trojitá cannot do much without one."));
1300     }
1301     applySizesAndState();
1302 }
1303 
authenticationRequested()1304 void MainWindow::authenticationRequested()
1305 {
1306     Plugins::PasswordPlugin *password = pluginManager()->password();
1307     if (password) {
1308         // FIXME: use another account-id at some point in future
1309         //        Currently the accountName will be empty unless Trojita has been
1310         //        called with a profile, and then the profile will be used as the
1311         //        accountName.
1312         QString accountName = m_imapAccess->accountName();
1313         if (accountName.isEmpty())
1314             accountName = QStringLiteral("account-0");
1315         Plugins::PasswordJob *job = password->requestPassword(accountName, QStringLiteral("imap"));
1316         if (job) {
1317             connect(job, &Plugins::PasswordJob::passwordAvailable, this, [this](const QString &password) {
1318                 authenticationContinue(password);
1319             });
1320             connect(job, &Plugins::PasswordJob::error, this, [this](const Plugins::PasswordJob::Error error, const QString &message) {
1321                 if (error == Plugins::PasswordJob::Error::NoSuchPassword) {
1322                     authenticationContinue(QString());
1323                 } else {
1324                     authenticationContinue(QString(), tr("Failed to retrieve password from the store: %1").arg(message));
1325                 }
1326             });
1327             job->setAutoDelete(true);
1328             job->start();
1329             return;
1330         }
1331     }
1332 
1333     authenticationContinue(QString());
1334 
1335 }
1336 
authenticationContinue(const QString & password,const QString & errorMessage)1337 void MainWindow::authenticationContinue(const QString &password, const QString &errorMessage)
1338 {
1339     const QString &user = m_settings->value(Common::SettingsNames::imapUserKey).toString();
1340     QString pass = password;
1341     if (m_ignoreStoredPassword || pass.isEmpty()) {
1342         auto dialog = PasswordDialog::getPassword(this, tr("Authentication Required"),
1343                                                   tr("<p>Please provide IMAP password for user <b>%1</b> on <b>%2</b>:</p>").arg(
1344                                                       user.toHtmlEscaped(),
1345                                                       m_settings->value(Common::SettingsNames::imapHostKey).toString().toHtmlEscaped()
1346                                                       ),
1347                                                   errorMessage + (errorMessage.isEmpty() ? QString() : QStringLiteral("\n\n"))
1348                                                   + imapModel()->imapAuthError());
1349         connect(dialog, &PasswordDialog::gotPassword, imapModel(), &Imap::Mailbox::Model::setImapPassword);
1350         connect(dialog, &PasswordDialog::rejected, imapModel(), &Imap::Mailbox::Model::unsetImapPassword);
1351     } else {
1352         imapModel()->setImapPassword(pass);
1353     }
1354 }
1355 
checkSslPolicy()1356 void MainWindow::checkSslPolicy()
1357 {
1358     m_imapAccess->setSslPolicy(QMessageBox(static_cast<QMessageBox::Icon>(m_imapAccess->sslInfoIcon()),
1359                                            m_imapAccess->sslInfoTitle(), m_imapAccess->sslInfoMessage(),
1360                                            QMessageBox::Yes | QMessageBox::No, this).exec() == QMessageBox::Yes);
1361 }
1362 
nukeModels()1363 void MainWindow::nukeModels()
1364 {
1365     m_messageWidget->messageView->setEmpty();
1366     mboxTree->setModel(0);
1367     msgListWidget->tree->setModel(0);
1368     allTree->setModel(0);
1369     taskTree->setModel(0);
1370     delete prettyMsgListModel;
1371     prettyMsgListModel = 0;
1372     delete prettyMboxModel;
1373     prettyMboxModel = 0;
1374 }
1375 
recoverDrafts()1376 void MainWindow::recoverDrafts()
1377 {
1378     QDir draftPath(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/"));
1379     QStringList drafts(draftPath.entryList(QStringList() << QStringLiteral("*.draft")));
1380     Q_FOREACH(const QString &draft, drafts) {
1381         ComposeWidget *w = ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, draftPath.filePath(draft)), this);
1382         // No need to further try creating widgets for drafts if a nullptr is being returned by ComposeWidget::warnIfMsaNotConfigured
1383         if (!w)
1384             break;
1385     }
1386 }
1387 
slotComposeMail()1388 void MainWindow::slotComposeMail()
1389 {
1390     ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createBlank(this), this);
1391 }
1392 
slotEditDraft()1393 void MainWindow::slotEditDraft()
1394 {
1395     QString path(Common::writablePath(Common::LOCATION_DATA) + tr("Drafts"));
1396     QDir().mkpath(path);
1397     path = QFileDialog::getOpenFileName(this, tr("Edit draft"), path, tr("Drafts") + QLatin1String(" (*.draft)"));
1398     if (!path.isNull()) {
1399         ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, path), this);
1400     }
1401 }
1402 
translatedSelection() const1403 QModelIndexList MainWindow::translatedSelection() const
1404 {
1405     QModelIndexList translatedIndexes;
1406     Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
1407         Q_ASSERT(item.isValid());
1408         if (item.column() != 0)
1409             continue;
1410         if (!item.data(Imap::Mailbox::RoleMessageUid).isValid())
1411             continue;
1412         translatedIndexes << Imap::deproxifiedIndex(item);
1413     }
1414     return translatedIndexes;
1415 }
1416 
handleMarkAsRead(bool value)1417 void MainWindow::handleMarkAsRead(bool value)
1418 {
1419     const QModelIndexList translatedIndexes = translatedSelection();
1420     if (translatedIndexes.isEmpty()) {
1421         qDebug() << "Model::handleMarkAsRead: no valid messages";
1422     } else {
1423         imapModel()->markMessagesRead(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1424         if (translatedIndexes.contains(m_messageWidget->messageView->currentMessage())) {
1425             m_messageWidget->messageView->stopAutoMarkAsRead();
1426         }
1427     }
1428 }
1429 
slotNextUnread()1430 void MainWindow::slotNextUnread()
1431 {
1432     QModelIndex current = msgListWidget->tree->currentIndex();
1433 
1434     bool wrapped = false;
1435     while (current.isValid()) {
1436         if (!current.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() && msgListWidget->tree->currentIndex() != current) {
1437             m_messageWidget->messageView->setMessage(current);
1438             msgListWidget->tree->setCurrentIndex(current);
1439             return;
1440         }
1441 
1442         QModelIndex child = current.child(0, 0);
1443         if (child.isValid()) {
1444             current = child;
1445             continue;
1446         }
1447 
1448         QModelIndex sibling = current.sibling(current.row() + 1, 0);
1449         if (sibling.isValid()) {
1450             current = sibling;
1451             continue;
1452         }
1453 
1454         while (current.isValid() && msgListWidget->tree->model()->rowCount(current.parent()) - 1 == current.row()) {
1455             current = current.parent();
1456         }
1457         current = current.sibling(current.row() + 1, 0);
1458 
1459         if (!current.isValid() && !wrapped) {
1460             wrapped = true;
1461             current = msgListWidget->tree->model()->index(0, 0);
1462         }
1463     }
1464 }
1465 
slotPreviousUnread()1466 void MainWindow::slotPreviousUnread()
1467 {
1468     QModelIndex current = msgListWidget->tree->currentIndex();
1469 
1470     bool wrapped = false;
1471     while (current.isValid()) {
1472         if (!current.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() && msgListWidget->tree->currentIndex() != current) {
1473             m_messageWidget->messageView->setMessage(current);
1474             msgListWidget->tree->setCurrentIndex(current);
1475             return;
1476         }
1477 
1478         QModelIndex candidate = current.sibling(current.row() - 1, 0);
1479         while (candidate.isValid() && current.model()->hasChildren(candidate)) {
1480             candidate = candidate.child(current.model()->rowCount(candidate) - 1, 0);
1481             Q_ASSERT(candidate.isValid());
1482         }
1483 
1484         if (candidate.isValid()) {
1485             current = candidate;
1486         } else {
1487             current = current.parent();
1488         }
1489         if (!current.isValid() && !wrapped) {
1490             wrapped = true;
1491             while (msgListWidget->tree->model()->hasChildren(current)) {
1492                 current = msgListWidget->tree->model()->index(msgListWidget->tree->model()->rowCount(current) - 1, 0, current);
1493             }
1494         }
1495     }
1496 }
1497 
handleMarkAsDeleted(bool value)1498 void MainWindow::handleMarkAsDeleted(bool value)
1499 {
1500     const QModelIndexList translatedIndexes = translatedSelection();
1501     if (translatedIndexes.isEmpty()) {
1502         qDebug() << "Model::handleMarkAsDeleted: no valid messages";
1503     } else {
1504         imapModel()->markMessagesDeleted(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1505     }
1506 }
1507 
handleMarkAsFlagged(const bool value)1508 void MainWindow::handleMarkAsFlagged(const bool value)
1509 {
1510     const QModelIndexList translatedIndexes = translatedSelection();
1511     if (translatedIndexes.isEmpty()) {
1512         qDebug() << "Model::handleMarkAsFlagged: no valid messages";
1513     } else {
1514         imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::flagged, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1515     }
1516 }
1517 
handleMarkAsJunk(const bool value)1518 void MainWindow::handleMarkAsJunk(const bool value)
1519 {
1520     const QModelIndexList translatedIndexes = translatedSelection();
1521     if (translatedIndexes.isEmpty()) {
1522         qDebug() << "Model::handleMarkAsJunk: no valid messages";
1523     } else {
1524         if (value) {
1525             imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, Imap::Mailbox::FLAG_REMOVE);
1526         }
1527         imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1528     }
1529 }
1530 
handleMarkAsNotJunk(const bool value)1531 void MainWindow::handleMarkAsNotJunk(const bool value)
1532 {
1533     const QModelIndexList translatedIndexes = translatedSelection();
1534     if (translatedIndexes.isEmpty()) {
1535         qDebug() << "Model::handleMarkAsNotJunk: no valid messages";
1536     } else {
1537         if (value) {
1538           imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, Imap::Mailbox::FLAG_REMOVE);
1539         }
1540         imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1541     }
1542 }
1543 
1544 
slotExpunge()1545 void MainWindow::slotExpunge()
1546 {
1547     imapModel()->expungeMailbox(qobject_cast<Imap::Mailbox::MsgListModel *>(m_imapAccess->msgListModel())->currentMailbox());
1548 }
1549 
slotMarkCurrentMailboxRead()1550 void MainWindow::slotMarkCurrentMailboxRead()
1551 {
1552     imapModel()->markMailboxAsRead(mboxTree->currentIndex());
1553 }
1554 
slotCreateMailboxBelowCurrent()1555 void MainWindow::slotCreateMailboxBelowCurrent()
1556 {
1557     createMailboxBelow(mboxTree->currentIndex());
1558 }
1559 
slotCreateTopMailbox()1560 void MainWindow::slotCreateTopMailbox()
1561 {
1562     createMailboxBelow(QModelIndex());
1563 }
1564 
createMailboxBelow(const QModelIndex & index)1565 void MainWindow::createMailboxBelow(const QModelIndex &index)
1566 {
1567     Imap::Mailbox::TreeItemMailbox *mboxPtr = index.isValid() ?
1568             dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
1569                 Imap::Mailbox::Model::realTreeItem(index)) :
1570             0;
1571 
1572     Ui::CreateMailboxDialog ui;
1573     QDialog *dialog = new QDialog(this);
1574     ui.setupUi(dialog);
1575 
1576     dialog->setWindowTitle(mboxPtr ?
1577                            tr("Create a Subfolder of %1").arg(mboxPtr->mailbox()) :
1578                            tr("Create a Top-level Mailbox"));
1579 
1580     if (dialog->exec() == QDialog::Accepted) {
1581         QStringList parts;
1582         if (mboxPtr)
1583             parts << mboxPtr->mailbox();
1584         parts << ui.mailboxName->text();
1585         if (ui.otherMailboxes->isChecked())
1586             parts << QString();
1587         QString targetName = parts.join(mboxPtr ? mboxPtr->separator() : QString());   // FIXME: top-level separator
1588         imapModel()->createMailbox(targetName,
1589                                    ui.subscribe->isChecked() ?
1590                                        Imap::Mailbox::AutoSubscription::SUBSCRIBE :
1591                                        Imap::Mailbox::AutoSubscription::NO_EXPLICIT_SUBSCRIPTION
1592                                        );
1593     }
1594 }
1595 
slotDeleteCurrentMailbox()1596 void MainWindow::slotDeleteCurrentMailbox()
1597 {
1598     if (! mboxTree->currentIndex().isValid())
1599         return;
1600 
1601     QModelIndex mailbox = Imap::deproxifiedIndex(mboxTree->currentIndex());
1602     Q_ASSERT(mailbox.isValid());
1603     QString name = mailbox.data(Imap::Mailbox::RoleMailboxName).toString();
1604 
1605     if (QMessageBox::question(this, tr("Delete Mailbox"),
1606                               tr("Are you sure to delete mailbox %1?").arg(name),
1607                               QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
1608         imapModel()->deleteMailbox(name);
1609     }
1610 }
1611 
updateMessageFlags()1612 void MainWindow::updateMessageFlags()
1613 {
1614     updateMessageFlagsOf(QModelIndex());
1615 }
1616 
updateMessageFlagsOf(const QModelIndex & index)1617 void MainWindow::updateMessageFlagsOf(const QModelIndex &index)
1618 {
1619     QModelIndexList indexes = index.isValid() ? QModelIndexList() << index : translatedSelection();
1620     const bool isValid = !indexes.isEmpty() &&
1621                          // either we operate on the -already valided- selection or the index must be valid
1622                          (!index.isValid() || index.data(Imap::Mailbox::RoleMessageUid).toUInt() > 0);
1623     const bool okToModify = imapModel()->isNetworkAvailable() && isValid;
1624 
1625     markAsRead->setEnabled(okToModify);
1626     markAsDeleted->setEnabled(okToModify);
1627     markAsFlagged->setEnabled(okToModify);
1628     markAsJunk->setEnabled(okToModify);
1629     markAsNotJunk->setEnabled(okToModify);
1630 
1631     bool isRead    = isValid,
1632          isDeleted = isValid,
1633          isFlagged = isValid,
1634          isJunk    = isValid,
1635          isNotJunk = isValid;
1636     Q_FOREACH (const QModelIndex &i, indexes) {
1637 #define UPDATE_STATE(PROP) \
1638         if (is##PROP && !i.data(Imap::Mailbox::RoleMessageIsMarked##PROP).toBool()) \
1639             is##PROP = false;
1640         UPDATE_STATE(Read)
1641         UPDATE_STATE(Deleted)
1642         UPDATE_STATE(Flagged)
1643         UPDATE_STATE(Junk)
1644         UPDATE_STATE(NotJunk)
1645 #undef UPDATE_STATE
1646     }
1647     markAsRead->setChecked(isRead);
1648     markAsDeleted->setChecked(isDeleted);
1649     markAsFlagged->setChecked(isFlagged);
1650     markAsJunk->setChecked(isJunk && !isNotJunk);
1651     markAsNotJunk->setChecked(isNotJunk && !isJunk);
1652 }
1653 
updateActionsOnlineOffline(bool online)1654 void MainWindow::updateActionsOnlineOffline(bool online)
1655 {
1656     reloadMboxList->setEnabled(online);
1657     resyncMbox->setEnabled(online);
1658     expunge->setEnabled(online);
1659     createChildMailbox->setEnabled(online);
1660     createTopMailbox->setEnabled(online);
1661     deleteCurrentMailbox->setEnabled(online);
1662     m_actionMarkMailboxAsRead->setEnabled(online);
1663     updateMessageFlags();
1664     showImapCapabilities->setEnabled(online);
1665     if (!online) {
1666         m_replyGuess->setEnabled(false);
1667         m_replyPrivate->setEnabled(false);
1668         m_replyAll->setEnabled(false);
1669         m_replyAllButMe->setEnabled(false);
1670         m_replyList->setEnabled(false);
1671         m_forwardAsAttachment->setEnabled(false);
1672     }
1673 }
1674 
slotUpdateMessageActions()1675 void MainWindow::slotUpdateMessageActions()
1676 {
1677     Composer::RecipientList dummy;
1678     m_replyPrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, senderIdentitiesModel(),
1679                                                                   m_messageWidget->messageView->currentMessage(), dummy));
1680     m_replyAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, senderIdentitiesModel(),
1681                                                                    m_messageWidget->messageView->currentMessage(), dummy));
1682     m_replyAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, senderIdentitiesModel(),
1683                                                               m_messageWidget->messageView->currentMessage(), dummy));
1684     m_replyList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, senderIdentitiesModel(),
1685                                                                m_messageWidget->messageView->currentMessage(), dummy));
1686     m_replyGuess->setEnabled(m_replyPrivate->isEnabled() || m_replyAllButMe->isEnabled()
1687                              || m_replyAll->isEnabled() || m_replyList->isEnabled());
1688 
1689     // Check the default reply mode
1690     // I suspect this is not going to work for everybody. Suggestions welcome...
1691     if (m_replyList->isEnabled()) {
1692         m_replyButton->setDefaultAction(m_replyList);
1693     } else if (m_replyAllButMe->isEnabled()) {
1694         m_replyButton->setDefaultAction(m_replyAllButMe);
1695     } else {
1696         m_replyButton->setDefaultAction(m_replyPrivate);
1697     }
1698 
1699     m_forwardAsAttachment->setEnabled(m_messageWidget->messageView->currentMessage().isValid());
1700 }
1701 
scrollMessageUp()1702 void MainWindow::scrollMessageUp()
1703 {
1704     m_messageWidget->area->ensureVisible(0, 0, 0, 0);
1705 }
1706 
slotReplyTo()1707 void MainWindow::slotReplyTo()
1708 {
1709     m_messageWidget->messageView->reply(this, Composer::REPLY_PRIVATE);
1710 }
1711 
slotReplyAll()1712 void MainWindow::slotReplyAll()
1713 {
1714     m_messageWidget->messageView->reply(this, Composer::REPLY_ALL);
1715 }
1716 
slotReplyAllButMe()1717 void MainWindow::slotReplyAllButMe()
1718 {
1719     m_messageWidget->messageView->reply(this, Composer::REPLY_ALL_BUT_ME);
1720 }
1721 
slotReplyList()1722 void MainWindow::slotReplyList()
1723 {
1724     m_messageWidget->messageView->reply(this, Composer::REPLY_LIST);
1725 }
1726 
slotReplyGuess()1727 void MainWindow::slotReplyGuess()
1728 {
1729     if (m_replyButton->defaultAction() == m_replyAllButMe) {
1730         slotReplyAllButMe();
1731     } else if (m_replyButton->defaultAction() == m_replyAll) {
1732         slotReplyAll();
1733     } else if (m_replyButton->defaultAction() == m_replyList) {
1734         slotReplyList();
1735     } else {
1736         slotReplyTo();
1737     }
1738 }
1739 
slotForwardAsAttachment()1740 void MainWindow::slotForwardAsAttachment()
1741 {
1742     m_messageWidget->messageView->forward(this, Composer::ForwardMode::FORWARD_AS_ATTACHMENT);
1743 }
1744 
slotComposeMailUrl(const QUrl & url)1745 void MainWindow::slotComposeMailUrl(const QUrl &url)
1746 {
1747     ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createFromUrl(this, url), this);
1748 }
1749 
slotManageContact(const QUrl & url)1750 void MainWindow::slotManageContact(const QUrl &url)
1751 {
1752     Imap::Message::MailAddress addr;
1753     if (!Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("x-trojita-manage-contact")))
1754         return;
1755 
1756     Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
1757     if (!addressbook)
1758         return;
1759 
1760     addressbook->openContactWindow(addr.mailbox + QLatin1Char('@') + addr.host, addr.name);
1761 }
1762 
invokeContactEditor()1763 void MainWindow::invokeContactEditor()
1764 {
1765     Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
1766     if (!addressbook)
1767         return;
1768 
1769     addressbook->openAddressbookWindow();
1770 }
1771 
1772 /** @short Create an MSAFactory as per the settings */
msaFactory()1773 MSA::MSAFactory *MainWindow::msaFactory()
1774 {
1775     using namespace Common;
1776     QString method = m_settings->value(SettingsNames::msaMethodKey).toString();
1777     MSA::MSAFactory *msaFactory = 0;
1778     if (method == SettingsNames::methodSMTP || method == SettingsNames::methodSSMTP) {
1779         msaFactory = new MSA::SMTPFactory(m_settings->value(SettingsNames::smtpHostKey).toString(),
1780                                           m_settings->value(SettingsNames::smtpPortKey).toInt(),
1781                                           (method == SettingsNames::methodSSMTP),
1782                                           (method == SettingsNames::methodSMTP)
1783                                           && m_settings->value(SettingsNames::smtpStartTlsKey).toBool(),
1784                                           m_settings->value(SettingsNames::smtpAuthKey).toBool(),
1785                                           m_settings->value(SettingsNames::smtpAuthReuseImapCredsKey, false).toBool() ?
1786                                               m_settings->value(SettingsNames::imapUserKey).toString() :
1787                                               m_settings->value(SettingsNames::smtpUserKey).toString());
1788     } else if (method == SettingsNames::methodSENDMAIL) {
1789         QStringList args = m_settings->value(SettingsNames::sendmailKey, SettingsNames::sendmailDefaultCmd).toString().split(QLatin1Char(' '));
1790         if (args.isEmpty()) {
1791             return 0;
1792         }
1793         QString appName = args.takeFirst();
1794         msaFactory = new MSA::SendmailFactory(appName, args);
1795     } else if (method == SettingsNames::methodImapSendmail) {
1796         if (!imapModel()->capabilities().contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"))) {
1797             return 0;
1798         }
1799         msaFactory = new MSA::ImapSubmitFactory(qobject_cast<Imap::Mailbox::Model*>(imapAccess()->imapModel()));
1800     } else {
1801         return 0;
1802     }
1803     return msaFactory;
1804 }
1805 
slotMailboxDeleteFailed(const QString & mailbox,const QString & msg)1806 void MainWindow::slotMailboxDeleteFailed(const QString &mailbox, const QString &msg)
1807 {
1808     QMessageBox::warning(this, tr("Can't delete mailbox"),
1809                          tr("Deleting mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
1810 }
1811 
slotMailboxCreateFailed(const QString & mailbox,const QString & msg)1812 void MainWindow::slotMailboxCreateFailed(const QString &mailbox, const QString &msg)
1813 {
1814     QMessageBox::warning(this, tr("Can't create mailbox"),
1815                          tr("Creating mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
1816 }
1817 
slotMailboxSyncFailed(const QString & mailbox,const QString & msg)1818 void MainWindow::slotMailboxSyncFailed(const QString &mailbox, const QString &msg)
1819 {
1820     QMessageBox::warning(this, tr("Can't open mailbox"),
1821                          tr("Opening mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
1822 }
1823 
slotMailboxChanged(const QModelIndex & mailbox)1824 void MainWindow::slotMailboxChanged(const QModelIndex &mailbox)
1825 {
1826     using namespace Imap::Mailbox;
1827     QString mailboxName = mailbox.data(RoleMailboxName).toString();
1828     bool isSentMailbox = mailbox.isValid() && !mailboxName.isEmpty() &&
1829             m_settings->value(Common::SettingsNames::composerSaveToImapKey).toBool() &&
1830             mailboxName == m_settings->value(Common::SettingsNames::composerImapSentKey).toString();
1831     QTreeView *tree = msgListWidget->tree;
1832 
1833     // Automatically trigger visibility of the TO and FROM columns
1834     if (isSentMailbox) {
1835         if (tree->isColumnHidden(MsgListModel::TO) && !tree->isColumnHidden(MsgListModel::FROM)) {
1836             tree->hideColumn(MsgListModel::FROM);
1837             tree->showColumn(MsgListModel::TO);
1838         }
1839     } else {
1840         if (tree->isColumnHidden(MsgListModel::FROM) && !tree->isColumnHidden(MsgListModel::TO)) {
1841             tree->hideColumn(MsgListModel::TO);
1842             tree->showColumn(MsgListModel::FROM);
1843         }
1844     }
1845 
1846     updateMessageFlags();
1847     slotScrollToUnseenMessage();
1848 }
1849 
showConnectionStatus(uint parserId,Imap::ConnectionState state)1850 void MainWindow::showConnectionStatus(uint parserId, Imap::ConnectionState state)
1851 {
1852     Q_UNUSED(parserId);
1853     static Imap::ConnectionState previousState = Imap::ConnectionState::CONN_STATE_NONE;
1854     QString message = connectionStateToString(state);
1855 
1856     if (state == Imap::ConnectionState::CONN_STATE_SELECTED && previousState >= Imap::ConnectionState::CONN_STATE_SELECTED) {
1857         // prevent excessive popups when we "reset the state" to something which is shown quite often
1858         showStatusMessage(QString());
1859     } else {
1860         showStatusMessage(message);
1861     }
1862     previousState = state;
1863 }
1864 
slotShowLinkTarget(const QString & link)1865 void MainWindow::slotShowLinkTarget(const QString &link)
1866 {
1867     if (link.isEmpty()) {
1868         QToolTip::hideText();
1869     } else {
1870         QToolTip::showText(QCursor::pos(), tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(link)));
1871     }
1872 }
1873 
slotShowAboutTrojita()1874 void MainWindow::slotShowAboutTrojita()
1875 {
1876     Ui::AboutDialog ui;
1877     QDialog *widget = new QDialog(this);
1878     widget->setAttribute(Qt::WA_DeleteOnClose);
1879     ui.setupUi(widget);
1880     ui.versionLabel->setText(Common::Application::version);
1881     QStringList copyright;
1882     {
1883         // Find the names of the authors and remove date codes from there
1884         QFile license(QStringLiteral(":/LICENSE"));
1885         license.open(QFile::ReadOnly);
1886         const QString prefix(QStringLiteral("Copyright (C) "));
1887         Q_FOREACH(const QString &line, QString::fromUtf8(license.readAll()).split(QLatin1Char('\n'))) {
1888             if (line.startsWith(prefix)) {
1889                 const int pos = prefix.size();
1890                 copyright << QChar(0xa9 /* COPYRIGHT SIGN */) + QLatin1Char(' ') +
1891                              line.mid(pos).replace(QRegExp(QLatin1String("(\\d) - (\\d)")),
1892                                                    QLatin1String("\\1") + QChar(0x2014 /* EM DASH */) + QLatin1String("\\2"));
1893             }
1894         }
1895     }
1896     ui.credits->setTextFormat(Qt::PlainText);
1897     ui.credits->setText(copyright.join(QStringLiteral("\n")));
1898     widget->show();
1899 }
1900 
slotDonateToTrojita()1901 void MainWindow::slotDonateToTrojita()
1902 {
1903     QDesktopServices::openUrl(QStringLiteral("https://sourceforge.net/p/trojita/donate/"));
1904 }
1905 
slotSaveCurrentMessageBody()1906 void MainWindow::slotSaveCurrentMessageBody()
1907 {
1908     Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
1909         Q_ASSERT(item.isValid());
1910         if (item.column() != 0)
1911             continue;
1912         if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
1913             continue;
1914 
1915         QModelIndex messageIndex = Imap::deproxifiedIndex(item);
1916 
1917         Imap::Network::MsgPartNetAccessManager *netAccess = new Imap::Network::MsgPartNetAccessManager(this);
1918         netAccess->setModelMessage(messageIndex);
1919         Imap::Network::FileDownloadManager *fileDownloadManager =
1920             new Imap::Network::FileDownloadManager(this, netAccess, messageIndex);
1921         connect(fileDownloadManager, &Imap::Network::FileDownloadManager::succeeded, fileDownloadManager, &QObject::deleteLater);
1922         connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, fileDownloadManager, &QObject::deleteLater);
1923         connect(fileDownloadManager, &Imap::Network::FileDownloadManager::fileNameRequested,
1924                 this, &MainWindow::slotDownloadMessageFileNameRequested);
1925         connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError,
1926                 this, &MainWindow::slotDownloadTransferError);
1927         connect(fileDownloadManager, &QObject::destroyed, netAccess, &QObject::deleteLater);
1928         fileDownloadManager->downloadMessage();
1929     }
1930 }
1931 
slotDownloadTransferError(const QString & errorString)1932 void MainWindow::slotDownloadTransferError(const QString &errorString)
1933 {
1934     QMessageBox::critical(this, tr("Can't save into file"),
1935                           tr("Unable to save into file. Error:\n%1").arg(errorString));
1936 }
1937 
slotDownloadMessageFileNameRequested(QString * fileName)1938 void MainWindow::slotDownloadMessageFileNameRequested(QString *fileName)
1939 {
1940     *fileName = QFileDialog::getSaveFileName(this, tr("Save Message"), *fileName, QString(), 0,
1941                                              QFileDialog::HideNameFilterDetails);
1942 }
1943 
slotViewMsgSource()1944 void MainWindow::slotViewMsgSource()
1945 {
1946     Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
1947         Q_ASSERT(item.isValid());
1948         if (item.column() != 0)
1949             continue;
1950         if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
1951             continue;
1952         auto w = messageSourceWidget(item);
1953         //: Translators: %1 is the UID of a message (a number) and %2 is the name of a mailbox.
1954         w->setWindowTitle(tr("Message source of UID %1 in %2").arg(
1955                               QString::number(item.data(Imap::Mailbox::RoleMessageUid).toUInt()),
1956                               Imap::deproxifiedIndex(item).parent().parent().data(Imap::Mailbox::RoleMailboxName).toString()
1957                               ));
1958         w->show();
1959     }
1960 }
1961 
messageSourceWidget(const QModelIndex & message)1962 QWidget *MainWindow::messageSourceWidget(const QModelIndex &message)
1963 {
1964     QModelIndex messageIndex = Imap::deproxifiedIndex(message);
1965     MessageSourceWidget *sourceWidget = new MessageSourceWidget(0, messageIndex);
1966     sourceWidget->setAttribute(Qt::WA_DeleteOnClose);
1967     QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), sourceWidget);
1968     sourceWidget->addAction(close);
1969     close->setShortcut(tr("Ctrl+W"));
1970     connect(close, &QAction::triggered, sourceWidget, &QWidget::close);
1971     return sourceWidget;
1972 }
1973 
slotViewMsgHeaders()1974 void MainWindow::slotViewMsgHeaders()
1975 {
1976     Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
1977         Q_ASSERT(item.isValid());
1978         if (item.column() != 0)
1979             continue;
1980         if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
1981             continue;
1982         QModelIndex messageIndex = Imap::deproxifiedIndex(item);
1983 
1984         auto widget = new MessageHeadersWidget(nullptr, messageIndex);
1985         widget->setAttribute(Qt::WA_DeleteOnClose);
1986         QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), widget);
1987         widget->addAction(close);
1988         close->setShortcut(tr("Ctrl+W"));
1989         connect(close, &QAction::triggered, widget, &QWidget::close);
1990         widget->show();
1991     }
1992 }
1993 
1994 #ifdef XTUPLE_CONNECT
slotXtSyncCurrentMailbox()1995 void MainWindow::slotXtSyncCurrentMailbox()
1996 {
1997     QModelIndex index = mboxTree->currentIndex();
1998     if (! index.isValid())
1999         return;
2000 
2001     QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString();
2002     QSettings s;
2003     QStringList mailboxes = s.value(Common::SettingsNames::xtSyncMailboxList).toStringList();
2004     if (xtIncludeMailboxInSync->isChecked()) {
2005         if (! mailboxes.contains(mailbox)) {
2006             mailboxes.append(mailbox);
2007         }
2008     } else {
2009         mailboxes.removeAll(mailbox);
2010     }
2011     s.setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes);
2012     QSettings(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")).setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes);
2013     prettyMboxModel->xtConnectStatusChanged(index);
2014 }
2015 #endif
2016 
slotSubscribeCurrentMailbox()2017 void MainWindow::slotSubscribeCurrentMailbox()
2018 {
2019     QModelIndex index = mboxTree->currentIndex();
2020     if (! index.isValid())
2021         return;
2022 
2023     QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString();
2024     if (m_actionSubscribeMailbox->isChecked()) {
2025         imapModel()->subscribeMailbox(mailbox);
2026     } else {
2027         imapModel()->unsubscribeMailbox(mailbox);
2028     }
2029 }
2030 
slotShowOnlySubscribed()2031 void MainWindow::slotShowOnlySubscribed()
2032 {
2033     if (m_actionShowOnlySubscribed->isEnabled()) {
2034         m_settings->setValue(Common::SettingsNames::guiMailboxListShowOnlySubscribed, m_actionShowOnlySubscribed->isChecked());
2035         prettyMboxModel->setShowOnlySubscribed(m_actionShowOnlySubscribed->isChecked());
2036     }
2037 }
2038 
slotScrollToUnseenMessage()2039 void MainWindow::slotScrollToUnseenMessage()
2040 {
2041     // Now this is much, much more reliable than messing around with finding out an "interesting message"...
2042     if (!m_actionSortNone->isChecked() && !m_actionSortThreading->isChecked()) {
2043         // we're using some funky sorting, better don't scroll anywhere
2044     }
2045     if (m_actionSortDescending->isChecked()) {
2046         msgListWidget->tree->scrollToTop();
2047     } else {
2048         msgListWidget->tree->scrollToBottom();
2049     }
2050 }
2051 
slotScrollToCurrent()2052 void MainWindow::slotScrollToCurrent()
2053 {
2054     // TODO: begs for lambda
2055     if (QScrollBar *vs = msgListWidget->tree->verticalScrollBar()) {
2056         vs->setValue(vs->maximum() - vs->value()); // implies vs->minimum() == 0
2057     }
2058 }
2059 
slotThreadMsgList()2060 void MainWindow::slotThreadMsgList()
2061 {
2062     // We want to save user's preferences and not override them with "threading disabled" when the server
2063     // doesn't report them, like in initial greetings. That's why we have to check for isEnabled() here.
2064     const bool useThreading = actionThreadMsgList->isChecked();
2065 
2066     // Switching between threaded/unthreaded view shall reset the sorting criteria. The goal is to make
2067     // sorting rather seldomly used as people shall instead use proper threading.
2068     if (useThreading) {
2069         m_actionSortThreading->setEnabled(true);
2070         if (!m_actionSortThreading->isChecked())
2071             m_actionSortThreading->trigger();
2072         m_actionSortNone->setEnabled(false);
2073     } else {
2074         m_actionSortNone->setEnabled(true);
2075         if (!m_actionSortNone->isChecked())
2076             m_actionSortNone->trigger();
2077         m_actionSortThreading->setEnabled(false);
2078     }
2079 
2080     QPersistentModelIndex currentItem = msgListWidget->tree->currentIndex();
2081 
2082     if (useThreading && actionThreadMsgList->isEnabled()) {
2083         msgListWidget->tree->setRootIsDecorated(true);
2084         qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(true);
2085     } else {
2086         msgListWidget->tree->setRootIsDecorated(false);
2087         qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(false);
2088     }
2089     m_settings->setValue(Common::SettingsNames::guiMsgListShowThreading, QVariant(useThreading));
2090 
2091     if (currentItem.isValid()) {
2092         msgListWidget->tree->scrollTo(currentItem);
2093     } else {
2094         // If we cannot determine the current item, at least scroll to a predictable place. Without this, the view
2095         // would jump to "weird" places, probably due to some heuristics about trying to show "roughly the same"
2096         // objects as what was visible before the reshuffling.
2097         slotScrollToUnseenMessage();
2098     }
2099 }
2100 
slotSortingPreferenceChanged()2101 void MainWindow::slotSortingPreferenceChanged()
2102 {
2103     Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
2104 
2105     using namespace Imap::Mailbox;
2106 
2107     int column = -1;
2108     if (m_actionSortByArrival->isChecked()) {
2109         column = MsgListModel::RECEIVED_DATE;
2110     } else if (m_actionSortByCc->isChecked()) {
2111         column = MsgListModel::CC;
2112     } else if (m_actionSortByDate->isChecked()) {
2113         column = MsgListModel::DATE;
2114     } else if (m_actionSortByFrom->isChecked()) {
2115         column = MsgListModel::FROM;
2116     } else if (m_actionSortBySize->isChecked()) {
2117         column = MsgListModel::SIZE;
2118     } else if (m_actionSortBySubject->isChecked()) {
2119         column = MsgListModel::SUBJECT;
2120     } else if (m_actionSortByTo->isChecked()) {
2121         column = MsgListModel::TO;
2122     } else {
2123         column = -1;
2124     }
2125 
2126     msgListWidget->tree->header()->setSortIndicator(column, order);
2127 }
2128 
slotSortingConfirmed(int column,Qt::SortOrder order)2129 void MainWindow::slotSortingConfirmed(int column, Qt::SortOrder order)
2130 {
2131     // don't do anything during initialization
2132     if (!m_actionSortNone)
2133         return;
2134 
2135     using namespace Imap::Mailbox;
2136     QAction *action;
2137 
2138     switch (column) {
2139     case MsgListModel::SEEN:
2140     case MsgListModel::FLAGGED:
2141     case MsgListModel::ATTACHMENT:
2142     case MsgListModel::COLUMN_COUNT:
2143     case MsgListModel::BCC:
2144     case -1:
2145         if (actionThreadMsgList->isChecked())
2146             action = m_actionSortThreading;
2147         else
2148             action = m_actionSortNone;
2149         break;
2150     case MsgListModel::SUBJECT:
2151         action = m_actionSortBySubject;
2152         break;
2153     case MsgListModel::FROM:
2154         action = m_actionSortByFrom;
2155         break;
2156     case MsgListModel::TO:
2157         action = m_actionSortByTo;
2158         break;
2159     case MsgListModel::CC:
2160         action = m_actionSortByCc;
2161         break;
2162     case MsgListModel::DATE:
2163         action = m_actionSortByDate;
2164         break;
2165     case MsgListModel::RECEIVED_DATE:
2166         action = m_actionSortByArrival;
2167         break;
2168     case MsgListModel::SIZE:
2169         action = m_actionSortBySize;
2170         break;
2171     default:
2172         action = m_actionSortNone;
2173     }
2174 
2175     action->setChecked(true);
2176     if (order == Qt::DescendingOrder)
2177         m_actionSortDescending->setChecked(true);
2178     else
2179         m_actionSortAscending->setChecked(true);
2180 }
2181 
slotSearchRequested(const QStringList & searchConditions)2182 void MainWindow::slotSearchRequested(const QStringList &searchConditions)
2183 {
2184     if (!searchConditions.isEmpty() && actionThreadMsgList->isChecked()) {
2185         // right now, searching and threading doesn't play well together at all
2186         actionThreadMsgList->trigger();
2187     }
2188     Imap::Mailbox::ThreadingMsgListModel * threadingMsgListModel =
2189             qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel());
2190     threadingMsgListModel->setUserSearchingSortingPreference(searchConditions, threadingMsgListModel->currentSortCriterium(),
2191                                                              threadingMsgListModel->currentSortOrder());
2192 }
2193 
slotHideRead()2194 void MainWindow::slotHideRead()
2195 {
2196     const bool hideRead = actionHideRead->isChecked();
2197     prettyMsgListModel->setHideRead(hideRead);
2198     m_settings->setValue(Common::SettingsNames::guiMsgListHideRead, QVariant(hideRead));
2199 }
2200 
slotCapabilitiesUpdated(const QStringList & capabilities)2201 void MainWindow::slotCapabilitiesUpdated(const QStringList &capabilities)
2202 {
2203     msgListWidget->tree->header()->viewport()->removeEventFilter(this);
2204     if (capabilities.contains(QStringLiteral("SORT"))) {
2205         m_actionSortByDate->actionGroup()->setEnabled(true);
2206     } else {
2207         m_actionSortByDate->actionGroup()->setEnabled(false);
2208         msgListWidget->tree->header()->viewport()->installEventFilter(this);
2209     }
2210 
2211     msgListWidget->setFuzzySearchSupported(capabilities.contains(QStringLiteral("SEARCH=FUZZY")));
2212 
2213     m_actionShowOnlySubscribed->setEnabled(capabilities.contains(QStringLiteral("LIST-EXTENDED")));
2214     m_actionShowOnlySubscribed->setChecked(m_actionShowOnlySubscribed->isEnabled() &&
2215                                            m_settings->value(
2216                                                Common::SettingsNames::guiMailboxListShowOnlySubscribed, false).toBool());
2217     m_actionSubscribeMailbox->setEnabled(m_actionShowOnlySubscribed->isEnabled());
2218 
2219     const QStringList supportedCapabilities = Imap::Mailbox::ThreadingMsgListModel::supportedCapabilities();
2220     Q_FOREACH(const QString &capability, capabilities) {
2221         if (supportedCapabilities.contains(capability)) {
2222             actionThreadMsgList->setEnabled(true);
2223             if (actionThreadMsgList->isChecked())
2224                 slotThreadMsgList();
2225             return;
2226         }
2227     }
2228     actionThreadMsgList->setEnabled(false);
2229 }
2230 
slotShowImapInfo()2231 void MainWindow::slotShowImapInfo()
2232 {
2233     QString caps;
2234     Q_FOREACH(const QString &cap, imapModel()->capabilities()) {
2235         caps += tr("<li>%1</li>\n").arg(cap);
2236     }
2237 
2238     QString idString;
2239     if (!imapModel()->serverId().isEmpty() && imapModel()->capabilities().contains(QStringLiteral("ID"))) {
2240         QMap<QByteArray,QByteArray> serverId = imapModel()->serverId();
2241 
2242 #define IMAP_ID_FIELD(Var, Name) bool has_##Var = serverId.contains(Name); \
2243     QString Var = has_##Var ? QString::fromUtf8(serverId[Name]).toHtmlEscaped() : tr("Unknown");
2244         IMAP_ID_FIELD(serverName, "name");
2245         IMAP_ID_FIELD(serverVersion, "version");
2246         IMAP_ID_FIELD(os, "os");
2247         IMAP_ID_FIELD(osVersion, "os-version");
2248         IMAP_ID_FIELD(vendor, "vendor");
2249         IMAP_ID_FIELD(supportUrl, "support-url");
2250         IMAP_ID_FIELD(address, "address");
2251         IMAP_ID_FIELD(date, "date");
2252         IMAP_ID_FIELD(command, "command");
2253         IMAP_ID_FIELD(arguments, "arguments");
2254         IMAP_ID_FIELD(environment, "environment");
2255 #undef IMAP_ID_FIELD
2256         if (has_serverName) {
2257             idString = tr("<p>");
2258             if (has_serverVersion)
2259                 idString += tr("Server: %1 %2").arg(serverName, serverVersion);
2260             else
2261                 idString += tr("Server: %1").arg(serverName);
2262 
2263             if (has_vendor) {
2264                 idString += tr(" (%1)").arg(vendor);
2265             }
2266             if (has_os) {
2267                 if (has_osVersion)
2268                     idString += tr(" on %1 %2", "%1 is the operating system of an IMAP server and %2 is its version.").arg(os, osVersion);
2269                 else
2270                     idString += tr(" on %1", "%1 is the operationg system of an IMAP server.").arg(os);
2271             }
2272             idString += tr("</p>");
2273         } else {
2274             idString = tr("<p>The IMAP server did not return usable information about itself.</p>");
2275         }
2276         QString fullId;
2277         for (QMap<QByteArray,QByteArray>::const_iterator it = serverId.constBegin(); it != serverId.constEnd(); ++it) {
2278             fullId += tr("<li>%1: %2</li>").arg(QString::fromUtf8(it.key()).toHtmlEscaped(), QString::fromUtf8(it.value()).toHtmlEscaped());
2279         }
2280         idString += tr("<ul>%1</ul>").arg(fullId);
2281     } else {
2282         idString = tr("<p>The server has not provided information about its software version.</p>");
2283     }
2284 
2285     QMessageBox::information(this, tr("IMAP Server Information"),
2286                              tr("%1"
2287                                 "<p>The following capabilities are currently advertised:</p>\n"
2288                                 "<ul>\n%2</ul>").arg(idString, caps));
2289 }
2290 
sizeHint() const2291 QSize MainWindow::sizeHint() const
2292 {
2293     return QSize(1150, 980);
2294 }
2295 
slotUpdateWindowTitle()2296 void MainWindow::slotUpdateWindowTitle()
2297 {
2298     QModelIndex mailbox = qobject_cast<Imap::Mailbox::MsgListModel *>(m_imapAccess->msgListModel())->currentMailbox();
2299     QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
2300     if (!profileName.isEmpty())
2301         profileName = QLatin1String(" [") + profileName + QLatin1Char(']');
2302     if (mailbox.isValid()) {
2303         if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) {
2304             setWindowTitle(trUtf8("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt())
2305                            .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName);
2306         } else {
2307             setWindowTitle(trUtf8("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName);
2308         }
2309     } else {
2310         setWindowTitle(trUtf8("Trojitá") + profileName);
2311     }
2312 }
2313 
slotLayoutCompact()2314 void MainWindow::slotLayoutCompact()
2315 {
2316     saveSizesAndState();
2317     if (!m_mainHSplitter) {
2318         m_mainHSplitter = new QSplitter();
2319         connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2320         connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2321     }
2322     if (!m_mainVSplitter) {
2323         m_mainVSplitter = new QSplitter();
2324         m_mainVSplitter->setOrientation(Qt::Vertical);
2325         connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2326         connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2327     }
2328 
2329     m_mainVSplitter->addWidget(msgListWidget);
2330     m_mainVSplitter->addWidget(m_messageWidget);
2331     m_mainHSplitter->addWidget(mboxTree);
2332     m_mainHSplitter->addWidget(m_mainVSplitter);
2333 
2334     mboxTree->show();
2335     msgListWidget->show();
2336     m_messageWidget->show();
2337     m_mainVSplitter->show();
2338     m_mainHSplitter->show();
2339 
2340     // The mboxTree shall not expand...
2341     m_mainHSplitter->setStretchFactor(0, 0);
2342     // ...while the msgListTree shall consume all the remaining space
2343     m_mainHSplitter->setStretchFactor(1, 1);
2344 
2345     setCentralWidget(m_mainHSplitter);
2346 
2347     delete m_mainStack;
2348 
2349     m_layoutMode = LAYOUT_COMPACT;
2350     m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact);
2351     applySizesAndState();
2352 }
2353 
slotLayoutWide()2354 void MainWindow::slotLayoutWide()
2355 {
2356     saveSizesAndState();
2357     if (!m_mainHSplitter) {
2358         m_mainHSplitter = new QSplitter();
2359         connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2360         connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2361     }
2362 
2363     m_mainHSplitter->addWidget(mboxTree);
2364     m_mainHSplitter->addWidget(msgListWidget);
2365     m_mainHSplitter->addWidget(m_messageWidget);
2366     msgListWidget->resize(mboxTree->size());
2367     m_messageWidget->resize(mboxTree->size());
2368     m_mainHSplitter->setStretchFactor(0, 0);
2369     m_mainHSplitter->setStretchFactor(1, 1);
2370     m_mainHSplitter->setStretchFactor(2, 1);
2371 
2372     mboxTree->show();
2373     msgListWidget->show();
2374     m_messageWidget->show();
2375     m_mainHSplitter->show();
2376 
2377     setCentralWidget(m_mainHSplitter);
2378 
2379     delete m_mainStack;
2380     delete m_mainVSplitter;
2381 
2382     m_layoutMode = LAYOUT_WIDE;
2383     m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide);
2384     applySizesAndState();
2385 }
2386 
slotLayoutOneAtTime()2387 void MainWindow::slotLayoutOneAtTime()
2388 {
2389     saveSizesAndState();
2390     if (m_mainStack)
2391         return;
2392 
2393     m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack);
2394     setCentralWidget(m_mainStack);
2395 
2396     delete m_mainHSplitter;
2397     delete m_mainVSplitter;
2398 
2399     m_layoutMode = LAYOUT_ONE_AT_TIME;
2400     m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime);
2401     applySizesAndState();
2402 }
2403 
imapModel() const2404 Imap::Mailbox::Model *MainWindow::imapModel() const
2405 {
2406     return qobject_cast<Imap::Mailbox::Model *>(m_imapAccess->imapModel());
2407 }
2408 
desktopGeometryChanged()2409 void MainWindow::desktopGeometryChanged()
2410 {
2411     m_delayedStateSaving->start();
2412 }
2413 
settingsKeyForLayout(const LayoutMode layout)2414 QString MainWindow::settingsKeyForLayout(const LayoutMode layout)
2415 {
2416     switch (layout) {
2417     case LAYOUT_COMPACT:
2418         return Common::SettingsNames::guiSizesInMainWinWhenCompact;
2419     case LAYOUT_WIDE:
2420         return Common::SettingsNames::guiSizesInMainWinWhenWide;
2421     case LAYOUT_ONE_AT_TIME:
2422         return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime;
2423         break;
2424     }
2425     return QString();
2426 }
2427 
saveSizesAndState()2428 void MainWindow::saveSizesAndState()
2429 {
2430     if (m_skipSavingOfUI)
2431         return;
2432 
2433     QRect geometry = qApp->desktop()->availableGeometry(this);
2434     QString key = settingsKeyForLayout(m_layoutMode);
2435     if (key.isEmpty())
2436         return;
2437 
2438     QList<QByteArray> items;
2439     items << saveGeometry();
2440     items << saveState();
2441     items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray());
2442     items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray());
2443     items << msgListWidget->tree->header()->saveState();
2444     items << QByteArray::number(msgListWidget->tree->header()->count());
2445     for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) {
2446         items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i));
2447     }
2448     // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number
2449     items << QByteArray::number(menuBar()->isVisible());
2450     QByteArray buf;
2451     QDataStream stream(&buf, QIODevice::WriteOnly);
2452     stream << items.size();
2453     Q_FOREACH(const QByteArray &item, items) {
2454         stream << item;
2455     }
2456 
2457     m_settings->setValue(key.arg(QString::number(geometry.width())), buf);
2458 }
2459 
saveRawStateSetting(bool enabled)2460 void MainWindow::saveRawStateSetting(bool enabled)
2461 {
2462     m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled);
2463 }
2464 
applySizesAndState()2465 void MainWindow::applySizesAndState()
2466 {
2467     QRect geometry = qApp->desktop()->availableGeometry(this);
2468     QString key = settingsKeyForLayout(m_layoutMode);
2469     if (key.isEmpty())
2470         return;
2471 
2472     QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray();
2473     if (buf.isEmpty())
2474         return;
2475 
2476     int size;
2477     QDataStream stream(&buf, QIODevice::ReadOnly);
2478     stream >> size;
2479     QByteArray item;
2480 
2481     // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to
2482     // save our geometries and state, which is what we must avoid while this method is executing.
2483     bool skipSaving = m_skipSavingOfUI;
2484     m_skipSavingOfUI = true;
2485 
2486     if (size-- && !stream.atEnd()) {
2487         stream >> item;
2488 
2489         // https://bugreports.qt-project.org/browse/QTBUG-30636
2490         if (windowState() & Qt::WindowMaximized) {
2491             // restoreGeometry(.) restores the wrong size for at least maximized window
2492             // However, QWidget does also not notice that the configure request for this
2493             // is ignored by many window managers (because users really don't like when windows
2494             // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from
2495             // the wrong assumption the request would have been hononred.
2496             //  So we just "fix" the internal geometry immediately afterwards to prevent
2497             // mislayouting
2498             // There's atm. no flicker due to this (and because Qt compresses events)
2499             // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true)
2500             QRect oldGeometry = MainWindow::geometry();
2501             restoreGeometry(item);
2502             if (windowState() & Qt::WindowMaximized)
2503                 setGeometry(oldGeometry);
2504         } else {
2505             restoreGeometry(item);
2506             if (windowState() & Qt::WindowMaximized) {
2507                 // ensure to try setting the proper geometry and have the WM constrain us
2508                 setGeometry(QApplication::desktop()->availableGeometry());
2509             }
2510         }
2511     }
2512 
2513     if (size-- && !stream.atEnd()) {
2514         stream >> item;
2515         restoreState(item);
2516     }
2517 
2518     if (size-- && !stream.atEnd()) {
2519         stream >> item;
2520         if (m_mainVSplitter) {
2521             m_mainVSplitter->restoreState(item);
2522         }
2523     }
2524 
2525     if (size-- && !stream.atEnd()) {
2526         stream >> item;
2527         if (m_mainHSplitter) {
2528             m_mainHSplitter->restoreState(item);
2529         }
2530     }
2531 
2532     if (size-- && !stream.atEnd()) {
2533         stream >> item;
2534         msgListWidget->tree->header()->restoreState(item);
2535         // got to manually update the state of the actions which control the visibility state
2536         msgListWidget->tree->updateActionsAfterRestoredState();
2537     }
2538 
2539     connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns);
2540 
2541     if (size-- && !stream.atEnd()) {
2542         stream >> item;
2543         bool ok;
2544         int columns = item.toInt(&ok);
2545         if (ok) {
2546             msgListWidget->tree->header()->setStretchLastSection(false);
2547             for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) {
2548                 stream >> item;
2549                 int sectionSize = item.toInt();
2550                 QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i);
2551                 if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) {
2552                     // fun fact: user cannot resize by mouse when size <= 0
2553                     msgListWidget->tree->setColumnWidth(i, sectionSize);
2554                 } else {
2555                     msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i));
2556                 }
2557                 msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode);
2558             }
2559         }
2560     }
2561 
2562     if (size-- && !stream.atEnd()) {
2563         stream >> item;
2564         bool ok;
2565         bool visibility = item.toInt(&ok);
2566         if (ok) {
2567             menuBar()->setVisible(visibility);
2568             showMenuBar->setChecked(visibility);
2569         }
2570     }
2571 
2572     m_skipSavingOfUI = skipSaving;
2573 }
2574 
resizeEvent(QResizeEvent *)2575 void MainWindow::resizeEvent(QResizeEvent *)
2576 {
2577     m_delayedStateSaving->start();
2578 }
2579 
2580 /** @short Make sure that the message gets loaded after the splitters have changed their position */
possiblyLoadMessageOnSplittersChanged()2581 void MainWindow::possiblyLoadMessageOnSplittersChanged()
2582 {
2583     if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) {
2584         // We do not have to check whether it's a different message; the setMessage() will do this or us
2585         // and there are multiple proxy models involved anyway
2586         QModelIndex index = msgListWidget->tree->currentIndex();
2587         if (index.isValid()) {
2588             // OTOH, setting an invalid QModelIndex would happily assert-fail
2589             m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex());
2590         }
2591     }
2592 }
2593 
imapAccess() const2594 Imap::ImapAccess *MainWindow::imapAccess() const
2595 {
2596     return m_imapAccess;
2597 }
2598 
enableLoggingToDisk()2599 void MainWindow::enableLoggingToDisk()
2600 {
2601     imapLogger->slotSetPersistentLogging(true);
2602 }
2603 
slotPluginsChanged()2604 void MainWindow::slotPluginsChanged()
2605 {
2606     Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
2607     if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow))
2608         m_actionContactEditor->setEnabled(false);
2609     else
2610         m_actionContactEditor->setEnabled(true);
2611 }
2612 
2613 /** @short Update the default action to make sure that we show a correct status of the network connection */
updateNetworkIndication()2614 void MainWindow::updateNetworkIndication()
2615 {
2616     if (QAction *action = qobject_cast<QAction*>(sender())) {
2617         if (action->isChecked()) {
2618             m_netToolbarDefaultAction->setIcon(action->icon());
2619         }
2620     }
2621 }
2622 
showStatusMessage(const QString & message)2623 void MainWindow::showStatusMessage(const QString &message)
2624 {
2625     networkIndicator->setToolTip(message);
2626     if (isActiveWindow())
2627         QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message);
2628 }
2629 
slotMessageModelChanged(QAbstractItemModel * model)2630 void MainWindow::slotMessageModelChanged(QAbstractItemModel *model)
2631 {
2632     mailMimeTree->setModel(model);
2633 }
2634 
2635 }
2636