diff --git a/src/Composer/Submission.cpp b/src/Composer/Submission.cpp index 0872bc27..ca9f278e 100644 --- a/src/Composer/Submission.cpp +++ b/src/Composer/Submission.cpp @@ -1,461 +1,460 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát Copyright (C) 2012 Peter Amidon This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include "Composer/Submission.h" #include "Composer/MessageComposer.h" #include "Imap/Model/Model.h" #include "Imap/Tasks/AppendTask.h" #include "Imap/Tasks/GenUrlAuthTask.h" #include "Imap/Tasks/UidSubmitTask.h" #include "MSA/Sendmail.h" #include "MSA/SMTP.h" namespace { const int PROGRESS_MAX = 1000; // We're very likely almost half-way there -- let's call it 45% const int PROGRESS_SAVING_DONE = PROGRESS_MAX * 0.45; // Updating flags might take roughly as much time as the URLAUTH const int PROGRESS_DELIVERY_DONE = PROGRESS_MAX * 0.95; const int PROGRESS_DELIVERY_START_WITHOUT_SAVING = PROGRESS_MAX * 0.10; const int PROGRESS_DELIVERY_START_WITH_SAVING = PROGRESS_MAX * 0.5; } namespace Composer { QString submissionProgressToString(const Submission::SubmissionProgress progress) { switch (progress) { case Submission::STATE_INIT: - return QStringLiteral("STATE_INIT"); + return QStringLiteral("INIT"); case Submission::STATE_BUILDING_MESSAGE: - return QStringLiteral("STATE_BUILDING_MESSAGE"); + return QStringLiteral("BUILDING_MESSAGE"); case Submission::STATE_SAVING: - return QStringLiteral("STATE_SAVING"); + return QStringLiteral("SAVING"); case Submission::STATE_PREPARING_URLAUTH: - return QStringLiteral("STATE_PREPARING_URLAUTH"); + return QStringLiteral("PREPARING_URLAUTH"); case Submission::STATE_SUBMITTING: - return QStringLiteral("STATE_SUBMITTING"); + return QStringLiteral("SUBMITTING"); case Submission::STATE_UPDATING_FLAGS: - return QStringLiteral("STATE_UPDATING_FLAGS"); + return QStringLiteral("UPDATING_FLAGS"); case Submission::STATE_SENT: - return QStringLiteral("STATE_SENT"); + return QStringLiteral("SENT"); case Submission::STATE_FAILED: - return QStringLiteral("STATE_FAILED"); + return QStringLiteral("FAILED"); } return QStringLiteral("[unknown: %1]").arg(QString::number(static_cast(progress))); } Submission::Submission(QObject *parent, std::shared_ptr composer, Imap::Mailbox::Model *model, MSA::MSAFactory *msaFactory, const QString &accountId) : QObject(parent) , m_appendUidReceived(false) , m_appendUidValidity(0) , m_appendUid(0) , m_genUrlAuthReceived(false) , m_saveToSentFolder(false) , m_useBurl(false) , m_useImapSubmit(false) , m_state(STATE_INIT) , m_msaMaximalProgress(0) , m_source(composer) , m_model(model) , m_msaFactory(msaFactory) , m_accountId(accountId) , m_updateReplyingToMessageFlagsTask(0) , m_updateForwardingMessageFlagsTask(0) { m_source->setPreloadEnabled(shouldBuildMessageLocally()); } Submission::~Submission() = default; QString Submission::accountId() const { return m_accountId; } void Submission::changeConnectionState(const SubmissionProgress state) { + emit logged(Common::LOG_SUBMISSION, QStringLiteral("Submission"), QStringLiteral("Progress: %1 -> %2").arg( + submissionProgressToString(m_state), submissionProgressToString(state))); m_state = state; - if (m_model) - m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), submissionProgressToString(m_state)); // Now broadcast a human-readable message and update the progress dialog switch (state) { case STATE_INIT: emit progressMin(0); emit progressMax(0); emit progress(0); emit updateStatusMessage(tr("Preparing to send")); break; case STATE_BUILDING_MESSAGE: emit progressMax(0); emit progress(0); emit updateStatusMessage(tr("Creating message")); break; case STATE_SAVING: emit progressMax(0); emit progress(0); emit updateStatusMessage(tr("Saving to the sent folder")); break; case STATE_PREPARING_URLAUTH: emit progressMax(PROGRESS_MAX); emit progress(PROGRESS_SAVING_DONE); emit updateStatusMessage(tr("Preparing message for delivery")); break; case STATE_SUBMITTING: emit progressMax(PROGRESS_MAX); emit progress(m_saveToSentFolder ? PROGRESS_DELIVERY_START_WITH_SAVING : PROGRESS_DELIVERY_START_WITHOUT_SAVING); emit updateStatusMessage(tr("Submitting message")); break; case STATE_UPDATING_FLAGS: emit progressMax(PROGRESS_MAX); emit progress(PROGRESS_DELIVERY_DONE); emit updateStatusMessage(tr("Updating message keywords")); break; case STATE_SENT: emit progressMax(PROGRESS_MAX); emit progress(PROGRESS_MAX); emit updateStatusMessage(tr("Message sent")); break; case STATE_FAILED: // revert to the busy indicator emit progressMin(0); emit progressMax(0); emit progress(0); emit updateStatusMessage(tr("Sending failed")); break; } } void Submission::setImapOptions(const bool saveToSentFolder, const QString &sentFolderName, const QString &hostname, const QString &username, const bool useImapSubmit) { m_saveToSentFolder = saveToSentFolder; m_sentFolderName = sentFolderName; m_imapHostname = hostname; m_imapUsername = username; m_useImapSubmit = useImapSubmit; m_source->setPreloadEnabled(shouldBuildMessageLocally()); } void Submission::setSmtpOptions(const bool useBurl, const QString &smtpUsername) { m_useBurl = useBurl; if (m_useBurl && !m_model->isGenUrlAuthSupported()) { - m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), QStringLiteral("Cannot BURL without the URLAUTH extension")); + emit logged(Common::LOG_SUBMISSION, QStringLiteral("Submission"), QStringLiteral("Cannot BURL without the URLAUTH extension")); m_useBurl = false; } m_smtpUsername = smtpUsername; m_source->setPreloadEnabled(shouldBuildMessageLocally()); } void Submission::send() { if (!m_model) { gotError(tr("The IMAP connection has disappeared. " "You'll have close the composer, save the draft and re-open it later. " "The attachments will have to be added later. Sorry for the trouble, " "please see https://projects.flaska.net/issues/640 " "for details.")); return; } // this double-updating is needed in case the same Submission attempts to send a message more than once changeConnectionState(STATE_INIT); changeConnectionState(STATE_BUILDING_MESSAGE); if (shouldBuildMessageLocally() && !m_source->isReadyForSerialization()) { // we have to wait until the data arrive // FIXME: relax this to wait here gotError(tr("Some data are not available yet")); } else { slotMessageDataAvailable(); } } void Submission::slotMessageDataAvailable() { m_rawMessageData.clear(); QBuffer buf(&m_rawMessageData); buf.open(QIODevice::WriteOnly); QString errorMessage; QList catenateable; if (shouldBuildMessageLocally() && !m_source->asRawMessage(&buf, &errorMessage)) { gotError(tr("Cannot send right now -- saving failed:\n %1").arg(errorMessage)); return; } if (m_model->isCatenateSupported() && !m_source->asCatenateData(catenateable, &errorMessage)) { gotError(tr("Cannot send right now -- saving (CATENATE) failed:\n %1").arg(errorMessage)); return; } if (m_saveToSentFolder) { Q_ASSERT(m_model); m_appendUidReceived = false; m_genUrlAuthReceived = false; changeConnectionState(STATE_SAVING); QPointer appendTask = 0; if (m_model->isCatenateSupported()) { // FIXME: without UIDPLUS, there isn't much point in $SubmitPending... appendTask = QPointer( m_model->appendIntoMailbox( m_sentFolderName, catenateable, QStringList() << QStringLiteral("\\Seen"), m_source->timestamp())); } else { // FIXME: without UIDPLUS, there isn't much point in $SubmitPending... appendTask = QPointer( m_model->appendIntoMailbox( m_sentFolderName, m_rawMessageData, QStringList() << QStringLiteral("\\Seen"), m_source->timestamp())); } Q_ASSERT(appendTask); connect(appendTask.data(), &Imap::Mailbox::AppendTask::appendUid, this, &Submission::slotAppendUidKnown); connect(appendTask.data(), &Imap::Mailbox::ImapTask::completed, this, &Submission::slotAppendSucceeded); connect(appendTask.data(), &Imap::Mailbox::ImapTask::failed, this, &Submission::slotAppendFailed); } else { slotInvokeMsaNow(); } } void Submission::slotAskForUrl() { Q_ASSERT(m_appendUidReceived && m_useBurl); changeConnectionState(STATE_PREPARING_URLAUTH); Imap::Mailbox::GenUrlAuthTask *genUrlAuthTask = QPointer( m_model->generateUrlAuthForMessage(m_imapHostname, killDomainPartFromString(m_imapUsername), m_sentFolderName, m_appendUidValidity, m_appendUid, QString(), QStringLiteral("submit+%1").arg( killDomainPartFromString(m_smtpUsername)) )); connect(genUrlAuthTask, &Imap::Mailbox::GenUrlAuthTask::gotAuth, this, &Submission::slotGenUrlAuthReceived); connect(genUrlAuthTask, &Imap::Mailbox::ImapTask::failed, this, &Submission::gotError); } void Submission::slotInvokeMsaNow() { changeConnectionState(STATE_SUBMITTING); MSA::AbstractMSA *msa = m_msaFactory->create(this); connect(msa, &MSA::AbstractMSA::progressMax, this, &Submission::onMsaProgressMaxChanged); connect(msa, &MSA::AbstractMSA::progress, this, &Submission::onMsaProgressCurrentChanged); connect(msa, &MSA::AbstractMSA::sent, this, &Submission::sent); connect(msa, &MSA::AbstractMSA::error, this, &Submission::gotError); connect(msa, &MSA::AbstractMSA::passwordRequested, this, &Submission::passwordRequested); connect(this, &Submission::gotPassword, msa, &MSA::AbstractMSA::setPassword); connect(this, &Submission::canceled, msa, &MSA::AbstractMSA::cancel); if (m_useImapSubmit && msa->supportsImapSending() && m_appendUidReceived) { Imap::Mailbox::UidSubmitOptionsList options; options.append(qMakePair("FROM", m_source->rawFromAddress())); Q_FOREACH(const QByteArray &recipient, m_source->rawRecipientAddresses()) { options.append(qMakePair("RECIPIENT", recipient)); } msa->sendImap(m_sentFolderName, m_appendUidValidity, m_appendUid, options); } else if (m_genUrlAuthReceived && m_useBurl) { msa->sendBurl(m_source->rawFromAddress(), m_source->rawRecipientAddresses(), m_urlauth.toUtf8()); } else { msa->sendMail(m_source->rawFromAddress(), m_source->rawRecipientAddresses(), m_rawMessageData); } } void Submission::setPassword(const QString &password) { emit gotPassword(password); } void Submission::cancelPassword() { emit canceled(); } void Submission::gotError(const QString &error) { - if (m_model) - m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), QStringLiteral("gotError: %1").arg(error)); + emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("Submission"), QStringLiteral("Error: ") + error); changeConnectionState(STATE_FAILED); emit failed(error); } void Submission::sent() { if (m_source->replyingToMessage().isValid()) { m_updateReplyingToMessageFlagsTask = m_model->setMessageFlags(QModelIndexList() << m_source->replyingToMessage(), QStringLiteral("\\Answered"), Imap::Mailbox::FLAG_ADD); connect(m_updateReplyingToMessageFlagsTask, &Imap::Mailbox::ImapTask::completed, this, &Submission::onUpdatingFlagsOfReplyingToSucceded); connect(m_updateReplyingToMessageFlagsTask, &Imap::Mailbox::ImapTask::failed, this, &Submission::onUpdatingFlagsOfReplyingToFailed); changeConnectionState(STATE_UPDATING_FLAGS); } else if (m_source->forwardingMessage().isValid()) { m_updateForwardingMessageFlagsTask = m_model->setMessageFlags(QModelIndexList() << m_source->forwardingMessage(), QStringLiteral("$Forwarded"), Imap::Mailbox::FLAG_ADD); connect(m_updateForwardingMessageFlagsTask, &Imap::Mailbox::ImapTask::completed, this, &Submission::onUpdatingFlagsOfForwardingSucceeded); connect(m_updateForwardingMessageFlagsTask, &Imap::Mailbox::ImapTask::failed, this, &Submission::onUpdatingFlagsOfForwardingFailed); changeConnectionState(STATE_UPDATING_FLAGS); } else { changeConnectionState(STATE_SENT); emit succeeded(); } #if 0 if (m_appendUidReceived) { // FIXME: check the UIDVALIDITY!!! // FIXME: doesn't work at all; the messageIndexByUid() only works on already selected mailboxes QModelIndex message = m_mainWindow->imapModel()-> messageIndexByUid(QSettings().value(Common::SettingsNames::composerImapSentKey, QStringLiteral("Sent")).toString(), m_appendUid); if (message.isValid()) { m_mainWindow->imapModel()->setMessageFlags(QModelIndexList() << message, QLatin1String("\\Seen $Submitted"), Imap::Mailbox::FLAG_USE_THESE); } } #endif // FIXME: move back to the currently selected mailbox } /** @short Remember the APPENDUID as reported by the APPEND operation */ void Submission::slotAppendUidKnown(const uint uidValidity, const uint uid) { m_appendUidValidity = uidValidity; m_appendUid = uid; } void Submission::slotAppendFailed(const QString &error) { gotError(tr("APPEND failed: %1").arg(error)); } void Submission::slotAppendSucceeded() { if (m_appendUid && m_appendUidValidity) { // Only ever consider valid UIDVALIDITY/UID pair m_appendUidReceived = true; if (m_useBurl) { slotAskForUrl(); } else { slotInvokeMsaNow(); } } else { m_useBurl = false; - m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), - QStringLiteral("APPEND does not contain APPENDUID or UIDVALIDITY, cannot use BURL or the SUBMIT command")); + emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("Submission"), + QStringLiteral("APPEND does not contain APPENDUID or UIDVALIDITY, cannot use BURL or the SUBMIT command")); slotInvokeMsaNow(); } } /** @short Remember the GENURLAUTH response */ void Submission::slotGenUrlAuthReceived(const QString &url) { m_urlauth = url; if (!m_urlauth.isEmpty()) { m_genUrlAuthReceived = true; slotInvokeMsaNow(); } else { gotError(tr("The URLAUTH response does not contain a proper URL")); } } /** @short Remove the "@domain" from a string */ QString Submission::killDomainPartFromString(const QString &s) { return s.split(QLatin1Char('@'))[0]; } /** @short Return true if the message payload shall be built locally */ bool Submission::shouldBuildMessageLocally() const { if (!m_useImapSubmit) { // sending via SMTP or Sendmail // Unless all of URLAUTH, CATENATE and BURL is present and enabled, we will still have to download the data in the end return ! (m_useBurl && m_model->isCatenateSupported() && m_model->isGenUrlAuthSupported()); } else { return ! m_model->isCatenateSupported(); } } void Submission::onUpdatingFlagsOfReplyingToSucceded() { m_updateReplyingToMessageFlagsTask = 0; changeConnectionState(STATE_SENT); emit succeeded(); } void Submission::onUpdatingFlagsOfReplyingToFailed() { m_updateReplyingToMessageFlagsTask = 0; - m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), - QStringLiteral("Cannot update flags of the message we replied to -- interesting, but we cannot do anything at this point anyway")); + emit logged(Common::LogKind::LOG_OTHER, QStringLiteral("Submission"), + QStringLiteral("Cannot update flags of the message we replied to -- interesting, but we cannot do anything at this point anyway")); changeConnectionState(STATE_SENT); emit succeeded(); } void Submission::onUpdatingFlagsOfForwardingSucceeded() { m_updateForwardingMessageFlagsTask = 0; changeConnectionState(STATE_SENT); emit succeeded(); } void Submission::onUpdatingFlagsOfForwardingFailed() { m_updateForwardingMessageFlagsTask = 0; - m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), - QStringLiteral("Cannot update flags of the message we forwarded -- interesting, but we cannot do anything at this point anyway")); + emit logged(Common::LogKind::LOG_OTHER, QStringLiteral("Submission"), + QStringLiteral("Cannot update flags of the message we forwarded -- interesting, but we cannot do anything at this point anyway")); changeConnectionState(STATE_SENT); emit succeeded(); } void Submission::onMsaProgressCurrentChanged(const int value) { if (m_msaMaximalProgress > 0) { // prevent division by zero or performing operations which do not make any sense int low = m_saveToSentFolder ? PROGRESS_DELIVERY_START_WITH_SAVING : PROGRESS_DELIVERY_START_WITHOUT_SAVING; int high = PROGRESS_DELIVERY_DONE; emit progress(1.0 * value / m_msaMaximalProgress * (high - low) + low); } } void Submission::onMsaProgressMaxChanged(const int max) { m_msaMaximalProgress = max; } } diff --git a/src/Composer/Submission.h b/src/Composer/Submission.h index f3d01d10..1614c73a 100644 --- a/src/Composer/Submission.h +++ b/src/Composer/Submission.h @@ -1,153 +1,155 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMPOSER_SUBMISSION_H #define COMPOSER_SUBMISSION_H #include #include #include -#include "Recipients.h" +#include "Common/Logging.h" +#include "Composer/Recipients.h" namespace Imap { namespace Mailbox { class ImapTask; class Model; } } namespace MSA { class MSAFactory; } namespace Composer { class AbstractComposer; /** @short Handle submission of an e-mail via multiple ways This class uses the MessageComposer for modelling a message and an MSA implementation for the actual submission. The whole process is (trying to be) interruptable once started. */ class Submission : public QObject { Q_OBJECT public: explicit Submission(QObject *parent, std::shared_ptr composer, Imap::Mailbox::Model *model, MSA::MSAFactory *msaFactory, const QString &accountId); virtual ~Submission(); QString accountId() const; void setImapOptions(const bool saveToSentFolder, const QString &sentFolderName, const QString &hostname, const QString &username, const bool useImapSubmit); void setSmtpOptions(const bool useBurl, const QString &smtpUsername); void send(); /** @short Progress of the current submission */ enum SubmissionProgress { STATE_INIT, /**< Nothing is happening yet */ STATE_BUILDING_MESSAGE, /**< Waiting for data to become available */ STATE_SAVING, /**< Saving the message to the Sent folder */ STATE_PREPARING_URLAUTH, /**< Making the resulting message available via IMAP's URLAUTH */ STATE_SUBMITTING, /**< Submitting the message via an MSA */ STATE_UPDATING_FLAGS, /**< Updating flags of the relevant message(s) */ STATE_SENT, /**< All done, succeeded */ STATE_FAILED /**< Unable to send */ }; public slots: void setPassword(const QString &password); void cancelPassword(); private slots: void gotError(const QString &error); void sent(); void slotAppendUidKnown(const uint uidValidity, const uint uid); void slotGenUrlAuthReceived(const QString &url); void slotAppendSucceeded(); void slotAppendFailed(const QString &error); void onUpdatingFlagsOfReplyingToSucceded(); void onUpdatingFlagsOfReplyingToFailed(); void onUpdatingFlagsOfForwardingSucceeded(); void onUpdatingFlagsOfForwardingFailed(); void slotMessageDataAvailable(); void slotAskForUrl(); void slotInvokeMsaNow(); void onMsaProgressMaxChanged(const int max); void onMsaProgressCurrentChanged(const int value); signals: void progressMin(const int min); void progressMax(const int max); void progress(const int progress); void updateStatusMessage(const QString &message); void failed(const QString &message); void succeeded(); void passwordRequested(const QString &user, const QString &host); void gotPassword(const QString &password); void canceled(); + void logged(const Common::LogKind kind, const QString& source, const QString& message); private: bool shouldBuildMessageLocally() const; static QString killDomainPartFromString(const QString &s); void changeConnectionState(const SubmissionProgress state); bool m_appendUidReceived; uint m_appendUidValidity; uint m_appendUid; bool m_genUrlAuthReceived; QString m_urlauth; bool m_saveToSentFolder; QString m_sentFolderName; QString m_imapHostname; QString m_imapUsername; bool m_useBurl; QString m_smtpUsername; bool m_useImapSubmit; SubmissionProgress m_state; QByteArray m_rawMessageData; int m_msaMaximalProgress; std::shared_ptr m_source; QPointer m_model; MSA::MSAFactory *m_msaFactory; QString m_accountId; Imap::Mailbox::ImapTask *m_updateReplyingToMessageFlagsTask; Imap::Mailbox::ImapTask *m_updateForwardingMessageFlagsTask; Submission(const Submission &); // don't implement Submission &operator=(const Submission &); // don't implement }; QString submissionProgressToString(const Submission::SubmissionProgress progress); } #endif // COMPOSER_SUBMISSION_H diff --git a/src/Gui/ComposeWidget.cpp b/src/Gui/ComposeWidget.cpp index e01e5e9e..58b6417c 100644 --- a/src/Gui/ComposeWidget.cpp +++ b/src/Gui/ComposeWidget.cpp @@ -1,1850 +1,1855 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát Copyright (C) 2012 Peter Amidon Copyright (C) 2013 - 2014 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_ComposeWidget.h" #include "Composer/ExistingMessageComposer.h" #include "Composer/MessageComposer.h" #include "Composer/ReplaceSignature.h" #include "Composer/Mailto.h" #include "Composer/SenderIdentitiesModel.h" #include "Composer/Submission.h" #include "Common/InvokeMethod.h" #include "Common/Paths.h" #include "Common/SettingsNames.h" #include "Gui/CompleteMessageWidget.h" #include "Gui/ComposeWidget.h" #include "Gui/FromAddressProxyModel.h" #include "Gui/LineEdit.h" #include "Gui/MessageView.h" #include "Gui/OverlayWidget.h" #include "Gui/PasswordDialog.h" #include "Gui/ProgressPopUp.h" #include "Gui/Util.h" #include "Gui/Window.h" #include "Imap/Model/ImapAccess.h" #include "Imap/Model/ItemRoles.h" #include "Imap/Model/Model.h" #include "Imap/Parser/MailAddress.h" #include "Imap/Tasks/AppendTask.h" #include "Imap/Tasks/GenUrlAuthTask.h" #include "Imap/Tasks/UidSubmitTask.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PluginManager.h" #include "ShortcutHandler/ShortcutHandler.h" #include "UiUtils/Color.h" #include "UiUtils/IconLoader.h" namespace { enum { OFFSET_OF_FIRST_ADDRESSEE = 1, MIN_MAX_VISIBLE_RECIPIENTS = 4 }; } namespace Gui { static const QString trojita_opacityAnimation = QStringLiteral("trojita_opacityAnimation"); /** @short Keep track of whether the document has been updated since the last save */ class ComposerSaveState { public: explicit ComposerSaveState(ComposeWidget* w) : composer(w) , messageUpdated(false) , messageEverEdited(false) { } void setMessageUpdated(bool updated) { if (updated == messageUpdated) return; messageUpdated = updated; updateText(); } void setMessageEverEdited(bool everEdited) { if (everEdited == messageEverEdited) return; messageEverEdited = everEdited; updateText(); } const bool everEdited() {return messageEverEdited;} const bool updated() {return messageUpdated;} private: ComposeWidget* composer; /** @short Has it been updated since the last time we auto-saved it? */ bool messageUpdated; /** @short Was this message ever editted by human? We have to track both of these. Simply changing the sender (and hence the signature) without any text being written shall not trigger automatic saving, but on the other hand changing the sender after something was already written is an important change. */ bool messageEverEdited; void updateText() { composer->cancelButton->setText((messageUpdated || messageEverEdited) ? QWidget::tr("Cancel...") : QWidget::tr("Cancel")); } }; /** @short Ignore dirtying events while we're preparing the widget's contents Under the normal course of operation, there's plenty of events (user typing some text, etc) which lead to the composer widget "remembering" that the human being has made some changes, and that these changes are probably worth a prompt for saving them upon a close. This guard object makes sure (via RAII) that these dirtifying events are ignored during its lifetime. */ class InhibitComposerDirtying { public: explicit InhibitComposerDirtying(ComposeWidget *w): w(w), wasEverEdited(w->m_saveState->everEdited()), wasEverUpdated(w->m_saveState->updated()) {} ~InhibitComposerDirtying() { w->m_saveState->setMessageEverEdited(wasEverEdited); w->m_saveState->setMessageUpdated(wasEverUpdated); } private: ComposeWidget *w; bool wasEverEdited, wasEverUpdated; }; ComposeWidget::ComposeWidget(MainWindow *mainWindow, std::shared_ptr messageComposer, MSA::MSAFactory *msaFactory) : QWidget(0, Qt::Window) , ui(new Ui::ComposeWidget) , m_maxVisibleRecipients(MIN_MAX_VISIBLE_RECIPIENTS) , m_sentMail(false) , m_explicitDraft(false) , m_appendUidReceived(false) , m_appendUidValidity(0) , m_appendUid(0) , m_genUrlAuthReceived(false) , m_mainWindow(mainWindow) , m_settings(mainWindow->settings()) , m_composer(messageComposer) , m_submission(nullptr) , m_completionPopup(nullptr) , m_completionReceiver(nullptr) { setAttribute(Qt::WA_DeleteOnClose, true); QIcon winIcon; winIcon.addFile(QStringLiteral(":/icons/trojita-edit-big.svg"), QSize(128, 128)); winIcon.addFile(QStringLiteral(":/icons/trojita-edit-small.svg"), QSize(22, 22)); setWindowIcon(winIcon); Q_ASSERT(m_mainWindow); + m_mainWindow->registerComposeWindow(this); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); QString accountId = profileName.isEmpty() ? QStringLiteral("account-0") : profileName; m_submission = new Composer::Submission(this, m_composer, m_mainWindow->imapModel(), msaFactory, accountId); connect(m_submission, &Composer::Submission::succeeded, this, &ComposeWidget::sent); connect(m_submission, &Composer::Submission::failed, this, &ComposeWidget::gotError); + connect(m_submission, &Composer::Submission::failed, this, [this](const QString& message) { + emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("ComposeWidget"), message); + }); + connect(m_submission, &Composer::Submission::logged, this, &ComposeWidget::logged); connect(m_submission, &Composer::Submission::passwordRequested, this, &ComposeWidget::passwordRequested, Qt::QueuedConnection); ui->setupUi(this); if (interactiveComposer()) { interactiveComposer()->setReportTrojitaVersions(m_settings->value(Common::SettingsNames::interopRevealVersions, true).toBool()); ui->attachmentsView->setComposer(interactiveComposer()); } sendButton = ui->buttonBox->addButton(tr("Send"), QDialogButtonBox::AcceptRole); sendButton->setIcon(UiUtils::loadIcon(QStringLiteral("mail-send"))); connect(sendButton, &QAbstractButton::clicked, this, &ComposeWidget::send); cancelButton = ui->buttonBox->addButton(QDialogButtonBox::Cancel); cancelButton->setIcon(UiUtils::loadIcon(QStringLiteral("dialog-cancel"))); connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::close); connect(ui->attachButton, &QAbstractButton::clicked, this, &ComposeWidget::slotAskForFileAttachment); m_saveState = std::unique_ptr(new ComposerSaveState(this)); m_completionPopup = new QMenu(this); m_completionPopup->installEventFilter(this); connect(m_completionPopup, &QMenu::triggered, this, &ComposeWidget::completeRecipient); // TODO: make this configurable? m_completionCount = 8; m_recipientListUpdateTimer = new QTimer(this); m_recipientListUpdateTimer->setSingleShot(true); m_recipientListUpdateTimer->setInterval(250); connect(m_recipientListUpdateTimer, &QTimer::timeout, this, &ComposeWidget::updateRecipientList); connect(ui->verticalSplitter, &QSplitter::splitterMoved, this, &ComposeWidget::calculateMaxVisibleRecipients); calculateMaxVisibleRecipients(); connect(ui->recipientSlider, &QAbstractSlider::valueChanged, this, &ComposeWidget::scrollRecipients); connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); ui->recipientSlider->setMinimum(0); ui->recipientSlider->setMaximum(0); ui->recipientSlider->setVisible(false); ui->envelopeWidget->installEventFilter(this); m_markButton = new QToolButton(ui->buttonBox); m_markButton->setPopupMode(QToolButton::MenuButtonPopup); m_markButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); m_markAsReply = new QActionGroup(m_markButton); m_markAsReply->setExclusive(true); auto *asReplyMenu = new QMenu(m_markButton); m_markButton->setMenu(asReplyMenu); m_actionStandalone = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-fill")), tr("New Thread")); m_actionStandalone->setActionGroup(m_markAsReply); m_actionStandalone->setCheckable(true); m_actionStandalone->setToolTip(tr("This mail will be sent as a standalone message.
Change to preserve the reply hierarchy.")); m_actionInReplyTo = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Threaded")); m_actionInReplyTo->setActionGroup(m_markAsReply); m_actionInReplyTo->setCheckable(true); // This is a "quick shortcut action". It shows the UI bits of the current option, but when the user clicks it, // the *other* action is triggered. m_actionToggleMarking = new QAction(m_markButton); connect(m_actionToggleMarking, &QAction::triggered, this, &ComposeWidget::toggleReplyMarking); m_markButton->setDefaultAction(m_actionToggleMarking); // Unfortunately, there's no signal for toggled(QAction*), so we'll have to call QAction::trigger() to have this working connect(m_markAsReply, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMarkingAction); m_actionStandalone->trigger(); m_replyModeButton = new QToolButton(ui->buttonBox); m_replyModeButton->setPopupMode(QToolButton::InstantPopup); m_replyModeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); QMenu *replyModeMenu = new QMenu(m_replyModeButton); m_replyModeButton->setMenu(replyModeMenu); m_replyModeActions = new QActionGroup(m_replyModeButton); m_replyModeActions->setExclusive(true); m_actionHandPickedRecipients = new QAction(UiUtils::loadIcon(QStringLiteral("document-edit")) ,QStringLiteral("Hand Picked Recipients"), this); replyModeMenu->addAction(m_actionHandPickedRecipients); m_actionHandPickedRecipients->setActionGroup(m_replyModeActions); m_actionHandPickedRecipients->setCheckable(true); replyModeMenu->addSeparator(); QAction *placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_private")); m_actionReplyModePrivate = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModePrivate->setActionGroup(m_replyModeActions); m_actionReplyModePrivate->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all_but_me")); m_actionReplyModeAllButMe = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeAllButMe->setActionGroup(m_replyModeActions); m_actionReplyModeAllButMe->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all")); m_actionReplyModeAll = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeAll->setActionGroup(m_replyModeActions); m_actionReplyModeAll->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_list")); m_actionReplyModeList = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeList->setActionGroup(m_replyModeActions); m_actionReplyModeList->setCheckable(true); connect(m_replyModeActions, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMode); // We want to have the button aligned to the left; the only "portable" way of this is the ResetRole // (thanks to TL for mentioning this, and for the Qt's doc for providing pretty pictures on different platforms) ui->buttonBox->addButton(m_markButton, QDialogButtonBox::ResetRole); // Using ResetRole for reasons same as with m_markButton. We want this button to be second from the left. ui->buttonBox->addButton(m_replyModeButton, QDialogButtonBox::ResetRole); m_markButton->hide(); m_replyModeButton->hide(); connect(ui->mailText, &ComposerTextEdit::urlsAdded, this, &ComposeWidget::slotAttachFiles); connect(ui->mailText, &ComposerTextEdit::sendRequest, this, &ComposeWidget::send); connect(ui->mailText, &QTextEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::updateWindowTitle); connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->subject, &QLineEdit::returnPressed, this, [=]() { ui->mailText->setFocus(); }); updateWindowTitle(); FromAddressProxyModel *proxy = new FromAddressProxyModel(this); proxy->setSourceModel(m_mainWindow->senderIdentitiesModel()); ui->sender->setModel(proxy); connect(ui->sender, static_cast(&QComboBox::currentIndexChanged), this, &ComposeWidget::slotUpdateSignature); connect(ui->sender, &QComboBox::editTextChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->sender->lineEdit(), &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender); QTimer *autoSaveTimer = new QTimer(this); connect(autoSaveTimer, &QTimer::timeout, this, &ComposeWidget::autoSaveDraft); autoSaveTimer->start(30*1000); // these are for the automatically saved drafts, i.e. no i18n for the dir name m_autoSavePath = QString(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/")); QDir().mkpath(m_autoSavePath); m_autoSavePath += QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1String(".draft"); // Add a blank recipient row to start with addRecipient(m_recipients.count(), interactiveComposer() ? Composer::ADDRESS_TO : Composer::ADDRESS_RESENT_TO, QString()); ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus(); slotUpdateSignature(); // default size int sz = ui->mailText->idealWidth(); ui->mailText->setMinimumSize(sz, 1000*sz/1618); // golden mean editor adjustSize(); ui->mailText->setMinimumSize(0, 0); resize(size().boundedTo(qApp->desktop()->availableGeometry().size())); } ComposeWidget::~ComposeWidget() { delete ui; } std::shared_ptr ComposeWidget::interactiveComposer() { return std::dynamic_pointer_cast(m_composer); } /** @short Throw a warning at an attempt to create a Compose Widget while the MSA is not configured */ ComposeWidget *ComposeWidget::warnIfMsaNotConfigured(ComposeWidget *widget, MainWindow *mainWindow) { if (!widget) QMessageBox::critical(mainWindow, tr("Error"), tr("Please set appropriate settings for outgoing messages.")); return widget; } /** @short Find a nice position near the mid of the main window, try to not fully occlude another sibling */ void ComposeWidget::placeOnMainWindow() { QRect area = m_mainWindow->geometry(); QRect origin(0, 0, width(), height()); origin.moveTo(area.x() + (area.width() - width()) / 2, area.y() + (area.height() - height()) / 2); QRect target = origin; QWidgetList siblings; foreach(const QWidget *w, QApplication::topLevelWidgets()) { if (w == this) continue; // I'm not a sibling of myself if (!qobject_cast(w)) continue; // random other stuff siblings << const_cast(w); } int dx = 20, dy = 20; int i = 0; // look for a position where the window would not fully cover another composer // (we don't want to mass open 10 composers stashing each other) // if such composer blocks our desired geometry, the new desired geometry is // tested at positions shifted by 20px circling around the original one. // if we're already more than 100px off the center (what implies the user // has > 20 composers open ...) we give up to not shift the window // too far away, maybe even off-screen. // Notice that it may still happen that some composers *together* stash a 3rd one while (i < siblings.count()) { if (target.contains(siblings.at(i)->geometry())) { target = origin.translated(dx, dy); if (dx < 0 && dy < 0) { dx = dy = -dx + 20; if (dx >= 120) // give up break; } else if (dx < 0 || dy < 0) { dx = -dx; if (dy > 0) dy = -dy; } else { dx = -dx; } i = 0; } else { ++i; } } setGeometry(target); } /** @short Create a blank composer window */ ComposeWidget *ComposeWidget::createBlank(MainWindow *mainWindow) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); w->placeOnMainWindow(); w->show(); return w; } /** @short Load a draft in composer window */ ComposeWidget *ComposeWidget::createDraft(MainWindow *mainWindow, const QString &path) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); w->loadDraft(path); w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window with data from a URL */ ComposeWidget *ComposeWidget::createFromUrl(MainWindow *mainWindow, const QUrl &url) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); QString subject; QString body; QList > recipients; QList inReplyTo; QList references; const QUrlQuery q(url); if (!q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")).isEmpty()) { // There should be only single email address created by Imap::Message::MailAddress::asUrl() Imap::Message::MailAddress addr; if (Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("mailto"))) recipients << qMakePair(Composer::ADDRESS_TO, addr.asPrettyString()); } else { // This should be real RFC 6068 mailto: Composer::parseRFC6068Mailto(url, subject, body, recipients, inReplyTo, references); } // NOTE: we need inReplyTo and references parameters without angle brackets, so remove them for (int i = 0; i < inReplyTo.size(); ++i) { if (inReplyTo[i].startsWith('<') && inReplyTo[i].endsWith('>')) { inReplyTo[i] = inReplyTo[i].mid(1, inReplyTo[i].size()-2); } } for (int i = 0; i < references.size(); ++i) { if (references[i].startsWith('<') && references[i].endsWith('>')) { references[i] = references[i].mid(1, references[i].size()-2); } } w->setResponseData(recipients, subject, body, inReplyTo, references, QModelIndex()); if (!inReplyTo.isEmpty() || !references.isEmpty()) { // We don't need to expose any UI here, but we want the in-reply-to and references information to be carried with this message w->m_actionInReplyTo->setChecked(true); } w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window for a reply */ ComposeWidget *ComposeWidget::createReply(MainWindow *mainWindow, const Composer::ReplyMode &mode, const QModelIndex &replyingToMessage, const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); w->setResponseData(recipients, subject, body, inReplyTo, references, replyingToMessage); bool ok = w->setReplyMode(mode); if (!ok) { QString err; switch (mode) { case Composer::REPLY_ALL: case Composer::REPLY_ALL_BUT_ME: // do nothing break; case Composer::REPLY_LIST: err = tr("It doesn't look like this is a message to the mailing list. Please fill in the recipients manually."); break; case Composer::REPLY_PRIVATE: err = trUtf8("Trojitá was unable to safely determine the real e-mail address of the author of the message. " "You might want to use the \"Reply All\" function and trim the list of addresses manually."); break; } if (!err.isEmpty()) { Gui::Util::messageBoxWarning(w, tr("Cannot Determine Recipients"), err); } } w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window for a mail-forward action */ ComposeWidget *ComposeWidget::createForward(MainWindow *mainWindow, const Composer::ForwardMode mode, const QModelIndex &forwardingMessage, const QString &subject, const QList &inReplyTo, const QList &references) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); w->setResponseData(QList>(), subject, QString(), inReplyTo, references, QModelIndex()); // We don't need to expose any UI here, but we want the in-reply-to and references information to be carried with this message w->m_actionInReplyTo->setChecked(true); // Prepare the message to be forwarded and add it to the attachments view w->interactiveComposer()->prepareForwarding(forwardingMessage, mode); w->placeOnMainWindow(); w->show(); return w; } ComposeWidget *ComposeWidget::createFromReadOnly(MainWindow *mainWindow, const QModelIndex &messageRoot, const QList>& recipients) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(messageRoot); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); for (int i = 0; i < recipients.size(); ++i) { w->addRecipient(i, recipients[i].first, recipients[i].second); } w->updateRecipientList(); // Disable what needs to be nuked w->ui->fromLabel->setText(tr("Sender")); w->ui->subject->hide(); w->ui->subjectLabel->hide(); w->ui->attachmentBox->hide(); w->ui->mailText->hide(); auto subject = messageRoot.data(Imap::Mailbox::RoleMessageSubject).toString(); w->setWindowTitle(tr("Bounce Mail: %1").arg(subject.isEmpty() ? tr("(no subject)") : subject)); // Show the full content of that e-mail as the "main body" within this widget CompleteMessageWidget *messageWidget = new CompleteMessageWidget(w, mainWindow->settings(), mainWindow->pluginManager(), mainWindow->favoriteTagsModel()); messageWidget->messageView->setMessage(messageRoot); messageWidget->messageView->setNetworkWatcher(qobject_cast(mainWindow->imapAccess()->networkWatcher())); messageWidget->setFocusPolicy(Qt::StrongFocus); w->ui->verticalSplitter->insertWidget(1, messageWidget); w->ui->verticalSplitter->setStretchFactor(1, 100); QStringList warnings; if (subject.isEmpty()) { warnings << tr("Message has no subject"); } if (messageRoot.data(Imap::Mailbox::RoleMessageMessageId).toByteArray().isEmpty()) { warnings << tr("The Message-ID header is missing"); } if (!messageRoot.data(Imap::Mailbox::RoleMessageDate).toDateTime().isValid()) { warnings << tr("Message has no date"); } if (messageRoot.data(Imap::Mailbox::RoleMessageFrom).toList().isEmpty()) { warnings << tr("Nothing in the From field"); } if (messageRoot.data(Imap::Mailbox::RoleMessageTo).toList().isEmpty()) { warnings << tr("No recipients in the To field"); } if (!warnings.isEmpty()) { auto lbl = new QLabel(tr("This message appears to be malformed, please be careful before sending it.") + QStringLiteral("
  • ") + warnings.join(QStringLiteral("
  • ")) + QStringLiteral("
