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 }