File indexing completed on 2024-11-24 04:53:00

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002    Copyright (C) 2012 Peter Amidon <peter@picnicpark.org>
0003    Copyright (C) 2013 - 2014 Pali Rohár <pali.rohar@gmail.com>
0004 
0005    This file is part of the Trojita Qt IMAP e-mail client,
0006    http://trojita.flaska.net/
0007 
0008    This program is free software; you can redistribute it and/or
0009    modify it under the terms of the GNU General Public License as
0010    published by the Free Software Foundation; either version 2 of
0011    the License or (at your option) version 3 or any later version
0012    accepted by the membership of KDE e.V. (or its successor approved
0013    by the membership of KDE e.V.), which shall act as a proxy
0014    defined in Section 14 of version 3 of the license.
0015 
0016    This program is distributed in the hope that it will be useful,
0017    but WITHOUT ANY WARRANTY; without even the implied warranty of
0018    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0019    GNU General Public License for more details.
0020 
0021    You should have received a copy of the GNU General Public License
0022    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0023 */
0024 #include <QAbstractProxyModel>
0025 #include <QBuffer>
0026 #include <QFileDialog>
0027 #include <QGraphicsOpacityEffect>
0028 #include <QKeyEvent>
0029 #include <QMenu>
0030 #include <QMessageBox>
0031 #include <QProgressDialog>
0032 #include <QPropertyAnimation>
0033 #include <QRegularExpression>
0034 #include <QSettings>
0035 #include <QScreen>
0036 #include <QTimer>
0037 #include <QToolButton>
0038 #include <QUrlQuery>
0039 
0040 #include "ui_ComposeWidget.h"
0041 #include "Composer/ExistingMessageComposer.h"
0042 #include "Composer/MessageComposer.h"
0043 #include "Composer/ReplaceSignature.h"
0044 #include "Composer/Mailto.h"
0045 #include "Composer/SenderIdentitiesModel.h"
0046 #include "Composer/Submission.h"
0047 #include "Common/InvokeMethod.h"
0048 #include "Common/Paths.h"
0049 #include "Common/SettingsNames.h"
0050 #include "Gui/CompleteMessageWidget.h"
0051 #include "Gui/ComposeWidget.h"
0052 #include "Gui/FromAddressProxyModel.h"
0053 #include "Gui/LineEdit.h"
0054 #include "Gui/MessageView.h"
0055 #include "Gui/OverlayWidget.h"
0056 #include "Gui/PasswordDialog.h"
0057 #include "Gui/ProgressPopUp.h"
0058 #include "Gui/Util.h"
0059 #include "Gui/Window.h"
0060 #include "Imap/Model/ImapAccess.h"
0061 #include "Imap/Model/ItemRoles.h"
0062 #include "Imap/Model/Model.h"
0063 #include "Imap/Parser/MailAddress.h"
0064 #include "Imap/Tasks/AppendTask.h"
0065 #include "Imap/Tasks/GenUrlAuthTask.h"
0066 #include "Imap/Tasks/UidSubmitTask.h"
0067 #include "Plugins/AddressbookPlugin.h"
0068 #include "Plugins/PluginManager.h"
0069 #include "ShortcutHandler/ShortcutHandler.h"
0070 #include "UiUtils/Color.h"
0071 #include "UiUtils/IconLoader.h"
0072 
0073 namespace
0074 {
0075 enum { OFFSET_OF_FIRST_ADDRESSEE = 1, MIN_MAX_VISIBLE_RECIPIENTS = 4 };
0076 }
0077 
0078 namespace Gui
0079 {
0080 
0081 static const QString trojita_opacityAnimation = QStringLiteral("trojita_opacityAnimation");
0082 
0083 /** @short Keep track of whether the document has been updated since the last save */
0084 class ComposerSaveState
0085 {
0086 public:
0087     explicit ComposerSaveState(ComposeWidget* w)
0088         : composer(w)
0089         , messageUpdated(false)
0090         , messageEverEdited(false)
0091     {
0092     }
0093 
0094     void setMessageUpdated(bool updated)
0095     {
0096         if (updated == messageUpdated)
0097             return;
0098         messageUpdated = updated;
0099         updateText();
0100 
0101     }
0102 
0103     void setMessageEverEdited(bool everEdited)
0104     {
0105         if (everEdited == messageEverEdited)
0106             return;
0107         messageEverEdited = everEdited;
0108         updateText();
0109     }
0110 
0111     bool everEdited() {return messageEverEdited;}
0112     bool updated() {return messageUpdated;}
0113 private:
0114     ComposeWidget* composer;
0115     /** @short Has it been updated since the last time we auto-saved it? */
0116     bool messageUpdated;
0117     /** @short Was this message ever editted by human?
0118 
0119     We have to track both of these. Simply changing the sender (and hence the signature) without any text being written
0120     shall not trigger automatic saving, but on the other hand changing the sender after something was already written
0121     is an important change.
0122     */
0123     bool messageEverEdited;
0124     void updateText()
0125     {
0126         composer->cancelButton->setText((messageUpdated || messageEverEdited) ? QWidget::tr("Cancel...") : QWidget::tr("Cancel"));
0127     }
0128 };
0129 
0130 /** @short Ignore dirtying events while we're preparing the widget's contents
0131 
0132 Under the normal course of operation, there's plenty of events (user typing some text, etc) which lead to the composer widget
0133 "remembering" that the human being has made some changes, and that these changes are probably worth a prompt for saving them
0134 upon a close.
0135 
0136 This guard object makes sure (via RAII) that these dirtifying events are ignored during its lifetime.
0137 */
0138 class InhibitComposerDirtying
0139 {
0140 public:
0141     explicit InhibitComposerDirtying(ComposeWidget *w): w(w), wasEverEdited(w->m_saveState->everEdited()), wasEverUpdated(w->m_saveState->updated()) {}
0142     ~InhibitComposerDirtying()
0143     {
0144         w->m_saveState->setMessageEverEdited(wasEverEdited);
0145         w->m_saveState->setMessageUpdated(wasEverUpdated);
0146     }
0147 private:
0148     ComposeWidget *w;
0149     bool wasEverEdited, wasEverUpdated;
0150 };
0151 
0152 ComposeWidget::ComposeWidget(MainWindow *mainWindow, std::shared_ptr<Composer::AbstractComposer> messageComposer, MSA::MSAFactory *msaFactory)
0153     : QWidget(0, Qt::Window)
0154     , ui(new Ui::ComposeWidget)
0155     , m_maxVisibleRecipients(MIN_MAX_VISIBLE_RECIPIENTS)
0156     , m_sentMail(false)
0157     , m_explicitDraft(false)
0158     , m_appendUidReceived(false)
0159     , m_appendUidValidity(0)
0160     , m_appendUid(0)
0161     , m_genUrlAuthReceived(false)
0162     , m_mainWindow(mainWindow)
0163     , m_settings(mainWindow->settings())
0164     , m_composer(messageComposer)
0165     , m_submission(nullptr)
0166     , m_completionPopup(nullptr)
0167     , m_completionReceiver(nullptr)
0168 {
0169     setAttribute(Qt::WA_DeleteOnClose, true);
0170 
0171     QIcon winIcon;
0172     winIcon.addFile(QStringLiteral(":/icons/trojita-edit-big.svg"), QSize(128, 128));
0173     winIcon.addFile(QStringLiteral(":/icons/trojita-edit-small.svg"), QSize(22, 22));
0174     setWindowIcon(winIcon);
0175 
0176     Q_ASSERT(m_mainWindow);
0177     m_mainWindow->registerComposeWindow(this);
0178     QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
0179     QString accountId = profileName.isEmpty() ? QStringLiteral("account-0") : profileName;
0180     m_submission = new Composer::Submission(this, m_composer, m_mainWindow->imapModel(), msaFactory, accountId);
0181     connect(m_submission, &Composer::Submission::succeeded, this, &ComposeWidget::sent);
0182     connect(m_submission, &Composer::Submission::failed, this, &ComposeWidget::gotError);
0183     connect(m_submission, &Composer::Submission::failed, this, [this](const QString& message) {
0184         emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("ComposeWidget"), message);
0185     });
0186     connect(m_submission, &Composer::Submission::logged, this, &ComposeWidget::logged);
0187     connect(m_submission, &Composer::Submission::passwordRequested, this, &ComposeWidget::passwordRequested, Qt::QueuedConnection);
0188     ui->setupUi(this);
0189 
0190     if (interactiveComposer()) {
0191         interactiveComposer()->setReportTrojitaVersions(m_settings->value(Common::SettingsNames::interopRevealVersions, true).toBool());
0192         ui->attachmentsView->setComposer(interactiveComposer());
0193     }
0194 
0195     sendButton = ui->buttonBox->addButton(tr("Send"), QDialogButtonBox::AcceptRole);
0196     sendButton->setIcon(UiUtils::loadIcon(QStringLiteral("mail-send")));
0197     connect(sendButton, &QAbstractButton::clicked, this, &ComposeWidget::send);
0198     cancelButton = ui->buttonBox->addButton(QDialogButtonBox::Cancel);
0199     cancelButton->setIcon(UiUtils::loadIcon(QStringLiteral("dialog-cancel")));
0200     connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::close);
0201     connect(ui->attachButton, &QAbstractButton::clicked, this, &ComposeWidget::slotAskForFileAttachment);
0202 
0203     m_saveState = std::unique_ptr<ComposerSaveState>(new ComposerSaveState(this));
0204 
0205     m_completionPopup = new QMenu(this);
0206     m_completionPopup->installEventFilter(this);
0207     connect(m_completionPopup, &QMenu::triggered, this, &ComposeWidget::completeRecipient);
0208 
0209     // TODO: make this configurable?
0210     m_completionCount = 8;
0211 
0212     m_recipientListUpdateTimer = new QTimer(this);
0213     m_recipientListUpdateTimer->setSingleShot(true);
0214     m_recipientListUpdateTimer->setInterval(250);
0215     connect(m_recipientListUpdateTimer, &QTimer::timeout, this, &ComposeWidget::updateRecipientList);
0216 
0217     connect(ui->verticalSplitter, &QSplitter::splitterMoved, this, &ComposeWidget::calculateMaxVisibleRecipients);
0218     calculateMaxVisibleRecipients();
0219 
0220     connect(ui->recipientSlider, &QAbstractSlider::valueChanged, this, &ComposeWidget::scrollRecipients);
0221     connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange);
0222     ui->recipientSlider->setMinimum(0);
0223     ui->recipientSlider->setMaximum(0);
0224     ui->recipientSlider->setVisible(false);
0225     ui->envelopeWidget->installEventFilter(this);
0226 
0227     m_markButton = new QToolButton(ui->buttonBox);
0228     m_markButton->setPopupMode(QToolButton::MenuButtonPopup);
0229     m_markButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0230     m_markAsReply = new QActionGroup(m_markButton);
0231     m_markAsReply->setExclusive(true);
0232     auto *asReplyMenu = new QMenu(m_markButton);
0233     m_markButton->setMenu(asReplyMenu);
0234     m_actionStandalone = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-fill")), tr("New Thread"));
0235     m_actionStandalone->setActionGroup(m_markAsReply);
0236     m_actionStandalone->setCheckable(true);
0237     m_actionStandalone->setToolTip(tr("This mail will be sent as a standalone message.<hr/>Change to preserve the reply hierarchy."));
0238     m_actionInReplyTo = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Threaded"));
0239     m_actionInReplyTo->setActionGroup(m_markAsReply);
0240     m_actionInReplyTo->setCheckable(true);
0241 
0242     // This is a "quick shortcut action". It shows the UI bits of the current option, but when the user clicks it,
0243     // the *other* action is triggered.
0244     m_actionToggleMarking = new QAction(m_markButton);
0245     connect(m_actionToggleMarking, &QAction::triggered, this, &ComposeWidget::toggleReplyMarking);
0246     m_markButton->setDefaultAction(m_actionToggleMarking);
0247 
0248     // Unfortunately, there's no signal for toggled(QAction*), so we'll have to call QAction::trigger() to have this working
0249     connect(m_markAsReply, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMarkingAction);
0250     m_actionStandalone->trigger();
0251 
0252     m_replyModeButton = new QToolButton(ui->buttonBox);
0253     m_replyModeButton->setPopupMode(QToolButton::InstantPopup);
0254     m_replyModeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0255 
0256     QMenu *replyModeMenu = new QMenu(m_replyModeButton);
0257     m_replyModeButton->setMenu(replyModeMenu);
0258 
0259     m_replyModeActions = new QActionGroup(m_replyModeButton);
0260     m_replyModeActions->setExclusive(true);
0261 
0262     m_actionHandPickedRecipients = new QAction(UiUtils::loadIcon(QStringLiteral("document-edit")) ,QStringLiteral("Hand Picked Recipients"), this);
0263     replyModeMenu->addAction(m_actionHandPickedRecipients);
0264     m_actionHandPickedRecipients->setActionGroup(m_replyModeActions);
0265     m_actionHandPickedRecipients->setCheckable(true);
0266 
0267     replyModeMenu->addSeparator();
0268 
0269     QAction *placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_private"));
0270     m_actionReplyModePrivate = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
0271     m_actionReplyModePrivate->setActionGroup(m_replyModeActions);
0272     m_actionReplyModePrivate->setCheckable(true);
0273 
0274     placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all_but_me"));
0275     m_actionReplyModeAllButMe = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
0276     m_actionReplyModeAllButMe->setActionGroup(m_replyModeActions);
0277     m_actionReplyModeAllButMe->setCheckable(true);
0278 
0279     placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all"));
0280     m_actionReplyModeAll = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
0281     m_actionReplyModeAll->setActionGroup(m_replyModeActions);
0282     m_actionReplyModeAll->setCheckable(true);
0283 
0284     placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_list"));
0285     m_actionReplyModeList = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
0286     m_actionReplyModeList->setActionGroup(m_replyModeActions);
0287     m_actionReplyModeList->setCheckable(true);
0288 
0289     connect(m_replyModeActions, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMode);
0290 
0291     // We want to have the button aligned to the left; the only "portable" way of this is the ResetRole
0292     // (thanks to TL for mentioning this, and for the Qt's doc for providing pretty pictures on different platforms)
0293     ui->buttonBox->addButton(m_markButton, QDialogButtonBox::ResetRole);
0294     // Using ResetRole for reasons same as with m_markButton. We want this button to be second from the left.
0295     ui->buttonBox->addButton(m_replyModeButton, QDialogButtonBox::ResetRole);
0296 
0297     m_markButton->hide();
0298     m_replyModeButton->hide();
0299 
0300     if (auto spellchecker = m_mainWindow->pluginManager()->spellchecker()) {
0301         spellchecker->actOnEditor(ui->mailText);
0302     }
0303 
0304     connect(ui->mailText, &ComposerTextEdit::urlsAdded, this, &ComposeWidget::slotAttachFiles);
0305     connect(ui->mailText, &ComposerTextEdit::sendRequest, this, &ComposeWidget::send);
0306     connect(ui->mailText, &QTextEdit::textChanged, this, &ComposeWidget::setMessageUpdated);
0307     connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::updateWindowTitle);
0308     connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated);
0309     connect(ui->subject, &QLineEdit::returnPressed, this, [=]() { ui->mailText->setFocus(); });
0310     updateWindowTitle();
0311 
0312     FromAddressProxyModel *proxy = new FromAddressProxyModel(this);
0313     proxy->setSourceModel(m_mainWindow->senderIdentitiesModel());
0314     ui->sender->setModel(proxy);
0315 
0316     connect(ui->sender, static_cast<void (QComboBox::*)(const int)>(&QComboBox::currentIndexChanged), this, &ComposeWidget::slotUpdateSignature);
0317     connect(ui->sender, &QComboBox::editTextChanged, this, &ComposeWidget::setMessageUpdated);
0318     connect(ui->sender->lineEdit(), &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender);
0319 
0320     QTimer *autoSaveTimer = new QTimer(this);
0321     connect(autoSaveTimer, &QTimer::timeout, this, &ComposeWidget::autoSaveDraft);
0322     autoSaveTimer->start(30*1000);
0323 
0324     // these are for the automatically saved drafts, i.e. no i18n for the dir name
0325     m_autoSavePath = QString(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/"));
0326     QDir().mkpath(m_autoSavePath);
0327 
0328     m_autoSavePath += QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1String(".draft");
0329 
0330     // Add a blank recipient row to start with
0331     addRecipient(m_recipients.count(), interactiveComposer() ? Composer::ADDRESS_TO : Composer::ADDRESS_RESENT_TO, QString());
0332     ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus();
0333 
0334     slotUpdateSignature();
0335 
0336     // default size
0337     int sz = ui->mailText->idealWidth();
0338     ui->mailText->setMinimumSize(sz, 1000*sz/1618); // golden mean editor
0339     adjustSize();
0340     ui->mailText->setMinimumSize(0, 0);
0341     resize(size().boundedTo(QGuiApplication::primaryScreen()->availableGeometry().size()));
0342 }
0343 
0344 ComposeWidget::~ComposeWidget()
0345 {
0346     delete ui;
0347 }
0348 
0349 std::shared_ptr<Composer::MessageComposer> ComposeWidget::interactiveComposer()
0350 {
0351     return std::dynamic_pointer_cast<Composer::MessageComposer>(m_composer);
0352 }
0353 
0354 /** @short Throw a warning at an attempt to create a Compose Widget while the MSA is not configured */
0355 ComposeWidget *ComposeWidget::warnIfMsaNotConfigured(ComposeWidget *widget, MainWindow *mainWindow)
0356 {
0357     if (!widget)
0358         QMessageBox::critical(mainWindow, tr("Error"), tr("Please set appropriate settings for outgoing messages."));
0359     return widget;
0360 }
0361 
0362 /** @short Find a nice position near the mid of the main window, try to not fully occlude another sibling */
0363 void ComposeWidget::placeOnMainWindow()
0364 {
0365     QRect area = m_mainWindow->geometry();
0366     QRect origin(0, 0, width(), height());
0367     origin.moveTo(area.x() + (area.width() - width()) / 2,
0368                   area.y() + (area.height() - height()) / 2);
0369     QRect target = origin;
0370 
0371     QWidgetList siblings;
0372     foreach(const QWidget *w, QApplication::topLevelWidgets()) {
0373         if (w == this)
0374             continue; // I'm not a sibling of myself
0375         if (!qobject_cast<const ComposeWidget*>(w))
0376             continue; // random other stuff
0377         siblings << const_cast<QWidget*>(w);
0378     }
0379     int dx = 20, dy = 20;
0380     int i = 0;
0381     // look for a position where the window would not fully cover another composer
0382     // (we don't want to mass open 10 composers stashing each other)
0383     // if such composer blocks our desired geometry, the new desired geometry is
0384     // tested at positions shifted by 20px circling around the original one.
0385     // if we're already more than 100px off the center (what implies the user
0386     // has > 20 composers open ...) we give up to not shift the window
0387     // too far away, maybe even off-screen.
0388     // Notice that it may still happen that some composers *together* stash a 3rd one
0389     while (i < siblings.count()) {
0390         if (target.contains(siblings.at(i)->geometry())) {
0391             target = origin.translated(dx, dy);
0392             if (dx < 0 && dy < 0) {
0393                 dx = dy = -dx + 20;
0394                 if (dx >= 120) // give up
0395                     break;
0396             } else if (dx < 0 || dy < 0) {
0397                 dx = -dx;
0398                 if (dy > 0)
0399                     dy = -dy;
0400             } else {
0401                 dx = -dx;
0402             }
0403             i = 0;
0404         } else {
0405             ++i;
0406         }
0407     }
0408     setGeometry(target);
0409 }
0410 
0411 /** @short Create a blank composer window */
0412 ComposeWidget *ComposeWidget::createBlank(MainWindow *mainWindow)
0413 {
0414     MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
0415     if (!msaFactory)
0416         return 0;
0417 
0418     auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
0419     ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
0420     w->placeOnMainWindow();
0421     w->show();
0422     return w;
0423 }
0424 
0425 /** @short Load a draft in composer window */
0426 ComposeWidget *ComposeWidget::createDraft(MainWindow *mainWindow, const QString &path)
0427 {
0428     MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
0429     if (!msaFactory)
0430         return 0;
0431 
0432     auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
0433     ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
0434     w->loadDraft(path);
0435     w->placeOnMainWindow();
0436     w->show();
0437     return w;
0438 }
0439 
0440 /** @short Create a composer window with data from a URL */
0441 ComposeWidget *ComposeWidget::createFromUrl(MainWindow *mainWindow, const QUrl &url)
0442 {
0443     MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
0444     if (!msaFactory)
0445         return 0;
0446 
0447     auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
0448     ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
0449     InhibitComposerDirtying inhibitor(w);
0450     QString subject;
0451     QString body;
0452     QList<QPair<Composer::RecipientKind,QString> > recipients;
0453     QList<QByteArray> inReplyTo;
0454     QList<QByteArray> references;
0455     const QUrlQuery q(url);
0456 
0457     if (!q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")).isEmpty()) {
0458         // There should be only single email address created by Imap::Message::MailAddress::asUrl()
0459         Imap::Message::MailAddress addr;
0460         if (Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("mailto")))
0461             recipients << qMakePair(Composer::ADDRESS_TO, addr.asPrettyString());
0462     } else {
0463         // This should be real RFC 6068 mailto:
0464         Composer::parseRFC6068Mailto(url, subject, body, recipients, inReplyTo, references);
0465     }
0466 
0467     // NOTE: we need inReplyTo and references parameters without angle brackets, so remove them
0468     for (int i = 0; i < inReplyTo.size(); ++i) {
0469         if (inReplyTo[i].startsWith('<') && inReplyTo[i].endsWith('>')) {
0470             inReplyTo[i] = inReplyTo[i].mid(1, inReplyTo[i].size()-2);
0471         }
0472     }
0473     for (int i = 0; i < references.size(); ++i) {
0474         if (references[i].startsWith('<') && references[i].endsWith('>')) {
0475             references[i] = references[i].mid(1, references[i].size()-2);
0476         }
0477     }
0478 
0479     w->setResponseData(recipients, subject, body, inReplyTo, references, QModelIndex());
0480     if (!inReplyTo.isEmpty() || !references.isEmpty()) {
0481       // 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
0482       w->m_actionInReplyTo->setChecked(true);
0483     }
0484     w->placeOnMainWindow();
0485     w->show();
0486     return w;
0487 }
0488 
0489 /** @short Create a composer window for a reply */
0490 ComposeWidget *ComposeWidget::createReply(MainWindow *mainWindow, const Composer::ReplyMode &mode, const QModelIndex &replyingToMessage,
0491                                           const QList<QPair<Composer::RecipientKind, QString> > &recipients, const QString &subject,
0492                                           const QString &body, const QList<QByteArray> &inReplyTo, const QList<QByteArray> &references)
0493 {
0494     MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
0495     if (!msaFactory)
0496         return 0;
0497 
0498     auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
0499     ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
0500     InhibitComposerDirtying inhibitor(w);
0501     w->setResponseData(recipients, subject, body, inReplyTo, references, replyingToMessage);
0502     bool ok = w->setReplyMode(mode);
0503     if (!ok) {
0504         QString err;
0505         switch (mode) {
0506         case Composer::REPLY_ALL:
0507         case Composer::REPLY_ALL_BUT_ME:
0508             // do nothing
0509             break;
0510         case Composer::REPLY_LIST:
0511             err = tr("It doesn't look like this is a message to the mailing list. Please fill in the recipients manually.");
0512             break;
0513         case Composer::REPLY_PRIVATE:
0514             err = tr("Trojitá was unable to safely determine the real e-mail address of the author of the message. "
0515                          "You might want to use the \"Reply All\" function and trim the list of addresses manually.");
0516             break;
0517         }
0518         if (!err.isEmpty()) {
0519             Gui::Util::messageBoxWarning(w, tr("Cannot Determine Recipients"), err);
0520         }
0521     }
0522     w->placeOnMainWindow();
0523     w->show();
0524     return w;
0525 }
0526 
0527 /** @short Create a composer window for a mail-forward action */
0528 ComposeWidget *ComposeWidget::createForward(MainWindow *mainWindow, const Composer::ForwardMode mode, const QModelIndex &forwardingMessage,
0529                                     const QString &subject, const QList<QByteArray> &inReplyTo, const QList<QByteArray> &references)
0530 {
0531     MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
0532     if (!msaFactory)
0533         return 0;
0534 
0535     auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
0536     ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
0537     InhibitComposerDirtying inhibitor(w);
0538     w->setResponseData(QList<QPair<Composer::RecipientKind, QString>>(), subject, QString(), inReplyTo, references, QModelIndex());
0539     // 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
0540     w->m_actionInReplyTo->setChecked(true);
0541 
0542     // Prepare the message to be forwarded and add it to the attachments view
0543     w->interactiveComposer()->prepareForwarding(forwardingMessage, mode);
0544 
0545     w->placeOnMainWindow();
0546     w->show();
0547     return w;
0548 }
0549 
0550 ComposeWidget *ComposeWidget::createFromReadOnly(MainWindow *mainWindow, const QModelIndex &messageRoot,
0551                                                  const QList<QPair<Composer::RecipientKind, QString>>& recipients)
0552 {
0553     MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
0554     if (!msaFactory)
0555         return 0;
0556 
0557     auto composer = std::make_shared<Composer::ExistingMessageComposer>(messageRoot);
0558     ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
0559 
0560     for (int i = 0; i < recipients.size(); ++i) {
0561         w->addRecipient(i, recipients[i].first, recipients[i].second);
0562     }
0563     w->updateRecipientList();
0564 
0565     // Disable what needs to be nuked
0566     w->ui->fromLabel->setText(tr("Sender"));
0567     w->ui->subject->hide();
0568     w->ui->subjectLabel->hide();
0569     w->ui->attachmentBox->hide();
0570     w->ui->mailText->hide();
0571     auto subject = messageRoot.data(Imap::Mailbox::RoleMessageSubject).toString();
0572     w->setWindowTitle(tr("Resend Mail: %1").arg(subject.isEmpty() ? tr("(no subject)") : subject));
0573 
0574     // Show the full content of that e-mail as the "main body" within this widget
0575     CompleteMessageWidget *messageWidget = new CompleteMessageWidget(w, mainWindow->settings(), mainWindow->pluginManager(), mainWindow->favoriteTagsModel());
0576     messageWidget->messageView->setMessage(messageRoot);
0577     messageWidget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(mainWindow->imapAccess()->networkWatcher()));
0578     messageWidget->setFocusPolicy(Qt::StrongFocus);
0579     w->ui->verticalSplitter->insertWidget(1, messageWidget);
0580     w->ui->verticalSplitter->setStretchFactor(1, 100);
0581 
0582     QStringList warnings;
0583     if (subject.isEmpty()) {
0584         warnings << tr("Message has no subject");
0585     }
0586     if (messageRoot.data(Imap::Mailbox::RoleMessageMessageId).toByteArray().isEmpty()) {
0587         warnings << tr("The Message-ID header is missing");
0588     }
0589     if (!messageRoot.data(Imap::Mailbox::RoleMessageDate).toDateTime().isValid()) {
0590         warnings << tr("Message has no date");
0591     }
0592     if (messageRoot.data(Imap::Mailbox::RoleMessageFrom).toList().isEmpty()) {
0593         warnings << tr("Nothing in the From field");
0594     }
0595     if (messageRoot.data(Imap::Mailbox::RoleMessageTo).toList().isEmpty()) {
0596         warnings << tr("No recipients in the To field");
0597     }
0598     if (!warnings.isEmpty()) {
0599         auto lbl = new QLabel(tr("<b>This message appears to be malformed, please be careful before sending it.</b>")
0600                               + QStringLiteral("<ul><li>") + warnings.join(QStringLiteral("</li><li>")) + QStringLiteral("</li></ul>"),
0601                               w);
0602         lbl->setStyleSheet(Gui::Util::cssWarningBorder());
0603         w->ui->verticalSplitter->insertWidget(1, lbl);
0604     }
0605 
0606     w->placeOnMainWindow();
0607     w->show();
0608     return w;
0609 }
0610 
0611 void ComposeWidget::updateReplyMode()
0612 {
0613     bool replyModeSet = false;
0614     if (m_actionReplyModePrivate->isChecked()) {
0615         replyModeSet = setReplyMode(Composer::REPLY_PRIVATE);
0616     } else if (m_actionReplyModeAllButMe->isChecked()) {
0617         replyModeSet = setReplyMode(Composer::REPLY_ALL_BUT_ME);
0618     } else if (m_actionReplyModeAll->isChecked()) {
0619         replyModeSet = setReplyMode(Composer::REPLY_ALL);
0620     } else if (m_actionReplyModeList->isChecked()) {
0621         replyModeSet = setReplyMode(Composer::REPLY_LIST);
0622     }
0623 
0624     if (!replyModeSet) {
0625         // This is for now by design going in one direction only, from enabled to disabled.
0626         // The index to the message cannot become valid again, and simply marking the buttons as disabled does the trick quite neatly.
0627         m_replyModeButton->setEnabled(m_actionHandPickedRecipients->isChecked());
0628         markReplyModeHandpicked();
0629     }
0630 }
0631 
0632 void ComposeWidget::markReplyModeHandpicked()
0633 {
0634     m_actionHandPickedRecipients->setChecked(true);
0635     m_replyModeButton->setText(m_actionHandPickedRecipients->text());
0636     m_replyModeButton->setIcon(m_actionHandPickedRecipients->icon());
0637 }
0638 
0639 void ComposeWidget::passwordRequested(const QString &user, const QString &host)
0640 {
0641     if (m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool()) {
0642         auto password = qobject_cast<const Imap::Mailbox::Model*>(m_mainWindow->imapAccess()->imapModel())->imapPassword();
0643         if (password.isNull()) {
0644             // This can happen for example when we've always been offline since the last profile change,
0645             // and the IMAP password is therefore not already cached in the IMAP model.
0646 
0647             // FIXME: it would be nice to "just" call out to MainWindow::authenticationRequested() in that case,
0648             // but there's no async callback when the password is available. Just some food for thought when
0649             // that part gets refactored :), eventually...
0650             askPassword(user, host);
0651         } else {
0652             m_submission->setPassword(password);
0653         }
0654         return;
0655     }
0656 
0657     Plugins::PasswordPlugin *password = m_mainWindow->pluginManager()->password();
0658     if (!password) {
0659         askPassword(user, host);
0660         return;
0661     }
0662 
0663     Plugins::PasswordJob *job = password->requestPassword(m_submission->accountId(), QStringLiteral("smtp"));
0664     if (!job) {
0665         askPassword(user, host);
0666         return;
0667     }
0668 
0669     connect(job, &Plugins::PasswordJob::passwordAvailable, m_submission, &Composer::Submission::setPassword);
0670     connect(job, &Plugins::PasswordJob::error, this, &ComposeWidget::passwordError);
0671 
0672     job->setAutoDelete(true);
0673     job->setProperty("user", user);
0674     job->setProperty("host", host);
0675     job->start();
0676 }
0677 
0678 void ComposeWidget::passwordError()
0679 {
0680     Plugins::PasswordJob *job = static_cast<Plugins::PasswordJob *>(sender());
0681     const QString &user = job->property("user").toString();
0682     const QString &host = job->property("host").toString();
0683     askPassword(user, host);
0684 }
0685 
0686 void ComposeWidget::askPassword(const QString &user, const QString &host)
0687 {
0688     auto w = Gui::PasswordDialog::getPassword(this, tr("Authentication Required"),
0689                                               tr("<p>Please provide SMTP password for user <b>%1</b> on <b>%2</b>:</p>").arg(
0690                                                   user.toHtmlEscaped(),
0691                                                   host.toHtmlEscaped()));
0692     connect(w, &Gui::PasswordDialog::gotPassword, m_submission, &Composer::Submission::setPassword);
0693     connect(w, &Gui::PasswordDialog::rejected, m_submission, &Composer::Submission::cancelPassword);
0694 }
0695 
0696 void ComposeWidget::changeEvent(QEvent *e)
0697 {
0698     QWidget::changeEvent(e);
0699     switch (e->type()) {
0700     case QEvent::LanguageChange:
0701         ui->retranslateUi(this);
0702         break;
0703     default:
0704         break;
0705     }
0706 }
0707 
0708 /**
0709  * We capture the close event and check whether there's something to save
0710  * (not sent, not up-to-date or persistent autostore)
0711  * The offer the user to store or omit the message or not close at all
0712  */
0713 
0714 void ComposeWidget::closeEvent(QCloseEvent *ce)
0715 {
0716     const bool noSaveRequired = m_sentMail || !m_saveState->everEdited() ||
0717                                 (m_explicitDraft && !m_saveState->updated())
0718                                 || !interactiveComposer(); // autosave to permanent draft and no update
0719 
0720     if (!noSaveRequired) {  // save is required
0721         QMessageBox msgBox(this);
0722         msgBox.setWindowModality(Qt::WindowModal);
0723         msgBox.setWindowTitle(tr("Save Draft?"));
0724         QString message(tr("The mail has not been sent.<br>Do you want to save the draft?"));
0725         if (ui->attachmentsView->model()->rowCount() > 0)
0726             message += tr("<br><span style=\"color:red\">Warning: Attachments are <b>not</b> saved with the draft!</span>");
0727         msgBox.setText(message);
0728         msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
0729         msgBox.setDefaultButton(QMessageBox::Save);
0730         int ret = msgBox.exec();
0731         if (ret == QMessageBox::Save) {
0732             if (m_explicitDraft) { // editing a present draft - override it
0733                 saveDraft(m_autoSavePath);
0734             } else {
0735                 // Explicitly stored drafts should be saved in a location with proper i18n support, so let's make sure both main
0736                 // window and this code uses the same tr() calls
0737                 QString path(Common::writablePath(Common::LOCATION_DATA) + Gui::MainWindow::tr("Drafts"));
0738                 QDir().mkpath(path);
0739                 QString filename = ui->subject->text();
0740                 if (filename.isEmpty()) {
0741                     filename = QDateTime::currentDateTime().toString(Qt::ISODate);
0742                 }
0743                 // Some characters are best avoided in file names. This is probably not a definitive list, but the hope is that
0744                 // it's going to be more readable than an unformatted hash or similar stuff.  The list of characters was taken
0745                 // from http://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words .
0746                 filename.replace(QRegularExpression(QLatin1String("[/\\\\:\"|<>*?]")), QStringLiteral("_"));
0747                 path = QFileDialog::getSaveFileName(this, tr("Save as"), path + QLatin1Char('/') + filename + QLatin1String(".draft"),
0748                                                     tr("Drafts") + QLatin1String(" (*.draft)"));
0749                 if (path.isEmpty()) { // cancelled save
0750                     ret = QMessageBox::Cancel;
0751                 } else {
0752                     m_explicitDraft = true;
0753                     saveDraft(path);
0754                     if (path != m_autoSavePath) // we can remove the temp save
0755                         QFile::remove(m_autoSavePath);
0756                 }
0757             }
0758         }
0759         if (ret == QMessageBox::Cancel) {
0760             ce->ignore(); // don't close the window
0761             return;
0762         }
0763     }
0764     if (m_sentMail || !m_explicitDraft) // is the mail has been sent or the user does not want to store it
0765         QFile::remove(m_autoSavePath); // get rid of draft
0766     ce->accept(); // ultimately close the window
0767 }
0768 
0769 
0770 
0771 bool ComposeWidget::buildMessageData()
0772 {
0773     // Recipients are checked at all times, including when bouncing/redirecting
0774     QList<QPair<Composer::RecipientKind,Imap::Message::MailAddress> > recipients;
0775     QString errorMessage;
0776     if (!parseRecipients(recipients, errorMessage)) {
0777         gotError(tr("Cannot parse recipients:\n%1").arg(errorMessage));
0778         return false;
0779     }
0780     if (recipients.isEmpty()) {
0781         gotError(tr("You haven't entered any recipients"));
0782         return false;
0783     }
0784     m_composer->setRecipients(recipients);
0785 
0786     // The same applies to the sender which is needed by some MSAs for origin information
0787     Imap::Message::MailAddress fromAddress;
0788     if (!Imap::Message::MailAddress::fromPrettyString(fromAddress, ui->sender->currentText())) {
0789         gotError(tr("The From: address does not look like a valid one"));
0790         return false;
0791     }
0792     m_composer->setFrom(fromAddress);
0793 
0794     if (auto composer = interactiveComposer()) {
0795         if (ui->subject->text().isEmpty()) {
0796             gotError(tr("You haven't entered any subject. Cannot send such a mail, sorry."));
0797             ui->subject->setFocus();
0798             return false;
0799         }
0800 
0801         composer->setTimestamp(QDateTime::currentDateTime());
0802         composer->setSubject(ui->subject->text());
0803 
0804         QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(ui->sender->model());
0805         Q_ASSERT(proxy);
0806 
0807         if (ui->sender->findText(ui->sender->currentText()) != -1) {
0808             QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex());
0809             Q_ASSERT(proxyIndex.isValid());
0810             composer->setOrganization(
0811                         proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(), Composer::SenderIdentitiesModel::COLUMN_ORGANIZATION)
0812                         .data().toString());
0813         }
0814         composer->setText(ui->mailText->toPlainText());
0815 
0816         if (m_actionInReplyTo->isChecked()) {
0817             composer->setInReplyTo(m_inReplyTo);
0818             composer->setReferences(m_references);
0819             composer->setReplyingToMessage(m_replyingToMessage);
0820         } else {
0821             composer->setInReplyTo(QList<QByteArray>());
0822             composer->setReferences(QList<QByteArray>());
0823             composer->setReplyingToMessage(QModelIndex());
0824         }
0825     }
0826 
0827     if (!m_composer->isReadyForSerialization()) {
0828         gotError(tr("Cannot prepare this e-mail for sending: some parts are not available"));
0829         return false;
0830     }
0831 
0832     return true;
0833 }
0834 
0835 void ComposeWidget::send()
0836 {
0837     if (interactiveComposer()) {
0838         // Well, Trojita is of course rock solid and will never ever crash :), but experience has shown that every now and then,
0839         // there is a subtle issue $somewhere. This means that it's probably a good idea to save the draft explicitly -- better
0840         // than losing some work. It's cheap anyway.
0841         saveDraft(m_autoSavePath);
0842     }
0843 
0844     if (!buildMessageData()) {
0845         return;
0846     }
0847 
0848     const bool reuseImapCreds = m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool();
0849     m_submission->setImapOptions(m_settings->value(Common::SettingsNames::composerSaveToImapKey, true).toBool(),
0850                                  m_settings->value(Common::SettingsNames::composerImapSentKey, QStringLiteral("Sent")).toString(),
0851                                  m_settings->value(Common::SettingsNames::imapHostKey).toString(),
0852                                  m_settings->value(Common::SettingsNames::imapUserKey).toString(),
0853                                  m_settings->value(Common::SettingsNames::msaMethodKey).toString() == Common::SettingsNames::methodImapSendmail);
0854     m_submission->setSmtpOptions(m_settings->value(Common::SettingsNames::smtpUseBurlKey, false).toBool(),
0855                                  reuseImapCreds ?
0856                                      m_mainWindow->imapAccess()->username() :
0857                                      m_settings->value(Common::SettingsNames::smtpUserKey).toString());
0858 
0859     ProgressPopUp *progress = new ProgressPopUp();
0860     OverlayWidget *overlay = new OverlayWidget(progress, this);
0861     overlay->show();
0862     setUiWidgetsEnabled(false);
0863 
0864     connect(m_submission, &Composer::Submission::progressMin, progress, &ProgressPopUp::setMinimum);
0865     connect(m_submission, &Composer::Submission::progressMax, progress, &ProgressPopUp::setMaximum);
0866     connect(m_submission, &Composer::Submission::progress, progress, &ProgressPopUp::setValue);
0867     connect(m_submission, &Composer::Submission::updateStatusMessage, progress, &ProgressPopUp::setLabelText);
0868     connect(m_submission, &Composer::Submission::succeeded, overlay, &QObject::deleteLater);
0869     connect(m_submission, &Composer::Submission::failed, overlay, &QObject::deleteLater);
0870 
0871     m_submission->send();
0872 }
0873 
0874 void ComposeWidget::setUiWidgetsEnabled(const bool enabled)
0875 {
0876     ui->verticalSplitter->setEnabled(enabled);
0877     ui->buttonBox->setEnabled(enabled);
0878 }
0879 
0880 /** @short Set private data members to get pre-filled by available parameters
0881 
0882 The semantics of the @arg inReplyTo and @arg references are the same as described for the Composer::MessageComposer,
0883 i.e. the data are not supposed to contain the angle bracket.  If the @arg replyingToMessage is present, it will be used
0884 as an index to a message which will get marked as replied to.  This is needed because IMAP doesn't really support site-wide
0885 search by a Message-Id (and it cannot possibly support it in general, either), and because Trojita's lazy loading and lack
0886 of cross-session persistent indexes means that "mark as replied" and "extract message-id from" are effectively two separate
0887 operations.
0888 */
0889 void ComposeWidget::setResponseData(const QList<QPair<Composer::RecipientKind, QString> > &recipients,
0890                             const QString &subject, const QString &body, const QList<QByteArray> &inReplyTo,
0891                             const QList<QByteArray> &references, const QModelIndex &replyingToMessage)
0892 {
0893     InhibitComposerDirtying inhibitor(this);
0894     for (int i = 0; i < recipients.size(); ++i) {
0895         addRecipient(i, recipients.at(i).first, recipients.at(i).second);
0896     }
0897     updateRecipientList();
0898     ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus();
0899     ui->subject->setText(subject);
0900     ui->mailText->setText(body);
0901     m_inReplyTo = inReplyTo;
0902 
0903     // Trim the References header as per RFC 5537
0904     QList<QByteArray> trimmedReferences = references;
0905     int referencesSize = QByteArray("References: ").size();
0906     const int lineOverhead = 3; // one for the " " prefix, two for the \r\n suffix
0907     Q_FOREACH(const QByteArray &item, references)
0908         referencesSize += item.size() + lineOverhead;
0909     // The magic numbers are from RFC 5537
0910     while (referencesSize >= 998 && trimmedReferences.size() > 3) {
0911         referencesSize -= trimmedReferences.takeAt(1).size() + lineOverhead;
0912     }
0913     m_references = trimmedReferences;
0914     m_replyingToMessage = replyingToMessage;
0915     if (m_replyingToMessage.isValid()) {
0916         m_markButton->show();
0917         m_replyModeButton->show();
0918         // Got to use trigger() so that the default action of the QToolButton is updated
0919         m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response<hr/>%1").arg(
0920                                           m_replyingToMessage.data(Imap::Mailbox::RoleMessageSubject).toString().toHtmlEscaped()
0921                                           ));
0922         m_actionInReplyTo->trigger();
0923 
0924         // Enable only those Reply Modes that are applicable to the message to be replied
0925         Composer::RecipientList dummy;
0926         m_actionReplyModePrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE,
0927                                                                                 m_mainWindow->senderIdentitiesModel(),
0928                                                                                 m_replyingToMessage, dummy));
0929         m_actionReplyModeAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME,
0930                                                                                  m_mainWindow->senderIdentitiesModel(),
0931                                                                                  m_replyingToMessage, dummy));
0932         m_actionReplyModeAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL,
0933                                                                             m_mainWindow->senderIdentitiesModel(),
0934                                                                             m_replyingToMessage, dummy));
0935         m_actionReplyModeList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST,
0936                                                                              m_mainWindow->senderIdentitiesModel(),
0937                                                                              m_replyingToMessage, dummy));
0938     } else {
0939         m_markButton->hide();
0940         m_replyModeButton->hide();
0941         m_actionInReplyTo->setToolTip(QString());
0942         m_actionStandalone->trigger();
0943     }
0944 
0945     int row = -1;
0946     bool ok = Composer::Util::chooseSenderIdentityForReply(m_mainWindow->senderIdentitiesModel(), replyingToMessage, row);
0947     if (ok) {
0948         Q_ASSERT(row >= 0 && row < m_mainWindow->senderIdentitiesModel()->rowCount());
0949         ui->sender->setCurrentIndex(row);
0950     }
0951 
0952     slotUpdateSignature();
0953 }
0954 
0955 /** @short Find out what type of recipient to use for the last row */
0956 Composer::RecipientKind ComposeWidget::recipientKindForNextRow(const Composer::RecipientKind kind)
0957 {
0958     using namespace Imap::Mailbox;
0959     switch (kind) {
0960     case Composer::ADDRESS_TO:
0961         // Heuristic: if the last one is "to", chances are that the next one shall not be "to" as well.
0962         // Cc is reasonable here.
0963         return Composer::ADDRESS_CC;
0964     case Composer::ADDRESS_RESENT_TO:
0965         return Composer::ADDRESS_RESENT_CC;
0966     case Composer::ADDRESS_CC:
0967     case Composer::ADDRESS_BCC:
0968     case Composer::ADDRESS_RESENT_CC:
0969     case Composer::ADDRESS_RESENT_BCC:
0970         // In any other case, it is probably better to just reuse the type of the last row
0971         return kind;
0972     case Composer::ADDRESS_FROM:
0973     case Composer::ADDRESS_SENDER:
0974     case Composer::ADDRESS_REPLY_TO:
0975     case Composer::ADDRESS_RESENT_FROM:
0976     case Composer::ADDRESS_RESENT_SENDER:
0977         // shall never be used here
0978         Q_ASSERT(false);
0979         return kind;
0980     }
0981     Q_ASSERT(false);
0982     return Composer::ADDRESS_TO;
0983 }
0984 
0985 //BEGIN QFormLayout workarounds
0986 
0987 /** First issue: QFormLayout messes up rows by never removing them
0988  * ----------------------------------------------------------------
0989  * As a result insertRow(int pos, .) does not pick the expected row, but usually minor
0990  * (if you ever removed all items of a row in this layout)
0991  *
0992  * Solution: we count all rows non empty rows and when we have enough, return the row suitable for
0993  * QFormLayout (which is usually behind the requested one)
0994  */
0995 static int actualRow(QFormLayout *form, int row)
0996 {
0997     for (int i = 0, c = 0; i < form->rowCount(); ++i) {
0998         if (c == row) {
0999             return i;
1000         }
1001         if (form->itemAt(i, QFormLayout::LabelRole) || form->itemAt(i, QFormLayout::FieldRole) ||
1002             form->itemAt(i, QFormLayout::SpanningRole))
1003             ++c;
1004     }
1005     return form->rowCount(); // append
1006 }
1007 
1008 /** Second (related) issue: QFormLayout messes the tab order
1009  * ----------------------------------------------------------
1010  * "Inserted" rows just get appended to the present ones and by this to the tab focus order
1011  * It's therefore necessary to fix this forcing setTabOrder()
1012  *
1013  * Approach: traverse all rows until we have the widget that shall be inserted in tab order and
1014  * return it's predecessor
1015  */
1016 
1017 static QWidget* formPredecessor(QFormLayout *form, QWidget *w)
1018 {
1019     QWidget *pred = 0;
1020     QWidget *runner = 0;
1021     QLayoutItem *item = 0;
1022     for (int i = 0; i < form->rowCount(); ++i) {
1023         if ((item = form->itemAt(i, QFormLayout::LabelRole))) {
1024             runner = item->widget();
1025             if (runner == w)
1026                 return pred;
1027             else if (runner)
1028                 pred = runner;
1029         }
1030         if ((item = form->itemAt(i, QFormLayout::FieldRole))) {
1031             runner = item->widget();
1032             if (runner == w)
1033                 return pred;
1034             else if (runner)
1035                 pred = runner;
1036         }
1037         if ((item = form->itemAt(i, QFormLayout::SpanningRole))) {
1038             runner = item->widget();
1039             if (runner == w)
1040                 return pred;
1041             else if (runner)
1042                 pred = runner;
1043         }
1044     }
1045     return pred;
1046 }
1047 
1048 //END QFormLayout workarounds
1049 
1050 void ComposeWidget::calculateMaxVisibleRecipients()
1051 {
1052     const int oldMaxVisibleRecipients = m_maxVisibleRecipients;
1053     int spacing, bottom;
1054     ui->envelopeLayout->getContentsMargins(&spacing, &spacing, &spacing, &bottom);
1055     // we abuse the fact that there's always an addressee and that they all look the same
1056     QRect itemRects[2];
1057     for (int i = 0; i < 2; ++i) {
1058         if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::LabelRole)) {
1059             itemRects[i] |= li->geometry();
1060         }
1061         if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::FieldRole)) {
1062             itemRects[i] |= li->geometry();
1063         }
1064         if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::SpanningRole)) {
1065             itemRects[i] |= li->geometry();
1066         }
1067     }
1068     int itemHeight = itemRects[0].height();
1069     spacing = qMax(0, itemRects[0].top() - itemRects[1].bottom() - 1); // QFormLayout::[vertical]spacing() is useless ...
1070     int firstTop = itemRects[0].top();
1071     const int subjectHeight = ui->subject->height();
1072     const int height = ui->verticalSplitter->sizes().at(0) - // entire splitter area
1073                        firstTop - // offset of first recipient
1074                        (subjectHeight + spacing) - // for the subject
1075                        bottom - // layout bottom padding
1076                        2; // extra pixels padding to detect that the user wants to shrink
1077     if (itemHeight + spacing == 0) {
1078         m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS;
1079     } else {
1080         m_maxVisibleRecipients = height / (itemHeight + spacing);
1081     }
1082     if (m_maxVisibleRecipients < MIN_MAX_VISIBLE_RECIPIENTS)
1083         m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS; // allow up to 4 recipients w/o need for a sliding
1084     if (oldMaxVisibleRecipients != m_maxVisibleRecipients) {
1085         const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients);
1086         int v = qRound(1.0f*(ui->recipientSlider->value()*m_maxVisibleRecipients)/oldMaxVisibleRecipients);
1087         ui->recipientSlider->setMaximum(max);
1088         ui->recipientSlider->setVisible(max > 0);
1089         scrollRecipients(qMin(qMax(0, v), max));
1090     }
1091 }
1092 
1093 void ComposeWidget::addRecipient(int position, Composer::RecipientKind kind, const QString &address)
1094 {
1095     QComboBox *combo = new QComboBox(this);
1096     if (interactiveComposer()) {
1097         combo->addItem(tr("To"), Composer::ADDRESS_TO);
1098         combo->addItem(tr("Cc"), Composer::ADDRESS_CC);
1099         combo->addItem(tr("Bcc"), Composer::ADDRESS_BCC);
1100     } else {
1101         combo->addItem(tr("Resent-To"), Composer::ADDRESS_RESENT_TO);
1102         combo->addItem(tr("Resent-Cc"), Composer::ADDRESS_RESENT_CC);
1103         combo->addItem(tr("Resent-Bcc"), Composer::ADDRESS_RESENT_BCC);
1104     }
1105     combo->setCurrentIndex(combo->findData(kind));
1106     LineEdit *edit = new LineEdit(address, this);
1107     slotCheckAddress(edit);
1108     connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender);
1109     connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated);
1110     connect(edit, &QLineEdit::textEdited, this, &ComposeWidget::completeRecipients);
1111     connect(edit, &QLineEdit::editingFinished, this, &ComposeWidget::collapseRecipients);
1112     connect(edit, &QLineEdit::textChanged, m_recipientListUpdateTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
1113     connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::markReplyModeHandpicked);
1114     connect(edit, &QLineEdit::returnPressed, this, [=]() { gotoNextInputLineFrom(edit); });
1115     m_recipients.insert(position, Recipient(combo, edit));
1116     ui->envelopeWidget->setUpdatesEnabled(false);
1117     ui->envelopeLayout->insertRow(actualRow(ui->envelopeLayout, position + OFFSET_OF_FIRST_ADDRESSEE), combo, edit);
1118     setTabOrder(formPredecessor(ui->envelopeLayout, combo), combo);
1119     setTabOrder(combo, edit);
1120     const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients);
1121     ui->recipientSlider->setMaximum(max);
1122     ui->recipientSlider->setVisible(max > 0);
1123     if (ui->recipientSlider->isVisible()) {
1124         const int v = ui->recipientSlider->value();
1125         int keepInSight = ++position;
1126         for (int i = 0; i < m_recipients.count(); ++i) {
1127             if (m_recipients.at(i).first->hasFocus() || m_recipients.at(i).second->hasFocus()) {
1128                 keepInSight = i;
1129                 break;
1130             }
1131         }
1132         if (qAbs(keepInSight - position) < m_maxVisibleRecipients)
1133             ui->recipientSlider->setValue(position*max/m_recipients.count());
1134         if (v == ui->recipientSlider->value()) // force scroll update
1135             scrollRecipients(v);
1136     }
1137     ui->envelopeWidget->setUpdatesEnabled(true);
1138 }
1139 
1140 void ComposeWidget::slotCheckAddressOfSender()
1141 {
1142     QLineEdit *edit = qobject_cast<QLineEdit*>(sender());
1143     Q_ASSERT(edit);
1144     slotCheckAddress(edit);
1145 }
1146 
1147 void ComposeWidget::slotCheckAddress(QLineEdit *edit)
1148 {
1149     Imap::Message::MailAddress addr;
1150     if (edit->text().isEmpty() || Imap::Message::MailAddress::fromPrettyString(addr, edit->text())) {
1151         edit->setPalette(QPalette());
1152     } else {
1153         QPalette p;
1154         p.setColor(QPalette::Base, UiUtils::tintColor(p.color(QPalette::Base), QColor(0xff, 0, 0, 0x20)));
1155         edit->setPalette(p);
1156     }
1157 }
1158 
1159 void ComposeWidget::removeRecipient(int pos)
1160 {
1161     // removing the widgets from the layout is important
1162     // a) not doing so leaks (minor)
1163     // b) deleteLater() crosses the evenchain and so our actualRow function would be tricked
1164     QWidget *formerFocus = QApplication::focusWidget();
1165     if (!formerFocus)
1166         formerFocus = m_lastFocusedRecipient;
1167 
1168     if (pos + 1 < m_recipients.count()) {
1169         if (m_recipients.at(pos).first == formerFocus) {
1170             m_recipients.at(pos + 1).first->setFocus();
1171             formerFocus = m_recipients.at(pos + 1).first;
1172         } else if (m_recipients.at(pos).second == formerFocus) {
1173             m_recipients.at(pos + 1).second->setFocus();
1174             formerFocus = m_recipients.at(pos + 1).second;
1175         }
1176     } else if (m_recipients.at(pos).first == formerFocus || m_recipients.at(pos).second == formerFocus) {
1177             formerFocus = 0;
1178     }
1179 
1180     ui->envelopeLayout->removeWidget(m_recipients.at(pos).first);
1181     ui->envelopeLayout->removeWidget(m_recipients.at(pos).second);
1182     m_recipients.at(pos).first->deleteLater();
1183     m_recipients.at(pos).second->deleteLater();
1184     m_recipients.removeAt(pos);
1185     const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients);
1186     ui->recipientSlider->setMaximum(max);
1187     ui->recipientSlider->setVisible(max > 0);
1188     if (formerFocus) {
1189         // skip event loop, remove might be triggered by imminent focus loss
1190         CALL_LATER_NOARG(formerFocus, setFocus);
1191     }
1192 }
1193 
1194 static inline Composer::RecipientKind currentRecipient(const QComboBox *box)
1195 {
1196     return Composer::RecipientKind(box->itemData(box->currentIndex()).toInt());
1197 }
1198 
1199 void ComposeWidget::updateRecipientList()
1200 {
1201     // we ensure there's always one empty available
1202     bool haveEmpty = false;
1203     for (int i = 0; i < m_recipients.count(); ++i) {
1204         if (m_recipients.at(i).second->text().isEmpty()) {
1205             if (haveEmpty) {
1206                 removeRecipient(i);
1207             }
1208             haveEmpty = true;
1209         }
1210     }
1211     if (!haveEmpty) {
1212         addRecipient(m_recipients.count(),
1213                      !interactiveComposer() ?
1214                          Composer::ADDRESS_RESENT_TO :
1215                          (
1216                              m_recipients.isEmpty() ?
1217                                  Composer::ADDRESS_TO :
1218                                  recipientKindForNextRow(currentRecipient(m_recipients.last().first))
1219                          ),
1220                      QString());
1221     }
1222 }
1223 
1224 void ComposeWidget::gotoNextInputLineFrom(QWidget *w)
1225 {
1226     bool wFound = false;
1227     for(Recipient recipient : m_recipients) {
1228         if (wFound) {
1229             recipient.second->setFocus();
1230             return;
1231         }
1232         if (recipient.second == w)
1233             wFound = true;
1234     }
1235     Q_ASSERT(wFound);
1236     ui->subject->setFocus();
1237 }
1238 
1239 void ComposeWidget::handleFocusChange()
1240 {
1241     // got explicit focus on other widget - don't restore former focused recipient on scrolling
1242     m_lastFocusedRecipient = QApplication::focusWidget();
1243 
1244     if (m_lastFocusedRecipient)
1245         QTimer::singleShot(150, this, SLOT(scrollToFocus())); // give user chance to notice the focus change disposition
1246 }
1247 
1248 void ComposeWidget::scrollToFocus()
1249 {
1250     if (!ui->recipientSlider->isVisible())
1251         return;
1252 
1253     QWidget *focus = QApplication::focusWidget();
1254     if (focus == ui->envelopeWidget)
1255         focus = m_lastFocusedRecipient;
1256     if (!focus)
1257         return;
1258 
1259     // if this is the first or last visible recipient, show one more (to hint there's more and allow tab progression)
1260     for (int i = 0, pos = 0; i < m_recipients.count(); ++i) {
1261         if (m_recipients.at(i).first->isVisible())
1262             ++pos;
1263         if (focus == m_recipients.at(i).first || focus == m_recipients.at(i).second) {
1264             if (pos > 1 && pos < m_maxVisibleRecipients) // prev & next are in sight
1265                 break;
1266             if (pos == 1)
1267                 ui->recipientSlider->setValue(i - 1); // scroll to prev
1268             else
1269                 ui->recipientSlider->setValue(i + 2 - m_maxVisibleRecipients);  // scroll to next
1270             break;
1271         }
1272     }
1273     if (focus == m_lastFocusedRecipient)
1274         focus->setFocus(); // in case we scrolled to m_lastFocusedRecipient
1275 }
1276 
1277 void ComposeWidget::fadeIn(QWidget *w)
1278 {
1279     QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(w);
1280     w->setGraphicsEffect(effect);
1281     QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity", w);
1282     connect(animation, &QAbstractAnimation::finished, this, &ComposeWidget::slotFadeFinished);
1283     animation->setObjectName(trojita_opacityAnimation);
1284     animation->setDuration(333);
1285     animation->setStartValue(0.0);
1286     animation->setEndValue(1.0);
1287     animation->start(QAbstractAnimation::DeleteWhenStopped);
1288 }
1289 
1290 void ComposeWidget::slotFadeFinished()
1291 {
1292     Q_ASSERT(sender());
1293     QWidget *animatedEffectWidget = qobject_cast<QWidget*>(sender()->parent());
1294     Q_ASSERT(animatedEffectWidget);
1295     animatedEffectWidget->setGraphicsEffect(0); // deletes old one
1296 }
1297 
1298 void ComposeWidget::scrollRecipients(int value)
1299 {
1300     // ignore focus changes caused by "scrolling"
1301     disconnect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange);
1302 
1303     QList<QWidget*> visibleWidgets;
1304     for (int i = 0; i < m_recipients.count(); ++i) {
1305         // remove all widgets from the form because of vspacing - causes spurious padding
1306 
1307         QWidget *toCC = m_recipients.at(i).first;
1308         QWidget *lineEdit = m_recipients.at(i).second;
1309         if (!m_lastFocusedRecipient) { // apply only _once_
1310             if (toCC->hasFocus())
1311                 m_lastFocusedRecipient = toCC;
1312             else if (lineEdit->hasFocus())
1313                 m_lastFocusedRecipient = lineEdit;
1314         }
1315         if (toCC->isVisible())
1316             visibleWidgets << toCC;
1317         if (lineEdit->isVisible())
1318             visibleWidgets << lineEdit;
1319         ui->envelopeLayout->removeWidget(toCC);
1320         ui->envelopeLayout->removeWidget(lineEdit);
1321         toCC->hide();
1322         lineEdit->hide();
1323     }
1324 
1325     const int begin = qMin(m_recipients.count(), value);
1326     const int end   = qMin(m_recipients.count(), value + m_maxVisibleRecipients);
1327     for (int i = begin, j = 0; i < end; ++i, ++j) {
1328         const int pos = actualRow(ui->envelopeLayout, j + OFFSET_OF_FIRST_ADDRESSEE);
1329         QWidget *toCC = m_recipients.at(i).first;
1330         QWidget *lineEdit = m_recipients.at(i).second;
1331         ui->envelopeLayout->insertRow(pos, toCC, lineEdit);
1332         if (!visibleWidgets.contains(toCC))
1333             fadeIn(toCC);
1334         visibleWidgets.removeOne(toCC);
1335         if (!visibleWidgets.contains(lineEdit))
1336             fadeIn(lineEdit);
1337         visibleWidgets.removeOne(lineEdit);
1338         toCC->show();
1339         lineEdit->show();
1340         setTabOrder(formPredecessor(ui->envelopeLayout, toCC), toCC);
1341         setTabOrder(toCC, lineEdit);
1342         if (toCC == m_lastFocusedRecipient)
1343             toCC->setFocus();
1344         else if (lineEdit == m_lastFocusedRecipient)
1345             lineEdit->setFocus();
1346     }
1347 
1348     if (m_lastFocusedRecipient && !m_lastFocusedRecipient->hasFocus() && QApplication::focusWidget())
1349         ui->envelopeWidget->setFocus();
1350 
1351     Q_FOREACH (QWidget *w, visibleWidgets) {
1352         // was visible, is no longer -> stop animation so it won't conflict later ones
1353         w->setGraphicsEffect(0); // deletes old one
1354         if (QPropertyAnimation *pa = w->findChild<QPropertyAnimation*>(trojita_opacityAnimation))
1355             pa->stop();
1356     }
1357     connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange);
1358 }
1359 
1360 void ComposeWidget::collapseRecipients()
1361 {
1362     QLineEdit *edit = qobject_cast<QLineEdit*>(sender());
1363     Q_ASSERT(edit);
1364     if (edit->hasFocus() || !edit->text().isEmpty())
1365         return; // nothing to clean up
1366 
1367     // an empty recipient line just lost focus -> we "place it at the end", ie. simply remove it
1368     // and append a clone
1369     bool needEmpty = false;
1370     Composer::RecipientKind carriedKind = recipientKindForNextRow(interactiveComposer() ?
1371                                                                       Composer::RecipientKind::ADDRESS_TO :
1372                                                                       Composer::RecipientKind::ADDRESS_RESENT_TO);
1373     for (int i = 0; i < m_recipients.count() - 1; ++i) { // sic! on the -1, no action if it trails anyway
1374         if (m_recipients.at(i).second == edit) {
1375             carriedKind = currentRecipient(m_recipients.last().first);
1376             removeRecipient(i);
1377             needEmpty = true;
1378             break;
1379         }
1380     }
1381     if (needEmpty)
1382         addRecipient(m_recipients.count(), carriedKind, QString());
1383 }
1384 
1385 void ComposeWidget::gotError(const QString &error)
1386 {
1387     QMessageBox::critical(this, tr("Failed to Send Mail"), error);
1388     setUiWidgetsEnabled(true);
1389 }
1390 
1391 void ComposeWidget::sent()
1392 {
1393     // FIXME: move back to the currently selected mailbox
1394 
1395     m_sentMail = true;
1396     QTimer::singleShot(0, this, SLOT(close()));
1397 }
1398 
1399 bool ComposeWidget::parseRecipients(QList<QPair<Composer::RecipientKind, Imap::Message::MailAddress> > &results, QString &errorMessage)
1400 {
1401     for (int i = 0; i < m_recipients.size(); ++i) {
1402         Composer::RecipientKind kind = currentRecipient(m_recipients.at(i).first);
1403 
1404         QString text = m_recipients.at(i).second->text();
1405         if (text.isEmpty())
1406             continue;
1407         Imap::Message::MailAddress addr;
1408         bool ok = Imap::Message::MailAddress::fromPrettyString(addr, text);
1409         if (ok) {
1410             // TODO: should we *really* learn every junk entered into a recipient field?
1411             // m_mainWindow->addressBook()->learn(addr);
1412             results << qMakePair(kind, addr);
1413         } else {
1414             errorMessage = tr("Can't parse \"%1\" as an e-mail address.").arg(text);
1415             return false;
1416         }
1417     }
1418     return true;
1419 }
1420 
1421 void ComposeWidget::completeRecipients(const QString &text)
1422 {
1423     if (text.isEmpty()) {
1424         // if there's a popup close it and set back the receiver
1425         m_completionPopup->close();
1426         m_completionReceiver = 0;
1427         return; // we do not suggest "nothing"
1428     }
1429     Q_ASSERT(sender());
1430     QLineEdit *toEdit = qobject_cast<QLineEdit*>(sender());
1431     Q_ASSERT(toEdit);
1432 
1433     Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.take(toEdit);
1434     Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.take(toEdit);
1435 
1436     // if two jobs are running, first was started before second so first should finish earlier
1437     // stop second job
1438     if (firstJob && secondJob) {
1439         disconnect(secondJob, nullptr, this, nullptr);
1440         secondJob->stop();
1441         secondJob->deleteLater();
1442         secondJob = 0;
1443     }
1444     // now at most one job is running
1445 
1446     Plugins::AddressbookPlugin *addressbook = m_mainWindow->pluginManager()->addressbook();
1447     if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureCompletion))
1448         return;
1449 
1450     auto newJob = addressbook->requestCompletion(text, QStringList(), m_completionCount);
1451 
1452     if (!newJob)
1453         return;
1454 
1455     if (secondJob) {
1456         // if only second job is running move second to first and push new as second
1457         firstJob = secondJob;
1458         secondJob = newJob;
1459     } else if (firstJob) {
1460         // if only first job is running push new job as second
1461         secondJob = newJob;
1462     } else {
1463         // if no jobs is running push new job as first
1464         firstJob = newJob;
1465     }
1466 
1467     if (firstJob)
1468         m_firstCompletionRequests.insert(toEdit, firstJob);
1469 
1470     if (secondJob)
1471         m_secondCompletionRequests.insert(toEdit, secondJob);
1472 
1473     connect(newJob, &Plugins::AddressbookCompletionJob::completionAvailable, this, &ComposeWidget::onCompletionAvailable);
1474     connect(newJob, &Plugins::AddressbookCompletionJob::error, this, &ComposeWidget::onCompletionFailed);
1475 
1476     newJob->setAutoDelete(true);
1477     newJob->start();
1478 }
1479 
1480 void ComposeWidget::onCompletionFailed(Plugins::AddressbookJob::Error error)
1481 {
1482     Q_UNUSED(error);
1483     onCompletionAvailable(Plugins::NameEmailList());
1484 }
1485 
1486 void ComposeWidget::onCompletionAvailable(const Plugins::NameEmailList &completion)
1487 {
1488     Plugins::AddressbookJob *job = qobject_cast<Plugins::AddressbookJob *>(sender());
1489     Q_ASSERT(job);
1490     QLineEdit *toEdit = m_firstCompletionRequests.key(job);
1491 
1492     if (!toEdit)
1493         toEdit = m_secondCompletionRequests.key(job);
1494 
1495     if (!toEdit)
1496         return;
1497 
1498     // jobs are removed from QMap below
1499     Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.value(toEdit);
1500     Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.value(toEdit);
1501 
1502     if (job == secondJob) {
1503         // second job finished before first and first was started before second
1504         // so stop first because it has old data
1505         if (firstJob) {
1506             disconnect(firstJob, nullptr, this, nullptr);
1507             firstJob->stop();
1508             firstJob->deleteLater();
1509             firstJob = nullptr;
1510         }
1511         m_firstCompletionRequests.remove(toEdit);
1512         m_secondCompletionRequests.remove(toEdit);
1513     } else if (job == firstJob) {
1514         // first job finished, but if second is still running it will have new data, so do not stop it
1515         m_firstCompletionRequests.remove(toEdit);
1516     }
1517 
1518     QStringList contacts;
1519 
1520     for (int i = 0; i < completion.size(); ++i) {
1521         const Plugins::NameEmail &item = completion.at(i);
1522         contacts << Imap::Message::MailAddress::fromNameAndMail(item.name, item.email).asPrettyString();
1523     }
1524 
1525     if (contacts.isEmpty()) {
1526         m_completionReceiver = 0;
1527         m_completionPopup->close();
1528     } else {
1529         m_completionReceiver = toEdit;
1530         m_completionPopup->setUpdatesEnabled(false);
1531         QList<QAction *> acts = m_completionPopup->actions();
1532         Q_FOREACH(const QString &s, contacts)
1533             m_completionPopup->addAction(s)->setData(s);
1534         Q_FOREACH(QAction *act, acts) {
1535             m_completionPopup->removeAction(act);
1536             delete act;
1537         }
1538         if (m_completionPopup->isHidden())
1539             m_completionPopup->popup(toEdit->mapToGlobal(QPoint(0, toEdit->height())));
1540         m_completionPopup->setUpdatesEnabled(true);
1541     }
1542 }
1543 
1544 void ComposeWidget::completeRecipient(QAction *act)
1545 {
1546     if (act->data().toString().isEmpty())
1547         return;
1548     m_completionReceiver->setText(act->data().toString());
1549     m_completionReceiver = 0;
1550     m_completionPopup->close();
1551 }
1552 
1553 bool ComposeWidget::eventFilter(QObject *o, QEvent *e)
1554 {
1555     if (o == m_completionPopup) {
1556         if (!m_completionPopup->isVisible())
1557             return false;
1558 
1559         if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) {
1560             QKeyEvent *ke = static_cast<QKeyEvent*>(e);
1561             if (!(  ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down || // Navigation
1562                     ke->key() == Qt::Key_Escape || // "escape"
1563                     ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { // selection
1564                 Q_ASSERT(m_completionReceiver);
1565                 QCoreApplication::sendEvent(m_completionReceiver, e);
1566                 return true;
1567             }
1568         }
1569         return false;
1570     }
1571 
1572     if (o == ui->envelopeWidget)  {
1573         if (e->type() == QEvent::Wheel) {
1574             int v = ui->recipientSlider->value();
1575             if (static_cast<QWheelEvent*>(e)->angleDelta().y() > 0)
1576                 --v;
1577             else
1578                 ++v;
1579             // just QApplication::sendEvent(ui->recipientSlider, e) will cause a recursion if
1580             // ui->recipientSlider ignores the event (eg. because it would lead to an invalid value)
1581             // since ui->recipientSlider is child of ui->envelopeWidget
1582             // my guts tell me to not send events to children if it can be avoided, but its just a gut feeling
1583             ui->recipientSlider->setValue(v);
1584             e->accept();
1585             return true;
1586         }
1587         if (e->type() == QEvent::KeyPress && ui->envelopeWidget->hasFocus()) {
1588             scrollToFocus();
1589             QWidget *focus = QApplication::focusWidget();
1590             if (focus && focus != ui->envelopeWidget) {
1591                 int key = static_cast<QKeyEvent*>(e)->key();
1592                 if (!(key == Qt::Key_Tab || key == Qt::Key_Backtab)) // those alter the focus again
1593                     QApplication::sendEvent(focus, e);
1594             }
1595             return true;
1596         }
1597         if (e->type() == QEvent::Resize) {
1598             QResizeEvent *re = static_cast<QResizeEvent*>(e);
1599             if (re->size().height() != re->oldSize().height())
1600                 calculateMaxVisibleRecipients();
1601             return false;
1602         }
1603         return false;
1604     }
1605 
1606     return false;
1607 }
1608 
1609 
1610 void ComposeWidget::slotAskForFileAttachment()
1611 {
1612     static QDir directory = QDir::home();
1613     QString fileName = QFileDialog::getOpenFileName(this, tr("Attach File..."), directory.absolutePath(), QString(), 0,
1614                                                     QFileDialog::DontResolveSymlinks);
1615     if (!fileName.isEmpty()) {
1616         directory = QFileInfo(fileName).absoluteDir();
1617         interactiveComposer()->addFileAttachment(fileName);
1618     }
1619 }
1620 
1621 void ComposeWidget::slotAttachFiles(QList<QUrl> urls)
1622 {
1623     foreach (const QUrl &url, urls) {
1624         if (url.isLocalFile()) {
1625             interactiveComposer()->addFileAttachment(url.path());
1626         }
1627     }
1628 }
1629 
1630 void ComposeWidget::slotUpdateSignature()
1631 {
1632     InhibitComposerDirtying inhibitor(this);
1633     QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(ui->sender->model());
1634     Q_ASSERT(proxy);
1635     QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex());
1636 
1637     if (!proxyIndex.isValid()) {
1638         // This happens when the settings dialog gets closed and the SenderIdentitiesModel reloads data from the on-disk cache
1639         return;
1640     }
1641 
1642     QString newSignature = proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(),
1643                                                                   Composer::SenderIdentitiesModel::COLUMN_SIGNATURE)
1644             .data().toString();
1645 
1646     Composer::Util::replaceSignature(ui->mailText->document(), newSignature);
1647 }
1648 
1649 /** @short Massage the list of recipients so that they match the desired type of reply
1650 
1651 In case of an error, the original list of recipients is left as is.
1652 */
1653 bool ComposeWidget::setReplyMode(const Composer::ReplyMode mode)
1654 {
1655     if (!m_replyingToMessage.isValid())
1656         return false;
1657 
1658     // Determine the new list of recipients
1659     Composer::RecipientList list;
1660     if (!Composer::Util::replyRecipientList(mode, m_mainWindow->senderIdentitiesModel(),
1661                                             m_replyingToMessage, list)) {
1662         return false;
1663     }
1664 
1665     while (!m_recipients.isEmpty())
1666         removeRecipient(0);
1667 
1668     Q_FOREACH(Composer::RecipientList::value_type recipient, list) {
1669         if (!recipient.second.hasUsefulDisplayName())
1670             recipient.second.name.clear();
1671         addRecipient(m_recipients.size(), recipient.first, recipient.second.asPrettyString());
1672     }
1673 
1674     updateRecipientList();
1675 
1676     switch (mode) {
1677     case Composer::REPLY_PRIVATE:
1678         m_actionReplyModePrivate->setChecked(true);
1679         break;
1680     case Composer::REPLY_ALL_BUT_ME:
1681         m_actionReplyModeAllButMe->setChecked(true);
1682         break;
1683     case Composer::REPLY_ALL:
1684         m_actionReplyModeAll->setChecked(true);
1685         break;
1686     case Composer::REPLY_LIST:
1687         m_actionReplyModeList->setChecked(true);
1688         break;
1689     }
1690 
1691     m_replyModeButton->setText(m_replyModeActions->checkedAction()->text());
1692     m_replyModeButton->setIcon(m_replyModeActions->checkedAction()->icon());
1693 
1694     ui->mailText->setFocus();
1695 
1696     return true;
1697 }
1698 
1699 /** local draft serializaton:
1700  * Version (int)
1701  * Whether this draft was stored explicitly (bool)
1702  * The sender (QString)
1703  * Amount of recipients (int)
1704  * n * (RecipientKind ("int") + recipient (QString))
1705  * Subject (QString)
1706  * The message text (QString)
1707  */
1708 
1709 void ComposeWidget::saveDraft(const QString &path)
1710 {
1711     static const int trojitaDraftVersion = 3;
1712     QFile file(path);
1713     if (!file.open(QIODevice::WriteOnly))
1714         return; // TODO: error message?
1715     QDataStream stream(&file);
1716     stream.setVersion(QDataStream::Qt_4_6);
1717     stream << trojitaDraftVersion << m_explicitDraft << ui->sender->currentText();
1718     stream << m_recipients.count();
1719     for (int i = 0; i < m_recipients.count(); ++i) {
1720         stream << m_recipients.at(i).first->itemData(m_recipients.at(i).first->currentIndex()).toInt();
1721         stream << m_recipients.at(i).second->text();
1722     }
1723     stream << m_composer->timestamp() << m_inReplyTo << m_references;
1724     stream << m_actionInReplyTo->isChecked();
1725     stream << ui->subject->text();
1726     stream << ui->mailText->toPlainText();
1727     // we spare attachments
1728     // a) serializing isn't an option, they could be HUUUGE
1729     // b) storing urls only works for urls
1730     // c) the data behind the url or the url validity might have changed
1731     // d) nasty part is writing mails - DnD a file into it is not a problem
1732     file.close();
1733     file.setPermissions(QFile::ReadOwner|QFile::WriteOwner);
1734 }
1735 
1736 /**
1737  * When loading a draft we omit the present autostorage (content is replaced anyway) and make
1738  * the loaded path the autosave path, so all further automatic storage goes into the present
1739  * draft file
1740  */
1741 
1742 void ComposeWidget::loadDraft(const QString &path)
1743 {
1744     QFile file(path);
1745     if (!file.open(QIODevice::ReadOnly))
1746         return;
1747 
1748     if (m_autoSavePath != path) {
1749         QFile::remove(m_autoSavePath);
1750         m_autoSavePath = path;
1751     }
1752 
1753     QDataStream stream(&file);
1754     stream.setVersion(QDataStream::Qt_4_6);
1755     QString string;
1756     int version, recipientCount;
1757     stream >> version;
1758     stream >> m_explicitDraft;
1759     stream >> string >> recipientCount; // sender / amount of recipients
1760     int senderIndex = ui->sender->findText(string);
1761     if (senderIndex != -1) {
1762         ui->sender->setCurrentIndex(senderIndex);
1763     } else {
1764         ui->sender->setEditText(string);
1765     }
1766     for (int i = 0; i < recipientCount; ++i) {
1767         int kind;
1768         stream >> kind >> string;
1769         if (!string.isEmpty())
1770             addRecipient(i, static_cast<Composer::RecipientKind>(kind), string);
1771     }
1772     if (version >= 2) {
1773         QDateTime timestamp;
1774         stream >> timestamp >> m_inReplyTo >> m_references;
1775         interactiveComposer()->setTimestamp(timestamp);
1776         if (!m_inReplyTo.isEmpty()) {
1777             m_markButton->show();
1778             // FIXME: in-reply-to's validitiy isn't the best check for showing or not showing the reply mode.
1779             // For eg: consider cases of mailto, forward, where valid in-reply-to won't mean choice of reply modes.
1780             m_replyModeButton->show();
1781 
1782             m_actionReplyModeAll->setEnabled(false);
1783             m_actionReplyModeAllButMe->setEnabled(false);
1784             m_actionReplyModeList->setEnabled(false);
1785             m_actionReplyModePrivate->setEnabled(false);
1786             markReplyModeHandpicked();
1787 
1788             // We do not have the message index at this point, but we can at least show the Message-Id here
1789             QStringList inReplyTo;
1790             Q_FOREACH(auto item, m_inReplyTo) {
1791                 // There's no HTML escaping to worry about
1792                 inReplyTo << QLatin1Char('<') + QString::fromUtf8(item.constData()) + QLatin1Char('>');
1793             }
1794             m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response<hr/>%1").arg(
1795                                               inReplyTo.join(tr("<br/>")).toHtmlEscaped()
1796                                               ));
1797             if (version == 2) {
1798                 // it is always marked as a reply in v2
1799                 m_actionInReplyTo->trigger();
1800             }
1801         }
1802     }
1803     if (version >= 3) {
1804         bool replyChecked;
1805         stream >> replyChecked;
1806         // Got to use trigger() so that the default action of the QToolButton is updated
1807         if (replyChecked) {
1808             m_actionInReplyTo->trigger();
1809         } else {
1810             m_actionStandalone->trigger();
1811         }
1812     }
1813     stream >> string;
1814     ui->subject->setText(string);
1815     stream >> string;
1816     ui->mailText->setPlainText(string);
1817     m_saveState->setMessageUpdated(false); // this is now the most up-to-date one
1818     file.close();
1819 }
1820 
1821 void ComposeWidget::autoSaveDraft()
1822 {
1823     if (m_saveState->updated()) {
1824         m_saveState->setMessageUpdated(false);
1825         saveDraft(m_autoSavePath);
1826     }
1827 }
1828 
1829 void ComposeWidget::setMessageUpdated()
1830 {
1831     m_saveState->setMessageUpdated(true);
1832     m_saveState->setMessageEverEdited(true);
1833 }
1834 
1835 void ComposeWidget::updateWindowTitle()
1836 {
1837     if (ui->subject->text().isEmpty()) {
1838         setWindowTitle(tr("Compose Mail"));
1839     } else {
1840         setWindowTitle(tr("%1 - Compose Mail").arg(ui->subject->text()));
1841     }
1842 }
1843 
1844 void ComposeWidget::toggleReplyMarking()
1845 {
1846     (m_actionInReplyTo->isChecked() ? m_actionStandalone : m_actionInReplyTo)->trigger();
1847 }
1848 
1849 void ComposeWidget::updateReplyMarkingAction()
1850 {
1851     auto action = m_markAsReply->checkedAction();
1852     m_actionToggleMarking->setText(action->text());
1853     m_actionToggleMarking->setIcon(action->icon());
1854     m_actionToggleMarking->setToolTip(action->toolTip());
1855 }
1856 }