"), w); lbl->setStyleSheet(Gui::Util::cssWarningBorder()); w->ui->verticalSplitter->insertWidget(1, lbl); } w->placeOnMainWindow(); w->show(); return w; } void ComposeWidget::updateReplyMode() { bool replyModeSet = false; if (m_actionReplyModePrivate->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_PRIVATE); } else if (m_actionReplyModeAllButMe->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_ALL_BUT_ME); } else if (m_actionReplyModeAll->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_ALL); } else if (m_actionReplyModeList->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_LIST); } if (!replyModeSet) { // This is for now by design going in one direction only, from enabled to disabled. // The index to the message cannot become valid again, and simply marking the buttons as disabled does the trick quite neatly. m_replyModeButton->setEnabled(m_actionHandPickedRecipients->isChecked()); markReplyModeHandpicked(); } } void ComposeWidget::markReplyModeHandpicked() { m_actionHandPickedRecipients->setChecked(true); m_replyModeButton->setText(m_actionHandPickedRecipients->text()); m_replyModeButton->setIcon(m_actionHandPickedRecipients->icon()); } void ComposeWidget::passwordRequested(const QString &user, const QString &host) { if (m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool()) { auto password = qobject_cast(m_mainWindow->imapAccess()->imapModel())->imapPassword(); if (password.isNull()) { // This can happen for example when we've always been offline since the last profile change, // and the IMAP password is therefore not already cached in the IMAP model. // FIXME: it would be nice to "just" call out to MainWindow::authenticationRequested() in that case, // but there's no async callback when the password is available. Just some food for thought when // that part gets refactored :), eventually... askPassword(user, host); } else { m_submission->setPassword(password); } return; } Plugins::PasswordPlugin *password = m_mainWindow->pluginManager()->password(); if (!password) { askPassword(user, host); return; } // FIXME: use another account-id at some point in future // we are now using the profile to avoid overwriting passwords of // other profiles in secure storage // 'account-0' is the hardcoded value when not using a profile Plugins::PasswordJob *job = password->requestPassword(m_submission->accountId(), QStringLiteral("smtp")); if (!job) { askPassword(user, host); return; } connect(job, &Plugins::PasswordJob::passwordAvailable, m_submission, &Composer::Submission::setPassword); connect(job, &Plugins::PasswordJob::error, this, &ComposeWidget::passwordError); job->setAutoDelete(true); job->setProperty("user", user); job->setProperty("host", host); job->start(); } void ComposeWidget::passwordError() { Plugins::PasswordJob *job = static_cast(sender()); const QString &user = job->property("user").toString(); const QString &host = job->property("host").toString(); askPassword(user, host); } void ComposeWidget::askPassword(const QString &user, const QString &host) { auto w = Gui::PasswordDialog::getPassword(this, tr("Authentication Required"), tr("

Please provide SMTP password for user %1 on %2:

").arg( user.toHtmlEscaped(), host.toHtmlEscaped())); connect(w, &Gui::PasswordDialog::gotPassword, m_submission, &Composer::Submission::setPassword); connect(w, &Gui::PasswordDialog::rejected, m_submission, &Composer::Submission::cancelPassword); } void ComposeWidget::changeEvent(QEvent *e) { QWidget::changeEvent(e); switch (e->type()) { case QEvent::LanguageChange: ui->retranslateUi(this); break; default: break; } } /** * We capture the close event and check whether there's something to save * (not sent, not up-to-date or persistent autostore) * The offer the user to store or omit the message or not close at all */ void ComposeWidget::closeEvent(QCloseEvent *ce) { const bool noSaveRequired = m_sentMail || !m_saveState->everEdited() || (m_explicitDraft && !m_saveState->updated()) || !interactiveComposer(); // autosave to permanent draft and no update if (!noSaveRequired) { // save is required QMessageBox msgBox(this); msgBox.setWindowModality(Qt::WindowModal); msgBox.setWindowTitle(tr("Save Draft?")); QString message(tr("The mail has not been sent.
Do you want to save the draft?")); if (ui->attachmentsView->model()->rowCount() > 0) message += tr("
Warning: Attachments are not saved with the draft!"); msgBox.setText(message); msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Save); int ret = msgBox.exec(); if (ret == QMessageBox::Save) { if (m_explicitDraft) { // editing a present draft - override it saveDraft(m_autoSavePath); } else { // Explicitly stored drafts should be saved in a location with proper i18n support, so let's make sure both main // window and this code uses the same tr() calls QString path(Common::writablePath(Common::LOCATION_DATA) + Gui::MainWindow::tr("Drafts")); QDir().mkpath(path); QString filename = ui->subject->text(); if (filename.isEmpty()) { filename = QDateTime::currentDateTime().toString(Qt::ISODate); } // Some characters are best avoided in file names. This is probably not a definitive list, but the hope is that // it's going to be more readable than an unformatted hash or similar stuff. The list of characters was taken // from http://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words . filename.replace(QRegExp(QLatin1String("[/\\\\:\"|<>*?]")), QStringLiteral("_")); path = QFileDialog::getSaveFileName(this, tr("Save as"), path + QLatin1Char('/') + filename + QLatin1String(".draft"), tr("Drafts") + QLatin1String(" (*.draft)")); if (path.isEmpty()) { // cancelled save ret = QMessageBox::Cancel; } else { m_explicitDraft = true; saveDraft(path); if (path != m_autoSavePath) // we can remove the temp save QFile::remove(m_autoSavePath); } } } if (ret == QMessageBox::Cancel) { ce->ignore(); // don't close the window return; } } if (m_sentMail || !m_explicitDraft) // is the mail has been sent or the user does not want to store it QFile::remove(m_autoSavePath); // get rid of draft ce->accept(); // ultimately close the window } bool ComposeWidget::buildMessageData() { // Recipients are checked at all times, including when bouncing/redirecting QList > recipients; QString errorMessage; if (!parseRecipients(recipients, errorMessage)) { gotError(tr("Cannot parse recipients:\n%1").arg(errorMessage)); return false; } if (recipients.isEmpty()) { gotError(tr("You haven't entered any recipients")); return false; } m_composer->setRecipients(recipients); // The same applies to the sender which is needed by some MSAs for origin information Imap::Message::MailAddress fromAddress; if (!Imap::Message::MailAddress::fromPrettyString(fromAddress, ui->sender->currentText())) { gotError(tr("The From: address does not look like a valid one")); return false; } m_composer->setFrom(fromAddress); if (auto composer = interactiveComposer()) { if (ui->subject->text().isEmpty()) { gotError(tr("You haven't entered any subject. Cannot send such a mail, sorry.")); ui->subject->setFocus(); return false; } composer->setTimestamp(QDateTime::currentDateTime()); composer->setSubject(ui->subject->text()); QAbstractProxyModel *proxy = qobject_cast(ui->sender->model()); Q_ASSERT(proxy); if (ui->sender->findText(ui->sender->currentText()) != -1) { QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex()); Q_ASSERT(proxyIndex.isValid()); composer->setOrganization( proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(), Composer::SenderIdentitiesModel::COLUMN_ORGANIZATION) .data().toString()); } composer->setText(ui->mailText->toPlainText()); if (m_actionInReplyTo->isChecked()) { composer->setInReplyTo(m_inReplyTo); composer->setReferences(m_references); composer->setReplyingToMessage(m_replyingToMessage); } else { composer->setInReplyTo(QList()); composer->setReferences(QList()); composer->setReplyingToMessage(QModelIndex()); } } if (!m_composer->isReadyForSerialization()) { gotError(tr("Cannot prepare this e-mail for sending: some parts are not available")); return false; } return true; } void ComposeWidget::send() { if (interactiveComposer()) { // Well, Trojita is of course rock solid and will never ever crash :), but experience has shown that every now and then, // there is a subtle issue $somewhere. This means that it's probably a good idea to save the draft explicitly -- better // than losing some work. It's cheap anyway. saveDraft(m_autoSavePath); } if (!buildMessageData()) { return; } const bool reuseImapCreds = m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool(); m_submission->setImapOptions(m_settings->value(Common::SettingsNames::composerSaveToImapKey, true).toBool(), m_settings->value(Common::SettingsNames::composerImapSentKey, QStringLiteral("Sent")).toString(), m_settings->value(Common::SettingsNames::imapHostKey).toString(), m_settings->value(Common::SettingsNames::imapUserKey).toString(), m_settings->value(Common::SettingsNames::msaMethodKey).toString() == Common::SettingsNames::methodImapSendmail); m_submission->setSmtpOptions(m_settings->value(Common::SettingsNames::smtpUseBurlKey, false).toBool(), reuseImapCreds ? m_mainWindow->imapAccess()->username() : m_settings->value(Common::SettingsNames::smtpUserKey).toString()); ProgressPopUp *progress = new ProgressPopUp(); OverlayWidget *overlay = new OverlayWidget(progress, this); overlay->show(); setUiWidgetsEnabled(false); connect(m_submission, &Composer::Submission::progressMin, progress, &ProgressPopUp::setMinimum); connect(m_submission, &Composer::Submission::progressMax, progress, &ProgressPopUp::setMaximum); connect(m_submission, &Composer::Submission::progress, progress, &ProgressPopUp::setValue); connect(m_submission, &Composer::Submission::updateStatusMessage, progress, &ProgressPopUp::setLabelText); connect(m_submission, &Composer::Submission::succeeded, overlay, &QObject::deleteLater); connect(m_submission, &Composer::Submission::failed, overlay, &QObject::deleteLater); m_submission->send(); } void ComposeWidget::setUiWidgetsEnabled(const bool enabled) { ui->verticalSplitter->setEnabled(enabled); ui->buttonBox->setEnabled(enabled); } /** @short Set private data members to get pre-filled by available parameters The semantics of the @arg inReplyTo and @arg references are the same as described for the Composer::MessageComposer, i.e. the data are not supposed to contain the angle bracket. If the @arg replyingToMessage is present, it will be used as an index to a message which will get marked as replied to. This is needed because IMAP doesn't really support site-wide search by a Message-Id (and it cannot possibly support it in general, either), and because Trojita's lazy loading and lack of cross-session persistent indexes means that "mark as replied" and "extract message-id from" are effectively two separate operations. */ void ComposeWidget::setResponseData(const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references, const QModelIndex &replyingToMessage) { InhibitComposerDirtying inhibitor(this); for (int i = 0; i < recipients.size(); ++i) { addRecipient(i, recipients.at(i).first, recipients.at(i).second); } updateRecipientList(); ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus(); ui->subject->setText(subject); ui->mailText->setText(body); m_inReplyTo = inReplyTo; // Trim the References header as per RFC 5537 QList trimmedReferences = references; int referencesSize = QByteArray("References: ").size(); const int lineOverhead = 3; // one for the " " prefix, two for the \r\n suffix Q_FOREACH(const QByteArray &item, references) referencesSize += item.size() + lineOverhead; // The magic numbers are from RFC 5537 while (referencesSize >= 998 && trimmedReferences.size() > 3) { referencesSize -= trimmedReferences.takeAt(1).size() + lineOverhead; } m_references = trimmedReferences; m_replyingToMessage = replyingToMessage; if (m_replyingToMessage.isValid()) { m_markButton->show(); m_replyModeButton->show(); // Got to use trigger() so that the default action of the QToolButton is updated m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response
%1").arg( m_replyingToMessage.data(Imap::Mailbox::RoleMessageSubject).toString().toHtmlEscaped() )); m_actionInReplyTo->trigger(); // Enable only those Reply Modes that are applicable to the message to be replied Composer::RecipientList dummy; m_actionReplyModePrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); m_actionReplyModeAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); m_actionReplyModeAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); m_actionReplyModeList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); } else { m_markButton->hide(); m_replyModeButton->hide(); m_actionInReplyTo->setToolTip(QString()); m_actionStandalone->trigger(); } int row = -1; bool ok = Composer::Util::chooseSenderIdentityForReply(m_mainWindow->senderIdentitiesModel(), replyingToMessage, row); if (ok) { Q_ASSERT(row >= 0 && row < m_mainWindow->senderIdentitiesModel()->rowCount()); ui->sender->setCurrentIndex(row); } slotUpdateSignature(); } /** @short Find out what type of recipient to use for the last row */ Composer::RecipientKind ComposeWidget::recipientKindForNextRow(const Composer::RecipientKind kind) { using namespace Imap::Mailbox; switch (kind) { case Composer::ADDRESS_TO: // Heuristic: if the last one is "to", chances are that the next one shall not be "to" as well. // Cc is reasonable here. return Composer::ADDRESS_CC; case Composer::ADDRESS_RESENT_TO: return Composer::ADDRESS_RESENT_CC; case Composer::ADDRESS_CC: case Composer::ADDRESS_BCC: case Composer::ADDRESS_RESENT_CC: case Composer::ADDRESS_RESENT_BCC: // In any other case, it is probably better to just reuse the type of the last row return kind; case Composer::ADDRESS_FROM: case Composer::ADDRESS_SENDER: case Composer::ADDRESS_REPLY_TO: case Composer::ADDRESS_RESENT_FROM: case Composer::ADDRESS_RESENT_SENDER: // shall never be used here Q_ASSERT(false); return kind; } Q_ASSERT(false); return Composer::ADDRESS_TO; } //BEGIN QFormLayout workarounds /** First issue: QFormLayout messes up rows by never removing them * ---------------------------------------------------------------- * As a result insertRow(int pos, .) does not pick the expected row, but usually minor * (if you ever removed all items of a row in this layout) * * Solution: we count all rows non empty rows and when we have enough, return the row suitable for * QFormLayout (which is usually behind the requested one) */ static int actualRow(QFormLayout *form, int row) { for (int i = 0, c = 0; i < form->rowCount(); ++i) { if (c == row) { return i; } if (form->itemAt(i, QFormLayout::LabelRole) || form->itemAt(i, QFormLayout::FieldRole) || form->itemAt(i, QFormLayout::SpanningRole)) ++c; } return form->rowCount(); // append } /** Second (related) issue: QFormLayout messes the tab order * ---------------------------------------------------------- * "Inserted" rows just get appended to the present ones and by this to the tab focus order * It's therefore necessary to fix this forcing setTabOrder() * * Approach: traverse all rows until we have the widget that shall be inserted in tab order and * return it's predecessor */ static QWidget* formPredecessor(QFormLayout *form, QWidget *w) { QWidget *pred = 0; QWidget *runner = 0; QLayoutItem *item = 0; for (int i = 0; i < form->rowCount(); ++i) { if ((item = form->itemAt(i, QFormLayout::LabelRole))) { runner = item->widget(); if (runner == w) return pred; else if (runner) pred = runner; } if ((item = form->itemAt(i, QFormLayout::FieldRole))) { runner = item->widget(); if (runner == w) return pred; else if (runner) pred = runner; } if ((item = form->itemAt(i, QFormLayout::SpanningRole))) { runner = item->widget(); if (runner == w) return pred; else if (runner) pred = runner; } } return pred; } //END QFormLayout workarounds void ComposeWidget::calculateMaxVisibleRecipients() { const int oldMaxVisibleRecipients = m_maxVisibleRecipients; int spacing, bottom; ui->envelopeLayout->getContentsMargins(&spacing, &spacing, &spacing, &bottom); // we abuse the fact that there's always an addressee and that they all look the same QRect itemRects[2]; for (int i = 0; i < 2; ++i) { if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::LabelRole)) { itemRects[i] |= li->geometry(); } if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::FieldRole)) { itemRects[i] |= li->geometry(); } if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::SpanningRole)) { itemRects[i] |= li->geometry(); } } int itemHeight = itemRects[0].height(); spacing = qMax(0, itemRects[0].top() - itemRects[1].bottom() - 1); // QFormLayout::[vertical]spacing() is useless ... int firstTop = itemRects[0].top(); const int subjectHeight = ui->subject->height(); const int height = ui->verticalSplitter->sizes().at(0) - // entire splitter area firstTop - // offset of first recipient (subjectHeight + spacing) - // for the subject bottom - // layout bottom padding 2; // extra pixels padding to detect that the user wants to shrink if (itemHeight + spacing == 0) { m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS; } else { m_maxVisibleRecipients = height / (itemHeight + spacing); } if (m_maxVisibleRecipients < MIN_MAX_VISIBLE_RECIPIENTS) m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS; // allow up to 4 recipients w/o need for a sliding if (oldMaxVisibleRecipients != m_maxVisibleRecipients) { const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients); int v = qRound(1.0f*(ui->recipientSlider->value()*m_maxVisibleRecipients)/oldMaxVisibleRecipients); ui->recipientSlider->setMaximum(max); ui->recipientSlider->setVisible(max > 0); scrollRecipients(qMin(qMax(0, v), max)); } } void ComposeWidget::addRecipient(int position, Composer::RecipientKind kind, const QString &address) { QComboBox *combo = new QComboBox(this); if (interactiveComposer()) { combo->addItem(tr("To"), Composer::ADDRESS_TO); combo->addItem(tr("Cc"), Composer::ADDRESS_CC); combo->addItem(tr("Bcc"), Composer::ADDRESS_BCC); } else { combo->addItem(tr("Resent-To"), Composer::ADDRESS_RESENT_TO); combo->addItem(tr("Resent-Cc"), Composer::ADDRESS_RESENT_CC); combo->addItem(tr("Resent-Bcc"), Composer::ADDRESS_RESENT_BCC); } combo->setCurrentIndex(combo->findData(kind)); LineEdit *edit = new LineEdit(address, this); slotCheckAddress(edit); connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender); connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(edit, &QLineEdit::textEdited, this, &ComposeWidget::completeRecipients); connect(edit, &QLineEdit::editingFinished, this, &ComposeWidget::collapseRecipients); connect(edit, &QLineEdit::textChanged, m_recipientListUpdateTimer, static_cast(&QTimer::start)); connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::markReplyModeHandpicked); connect(edit, &QLineEdit::returnPressed, this, [=]() { gotoNextInputLineFrom(edit); }); m_recipients.insert(position, Recipient(combo, edit)); ui->envelopeWidget->setUpdatesEnabled(false); ui->envelopeLayout->insertRow(actualRow(ui->envelopeLayout, position + OFFSET_OF_FIRST_ADDRESSEE), combo, edit); setTabOrder(formPredecessor(ui->envelopeLayout, combo), combo); setTabOrder(combo, edit); const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients); ui->recipientSlider->setMaximum(max); ui->recipientSlider->setVisible(max > 0); if (ui->recipientSlider->isVisible()) { const int v = ui->recipientSlider->value(); int keepInSight = ++position; for (int i = 0; i < m_recipients.count(); ++i) { if (m_recipients.at(i).first->hasFocus() || m_recipients.at(i).second->hasFocus()) { keepInSight = i; break; } } if (qAbs(keepInSight - position) < m_maxVisibleRecipients) ui->recipientSlider->setValue(position*max/m_recipients.count()); if (v == ui->recipientSlider->value()) // force scroll update scrollRecipients(v); } ui->envelopeWidget->setUpdatesEnabled(true); } void ComposeWidget::slotCheckAddressOfSender() { QLineEdit *edit = qobject_cast(sender()); Q_ASSERT(edit); slotCheckAddress(edit); } void ComposeWidget::slotCheckAddress(QLineEdit *edit) { Imap::Message::MailAddress addr; if (edit->text().isEmpty() || Imap::Message::MailAddress::fromPrettyString(addr, edit->text())) { edit->setPalette(QPalette()); } else { QPalette p; p.setColor(QPalette::Base, UiUtils::tintColor(p.color(QPalette::Base), QColor(0xff, 0, 0, 0x20))); edit->setPalette(p); } } void ComposeWidget::removeRecipient(int pos) { // removing the widgets from the layout is important // a) not doing so leaks (minor) // b) deleteLater() crosses the evenchain and so our actualRow function would be tricked QWidget *formerFocus = QApplication::focusWidget(); if (!formerFocus) formerFocus = m_lastFocusedRecipient; if (pos + 1 < m_recipients.count()) { if (m_recipients.at(pos).first == formerFocus) { m_recipients.at(pos + 1).first->setFocus(); formerFocus = m_recipients.at(pos + 1).first; } else if (m_recipients.at(pos).second == formerFocus) { m_recipients.at(pos + 1).second->setFocus(); formerFocus = m_recipients.at(pos + 1).second; } } else if (m_recipients.at(pos).first == formerFocus || m_recipients.at(pos).second == formerFocus) { formerFocus = 0; } ui->envelopeLayout->removeWidget(m_recipients.at(pos).first); ui->envelopeLayout->removeWidget(m_recipients.at(pos).second); m_recipients.at(pos).first->deleteLater(); m_recipients.at(pos).second->deleteLater(); m_recipients.removeAt(pos); const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients); ui->recipientSlider->setMaximum(max); ui->recipientSlider->setVisible(max > 0); if (formerFocus) { // skip event loop, remove might be triggered by imminent focus loss CALL_LATER_NOARG(formerFocus, setFocus); } } static inline Composer::RecipientKind currentRecipient(const QComboBox *box) { return Composer::RecipientKind(box->itemData(box->currentIndex()).toInt()); } void ComposeWidget::updateRecipientList() { // we ensure there's always one empty available bool haveEmpty = false; for (int i = 0; i < m_recipients.count(); ++i) { if (m_recipients.at(i).second->text().isEmpty()) { if (haveEmpty) { removeRecipient(i); } haveEmpty = true; } } if (!haveEmpty) { addRecipient(m_recipients.count(), !interactiveComposer() ? Composer::ADDRESS_RESENT_TO : ( m_recipients.isEmpty() ? Composer::ADDRESS_TO : recipientKindForNextRow(currentRecipient(m_recipients.last().first)) ), QString()); } } void ComposeWidget::gotoNextInputLineFrom(QWidget *w) { bool wFound = false; for(Recipient recipient : m_recipients) { if (wFound) { recipient.second->setFocus(); return; } if (recipient.second == w) wFound = true; } Q_ASSERT(wFound); ui->subject->setFocus(); } void ComposeWidget::handleFocusChange() { // got explicit focus on other widget - don't restore former focused recipient on scrolling m_lastFocusedRecipient = QApplication::focusWidget(); if (m_lastFocusedRecipient) QTimer::singleShot(150, this, SLOT(scrollToFocus())); // give user chance to notice the focus change disposition } void ComposeWidget::scrollToFocus() { if (!ui->recipientSlider->isVisible()) return; QWidget *focus = QApplication::focusWidget(); if (focus == ui->envelopeWidget) focus = m_lastFocusedRecipient; if (!focus) return; // if this is the first or last visible recipient, show one more (to hint there's more and allow tab progression) for (int i = 0, pos = 0; i < m_recipients.count(); ++i) { if (m_recipients.at(i).first->isVisible()) ++pos; if (focus == m_recipients.at(i).first || focus == m_recipients.at(i).second) { if (pos > 1 && pos < m_maxVisibleRecipients) // prev & next are in sight break; if (pos == 1) ui->recipientSlider->setValue(i - 1); // scroll to prev else ui->recipientSlider->setValue(i + 2 - m_maxVisibleRecipients); // scroll to next break; } } if (focus == m_lastFocusedRecipient) focus->setFocus(); // in case we scrolled to m_lastFocusedRecipient } void ComposeWidget::fadeIn(QWidget *w) { QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(w); w->setGraphicsEffect(effect); QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity", w); connect(animation, &QAbstractAnimation::finished, this, &ComposeWidget::slotFadeFinished); animation->setObjectName(trojita_opacityAnimation); animation->setDuration(333); animation->setStartValue(0.0); animation->setEndValue(1.0); animation->start(QAbstractAnimation::DeleteWhenStopped); } void ComposeWidget::slotFadeFinished() { Q_ASSERT(sender()); QWidget *animatedEffectWidget = qobject_cast(sender()->parent()); Q_ASSERT(animatedEffectWidget); animatedEffectWidget->setGraphicsEffect(0); // deletes old one } void ComposeWidget::scrollRecipients(int value) { // ignore focus changes caused by "scrolling" disconnect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); QList visibleWidgets; for (int i = 0; i < m_recipients.count(); ++i) { // remove all widgets from the form because of vspacing - causes spurious padding QWidget *toCC = m_recipients.at(i).first; QWidget *lineEdit = m_recipients.at(i).second; if (!m_lastFocusedRecipient) { // apply only _once_ if (toCC->hasFocus()) m_lastFocusedRecipient = toCC; else if (lineEdit->hasFocus()) m_lastFocusedRecipient = lineEdit; } if (toCC->isVisible()) visibleWidgets << toCC; if (lineEdit->isVisible()) visibleWidgets << lineEdit; ui->envelopeLayout->removeWidget(toCC); ui->envelopeLayout->removeWidget(lineEdit); toCC->hide(); lineEdit->hide(); } const int begin = qMin(m_recipients.count(), value); const int end = qMin(m_recipients.count(), value + m_maxVisibleRecipients); for (int i = begin, j = 0; i < end; ++i, ++j) { const int pos = actualRow(ui->envelopeLayout, j + OFFSET_OF_FIRST_ADDRESSEE); QWidget *toCC = m_recipients.at(i).first; QWidget *lineEdit = m_recipients.at(i).second; ui->envelopeLayout->insertRow(pos, toCC, lineEdit); if (!visibleWidgets.contains(toCC)) fadeIn(toCC); visibleWidgets.removeOne(toCC); if (!visibleWidgets.contains(lineEdit)) fadeIn(lineEdit); visibleWidgets.removeOne(lineEdit); toCC->show(); lineEdit->show(); setTabOrder(formPredecessor(ui->envelopeLayout, toCC), toCC); setTabOrder(toCC, lineEdit); if (toCC == m_lastFocusedRecipient) toCC->setFocus(); else if (lineEdit == m_lastFocusedRecipient) lineEdit->setFocus(); } if (m_lastFocusedRecipient && !m_lastFocusedRecipient->hasFocus() && QApplication::focusWidget()) ui->envelopeWidget->setFocus(); Q_FOREACH (QWidget *w, visibleWidgets) { // was visible, is no longer -> stop animation so it won't conflict later ones w->setGraphicsEffect(0); // deletes old one if (QPropertyAnimation *pa = w->findChild(trojita_opacityAnimation)) pa->stop(); } connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); } void ComposeWidget::collapseRecipients() { QLineEdit *edit = qobject_cast(sender()); Q_ASSERT(edit); if (edit->hasFocus() || !edit->text().isEmpty()) return; // nothing to clean up // an empty recipient line just lost focus -> we "place it at the end", ie. simply remove it // and append a clone bool needEmpty = false; Composer::RecipientKind carriedKind = recipientKindForNextRow(interactiveComposer() ? Composer::RecipientKind::ADDRESS_TO : Composer::RecipientKind::ADDRESS_RESENT_TO); for (int i = 0; i < m_recipients.count() - 1; ++i) { // sic! on the -1, no action if it trails anyway if (m_recipients.at(i).second == edit) { carriedKind = currentRecipient(m_recipients.last().first); removeRecipient(i); needEmpty = true; break; } } if (needEmpty) addRecipient(m_recipients.count(), carriedKind, QString()); } void ComposeWidget::gotError(const QString &error) { QMessageBox::critical(this, tr("Failed to Send Mail"), error); setUiWidgetsEnabled(true); } void ComposeWidget::sent() { // FIXME: move back to the currently selected mailbox m_sentMail = true; QTimer::singleShot(0, this, SLOT(close())); } bool ComposeWidget::parseRecipients(QList > &results, QString &errorMessage) { for (int i = 0; i < m_recipients.size(); ++i) { Composer::RecipientKind kind = currentRecipient(m_recipients.at(i).first); QString text = m_recipients.at(i).second->text(); if (text.isEmpty()) continue; Imap::Message::MailAddress addr; bool ok = Imap::Message::MailAddress::fromPrettyString(addr, text); if (ok) { // TODO: should we *really* learn every junk entered into a recipient field? // m_mainWindow->addressBook()->learn(addr); results << qMakePair(kind, addr); } else { errorMessage = tr("Can't parse \"%1\" as an e-mail address.").arg(text); return false; } } return true; } void ComposeWidget::completeRecipients(const QString &text) { if (text.isEmpty()) { // if there's a popup close it and set back the receiver m_completionPopup->close(); m_completionReceiver = 0; return; // we do not suggest "nothing" } Q_ASSERT(sender()); QLineEdit *toEdit = qobject_cast(sender()); Q_ASSERT(toEdit); Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.take(toEdit); Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.take(toEdit); // if two jobs are running, first was started before second so first should finish earlier // stop second job if (firstJob && secondJob) { disconnect(secondJob, nullptr, this, nullptr); secondJob->stop(); secondJob->deleteLater(); secondJob = 0; } // now at most one job is running Plugins::AddressbookPlugin *addressbook = m_mainWindow->pluginManager()->addressbook(); if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureCompletion)) return; auto newJob = addressbook->requestCompletion(text, QStringList(), m_completionCount); if (!newJob) return; if (secondJob) { // if only second job is running move second to first and push new as second firstJob = secondJob; secondJob = newJob; } else if (firstJob) { // if only first job is running push new job as second secondJob = newJob; } else { // if no jobs is running push new job as first firstJob = newJob; } if (firstJob) m_firstCompletionRequests.insert(toEdit, firstJob); if (secondJob) m_secondCompletionRequests.insert(toEdit, secondJob); connect(newJob, &Plugins::AddressbookCompletionJob::completionAvailable, this, &ComposeWidget::onCompletionAvailable); connect(newJob, &Plugins::AddressbookCompletionJob::error, this, &ComposeWidget::onCompletionFailed); newJob->setAutoDelete(true); newJob->start(); } void ComposeWidget::onCompletionFailed(Plugins::AddressbookJob::Error error) { Q_UNUSED(error); onCompletionAvailable(Plugins::NameEmailList()); } void ComposeWidget::onCompletionAvailable(const Plugins::NameEmailList &completion) { Plugins::AddressbookJob *job = qobject_cast(sender()); Q_ASSERT(job); QLineEdit *toEdit = m_firstCompletionRequests.key(job); if (!toEdit) toEdit = m_secondCompletionRequests.key(job); if (!toEdit) return; // jobs are removed from QMap below Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.value(toEdit); Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.value(toEdit); if (job == secondJob) { // second job finished before first and first was started before second // so stop first because it has old data if (firstJob) { disconnect(firstJob, nullptr, this, nullptr); firstJob->stop(); firstJob->deleteLater(); firstJob = nullptr; } m_firstCompletionRequests.remove(toEdit); m_secondCompletionRequests.remove(toEdit); } else if (job == firstJob) { // first job finished, but if second is still running it will have new data, so do not stop it m_firstCompletionRequests.remove(toEdit); } QStringList contacts; for (int i = 0; i < completion.size(); ++i) { const Plugins::NameEmail &item = completion.at(i); contacts << Imap::Message::MailAddress::fromNameAndMail(item.name, item.email).asPrettyString(); } if (contacts.isEmpty()) { m_completionReceiver = 0; m_completionPopup->close(); } else { m_completionReceiver = toEdit; m_completionPopup->setUpdatesEnabled(false); QList acts = m_completionPopup->actions(); Q_FOREACH(const QString &s, contacts) m_completionPopup->addAction(s)->setData(s); Q_FOREACH(QAction *act, acts) { m_completionPopup->removeAction(act); delete act; } if (m_completionPopup->isHidden()) m_completionPopup->popup(toEdit->mapToGlobal(QPoint(0, toEdit->height()))); m_completionPopup->setUpdatesEnabled(true); } } void ComposeWidget::completeRecipient(QAction *act) { if (act->data().toString().isEmpty()) return; m_completionReceiver->setText(act->data().toString()); m_completionReceiver = 0; m_completionPopup->close(); } bool ComposeWidget::eventFilter(QObject *o, QEvent *e) { if (o == m_completionPopup) { if (!m_completionPopup->isVisible()) return false; if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) { QKeyEvent *ke = static_cast(e); if (!( ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down || // Navigation ke->key() == Qt::Key_Escape || // "escape" ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { // selection Q_ASSERT(m_completionReceiver); QCoreApplication::sendEvent(m_completionReceiver, e); return true; } } return false; } if (o == ui->envelopeWidget) { if (e->type() == QEvent::Wheel) { int v = ui->recipientSlider->value(); if (static_cast(e)->delta() > 0) --v; else ++v; // just QApplication::sendEvent(ui->recipientSlider, e) will cause a recursion if // ui->recipientSlider ignores the event (eg. because it would lead to an invalid value) // since ui->recipientSlider is child of ui->envelopeWidget // my guts tell me to not send events to children if it can be avoided, but its just a gut feeling ui->recipientSlider->setValue(v); e->accept(); return true; } if (e->type() == QEvent::KeyPress && ui->envelopeWidget->hasFocus()) { scrollToFocus(); QWidget *focus = QApplication::focusWidget(); if (focus && focus != ui->envelopeWidget) { int key = static_cast(e)->key(); if (!(key == Qt::Key_Tab || key == Qt::Key_Backtab)) // those alter the focus again QApplication::sendEvent(focus, e); } return true; } if (e->type() == QEvent::Resize) { QResizeEvent *re = static_cast(e); if (re->size().height() != re->oldSize().height()) calculateMaxVisibleRecipients(); return false; } return false; } return false; } void ComposeWidget::slotAskForFileAttachment() { static QDir directory = QDir::home(); QString fileName = QFileDialog::getOpenFileName(this, tr("Attach File..."), directory.absolutePath(), QString(), 0, QFileDialog::DontResolveSymlinks); if (!fileName.isEmpty()) { directory = QFileInfo(fileName).absoluteDir(); interactiveComposer()->addFileAttachment(fileName); } } void ComposeWidget::slotAttachFiles(QList urls) { foreach (const QUrl &url, urls) { if (url.isLocalFile()) { interactiveComposer()->addFileAttachment(url.path()); } } } void ComposeWidget::slotUpdateSignature() { InhibitComposerDirtying inhibitor(this); QAbstractProxyModel *proxy = qobject_cast(ui->sender->model()); Q_ASSERT(proxy); QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex()); if (!proxyIndex.isValid()) { // This happens when the settings dialog gets closed and the SenderIdentitiesModel reloads data from the on-disk cache return; } QString newSignature = proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(), Composer::SenderIdentitiesModel::COLUMN_SIGNATURE) .data().toString(); Composer::Util::replaceSignature(ui->mailText->document(), newSignature); } /** @short Massage the list of recipients so that they match the desired type of reply In case of an error, the original list of recipients is left as is. */ bool ComposeWidget::setReplyMode(const Composer::ReplyMode mode) { if (!m_replyingToMessage.isValid()) return false; // Determine the new list of recipients Composer::RecipientList list; if (!Composer::Util::replyRecipientList(mode, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, list)) { return false; } while (!m_recipients.isEmpty()) removeRecipient(0); Q_FOREACH(Composer::RecipientList::value_type recipient, list) { if (!recipient.second.hasUsefulDisplayName()) recipient.second.name.clear(); addRecipient(m_recipients.size(), recipient.first, recipient.second.asPrettyString()); } updateRecipientList(); switch (mode) { case Composer::REPLY_PRIVATE: m_actionReplyModePrivate->setChecked(true); break; case Composer::REPLY_ALL_BUT_ME: m_actionReplyModeAllButMe->setChecked(true); break; case Composer::REPLY_ALL: m_actionReplyModeAll->setChecked(true); break; case Composer::REPLY_LIST: m_actionReplyModeList->setChecked(true); break; } m_replyModeButton->setText(m_replyModeActions->checkedAction()->text()); m_replyModeButton->setIcon(m_replyModeActions->checkedAction()->icon()); ui->mailText->setFocus(); return true; } /** local draft serializaton: * Version (int) * Whether this draft was stored explicitly (bool) * The sender (QString) * Amount of recipients (int) * n * (RecipientKind ("int") + recipient (QString)) * Subject (QString) * The message text (QString) */ void ComposeWidget::saveDraft(const QString &path) { static const int trojitaDraftVersion = 3; QFile file(path); if (!file.open(QIODevice::WriteOnly)) return; // TODO: error message? QDataStream stream(&file); stream.setVersion(QDataStream::Qt_4_6); stream << trojitaDraftVersion << m_explicitDraft << ui->sender->currentText(); stream << m_recipients.count(); for (int i = 0; i < m_recipients.count(); ++i) { stream << m_recipients.at(i).first->itemData(m_recipients.at(i).first->currentIndex()).toInt(); stream << m_recipients.at(i).second->text(); } stream << m_composer->timestamp() << m_inReplyTo << m_references; stream << m_actionInReplyTo->isChecked(); stream << ui->subject->text(); stream << ui->mailText->toPlainText(); // we spare attachments // a) serializing isn't an option, they could be HUUUGE // b) storing urls only works for urls // c) the data behind the url or the url validity might have changed // d) nasty part is writing mails - DnD a file into it is not a problem file.close(); file.setPermissions(QFile::ReadOwner|QFile::WriteOwner); } /** * When loading a draft we omit the present autostorage (content is replaced anyway) and make * the loaded path the autosave path, so all further automatic storage goes into the present * draft file */ void ComposeWidget::loadDraft(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return; if (m_autoSavePath != path) { QFile::remove(m_autoSavePath); m_autoSavePath = path; } QDataStream stream(&file); stream.setVersion(QDataStream::Qt_4_6); QString string; int version, recipientCount; stream >> version; stream >> m_explicitDraft; stream >> string >> recipientCount; // sender / amount of recipients int senderIndex = ui->sender->findText(string); if (senderIndex != -1) { ui->sender->setCurrentIndex(senderIndex); } else { ui->sender->setEditText(string); } for (int i = 0; i < recipientCount; ++i) { int kind; stream >> kind >> string; if (!string.isEmpty()) addRecipient(i, static_cast(kind), string); } if (version >= 2) { QDateTime timestamp; stream >> timestamp >> m_inReplyTo >> m_references; interactiveComposer()->setTimestamp(timestamp); if (!m_inReplyTo.isEmpty()) { m_markButton->show(); // FIXME: in-reply-to's validitiy isn't the best check for showing or not showing the reply mode. // For eg: consider cases of mailto, forward, where valid in-reply-to won't mean choice of reply modes. m_replyModeButton->show(); m_actionReplyModeAll->setEnabled(false); m_actionReplyModeAllButMe->setEnabled(false); m_actionReplyModeList->setEnabled(false); m_actionReplyModePrivate->setEnabled(false); markReplyModeHandpicked(); // We do not have the message index at this point, but we can at least show the Message-Id here QStringList inReplyTo; Q_FOREACH(auto item, m_inReplyTo) { // There's no HTML escaping to worry about inReplyTo << QLatin1Char('<') + QString::fromUtf8(item.constData()) + QLatin1Char('>'); } m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response
%1").arg( inReplyTo.join(tr("
")).toHtmlEscaped() )); if (version == 2) { // it is always marked as a reply in v2 m_actionInReplyTo->trigger(); } } } if (version >= 3) { bool replyChecked; stream >> replyChecked; // Got to use trigger() so that the default action of the QToolButton is updated if (replyChecked) { m_actionInReplyTo->trigger(); } else { m_actionStandalone->trigger(); } } stream >> string; ui->subject->setText(string); stream >> string; ui->mailText->setPlainText(string); m_saveState->setMessageUpdated(false); // this is now the most up-to-date one file.close(); } void ComposeWidget::autoSaveDraft() { if (m_saveState->updated()) { m_saveState->setMessageUpdated(false); saveDraft(m_autoSavePath); } } void ComposeWidget::setMessageUpdated() { m_saveState->setMessageUpdated(true); m_saveState->setMessageEverEdited(true); } void ComposeWidget::updateWindowTitle() { if (ui->subject->text().isEmpty()) { setWindowTitle(tr("Compose Mail")); } else { setWindowTitle(tr("%1 - Compose Mail").arg(ui->subject->text())); } } void ComposeWidget::toggleReplyMarking() { (m_actionInReplyTo->isChecked() ? m_actionStandalone : m_actionInReplyTo)->trigger(); } void ComposeWidget::updateReplyMarkingAction() { auto action = m_markAsReply->checkedAction(); m_actionToggleMarking->setText(action->text()); m_actionToggleMarking->setIcon(action->icon()); m_actionToggleMarking->setToolTip(action->toolTip()); } } diff --git a/src/Gui/ComposeWidget.h b/src/Gui/ComposeWidget.h index fe6fad1c..37cd4a5c 100644 --- a/src/Gui/ComposeWidget.h +++ b/src/Gui/ComposeWidget.h @@ -1,218 +1,222 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COMPOSEWIDGET_H #define COMPOSEWIDGET_H #include #include #include #include #include #include #include +#include "Common/Logging.h" #include "Composer/Recipients.h" #include "Composer/MessageComposer.h" #include "Plugins/AddressbookPlugin.h" namespace Ui { class ComposeWidget; } class QAbstractListModel; class QActionGroup; class QComboBox; class QLineEdit; class QMenu; class QPushButton; class QSettings; class QToolButton; namespace Composer { class Submission; } namespace MSA { class MSAFactory; } namespace Gui { class MainWindow; class InhibitComposerDirtying; class ComposerSaveState; /** @short A "Compose New Mail..." dialog Implements a widget which can act as a standalone window for composing e-mail messages. Uses Imap::Mailbox::MessageComposer as a backend for composing a message. @see Imap::Mailbox::MessageComposer */ class ComposeWidget : public QWidget { Q_OBJECT public: ~ComposeWidget(); static ComposeWidget *warnIfMsaNotConfigured(ComposeWidget *widget, MainWindow *mainWindow); static ComposeWidget *createDraft(MainWindow *mainWindow, const QString &path); static ComposeWidget *createBlank(MainWindow *mainWindow); static ComposeWidget *createFromUrl(MainWindow *mainWindow, const QUrl &url); static ComposeWidget *createReply(MainWindow *mainWindow, const Composer::ReplyMode &mode, const QModelIndex &replyingToMessage, const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references); static ComposeWidget *createForward(MainWindow *mainWindow, const Composer::ForwardMode mode, const QModelIndex &forwardingMessage, const QString &subject, const QList &inReplyTo, const QList &references); static ComposeWidget *createFromReadOnly(MainWindow *mainWindow, const QModelIndex &messageRoot, const QList> &recipients); void placeOnMainWindow(); protected: void changeEvent(QEvent *e); void closeEvent(QCloseEvent *ce); bool eventFilter(QObject *o, QEvent *e); private slots: void calculateMaxVisibleRecipients(); void collapseRecipients(); void completeRecipient(QAction *act); void completeRecipients(const QString &text); void send(); void gotError(const QString &error); void sent(); void updateRecipientList(); void scrollRecipients(int); void gotoNextInputLineFrom(QWidget *w); void handleFocusChange(); void scrollToFocus(); void slotFadeFinished(); void slotCheckAddressOfSender(); void slotCheckAddress(QLineEdit *edit); void slotAskForFileAttachment(); void slotAttachFiles(QList urls); void slotUpdateSignature(); void updateWindowTitle(); void autoSaveDraft(); void setMessageUpdated(); void setUiWidgetsEnabled(const bool enabled); void onCompletionAvailable(const Plugins::NameEmailList &completion); void onCompletionFailed(Plugins::AddressbookJob::Error error); void passwordRequested(const QString &user, const QString &host); void passwordError(); void toggleReplyMarking(); void updateReplyMarkingAction(); void updateReplyMode(); void markReplyModeHandpicked(); +signals: + void logged(const Common::LogKind kind, const QString& source, const QString& message); + private: ComposeWidget(MainWindow *mainWindow, std::shared_ptr messageComposer, MSA::MSAFactory *msaFactory); void setResponseData(const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references, const QModelIndex &replyingToMessage); bool setReplyMode(const Composer::ReplyMode mode); static QByteArray extractMailAddress(const QString &text, bool &ok); static Composer::RecipientKind recipientKindForNextRow(const Composer::RecipientKind kind); void addRecipient(int position, Composer::RecipientKind kind, const QString &address); bool parseRecipients(QList > &results, QString &errorMessage); void removeRecipient(int position); void fadeIn(QWidget *w); void askPassword(const QString &user, const QString &host); bool buildMessageData(); bool shouldBuildMessageLocally() const; void saveDraft(const QString &path); void loadDraft(const QString &path); std::shared_ptr interactiveComposer(); Ui::ComposeWidget *ui; QPushButton *sendButton; QPushButton *cancelButton; QToolButton *m_markButton; QActionGroup *m_markAsReply; QAction *m_actionStandalone; QAction *m_actionInReplyTo; QAction *m_actionToggleMarking; QToolButton *m_replyModeButton; QActionGroup *m_replyModeActions; QAction *m_actionHandPickedRecipients; QAction *m_actionReplyModePrivate; QAction *m_actionReplyModeAllButMe; QAction *m_actionReplyModeAll; QAction *m_actionReplyModeList; typedef QPair Recipient; QList m_recipients; QTimer *m_recipientListUpdateTimer; QPointer m_lastFocusedRecipient; int m_maxVisibleRecipients; bool m_sentMail; bool m_explicitDraft; QString m_autoSavePath; QList m_inReplyTo; QList m_references; QPersistentModelIndex m_replyingToMessage; std::unique_ptr m_saveState; bool m_appendUidReceived; uint m_appendUidValidity; uint m_appendUid; bool m_genUrlAuthReceived; QString m_urlauth; MainWindow *m_mainWindow; QSettings *m_settings; std::shared_ptr m_composer; Composer::Submission *m_submission; QMenu *m_completionPopup; QLineEdit *m_completionReceiver; int m_completionCount; QMap m_firstCompletionRequests; QMap m_secondCompletionRequests; friend class InhibitComposerDirtying; friend class ComposerSaveState; ComposeWidget(const ComposeWidget &); // don't implement ComposeWidget &operator=(const ComposeWidget &); // don't implement }; } #endif // COMPOSEWIDGET_H diff --git a/src/Gui/Window.cpp b/src/Gui/Window.cpp index 3c38577a..a5a3c3c1 100644 --- a/src/Gui/Window.cpp +++ b/src/Gui/Window.cpp @@ -1,2887 +1,2894 @@ /* Copyright (C) 2006 - 2015 Jan Kundrát Copyright (C) 2013 - 2015 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "configure.cmake.h" #include "Common/Application.h" #include "Common/Paths.h" #include "Common/PortNumbers.h" #include "Common/SettingsNames.h" #include "Composer/Mailto.h" #include "Composer/SenderIdentitiesModel.h" #ifdef TROJITA_HAVE_CRYPTO_MESSAGES # ifdef TROJITA_HAVE_GPGMEPP # include "Cryptography/GpgMe++.h" # endif #endif #include "Imap/Model/ImapAccess.h" #include "Imap/Model/MailboxTree.h" #include "Imap/Model/Model.h" #include "Imap/Model/ModelWatcher.h" #include "Imap/Model/MsgListModel.h" #include "Imap/Model/NetworkWatcher.h" #include "Imap/Model/PrettyMailboxModel.h" #include "Imap/Model/PrettyMsgListModel.h" #include "Imap/Model/SpecialFlagNames.h" #include "Imap/Model/ThreadingMsgListModel.h" #include "Imap/Model/FavoriteTagsModel.h" #include "Imap/Model/Utils.h" #include "Imap/Tasks/ImapTask.h" #include "Imap/Network/FileDownloadManager.h" #include "MSA/ImapSubmit.h" #include "MSA/Sendmail.h" #include "MSA/SMTP.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PasswordPlugin.h" #include "Plugins/PluginManager.h" #include "CompleteMessageWidget.h" #include "ComposeWidget.h" #include "MailBoxTreeView.h" #include "MessageListWidget.h" #include "MessageView.h" #include "MessageSourceWidget.h" #include "Gui/MessageHeadersWidget.h" #include "MsgListView.h" #include "OnePanelAtTimeWidget.h" #include "PasswordDialog.h" #include "ProtocolLoggerWidget.h" #include "SettingsDialog.h" #include "SimplePartWidget.h" #include "Streams/SocketFactory.h" #include "TaskProgressIndicator.h" #include "Util.h" #include "Window.h" #include "ShortcutHandler/ShortcutHandler.h" #include "ui_CreateMailboxDialog.h" #include "ui_AboutDialog.h" #include "Imap/Model/ModelTest/modeltest.h" #include "UiUtils/IconLoader.h" #include "UiUtils/QaimDfsIterator.h" /** @short All user-facing widgets and related classes */ namespace Gui { static const char * const netErrorUnseen = "net_error_unseen"; MainWindow::MainWindow(QSettings *settings): QMainWindow(), m_imapAccess(0), m_mainHSplitter(0), m_mainVSplitter(0), m_mainStack(0), m_layoutMode(LAYOUT_COMPACT), m_skipSavingOfUI(true), m_delayedStateSaving(0), m_actionSortNone(0), m_ignoreStoredPassword(false), m_settings(settings), m_pluginManager(0), m_networkErrorMessageBox(0), m_trayIcon(0) { setAttribute(Qt::WA_AlwaysShowToolTips); // m_pluginManager must be created before calling createWidgets m_pluginManager = new Plugins::PluginManager(this, m_settings, Common::SettingsNames::addressbookPlugin, Common::SettingsNames::passwordPlugin); connect(m_pluginManager, &Plugins::PluginManager::pluginsChanged, this, &MainWindow::slotPluginsChanged); connect(m_pluginManager, &Plugins::PluginManager::pluginError, this, [this](const QString &errorMessage) { Gui::Util::messageBoxWarning(this, tr("Plugin Error"), //: The %1 placeholder is a full error message as provided by Qt, ready for human consumption. trUtf8("A plugin failed to load, therefore some functionality might be lost. " "You might want to update your system or report a bug to your vendor." "\n\n%1").arg(errorMessage)); }); #ifdef TROJITA_HAVE_CRYPTO_MESSAGES Plugins::PluginManager::MimePartReplacers replacers; #ifdef TROJITA_HAVE_GPGMEPP replacers.emplace_back(std::make_shared()); #endif m_pluginManager->setMimePartReplacers(replacers); #endif // ImapAccess contains a wrapper for retrieving passwords through some plugin. // That PasswordWatcher is used by the SettingsDialog's widgets *and* by this class, // which means that ImapAccess has to be constructed before we go and open the settings dialog. // FIXME: use another account-id at some point in future // we are now using the profile to avoid overwriting passwords of // other profiles in secure storage QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); m_imapAccess = new Imap::ImapAccess(this, m_settings, m_pluginManager, profileName); connect(m_imapAccess, &Imap::ImapAccess::cacheError, this, &MainWindow::cacheError); connect(m_imapAccess, &Imap::ImapAccess::checkSslPolicy, this, &MainWindow::checkSslPolicy, Qt::QueuedConnection); ShortcutHandler *shortcutHandler = new ShortcutHandler(this); shortcutHandler->setSettingsObject(m_settings); defineActions(); shortcutHandler->readSettings(); // must happen after defineActions() // must be created before calling createWidgets m_favoriteTags = new Imap::Mailbox::FavoriteTagsModel(this); m_favoriteTags->loadFromSettings(*m_settings); createWidgets(); Imap::migrateSettings(m_settings); m_senderIdentities = new Composer::SenderIdentitiesModel(this); m_senderIdentities->loadFromSettings(*m_settings); if (! m_settings->contains(Common::SettingsNames::imapMethodKey)) { QTimer::singleShot(0, this, SLOT(slotShowSettings())); } setupModels(); createActions(); createMenus(); slotToggleSysTray(); slotPluginsChanged(); slotFavoriteTagsChanged(); connect(m_favoriteTags, &QAbstractItemModel::modelReset, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::layoutChanged, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsMoved, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsInserted, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsRemoved, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::dataChanged, this, &MainWindow::slotFavoriteTagsChanged); // Please note that Qt 4.6.1 really requires passing the method signature this way, *not* using the SLOT() macro QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "slotComposeMailUrl"); QDesktopServices::setUrlHandler(QStringLiteral("x-trojita-manage-contact"), this, "slotManageContact"); slotUpdateWindowTitle(); recoverDrafts(); if (m_actionLayoutWide->isEnabled() && m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutWide) { m_actionLayoutWide->trigger(); } else if (m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutOneAtTime) { m_actionLayoutOneAtTime->trigger(); } else { m_actionLayoutCompact->trigger(); } connect(qApp, &QGuiApplication::applicationStateChanged, this, [&](Qt::ApplicationState state) { if (state == Qt::ApplicationActive && m_networkErrorMessageBox && m_networkErrorMessageBox->property(netErrorUnseen).toBool()) { m_networkErrorMessageBox->setProperty(netErrorUnseen, false); m_networkErrorMessageBox->show(); } }); // Don't listen to QDesktopWidget::resized; that is emitted too early (when it gets fired, the screen size has changed, but // the workspace area is still the old one). Instead, listen to workAreaResized which gets emitted at an appropriate time. // The delay is still there to guarantee some smoothing; on jkt's box there are typically three events in a rapid sequence // (some of them most likely due to the fact that at first, the actual desktop gets resized, the plasma panel reacts // to that and only after the panel gets resized, the available size of "the rest" is correct again). // Which is why it makes sense to introduce some delay in there. The 0.5s delay is my best guess and "should work" (especially // because every change bumps the timer anyway, as Thomas pointed out). QTimer *delayedResize = new QTimer(this); delayedResize->setSingleShot(true); delayedResize->setInterval(500); connect(delayedResize, &QTimer::timeout, this, &MainWindow::desktopGeometryChanged); connect(qApp->desktop(), &QDesktopWidget::workAreaResized, delayedResize, static_cast(&QTimer::start)); m_skipSavingOfUI = false; } void MainWindow::defineActions() { ShortcutHandler *shortcutHandler = ShortcutHandler::instance(); shortcutHandler->defineAction(QStringLiteral("action_application_exit"), QStringLiteral("application-exit"), tr("E&xit"), QKeySequence::Quit); shortcutHandler->defineAction(QStringLiteral("action_compose_mail"), QStringLiteral("document-edit"), tr("&New Message..."), QKeySequence::New); shortcutHandler->defineAction(QStringLiteral("action_compose_draft"), QStringLiteral("document-open-recent"), tr("&Edit Draft...")); shortcutHandler->defineAction(QStringLiteral("action_show_menubar"), QStringLiteral("view-list-text"), tr("Show Main Menu &Bar"), tr("Ctrl+M")); shortcutHandler->defineAction(QStringLiteral("action_expunge"), QStringLiteral("trash-empty"), tr("Exp&unge"), tr("Ctrl+E")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_read"), QStringLiteral("mail-mark-read"), tr("Mark as &Read"), QStringLiteral("M")); shortcutHandler->defineAction(QStringLiteral("action_go_to_next_unread"), QStringLiteral("arrow-right"), tr("&Next Unread Message"), QStringLiteral("N")); shortcutHandler->defineAction(QStringLiteral("action_go_to_previous_unread"), QStringLiteral("arrow-left"), tr("&Previous Unread Message"), QStringLiteral("P")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_deleted"), QStringLiteral("list-remove"), tr("Mark as &Deleted"), QKeySequence(Qt::Key_Delete).toString()); shortcutHandler->defineAction(QStringLiteral("action_mark_as_flagged"), QStringLiteral("mail-flagged"), tr("Mark as &Flagged"), QStringLiteral("S")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_junk"), QStringLiteral("mail-mark-junk"), tr("Mark as &Junk"), QStringLiteral("J")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_notjunk"), QStringLiteral("mail-mark-notjunk"), tr("Mark as Not &junk"), QStringLiteral("Shift+J")); shortcutHandler->defineAction(QStringLiteral("action_save_message_as"), QStringLiteral("document-save"), tr("&Save Message...")); shortcutHandler->defineAction(QStringLiteral("action_view_message_source"), QString(), tr("View Message &Source...")); shortcutHandler->defineAction(QStringLiteral("action_view_message_headers"), QString(), tr("View Message &Headers..."), tr("Ctrl+U")); shortcutHandler->defineAction(QStringLiteral("action_reply_private"), QStringLiteral("mail-reply-sender"), tr("&Private Reply"), tr("Ctrl+Shift+A")); shortcutHandler->defineAction(QStringLiteral("action_reply_all_but_me"), QStringLiteral("mail-reply-all"), tr("Reply to All &but Me"), tr("Ctrl+Shift+R")); shortcutHandler->defineAction(QStringLiteral("action_reply_all"), QStringLiteral("mail-reply-all"), tr("Reply to &All"), tr("Ctrl+Alt+Shift+R")); shortcutHandler->defineAction(QStringLiteral("action_reply_list"), QStringLiteral("mail-reply-list"), tr("Reply to &Mailing List"), tr("Ctrl+L")); shortcutHandler->defineAction(QStringLiteral("action_reply_guess"), QString(), tr("Reply by &Guess"), tr("Ctrl+R")); shortcutHandler->defineAction(QStringLiteral("action_forward_attachment"), QStringLiteral("mail-forward"), tr("&Forward"), tr("Ctrl+Shift+F")); shortcutHandler->defineAction(QStringLiteral("action_bounce"), QStringLiteral("mail-bounce"), tr("Edit as New E-Mail Message...")); shortcutHandler->defineAction(QStringLiteral("action_archive"), QStringLiteral("mail-move-to-archive"), tr("&Archive"), QStringLiteral("A")); shortcutHandler->defineAction(QStringLiteral("action_contact_editor"), QStringLiteral("contact-unknown"), tr("Address Book...")); shortcutHandler->defineAction(QStringLiteral("action_network_offline"), QStringLiteral("network-disconnect"), tr("&Offline")); shortcutHandler->defineAction(QStringLiteral("action_network_expensive"), QStringLiteral("network-wireless"), tr("&Expensive Connection")); shortcutHandler->defineAction(QStringLiteral("action_network_online"), QStringLiteral("network-connect"), tr("&Free Access")); shortcutHandler->defineAction(QStringLiteral("action_messagewindow_close"), QStringLiteral("window-close"), tr("Close Standalone Message Window")); shortcutHandler->defineAction(QStringLiteral("action_open_messagewindow"), QString(), tr("Open message in New Window..."), QStringLiteral("Ctrl+Return")); shortcutHandler->defineAction(QStringLiteral("action_oneattime_go_back"), QStringLiteral("go-previous"), tr("Navigate Back"), QKeySequence(QKeySequence::Back).toString()); shortcutHandler->defineAction(QStringLiteral("action_zoom_in"), QStringLiteral("zoom-in"), tr("Zoom In"), QKeySequence::ZoomIn); shortcutHandler->defineAction(QStringLiteral("action_zoom_out"), QStringLiteral("zoom-out"), tr("Zoom Out"), QKeySequence::ZoomOut); shortcutHandler->defineAction(QStringLiteral("action_zoom_original"), QStringLiteral("zoom-original"), tr("Original Size")); shortcutHandler->defineAction(QStringLiteral("action_focus_mailbox_tree"), QString(), tr("Move Focus to Mailbox List")); shortcutHandler->defineAction(QStringLiteral("action_focus_msg_list"), QString(), tr("Move Focus to Message List")); shortcutHandler->defineAction(QStringLiteral("action_focus_quick_search"), QString(), tr("Move Focus to Quick Search"), QStringLiteral("/")); shortcutHandler->defineAction(QStringLiteral("action_tag_1"), QStringLiteral("mail-tag-1"), tr("Tag with &1st tag"), QStringLiteral("1")); shortcutHandler->defineAction(QStringLiteral("action_tag_2"), QStringLiteral("mail-tag-2"), tr("Tag with &2nd tag"), QStringLiteral("2")); shortcutHandler->defineAction(QStringLiteral("action_tag_3"), QStringLiteral("mail-tag-3"), tr("Tag with &3rd tag"), QStringLiteral("3")); shortcutHandler->defineAction(QStringLiteral("action_tag_4"), QStringLiteral("mail-tag-4"), tr("Tag with &4th tag"), QStringLiteral("4")); shortcutHandler->defineAction(QStringLiteral("action_tag_5"), QStringLiteral("mail-tag-5"), tr("Tag with &5th tag"), QStringLiteral("5")); shortcutHandler->defineAction(QStringLiteral("action_tag_6"), QStringLiteral("mail-tag-6"), tr("Tag with &6th tag"), QStringLiteral("6")); shortcutHandler->defineAction(QStringLiteral("action_tag_7"), QStringLiteral("mail-tag-7"), tr("Tag with &7th tag"), QStringLiteral("7")); shortcutHandler->defineAction(QStringLiteral("action_tag_8"), QStringLiteral("mail-tag-8"), tr("Tag with &8th tag"), QStringLiteral("8")); shortcutHandler->defineAction(QStringLiteral("action_tag_9"), QStringLiteral("mail-tag-9"), tr("Tag with &9th tag"), QStringLiteral("9")); } void MainWindow::createActions() { // The shortcuts are a little bit complicated, unfortunately. This is what the other applications use by default: // // Thunderbird: // private: Ctrl+R // all: Ctrl+Shift+R // list: Ctrl+Shift+L // forward: Ctrl+L // (no shortcuts for type of forwarding) // bounce: ctrl+B // new message: Ctrl+N // // KMail: // "reply": R // private: Shift+A // all: A // list: L // forward as attachment: F // forward inline: Shift+F // bounce: E // new: Ctrl+N m_actionContactEditor = ShortcutHandler::instance()->createAction(QStringLiteral("action_contact_editor"), this, SLOT(invokeContactEditor()), this); m_mainToolbar = addToolBar(tr("Navigation")); m_mainToolbar->setObjectName(QStringLiteral("mainToolbar")); reloadMboxList = new QAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("&Update List of Child Mailboxes"), this); connect(reloadMboxList, &QAction::triggered, this, &MainWindow::slotReloadMboxList); resyncMbox = new QAction(UiUtils::loadIcon(QStringLiteral("view-refresh")), tr("Check for &New Messages"), this); connect(resyncMbox, &QAction::triggered, this, &MainWindow::slotResyncMbox); reloadAllMailboxes = new QAction(tr("&Reload Everything"), this); // connect later exitAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_application_exit"), qApp, SLOT(quit()), this); exitAction->setStatusTip(tr("Exit the application")); netOffline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_offline")); netOffline->setCheckable(true); // connect later netExpensive = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_expensive")); netExpensive->setCheckable(true); // connect later netOnline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_online")); netOnline->setCheckable(true); // connect later QActionGroup *netPolicyGroup = new QActionGroup(this); netPolicyGroup->setExclusive(true); netPolicyGroup->addAction(netOffline); netPolicyGroup->addAction(netExpensive); netPolicyGroup->addAction(netOnline); //: a debugging tool showing the full contents of the whole IMAP server; all folders, messages and their parts showFullView = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find-mail")), tr("Show Full &Tree Window"), this); showFullView->setCheckable(true); connect(showFullView, &QAction::triggered, allDock, &QWidget::setVisible); connect(allDock, &QDockWidget::visibilityChanged, showFullView, &QAction::setChecked); //: list of active "tasks", entities which are performing certain action like downloading a message or syncing a mailbox showTaskView = new QAction(tr("Show ImapTask t&ree"), this); showTaskView->setCheckable(true); connect(showTaskView, &QAction::triggered, taskDock, &QWidget::setVisible); connect(taskDock, &QDockWidget::visibilityChanged, showTaskView, &QAction::setChecked); //: a debugging tool showing the mime tree of the current message showMimeView = new QAction(tr("Show &MIME tree"), this); showMimeView->setCheckable(true); connect(showMimeView, &QAction::triggered, mailMimeDock, &QWidget::setVisible); connect(mailMimeDock, &QDockWidget::visibilityChanged, showMimeView, &QAction::setChecked); showImapLogger = new QAction(tr("Show IMAP protocol &log"), this); showImapLogger->setCheckable(true); connect(showImapLogger, &QAction::toggled, imapLoggerDock, &QWidget::setVisible); connect(imapLoggerDock, &QDockWidget::visibilityChanged, showImapLogger, &QAction::setChecked); //: file to save the debug log into logPersistent = new QAction(tr("Log &into %1").arg(Imap::Mailbox::persistentLogFileName()), this); logPersistent->setCheckable(true); connect(logPersistent, &QAction::triggered, imapLogger, &ProtocolLoggerWidget::slotSetPersistentLogging); connect(imapLogger, &ProtocolLoggerWidget::persistentLoggingChanged, logPersistent, &QAction::setChecked); showImapCapabilities = new QAction(tr("IMAP Server In&formation..."), this); connect(showImapCapabilities, &QAction::triggered, this, &MainWindow::slotShowImapInfo); showMenuBar = ShortcutHandler::instance()->createAction(QStringLiteral("action_show_menubar"), this); showMenuBar->setCheckable(true); showMenuBar->setChecked(true); connect(showMenuBar, &QAction::triggered, menuBar(), &QMenuBar::setVisible); connect(showMenuBar, &QAction::triggered, m_delayedStateSaving, static_cast(&QTimer::start)); showToolBar = new QAction(tr("Show &Toolbar"), this); showToolBar->setCheckable(true); connect(showToolBar, &QAction::triggered, m_mainToolbar, &QWidget::setVisible); connect(m_mainToolbar, &QToolBar::visibilityChanged, showToolBar, &QAction::setChecked); connect(m_mainToolbar, &QToolBar::visibilityChanged, m_delayedStateSaving, static_cast(&QTimer::start)); configSettings = new QAction(UiUtils::loadIcon(QStringLiteral("configure")), tr("&Settings..."), this); connect(configSettings, &QAction::triggered, this, &MainWindow::slotShowSettings); QAction *triggerSearch = new QAction(this); addAction(triggerSearch); triggerSearch->setShortcut(QKeySequence(QStringLiteral(":, ="))); connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusRawSearch); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_quick_search"), msgListWidget, SLOT(focusSearch()), this)); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_mailbox_tree"), mboxTree, SLOT(setFocus()), this)); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_msg_list"), msgListWidget->tree, SLOT(setFocus()), this)); m_oneAtTimeGoBack = ShortcutHandler::instance()->createAction(QStringLiteral("action_oneattime_go_back"), this); m_oneAtTimeGoBack->setEnabled(false); composeMail = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_mail"), this, SLOT(slotComposeMail()), this); m_editDraft = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_draft"), this, SLOT(slotEditDraft()), this); expunge = ShortcutHandler::instance()->createAction(QStringLiteral("action_expunge"), this, SLOT(slotExpunge()), this); m_forwardAsAttachment = ShortcutHandler::instance()->createAction(QStringLiteral("action_forward_attachment"), this, SLOT(slotForwardAsAttachment()), this); m_bounce = ShortcutHandler::instance()->createAction(QStringLiteral("action_bounce"), this, SLOT(slotBounce()), this); markAsRead = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_read"), this); markAsRead->setCheckable(true); msgListWidget->tree->addAction(markAsRead); connect(markAsRead, &QAction::triggered, this, &MainWindow::handleMarkAsRead); m_nextMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_next_unread"), this, SLOT(slotNextUnread()), this); msgListWidget->tree->addAction(m_nextMessage); m_messageWidget->messageView->addAction(m_nextMessage); m_previousMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_previous_unread"), this, SLOT(slotPreviousUnread()), this); msgListWidget->tree->addAction(m_previousMessage); m_messageWidget->messageView->addAction(m_previousMessage); markAsDeleted = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_deleted"), this); markAsDeleted->setCheckable(true); msgListWidget->tree->addAction(markAsDeleted); connect(markAsDeleted, &QAction::triggered, this, &MainWindow::handleMarkAsDeleted); markAsFlagged = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_flagged"), this); markAsFlagged->setCheckable(true); connect(markAsFlagged, &QAction::triggered, this, &MainWindow::handleMarkAsFlagged); markAsJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_junk"), this); markAsJunk->setCheckable(true); connect(markAsJunk, &QAction::triggered, this, &MainWindow::handleMarkAsJunk); markAsNotJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_notjunk"), this); markAsNotJunk->setCheckable(true); connect(markAsNotJunk, &QAction::triggered, this, &MainWindow::handleMarkAsNotJunk); saveWholeMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_save_message_as"), this, SLOT(slotSaveCurrentMessageBody()), this); msgListWidget->tree->addAction(saveWholeMessage); viewMsgSource = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_source"), this, SLOT(slotViewMsgSource()), this); msgListWidget->tree->addAction(viewMsgSource); viewMsgHeaders = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_headers"), this, SLOT(slotViewMsgHeaders()), this); msgListWidget->tree->addAction(viewMsgHeaders); msgListWidget->tree->addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_open_messagewindow"), this, SLOT(openCompleteMessageWidget()), this)); moveToArchive = ShortcutHandler::instance()->createAction(QStringLiteral("action_archive"), this); connect(moveToArchive, &QAction::triggered, this, &MainWindow::handleMoveToArchive); auto addTagAction = [=](int row) { QAction *tag = ShortcutHandler::instance()->createAction(QStringLiteral("action_tag_").append(QString::number(row)), this); tag->setCheckable(true); msgListWidget->tree->addAction(tag); connect(tag, &QAction::triggered, this, [=](const bool checked) { handleTag(checked, row - 1); }); return tag; }; tag1 = addTagAction(1); tag2 = addTagAction(2); tag3 = addTagAction(3); tag4 = addTagAction(4); tag5 = addTagAction(5); tag6 = addTagAction(6); tag7 = addTagAction(7); tag8 = addTagAction(8); tag9 = addTagAction(9); //: "mailbox" as a "folder of messages", not as a "mail account" createChildMailbox = new QAction(tr("Create &Child Mailbox..."), this); connect(createChildMailbox, &QAction::triggered, this, &MainWindow::slotCreateMailboxBelowCurrent); //: "mailbox" as a "folder of messages", not as a "mail account" createTopMailbox = new QAction(tr("Create &New Mailbox..."), this); connect(createTopMailbox, &QAction::triggered, this, &MainWindow::slotCreateTopMailbox); m_actionMarkMailboxAsRead = new QAction(tr("&Mark Mailbox as Read"), this); connect(m_actionMarkMailboxAsRead, &QAction::triggered, this, &MainWindow::slotMarkCurrentMailboxRead); //: "mailbox" as a "folder of messages", not as a "mail account" deleteCurrentMailbox = new QAction(tr("&Remove Mailbox"), this); connect(deleteCurrentMailbox, &QAction::triggered, this, &MainWindow::slotDeleteCurrentMailbox); #ifdef XTUPLE_CONNECT xtIncludeMailboxInSync = new QAction(tr("&Synchronize with xTuple"), this); xtIncludeMailboxInSync->setCheckable(true); connect(xtIncludeMailboxInSync, SIGNAL(triggered()), this, SLOT(slotXtSyncCurrentMailbox())); #endif m_replyPrivate = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_private"), this, SLOT(slotReplyTo()), this); m_replyPrivate->setEnabled(false); m_replyAllButMe = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all_but_me"), this, SLOT(slotReplyAllButMe()), this); m_replyAllButMe->setEnabled(false); m_replyAll = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all"), this, SLOT(slotReplyAll()), this); m_replyAll->setEnabled(false); m_replyList = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_list"), this, SLOT(slotReplyList()), this); m_replyList->setEnabled(false); m_replyGuess = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_guess"), this, SLOT(slotReplyGuess()), this); m_replyGuess->setEnabled(true); actionThreadMsgList = new QAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Show Messages in &Threads"), this); actionThreadMsgList->setCheckable(true); // This action is enabled/disabled by model's capabilities actionThreadMsgList->setEnabled(false); if (m_settings->value(Common::SettingsNames::guiMsgListShowThreading).toBool()) { actionThreadMsgList->setChecked(true); // The actual threading will be performed only when model updates its capabilities } connect(actionThreadMsgList, &QAction::triggered, this, &MainWindow::slotThreadMsgList); QActionGroup *sortOrderGroup = new QActionGroup(this); m_actionSortAscending = new QAction(tr("&Ascending"), sortOrderGroup); m_actionSortAscending->setCheckable(true); m_actionSortAscending->setChecked(true); m_actionSortDescending = new QAction(tr("&Descending"), sortOrderGroup); m_actionSortDescending->setCheckable(true); // QActionGroup has no toggle signal, but connecting descending will implicitly catch the acscending complement ;-) connect(m_actionSortDescending, &QAction::toggled, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_actionSortDescending, &QAction::toggled, this, &MainWindow::slotScrollToCurrent); connect(sortOrderGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged); QActionGroup *sortColumnGroup = new QActionGroup(this); m_actionSortNone = new QAction(tr("&No sorting"), sortColumnGroup); m_actionSortNone->setCheckable(true); m_actionSortThreading = new QAction(tr("Sorted by &Threading"), sortColumnGroup); m_actionSortThreading->setCheckable(true); m_actionSortByArrival = new QAction(tr("A&rrival"), sortColumnGroup); m_actionSortByArrival->setCheckable(true); m_actionSortByCc = new QAction(tr("&Cc (Carbon Copy)"), sortColumnGroup); m_actionSortByCc->setCheckable(true); m_actionSortByDate = new QAction(tr("Date from &Message Headers"), sortColumnGroup); m_actionSortByDate->setCheckable(true); m_actionSortByFrom = new QAction(tr("&From Address"), sortColumnGroup); m_actionSortByFrom->setCheckable(true); m_actionSortBySize = new QAction(tr("&Size"), sortColumnGroup); m_actionSortBySize->setCheckable(true); m_actionSortBySubject = new QAction(tr("S&ubject"), sortColumnGroup); m_actionSortBySubject->setCheckable(true); m_actionSortByTo = new QAction(tr("T&o Address"), sortColumnGroup); m_actionSortByTo->setCheckable(true); connect(sortColumnGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged); slotSortingConfirmed(-1, Qt::AscendingOrder); actionHideRead = new QAction(tr("&Hide Read Messages"), this); actionHideRead->setCheckable(true); if (m_settings->value(Common::SettingsNames::guiMsgListHideRead).toBool()) { actionHideRead->setChecked(true); prettyMsgListModel->setHideRead(true); } connect(actionHideRead, &QAction::triggered, this, &MainWindow::slotHideRead); QActionGroup *layoutGroup = new QActionGroup(this); m_actionLayoutCompact = new QAction(tr("&Compact"), layoutGroup); m_actionLayoutCompact->setCheckable(true); m_actionLayoutCompact->setChecked(true); connect(m_actionLayoutCompact, &QAction::triggered, this, &MainWindow::slotLayoutCompact); m_actionLayoutWide = new QAction(tr("&Wide"), layoutGroup); m_actionLayoutWide->setCheckable(true); connect(m_actionLayoutWide, &QAction::triggered, this, &MainWindow::slotLayoutWide); m_actionLayoutOneAtTime = new QAction(tr("&One At Time"), layoutGroup); m_actionLayoutOneAtTime->setCheckable(true); connect(m_actionLayoutOneAtTime, &QAction::triggered, this, &MainWindow::slotLayoutOneAtTime); m_actionShowOnlySubscribed = new QAction(tr("Show Only S&ubscribed Folders"), this); m_actionShowOnlySubscribed->setCheckable(true); m_actionShowOnlySubscribed->setEnabled(false); connect(m_actionShowOnlySubscribed, &QAction::toggled, this, &MainWindow::slotShowOnlySubscribed); m_actionSubscribeMailbox = new QAction(tr("Su&bscribed"), this); m_actionSubscribeMailbox->setCheckable(true); m_actionSubscribeMailbox->setEnabled(false); connect(m_actionSubscribeMailbox, &QAction::triggered, this, &MainWindow::slotSubscribeCurrentMailbox); aboutTrojita = new QAction(trUtf8("&About Trojitá..."), this); connect(aboutTrojita, &QAction::triggered, this, &MainWindow::slotShowAboutTrojita); donateToTrojita = new QAction(tr("&Donate to the project"), this); connect(donateToTrojita, &QAction::triggered, this, &MainWindow::slotDonateToTrojita); connectModelActions(); m_composeMenu = new QMenu(tr("Compose Mail"), this); m_composeMenu->addAction(composeMail); m_composeMenu->addAction(m_editDraft); m_composeButton = new QToolButton(this); m_composeButton->setPopupMode(QToolButton::MenuButtonPopup); m_composeButton->setMenu(m_composeMenu); m_composeButton->setDefaultAction(composeMail); m_replyButton = new QToolButton(this); m_replyButton->setPopupMode(QToolButton::MenuButtonPopup); m_replyMenu = new QMenu(m_replyButton); m_replyMenu->addAction(m_replyPrivate); m_replyMenu->addAction(m_replyAllButMe); m_replyMenu->addAction(m_replyAll); m_replyMenu->addAction(m_replyList); m_replyButton->setMenu(m_replyMenu); m_replyButton->setDefaultAction(m_replyPrivate); m_mainToolbar->addWidget(m_composeButton); m_mainToolbar->addWidget(m_replyButton); m_mainToolbar->addAction(m_forwardAsAttachment); m_mainToolbar->addAction(expunge); m_mainToolbar->addSeparator(); m_mainToolbar->addAction(markAsRead); m_mainToolbar->addAction(markAsDeleted); m_mainToolbar->addAction(markAsFlagged); m_mainToolbar->addAction(markAsJunk); m_mainToolbar->addAction(markAsNotJunk); m_mainToolbar->addAction(moveToArchive); // Push the status indicators all the way to the other side of the toolbar -- either to the far right, or far bottom. QWidget *toolbarSpacer = new QWidget(m_mainToolbar); toolbarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_mainToolbar->addWidget(toolbarSpacer); m_mainToolbar->addSeparator(); m_mainToolbar->addWidget(busyParsersIndicator); networkIndicator = new QToolButton(this); // This is deliberate; we want to show this button in the same style as the other ones in the toolbar networkIndicator->setPopupMode(QToolButton::MenuButtonPopup); m_mainToolbar->addWidget(networkIndicator); m_menuFromToolBar = new QToolButton(this); m_menuFromToolBar->setIcon(UiUtils::loadIcon(QStringLiteral("menu_new"))); m_menuFromToolBar->setText(QChar(0x205d)); // Unicode 'TRICOLON' m_menuFromToolBar->setPopupMode(QToolButton::MenuButtonPopup); connect(m_menuFromToolBar, &QAbstractButton::clicked, m_menuFromToolBar, &QToolButton::showMenu); m_mainToolbar->addWidget(m_menuFromToolBar); connect(showMenuBar, &QAction::toggled, [this](const bool menuBarVisible) { // https://bugreports.qt.io/browse/QTBUG-35768 , we have to work on the QAction, not QToolButton m_mainToolbar->actions().last()->setVisible(!menuBarVisible); }); m_mainToolbar->actions().last()->setVisible(false); // initial state to complement the default of the QMenuBar's visibility busyParsersIndicator->setFixedSize(m_mainToolbar->iconSize()); { // Custom widgets which are added into a QToolBar are by default aligned to the left, while QActions are justified. // That sucks, because some of our widgets use multiple actions with an expanding arrow at right. // Make sure everything is aligned to the left, so that the actual buttons are aligned properly and the extra arrows // are, well, at right. // I have no idea how this works on RTL layouts. QLayout *lay = m_mainToolbar->layout(); for (int i = 0; i < lay->count(); ++i) { QLayoutItem *it = lay->itemAt(i); if (it->widget() == toolbarSpacer) { // Don't align this one, otherwise it won't push stuff when in horizontal direction continue; } if (it->widget() == busyParsersIndicator) { // It looks much better when centered it->setAlignment(Qt::AlignJustify); continue; } it->setAlignment(Qt::AlignLeft); } } updateMessageFlags(); } void MainWindow::connectModelActions() { connect(reloadAllMailboxes, &QAction::triggered, imapModel(), &Imap::Mailbox::Model::reloadMailboxList); connect(netOffline, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOffline); connect(netExpensive, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkExpensive); connect(netOnline, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOnline); netExpensive->setEnabled(imapAccess()->isConfigured()); netOnline->setEnabled(imapAccess()->isConfigured()); } void MainWindow::createMenus() { #define ADD_ACTION(MENU, ACTION) \ MENU->addAction(ACTION); \ addAction(ACTION); QMenu *imapMenu = menuBar()->addMenu(tr("&IMAP")); imapMenu->addMenu(m_composeMenu); ADD_ACTION(imapMenu, m_actionContactEditor); ADD_ACTION(imapMenu, m_replyGuess); ADD_ACTION(imapMenu, m_replyPrivate); ADD_ACTION(imapMenu, m_replyAll); ADD_ACTION(imapMenu, m_replyAllButMe); ADD_ACTION(imapMenu, m_replyList); imapMenu->addSeparator(); ADD_ACTION(imapMenu, m_forwardAsAttachment); ADD_ACTION(imapMenu, m_bounce); imapMenu->addSeparator(); ADD_ACTION(imapMenu, expunge); imapMenu->addSeparator()->setText(tr("Network Access")); QMenu *netPolicyMenu = imapMenu->addMenu(tr("&Network Access")); ADD_ACTION(netPolicyMenu, netOffline); ADD_ACTION(netPolicyMenu, netExpensive); ADD_ACTION(netPolicyMenu, netOnline); QMenu *debugMenu = imapMenu->addMenu(tr("&Debugging")); ADD_ACTION(debugMenu, showFullView); ADD_ACTION(debugMenu, showTaskView); ADD_ACTION(debugMenu, showMimeView); ADD_ACTION(debugMenu, showImapLogger); ADD_ACTION(debugMenu, logPersistent); ADD_ACTION(debugMenu, showImapCapabilities); imapMenu->addSeparator(); ADD_ACTION(imapMenu, configSettings); ADD_ACTION(imapMenu, ShortcutHandler::instance()->shortcutConfigAction()); imapMenu->addSeparator(); ADD_ACTION(imapMenu, exitAction); QMenu *viewMenu = menuBar()->addMenu(tr("&View")); ADD_ACTION(viewMenu, showMenuBar); ADD_ACTION(viewMenu, showToolBar); QMenu *layoutMenu = viewMenu->addMenu(tr("&Layout")); ADD_ACTION(layoutMenu, m_actionLayoutCompact); ADD_ACTION(layoutMenu, m_actionLayoutWide); ADD_ACTION(layoutMenu, m_actionLayoutOneAtTime); viewMenu->addSeparator(); ADD_ACTION(viewMenu, m_previousMessage); ADD_ACTION(viewMenu, m_nextMessage); viewMenu->addSeparator(); QMenu *sortMenu = viewMenu->addMenu(tr("S&orting")); ADD_ACTION(sortMenu, m_actionSortNone); ADD_ACTION(sortMenu, m_actionSortThreading); ADD_ACTION(sortMenu, m_actionSortByArrival); ADD_ACTION(sortMenu, m_actionSortByCc); ADD_ACTION(sortMenu, m_actionSortByDate); ADD_ACTION(sortMenu, m_actionSortByFrom); ADD_ACTION(sortMenu, m_actionSortBySize); ADD_ACTION(sortMenu, m_actionSortBySubject); ADD_ACTION(sortMenu, m_actionSortByTo); sortMenu->addSeparator(); ADD_ACTION(sortMenu, m_actionSortAscending); ADD_ACTION(sortMenu, m_actionSortDescending); ADD_ACTION(viewMenu, actionThreadMsgList); ADD_ACTION(viewMenu, actionHideRead); ADD_ACTION(viewMenu, m_actionShowOnlySubscribed); QMenu *mailboxMenu = menuBar()->addMenu(tr("&Mailbox")); ADD_ACTION(mailboxMenu, resyncMbox); mailboxMenu->addSeparator(); ADD_ACTION(mailboxMenu, reloadAllMailboxes); QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); ADD_ACTION(helpMenu, donateToTrojita); helpMenu->addSeparator(); ADD_ACTION(helpMenu, aboutTrojita); QMenu *mainMenuBehindToolBar = new QMenu(this); m_menuFromToolBar->setMenu(mainMenuBehindToolBar); m_menuFromToolBar->menu()->addMenu(imapMenu); m_menuFromToolBar->menu()->addMenu(viewMenu); m_menuFromToolBar->menu()->addMenu(mailboxMenu); m_menuFromToolBar->menu()->addMenu(helpMenu); m_menuFromToolBar->menu()->addSeparator(); m_menuFromToolBar->menu()->addAction(showMenuBar); networkIndicator->setMenu(netPolicyMenu); m_netToolbarDefaultAction = new QAction(this); networkIndicator->setDefaultAction(m_netToolbarDefaultAction); connect(m_netToolbarDefaultAction, &QAction::triggered, networkIndicator, &QToolButton::showMenu); connect(netOffline, &QAction::toggled, this, &MainWindow::updateNetworkIndication); connect(netExpensive, &QAction::toggled, this, &MainWindow::updateNetworkIndication); connect(netOnline, &QAction::toggled, this, &MainWindow::updateNetworkIndication); #undef ADD_ACTION } void MainWindow::createWidgets() { // The state of the GUI is only saved after a certain time has passed. This is just an optimization to make sure // we do not hit the disk continually when e.g. resizing some random widget. m_delayedStateSaving = new QTimer(this); m_delayedStateSaving->setInterval(1000); m_delayedStateSaving->setSingleShot(true); connect(m_delayedStateSaving, &QTimer::timeout, this, &MainWindow::saveSizesAndState); mboxTree = new MailBoxTreeView(); mboxTree->setDesiredExpansion(m_settings->value(Common::SettingsNames::guiExpandedMailboxes).toStringList()); connect(mboxTree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMboxTree); connect(mboxTree, &MailBoxTreeView::mailboxExpansionChanged, this, [this](const QStringList &mailboxNames) { m_settings->setValue(Common::SettingsNames::guiExpandedMailboxes, mailboxNames); }); msgListWidget = new MessageListWidget(nullptr, m_favoriteTags); msgListWidget->tree->setContextMenuPolicy(Qt::CustomContextMenu); msgListWidget->tree->setAlternatingRowColors(true); msgListWidget->setRawSearchEnabled(m_settings->value(Common::SettingsNames::guiAllowRawSearch).toBool()); connect (msgListWidget, &MessageListWidget::rawSearchSettingChanged, this, &MainWindow::saveRawStateSetting); connect(msgListWidget->tree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMsgListTree); connect(msgListWidget->tree, &QAbstractItemView::activated, this, &MainWindow::msgListClicked); connect(msgListWidget->tree, &QAbstractItemView::clicked, this, &MainWindow::msgListClicked); connect(msgListWidget->tree, &QAbstractItemView::doubleClicked, this, &MainWindow::openCompleteMessageWidget); connect(msgListWidget, &MessageListWidget::requestingSearch, this, &MainWindow::slotSearchRequested); connect(msgListWidget->tree->header(), &QHeaderView::sectionMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(msgListWidget->tree->header(), &QHeaderView::sectionResized, m_delayedStateSaving, static_cast(&QTimer::start)); msgListWidget->tree->installEventFilter(this); m_messageWidget = new CompleteMessageWidget(this, m_settings, m_pluginManager, m_favoriteTags); connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::scrollMessageUp); connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::slotUpdateMessageActions); #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) connect(m_messageWidget->messageView, &MessageView::linkHovered, [](const QString &url) { if (url.isEmpty()) { QToolTip::hideText(); } else { // indirection due to https://bugs.kde.org/show_bug.cgi?id=363783 QTimer::singleShot(250, [url]() { QToolTip::showText(QCursor::pos(), QObject::tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(url))); }); } }); #endif connect(m_messageWidget->messageView, &MessageView::transferError, this, &MainWindow::slotDownloadTransferError); // Do not try to get onto the homepage when we are on EXPENSIVE connection if (m_settings->value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool() && m_imapAccess->preferredNetworkPolicy() == Imap::Mailbox::NETWORK_ONLINE) { m_messageWidget->messageView->setHomepageUrl(QUrl(QStringLiteral("http://welcome.trojita.flaska.net/%1").arg(Common::Application::version))); } allDock = new QDockWidget(tr("Everything"), this); allDock->setObjectName(QStringLiteral("allDock")); allTree = new QTreeView(allDock); allDock->hide(); allTree->setUniformRowHeights(true); allTree->setHeaderHidden(true); allDock->setWidget(allTree); addDockWidget(Qt::LeftDockWidgetArea, allDock); taskDock = new QDockWidget(tr("IMAP Tasks"), this); taskDock->setObjectName(QStringLiteral("taskDock")); taskTree = new QTreeView(taskDock); taskDock->hide(); taskTree->setHeaderHidden(true); taskDock->setWidget(taskTree); addDockWidget(Qt::LeftDockWidgetArea, taskDock); mailMimeDock = new QDockWidget(tr("MIME Tree"), this); mailMimeDock->setObjectName(QStringLiteral("mailMimeDock")); mailMimeTree = new QTreeView(mailMimeDock); mailMimeDock->hide(); mailMimeTree->setUniformRowHeights(true); mailMimeTree->setHeaderHidden(true); mailMimeDock->setWidget(mailMimeTree); addDockWidget(Qt::RightDockWidgetArea, mailMimeDock); connect(m_messageWidget->messageView, &MessageView::messageModelChanged, this, &MainWindow::slotMessageModelChanged); imapLoggerDock = new QDockWidget(tr("IMAP Protocol"), this); imapLoggerDock->setObjectName(QStringLiteral("imapLoggerDock")); imapLogger = new ProtocolLoggerWidget(imapLoggerDock); imapLoggerDock->hide(); imapLoggerDock->setWidget(imapLogger); addDockWidget(Qt::BottomDockWidgetArea, imapLoggerDock); busyParsersIndicator = new TaskProgressIndicator(this); } void MainWindow::setupModels() { m_imapAccess->reloadConfiguration(); m_imapAccess->doConnect(); m_messageWidget->messageView->setNetworkWatcher(qobject_cast(m_imapAccess->networkWatcher())); auto realThreadingModel = qobject_cast(m_imapAccess->threadingMsgListModel()); Q_ASSERT(realThreadingModel); auto realMsgListModel = qobject_cast(m_imapAccess->msgListModel()); Q_ASSERT(realMsgListModel); prettyMboxModel = new Imap::Mailbox::PrettyMailboxModel(this, qobject_cast(m_imapAccess->mailboxModel())); prettyMboxModel->setObjectName(QStringLiteral("prettyMboxModel")); connect(realThreadingModel, &Imap::Mailbox::ThreadingMsgListModel::sortingFailed, msgListWidget, &MessageListWidget::slotSortingFailed); prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this); prettyMsgListModel->setSourceModel(m_imapAccess->threadingMsgListModel()); prettyMsgListModel->setObjectName(QStringLiteral("prettyMsgListModel")); connect(mboxTree, &MailBoxTreeView::clicked, realMsgListModel, static_cast(&Imap::Mailbox::MsgListModel::setMailbox)); connect(mboxTree, &MailBoxTreeView::activated, realMsgListModel, static_cast(&Imap::Mailbox::MsgListModel::setMailbox)); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::dataChanged, this, &MainWindow::updateMessageFlags); connect(qobject_cast(m_imapAccess->msgListModel()), &Imap::Mailbox::MsgListModel::messagesAvailable, this, &MainWindow::slotScrollToUnseenMessage); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsInserted, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateMessageFlags); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, this, &MainWindow::updateMessageFlags); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::updateMessageFlags); connect(realMsgListModel, &Imap::Mailbox::MsgListModel::mailboxChanged, this, &MainWindow::slotMailboxChanged); connect(imapModel(), &Imap::Mailbox::Model::alertReceived, this, &MainWindow::alertReceived); connect(imapModel(), &Imap::Mailbox::Model::imapError, this, &MainWindow::imapError); connect(imapModel(), &Imap::Mailbox::Model::networkError, this, &MainWindow::networkError); connect(imapModel(), &Imap::Mailbox::Model::authRequested, this, &MainWindow::authenticationRequested, Qt::QueuedConnection); connect(imapModel(), &Imap::Mailbox::Model::authAttemptFailed, this, [this]() { m_ignoreStoredPassword = true; }); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOffline, this, &MainWindow::networkPolicyOffline); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyExpensive, this, &MainWindow::networkPolicyExpensive); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOnline, this, &MainWindow::networkPolicyOnline); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, [this](uint, const Imap::ConnectionState state) { if (state == Imap::CONN_STATE_AUTHENTICATED) { m_ignoreStoredPassword = false; } }); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, &MainWindow::showConnectionStatus); connect(imapModel(), &Imap::Mailbox::Model::mailboxDeletionFailed, this, &MainWindow::slotMailboxDeleteFailed); connect(imapModel(), &Imap::Mailbox::Model::mailboxCreationFailed, this, &MainWindow::slotMailboxCreateFailed); connect(imapModel(), &Imap::Mailbox::Model::mailboxSyncFailed, this, &MainWindow::slotMailboxSyncFailed); connect(imapModel(), &Imap::Mailbox::Model::logged, imapLogger, &ProtocolLoggerWidget::log); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, imapLogger, &ProtocolLoggerWidget::onConnectionClosed); auto nw = qobject_cast(m_imapAccess->networkWatcher()); Q_ASSERT(nw); connect(nw, &Imap::Mailbox::NetworkWatcher::reconnectAttemptScheduled, this, [this](const int timeout) { showStatusMessage(tr("Attempting to reconnect in %n seconds..", 0, timeout/1000)); }); connect(nw, &Imap::Mailbox::NetworkWatcher::resetReconnectState, this, &MainWindow::slotResetReconnectState); connect(imapModel(), &Imap::Mailbox::Model::mailboxFirstUnseenMessage, this, &MainWindow::slotScrollToUnseenMessage); connect(imapModel(), &Imap::Mailbox::Model::capabilitiesUpdated, this, &MainWindow::slotCapabilitiesUpdated); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::slotUpdateWindowTitle); connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::slotUpdateWindowTitle); connect(prettyMsgListModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MainWindow::slotSortingConfirmed); //Imap::Mailbox::ModelWatcher* w = new Imap::Mailbox::ModelWatcher( this ); //w->setModel( imapModel() ); //ModelTest* tester = new ModelTest( prettyMboxModel, this ); // when testing, test just one model at time mboxTree->setModel(prettyMboxModel); msgListWidget->tree->setModel(prettyMsgListModel); connect(msgListWidget->tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateMessageFlags); allTree->setModel(imapModel()); taskTree->setModel(imapModel()->taskModel()); connect(imapModel()->taskModel(), &QAbstractItemModel::layoutChanged, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::modelReset, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsInserted, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsRemoved, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsMoved, taskTree, &QTreeView::expandAll); busyParsersIndicator->setImapModel(imapModel()); auto accountIconName = m_settings->value(Common::SettingsNames::imapAccountIcon).toString(); if (accountIconName.isEmpty()) { qApp->setWindowIcon(UiUtils::loadIcon(QStringLiteral("trojita"))); } else if (accountIconName.contains(QDir::separator())) { // Absolute paths are OK for users, but unsupported by our icon loader qApp->setWindowIcon(QIcon(accountIconName)); } else { qApp->setWindowIcon(UiUtils::loadIcon(accountIconName)); } } void MainWindow::createSysTray() { if (m_trayIcon) return; qApp->setQuitOnLastWindowClosed(false); m_trayIcon = new QSystemTrayIcon(this); handleTrayIconChange(); QAction* quitAction = new QAction(tr("&Quit"), m_trayIcon); connect(quitAction, &QAction::triggered, qApp, &QApplication::quit); QMenu *trayIconMenu = new QMenu(this); trayIconMenu->addAction(quitAction); m_trayIcon->setContextMenu(trayIconMenu); // QMenu cannot be a child of QSystemTrayIcon, and we don't want the QMenu in MainWindow scope. connect(m_trayIcon, &QObject::destroyed, trayIconMenu, &QObject::deleteLater); connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::slotIconActivated); connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::handleTrayIconChange); m_trayIcon->setVisible(true); m_trayIcon->show(); } void MainWindow::removeSysTray() { delete m_trayIcon; m_trayIcon = 0; qApp->setQuitOnLastWindowClosed(true); } void MainWindow::slotToggleSysTray() { bool showSystray = m_settings->value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool(); if (showSystray && !m_trayIcon && QSystemTrayIcon::isSystemTrayAvailable()) { createSysTray(); } else if (!showSystray && m_trayIcon) { removeSysTray(); } } void MainWindow::handleTrayIconChange() { if (!m_trayIcon) return; const bool isOffline = qobject_cast(m_imapAccess->networkWatcher())->effectiveNetworkPolicy() == Imap::Mailbox::NETWORK_OFFLINE; auto pixmap = qApp->windowIcon() .pixmap(QSize(32, 32), isOffline ? QIcon::Disabled : QIcon::Normal); QString tooltip; auto profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (profileName.isEmpty()) { tooltip = QStringLiteral("Trojitá"); } else { tooltip = QStringLiteral("Trojitá [%1]").arg(profileName); } uint unreadCount = 0; bool numbersValid = false; auto watchingMode = settings()->value(Common::SettingsNames::watchedFoldersKey).toString(); if (watchingMode == Common::SettingsNames::watchAll || watchingMode == Common::SettingsNames::watchSubscribed) { bool subscribedOnly = watchingMode == Common::SettingsNames::watchSubscribed; unreadCount = std::accumulate(UiUtils::QaimDfsIterator(m_imapAccess->mailboxModel()->index(0, 0)), UiUtils::QaimDfsIterator(), 0, [subscribedOnly](const uint acc, const QModelIndex &idx) { if (subscribedOnly && !idx.data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool()) return acc; auto x = idx.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(); if (x > 0) { return acc + x; } else { return acc; } }); // only show stuff if there are some mailboxes, and if there are such messages numbersValid = m_imapAccess->mailboxModel()->hasChildren() && unreadCount > 0; } else { // just for the INBOX QModelIndex mailbox = imapModel()->index(1, 0, QModelIndex()); if (mailbox.isValid() && mailbox.data(Imap::Mailbox::RoleMailboxName).toString() == QLatin1String("INBOX") && mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt() > 0) { unreadCount = mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(); numbersValid = true; } } if (numbersValid) { QFont f; f.setPixelSize(pixmap.height() * 0.59); f.setWeight(QFont::Bold); QString text = QString::number(unreadCount); QFontMetrics fm(f); if (unreadCount > 666) { // You just have too many messages. text = QStringLiteral("🐮"); fm = QFontMetrics(f); } else if (fm.width(text) > pixmap.width()) { f.setPixelSize(f.pixelSize() * pixmap.width() / fm.width(text)); fm = QFontMetrics(f); } QRect boundingRect = fm.tightBoundingRect(text); boundingRect.setWidth(boundingRect.width() + 2); boundingRect.setHeight(boundingRect.height() + 2); boundingRect.moveCenter(QPoint(pixmap.width() / 2, pixmap.height() / 2)); boundingRect = boundingRect.intersected(pixmap.rect()); QPainterPath path; path.addText(boundingRect.bottomLeft(), f, text); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(QColor(255,255,255, 180)); painter.setBrush(isOffline ? Qt::red : Qt::black); painter.drawPath(path); //: This is a tooltip for the tray icon. It will be prefixed by something like "Trojita" or "Trojita [work]" tooltip += trUtf8(" - %n unread message(s)", 0, unreadCount); } else if (isOffline) { //: A tooltip suffix when offline. The prefix is something like "Trojita" or "Trojita [work]" tooltip += tr(" - offline"); } m_trayIcon->setToolTip(tooltip); m_trayIcon->setIcon(QIcon(pixmap)); } void MainWindow::closeEvent(QCloseEvent *event) { if (m_trayIcon && m_trayIcon->isVisible()) { Util::askForSomethingUnlessTold(trUtf8("Trojitá"), tr("The application will continue in systray. This can be disabled within the settings."), Common::SettingsNames::guiOnSystrayClose, QMessageBox::Ok, this, m_settings); hide(); event->ignore(); } } bool MainWindow::eventFilter(QObject *o, QEvent *e) { if (msgListWidget && o == msgListWidget->tree && m_messageWidget->messageView) { if (e->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(e); if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace) { QCoreApplication::sendEvent(m_messageWidget, keyEvent); return true; } return false; } return false; } if (msgListWidget && msgListWidget->tree && o == msgListWidget->tree->header()->viewport()) { // installed if sorting is not really possible. QWidget *header = static_cast(o); QMouseEvent *mouse = static_cast(e); if (e->type() == QEvent::MouseButtonPress) { if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor) { m_headerDragStart = mouse->pos(); } return false; } if (e->type() == QEvent::MouseButtonRelease) { if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor && (m_headerDragStart - mouse->pos()).manhattanLength() < QApplication::startDragDistance()) { m_actionSortDescending->toggle(); Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder; msgListWidget->tree->header()->setSortIndicator(-1, order); return true; // prevent regular click } } } return false; } void MainWindow::slotIconActivated(const QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { setVisible(!isVisible()); if (isVisible()) showMainWindow(); } } void MainWindow::showMainWindow() { setVisible(true); activateWindow(); raise(); } void MainWindow::msgListClicked(const QModelIndex &index) { Q_ASSERT(index.isValid()); if (qApp->keyboardModifiers() & Qt::ShiftModifier || qApp->keyboardModifiers() & Qt::ControlModifier) return; if (! index.data(Imap::Mailbox::RoleMessageUid).isValid()) return; // Because it's quite possible that we have switched into another mailbox, make sure that we're in the "current" one so that // user will be notified about new arrivals, etc. QModelIndex translated = Imap::deproxifiedIndex(index); imapModel()->switchToMailbox(translated.parent().parent()); if (index.column() == Imap::Mailbox::MsgListModel::SEEN) { if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool()) return; Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() ? Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD; imapModel()->markMessagesRead(QModelIndexList() << translated, flagOp); if (translated == m_messageWidget->messageView->currentMessage()) { m_messageWidget->messageView->stopAutoMarkAsRead(); } } else if (index.column() == Imap::Mailbox::MsgListModel::FLAGGED) { if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool()) return; Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedFlagged).toBool() ? Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD; imapModel()->setMessageFlags(QModelIndexList() << translated, Imap::Mailbox::FlagNames::flagged, flagOp); } else { if ((m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) || m_layoutMode == LAYOUT_ONE_AT_TIME) { // isVisible() won't work, the splitter manipulates width, not the visibility state m_messageWidget->messageView->setMessage(index); } msgListWidget->tree->setCurrentIndex(index); } } void MainWindow::openCompleteMessageWidget() { const QModelIndex index = msgListWidget->tree->currentIndex(); if (! index.data(Imap::Mailbox::RoleMessageUid).isValid()) return; CompleteMessageWidget *widget = new CompleteMessageWidget(0, m_settings, m_pluginManager, m_favoriteTags); widget->messageView->setMessage(index); widget->messageView->setNetworkWatcher(qobject_cast(m_imapAccess->networkWatcher())); widget->setFocusPolicy(Qt::StrongFocus); widget->setWindowTitle(index.data(Imap::Mailbox::RoleMessageSubject).toString()); widget->setAttribute(Qt::WA_DeleteOnClose); QAction *closeAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_messagewindow_close"), widget, SLOT(close()), widget); widget->addAction(closeAction); widget->show(); } void MainWindow::showContextMenuMboxTree(const QPoint &position) { QList actionList; if (mboxTree->indexAt(position).isValid()) { actionList.append(createChildMailbox); actionList.append(deleteCurrentMailbox); actionList.append(m_actionMarkMailboxAsRead); actionList.append(resyncMbox); actionList.append(reloadMboxList); actionList.append(m_actionSubscribeMailbox); m_actionSubscribeMailbox->setChecked(mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool()); #ifdef XTUPLE_CONNECT actionList.append(xtIncludeMailboxInSync); xtIncludeMailboxInSync->setChecked( m_settings->value(Common::SettingsNames::xtSyncMailboxList).toStringList().contains( mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxName).toString())); #endif } else { actionList.append(createTopMailbox); } actionList.append(reloadAllMailboxes); actionList.append(m_actionShowOnlySubscribed); QMenu::exec(actionList, mboxTree->mapToGlobal(position), nullptr, this); } void MainWindow::showContextMenuMsgListTree(const QPoint &position) { QList actionList; QModelIndex index = msgListWidget->tree->indexAt(position); if (index.isValid()) { updateMessageFlagsOf(index); actionList.append(markAsRead); actionList.append(markAsDeleted); actionList.append(markAsFlagged); actionList.append(markAsJunk); actionList.append(markAsNotJunk); actionList.append(moveToArchive); actionList.append(m_actionMarkMailboxAsRead); actionList.append(saveWholeMessage); actionList.append(viewMsgSource); actionList.append(viewMsgHeaders); auto appendTagIfExists = [this,&actionList](const int row, QAction *tag) { if (m_favoriteTags->rowCount() > row - 1) actionList.append(tag); }; appendTagIfExists(1, tag1); appendTagIfExists(2, tag2); appendTagIfExists(3, tag3); appendTagIfExists(4, tag4); appendTagIfExists(5, tag5); appendTagIfExists(6, tag6); appendTagIfExists(7, tag7); appendTagIfExists(8, tag8); appendTagIfExists(9, tag9); } if (! actionList.isEmpty()) QMenu::exec(actionList, msgListWidget->tree->mapToGlobal(position), nullptr, this); } /** @short Ask for an updated list of mailboxes situated below the selected one */ void MainWindow::slotReloadMboxList() { Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; Imap::Mailbox::TreeItemMailbox *mbox = dynamic_cast( Imap::Mailbox::Model::realTreeItem(item) ); Q_ASSERT(mbox); mbox->rescanForChildMailboxes(imapModel()); } } /** @short Request a check for new messages in selected mailbox */ void MainWindow::slotResyncMbox() { if (! imapModel()->isNetworkAvailable()) return; Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; imapModel()->resyncMailbox(item); } } void MainWindow::alertReceived(const QString &message) { //: "ALERT" is a special warning which we're required to show to the user Gui::Util::messageBoxWarning(this, tr("IMAP Alert"), message); } void MainWindow::imapError(const QString &message) { Gui::Util::messageBoxCritical(this, tr("IMAP Protocol Error"), message); // Show the IMAP logger -- maybe some user will take that as a hint that they shall include it in the bug report. // showImapLogger->setChecked(true); } void MainWindow::networkError(const QString &message) { const QString title = tr("Network Error"); if (!m_networkErrorMessageBox) { m_networkErrorMessageBox = new QMessageBox(QMessageBox::Critical, title, QString(), QMessageBox::Ok, this); } // User must be informed about a new (but not recurring) error if (message != m_networkErrorMessageBox->text()) { m_networkErrorMessageBox->setText(message); if (qApp->applicationState() == Qt::ApplicationActive) { m_networkErrorMessageBox->setProperty(netErrorUnseen, false); m_networkErrorMessageBox->show(); } else { m_networkErrorMessageBox->setProperty(netErrorUnseen, true); if (m_trayIcon && m_trayIcon->isVisible()) m_trayIcon->showMessage(title, message, QSystemTrayIcon::Warning, 3333); } } } void MainWindow::cacheError(const QString &message) { Gui::Util::messageBoxCritical(this, tr("IMAP Cache Error"), tr("The caching subsystem managing a cache of the data already " "downloaded from the IMAP server is having troubles. " "All caching will be disabled.\n\n%1").arg(message)); } void MainWindow::networkPolicyOffline() { netExpensive->setChecked(false); netOnline->setChecked(false); netOffline->setChecked(true); updateActionsOnlineOffline(false); showStatusMessage(tr("Offline")); handleTrayIconChange(); } void MainWindow::networkPolicyExpensive() { netOffline->setChecked(false); netOnline->setChecked(false); netExpensive->setChecked(true); updateActionsOnlineOffline(true); handleTrayIconChange(); } void MainWindow::networkPolicyOnline() { netOffline->setChecked(false); netExpensive->setChecked(false); netOnline->setChecked(true); updateActionsOnlineOffline(true); handleTrayIconChange(); } /** @short Deletes a network error message box instance upon resetting of reconnect state */ void MainWindow::slotResetReconnectState() { if (m_networkErrorMessageBox) { delete m_networkErrorMessageBox; m_networkErrorMessageBox = 0; } } void MainWindow::slotShowSettings() { SettingsDialog *dialog = new SettingsDialog(this, m_senderIdentities, m_favoriteTags, m_settings); if (dialog->exec() == QDialog::Accepted) { // FIXME: wipe cache in case we're moving between servers nukeModels(); setupModels(); connectModelActions(); // The systray is still connected to the old model -- got to make sure it's getting updated removeSysTray(); slotToggleSysTray(); } QString method = m_settings->value(Common::SettingsNames::imapMethodKey).toString(); if (method != Common::SettingsNames::methodTCP && method != Common::SettingsNames::methodSSL && method != Common::SettingsNames::methodProcess ) { Gui::Util::messageBoxCritical(this, tr("No Configuration"), trUtf8("No IMAP account is configured. Trojitá cannot do much without one.")); } applySizesAndState(); } void MainWindow::authenticationRequested() { Plugins::PasswordPlugin *password = pluginManager()->password(); if (password) { // FIXME: use another account-id at some point in future // Currently the accountName will be empty unless Trojita has been // called with a profile, and then the profile will be used as the // accountName. QString accountName = m_imapAccess->accountName(); if (accountName.isEmpty()) accountName = QStringLiteral("account-0"); Plugins::PasswordJob *job = password->requestPassword(accountName, QStringLiteral("imap")); if (job) { connect(job, &Plugins::PasswordJob::passwordAvailable, this, [this](const QString &password) { authenticationContinue(password); }); connect(job, &Plugins::PasswordJob::error, this, [this](const Plugins::PasswordJob::Error error, const QString &message) { if (error == Plugins::PasswordJob::Error::NoSuchPassword) { authenticationContinue(QString()); } else { authenticationContinue(QString(), tr("Failed to retrieve password from the store: %1").arg(message)); } }); job->setAutoDelete(true); job->start(); return; } } authenticationContinue(QString()); } void MainWindow::authenticationContinue(const QString &password, const QString &errorMessage) { const QString &user = m_settings->value(Common::SettingsNames::imapUserKey).toString(); QString pass = password; if (m_ignoreStoredPassword || pass.isEmpty()) { auto dialog = PasswordDialog::getPassword(this, tr("Authentication Required"), tr("

Please provide IMAP password for user %1 on %2:

").arg( user.toHtmlEscaped(), m_settings->value(Common::SettingsNames::imapHostKey).toString().toHtmlEscaped() ), errorMessage + (errorMessage.isEmpty() ? QString() : QStringLiteral("\n\n")) + imapModel()->imapAuthError()); connect(dialog, &PasswordDialog::gotPassword, imapModel(), &Imap::Mailbox::Model::setImapPassword); connect(dialog, &PasswordDialog::rejected, imapModel(), &Imap::Mailbox::Model::unsetImapPassword); } else { imapModel()->setImapPassword(pass); } } void MainWindow::checkSslPolicy() { m_imapAccess->setSslPolicy(QMessageBox(static_cast(m_imapAccess->sslInfoIcon()), m_imapAccess->sslInfoTitle(), m_imapAccess->sslInfoMessage(), QMessageBox::Yes | QMessageBox::No, this).exec() == QMessageBox::Yes); } void MainWindow::nukeModels() { m_messageWidget->messageView->setEmpty(); mboxTree->setModel(0); msgListWidget->tree->setModel(0); allTree->setModel(0); taskTree->setModel(0); delete prettyMsgListModel; prettyMsgListModel = 0; delete prettyMboxModel; prettyMboxModel = 0; } void MainWindow::recoverDrafts() { QDir draftPath(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/")); QStringList drafts(draftPath.entryList(QStringList() << QStringLiteral("*.draft"))); Q_FOREACH(const QString &draft, drafts) { ComposeWidget *w = ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, draftPath.filePath(draft)), this); // No need to further try creating widgets for drafts if a nullptr is being returned by ComposeWidget::warnIfMsaNotConfigured if (!w) break; } } void MainWindow::slotComposeMail() { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createBlank(this), this); } void MainWindow::slotEditDraft() { QString path(Common::writablePath(Common::LOCATION_DATA) + tr("Drafts")); QDir().mkpath(path); path = QFileDialog::getOpenFileName(this, tr("Edit draft"), path, tr("Drafts") + QLatin1String(" (*.draft)")); if (!path.isNull()) { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, path), this); } } QModelIndexList MainWindow::translatedSelection() const { QModelIndexList translatedIndexes; Q_FOREACH(const QModelIndex &index, msgListWidget->tree->selectedTree()) { translatedIndexes << Imap::deproxifiedIndex(index); } return translatedIndexes; } void MainWindow::handleMarkAsRead(bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsRead: no valid messages"; } else { imapModel()->markMessagesRead(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); if (translatedIndexes.contains(m_messageWidget->messageView->currentMessage())) { m_messageWidget->messageView->stopAutoMarkAsRead(); } } } void MainWindow::slotNextUnread() { QModelIndex current = msgListWidget->tree->currentIndex(); UiUtils::gotoNext(msgListWidget->tree->model(), current, [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); }, [this](const QModelIndex &idx) { Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool()); m_messageWidget->messageView->setMessage(idx); msgListWidget->tree->setCurrentIndex(idx); }, []() { // nothing to do }); } void MainWindow::slotPreviousUnread() { QModelIndex current = msgListWidget->tree->currentIndex(); UiUtils::gotoPrevious(msgListWidget->tree->model(), current, [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); }, [this](const QModelIndex &idx) { Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool()); m_messageWidget->messageView->setMessage(idx); msgListWidget->tree->setCurrentIndex(idx); }, []() { // nothing to do }); } void MainWindow::handleTag(const bool checked, const int index) { const QModelIndexList &translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleTag: no valid messages"; } else { const auto &tagName = m_favoriteTags->tagNameByIndex(index); if (!tagName.isEmpty()) imapModel()->setMessageFlags(translatedIndexes, tagName, checked ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsDeleted(bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsDeleted: no valid messages"; } else { imapModel()->markMessagesDeleted(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsFlagged(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsFlagged: no valid messages"; } else { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::flagged, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsJunk(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsJunk: no valid messages"; } else { if (value) { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, Imap::Mailbox::FLAG_REMOVE); } imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsNotJunk(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsNotJunk: no valid messages"; } else { if (value) { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, Imap::Mailbox::FLAG_REMOVE); } imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::slotMoveToArchiveFailed(const QString &error) { // XXX disable busy cursor QMessageBox::critical(this, tr("Failed to archive"), error); } void MainWindow::handleMoveToArchive() { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMoveToArchive: no valid messages"; } else { auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString(); auto copyMoveMessagesTask = imapModel()->copyMoveMessages( archiveFolderName.isEmpty() ? Common::SettingsNames::imapDefaultArchiveFolderName : archiveFolderName, translatedIndexes, Imap::Mailbox::CopyMoveOperation::MOVE); connect(copyMoveMessagesTask, &Imap::Mailbox::ImapTask::failed, this, &MainWindow::slotMoveToArchiveFailed); } } void MainWindow::slotExpunge() { imapModel()->expungeMailbox(qobject_cast(m_imapAccess->msgListModel())->currentMailbox()); } void MainWindow::slotMarkCurrentMailboxRead() { imapModel()->markMailboxAsRead(mboxTree->currentIndex()); } void MainWindow::slotCreateMailboxBelowCurrent() { createMailboxBelow(mboxTree->currentIndex()); } void MainWindow::slotCreateTopMailbox() { createMailboxBelow(QModelIndex()); } void MainWindow::createMailboxBelow(const QModelIndex &index) { Imap::Mailbox::TreeItemMailbox *mboxPtr = index.isValid() ? dynamic_cast( Imap::Mailbox::Model::realTreeItem(index)) : 0; Ui::CreateMailboxDialog ui; QDialog *dialog = new QDialog(this); ui.setupUi(dialog); dialog->setWindowTitle(mboxPtr ? tr("Create a Subfolder of %1").arg(mboxPtr->mailbox()) : tr("Create a Top-level Mailbox")); if (dialog->exec() == QDialog::Accepted) { QStringList parts; if (mboxPtr) parts << mboxPtr->mailbox(); parts << ui.mailboxName->text(); if (ui.otherMailboxes->isChecked()) parts << QString(); QString targetName = parts.join(mboxPtr ? mboxPtr->separator() : QString()); // FIXME: top-level separator imapModel()->createMailbox(targetName, ui.subscribe->isChecked() ? Imap::Mailbox::AutoSubscription::SUBSCRIBE : Imap::Mailbox::AutoSubscription::NO_EXPLICIT_SUBSCRIPTION ); } } void MainWindow::slotDeleteCurrentMailbox() { if (! mboxTree->currentIndex().isValid()) return; QModelIndex mailbox = Imap::deproxifiedIndex(mboxTree->currentIndex()); Q_ASSERT(mailbox.isValid()); QString name = mailbox.data(Imap::Mailbox::RoleMailboxName).toString(); if (QMessageBox::question(this, tr("Delete Mailbox"), tr("Are you sure to delete mailbox %1?").arg(name), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { imapModel()->deleteMailbox(name); } } void MainWindow::updateMessageFlags() { updateMessageFlagsOf(QModelIndex()); } void MainWindow::updateMessageFlagsOf(const QModelIndex &index) { QModelIndexList indexes = index.isValid() ? QModelIndexList() << index : translatedSelection(); const bool isValid = !indexes.isEmpty() && // either we operate on the -already valided- selection or the index must be valid (!index.isValid() || index.data(Imap::Mailbox::RoleMessageUid).toUInt() > 0); const bool okToModify = imapModel()->isNetworkAvailable() && isValid; markAsRead->setEnabled(okToModify); markAsDeleted->setEnabled(okToModify); markAsFlagged->setEnabled(okToModify); markAsJunk->setEnabled(okToModify); markAsNotJunk->setEnabled(okToModify); // There's no point in moving from Archive to, well, Archive auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString(); if (archiveFolderName.isEmpty()) { archiveFolderName = Common::SettingsNames::imapDefaultArchiveFolderName; } moveToArchive->setEnabled(okToModify && std::any_of(indexes.cbegin(), indexes.cend(), [archiveFolderName](const QModelIndex &i) { return i.data(Imap::Mailbox::RoleMailboxName) != archiveFolderName; })); tag1->setEnabled(okToModify); tag2->setEnabled(okToModify); tag3->setEnabled(okToModify); tag4->setEnabled(okToModify); tag5->setEnabled(okToModify); tag6->setEnabled(okToModify); tag7->setEnabled(okToModify); tag8->setEnabled(okToModify); tag9->setEnabled(okToModify); bool isRead = isValid, isDeleted = isValid, isFlagged = isValid, isJunk = isValid, isNotJunk = isValid, hasTag1 = isValid, hasTag2 = isValid, hasTag3 = isValid, hasTag4 = isValid, hasTag5 = isValid, hasTag6 = isValid, hasTag7 = isValid, hasTag8 = isValid, hasTag9 = isValid; auto updateTag = [=](const QModelIndex &i, bool &hasTag, int index) { if (hasTag && !m_favoriteTags->tagNameByIndex(index).isEmpty() && !i.data(Imap::Mailbox::RoleMessageFlags).toStringList().contains(m_favoriteTags->tagNameByIndex(index))) { hasTag = false; } }; Q_FOREACH (const QModelIndex &i, indexes) { #define UPDATE_STATE(PROP) \ if (is##PROP && !i.data(Imap::Mailbox::RoleMessageIsMarked##PROP).toBool()) \ is##PROP = false; UPDATE_STATE(Read) UPDATE_STATE(Deleted) UPDATE_STATE(Flagged) UPDATE_STATE(Junk) UPDATE_STATE(NotJunk) #undef UPDATE_STATE updateTag(i, hasTag1, 0); updateTag(i, hasTag2, 1); updateTag(i, hasTag3, 2); updateTag(i, hasTag4, 3); updateTag(i, hasTag5, 4); updateTag(i, hasTag6, 5); updateTag(i, hasTag7, 6); updateTag(i, hasTag8, 7); updateTag(i, hasTag9, 8); } markAsRead->setChecked(isRead); markAsDeleted->setChecked(isDeleted); markAsFlagged->setChecked(isFlagged); markAsJunk->setChecked(isJunk && !isNotJunk); markAsNotJunk->setChecked(isNotJunk && !isJunk); tag1->setChecked(hasTag1); tag2->setChecked(hasTag2); tag3->setChecked(hasTag3); tag4->setChecked(hasTag4); tag5->setChecked(hasTag5); tag6->setChecked(hasTag6); tag7->setChecked(hasTag7); tag8->setChecked(hasTag8); tag9->setChecked(hasTag9); } void MainWindow::updateActionsOnlineOffline(bool online) { reloadMboxList->setEnabled(online); resyncMbox->setEnabled(online); expunge->setEnabled(online); createChildMailbox->setEnabled(online); createTopMailbox->setEnabled(online); deleteCurrentMailbox->setEnabled(online); m_actionMarkMailboxAsRead->setEnabled(online); updateMessageFlags(); showImapCapabilities->setEnabled(online); if (!online) { m_replyGuess->setEnabled(false); m_replyPrivate->setEnabled(false); m_replyAll->setEnabled(false); m_replyAllButMe->setEnabled(false); m_replyList->setEnabled(false); m_forwardAsAttachment->setEnabled(false); m_bounce->setEnabled(false); } } void MainWindow::slotUpdateMessageActions() { Composer::RecipientList dummy; m_replyPrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyGuess->setEnabled(m_replyPrivate->isEnabled() || m_replyAllButMe->isEnabled() || m_replyAll->isEnabled() || m_replyList->isEnabled()); // Check the default reply mode // I suspect this is not going to work for everybody. Suggestions welcome... if (m_replyList->isEnabled()) { m_replyButton->setDefaultAction(m_replyList); } else if (m_replyAllButMe->isEnabled()) { m_replyButton->setDefaultAction(m_replyAllButMe); } else { m_replyButton->setDefaultAction(m_replyPrivate); } m_forwardAsAttachment->setEnabled(m_messageWidget->messageView->currentMessage().isValid()); m_bounce->setEnabled(m_messageWidget->messageView->currentMessage().isValid()); } void MainWindow::scrollMessageUp() { m_messageWidget->area->ensureVisible(0, 0, 0, 0); } void MainWindow::slotReplyTo() { m_messageWidget->messageView->reply(this, Composer::REPLY_PRIVATE); } void MainWindow::slotReplyAll() { m_messageWidget->messageView->reply(this, Composer::REPLY_ALL); } void MainWindow::slotReplyAllButMe() { m_messageWidget->messageView->reply(this, Composer::REPLY_ALL_BUT_ME); } void MainWindow::slotReplyList() { m_messageWidget->messageView->reply(this, Composer::REPLY_LIST); } void MainWindow::slotReplyGuess() { if (m_replyButton->defaultAction() == m_replyAllButMe) { slotReplyAllButMe(); } else if (m_replyButton->defaultAction() == m_replyAll) { slotReplyAll(); } else if (m_replyButton->defaultAction() == m_replyList) { slotReplyList(); } else { slotReplyTo(); } } void MainWindow::slotForwardAsAttachment() { m_messageWidget->messageView->forward(this, Composer::ForwardMode::FORWARD_AS_ATTACHMENT); } void MainWindow::slotBounce() { QModelIndex index; Imap::Mailbox::Model::realTreeItem(msgListWidget->tree->currentIndex(), nullptr, &index); if (!index.isValid()) return; auto recipients = QList>(); for (const auto &kind: {Imap::Mailbox::RoleMessageTo, Imap::Mailbox::RoleMessageCc, Imap::Mailbox::RoleMessageBcc}) { for (const auto &oneAddr : index.data(kind).toList()) { Q_ASSERT(oneAddr.type() == QVariant::StringList); QStringList item = oneAddr.toStringList(); Q_ASSERT(item.size() == 4); Imap::Message::MailAddress a(item[0], item[1], item[2], item[3]); Composer::RecipientKind translatedKind = Composer::RecipientKind::ADDRESS_TO; switch (kind) { case Imap::Mailbox::RoleMessageTo: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_TO; break; case Imap::Mailbox::RoleMessageCc: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_CC; break; case Imap::Mailbox::RoleMessageBcc: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_BCC; break; default: Q_ASSERT(false); break; } recipients.push_back({translatedKind, a.asPrettyString()}); } } ComposeWidget::warnIfMsaNotConfigured( ComposeWidget::createFromReadOnly(this, index, recipients), this); } void MainWindow::slotComposeMailUrl(const QUrl &url) { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createFromUrl(this, url), this); } void MainWindow::slotManageContact(const QUrl &url) { Imap::Message::MailAddress addr; if (!Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("x-trojita-manage-contact"))) return; Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook) return; addressbook->openContactWindow(addr.mailbox + QLatin1Char('@') + addr.host, addr.name); } void MainWindow::invokeContactEditor() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook) return; addressbook->openAddressbookWindow(); } /** @short Create an MSAFactory as per the settings */ MSA::MSAFactory *MainWindow::msaFactory() { using namespace Common; QString method = m_settings->value(SettingsNames::msaMethodKey).toString(); MSA::MSAFactory *msaFactory = 0; if (method == SettingsNames::methodSMTP || method == SettingsNames::methodSSMTP) { msaFactory = new MSA::SMTPFactory(m_settings->value(SettingsNames::smtpHostKey).toString(), m_settings->value(SettingsNames::smtpPortKey).toInt(), (method == SettingsNames::methodSSMTP), (method == SettingsNames::methodSMTP) && m_settings->value(SettingsNames::smtpStartTlsKey).toBool(), m_settings->value(SettingsNames::smtpAuthKey).toBool(), m_settings->value(SettingsNames::smtpAuthReuseImapCredsKey, false).toBool() ? m_settings->value(SettingsNames::imapUserKey).toString() : m_settings->value(SettingsNames::smtpUserKey).toString()); } else if (method == SettingsNames::methodSENDMAIL) { QStringList args = m_settings->value(SettingsNames::sendmailKey, SettingsNames::sendmailDefaultCmd).toString().split(QLatin1Char(' ')); if (args.isEmpty()) { return 0; } QString appName = args.takeFirst(); msaFactory = new MSA::SendmailFactory(appName, args); } else if (method == SettingsNames::methodImapSendmail) { if (!imapModel()->capabilities().contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"))) { return 0; } msaFactory = new MSA::ImapSubmitFactory(qobject_cast(imapAccess()->imapModel())); } else { return 0; } return msaFactory; } void MainWindow::slotMailboxDeleteFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't delete mailbox"), tr("Deleting mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxCreateFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't create mailbox"), tr("Creating mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxSyncFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't open mailbox"), tr("Opening mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxChanged(const QModelIndex &mailbox) { using namespace Imap::Mailbox; QString mailboxName = mailbox.data(RoleMailboxName).toString(); bool isSentMailbox = mailbox.isValid() && !mailboxName.isEmpty() && m_settings->value(Common::SettingsNames::composerSaveToImapKey).toBool() && mailboxName == m_settings->value(Common::SettingsNames::composerImapSentKey).toString(); QTreeView *tree = msgListWidget->tree; // Automatically trigger visibility of the TO and FROM columns if (isSentMailbox) { if (tree->isColumnHidden(MsgListModel::TO) && !tree->isColumnHidden(MsgListModel::FROM)) { tree->hideColumn(MsgListModel::FROM); tree->showColumn(MsgListModel::TO); } } else { if (tree->isColumnHidden(MsgListModel::FROM) && !tree->isColumnHidden(MsgListModel::TO)) { tree->hideColumn(MsgListModel::TO); tree->showColumn(MsgListModel::FROM); } } updateMessageFlags(); slotScrollToUnseenMessage(); } void MainWindow::showConnectionStatus(uint parserId, Imap::ConnectionState state) { Q_UNUSED(parserId); static Imap::ConnectionState previousState = Imap::ConnectionState::CONN_STATE_NONE; QString message = connectionStateToString(state); if (state == Imap::ConnectionState::CONN_STATE_SELECTED && previousState >= Imap::ConnectionState::CONN_STATE_SELECTED) { // prevent excessive popups when we "reset the state" to something which is shown quite often showStatusMessage(QString()); } else { showStatusMessage(message); } previousState = state; } void MainWindow::slotShowLinkTarget(const QString &link) { if (link.isEmpty()) { QToolTip::hideText(); } else { QToolTip::showText(QCursor::pos(), tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(link))); } } void MainWindow::slotShowAboutTrojita() { Ui::AboutDialog ui; QDialog *widget = new QDialog(this); widget->setAttribute(Qt::WA_DeleteOnClose); ui.setupUi(widget); ui.versionLabel->setText(Common::Application::version); ui.qtVersion->setText(QStringLiteral("Qt " QT_VERSION_STR "")); connect(ui.qtVersion, &QLabel::linkActivated, qApp, &QApplication::aboutQt); std::vector> features; features.emplace_back(tr("Plugins"), #ifdef WITH_SHARED_PLUGINS true #else false #endif ); features.emplace_back(tr("Encrypted and signed messages"), #ifdef TROJITA_HAVE_CRYPTO_MESSAGES true #else false #endif ); features.emplace_back(tr("IMAP compression"), #ifdef TROJITA_HAVE_ZLIB true #else false #endif ); QString featuresText = QStringLiteral("
    "); for (const auto x: features) { featuresText += x.second ? tr("
  • %1: supported
  • ").arg(x.first) : tr("
  • %1: disabled
  • ").arg(x.first); } featuresText += QStringLiteral("
"); ui.descriptionLabel->setText(ui.descriptionLabel->text() + featuresText); QStringList copyright; { // Find the names of the authors and remove date codes from there QFile license(QStringLiteral(":/LICENSE")); license.open(QFile::ReadOnly); const QString prefix(QStringLiteral("Copyright (C) ")); Q_FOREACH(const QString &line, QString::fromUtf8(license.readAll()).split(QLatin1Char('\n'))) { if (line.startsWith(prefix)) { const int pos = prefix.size(); copyright << QChar(0xa9 /* COPYRIGHT SIGN */) + QLatin1Char(' ') + line.mid(pos).replace(QRegExp(QLatin1String("(\\d) - (\\d)")), QLatin1String("\\1") + QChar(0x2014 /* EM DASH */) + QLatin1String("\\2")); } } } ui.credits->setTextFormat(Qt::PlainText); ui.credits->setText(copyright.join(QStringLiteral("\n"))); widget->show(); } void MainWindow::slotDonateToTrojita() { QDesktopServices::openUrl(QStringLiteral("https://sourceforge.net/p/trojita/donate/")); } void MainWindow::slotSaveCurrentMessageBody() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; QModelIndex messageIndex = Imap::deproxifiedIndex(item); Imap::Network::MsgPartNetAccessManager *netAccess = new Imap::Network::MsgPartNetAccessManager(this); netAccess->setModelMessage(messageIndex); Imap::Network::FileDownloadManager *fileDownloadManager = new Imap::Network::FileDownloadManager(this, netAccess, messageIndex); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::succeeded, fileDownloadManager, &QObject::deleteLater); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, fileDownloadManager, &QObject::deleteLater); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &MainWindow::slotDownloadMessageFileNameRequested); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, this, &MainWindow::slotDownloadTransferError); connect(fileDownloadManager, &QObject::destroyed, netAccess, &QObject::deleteLater); fileDownloadManager->downloadMessage(); } } void MainWindow::slotDownloadTransferError(const QString &errorString) { Gui::Util::messageBoxCritical(this, tr("Can't save into file"), tr("Unable to save into file. Error:\n%1").arg(errorString)); } void MainWindow::slotDownloadMessageFileNameRequested(QString *fileName) { *fileName = QFileDialog::getSaveFileName(this, tr("Save Message"), *fileName, QString(), 0, QFileDialog::HideNameFilterDetails); } void MainWindow::slotViewMsgSource() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; auto w = messageSourceWidget(item); //: Translators: %1 is the UID of a message (a number) and %2 is the name of a mailbox. w->setWindowTitle(tr("Message source of UID %1 in %2").arg( QString::number(item.data(Imap::Mailbox::RoleMessageUid).toUInt()), Imap::deproxifiedIndex(item).parent().parent().data(Imap::Mailbox::RoleMailboxName).toString() )); w->show(); } } QWidget *MainWindow::messageSourceWidget(const QModelIndex &message) { QModelIndex messageIndex = Imap::deproxifiedIndex(message); MessageSourceWidget *sourceWidget = new MessageSourceWidget(0, messageIndex); sourceWidget->setAttribute(Qt::WA_DeleteOnClose); QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), sourceWidget); sourceWidget->addAction(close); close->setShortcut(tr("Ctrl+W")); connect(close, &QAction::triggered, sourceWidget, &QWidget::close); return sourceWidget; } void MainWindow::slotViewMsgHeaders() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; QModelIndex messageIndex = Imap::deproxifiedIndex(item); auto widget = new MessageHeadersWidget(nullptr, messageIndex); widget->setAttribute(Qt::WA_DeleteOnClose); QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), widget); widget->addAction(close); close->setShortcut(tr("Ctrl+W")); connect(close, &QAction::triggered, widget, &QWidget::close); widget->show(); } } #ifdef XTUPLE_CONNECT void MainWindow::slotXtSyncCurrentMailbox() { QModelIndex index = mboxTree->currentIndex(); if (! index.isValid()) return; QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString(); QSettings s; QStringList mailboxes = s.value(Common::SettingsNames::xtSyncMailboxList).toStringList(); if (xtIncludeMailboxInSync->isChecked()) { if (! mailboxes.contains(mailbox)) { mailboxes.append(mailbox); } } else { mailboxes.removeAll(mailbox); } s.setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes); QSettings(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")).setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes); prettyMboxModel->xtConnectStatusChanged(index); } #endif void MainWindow::slotSubscribeCurrentMailbox() { QModelIndex index = mboxTree->currentIndex(); if (! index.isValid()) return; QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString(); if (m_actionSubscribeMailbox->isChecked()) { imapModel()->subscribeMailbox(mailbox); } else { imapModel()->unsubscribeMailbox(mailbox); } } void MainWindow::slotShowOnlySubscribed() { if (m_actionShowOnlySubscribed->isEnabled()) { m_settings->setValue(Common::SettingsNames::guiMailboxListShowOnlySubscribed, m_actionShowOnlySubscribed->isChecked()); prettyMboxModel->setShowOnlySubscribed(m_actionShowOnlySubscribed->isChecked()); } } void MainWindow::slotScrollToUnseenMessage() { // Now this is much, much more reliable than messing around with finding out an "interesting message"... if (!m_actionSortNone->isChecked() && !m_actionSortThreading->isChecked()) { // we're using some funky sorting, better don't scroll anywhere } if (m_actionSortDescending->isChecked()) { msgListWidget->tree->scrollToTop(); } else { msgListWidget->tree->scrollToBottom(); } } void MainWindow::slotScrollToCurrent() { // TODO: begs for lambda if (QScrollBar *vs = msgListWidget->tree->verticalScrollBar()) { vs->setValue(vs->maximum() - vs->value()); // implies vs->minimum() == 0 } } void MainWindow::slotThreadMsgList() { // We want to save user's preferences and not override them with "threading disabled" when the server // doesn't report them, like in initial greetings. That's why we have to check for isEnabled() here. const bool useThreading = actionThreadMsgList->isChecked(); // Switching between threaded/unthreaded view shall reset the sorting criteria. The goal is to make // sorting rather seldomly used as people shall instead use proper threading. if (useThreading) { m_actionSortThreading->setEnabled(true); if (!m_actionSortThreading->isChecked()) m_actionSortThreading->trigger(); m_actionSortNone->setEnabled(false); } else { m_actionSortNone->setEnabled(true); if (!m_actionSortNone->isChecked()) m_actionSortNone->trigger(); m_actionSortThreading->setEnabled(false); } QPersistentModelIndex currentItem = msgListWidget->tree->currentIndex(); if (useThreading && actionThreadMsgList->isEnabled()) { msgListWidget->tree->setRootIsDecorated(true); qobject_cast(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(true); } else { msgListWidget->tree->setRootIsDecorated(false); qobject_cast(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(false); } m_settings->setValue(Common::SettingsNames::guiMsgListShowThreading, QVariant(useThreading)); if (currentItem.isValid()) { msgListWidget->tree->scrollTo(currentItem); } else { // If we cannot determine the current item, at least scroll to a predictable place. Without this, the view // would jump to "weird" places, probably due to some heuristics about trying to show "roughly the same" // objects as what was visible before the reshuffling. slotScrollToUnseenMessage(); } } void MainWindow::slotSortingPreferenceChanged() { Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder; using namespace Imap::Mailbox; int column = -1; if (m_actionSortByArrival->isChecked()) { column = MsgListModel::RECEIVED_DATE; } else if (m_actionSortByCc->isChecked()) { column = MsgListModel::CC; } else if (m_actionSortByDate->isChecked()) { column = MsgListModel::DATE; } else if (m_actionSortByFrom->isChecked()) { column = MsgListModel::FROM; } else if (m_actionSortBySize->isChecked()) { column = MsgListModel::SIZE; } else if (m_actionSortBySubject->isChecked()) { column = MsgListModel::SUBJECT; } else if (m_actionSortByTo->isChecked()) { column = MsgListModel::TO; } else { column = -1; } msgListWidget->tree->header()->setSortIndicator(column, order); } void MainWindow::slotSortingConfirmed(int column, Qt::SortOrder order) { // don't do anything during initialization if (!m_actionSortNone) return; using namespace Imap::Mailbox; QAction *action; switch (column) { case MsgListModel::SEEN: case MsgListModel::FLAGGED: case MsgListModel::ATTACHMENT: case MsgListModel::COLUMN_COUNT: case MsgListModel::BCC: case -1: if (actionThreadMsgList->isChecked()) action = m_actionSortThreading; else action = m_actionSortNone; break; case MsgListModel::SUBJECT: action = m_actionSortBySubject; break; case MsgListModel::FROM: action = m_actionSortByFrom; break; case MsgListModel::TO: action = m_actionSortByTo; break; case MsgListModel::CC: action = m_actionSortByCc; break; case MsgListModel::DATE: action = m_actionSortByDate; break; case MsgListModel::RECEIVED_DATE: action = m_actionSortByArrival; break; case MsgListModel::SIZE: action = m_actionSortBySize; break; default: action = m_actionSortNone; } action->setChecked(true); if (order == Qt::DescendingOrder) m_actionSortDescending->setChecked(true); else m_actionSortAscending->setChecked(true); } void MainWindow::slotSearchRequested(const QStringList &searchConditions) { if (!searchConditions.isEmpty() && actionThreadMsgList->isChecked()) { // right now, searching and threading doesn't play well together at all actionThreadMsgList->trigger(); } Imap::Mailbox::ThreadingMsgListModel * threadingMsgListModel = qobject_cast(m_imapAccess->threadingMsgListModel()); threadingMsgListModel->setUserSearchingSortingPreference(searchConditions, threadingMsgListModel->currentSortCriterium(), threadingMsgListModel->currentSortOrder()); } void MainWindow::slotHideRead() { const bool hideRead = actionHideRead->isChecked(); prettyMsgListModel->setHideRead(hideRead); m_settings->setValue(Common::SettingsNames::guiMsgListHideRead, QVariant(hideRead)); } void MainWindow::slotCapabilitiesUpdated(const QStringList &capabilities) { msgListWidget->tree->header()->viewport()->removeEventFilter(this); if (capabilities.contains(QStringLiteral("SORT"))) { m_actionSortByDate->actionGroup()->setEnabled(true); } else { m_actionSortByDate->actionGroup()->setEnabled(false); msgListWidget->tree->header()->viewport()->installEventFilter(this); } msgListWidget->setFuzzySearchSupported(capabilities.contains(QStringLiteral("SEARCH=FUZZY"))); m_actionShowOnlySubscribed->setEnabled(capabilities.contains(QStringLiteral("LIST-EXTENDED"))); m_actionShowOnlySubscribed->setChecked(m_actionShowOnlySubscribed->isEnabled() && m_settings->value( Common::SettingsNames::guiMailboxListShowOnlySubscribed, false).toBool()); m_actionSubscribeMailbox->setEnabled(m_actionShowOnlySubscribed->isEnabled()); const QStringList supportedCapabilities = Imap::Mailbox::ThreadingMsgListModel::supportedCapabilities(); Q_FOREACH(const QString &capability, capabilities) { if (supportedCapabilities.contains(capability)) { actionThreadMsgList->setEnabled(true); if (actionThreadMsgList->isChecked()) slotThreadMsgList(); return; } } actionThreadMsgList->setEnabled(false); } void MainWindow::slotShowImapInfo() { QString caps; Q_FOREACH(const QString &cap, imapModel()->capabilities()) { caps += tr("
  • %1
  • \n").arg(cap); } QString idString; if (!imapModel()->serverId().isEmpty() && imapModel()->capabilities().contains(QStringLiteral("ID"))) { QMap serverId = imapModel()->serverId(); #define IMAP_ID_FIELD(Var, Name) bool has_##Var = serverId.contains(Name); \ QString Var = has_##Var ? QString::fromUtf8(serverId[Name]).toHtmlEscaped() : tr("Unknown"); IMAP_ID_FIELD(serverName, "name"); IMAP_ID_FIELD(serverVersion, "version"); IMAP_ID_FIELD(os, "os"); IMAP_ID_FIELD(osVersion, "os-version"); IMAP_ID_FIELD(vendor, "vendor"); IMAP_ID_FIELD(supportUrl, "support-url"); IMAP_ID_FIELD(address, "address"); IMAP_ID_FIELD(date, "date"); IMAP_ID_FIELD(command, "command"); IMAP_ID_FIELD(arguments, "arguments"); IMAP_ID_FIELD(environment, "environment"); #undef IMAP_ID_FIELD if (has_serverName) { idString = tr("

    "); if (has_serverVersion) idString += tr("Server: %1 %2").arg(serverName, serverVersion); else idString += tr("Server: %1").arg(serverName); if (has_vendor) { idString += tr(" (%1)").arg(vendor); } if (has_os) { if (has_osVersion) idString += tr(" on %1 %2", "%1 is the operating system of an IMAP server and %2 is its version.").arg(os, osVersion); else idString += tr(" on %1", "%1 is the operationg system of an IMAP server.").arg(os); } idString += tr("

    "); } else { idString = tr("

    The IMAP server did not return usable information about itself.

    "); } QString fullId; for (QMap::const_iterator it = serverId.constBegin(); it != serverId.constEnd(); ++it) { fullId += tr("
  • %1: %2
  • ").arg(QString::fromUtf8(it.key()).toHtmlEscaped(), QString::fromUtf8(it.value()).toHtmlEscaped()); } idString += tr("
      %1
    ").arg(fullId); } else { idString = tr("

    The server has not provided information about its software version.

    "); } QMessageBox::information(this, tr("IMAP Server Information"), tr("%1" "

    The following capabilities are currently advertised:

    \n" "
      \n%2
    ").arg(idString, caps)); } QSize MainWindow::sizeHint() const { return QSize(1150, 980); } void MainWindow::slotUpdateWindowTitle() { QModelIndex mailbox = qobject_cast(m_imapAccess->msgListModel())->currentMailbox(); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (!profileName.isEmpty()) profileName = QLatin1String(" [") + profileName + QLatin1Char(']'); if (mailbox.isValid()) { if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) { setWindowTitle(trUtf8("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } else { setWindowTitle(trUtf8("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } } else { setWindowTitle(trUtf8("Trojitá") + profileName); } } void MainWindow::slotLayoutCompact() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } if (!m_mainVSplitter) { m_mainVSplitter = new QSplitter(); m_mainVSplitter->setOrientation(Qt::Vertical); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainVSplitter->addWidget(msgListWidget); m_mainVSplitter->addWidget(m_messageWidget); m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(m_mainVSplitter); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainVSplitter->show(); m_mainHSplitter->show(); // The mboxTree shall not expand... m_mainHSplitter->setStretchFactor(0, 0); // ...while the msgListTree shall consume all the remaining space m_mainHSplitter->setStretchFactor(1, 1); // The CompleteMessageWidget shall not not collapse m_mainVSplitter->setCollapsible(m_mainVSplitter->indexOf(m_messageWidget), false); setCentralWidget(m_mainHSplitter); delete m_mainStack; m_layoutMode = LAYOUT_COMPACT; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact); applySizesAndState(); } void MainWindow::slotLayoutWide() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(msgListWidget); m_mainHSplitter->addWidget(m_messageWidget); msgListWidget->resize(mboxTree->size()); m_messageWidget->resize(mboxTree->size()); m_mainHSplitter->setStretchFactor(0, 0); m_mainHSplitter->setStretchFactor(1, 1); m_mainHSplitter->setStretchFactor(2, 1); m_mainHSplitter->setCollapsible(m_mainHSplitter->indexOf(m_messageWidget), false); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainHSplitter->show(); setCentralWidget(m_mainHSplitter); delete m_mainStack; delete m_mainVSplitter; m_layoutMode = LAYOUT_WIDE; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide); applySizesAndState(); } void MainWindow::slotLayoutOneAtTime() { saveSizesAndState(); if (m_mainStack) return; m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack); setCentralWidget(m_mainStack); delete m_mainHSplitter; delete m_mainVSplitter; m_layoutMode = LAYOUT_ONE_AT_TIME; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime); applySizesAndState(); } Imap::Mailbox::Model *MainWindow::imapModel() const { return qobject_cast(m_imapAccess->imapModel()); } void MainWindow::desktopGeometryChanged() { m_delayedStateSaving->start(); } QString MainWindow::settingsKeyForLayout(const LayoutMode layout) { switch (layout) { case LAYOUT_COMPACT: return Common::SettingsNames::guiSizesInMainWinWhenCompact; case LAYOUT_WIDE: return Common::SettingsNames::guiSizesInMainWinWhenWide; case LAYOUT_ONE_AT_TIME: return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime; break; } return QString(); } void MainWindow::saveSizesAndState() { if (m_skipSavingOfUI) return; QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QList items; items << saveGeometry(); items << saveState(); items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray()); items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray()); items << msgListWidget->tree->header()->saveState(); items << QByteArray::number(msgListWidget->tree->header()->count()); for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) { items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i)); } // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number items << QByteArray::number(menuBar()->isVisible()); QByteArray buf; QDataStream stream(&buf, QIODevice::WriteOnly); stream << items.size(); Q_FOREACH(const QByteArray &item, items) { stream << item; } m_settings->setValue(key.arg(QString::number(geometry.width())), buf); } void MainWindow::saveRawStateSetting(bool enabled) { m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled); } void MainWindow::applySizesAndState() { QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray(); if (buf.isEmpty()) return; int size; QDataStream stream(&buf, QIODevice::ReadOnly); stream >> size; QByteArray item; // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to // save our geometries and state, which is what we must avoid while this method is executing. bool skipSaving = m_skipSavingOfUI; m_skipSavingOfUI = true; if (size-- && !stream.atEnd()) { stream >> item; // https://bugreports.qt-project.org/browse/QTBUG-30636 if (windowState() & Qt::WindowMaximized) { // restoreGeometry(.) restores the wrong size for at least maximized window // However, QWidget does also not notice that the configure request for this // is ignored by many window managers (because users really don't like when windows // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from // the wrong assumption the request would have been hononred. // So we just "fix" the internal geometry immediately afterwards to prevent // mislayouting // There's atm. no flicker due to this (and because Qt compresses events) // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true) QRect oldGeometry = MainWindow::geometry(); restoreGeometry(item); if (windowState() & Qt::WindowMaximized) setGeometry(oldGeometry); } else { restoreGeometry(item); if (windowState() & Qt::WindowMaximized) { // ensure to try setting the proper geometry and have the WM constrain us setGeometry(QApplication::desktop()->availableGeometry()); } } } if (size-- && !stream.atEnd()) { stream >> item; restoreState(item); } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainVSplitter) { m_mainVSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainHSplitter) { m_mainHSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; msgListWidget->tree->header()->restoreState(item); // got to manually update the state of the actions which control the visibility state msgListWidget->tree->updateActionsAfterRestoredState(); } connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns); if (size-- && !stream.atEnd()) { stream >> item; bool ok; int columns = item.toInt(&ok); if (ok) { msgListWidget->tree->header()->setStretchLastSection(false); for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) { stream >> item; int sectionSize = item.toInt(); QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i); if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) { // fun fact: user cannot resize by mouse when size <= 0 msgListWidget->tree->setColumnWidth(i, sectionSize); } else { msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i)); } msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode); } } } if (size-- && !stream.atEnd()) { stream >> item; bool ok; bool visibility = item.toInt(&ok); if (ok) { menuBar()->setVisible(visibility); showMenuBar->setChecked(visibility); } } m_skipSavingOfUI = skipSaving; } void MainWindow::resizeEvent(QResizeEvent *) { m_delayedStateSaving->start(); } /** @short Make sure that the message gets loaded after the splitters have changed their position */ void MainWindow::possiblyLoadMessageOnSplittersChanged() { if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) { // We do not have to check whether it's a different message; the setMessage() will do this or us // and there are multiple proxy models involved anyway QModelIndex index = msgListWidget->tree->currentIndex(); if (index.isValid()) { // OTOH, setting an invalid QModelIndex would happily assert-fail m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex()); } } } Imap::ImapAccess *MainWindow::imapAccess() const { return m_imapAccess; } void MainWindow::enableLoggingToDisk() { imapLogger->slotSetPersistentLogging(true); } void MainWindow::slotPluginsChanged() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow)) m_actionContactEditor->setEnabled(false); else m_actionContactEditor->setEnabled(true); } /** @short Update the default action to make sure that we show a correct status of the network connection */ void MainWindow::updateNetworkIndication() { if (QAction *action = qobject_cast(sender())) { if (action->isChecked()) { m_netToolbarDefaultAction->setIcon(action->icon()); } } } void MainWindow::showStatusMessage(const QString &message) { networkIndicator->setToolTip(message); if (isActiveWindow()) QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message); } void MainWindow::slotMessageModelChanged(QAbstractItemModel *model) { mailMimeTree->setModel(model); } void MainWindow::slotFavoriteTagsChanged() { for (int i = 1; i <= m_favoriteTags->rowCount(); ++i) { QAction *action = ShortcutHandler::instance()->action(QStringLiteral("action_tag_") + QString::number(i)); if (action) action->setText(tr("Tag with \"%1\"").arg(m_favoriteTags->tagNameByIndex(i - 1))); } } +void MainWindow::registerComposeWindow(ComposeWidget* widget) +{ + connect(widget, &ComposeWidget::logged, this, [this](const Common::LogKind kind, const QString& source, const QString& message) { + imapLogger->log(0, Common::LogMessage(QDateTime::currentDateTime(), kind, source, message, 0)); + }); +} + } diff --git a/src/Gui/Window.h b/src/Gui/Window.h index 473b8faf..e8160c61 100644 --- a/src/Gui/Window.h +++ b/src/Gui/Window.h @@ -1,386 +1,388 @@ /* Copyright (C) 2006 - 2015 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TROJITA_WINDOW_H #define TROJITA_WINDOW_H #include #include #include #include #include "Composer/Recipients.h" #include "Imap/ConnectionState.h" #include "Imap/Model/Cache.h" class QAuthenticator; class QCloseEvent; class QItemSelection; class QMessageBox; class QModelIndex; class QScrollArea; class QSettings; class QSplitter; class QSslCertificate; class QSslError; class QStackedWidget; class QToolButton; class QTreeView; namespace Composer { class SenderIdentitiesModel; } namespace Imap { class ImapAccess; namespace Mailbox { class Model; class PrettyMailboxModel; class ThreadingMsgListModel; class PrettyMsgListModel; class FavoriteTagsModel; } } namespace Plugins { class PluginManager; } namespace MSA { class MSAFactory; } namespace Gui { class CompleteMessageWidget; class ComposeWidget; class MailBoxTreeView; class MessageListWidget; class ProtocolLoggerWidget; class TaskProgressIndicator; class MainWindow: public QMainWindow { Q_OBJECT typedef QList > RecipientsType; typedef enum { LAYOUT_COMPACT, LAYOUT_WIDE, LAYOUT_ONE_AT_TIME } LayoutMode; public: MainWindow(QSettings *settings); void showMainWindow(); QSize sizeHint() const; Imap::Mailbox::Model *imapModel() const; Composer::SenderIdentitiesModel *senderIdentitiesModel() { return m_senderIdentities; } Imap::Mailbox::FavoriteTagsModel *favoriteTagsModel() { return m_favoriteTags; } Plugins::PluginManager *pluginManager() { return m_pluginManager; } QSettings *settings() const { return m_settings; } MSA::MSAFactory *msaFactory(); // FIXME: this should be changed to some wrapper when support for multiple accounts is available Imap::ImapAccess *imapAccess() const; void enableLoggingToDisk(); static QWidget *messageSourceWidget(const QModelIndex &message); + void registerComposeWindow(ComposeWidget* widget); + public slots: void slotComposeMailUrl(const QUrl &url); void slotComposeMail(); void invokeContactEditor(); protected: void closeEvent(QCloseEvent *event); bool eventFilter(QObject *o, QEvent *e); private slots: void showContextMenuMboxTree(const QPoint &position); void showContextMenuMsgListTree(const QPoint &position); void slotReloadMboxList(); void slotResyncMbox(); void alertReceived(const QString &message); void networkPolicyOffline(); void networkPolicyExpensive(); void networkPolicyOnline(); void slotShowSettings(); void slotShowImapInfo(); void slotExpunge(); void imapError(const QString &message); void networkError(const QString &message); void cacheError(const QString &message); void authenticationRequested(); void authenticationContinue(const QString &pass, const QString &errorMessage = QString()); void checkSslPolicy(); void slotManageContact(const QUrl &url); void slotEditDraft(); void slotReplyTo(); void slotReplyAllButMe(); void slotReplyAll(); void slotReplyList(); void slotReplyGuess(); void slotForwardAsAttachment(); void slotBounce(); void slotUpdateMessageActions(); void handleTag(const bool checked, const int index); void handleMarkAsRead(bool); void handleMarkAsDeleted(bool); void handleMarkAsFlagged(const bool); void handleMarkAsJunk(const bool); void handleMarkAsNotJunk(const bool); void slotMoveToArchiveFailed(const QString &error); void handleMoveToArchive(); void slotNextUnread(); void slotPreviousUnread(); void msgListClicked(const QModelIndex &); void openCompleteMessageWidget(); void slotCreateMailboxBelowCurrent(); void slotMarkCurrentMailboxRead(); void slotCreateTopMailbox(); void slotDeleteCurrentMailbox(); void handleTrayIconChange(); #ifdef XTUPLE_CONNECT void slotXtSyncCurrentMailbox(); #endif void slotSubscribeCurrentMailbox(); void slotShowOnlySubscribed(); void updateMessageFlags(); void updateMessageFlagsOf(const QModelIndex &index); void scrollMessageUp(); void slotMessageModelChanged(QAbstractItemModel *model); void showConnectionStatus(uint parserId, Imap::ConnectionState state); void slotShowLinkTarget(const QString &link); void slotShowAboutTrojita(); void slotDonateToTrojita(); void slotSaveCurrentMessageBody(); void slotViewMsgSource(); void slotViewMsgHeaders(); void slotThreadMsgList(); void slotHideRead(); void slotSortingPreferenceChanged(); void slotSortingConfirmed(int column, Qt::SortOrder order); void slotSearchRequested(const QStringList &searchConditions); void slotCapabilitiesUpdated(const QStringList &capabilities); void slotMailboxDeleteFailed(const QString &mailbox, const QString &msg); void slotMailboxCreateFailed(const QString &mailbox, const QString &msg); void slotMailboxSyncFailed(const QString &mailbox, const QString &msg); void slotMailboxChanged(const QModelIndex &mailbox); void slotDownloadTransferError(const QString &errorString); void slotDownloadMessageFileNameRequested(QString *fileName); void slotScrollToUnseenMessage(); void slotScrollToCurrent(); void slotUpdateWindowTitle(); void slotLayoutCompact(); void slotLayoutWide(); void slotLayoutOneAtTime(); void saveSizesAndState(); void saveRawStateSetting(bool enabled); void possiblyLoadMessageOnSplittersChanged(); void updateNetworkIndication(); void desktopGeometryChanged(); void slotIconActivated(const QSystemTrayIcon::ActivationReason reason); void slotToggleSysTray(); void slotResetReconnectState(); void slotPluginsChanged(); void showStatusMessage(const QString &message); void slotFavoriteTagsChanged(); protected: void resizeEvent(QResizeEvent *); private: void defineActions(); void createMenus(); void createActions(); void createWidgets(); void setupModels(); void nukeModels(); void connectModelActions(); void createMailboxBelow(const QModelIndex &index); void updateActionsOnlineOffline(bool online); void applySizesAndState(); QString settingsKeyForLayout(const LayoutMode layout); void recoverDrafts(); void createSysTray(); void removeSysTray(); QModelIndexList translatedSelection() const; Imap::ImapAccess *m_imapAccess; Imap::Mailbox::PrettyMailboxModel *prettyMboxModel; Imap::Mailbox::PrettyMsgListModel *prettyMsgListModel; Composer::SenderIdentitiesModel *m_senderIdentities; Imap::Mailbox::FavoriteTagsModel *m_favoriteTags; MailBoxTreeView *mboxTree; MessageListWidget *msgListWidget; QTreeView *allTree; QDockWidget *allDock; QTreeView *taskTree; QDockWidget *taskDock; QTreeView *mailMimeTree; QDockWidget *mailMimeDock; CompleteMessageWidget *m_messageWidget; ProtocolLoggerWidget *imapLogger; QDockWidget *imapLoggerDock; QPointer m_mainHSplitter; QPointer m_mainVSplitter; QPointer m_mainStack; LayoutMode m_layoutMode; bool m_skipSavingOfUI; QTimer *m_delayedStateSaving; QAction *reloadMboxList; QAction *reloadAllMailboxes; QAction *resyncMbox; QAction *netOffline; QAction *netExpensive; QAction *netOnline; QAction *m_netToolbarDefaultAction; QAction *exitAction; QAction *showFullView; QAction *showTaskView; QAction *showMimeView; QAction *showImapLogger; QAction *logPersistent; QAction *showImapCapabilities; QAction *showMenuBar; QAction *showToolBar; QAction *configSettings; QAction *m_oneAtTimeGoBack; QAction *composeMail; QAction *m_editDraft; QAction *m_replyPrivate; QAction *m_replyAllButMe; QAction *m_replyAll; QAction *m_replyList; QAction *m_replyGuess; QAction *m_forwardAsAttachment; QAction *m_bounce; QAction *expunge; QAction *createChildMailbox; QAction *createTopMailbox; QAction *deleteCurrentMailbox; #ifdef XTUPLE_CONNECT QAction *xtIncludeMailboxInSync; #endif QAction *aboutTrojita; QAction *donateToTrojita; QAction *tag1; QAction *tag2; QAction *tag3; QAction *tag4; QAction *tag5; QAction *tag6; QAction *tag7; QAction *tag8; QAction *tag9; QAction *markAsRead; QAction *markAsDeleted; QAction *markAsFlagged; QAction *markAsJunk; QAction *markAsNotJunk; QAction *moveToArchive; QAction *saveWholeMessage; QAction *viewMsgSource; QAction *viewMsgHeaders; QAction *m_nextMessage; QAction *m_previousMessage; QAction *actionThreadMsgList; QAction *actionHideRead; QAction *m_actionSortByArrival; QAction *m_actionSortByCc; QAction *m_actionSortByDate; QAction *m_actionSortByFrom; QAction *m_actionSortBySize; QAction *m_actionSortBySubject; QAction *m_actionSortByTo; QAction *m_actionSortThreading; QAction *m_actionSortNone; QAction *m_actionSortAscending; QAction *m_actionSortDescending; QAction *m_actionLayoutCompact; QAction *m_actionLayoutWide; QAction *m_actionLayoutOneAtTime; QAction *m_actionMarkMailboxAsRead; QAction *m_actionSubscribeMailbox; QAction *m_actionShowOnlySubscribed; QAction *m_actionContactEditor; QToolBar *m_mainToolbar; QToolButton *m_menuFromToolBar; QToolButton *m_replyButton; QMenu *m_replyMenu; QToolButton *m_composeButton; QMenu *m_composeMenu; TaskProgressIndicator *busyParsersIndicator; QToolButton *networkIndicator; bool m_ignoreStoredPassword; QSettings *m_settings; Plugins::PluginManager *m_pluginManager; QMessageBox *m_networkErrorMessageBox; MainWindow(const MainWindow &); // don't implement MainWindow &operator=(const MainWindow &); // don't implement QSystemTrayIcon *m_trayIcon; QPoint m_headerDragStart; }; } #endif