diff --git a/src/Composer/QuoteText.cpp b/src/Composer/QuoteText.cpp index c0766b32..80e62015 100644 --- a/src/Composer/QuoteText.cpp +++ b/src/Composer/QuoteText.cpp @@ -1,85 +1,85 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Composer/QuoteText.h" #include "UiUtils/PlainTextFormatter.h" namespace Composer { /** @short Take the initial text and mark it as a quotation */ QStringList quoteText(QStringList inputLines) { QStringList quote; for (QStringList::iterator line = inputLines.begin(); line != inputLines.end(); ++line) { - if (UiUtils::signatureSeparator().exactMatch(*line)) { + if (UiUtils::signatureSeparator().match(*line).hasMatch()) { // This is the signature separator, we should not include anything below that in the quote break; } // rewrap - we need to keep the quotes at < 79 chars, yet the grow with every level if (line->length() < 79-2) { // this line is short enough, prepend quote mark and continue if (line->isEmpty() || line->at(0) == QLatin1Char('>')) line->prepend(QLatin1Char('>')); else line->prepend(QLatin1String("> ")); quote << *line; continue; } // long line -> needs to be wrapped // 1st, detect the quote depth and eventually stript the quotes from the line int quoteLevel = 0; int contentStart = 0; if (line->at(0) == QLatin1Char('>')) { quoteLevel = 1; while (quoteLevel < line->length() && line->at(quoteLevel) == QLatin1Char('>')) ++quoteLevel; contentStart = quoteLevel; if (quoteLevel < line->length() && line->at(quoteLevel) == QLatin1Char(' ')) ++contentStart; } // 2nd, build a quote string QString quotemarks; for (int i = 0; i < quoteLevel; ++i) quotemarks += QLatin1Char('>'); quotemarks += QLatin1String("> "); // 3rd, wrap the line, prepend the quotemarks to each line and add it to the quote text int space(contentStart), lastSpace(contentStart), pos(contentStart), length(0); while (pos < line->length()) { if (line->at(pos) == QLatin1Char(' ')) space = pos+1; ++length; if (length > 65-quotemarks.length() && space != lastSpace) { // wrap quote << quotemarks + line->mid(lastSpace, space - lastSpace); lastSpace = space; length = pos - space; } ++pos; } quote << quotemarks + line->mid(lastSpace); } return quote; } } diff --git a/src/Composer/SubjectMangling.cpp b/src/Composer/SubjectMangling.cpp index 219f7499..0ba4bdbf 100644 --- a/src/Composer/SubjectMangling.cpp +++ b/src/Composer/SubjectMangling.cpp @@ -1,84 +1,73 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát + Copyright (C) 2018 Erik Quaeghebeur This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include -#include +#include +#include #include #include "SubjectMangling.h" namespace Composer { namespace Util { /** @short Prepare a subject to be used in a reply message */ QString replySubject(const QString &subject) { // These operations should *not* check for internationalized variants of "Re"; these are evil. - -#define RE_PREFIX_RE "(?:(?:Re:\\s*)*)" -#define RE_PREFIX_ML "(?:(\\[[^\\]]+\\]\\s*)?)" - - static QRegExp rePrefixMatcher(QLatin1String("^" - RE_PREFIX_RE // a sequence of "Re: " prefixes - RE_PREFIX_ML // something like a mailing list prefix - RE_PREFIX_RE // a sequence of "Re: " prefixes - ), Qt::CaseInsensitive); - rePrefixMatcher.setPatternSyntax(QRegExp::RegExp2); - QLatin1String correctedPrefix("Re: "); - - if (rePrefixMatcher.indexIn(subject) == -1) { - // Our regular expression has failed, so better play it safe and blindly prepend "Re: " - return correctedPrefix + subject; - } else { - QStringList listPrefixes; - int pos = 0; - int oldPos = 0; - while ((pos = rePrefixMatcher.indexIn(subject, pos, QRegExp::CaretAtOffset)) != -1) { - if (rePrefixMatcher.matchedLength() == 0) - break; - pos += rePrefixMatcher.matchedLength(); - if (!listPrefixes.contains(rePrefixMatcher.cap(1))) - listPrefixes << rePrefixMatcher.cap(1); - oldPos = pos; - } - - QString mlPrefix = listPrefixes.join(QString()).trimmed(); - QString baseSubject = subject.mid(oldPos + qMax(0, rePrefixMatcher.matchedLength())); - - if (!mlPrefix.isEmpty() && !baseSubject.isEmpty()) - mlPrefix += QLatin1Char(' '); - - return correctedPrefix + mlPrefix + baseSubject; + static const QRegularExpression rePrefixMatcher(QLatin1String( + /* initial whitespace */ "\\s*" + /* either Re: or mailing list tag */ "(?:Re:|(\\[.+?\\]))" + /* repetitions of the above */ "(?:\\s|Re:|\\1)*"), + QRegularExpression::CaseInsensitiveOption); + + QStringList reply_subject(QStringLiteral("Re:")); + int start = 0; + + // extract mailing list tags & find start of ‘base’ subject + QRegularExpressionMatchIterator i = rePrefixMatcher.globalMatch(subject); + while (i.hasNext() && i.peekNext().capturedStart() == start) { + if (!i.peekNext().captured(1).isEmpty()) + reply_subject << i.peekNext().captured(1); + start = i.next().capturedEnd(); } + + // no trailing space after last mailing list tag before empty ‘base’ subject + // TODO: this is for test-suite compliance only; remove? + if (start < subject.length() || reply_subject.length() == 1) + reply_subject << subject.mid(start); + + return reply_subject.join(QLatin1Char(' ')); } /** @short Prepare a subject to be used in a message to be forwarded */ QString forwardSubject(const QString &subject) { QLatin1String forwardPrefix("Fwd: "); return forwardPrefix + subject; } } } diff --git a/src/Gui/ComposeWidget.cpp b/src/Gui/ComposeWidget.cpp index d80dd634..d9a52020 100644 --- a/src/Gui/ComposeWidget.cpp +++ b/src/Gui/ComposeWidget.cpp @@ -1,1859 +1,1860 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát Copyright (C) 2012 Peter Amidon Copyright (C) 2013 - 2014 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include "ui_ComposeWidget.h" #include "Composer/ExistingMessageComposer.h" #include "Composer/MessageComposer.h" #include "Composer/ReplaceSignature.h" #include "Composer/Mailto.h" #include "Composer/SenderIdentitiesModel.h" #include "Composer/Submission.h" #include "Common/InvokeMethod.h" #include "Common/Paths.h" #include "Common/SettingsNames.h" #include "Gui/CompleteMessageWidget.h" #include "Gui/ComposeWidget.h" #include "Gui/FromAddressProxyModel.h" #include "Gui/LineEdit.h" #include "Gui/MessageView.h" #include "Gui/OverlayWidget.h" #include "Gui/PasswordDialog.h" #include "Gui/ProgressPopUp.h" #include "Gui/Util.h" #include "Gui/Window.h" #include "Imap/Model/ImapAccess.h" #include "Imap/Model/ItemRoles.h" #include "Imap/Model/Model.h" #include "Imap/Parser/MailAddress.h" #include "Imap/Tasks/AppendTask.h" #include "Imap/Tasks/GenUrlAuthTask.h" #include "Imap/Tasks/UidSubmitTask.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PluginManager.h" #include "ShortcutHandler/ShortcutHandler.h" #include "UiUtils/Color.h" #include "UiUtils/IconLoader.h" namespace { enum { OFFSET_OF_FIRST_ADDRESSEE = 1, MIN_MAX_VISIBLE_RECIPIENTS = 4 }; } namespace Gui { static const QString trojita_opacityAnimation = QStringLiteral("trojita_opacityAnimation"); /** @short Keep track of whether the document has been updated since the last save */ class ComposerSaveState { public: explicit ComposerSaveState(ComposeWidget* w) : composer(w) , messageUpdated(false) , messageEverEdited(false) { } void setMessageUpdated(bool updated) { if (updated == messageUpdated) return; messageUpdated = updated; updateText(); } void setMessageEverEdited(bool everEdited) { if (everEdited == messageEverEdited) return; messageEverEdited = everEdited; updateText(); } const bool everEdited() {return messageEverEdited;} const bool updated() {return messageUpdated;} private: ComposeWidget* composer; /** @short Has it been updated since the last time we auto-saved it? */ bool messageUpdated; /** @short Was this message ever editted by human? We have to track both of these. Simply changing the sender (and hence the signature) without any text being written shall not trigger automatic saving, but on the other hand changing the sender after something was already written is an important change. */ bool messageEverEdited; void updateText() { composer->cancelButton->setText((messageUpdated || messageEverEdited) ? QWidget::tr("Cancel...") : QWidget::tr("Cancel")); } }; /** @short Ignore dirtying events while we're preparing the widget's contents Under the normal course of operation, there's plenty of events (user typing some text, etc) which lead to the composer widget "remembering" that the human being has made some changes, and that these changes are probably worth a prompt for saving them upon a close. This guard object makes sure (via RAII) that these dirtifying events are ignored during its lifetime. */ class InhibitComposerDirtying { public: explicit InhibitComposerDirtying(ComposeWidget *w): w(w), wasEverEdited(w->m_saveState->everEdited()), wasEverUpdated(w->m_saveState->updated()) {} ~InhibitComposerDirtying() { w->m_saveState->setMessageEverEdited(wasEverEdited); w->m_saveState->setMessageUpdated(wasEverUpdated); } private: ComposeWidget *w; bool wasEverEdited, wasEverUpdated; }; ComposeWidget::ComposeWidget(MainWindow *mainWindow, std::shared_ptr messageComposer, MSA::MSAFactory *msaFactory) : QWidget(0, Qt::Window) , ui(new Ui::ComposeWidget) , m_maxVisibleRecipients(MIN_MAX_VISIBLE_RECIPIENTS) , m_sentMail(false) , m_explicitDraft(false) , m_appendUidReceived(false) , m_appendUidValidity(0) , m_appendUid(0) , m_genUrlAuthReceived(false) , m_mainWindow(mainWindow) , m_settings(mainWindow->settings()) , m_composer(messageComposer) , m_submission(nullptr) , m_completionPopup(nullptr) , m_completionReceiver(nullptr) { setAttribute(Qt::WA_DeleteOnClose, true); QIcon winIcon; winIcon.addFile(QStringLiteral(":/icons/trojita-edit-big.svg"), QSize(128, 128)); winIcon.addFile(QStringLiteral(":/icons/trojita-edit-small.svg"), QSize(22, 22)); setWindowIcon(winIcon); Q_ASSERT(m_mainWindow); m_mainWindow->registerComposeWindow(this); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); QString accountId = profileName.isEmpty() ? QStringLiteral("account-0") : profileName; m_submission = new Composer::Submission(this, m_composer, m_mainWindow->imapModel(), msaFactory, accountId); connect(m_submission, &Composer::Submission::succeeded, this, &ComposeWidget::sent); connect(m_submission, &Composer::Submission::failed, this, &ComposeWidget::gotError); connect(m_submission, &Composer::Submission::failed, this, [this](const QString& message) { emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("ComposeWidget"), message); }); connect(m_submission, &Composer::Submission::logged, this, &ComposeWidget::logged); connect(m_submission, &Composer::Submission::passwordRequested, this, &ComposeWidget::passwordRequested, Qt::QueuedConnection); ui->setupUi(this); if (interactiveComposer()) { interactiveComposer()->setReportTrojitaVersions(m_settings->value(Common::SettingsNames::interopRevealVersions, true).toBool()); ui->attachmentsView->setComposer(interactiveComposer()); } sendButton = ui->buttonBox->addButton(tr("Send"), QDialogButtonBox::AcceptRole); sendButton->setIcon(UiUtils::loadIcon(QStringLiteral("mail-send"))); connect(sendButton, &QAbstractButton::clicked, this, &ComposeWidget::send); cancelButton = ui->buttonBox->addButton(QDialogButtonBox::Cancel); cancelButton->setIcon(UiUtils::loadIcon(QStringLiteral("dialog-cancel"))); connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::close); connect(ui->attachButton, &QAbstractButton::clicked, this, &ComposeWidget::slotAskForFileAttachment); m_saveState = std::unique_ptr(new ComposerSaveState(this)); m_completionPopup = new QMenu(this); m_completionPopup->installEventFilter(this); connect(m_completionPopup, &QMenu::triggered, this, &ComposeWidget::completeRecipient); // TODO: make this configurable? m_completionCount = 8; m_recipientListUpdateTimer = new QTimer(this); m_recipientListUpdateTimer->setSingleShot(true); m_recipientListUpdateTimer->setInterval(250); connect(m_recipientListUpdateTimer, &QTimer::timeout, this, &ComposeWidget::updateRecipientList); connect(ui->verticalSplitter, &QSplitter::splitterMoved, this, &ComposeWidget::calculateMaxVisibleRecipients); calculateMaxVisibleRecipients(); connect(ui->recipientSlider, &QAbstractSlider::valueChanged, this, &ComposeWidget::scrollRecipients); connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); ui->recipientSlider->setMinimum(0); ui->recipientSlider->setMaximum(0); ui->recipientSlider->setVisible(false); ui->envelopeWidget->installEventFilter(this); m_markButton = new QToolButton(ui->buttonBox); m_markButton->setPopupMode(QToolButton::MenuButtonPopup); m_markButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); m_markAsReply = new QActionGroup(m_markButton); m_markAsReply->setExclusive(true); auto *asReplyMenu = new QMenu(m_markButton); m_markButton->setMenu(asReplyMenu); m_actionStandalone = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-fill")), tr("New Thread")); m_actionStandalone->setActionGroup(m_markAsReply); m_actionStandalone->setCheckable(true); m_actionStandalone->setToolTip(tr("This mail will be sent as a standalone message.
Change to preserve the reply hierarchy.")); m_actionInReplyTo = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Threaded")); m_actionInReplyTo->setActionGroup(m_markAsReply); m_actionInReplyTo->setCheckable(true); // This is a "quick shortcut action". It shows the UI bits of the current option, but when the user clicks it, // the *other* action is triggered. m_actionToggleMarking = new QAction(m_markButton); connect(m_actionToggleMarking, &QAction::triggered, this, &ComposeWidget::toggleReplyMarking); m_markButton->setDefaultAction(m_actionToggleMarking); // Unfortunately, there's no signal for toggled(QAction*), so we'll have to call QAction::trigger() to have this working connect(m_markAsReply, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMarkingAction); m_actionStandalone->trigger(); m_replyModeButton = new QToolButton(ui->buttonBox); m_replyModeButton->setPopupMode(QToolButton::InstantPopup); m_replyModeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); QMenu *replyModeMenu = new QMenu(m_replyModeButton); m_replyModeButton->setMenu(replyModeMenu); m_replyModeActions = new QActionGroup(m_replyModeButton); m_replyModeActions->setExclusive(true); m_actionHandPickedRecipients = new QAction(UiUtils::loadIcon(QStringLiteral("document-edit")) ,QStringLiteral("Hand Picked Recipients"), this); replyModeMenu->addAction(m_actionHandPickedRecipients); m_actionHandPickedRecipients->setActionGroup(m_replyModeActions); m_actionHandPickedRecipients->setCheckable(true); replyModeMenu->addSeparator(); QAction *placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_private")); m_actionReplyModePrivate = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModePrivate->setActionGroup(m_replyModeActions); m_actionReplyModePrivate->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all_but_me")); m_actionReplyModeAllButMe = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeAllButMe->setActionGroup(m_replyModeActions); m_actionReplyModeAllButMe->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all")); m_actionReplyModeAll = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeAll->setActionGroup(m_replyModeActions); m_actionReplyModeAll->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_list")); m_actionReplyModeList = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeList->setActionGroup(m_replyModeActions); m_actionReplyModeList->setCheckable(true); connect(m_replyModeActions, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMode); // We want to have the button aligned to the left; the only "portable" way of this is the ResetRole // (thanks to TL for mentioning this, and for the Qt's doc for providing pretty pictures on different platforms) ui->buttonBox->addButton(m_markButton, QDialogButtonBox::ResetRole); // Using ResetRole for reasons same as with m_markButton. We want this button to be second from the left. ui->buttonBox->addButton(m_replyModeButton, QDialogButtonBox::ResetRole); m_markButton->hide(); m_replyModeButton->hide(); if (auto spellchecker = m_mainWindow->pluginManager()->spellchecker()) { spellchecker->actOnEditor(ui->mailText); } connect(ui->mailText, &ComposerTextEdit::urlsAdded, this, &ComposeWidget::slotAttachFiles); connect(ui->mailText, &ComposerTextEdit::sendRequest, this, &ComposeWidget::send); connect(ui->mailText, &QTextEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::updateWindowTitle); connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->subject, &QLineEdit::returnPressed, this, [=]() { ui->mailText->setFocus(); }); updateWindowTitle(); FromAddressProxyModel *proxy = new FromAddressProxyModel(this); proxy->setSourceModel(m_mainWindow->senderIdentitiesModel()); ui->sender->setModel(proxy); connect(ui->sender, static_cast(&QComboBox::currentIndexChanged), this, &ComposeWidget::slotUpdateSignature); connect(ui->sender, &QComboBox::editTextChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->sender->lineEdit(), &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender); QTimer *autoSaveTimer = new QTimer(this); connect(autoSaveTimer, &QTimer::timeout, this, &ComposeWidget::autoSaveDraft); autoSaveTimer->start(30*1000); // these are for the automatically saved drafts, i.e. no i18n for the dir name m_autoSavePath = QString(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/")); QDir().mkpath(m_autoSavePath); m_autoSavePath += QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1String(".draft"); // Add a blank recipient row to start with addRecipient(m_recipients.count(), interactiveComposer() ? Composer::ADDRESS_TO : Composer::ADDRESS_RESENT_TO, QString()); ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus(); slotUpdateSignature(); // default size int sz = ui->mailText->idealWidth(); ui->mailText->setMinimumSize(sz, 1000*sz/1618); // golden mean editor adjustSize(); ui->mailText->setMinimumSize(0, 0); resize(size().boundedTo(qApp->desktop()->availableGeometry().size())); } ComposeWidget::~ComposeWidget() { delete ui; } std::shared_ptr ComposeWidget::interactiveComposer() { return std::dynamic_pointer_cast(m_composer); } /** @short Throw a warning at an attempt to create a Compose Widget while the MSA is not configured */ ComposeWidget *ComposeWidget::warnIfMsaNotConfigured(ComposeWidget *widget, MainWindow *mainWindow) { if (!widget) QMessageBox::critical(mainWindow, tr("Error"), tr("Please set appropriate settings for outgoing messages.")); return widget; } /** @short Find a nice position near the mid of the main window, try to not fully occlude another sibling */ void ComposeWidget::placeOnMainWindow() { QRect area = m_mainWindow->geometry(); QRect origin(0, 0, width(), height()); origin.moveTo(area.x() + (area.width() - width()) / 2, area.y() + (area.height() - height()) / 2); QRect target = origin; QWidgetList siblings; foreach(const QWidget *w, QApplication::topLevelWidgets()) { if (w == this) continue; // I'm not a sibling of myself if (!qobject_cast(w)) continue; // random other stuff siblings << const_cast(w); } int dx = 20, dy = 20; int i = 0; // look for a position where the window would not fully cover another composer // (we don't want to mass open 10 composers stashing each other) // if such composer blocks our desired geometry, the new desired geometry is // tested at positions shifted by 20px circling around the original one. // if we're already more than 100px off the center (what implies the user // has > 20 composers open ...) we give up to not shift the window // too far away, maybe even off-screen. // Notice that it may still happen that some composers *together* stash a 3rd one while (i < siblings.count()) { if (target.contains(siblings.at(i)->geometry())) { target = origin.translated(dx, dy); if (dx < 0 && dy < 0) { dx = dy = -dx + 20; if (dx >= 120) // give up break; } else if (dx < 0 || dy < 0) { dx = -dx; if (dy > 0) dy = -dy; } else { dx = -dx; } i = 0; } else { ++i; } } setGeometry(target); } /** @short Create a blank composer window */ ComposeWidget *ComposeWidget::createBlank(MainWindow *mainWindow) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); w->placeOnMainWindow(); w->show(); return w; } /** @short Load a draft in composer window */ ComposeWidget *ComposeWidget::createDraft(MainWindow *mainWindow, const QString &path) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); w->loadDraft(path); w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window with data from a URL */ ComposeWidget *ComposeWidget::createFromUrl(MainWindow *mainWindow, const QUrl &url) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); QString subject; QString body; QList > recipients; QList inReplyTo; QList references; const QUrlQuery q(url); if (!q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")).isEmpty()) { // There should be only single email address created by Imap::Message::MailAddress::asUrl() Imap::Message::MailAddress addr; if (Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("mailto"))) recipients << qMakePair(Composer::ADDRESS_TO, addr.asPrettyString()); } else { // This should be real RFC 6068 mailto: Composer::parseRFC6068Mailto(url, subject, body, recipients, inReplyTo, references); } // NOTE: we need inReplyTo and references parameters without angle brackets, so remove them for (int i = 0; i < inReplyTo.size(); ++i) { if (inReplyTo[i].startsWith('<') && inReplyTo[i].endsWith('>')) { inReplyTo[i] = inReplyTo[i].mid(1, inReplyTo[i].size()-2); } } for (int i = 0; i < references.size(); ++i) { if (references[i].startsWith('<') && references[i].endsWith('>')) { references[i] = references[i].mid(1, references[i].size()-2); } } w->setResponseData(recipients, subject, body, inReplyTo, references, QModelIndex()); if (!inReplyTo.isEmpty() || !references.isEmpty()) { // We don't need to expose any UI here, but we want the in-reply-to and references information to be carried with this message w->m_actionInReplyTo->setChecked(true); } w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window for a reply */ ComposeWidget *ComposeWidget::createReply(MainWindow *mainWindow, const Composer::ReplyMode &mode, const QModelIndex &replyingToMessage, const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); w->setResponseData(recipients, subject, body, inReplyTo, references, replyingToMessage); bool ok = w->setReplyMode(mode); if (!ok) { QString err; switch (mode) { case Composer::REPLY_ALL: case Composer::REPLY_ALL_BUT_ME: // do nothing break; case Composer::REPLY_LIST: err = tr("It doesn't look like this is a message to the mailing list. Please fill in the recipients manually."); break; case Composer::REPLY_PRIVATE: err = trUtf8("Trojitá was unable to safely determine the real e-mail address of the author of the message. " "You might want to use the \"Reply All\" function and trim the list of addresses manually."); break; } if (!err.isEmpty()) { Gui::Util::messageBoxWarning(w, tr("Cannot Determine Recipients"), err); } } w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window for a mail-forward action */ ComposeWidget *ComposeWidget::createForward(MainWindow *mainWindow, const Composer::ForwardMode mode, const QModelIndex &forwardingMessage, const QString &subject, const QList &inReplyTo, const QList &references) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); w->setResponseData(QList>(), subject, QString(), inReplyTo, references, QModelIndex()); // We don't need to expose any UI here, but we want the in-reply-to and references information to be carried with this message w->m_actionInReplyTo->setChecked(true); // Prepare the message to be forwarded and add it to the attachments view w->interactiveComposer()->prepareForwarding(forwardingMessage, mode); w->placeOnMainWindow(); w->show(); return w; } ComposeWidget *ComposeWidget::createFromReadOnly(MainWindow *mainWindow, const QModelIndex &messageRoot, const QList>& recipients) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(messageRoot); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); for (int i = 0; i < recipients.size(); ++i) { w->addRecipient(i, recipients[i].first, recipients[i].second); } w->updateRecipientList(); // Disable what needs to be nuked w->ui->fromLabel->setText(tr("Sender")); w->ui->subject->hide(); w->ui->subjectLabel->hide(); w->ui->attachmentBox->hide(); w->ui->mailText->hide(); auto subject = messageRoot.data(Imap::Mailbox::RoleMessageSubject).toString(); w->setWindowTitle(tr("Bounce Mail: %1").arg(subject.isEmpty() ? tr("(no subject)") : subject)); // Show the full content of that e-mail as the "main body" within this widget CompleteMessageWidget *messageWidget = new CompleteMessageWidget(w, mainWindow->settings(), mainWindow->pluginManager(), mainWindow->favoriteTagsModel()); messageWidget->messageView->setMessage(messageRoot); messageWidget->messageView->setNetworkWatcher(qobject_cast(mainWindow->imapAccess()->networkWatcher())); messageWidget->setFocusPolicy(Qt::StrongFocus); w->ui->verticalSplitter->insertWidget(1, messageWidget); w->ui->verticalSplitter->setStretchFactor(1, 100); QStringList warnings; if (subject.isEmpty()) { warnings << tr("Message has no subject"); } if (messageRoot.data(Imap::Mailbox::RoleMessageMessageId).toByteArray().isEmpty()) { warnings << tr("The Message-ID header is missing"); } if (!messageRoot.data(Imap::Mailbox::RoleMessageDate).toDateTime().isValid()) { warnings << tr("Message has no date"); } if (messageRoot.data(Imap::Mailbox::RoleMessageFrom).toList().isEmpty()) { warnings << tr("Nothing in the From field"); } if (messageRoot.data(Imap::Mailbox::RoleMessageTo).toList().isEmpty()) { warnings << tr("No recipients in the To field"); } if (!warnings.isEmpty()) { auto lbl = new QLabel(tr("This message appears to be malformed, please be careful before sending it.") + QStringLiteral("
  • ") + warnings.join(QStringLiteral("
  • ")) + QStringLiteral("
"), w); lbl->setStyleSheet(Gui::Util::cssWarningBorder()); w->ui->verticalSplitter->insertWidget(1, lbl); } w->placeOnMainWindow(); w->show(); return w; } void ComposeWidget::updateReplyMode() { bool replyModeSet = false; if (m_actionReplyModePrivate->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_PRIVATE); } else if (m_actionReplyModeAllButMe->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_ALL_BUT_ME); } else if (m_actionReplyModeAll->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_ALL); } else if (m_actionReplyModeList->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_LIST); } if (!replyModeSet) { // This is for now by design going in one direction only, from enabled to disabled. // The index to the message cannot become valid again, and simply marking the buttons as disabled does the trick quite neatly. m_replyModeButton->setEnabled(m_actionHandPickedRecipients->isChecked()); markReplyModeHandpicked(); } } void ComposeWidget::markReplyModeHandpicked() { m_actionHandPickedRecipients->setChecked(true); m_replyModeButton->setText(m_actionHandPickedRecipients->text()); m_replyModeButton->setIcon(m_actionHandPickedRecipients->icon()); } void ComposeWidget::passwordRequested(const QString &user, const QString &host) { if (m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool()) { auto password = qobject_cast(m_mainWindow->imapAccess()->imapModel())->imapPassword(); if (password.isNull()) { // This can happen for example when we've always been offline since the last profile change, // and the IMAP password is therefore not already cached in the IMAP model. // FIXME: it would be nice to "just" call out to MainWindow::authenticationRequested() in that case, // but there's no async callback when the password is available. Just some food for thought when // that part gets refactored :), eventually... askPassword(user, host); } else { m_submission->setPassword(password); } return; } Plugins::PasswordPlugin *password = m_mainWindow->pluginManager()->password(); if (!password) { askPassword(user, host); return; } // FIXME: use another account-id at some point in future // we are now using the profile to avoid overwriting passwords of // other profiles in secure storage // 'account-0' is the hardcoded value when not using a profile Plugins::PasswordJob *job = password->requestPassword(m_submission->accountId(), QStringLiteral("smtp")); if (!job) { askPassword(user, host); return; } connect(job, &Plugins::PasswordJob::passwordAvailable, m_submission, &Composer::Submission::setPassword); connect(job, &Plugins::PasswordJob::error, this, &ComposeWidget::passwordError); job->setAutoDelete(true); job->setProperty("user", user); job->setProperty("host", host); job->start(); } void ComposeWidget::passwordError() { Plugins::PasswordJob *job = static_cast(sender()); const QString &user = job->property("user").toString(); const QString &host = job->property("host").toString(); askPassword(user, host); } void ComposeWidget::askPassword(const QString &user, const QString &host) { auto w = Gui::PasswordDialog::getPassword(this, tr("Authentication Required"), tr("

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

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

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

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

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

    "); } else { idString = tr("

    The IMAP server did not return usable information about itself.

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

    The server has not provided information about its software version.

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

    The following capabilities are currently advertised:

    \n" "
      \n%2
    ").arg(idString, caps)); } QSize MainWindow::sizeHint() const { return QSize(1150, 980); } void MainWindow::slotUpdateWindowTitle() { QModelIndex mailbox = qobject_cast(m_imapAccess->msgListModel())->currentMailbox(); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (!profileName.isEmpty()) profileName = QLatin1String(" [") + profileName + QLatin1Char(']'); if (mailbox.isValid()) { if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) { setWindowTitle(trUtf8("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } else { setWindowTitle(trUtf8("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } } else { setWindowTitle(trUtf8("Trojitá") + profileName); } } void MainWindow::slotLayoutCompact() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } if (!m_mainVSplitter) { m_mainVSplitter = new QSplitter(); m_mainVSplitter->setOrientation(Qt::Vertical); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainVSplitter->addWidget(msgListWidget); m_mainVSplitter->addWidget(m_messageWidget); m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(m_mainVSplitter); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainVSplitter->show(); m_mainHSplitter->show(); // The mboxTree shall not expand... m_mainHSplitter->setStretchFactor(0, 0); // ...while the msgListTree shall consume all the remaining space m_mainHSplitter->setStretchFactor(1, 1); // The CompleteMessageWidget shall not not collapse m_mainVSplitter->setCollapsible(m_mainVSplitter->indexOf(m_messageWidget), false); setCentralWidget(m_mainHSplitter); delete m_mainStack; m_layoutMode = LAYOUT_COMPACT; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact); applySizesAndState(); } void MainWindow::slotLayoutWide() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(msgListWidget); m_mainHSplitter->addWidget(m_messageWidget); msgListWidget->resize(mboxTree->size()); m_messageWidget->resize(mboxTree->size()); m_mainHSplitter->setStretchFactor(0, 0); m_mainHSplitter->setStretchFactor(1, 1); m_mainHSplitter->setStretchFactor(2, 1); m_mainHSplitter->setCollapsible(m_mainHSplitter->indexOf(m_messageWidget), false); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainHSplitter->show(); setCentralWidget(m_mainHSplitter); delete m_mainStack; delete m_mainVSplitter; m_layoutMode = LAYOUT_WIDE; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide); applySizesAndState(); } void MainWindow::slotLayoutOneAtTime() { saveSizesAndState(); if (m_mainStack) return; m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack); setCentralWidget(m_mainStack); delete m_mainHSplitter; delete m_mainVSplitter; m_layoutMode = LAYOUT_ONE_AT_TIME; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime); applySizesAndState(); } Imap::Mailbox::Model *MainWindow::imapModel() const { return qobject_cast(m_imapAccess->imapModel()); } void MainWindow::desktopGeometryChanged() { m_delayedStateSaving->start(); } QString MainWindow::settingsKeyForLayout(const LayoutMode layout) { switch (layout) { case LAYOUT_COMPACT: return Common::SettingsNames::guiSizesInMainWinWhenCompact; case LAYOUT_WIDE: return Common::SettingsNames::guiSizesInMainWinWhenWide; case LAYOUT_ONE_AT_TIME: return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime; break; } return QString(); } void MainWindow::saveSizesAndState() { if (m_skipSavingOfUI) return; QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QList items; items << saveGeometry(); items << saveState(); items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray()); items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray()); items << msgListWidget->tree->header()->saveState(); items << QByteArray::number(msgListWidget->tree->header()->count()); for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) { items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i)); } // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number items << QByteArray::number(menuBar()->isVisible()); QByteArray buf; QDataStream stream(&buf, QIODevice::WriteOnly); stream << items.size(); Q_FOREACH(const QByteArray &item, items) { stream << item; } m_settings->setValue(key.arg(QString::number(geometry.width())), buf); } void MainWindow::saveRawStateSetting(bool enabled) { m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled); } void MainWindow::applySizesAndState() { QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray(); if (buf.isEmpty()) return; int size; QDataStream stream(&buf, QIODevice::ReadOnly); stream >> size; QByteArray item; // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to // save our geometries and state, which is what we must avoid while this method is executing. bool skipSaving = m_skipSavingOfUI; m_skipSavingOfUI = true; if (size-- && !stream.atEnd()) { stream >> item; // https://bugreports.qt-project.org/browse/QTBUG-30636 if (windowState() & Qt::WindowMaximized) { // restoreGeometry(.) restores the wrong size for at least maximized window // However, QWidget does also not notice that the configure request for this // is ignored by many window managers (because users really don't like when windows // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from // the wrong assumption the request would have been hononred. // So we just "fix" the internal geometry immediately afterwards to prevent // mislayouting // There's atm. no flicker due to this (and because Qt compresses events) // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true) QRect oldGeometry = MainWindow::geometry(); restoreGeometry(item); if (windowState() & Qt::WindowMaximized) setGeometry(oldGeometry); } else { restoreGeometry(item); if (windowState() & Qt::WindowMaximized) { // ensure to try setting the proper geometry and have the WM constrain us setGeometry(QApplication::desktop()->availableGeometry()); } } } if (size-- && !stream.atEnd()) { stream >> item; restoreState(item); } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainVSplitter) { m_mainVSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainHSplitter) { m_mainHSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; msgListWidget->tree->header()->restoreState(item); // got to manually update the state of the actions which control the visibility state msgListWidget->tree->updateActionsAfterRestoredState(); } connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns); if (size-- && !stream.atEnd()) { stream >> item; bool ok; int columns = item.toInt(&ok); if (ok) { msgListWidget->tree->header()->setStretchLastSection(false); for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) { stream >> item; int sectionSize = item.toInt(); QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i); if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) { // fun fact: user cannot resize by mouse when size <= 0 msgListWidget->tree->setColumnWidth(i, sectionSize); } else { msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i)); } msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode); } } } if (size-- && !stream.atEnd()) { stream >> item; bool ok; bool visibility = item.toInt(&ok); if (ok) { menuBar()->setVisible(visibility); showMenuBar->setChecked(visibility); } } m_skipSavingOfUI = skipSaving; } void MainWindow::resizeEvent(QResizeEvent *) { m_delayedStateSaving->start(); } /** @short Make sure that the message gets loaded after the splitters have changed their position */ void MainWindow::possiblyLoadMessageOnSplittersChanged() { if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) { // We do not have to check whether it's a different message; the setMessage() will do this or us // and there are multiple proxy models involved anyway QModelIndex index = msgListWidget->tree->currentIndex(); if (index.isValid()) { // OTOH, setting an invalid QModelIndex would happily assert-fail m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex()); } } } Imap::ImapAccess *MainWindow::imapAccess() const { return m_imapAccess; } void MainWindow::enableLoggingToDisk() { imapLogger->slotSetPersistentLogging(true); } void MainWindow::slotPluginsChanged() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow)) m_actionContactEditor->setEnabled(false); else m_actionContactEditor->setEnabled(true); } /** @short Update the default action to make sure that we show a correct status of the network connection */ void MainWindow::updateNetworkIndication() { if (QAction *action = qobject_cast(sender())) { if (action->isChecked()) { m_netToolbarDefaultAction->setIcon(action->icon()); } } } void MainWindow::showStatusMessage(const QString &message) { networkIndicator->setToolTip(message); if (isActiveWindow()) QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message); } void MainWindow::slotMessageModelChanged(QAbstractItemModel *model) { mailMimeTree->setModel(model); } void MainWindow::slotFavoriteTagsChanged() { for (int i = 1; i <= m_favoriteTags->rowCount(); ++i) { QAction *action = ShortcutHandler::instance()->action(QStringLiteral("action_tag_") + QString::number(i)); if (action) action->setText(tr("Tag with \"%1\"").arg(m_favoriteTags->tagNameByIndex(i - 1))); } } void MainWindow::registerComposeWindow(ComposeWidget* widget) { connect(widget, &ComposeWidget::logged, this, [this](const Common::LogKind kind, const QString& source, const QString& message) { imapLogger->log(0, Common::LogMessage(QDateTime::currentDateTime(), kind, source, message, 0)); }); } } diff --git a/src/Imap/Encoders.cpp b/src/Imap/Encoders.cpp index 88889641..f37bad88 100644 --- a/src/Imap/Encoders.cpp +++ b/src/Imap/Encoders.cpp @@ -1,742 +1,746 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Messaging Framework. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ + +#include +#include + #include "Encoders.h" #include "Parser/3rdparty/rfccodecs.h" #include "Parser/3rdparty/kcodecs.h" namespace { static QTextCodec* codecForName(const QByteArray& charset, bool translateAscii = true) { QByteArray encoding(charset.toLower()); if (!encoding.isEmpty()) { int index; if (translateAscii && encoding.contains("ascii")) { // We'll assume the text is plain ASCII, to be extracted to Latin-1 encoding = "ISO-8859-1"; } else if ((index = encoding.indexOf('*')) != -1) { // This charset specification includes a trailing language specifier encoding = encoding.left(index); } QTextCodec* codec = QTextCodec::codecForName(encoding); if (!codec) { qWarning() << "codecForName: Unable to find codec for charset" << encoding; } return codec; } return 0; } // ASCII character values used throughout const unsigned char MaxPrintableRange = 0x7e; const unsigned char Space = 0x20; const unsigned char Equals = 0x3d; const unsigned char QuestionMark = 0x3f; const unsigned char Underscore = 0x5f; const unsigned char ExclamationMark = 0x21; const unsigned char Star = 0x2a; const unsigned char Plus = 0x2b; const unsigned char Minus = 0x2d; const unsigned char Ascii_Dot = 0x2e; const unsigned char Slash = 0x2f; const unsigned char Ascii_Zero = 0x30; const unsigned char Ascii_Nine = 0x39; const unsigned char Ascii_A = 0x41; const unsigned char Ascii_Z = 0x5a; const unsigned char Ascii_a = 0x61; const unsigned char Ascii_z = 0x7a; /** @short Check the given unicode code point if it has to be escaped in the quoted-printable encoding according to RFC2047 */ static inline bool rfc2047QPNeedsEscpaing(const int unicode, const Imap::Rfc2047ProductionType production) { switch (production) { case Imap::Rfc2047ProductionType::Text: if (unicode <= Space) return true; if (unicode == Equals || unicode == QuestionMark || unicode == Underscore) return true; if (unicode > MaxPrintableRange) return true; return false; case Imap::Rfc2047ProductionType::Phrase: if ( (unicode >= Ascii_a && unicode <= Ascii_z) || (unicode >= Ascii_A && unicode <= Ascii_Z) || (unicode >= Ascii_Zero && unicode <= Ascii_Nine) ) { return false; } switch (unicode) { case ExclamationMark: case Star: case Plus: case Minus: case Slash: case Equals: case Underscore: return false; default: return true; } } Q_ASSERT(false); return false; } /** @short Check the given unicode code point if it has to be escaped according to RFC 2231 The rules might be a little stricter than required, actually. */ static inline bool rfc2311NeedsEscaping(const int unicode) { if (unicode == Minus || unicode == Ascii_Dot || unicode == Underscore) return false; if (unicode >= Ascii_Zero && unicode <= Ascii_Nine) return false; if (unicode >= Ascii_A && unicode <= Ascii_Z) return false; if (unicode >= Ascii_a && unicode <= Ascii_z) return false; return true; } /** @short Find the most efficient encoding for the given unicode string It can be either just plain ASCII, or ISO-Latin1 using the Quoted-Printable encoding, or a full-blown UTF-8 scheme with Base64 encoding. */ static Imap::Rfc2047StringCharacterSetType charsetForInput(const QString& input) { // shamelessly stolen from QMF's qmailmessage.cpp // See if this input needs encoding Imap::Rfc2047StringCharacterSetType latin1 = Imap::RFC2047_STRING_ASCII; const QChar* it = input.constData(); const QChar* const end = it + input.length(); for ( ; it != end; ++it) { if ((*it).unicode() > 0xff) { // Multi-byte characters included - we need to use UTF-8 return Imap::RFC2047_STRING_UTF8; } else if (!latin1 && rfc2047QPNeedsEscpaing(it->unicode(), Imap::Rfc2047ProductionType::Text)) { // We need encoding from latin-1 latin1 = Imap::RFC2047_STRING_LATIN; } } return latin1; } /** @short Convert a hex digit into a number */ static inline int hexValueOfChar(const char input) { if (input >= '0' && input <= '9') { return input - '0'; } else if (input >= 'A' && input <= 'F') { return 0x0a + input - 'A'; } else if (input >= 'a' && input <= 'f') { return 0x0a + input - 'a'; } else { return -1; } } /** @short Translate a quoted-printable-encoded array of bytes into binary characters The transformations performed are according to RFC 2047; underscores are transferred into spaces and the three-character =12 escapes are turned into a single byte value. */ static inline QByteArray translateQuotedPrintableToBin(const QByteArray &input) { QByteArray res; for (int i = 0; i < input.size(); ++i) { if (input[i] == '_') { res += ' '; } else if (input[i] == '=' && i < input.size() - 2) { int hi = hexValueOfChar(input[++i]); int lo = hexValueOfChar(input[++i]); if (hi != -1 && lo != -1) { res += static_cast((hi << 4) + lo); } else { res += input.mid(i - 2, 3); } } else { res += input[i]; } } return res; } /** @short Translate a percent-encoded array of bytes into binary characters The only transformation performed is RFC 2231 percent decoding. */ static inline QByteArray translatePercentToBin(const QByteArray &input) { QByteArray res; for (int i = 0; i < input.size(); ++i) { if (input[i] == '%' && i < input.size() - 2) { int hi = hexValueOfChar(input[++i]); int lo = hexValueOfChar(input[++i]); if (hi != -1 && lo != -1) { res += static_cast((hi << 4) + lo); } else { res += input.mid(i - 2, 3); } } else { res += input[i]; } } return res; } /** @short Decode an encoded-word as per RFC2047 into a unicode string */ static QString decodeWord(const QByteArray &fullWord, const QByteArray &charset, const QByteArray &encoding, const QByteArray &encoded) { if (encoding == "Q") { return Imap::decodeByteArray(translateQuotedPrintableToBin(encoded), charset); } else if (encoding == "B") { return Imap::decodeByteArray(QByteArray::fromBase64(encoded), charset); } else { return QString::fromUtf8(fullWord); } } /** @short Decode a header in the RFC 2047 format into a unicode string */ static QString decodeWordSequence(const QByteArray& input) { - QRegExp whitespace(QLatin1String("^\\s+$")); + QRegularExpression whitespace(QLatin1String("^\\s+$")); // the regexp library operates on unicode strings, unfortunately QString str = QString::fromUtf8(input); QString out; - // Any idea why this isn't matching? - //QRegExp encodedWord("\\b=\\?\\S+\\?\\S+\\?\\S*\\?=\\b"); - QRegExp encodedWord(QLatin1String("\"?=\\?(\\S+)\\?(\\S+)\\?(.*)\\?=\"?")); - - // set minimal=true, to match sequences which do not have white space in between 2 encoded words; otherwise by default greedy matching is performed - // eg. "Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord" will match "=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=" as a single encoded word without minimal=true - // with minimal=true, "=?ISO-8859-1?B?9g==?=" will be the first encoded word and "=?ISO-8859-1?B?5Q==?=" the second. + // This wasn't matching for QRegExp, but perhaps it does work for QRegularExpression + //QRegularExpression encodedWord("\\b=\\?\\S+\\?\\S+\\?\\S*\\?=\\b"); + QRegularExpression encodedWord(QLatin1String("\"?=\\?(\\S+)\\?(\\S+)\\?(.*)\\?=\"?"), + QRegularExpression::InvertedGreedinessOption); + // we set InvertedGreedinessOption, to match sequences which do not have white space in between 2 encoded words; otherwise by default greedy matching is performed + // eg. "Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord" will match "=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=" as a single encoded word without InvertedGreedinessOption + // with InvertedGreedinessOption, "=?ISO-8859-1?B?9g==?=" will be the first encoded word and "=?ISO-8859-1?B?5Q==?=" the second. // -- assuming there are no nested encodings, will there be? - encodedWord.setMinimal(true); int pos = 0; int lastPos = 0; while (pos != -1) { - pos = encodedWord.indexIn(str, pos); + QRegularExpressionMatch match = encodedWord.match(str, pos); + pos = match.capturedStart(); if (pos != -1) { - int endPos = pos + encodedWord.matchedLength(); + int endPos = pos + match.capturedLength(); QString preceding(str.mid(lastPos, (pos - lastPos))); - QString decoded = decodeWord(str.mid(pos, (endPos - pos)).toUtf8(), encodedWord.cap(1).toLatin1(), - encodedWord.cap(2).toUpper().toLatin1(), encodedWord.cap(3).toLatin1()); + QString decoded = decodeWord(str.mid(pos, (endPos - pos)).toUtf8(), match.captured(1).toLatin1(), + match.captured(2).toUpper().toLatin1(), match.captured(3).toLatin1()); // If there is only whitespace between two encoded words, it should not be included - if (!whitespace.exactMatch(preceding)) + if (!whitespace.match(preceding).hasMatch()) out.append(preceding); out.append(decoded); pos = endPos; lastPos = pos; } } // Copy anything left out.append(str.midRef(lastPos)); return out; } QByteArray toHexChar(const ushort unicode, char prefix) { const char hexChars[] = "0123456789ABCDEF"; return QByteArray() + prefix + hexChars[(unicode >> 4) & 0xf] + hexChars[unicode & 0xf]; } } namespace Imap { QByteArray encodeRFC2047String(const QString &text, const Rfc2047StringCharacterSetType charset, const Rfc2047ProductionType productionType) { // We can't allow more than 75 chars per encoded-word, including the boiler plate (7 chars and the size of the encoding spec) // -- this is defined by RFC2047. int maximumEncoded = 75 - 7; QByteArray encoding; if (charset == RFC2047_STRING_UTF8) encoding = "utf-8"; else encoding = "iso-8859-1"; maximumEncoded -= encoding.size(); // If this is an encodedWord, we need to include any whitespace that we don't want to lose if (charset == RFC2047_STRING_UTF8) { QByteArray res; int start = 0; while (start < text.size()) { // as long as we have something to work on... int size = maximumEncoded; QByteArray candidate; // Find the character boundary at which we have to split the input. // Remember that we're iterating on Unicode codepoints now, not on raw bytes. while (true) { candidate = text.mid(start, size).toUtf8(); int utf8Size = candidate.size(); int base64Size = utf8Size * 4 / 3 + utf8Size % 3; if (base64Size <= maximumEncoded) { // if this chunk's size is small enough, great QByteArray encoded = candidate.toBase64(); if (!res.isEmpty()) res.append("\r\n "); res.append("=?utf-8?B?" + encoded + "?="); start += size; break; } else { // otherwise, try with something smaller --size; Q_ASSERT(size >= 1); } } } return res; } else { QByteArray buf = "=?" + encoding + "?Q?"; int i = 0; int currentLineLength = 0; while (i < text.size()) { QByteArray symbol; const ushort unicode = text[i].unicode(); if (unicode == 0x20) { symbol = "_"; } else if (!rfc2047QPNeedsEscpaing(unicode, productionType)) { symbol += text[i].toLatin1(); } else { symbol = toHexChar(unicode, '='); } currentLineLength += symbol.size(); if (currentLineLength > maximumEncoded) { buf += "?=\r\n =?" + encoding + "?Q?"; currentLineLength = 0; } buf += symbol; ++i; } buf += "?="; return buf; } } /** @short Interpret the raw byte array as a sequence of bytes in the given encoding */ QString decodeByteArray(const QByteArray &encoded, const QByteArray &charset) { if (QTextCodec *codec = codecForName(charset)) { return codec->toUnicode(encoded); } return QString::fromUtf8(encoded, encoded.size()); } /** @short Encode the given string into RFC2047 form, preserving the ASCII leading part if possible */ QByteArray encodeRFC2047StringWithAsciiPrefix(const QString &text) { // The maximal recommended line length, as defined by RFC 5322 const int maxLineLength = 78; // Find first character which needs escaping int pos = 0; while (pos < text.size() && pos < maxLineLength && (text[pos].unicode() == 0x20 || !rfc2047QPNeedsEscpaing(text[pos].unicode(), Rfc2047ProductionType::Text))) ++pos; // Find last character of a word which doesn't need escaping if (pos != text.size()) { while (pos > 0 && text[pos-1].unicode() != 0x20) --pos; if (pos > 0 && text[pos].unicode() == 0x20) --pos; } QByteArray prefix = text.left(pos).toUtf8(); if (pos == text.size()) return prefix; QString rest = text.mid(pos); Rfc2047StringCharacterSetType charset = charsetForInput(rest); return prefix + encodeRFC2047String(rest, charset, Rfc2047ProductionType::Text); } QString decodeRFC2047String( const QByteArray& raw ) { return ::decodeWordSequence( raw ); } QByteArray encodeImapFolderName(const QString &text) { return KIMAP::encodeImapFolderName(text); } QString decodeImapFolderName(const QByteArray &raw) { return KIMAP::decodeImapFolderName(raw); } QByteArray quotedPrintableDecode( const QByteArray& raw ) { return KCodecs::quotedPrintableDecode( raw ); } QByteArray quotedPrintableEncode(const QByteArray &raw) { return KCodecs::quotedPrintableEncode(raw); } QByteArray quotedString( const QByteArray& unquoted, QuotedStringStyle style ) { QByteArray quoted; char lhq, rhq; /* Compose a double-quoted string according to RFC2822 3.2.5 "quoted-string" */ switch (style) { default: case DoubleQuoted: lhq = rhq = '"'; break; case SquareBrackets: lhq = '['; rhq = ']'; break; case Parentheses: lhq = '('; rhq = ')'; break; } quoted.append(lhq); for(int i = 0; i < unquoted.size(); i++) { char ch = unquoted[i]; if (ch == 9 || ch == 10 || ch == 13) { /* Newlines and tabs: these are only allowed in quoted-strings as folding-whitespace, where they are "semantically invisible". If we really want to include them, we probably need to do so as RFC2047 strings. But it's unlikely that that's a desirable behavior in the final application. Instead, translate embedded tabs/newlines into normal whitespace. */ quoted.append(' '); } else { if (ch == lhq || ch == rhq || ch == '\\') quoted.append('\\'); /* Quoted-pair */ quoted.append(ch); } } quoted.append(rhq); return quoted; } /* encodeRFC2047Phrase encodes an arbitrary string into a byte-sequence for use in a "structured" mail header (such as To:, From:, or Received:). The result will match the "phrase" production. */ -static QRegExp atomPhraseRx(QLatin1String("[ \\tA-Za-z0-9!#$&'*+/=?^_`{}|~-]*")); +static QRegularExpression atomPhraseRx(QLatin1String("^[ \\tA-Za-z0-9!#$&'*+/=?^_`{}|~-]*$")); QByteArray encodeRFC2047Phrase(const QString &text) { /* We want to know if we can encode as ASCII. But bizarrely, Qt (on my system at least) doesn't have an ASCII codec. So we use the ISO-8859-1 superset, and check for any non-ASCII characters in the result. */ QTextCodec *latin1 = QTextCodec::codecForMib(4); if (latin1->canEncode(text)) { /* Attempt to represent it as an RFC2822 'phrase' --- either a sequence of atoms or as a quoted-string. */ - if (atomPhraseRx.exactMatch(text)) { + if (atomPhraseRx.match(text).hasMatch()) { /* Simplest case: a sequence of atoms (not dot-atoms) */ return latin1->fromUnicode(text); } else { /* Next-simplest representation: a quoted-string */ QByteArray unquoted = latin1->fromUnicode(text); /* Check for non-ASCII characters. */ for(int i = 0; i < unquoted.size(); i++) { char ch = unquoted[i]; if (ch < 1 || ch >= 127) { /* This string contains non-ASCII characters, so the only way to represent it in a mail header is as an RFC2047 encoded-word. */ return encodeRFC2047String(text, RFC2047_STRING_LATIN, Rfc2047ProductionType::Phrase); } } return quotedString(unquoted); } } /* If the text has characters outside of the basic ASCII set, then it has to be encoded using the RFC2047 encoded-word syntax. */ return encodeRFC2047String(text, RFC2047_STRING_UTF8, Rfc2047ProductionType::Phrase); } /** @short Decode RFC2231-style eextended parameter values into a real Unicode string */ QString extractRfc2231Param(const QMap ¶meters, const QByteArray &key) { QMap::const_iterator it = parameters.constFind(key); if (it != parameters.constEnd()) { // This parameter is not using the RFC 2231 syntax for extended parameters. // I have no idea whether this is correct, but I *guess* that trying to use RFC2047 is not going to hurt. return decodeRFC2047String(*it); } if (parameters.constFind(key + "*0") != parameters.constEnd()) { // There's a 2231-style continuation *without* the language/charset extension QByteArray raw; int num = 0; while ((it = parameters.constFind(key + '*' + QByteArray::number(num++))) != parameters.constEnd()) { raw += *it; } return decodeRFC2047String(raw); } QByteArray raw; if ((it = parameters.constFind(key + '*')) != parameters.constEnd()) { // No continuation, but language/charset is present raw = *it; } else if (parameters.constFind(key + "*0*") != parameters.constEnd()) { // Both continuation *and* the lang/charset extension are in there int num = 0; // The funny thing is that the other values might or might not end with the trailing star, // at least according to the example in the RFC do { if ((it = parameters.constFind(key + '*' + QByteArray::number(num))) != parameters.constEnd()) raw += *it; else if ((it = parameters.constFind(key + '*' + QByteArray::number(num) + '*')) != parameters.constEnd()) raw += *it; ++num; } while (it != parameters.constEnd()); } // Process 2231-style language/charset continuation, if present int pos1 = raw.indexOf('\'', 0); int pos2 = raw.indexOf('\'', qMax(1, pos1 + 1)); if (pos1 != -1 && pos2 != -1) { return decodeByteArray(translatePercentToBin(raw.mid(pos2 + 1)), raw.left(pos1)); } // Fallback: it could be empty, or otherwise malformed. Just treat it as UTF-8 for compatibility return QString::fromUtf8(raw); } /** @short Produce a parameter for a MIME message header */ QByteArray encodeRfc2231Parameter(const QByteArray &key, const QString &value) { if (value.isEmpty()) return key + "=\"\""; bool safeAscii = true; // Find "dangerous" characters for (int i = 0; i < value.size(); ++i) { if (rfc2311NeedsEscaping(value[i].unicode())) { safeAscii = false; break; } } if (safeAscii) return key + '=' + value.toUtf8(); // FIXME: split the string into smaller chunks, use continuations QByteArray res = key + "*=utf-8''"; QByteArray encoded = value.toUtf8(); for (int i = 0; i < encoded.size(); ++i) { char unicode = encoded[i]; if (rfc2311NeedsEscaping(unicode)) res += toHexChar(unicode, '%'); else res += unicode; } return res; } /** @short Insert extra spaces to match the SHOULD from RFC3676 The format=flowed specification contains a SHOULD provision that long paragraphs are to be wrapped so that they are no longer than 78 characters. This function will insert line breaks (with the appropriate space guards) so that the lines of text are no longer than a certain amount of *characters*. Please note that this limit is about Unicode code points as provided by QString and hence not a limit on the number of actual bytes that the line will occupy after being encoded into UTF-8 and the result encoded in quoted-printable. This function also pretends that "space" is only the ASCII space; it will not attempt to insert any spacing into the middle of a sequence of other characters, even if that sequence contained something which can be considered a "word boundary" in some non-Latin script. I guess that violating a SHOULD by producing slightly longer chunk of bytes is better than breaking a foreign script which I know nothing about. */ QString wrapFormatFlowed(const QString &input) { // Determining a proper cutoff is not easy. The Q-P allows for at most 76 chars per line, not counting the trailing CR LF. // Suppose there's exactly one multibyte character which gets decoded into two bytes in UTF-8. After these two bytes are // encoded via quoted-printable, they will take up six bytes together (each byte is converted to hex and prepended by "="). // This means that such a line could contain only 70 ASCII characters and one non-ASCII one (assuming it gets encoded as a // two-byte sequence in UTF-8) and it will still fit into a single line in Q-P. // // However, the only points for making this guess right are: // a) to fit the SHOULD requirement in RFC 3676 [1] which says that line lengths should be <= 78 characters, // b) to avoid Q-P doing excessive line wrapping later on // // If the text contain non-ASCII characters, these are not going to be readable without a proper Q-P decoder. As such, the // argument for not triggering "useless" breaks at the QP time has much lower weight in such situations -- the aesthetics of // the raw, QP-encoded form are not terribly relevant IMHO. // // The b) leads us to 76 characters, while yet another sentence in RFC 3676 suggests wrapping at 72, and even speaks about // 66 as aestheticaly pleasing. // // Finally, because these "76 characters" have to include the trailing space, we're now at 75. // // [1] http://tools.ietf.org/html/rfc3676#section-4.2 const int defaultCutof = 75; QStringList res; Q_FOREACH(QString line, input.split(QLatin1Char('\n'), QString::KeepEmptyParts)) { line.remove(QLatin1Char('\r')); if (line.isEmpty()) { res << line; continue; } if (line.startsWith(QLatin1Char('>'))) { // Don't rewrap the quoted lines. The code below is buggy in that it will fail to preserve the quoting level when // wrapping. It is also a question of what exactly one should do when an overly long quotation is to be wrapped. // Assuming that the source was format=flowed might be a dangerous one, so it's probably better to just // produce overly long quotes for now. // Note that when we actually generate the quote, that text *is* (more or less correctly) wrapped and the quote // marks are prepended. The bug was in *this* function which tried to re-wrap the text once again. res << line; continue; } int previousBreak = 0; while (previousBreak < line.size()) { // Find a place to insert the line break int size = defaultCutof; if (line.size() <= previousBreak + size) { // We can safely use this chunk } else if (line.at(previousBreak + size) == QLatin1Char(' ')) { // We've found our space -- let's just use it and be done here } else { // OK, let's find a place to break the words at. At first, go back and try to find any possibility of reusing // an existing space. while (size > 0 && line.at(previousBreak + size) != QLatin1Char(' ')) { --size; } if (size == 0) { size = defaultCutof; while (previousBreak + size < line.size() && line.at(previousBreak + size) != QLatin1Char(' ')) { ++size; } } } Q_ASSERT(previousBreak + size >= line.size() || line.at(previousBreak + size) == QLatin1Char(' ')); // Now we want to insert the newline *after* the space ++size; res << line.mid(previousBreak, size); previousBreak += size; } } return res.join(QStringLiteral("\r\n")); } void decodeContentTransferEncoding(const QByteArray &rawData, const QByteArray &encoding, QByteArray *outputData) { Q_ASSERT(outputData); if (encoding == "quoted-printable") { *outputData = quotedPrintableDecode(rawData); } else if (encoding == "base64") { *outputData = QByteArray::fromBase64(rawData); } else if (encoding.isEmpty() || encoding == "7bit" || encoding == "8bit" || encoding == "binary") { *outputData = rawData; } else { qDebug() << "Warning: unknown encoding" << encoding; *outputData = rawData; } } } diff --git a/src/Imap/Parser/3rdparty/rfccodecs.cpp b/src/Imap/Parser/3rdparty/rfccodecs.cpp index 08839753..f0434ce9 100644 --- a/src/Imap/Parser/3rdparty/rfccodecs.cpp +++ b/src/Imap/Parser/3rdparty/rfccodecs.cpp @@ -1,250 +1,249 @@ /********************************************************************** * * rfccodecs.cpp - handler for various rfc/mime encodings * Copyright (C) 2000 s.carstens@gmx.de * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * *********************************************************************/ /** * @file * This file is part of the IMAP support library and defines the * RfcCodecs class. * * @brief * Defines the RfcCodecs class. * * @author Sven Carstens */ #include "rfccodecs.h" #include #include #include #include #include #include -#include #include #include #include "kcodecs.h" using namespace KIMAP; // This part taken from rfc 2192 IMAP URL Scheme. C. Newman. September 1997. // adapted to QT-Toolkit by Sven Carstens 2000 //@cond PRIVATE static const unsigned char base64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,"; #define UNDEFINED 64 #define MAXLINE 76 /* UTF16 definitions */ #define UTF16MASK 0x03FFUL #define UTF16SHIFT 10 #define UTF16BASE 0x10000UL #define UTF16HIGHSTART 0xD800UL #define UTF16HIGHEND 0xDBFFUL #define UTF16LOSTART 0xDC00UL #define UTF16LOEND 0xDFFFUL //@endcond //----------------------------------------------------------------------------- QString KIMAP::decodeImapFolderName( const QByteArray &src ) { unsigned char c, i, bitcount; unsigned long ucs4, utf16, bitbuf; unsigned char base64[256], utf8[6]; unsigned int srcPtr = 0; QByteArray dst; uint srcLen = src.length(); /* initialize modified base64 decoding table */ memset( base64, UNDEFINED, sizeof( base64 ) ); for ( i = 0; i < sizeof( base64chars ); ++i ) { base64[(int)base64chars[i]] = i; } /* loop until end of string */ while ( srcPtr < srcLen ) { c = src[srcPtr++]; /* deal with literal characters and &- */ if ( c != '&' || src[srcPtr] == '-' ) { /* encode literally */ dst += c; /* skip over the '-' if this is an &- sequence */ if ( c == '&' ) { srcPtr++; } } else { /* convert modified UTF-7 -> UTF-16 -> UCS-4 -> UTF-8 -> HEX */ bitbuf = 0; bitcount = 0; ucs4 = 0; while ( ( c = base64[(unsigned char)src[srcPtr]] ) != UNDEFINED ) { ++srcPtr; bitbuf = ( bitbuf << 6 ) | c; bitcount += 6; /* enough bits for a UTF-16 character? */ if ( bitcount >= 16 ) { bitcount -= 16; utf16 = ( bitcount ? bitbuf >> bitcount : bitbuf ) & 0xffff; /* convert UTF16 to UCS4 */ if ( utf16 >= UTF16HIGHSTART && utf16 <= UTF16HIGHEND ) { ucs4 = ( utf16 - UTF16HIGHSTART ) << UTF16SHIFT; continue; } else if ( utf16 >= UTF16LOSTART && utf16 <= UTF16LOEND ) { ucs4 += utf16 - UTF16LOSTART + UTF16BASE; } else { ucs4 = utf16; } /* convert UTF-16 range of UCS4 to UTF-8 */ if ( ucs4 <= 0x7fUL ) { utf8[0] = ucs4; i = 1; } else if ( ucs4 <= 0x7ffUL ) { utf8[0] = 0xc0 | ( ucs4 >> 6 ); utf8[1] = 0x80 | ( ucs4 & 0x3f ); i = 2; } else if ( ucs4 <= 0xffffUL ) { utf8[0] = 0xe0 | ( ucs4 >> 12 ); utf8[1] = 0x80 | ( ( ucs4 >> 6 ) & 0x3f ); utf8[2] = 0x80 | ( ucs4 & 0x3f ); i = 3; } else { utf8[0] = 0xf0 | ( ucs4 >> 18 ); utf8[1] = 0x80 | ( ( ucs4 >> 12 ) & 0x3f ); utf8[2] = 0x80 | ( ( ucs4 >> 6 ) & 0x3f ); utf8[3] = 0x80 | ( ucs4 & 0x3f ); i = 4; } /* copy it */ for ( c = 0; c < i; ++c ) { dst += utf8[c]; } } } /* skip over trailing '-' in modified UTF-7 encoding */ if ( src[srcPtr] == '-' ) { ++srcPtr; } } } return QString::fromUtf8( dst.data () ); } //----------------------------------------------------------------------------- QByteArray KIMAP::encodeImapFolderName( const QString &inSrc ) { unsigned int utf8pos, utf8total, c, utf7mode, bitstogo, utf16flag; unsigned int ucs4, bitbuf; QByteArray src = inSrc.toUtf8 (); QByteArray dst; int srcPtr = 0; utf7mode = 0; utf8total = 0; bitstogo = 0; utf8pos = 0; bitbuf = 0; ucs4 = 0; while ( srcPtr < src.length () ) { c = (unsigned char)src[srcPtr++]; /* normal character? */ if ( c >= ' ' && c <= '~' ) { /* switch out of UTF-7 mode */ if ( utf7mode ) { if ( bitstogo ) { dst += base64chars[( bitbuf << ( 6 - bitstogo ) ) & 0x3F]; bitstogo = 0; } dst += '-'; utf7mode = 0; } dst += c; /* encode '&' as '&-' */ if ( c == '&' ) { dst += '-'; } continue; } /* switch to UTF-7 mode */ if ( !utf7mode ) { dst += '&'; utf7mode = 1; } /* Encode US-ASCII characters as themselves */ if ( c < 0x80 ) { ucs4 = c; utf8total = 1; } else if ( utf8total ) { /* save UTF8 bits into UCS4 */ ucs4 = ( ucs4 << 6 ) | ( c & 0x3FUL ); if ( ++utf8pos < utf8total ) { continue; } } else { utf8pos = 1; if ( c < 0xE0 ) { utf8total = 2; ucs4 = c & 0x1F; } else if ( c < 0xF0 ) { utf8total = 3; ucs4 = c & 0x0F; } else { /* NOTE: can't convert UTF8 sequences longer than 4 */ utf8total = 4; ucs4 = c & 0x03; } continue; } /* loop to split ucs4 into two utf16 chars if necessary */ utf8total = 0; do { if ( ucs4 >= UTF16BASE ) { ucs4 -= UTF16BASE; bitbuf = ( bitbuf << 16 ) | ( ( ucs4 >> UTF16SHIFT ) + UTF16HIGHSTART ); ucs4 = ( ucs4 & UTF16MASK ) + UTF16LOSTART; utf16flag = 1; } else { bitbuf = ( bitbuf << 16 ) | ucs4; utf16flag = 0; } bitstogo += 16; /* spew out base64 */ while ( bitstogo >= 6 ) { bitstogo -= 6; dst += base64chars[( bitstogo ? ( bitbuf >> bitstogo ) : bitbuf ) & 0x3F]; } } while ( utf16flag ); } /* if in UTF-7 mode, finish in ASCII */ if ( utf7mode ) { if ( bitstogo ) { dst += base64chars[( bitbuf << ( 6 - bitstogo ) ) & 0x3F]; } dst += '-'; } return dst; } diff --git a/src/Imap/Parser/LowLevelParser.cpp b/src/Imap/Parser/LowLevelParser.cpp index 3de2bb45..f5416904 100644 --- a/src/Imap/Parser/LowLevelParser.cpp +++ b/src/Imap/Parser/LowLevelParser.cpp @@ -1,490 +1,510 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát + Copyright (C) 2018 Erik Quaeghebeur This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include +#include #include +#include +#include #include #include #include #include "LowLevelParser.h" #include "../Exceptions.h" #include "Imap/Encoders.h" namespace Imap { namespace LowLevelParser { template T extractNumber(const QByteArray &line, int &start) { if (start >= line.size()) throw NoData("extractNumber: no data", line, start); const char *c_str = line.constData(); c_str += start; if (*c_str < '0' || *c_str > '9') throw ParseError("extractNumber: not a number", line, start); T res = 0; // well, it's an inline function, but clang still won't cache its result by default const T absoluteMax = std::numeric_limits::max(); const T softLimit = (absoluteMax - 10) / 10; while (*c_str >= '0' && *c_str <= '9') { auto digit = *c_str - '0'; if (res <= softLimit) { res *= 10; res += digit; } else { if (res > absoluteMax / 10) throw ParseError("extractNumber: out of range", line, start); res *= 10; if (res > absoluteMax - digit) throw ParseError("extractNumber: out of range", line, start); res += digit; } ++c_str; ++start; } return res; } uint getUInt(const QByteArray &line, int &start) { return extractNumber(line, start); } quint64 getUInt64(const QByteArray &line, int &start) { return extractNumber(line, start); } #define C_STR_CHECK_FOR_ATOM_CHARS \ *c_str > '\x20' && *c_str != '\x7f' /* SP and CTL */ \ && *c_str != '(' && *c_str != ')' && *c_str != '{' /* explicitly forbidden */ \ && *c_str != '%' && *c_str != '*' /* list-wildcards */ \ && *c_str != '"' && *c_str != '\\' /* quoted-specials */ \ && *c_str != ']' /* resp-specials */ bool startsWithNil(const QByteArray &line, int start) { const char *c_str = line.constData(); c_str += start; // Case-insensitive NIL. We cannot use strncasecmp because that function respects locale settings which // is absolutely not something we want to do here. if (!(start <= line.size() + 3 && (*c_str == 'N' || *c_str == 'n') && (*(c_str+1) == 'I' || *(c_str+1) == 'i') && (*(c_str+2) == 'L' || *(c_str+2) == 'l'))) { return false; } // At this point we know that it starts with a NIL. To prevent parsing ambiguity with atoms, we have to // check the next character. c_str += 3; // That macro already checks for NULL bytes and the input is guaranteed to be null-terminated, so we're safe here if (C_STR_CHECK_FOR_ATOM_CHARS) { // The next character is apparently a valid atom-char, so this cannot possibly be a NIL return false; } return true; } QByteArray getAtom(const QByteArray &line, int &start) { if (start == line.size()) throw NoData("getAtom: no data", line, start); const char *c_str = line.constData(); c_str += start; const char * const old_str = c_str; while (C_STR_CHECK_FOR_ATOM_CHARS) { ++c_str; } auto size = c_str - old_str; if (!size) throw ParseError("getAtom: did not read anything", line, start); start += size; return QByteArray(old_str, size); } /** @short Special variation of getAtom which also accepts leading backslash */ QByteArray getPossiblyBackslashedAtom(const QByteArray &line, int &start) { if (start == line.size()) throw NoData("getPossiblyBackslashedAtom: no data", line, start); const char *c_str = line.constData(); c_str += start; const char * const old_str = c_str; if (*c_str == '\\') ++c_str; while (C_STR_CHECK_FOR_ATOM_CHARS) { ++c_str; } auto size = c_str - old_str; if (!size) throw ParseError("getPossiblyBackslashedAtom: did not read anything", line, start); start += size; return QByteArray(old_str, size); } QPair getString(const QByteArray &line, int &start) { if (start == line.size()) throw NoData("getString: no data", line, start); if (line[start] == '"') { // quoted string ++start; bool escaping = false; QByteArray res; bool terminated = false; while (start != line.size() && !terminated) { if (escaping) { escaping = false; if (line[start] == '"' || line[start] == '\\') { res.append(line[start]); } else if (line[start] == '(' || line[start] == ')') { // Got to support broken IMAP servers like Groupwise. // See https://bugs.kde.org/show_bug.cgi?id=334456 res.append(line[start]); // FIXME: change this to parser warning when they're implemented qDebug() << "IMAP parser: quoted-string escapes something else than quoted-specials"; } else { throw UnexpectedHere("getString: escaping invalid character", line, start); } } else { switch (line[start]) { case '"': terminated = true; break; case '\\': escaping = true; break; case '\r': case '\n': throw ParseError("getString: premature end of quoted string", line, start); default: res.append(line[start]); } } ++start; } if (!terminated) throw NoData("getString: unterminated quoted string", line, start); return qMakePair(res, QUOTED); } else if (line[start] == '{') { // literal ++start; int size = getUInt(line, start); if (line.mid(start, 3) != "}\r\n") throw ParseError("getString: malformed literal specification", line, start); start += 3; if (start + size > line.size()) throw NoData("getString: run out of data", line, start); int old(start); start += size; return qMakePair(line.mid(old, size), LITERAL); } else if (start < line.size() - 3 && line[start] == '~' && line[start + 1] == '{' ) { // literal8 start += 2; int size = getUInt(line, start); if (line.mid(start, 3) != "}\r\n") throw ParseError("getString: malformed literal8 specification", line, start); start += 3; if (start + size > line.size()) throw NoData("getString: literal8: run out of data", line, start); int old(start); start += size; return qMakePair(line.mid(old, size), LITERAL8); } else { throw UnexpectedHere("getString: did not get quoted string or literal", line, start); } } QPair getAString(const QByteArray &line, int &start) { if (start >= line.size()) throw NoData("getAString: no data", line, start); if (line[start] == '{' || line[start] == '"' || line[start] == '~') { return getString(line, start); } else { const char *c_str = line.constData(); c_str += start; const char * const old_str = c_str; bool gotRespSpecials = false; while (true) { while (C_STR_CHECK_FOR_ATOM_CHARS) { ++c_str; } if (*c_str == ']' /* got to explicitly allow resp-specials again...*/ ) { ++c_str; gotRespSpecials = true; continue; } else { break; } } auto size = c_str - old_str; if (!size) throw ParseError("getAString: did not read anything", line, start); start += size; return qMakePair(QByteArray(old_str, size), gotRespSpecials ? ASTRING : ATOM); } } QPair getNString(const QByteArray &line, int &start) { if (startsWithNil(line, start)) { start += 3; return qMakePair<>(QByteArray(), NIL); } else { return getAString(line, start); } } QString getMailbox(const QByteArray &line, int &start) { QPair r = getAString(line, start); if (r.first.toUpper() == "INBOX") return QStringLiteral("INBOX"); else return decodeImapFolderName(r.first); } QVariantList parseList(const char open, const char close, const QByteArray &line, int &start) { if (start >= line.size()) throw NoData("Could not parse list: no more data", line, start); if (line[start] == open) { // found the opening parenthesis ++start; if (start >= line.size()) throw NoData("Could not parse list: just the opening bracket", line, start); QVariantList res; if (line[start] == close) { ++start; return res; } while (line[start] != close) { // We want to be benevolent here and eat extra whitespace eatSpaces(line, start); res.append(getAnything(line, start)); if (start >= line.size()) throw NoData("Could not parse list: truncated data", line, start); // Eat whitespace after each token, too eatSpaces(line, start); if (line[start] == close) { ++start; return res; } } return res; } else { throw UnexpectedHere(std::string("Could not parse list: expected a list enclosed in ") + open + close + ", but got something else instead", line, start); } } QVariant getAnything(const QByteArray &line, int &start) { if (start >= line.size()) throw NoData("getAnything: no data", line, start); if (line[start] == '[') { QVariant res = parseList('[', ']', line, start); return res; } else if (line[start] == '(') { QVariant res = parseList('(', ')', line, start); return res; } else if (line[start] == '"' || line[start] == '{' || line[start] == '~') { QPair res = getString(line, start); return res.first; } else if (startsWithNil(line, start)) { start += 3; return QByteArray(); } else if (line[start] == '\\') { // valid for "flag" ++start; if (start >= line.size()) throw NoData("getAnything: backslash-nothing is invalid", line, start); if (line[start] == '*') { ++start; return QByteArray("\\*"); } return QByteArray(QByteArray(1, '\\') + getAtom(line, start)); } else { QByteArray atom = getAtom(line, start); if (atom.indexOf('[', 0) != -1) { // "BODY[something]" -- there's no whitespace between "[" and // next atom... int pos = line.indexOf(']', start); if (pos == -1) throw ParseError("getAnything: can't find ']' for the '['", line, start); ++pos; atom += line.mid(start, pos - start); start = pos; if (start < line.size() && line[start] == '<') { // Let's check if it continues with "" pos = line.indexOf('>', start); if (pos == -1) throw ParseError("getAnything: can't find proper ", line, start); ++pos; atom += line.mid(start, pos - start); start = pos; } } return atom; } } Imap::Uids getSequence(const QByteArray &line, int &start) { uint num = LowLevelParser::getUInt(line, start); if (start >= line.size() - 2) { // It's definitely just a number because there's no more data in here return Imap::Uids() << num; } else { Imap::Uids numbers; numbers << num; enum {COMMA, RANGE} currentType = COMMA; // Try to find further items in the sequence set while (line[start] == ':' || line[start] == ',') { // it's a sequence set if (line[start] == ':') { if (currentType == RANGE) { // Now "x:y:z" is a funny syntax throw UnexpectedHere("Sequence set: range cannot me defined by three numbers", line, start); } currentType = RANGE; } else { currentType = COMMA; } ++start; if (start >= line.size() - 2) throw NoData("Truncated sequence set", line, start); uint num = LowLevelParser::getUInt(line, start); if (currentType == COMMA) { // just adding one more to the set numbers << num; } else { // working with a range if (numbers.last() >= num) throw UnexpectedHere("Sequence set contains an invalid range. " "First item of a range must always be smaller than the second item.", line, start); for (uint i = numbers.last() + 1; i <= num; ++i) numbers << i; } } return numbers; } } QDateTime parseRFC2822DateTime(const QByteArray &input) { - QStringList monthNames = QStringList() << QStringLiteral("jan") << QStringLiteral("feb") << QStringLiteral("mar") - << QStringLiteral("apr") << QStringLiteral("may") << QStringLiteral("jun") - << QStringLiteral("jul") << QStringLiteral("aug") << QStringLiteral("sep") - << QStringLiteral("oct") << QStringLiteral("nov") << QStringLiteral("dec"); - - QRegExp rx(QString::fromUtf8("^(?:\\s*([A-Z][a-z]+)\\s*,\\s*)?" // date-of-week - "(\\d{1,2})\\s+(%1)\\s+(\\d{2,4})" // date - "\\s+(\\d{1,2})\\s*:(\\d{1,2})\\s*(?::\\s*(\\d{1,2})\\s*)?" // time - "(\\s+(?:(?:([+-]?)(\\d{2})(\\d{2}))|(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Za-ik-z])))?" // timezone - ).arg(monthNames.join(QLatin1String("|"))), Qt::CaseInsensitive); - int pos = rx.indexIn(QString::fromUtf8(input)); - - if (pos == -1) + static const QMap monthnumbers({ // default value is 0 + {QLatin1String("jan"), 1}, {QLatin1String("feb"), 2}, + {QLatin1String("mar"), 3}, {QLatin1String("apr"), 4}, + {QLatin1String("may"), 5}, {QLatin1String("jun"), 6}, + {QLatin1String("jul"), 7}, {QLatin1String("aug"), 8}, + {QLatin1String("sep"), 9}, {QLatin1String("oct"), 10}, + {QLatin1String("nov"), 11}, {QLatin1String("dec"), 12} + }); + + static const QMap tzoffsethrs({ // default value is 0 + {QLatin1String("EST"), 5}, {QLatin1String("EDT"), 4}, + {QLatin1String("CST"), 6}, {QLatin1String("CDT"), 5}, + {QLatin1String("MST"), 7}, {QLatin1String("MDT"), 6}, + {QLatin1String("PST"), 8}, {QLatin1String("PDT"), 7} + }); + + static const QRegularExpression rx( + QLatin1String("^\\s*" + "(?:" + "([A-Z][a-z]+)" // 1: day-of-week (may be empty) + "\\s*,\\s*" + ")?" + "(\\d{1,2})" // 2: day + "\\s+" + "(") // 3: month + + QStringList(monthnumbers.keys()).join(QLatin1Char('|')) + // wrapping with QStringList because Qt 5.2 has no join for Qlist, unlike Qt >=5.5 (Qt 5.3-4?) + + QLatin1String(")" + "\\s+" + "(\\d{2,4})" // 4: year + "\\s+" + "(\\d{1,2})" // 5: hours + "\\s*:\\s*" + "(\\d{1,2})" // 6: minutes + "(?:" + "\\s*:\\s*" + "(\\d{1,2})" // 7: seconds (may be empty) + ")?" + "(?:" + "\\s+" + "(?:" // timezone (some or all may be empty) + "(" // 8: timezone offset + "([+-]?)" // 9: offset direction + "(\\d{2})" // 10: offset hours + "(\\d{2})" // 11: offset minutes + ")" + "|" + "(") // 12: timezone code + + QStringList(tzoffsethrs.keys()).join(QLatin1Char('|')) + // codes not considered are ignored and implicitly assumed to correspond to UTC + // wrapping with QStringList because Qt 5.2 has no join for Qlist, unlike Qt >=5.5 (Qt 5.3-4?) + + QLatin1String( ")" + ")" + ")?" + "\\s*"), + QRegularExpression::CaseInsensitiveOption); + + QRegularExpressionMatch match = rx.match(QString::fromUtf8(input)); + if (!match.hasMatch()) throw ParseError("Date format not recognized"); - QStringList list = rx.capturedTexts(); - - if (list.size() != 13) - throw ParseError("Date regular expression returned weird data (internal error?)"); - - int year = list[4].toInt(); - int month = monthNames.indexOf(list[3].toLower()) + 1; + int year = match.captured(4).toInt(); + int month = monthnumbers[match.captured(3).toLower()]; if (month == 0) throw ParseError("Invalid month name"); - int day = list[2].toInt(); - int hours = list[5].toInt(); - int minutes = list[6].toInt(); - int seconds = list[7].toInt(); - int shift = list[10].toInt() * 60 + list[11].toInt(); - if (list[9] == QLatin1String("-")) - shift *= 60; - else - shift *= -60; - if (! list[12].isEmpty()) { - const QString tz = list[12].toUpper(); - if (tz == QLatin1String("UT") || tz == QLatin1String("GMT")) - shift = 0; - else if (tz == QLatin1String("EST")) - shift = 5 * 3600; - else if (tz == QLatin1String("EDT")) - shift = 4 * 3600; - else if (tz == QLatin1String("CST")) - shift = 6 * 3600; - else if (tz == QLatin1String("CDT")) - shift = 5 * 3600; - else if (tz == QLatin1String("MST")) - shift = 7 * 3600; - else if (tz == QLatin1String("MDT")) - shift = 6 * 3600; - else if (tz == QLatin1String("PST")) - shift = 8 * 3600; - else if (tz == QLatin1String("PDT")) - shift = 7 * 3600; - else if (tz.size() == 1) - shift = 0; - else - throw ParseError("Invalid TZ specification"); - } - - QDateTime date(QDate(year, month, day), QTime(hours, minutes, seconds), Qt::UTC); - date = date.addSecs(shift); - - return date; + int day = match.captured(2).toInt(); + int hours = match.captured(5).toInt(); + int minutes = match.captured(6).toInt(); + int seconds = match.captured(7).toInt(); + int shift(0); + if (!match.captured(8).isEmpty()) { + shift = (match.captured(10).toInt() * 60 + match.captured(11).toInt()) * 60; + if (match.captured(9) != QLatin1String("-")) + shift *= -1; + } else if (!match.captured(12).isEmpty()) + shift = tzoffsethrs[match.captured(12).toUpper()] * 3600; + + return QDateTime(QDate(year, month, day), QTime(hours, minutes, seconds), + Qt::UTC).addSecs(shift); // TODO: perhaps use Qt::OffsetFromUTC timespec instead to preserve more information } void eatSpaces(const QByteArray &line, int &start) { while (line.size() > start && line[start] == ' ') ++start; } } } diff --git a/src/Imap/Parser/MailAddress.cpp b/src/Imap/Parser/MailAddress.cpp index ba7b1a72..64823fee 100644 --- a/src/Imap/Parser/MailAddress.cpp +++ b/src/Imap/Parser/MailAddress.cpp @@ -1,338 +1,341 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include +#include +#include #include #include #include #include #include "MailAddress.h" #include "../Model/MailboxTree.h" #include "../Encoders.h" #include "../Parser/Rfc5322HeaderParser.h" #include "UiUtils/Formatting.h" namespace Imap { namespace Message { bool MailAddress::fromPrettyString(MailAddress &into, const QString &address) { int offset = 0; if (!parseOneAddress(into, address, offset)) return false; if (offset < address.size()) return false; return true; } /* Regexpes to match an address typed into the input field. */ -static QRegExp mailishRx1(QLatin1String("^\\s*([\\w!#$%&'*+-/=?^_`{|}~]+)\\s*\\@" - "\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))\\s*$")); -static QRegExp mailishRx2(QLatin1String("\\s*<([\\w!#$%&'*+-/=?^_`{|}~]+)\\s*\\@" - "\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))>\\s*$")); +static QRegularExpression mailishRx1(QLatin1String("^\\s*([\\w!#$%&'*+-/=?^_`{|}~]+)\\s*\\@" + "\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))\\s*$")); +static QRegularExpression mailishRx2(QLatin1String("\\s*<([\\w!#$%&'*+-/=?^_`{|}~]+)\\s*\\@" + "\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))>\\s*$")); /* This is of course far from complete, but at least catches "Real Name" . It needs to recognize the things people actually type, and it should also recognize anything that's a valid rfc2822 address. */ bool MailAddress::parseOneAddress(Imap::Message::MailAddress &into, const QString &address, int &startOffset) { - for (QRegExp mailishRx : {mailishRx2, mailishRx1}) { - int offset = mailishRx.indexIn(address, startOffset); - if (offset >= 0) { + for (QRegularExpression mailishRx : {mailishRx2, mailishRx1}) { + QRegularExpressionMatch match = mailishRx.match(address, startOffset); + int offset = match.capturedStart(); + if (match.hasMatch()) { QString before = address.mid(startOffset, offset - startOffset); - into = MailAddress(before.simplified(), QString(), mailishRx.cap(1), mailishRx.cap(2)); + into = MailAddress(before.simplified(), QString(), match.captured(1), match.captured(2)); - offset += mailishRx.matchedLength(); + offset += match.capturedLength(); startOffset = offset; return true; } } return false; } MailAddress::MailAddress(const QVariantList &input, const QByteArray &line, const int start) { // FIXME: all offsets are wrong here if (input.size() != 4) throw ParseError("MailAddress: not four items", line, start); if (input[0].type() != QVariant::ByteArray) throw UnexpectedHere("MailAddress: item#1 not a QByteArray", line, start); if (input[1].type() != QVariant::ByteArray) throw UnexpectedHere("MailAddress: item#2 not a QByteArray", line, start); if (input[2].type() != QVariant::ByteArray) throw UnexpectedHere("MailAddress: item#3 not a QByteArray", line, start); if (input[3].type() != QVariant::ByteArray) throw UnexpectedHere("MailAddress: item#4 not a QByteArray", line, start); name = Imap::decodeRFC2047String(input[0].toByteArray()); adl = Imap::decodeRFC2047String(input[1].toByteArray()); mailbox = Imap::decodeRFC2047String(input[2].toByteArray()); host = Imap::decodeRFC2047String(input[3].toByteArray()); } QUrl MailAddress::asUrl() const { QUrl url; url.setScheme(QStringLiteral("mailto")); url.setPath(QStringLiteral("%1@%2").arg(mailbox, host)); if (!name.isEmpty()) { QUrlQuery q(url); q.addQueryItem(QStringLiteral("X-Trojita-DisplayName"), name); url.setQuery(q); } return url; } QString MailAddress::prettyName(FormattingMode mode) const { bool hasNiceName = !name.isEmpty(); if (!hasNiceName && mode == FORMAT_JUST_NAME) mode = FORMAT_READABLE; if (mode == FORMAT_JUST_NAME) { return name; } else { QString address = mailbox + QLatin1Char('@') + host; QString niceName; if (hasNiceName) { niceName = name; } else { niceName = address; } if (mode == FORMAT_READABLE) { if (hasNiceName) { return name + QLatin1String(" <") + address + QLatin1Char('>'); } else { return address; } } else { if (mode == FORMAT_SHORT_CLICKABLE) UiUtils::elideAddress(niceName); return QStringLiteral("%2").arg(asUrl().toString().toHtmlEscaped(), niceName.toHtmlEscaped()); } } } QString MailAddress::prettyList(const QList &list, FormattingMode mode) { QStringList buf; for (QList::const_iterator it = list.begin(); it != list.end(); ++it) buf << it->prettyName(mode); return buf.join(QStringLiteral(", ")); } QString MailAddress::prettyList(const QVariantList &list, FormattingMode mode) { QStringList buf; for (QVariantList::const_iterator it = list.begin(); it != list.end(); ++it) { Q_ASSERT(it->type() == QVariant::StringList); QStringList item = it->toStringList(); Q_ASSERT(item.size() == 4); MailAddress a(item[0], item[1], item[2], item[3]); buf << a.prettyName(mode); } return buf.join(QStringLiteral(", ")); } -static QRegExp dotAtomRx(QLatin1String("[A-Za-z0-9!#$&'*+/=?^_`{}|~-]+(?:\\.[A-Za-z0-9!#$&'*+/=?^_`{}|~-]+)*")); +static QRegularExpression dotAtomRx(QLatin1String("^[A-Za-z0-9!#$&'*+/=?^_`{}|~-]+(?:\\.[A-Za-z0-9!#$&'*+/=?^_`{}|~-]+)*$")); /* This returns the address formatted for use in an SMTP MAIL or RCPT command; specifically, it matches the "Mailbox" production of RFC2821. The surrounding angle-brackets are not included. */ QByteArray MailAddress::asSMTPMailbox() const { QByteArray result; /* Check whether the local-part contains any characters preventing it from being a dot-atom. */ - if (dotAtomRx.exactMatch(mailbox)) { + if (dotAtomRx.match(mailbox).hasMatch()) { /* Using .toLatin1() here even though we know it only contains ASCII, because QString.toAscii() does not necessarily convert to ASCII (despite the name). .toLatin1() always converts to Latin-1. */ result = mailbox.toLatin1(); } else { /* The other syntax allowed for local-parts is a double-quoted string. Note that RFC2047 tokens aren't allowed there --- local-parts are fundamentally bytestrings, apparently, whose interpretation is up to the receiving system. If someone types non-ASCII characters into the address field we'll generate non-conforming headers, but it's the best we can do. */ result = Imap::quotedString(mailbox.toUtf8()); } result.append("@"); QByteArray domainpart; if (!(host.startsWith(QLatin1Char('[')) || host.endsWith(QLatin1Char(']')))) { /* IDN-encode the hostname part of the address */ domainpart = QUrl::toAce(host); /* TODO: QUrl::toAce() is documented to return an empty result if the string isn't a valid hostname --- for example, if it's a domain literal containing an IP address. In that case, we'll need to encode it ourselves (making sure there are square brackets, no forbidden characters, appropriate backslashes, and so on). */ } if (domainpart.isEmpty()) { /* Either the domainpart looks like a domain-literal, or toAce() failed. */ domainpart = host.toUtf8(); if (domainpart.startsWith('[')) { domainpart.remove(0, 1); } if (domainpart.endsWith(']')) { domainpart.remove(domainpart.size()-1, 1); } result.append(Imap::quotedString(domainpart, Imap::SquareBrackets)); } else { result.append(domainpart); } return result; } QByteArray MailAddress::asMailHeader() const { QByteArray result = Imap::encodeRFC2047Phrase(name); if (!result.isEmpty()) result.append(" "); result.append("<"); result.append(asSMTPMailbox()); result.append(">"); return result; } /** @short The mail address usable for manipulation by user */ QString MailAddress::asPrettyString() const { return name.isEmpty() ? QString::fromUtf8(asSMTPMailbox()) : name + QLatin1Char(' ') + QLatin1Char('<') + QString::fromUtf8(asSMTPMailbox()) + QLatin1Char('>'); } /** @short Is the human-readable part "useful", i.e. does it contain something else besides the e-mail address? */ bool MailAddress::hasUsefulDisplayName() const { return !name.isEmpty() && name.trimmed().toUtf8().toLower() != asSMTPMailbox().toLower(); } /** @short Convert a QUrl into a MailAddress instance */ bool MailAddress::fromUrl(MailAddress &into, const QUrl &url, const QString &expectedScheme) { if (url.scheme().toLower() != expectedScheme.toLower()) return false; QStringList list = url.path().split(QLatin1Char('@')); if (list.size() != 2) return false; QUrlQuery q(url); Imap::Message::MailAddress addr(q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")), QString(), list[0], list[1]); if (!addr.hasUsefulDisplayName()) addr.name.clear(); into = addr; return true; } /** @short Helper to construct this from a pair of (human readable name, e-mail address) This is mainly useful to prevent reimplementing the @-based joining all the time. */ MailAddress MailAddress::fromNameAndMail(const QString &name, const QString &email) { auto components = email.split(QLatin1Char('@')); if (components.size() == 2) { return MailAddress(name, QString(), components[0], components[1]); } else { // garbage in, garbage out return MailAddress(name, QString(), email, QString()); } } QTextStream &operator<<(QTextStream &stream, const MailAddress &address) { stream << '"' << address.name << "\" <"; if (!address.host.isNull()) stream << address.mailbox << '@' << address.host; else stream << address.mailbox; stream << '>'; return stream; } bool operator==(const MailAddress &a, const MailAddress &b) { return a.name == b.name && a.adl == b.adl && a.mailbox == b.mailbox && a.host == b.host; } MailAddressesEqualByMail::result_type MailAddressesEqualByMail::operator()(const MailAddress &a, const MailAddress &b) const { // FIXME: fancy stuff like the IDN? return a.mailbox.toLower() == b.mailbox.toLower() && a.host.toLower() == b.host.toLower(); } MailAddressesEqualByDomain::result_type MailAddressesEqualByDomain::operator()(const MailAddress &a, const MailAddress &b) const { // FIXME: fancy stuff like the IDN? return a.host.toLower() == b.host.toLower(); } MailAddressesEqualByDomainSuffix::result_type MailAddressesEqualByDomainSuffix::operator()(const MailAddress &a, const MailAddress &b) const { // FIXME: fancy stuff like the IDN? auto aHost = a.host.toLower(); auto bHost = b.host.toLower(); return aHost == bHost || aHost.endsWith(QLatin1Char('.') + bHost); } } } QDataStream &operator>>(QDataStream &stream, Imap::Message::MailAddress &a) { return stream >> a.adl >> a.host >> a.mailbox >> a.name; } QDataStream &operator<<(QDataStream &stream, const Imap::Message::MailAddress &a) { return stream << a.adl << a.host << a.mailbox << a.name; } diff --git a/src/Imap/Parser/Message.cpp b/src/Imap/Parser/Message.cpp index 7323e73e..ccbc3c0e 100644 --- a/src/Imap/Parser/Message.cpp +++ b/src/Imap/Parser/Message.cpp @@ -1,875 +1,872 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include "Message.h" #include "MailAddress.h" #include "LowLevelParser.h" #include "../Model/MailboxTree.h" #include "../Encoders.h" #include "../Parser/Rfc5322HeaderParser.h" namespace Imap { namespace Message { -/* A simple regexp to match an address typed into the input field. */ -static QRegExp mailishRx(QLatin1String("(?:\\b|\\<)([\\w_.-+]+)\\s*\\@\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))(?:\\b|\\>)")); - QList Envelope::getListOfAddresses(const QVariant &in, const QByteArray &line, const int start) { if (in.type() == QVariant::ByteArray) { if (! in.toByteArray().isNull()) throw UnexpectedHere("getListOfAddresses: byte array not null", line, start); } else if (in.type() != QVariant::List) { throw ParseError("getListOfAddresses: not a list", line, start); } QVariantList list = in.toList(); QList res; for (QVariantList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { if (it->type() != QVariant::List) throw UnexpectedHere("getListOfAddresses: split item not a list", line, start); // FIXME: wrong offset res.append(MailAddress(it->toList(), line, start)); } return res; } Envelope Envelope::fromList(const QVariantList &items, const QByteArray &line, const int start) { if (items.size() != 10) throw ParseError("Envelope::fromList: size != 10", line, start); // FIXME: wrong offset // date QDateTime date; if (items[0].type() == QVariant::ByteArray) { QByteArray dateStr = items[0].toByteArray(); if (! dateStr.isEmpty()) { try { date = LowLevelParser::parseRFC2822DateTime(dateStr); } catch (ParseError &) { // FIXME: log this //throw ParseError( e.what(), line, start ); } } } // Otherwise it's "invalid", null. QString subject = Imap::decodeRFC2047String(items[1].toByteArray()); QList from, sender, replyTo, to, cc, bcc; from = Envelope::getListOfAddresses(items[2], line, start); sender = Envelope::getListOfAddresses(items[3], line, start); replyTo = Envelope::getListOfAddresses(items[4], line, start); to = Envelope::getListOfAddresses(items[5], line, start); cc = Envelope::getListOfAddresses(items[6], line, start); bcc = Envelope::getListOfAddresses(items[7], line, start); LowLevelParser::Rfc5322HeaderParser headerParser; if (items[8].type() != QVariant::ByteArray) throw UnexpectedHere("Envelope::fromList: inReplyTo not a QByteArray", line, start); QByteArray inReplyTo = items[8].toByteArray(); if (items[9].type() != QVariant::ByteArray) throw UnexpectedHere("Envelope::fromList: messageId not a QByteArray", line, start); QByteArray messageId = items[9].toByteArray(); QByteArray buf; if (!messageId.isEmpty()) buf += "Message-Id: " + messageId + "\r\n"; if (!inReplyTo.isEmpty()) buf += "In-Reply-To: " + inReplyTo + "\r\n"; if (!buf.isEmpty()) { bool ok = headerParser.parse(buf); if (!ok) { qDebug() << "Envelope::fromList: malformed headers"; } } // If the Message-Id fails to parse, well, bad luck. This enforced sanitizaion is hopefully better than // generating garbage in outgoing e-mails. messageId = headerParser.messageId.size() == 1 ? headerParser.messageId.front() : QByteArray(); return Envelope(date, subject, from, sender, replyTo, to, cc, bcc, headerParser.inReplyTo, messageId); } void Envelope::clear() { date = QDateTime(); subject.clear(); from.clear(); sender.clear(); replyTo.clear(); to.clear(); cc.clear(); bcc.clear(); inReplyTo.clear(); messageId.clear(); } bool OneMessage::eq(const AbstractData &other) const { try { const OneMessage &o = dynamic_cast(other); return o.mediaType == mediaType && mediaSubType == o.mediaSubType && bodyFldParam == o.bodyFldParam && bodyFldId == o.bodyFldId && bodyFldDesc == o.bodyFldDesc && bodyFldEnc == o.bodyFldEnc && bodyFldOctets == o.bodyFldOctets && bodyFldMd5 == o.bodyFldMd5 && bodyFldDsp == o.bodyFldDsp && bodyFldLang == o.bodyFldLang && bodyFldLoc == o.bodyFldLoc && bodyExtension == o.bodyExtension; } catch (std::bad_cast &) { return false; } } bool TextMessage::eq(const AbstractData &other) const { try { const TextMessage &o = dynamic_cast(other); return OneMessage::eq(o) && bodyFldLines == o.bodyFldLines; } catch (std::bad_cast &) { return false; } } QTextStream &TextMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); return s << QByteArray(indent, ' ') << "TextMessage( " << mediaType << "/" << mediaSubType << lf << i << "body-fld-param: " << bodyFldParam << lf << i << "body-fld-id: " << bodyFldId << lf << i << "body-fld-desc: " << bodyFldDesc << lf << i << "body-fld-enc: " << bodyFldEnc << lf << i << "body-fld-octets: " << bodyFldOctets << lf << i << "bodyFldMd5: " << bodyFldMd5 << lf << i << "body-fld-dsp: " << bodyFldDsp << lf << i << "body-fld-lang: " << bodyFldLang << lf << i << "body-fld-loc: " << bodyFldLoc << lf << i << "body-extension is " << bodyExtension.typeName() << lf << i << "body-fld-lines: " << bodyFldLines << lf << QByteArray(indent, ' ') << ")"; // FIXME: operator<< for QVariant... } bool MsgMessage::eq(const AbstractData &other) const { try { const MsgMessage &o = dynamic_cast(other); if (o.body) { if (body) { if (*body != *o.body) { return false; } } else { return false; } } else if (body) { return false; } return OneMessage::eq(o) && bodyFldLines == o.bodyFldLines && envelope == o.envelope; } catch (std::bad_cast &) { return false; } } QTextStream &MsgMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); s << QByteArray(indent, ' ') << "MsgMessage(" << lf; envelope.dump(s, indent + 1); s << i << "body-fld-lines " << bodyFldLines << lf << i << "body:" << lf; s << i << "body-fld-param: " << bodyFldParam << lf << i << "body-fld-id: " << bodyFldId << lf << i << "body-fld-desc: " << bodyFldDesc << lf << i << "body-fld-enc: " << bodyFldEnc << lf << i << "body-fld-octets: " << bodyFldOctets << lf << i << "bodyFldMd5: " << bodyFldMd5 << lf << i << "body-fld-dsp: " << bodyFldDsp << lf << i << "body-fld-lang: " << bodyFldLang << lf << i << "body-fld-loc: " << bodyFldLoc << lf << i << "body-extension is " << bodyExtension.typeName() << lf; if (body) body->dump(s, indent + 2); else s << i << " (null)"; return s << lf << QByteArray(indent, ' ') << ")"; } QTextStream &BasicMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); return s << QByteArray(indent, ' ') << "BasicMessage( " << mediaType << "/" << mediaSubType << lf << i << "body-fld-param: " << bodyFldParam << lf << i << "body-fld-id: " << bodyFldId << lf << i << "body-fld-desc: " << bodyFldDesc << lf << i << "body-fld-enc: " << bodyFldEnc << lf << i << "body-fld-octets: " << bodyFldOctets << lf << i << "bodyFldMd5: " << bodyFldMd5 << lf << i << "body-fld-dsp: " << bodyFldDsp << lf << i << "body-fld-lang: " << bodyFldLang << lf << i << "body-fld-loc: " << bodyFldLoc << lf << i << "body-extension is " << bodyExtension.typeName() << lf << QByteArray(indent, ' ') << ")"; // FIXME: operator<< for QVariant... } bool MultiMessage::eq(const AbstractData &other) const { try { const MultiMessage &o = dynamic_cast(other); if (bodies.count() != o.bodies.count()) { return false; } for (int i = 0; i < bodies.count(); ++i) { if (bodies[i]) { if (o.bodies[i]) { if (*bodies[i] != *o.bodies[i]) { return false; } } else { return false; } } else if (! o.bodies[i]) { return false; } } return mediaSubType == o.mediaSubType && bodyFldParam == o.bodyFldParam && bodyFldDsp == o.bodyFldDsp && bodyFldLang == o.bodyFldLang && bodyFldLoc == o.bodyFldLoc && bodyExtension == o.bodyExtension; } catch (std::bad_cast &) { return false; } } QTextStream &MultiMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); s << QByteArray(indent, ' ') << "MultiMessage( multipart/" << mediaSubType << lf << i << "body-fld-param " << bodyFldParam << lf << i << "body-fld-dsp " << bodyFldDsp << lf << i << "body-fld-lang " << bodyFldLang << lf << i << "body-fld-loc " << bodyFldLoc << lf << i << "bodyExtension is " << bodyExtension.typeName() << lf << i << "bodies: [ " << lf; for (QList >::const_iterator it = bodies.begin(); it != bodies.end(); ++it) if (*it) { (**it).dump(s, indent + 2); s << lf; } else s << i << " (null)" << lf; return s << QByteArray(indent, ' ') << "] )"; } AbstractMessage::bodyFldParam_t AbstractMessage::makeBodyFldParam(const QVariant &input, const QByteArray &line, const int start) { bodyFldParam_t map; if (input.type() != QVariant::List) { if (input.type() == QVariant::ByteArray && input.toByteArray().isNull()) return map; throw UnexpectedHere("body-fld-param: not a list / nil", line, start); } QVariantList list = input.toList(); if (list.size() % 2) throw UnexpectedHere("body-fld-param: wrong number of entries", line, start); for (int j = 0; j < list.size(); j += 2) if (list[j].type() != QVariant::ByteArray || list[j+1].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-param: string not found", line, start); else map[ list[j].toByteArray().toUpper() ] = list[j+1].toByteArray(); return map; } AbstractMessage::bodyFldDsp_t AbstractMessage::makeBodyFldDsp(const QVariant &input, const QByteArray &line, const int start) { bodyFldDsp_t res; if (input.type() != QVariant::List) { if (input.type() == QVariant::ByteArray) { if (input.toByteArray().isNull()) { return res; } else { qDebug() << "IMAP Parser warning: body-fld-dsp not a list or nil, got this instead: " << input.toByteArray(); return res; } } throw UnexpectedHere("body-fld-dsp: not a list / nil", line, start); } QVariantList list = input.toList(); if (list.size() < 1) { throw ParseError("body-fld-dsp: empty list is not allowed", line, start); } if (list[0].type() != QVariant::ByteArray) { throw UnexpectedHere("body-fld-dsp: first item is not a string", line, start); } res.first = list[0].toByteArray(); if (list.size() > 2) { throw ParseError("body-fld-dsp: too many items in the list", line, start); } else if (list.size() == 2) { res.second = makeBodyFldParam(list[1], line, start); } else { qDebug() << "IMAP Parser warning: body-fld-dsp: second item not present, ignoring"; } return res; } QList AbstractMessage::makeBodyFldLang(const QVariant &input, const QByteArray &line, const int start) { QList res; if (input.type() == QVariant::ByteArray) { if (input.toByteArray().isNull()) // handle NIL return res; res << input.toByteArray(); } else if (input.type() == QVariant::List) { QVariantList list = input.toList(); for (QVariantList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) if (it->type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-lang has wrong structure", line, start); else res << it->toByteArray(); } else throw UnexpectedHere("body-fld-lang not found", line, start); return res; } uint AbstractMessage::extractUInt(const QVariant &var, const QByteArray &line, const int start) { if (var.type() == QVariant::UInt) return var.toUInt(); if (var.type() == QVariant::ByteArray) { bool ok = false; int number = var.toInt(&ok); if (ok) { if (number >= 0) { return number; } else { qDebug() << "Parser warning:" << number << "is not an unsigned int"; return 0; } } else if (var.toByteArray().isEmpty()) { qDebug() << "Parser warning: expected unsigned int, but got NIL or an empty string instead, yuck"; return 0; } else { throw UnexpectedHere("extractUInt: not a number", line, start); } } throw UnexpectedHere("extractUInt: weird data type", line, start); } quint64 AbstractMessage::extractUInt64(const QVariant &var, const QByteArray &line, const int start) { if (var.type() == QVariant::ULongLong) return var.toULongLong(); if (var.type() == QVariant::ByteArray) { bool ok = false; qint64 number = var.toLongLong(&ok); if (ok) { if (number >= 0) { return number; } else { qDebug() << "Parser warning:" << number << "is not an unsigned 64 bit int"; return 0; } } else if (var.toByteArray().isEmpty()) { qDebug() << "Parser warning: expected unsigned 64 bit int, but got NIL or an empty string instead, yuck"; return 0; } else { throw UnexpectedHere("extractUInt64: not a number", line, start); } } throw UnexpectedHere("extractUInt64: weird data type", line, start); } QSharedPointer AbstractMessage::fromList(const QVariantList &items, const QByteArray &line, const int start) { if (items.size() < 2) throw NoData("AbstractMessage::fromList: no data", line, start); if (items[0].type() == QVariant::ByteArray) { // it's a single-part message, hurray int i = 0; QByteArray mediaType = items[i].toByteArray().toLower(); ++i; QByteArray mediaSubType = items[i].toByteArray().toLower(); ++i; if (items.size() < 7) { qDebug() << "AbstractMessage::fromList(): body-type-basic(?): yuck, too few items, using what we've got"; } bodyFldParam_t bodyFldParam; if (i < items.size()) { bodyFldParam = makeBodyFldParam(items[i], line, start); ++i; } QByteArray bodyFldId; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-id not recognized as a ByteArray", line, start); bodyFldId = items[i].toByteArray(); ++i; } QByteArray bodyFldDesc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-desc not recognized as a ByteArray", line, start); bodyFldDesc = items[i].toByteArray(); ++i; } QByteArray bodyFldEnc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-enc not recognized as a ByteArray", line, start); bodyFldEnc = items[i].toByteArray(); ++i; } quint64 bodyFldOctets = 0; if (i < items.size()) { bodyFldOctets = extractUInt64(items[i], line, start); ++i; } uint bodyFldLines = 0; Envelope envelope; QSharedPointer body; enum { MESSAGE, TEXT, BASIC} kind; if (mediaType == "message" && mediaSubType == "rfc822") { // extract envelope, body, body-fld-lines if (items.size() < 10) throw NoData("too few fields for a Message-message", line, start); kind = MESSAGE; if (items[i].type() == QVariant::ByteArray && items[i].toByteArray().isEmpty()) { // ENVELOPE is NIL, this shouldn't really happen qDebug() << "AbstractMessage::fromList(): message/rfc822: yuck, got NIL for envelope"; } else if (items[i].type() != QVariant::List) { throw UnexpectedHere("message/rfc822: envelope not a list", line, start); } else { envelope = Envelope::fromList(items[i].toList(), line, start); } ++i; if (items[i].type() != QVariant::List) throw UnexpectedHere("message/rfc822: body not recognized as a list", line, start); body = AbstractMessage::fromList(items[i].toList(), line, start); ++i; try { bodyFldLines = extractUInt(items[i], line, start); } catch (const UnexpectedHere &) { qDebug() << "AbstractMessage::fromList(): message/rfc822: yuck, invalid body-fld-lines"; } ++i; } else if (mediaType == "text") { kind = TEXT; if (i < items.size()) { // extract body-fld-lines bodyFldLines = extractUInt(items[i], line, start); ++i; } } else { // don't extract anything as we're done here kind = BASIC; } // extract body-ext-1part // body-fld-md5 QByteArray bodyFldMd5; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-md5 not a ByteArray", line, start); bodyFldMd5 = items[i].toByteArray(); ++i; } // body-fld-dsp bodyFldDsp_t bodyFldDsp; if (i < items.size()) { bodyFldDsp = makeBodyFldDsp(items[i], line, start); ++i; } // body-fld-lang QList bodyFldLang; if (i < items.size()) { bodyFldLang = makeBodyFldLang(items[i], line, start); ++i; } // body-fld-loc QByteArray bodyFldLoc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-loc not found", line, start); bodyFldLoc = items[i].toByteArray(); ++i; } // body-extension QVariant bodyExtension; if (i < items.size()) { if (i == items.size() - 1) { bodyExtension = items[i]; ++i; } else { QVariantList list; for (; i < items.size(); ++i) list << items[i]; bodyExtension = list; } } switch (kind) { case MESSAGE: return QSharedPointer( new MsgMessage(mediaType, mediaSubType, bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, bodyFldMd5, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension, envelope, body, bodyFldLines) ); case TEXT: return QSharedPointer( new TextMessage(mediaType, mediaSubType, bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, bodyFldMd5, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension, bodyFldLines) ); case BASIC: default: return QSharedPointer( new BasicMessage(mediaType, mediaSubType, bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, bodyFldMd5, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension) ); } } else if (items[0].type() == QVariant::List) { if (items.size() < 2) throw ParseError("body-type-mpart: structure should be \"body* string\"", line, start); int i = 0; QList > bodies; while (items[i].type() == QVariant::List) { bodies << fromList(items[i].toList(), line, start); ++i; } if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-type-mpart: media-subtype not recognized", line, start); QByteArray mediaSubType = items[i].toByteArray().toLower(); ++i; // body-ext-mpart // body-fld-param bodyFldParam_t bodyFldParam; if (i < items.size()) { bodyFldParam = makeBodyFldParam(items[i], line, start); ++i; } // body-fld-dsp bodyFldDsp_t bodyFldDsp; if (i < items.size()) { bodyFldDsp = makeBodyFldDsp(items[i], line, start); ++i; } // body-fld-lang QList bodyFldLang; if (i < items.size()) { bodyFldLang = makeBodyFldLang(items[i], line, start); ++i; } // body-fld-loc QByteArray bodyFldLoc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-loc not found", line, start); bodyFldLoc = items[i].toByteArray(); ++i; } // body-extension QVariant bodyExtension; if (i < items.size()) { if (i == items.size() - 1) { bodyExtension = items[i]; ++i; } else { QVariantList list; for (; i < items.size(); ++i) list << items[i]; bodyExtension = list; } } return QSharedPointer( new MultiMessage(bodies, mediaSubType, bodyFldParam, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension)); } else { throw UnexpectedHere("AbstractMessage::fromList: invalid data type of first item", line, start); } } void dumpListOfAddresses(QTextStream &stream, const QList &list, const int indent) { QByteArray lf("\n"); switch (list.size()) { case 0: stream << "[ ]" << lf; break; case 1: stream << "[ " << list.front() << " ]" << lf; break; default: { QByteArray i(indent + 1, ' '); stream << "[" << lf; for (QList::const_iterator it = list.begin(); it != list.end(); ++it) stream << i << *it << lf; stream << QByteArray(indent, ' ') << "]" << lf; } } } QTextStream &Envelope::dump(QTextStream &stream, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); stream << QByteArray(indent, ' ') << "Envelope(" << lf << i << "Date: " << date.toString() << lf << i << "Subject: " << subject << lf; stream << i << "From: "; dumpListOfAddresses(stream, from, indent + 1); stream << i << "Sender: "; dumpListOfAddresses(stream, sender, indent + 1); stream << i << "Reply-To: "; dumpListOfAddresses(stream, replyTo, indent + 1); stream << i << "To: "; dumpListOfAddresses(stream, to, indent + 1); stream << i << "Cc: "; dumpListOfAddresses(stream, cc, indent + 1); stream << i << "Bcc: "; dumpListOfAddresses(stream, bcc, indent + 1); stream << i << "In-Reply-To: " << inReplyTo << lf << i << "Message-Id: " << messageId << lf; return stream << QByteArray(indent, ' ') << ")" << lf; } QTextStream &operator<<(QTextStream &stream, const Envelope &e) { return e.dump(stream, 0); } QTextStream &operator<<(QTextStream &stream, const AbstractMessage::bodyFldParam_t &p) { stream << "bodyFldParam[ "; bool first = true; for (AbstractMessage::bodyFldParam_t::const_iterator it = p.begin(); it != p.end(); ++it, first = false) stream << (first ? "" : ", ") << it.key() << ": " << it.value(); return stream << "]"; } QTextStream &operator<<(QTextStream &stream, const AbstractMessage::bodyFldDsp_t &p) { return stream << "bodyFldDsp( " << p.first << ", " << p.second << ")"; } QTextStream &operator<<(QTextStream &stream, const QList &list) { stream << "( "; bool first = true; for (QList::const_iterator it = list.begin(); it != list.end(); ++it, first = false) stream << (first ? "" : ", ") << *it; return stream << " )"; } bool operator==(const Envelope &a, const Envelope &b) { return a.date == b.date && a.subject == b.subject && a.from == b.from && a.sender == b.sender && a.replyTo == b.replyTo && a.to == b.to && a.cc == b.cc && a.bcc == b.bcc && a.inReplyTo == b.inReplyTo && a.messageId == b.messageId; } /** @short Extract interesting part-specific metadata from the BODYSTRUCTURE into the actual part Examples are stuff like the charset, or the suggested filename. */ void AbstractMessage::storeInterestingFields(Mailbox::TreeItemPart *p) const { p->setBodyFldParam(bodyFldParam); // Charset bodyFldParam_t::const_iterator it = bodyFldParam.find("CHARSET"); if (it != bodyFldParam.end()) { p->setCharset(*it); } // Support for format=flowed, RFC3676 it = bodyFldParam.find("FORMAT"); if (it != bodyFldParam.end()) { p->setContentFormat(*it); it = bodyFldParam.find("DELSP"); if (it != bodyFldParam.end()) { p->setContentDelSp(*it); } } // Filename and content-disposition if (!bodyFldDsp.first.isNull()) { p->setBodyDisposition(bodyFldDsp.first); p->setFileName(Imap::extractRfc2231Param(bodyFldDsp.second, "FILENAME")); } // Try to look for the obsolete "name" right in the Content-Type header (as parsed by the IMAP server) as a fallback // As per Thomas' suggestion, an empty-but-specified filename is happily overwritten here by design. if (p->fileName().isEmpty()) { p->setFileName(Imap::extractRfc2231Param(bodyFldParam, "NAME")); } } void OneMessage::storeInterestingFields(Mailbox::TreeItemPart *p) const { AbstractMessage::storeInterestingFields(p); p->setTransferEncoding(bodyFldEnc.toLower()); p->setOctets(bodyFldOctets); p->setBodyFldId(bodyFldId); } void MultiMessage::storeInterestingFields(Mailbox::TreeItemPart *p) const { AbstractMessage::storeInterestingFields(p); // The multipart/related can specify the root part to show if (mediaSubType == "related") { bodyFldParam_t::const_iterator it = bodyFldParam.find("START"); if (it != bodyFldParam.end()) { p->setMultipartRelatedStartPart(*it); } } } Mailbox::TreeItemChildrenList TextMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list; Mailbox::TreeItemPart *p = new Mailbox::TreeItemPart(parent, mediaType + "/" + mediaSubType); storeInterestingFields(p); list << p; return list; } Mailbox::TreeItemChildrenList BasicMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list; Mailbox::TreeItemPart *p = new Mailbox::TreeItemPart(parent, mediaType + "/" + mediaSubType); storeInterestingFields(p); list << p; return list; } Mailbox::TreeItemChildrenList MsgMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list; Mailbox::TreeItemPart *part = new Mailbox::TreeItemPartMultipartMessage(parent, envelope); part->setChildren(body->createTreeItems(part)); // always returns an empty list -> no need to qDeleteAll() storeInterestingFields(part); list << part; return list; } Mailbox::TreeItemChildrenList MultiMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list, list2; Mailbox::TreeItemPart *part = new Mailbox::TreeItemPart(parent, "multipart/" + mediaSubType); for (QList >::const_iterator it = bodies.begin(); it != bodies.end(); ++it) { list2 << (*it)->createTreeItems(part); } part->setChildren(list2); // always returns an empty list -> no need to qDeleteAll() storeInterestingFields(part); list << part; return list; } } } QDebug operator<<(QDebug dbg, const Imap::Message::Envelope &envelope) { using namespace Imap::Message; return dbg << "Envelope( FROM" << MailAddress::prettyList(envelope.from, MailAddress::FORMAT_READABLE) << "TO" << MailAddress::prettyList(envelope.to, MailAddress::FORMAT_READABLE) << "CC" << MailAddress::prettyList(envelope.cc, MailAddress::FORMAT_READABLE) << "BCC" << MailAddress::prettyList(envelope.bcc, MailAddress::FORMAT_READABLE) << "SUBJECT" << envelope.subject << "DATE" << envelope.date << "MESSAGEID" << envelope.messageId; } QDataStream &operator>>(QDataStream &stream, Imap::Message::Envelope &e) { return stream >> e.bcc >> e.cc >> e.date >> e.from >> e.inReplyTo >> e.messageId >> e.replyTo >> e.sender >> e.subject >> e.to; } QDataStream &operator<<(QDataStream &stream, const Imap::Message::Envelope &e) { return stream << e.bcc << e.cc << e.date << e.from << e.inReplyTo << e.messageId << e.replyTo << e.sender << e.subject << e.to; } diff --git a/src/Plugins/AbookAddressbook/AbookAddressbook.cpp b/src/Plugins/AbookAddressbook/AbookAddressbook.cpp index 48a37c3c..8f2ab67e 100644 --- a/src/Plugins/AbookAddressbook/AbookAddressbook.cpp +++ b/src/Plugins/AbookAddressbook/AbookAddressbook.cpp @@ -1,403 +1,406 @@ /* Copyright (C) 2012 Thomas Lübking Copyright (C) 2013 Caspar Schutijser Copyright (C) 2006 - 2014 Jan Kundrát Copyright (C) 2013 - 2014 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "AbookAddressbook.h" #include "be-contacts.h" #include #include +#include #include #include #include #include #include "Common/SettingsCategoryGuard.h" class AbookAddressbookCompletionJob : public AddressbookCompletionJob { public: AbookAddressbookCompletionJob(const QString &input, const QStringList &ignores, int max, AbookAddressbook *parent) : AddressbookCompletionJob(parent), m_input(input), m_ignores(ignores), m_max(max), m_parent(parent) {} public slots: virtual void doStart() { NameEmailList completion = m_parent->complete(m_input, m_ignores, m_max); emit completionAvailable(completion); finished(); } virtual void doStop() { emit error(AddressbookJob::Stopped); finished(); } private: QString m_input; QStringList m_ignores; int m_max; AbookAddressbook *m_parent; }; class AbookAddressbookNamesJob : public AddressbookNamesJob { public: AbookAddressbookNamesJob(const QString &email, AbookAddressbook *parent) : AddressbookNamesJob(parent), m_email(email), m_parent(parent) {} public slots: virtual void doStart() { QStringList displayNames = m_parent->prettyNamesForAddress(m_email); emit prettyNamesForAddressAvailable(displayNames); finished(); } virtual void doStop() { emit error(AddressbookJob::Stopped); finished(); } private: QString m_email; AbookAddressbook *m_parent; }; AbookAddressbook::AbookAddressbook(QObject *parent): AddressbookPlugin(parent), m_updateTimer(0) { #define ADD(TYPE, KEY) \ m_fields << qMakePair(TYPE, QLatin1String(KEY)) ADD(Name, "name"); ADD(Mail, "email"); ADD(Address, "address"); ADD(City, "city"); ADD(State, "state"); ADD(ZIP, "zip"); ADD(Country, "country"); ADD(Phone, "phone"); ADD(Workphone, "workphone"); ADD(Fax, "fax"); ADD(Mobile, "mobile"); ADD(Nick, "nick"); ADD(URL, "url"); ADD(Notes, "notes"); ADD(Anniversary, "anniversary"); ADD(Photo, "photo"); #undef ADD m_contacts = new QStandardItemModel(this); ensureAbookPath(); // read abook readAbook(false); m_filesystemWatcher = new QFileSystemWatcher(this); m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook")); connect (m_filesystemWatcher, &QFileSystemWatcher::fileChanged, this, &AbookAddressbook::scheduleAbookUpdate); } AbookAddressbook::~AbookAddressbook() { } AddressbookPlugin::Features AbookAddressbook::features() const { return FeatureAddressbookWindow | FeatureContactWindow | FeatureAddContact | FeatureEditContact | FeatureCompletion | FeaturePrettyNames; } AddressbookCompletionJob *AbookAddressbook::requestCompletion(const QString &input, const QStringList &ignores, int max) { return new AbookAddressbookCompletionJob(input, ignores, max, this); } AddressbookNamesJob *AbookAddressbook::requestPrettyNamesForAddress(const QString &email) { return new AbookAddressbookNamesJob(email, this); } void AbookAddressbook::openAddressbookWindow() { BE::Contacts *window = new BE::Contacts(this); window->setAttribute(Qt::WA_DeleteOnClose, true); //: Translators: BE::Contacts is the name of a stand-alone address book application. //: BE refers to Bose/Einstein (condensate). window->setWindowTitle(BE::Contacts::tr("BE::Contacts")); window->show(); } void AbookAddressbook::openContactWindow(const QString &email, const QString &displayName) { BE::Contacts *window = new BE::Contacts(this); window->setAttribute(Qt::WA_DeleteOnClose, true); window->manageContact(email, displayName); window->show(); } QStandardItemModel *AbookAddressbook::model() const { return m_contacts; } void AbookAddressbook::remonitorAdressbook() { m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook")); } void AbookAddressbook::ensureAbookPath() { if (!QDir::home().exists(QStringLiteral(".abook"))) { QDir::home().mkdir(QStringLiteral(".abook")); } QDir abook(QDir::homePath() + QLatin1String("/.abook/")); QStringList abookrc; QFile file(QDir::homePath() + QLatin1String("/.abook/abookrc")); if (file.exists() && file.open(QIODevice::ReadWrite|QIODevice::Text)) { abookrc = QString::fromLocal8Bit(file.readAll()).split(QStringLiteral("\n")); bool havePhoto = false; for (QStringList::iterator it = abookrc.begin(), end = abookrc.end(); it != end; ++it) { if (it->contains(QLatin1String("preserve_fields"))) *it = QStringLiteral("set preserve_fields=all"); else if (it->contains(QLatin1String("photo")) && it->contains(QLatin1String("field"))) havePhoto = true; } if (!havePhoto) abookrc << QStringLiteral("field photo = Photo"); } else { abookrc << QStringLiteral("field photo = Photo") << QStringLiteral("set preserve_fields=all"); file.open(QIODevice::WriteOnly|QIODevice::Text); } if (file.isOpen()) { if (file.isWritable()) { file.seek(0); file.write(abookrc.join(QStringLiteral("\n")).toLocal8Bit()); } file.close(); } QFile abookFile(abook.filePath(QStringLiteral("addressbook"))); if (!abookFile.exists()) { abookFile.open(QIODevice::WriteOnly); } } void AbookAddressbook::scheduleAbookUpdate() { // we need to schedule this because the filesystemwatcher usually fires while the file is re/written if (!m_updateTimer) { m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect(m_updateTimer, &QTimer::timeout, this, &AbookAddressbook::updateAbook); } m_updateTimer->start(500); } void AbookAddressbook::updateAbook() { readAbook(true); // QFileSystemWatcher will usually unhook from the file when it's re/written - the entire watcher ain't so great :-( m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook")); } void AbookAddressbook::readAbook(bool update) { // QElapsedTimer profile; // profile.start(); QSettings abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat); abook.setIniCodec("UTF-8"); QStringList contacts = abook.childGroups(); foreach (const QString &contact, contacts) { Common::SettingsCategoryGuard guard(&abook, contact); QStandardItem *item = 0; QStringList mails; if (update) { QList list = m_contacts->findItems(abook.value(QStringLiteral("name")).toString()); if (list.count() == 1) item = list.at(0); else if (list.count() > 1) { mails = abook.value(QStringLiteral("email"), QString()).toStringList(); const QString mailString = mails.join(QStringLiteral("\n")); foreach (QStandardItem *it, list) { if (it->data(Mail).toString() == mailString) { item = it; break; } } } if (item && item->data(Dirty).toBool()) { continue; } } bool add = !item; if (add) item = new QStandardItem; QMap unknownKeys; foreach (const QString &key, abook.allKeys()) { QList >::const_iterator field = m_fields.constBegin(); while (field != m_fields.constEnd()) { if (field->second == key) break; ++field; } if (field == m_fields.constEnd()) unknownKeys.insert(key, abook.value(key)); else if (field->first == Mail) { if (mails.isEmpty()) mails = abook.value(field->second, QString()).toStringList(); // to fix the name field item->setData( mails.join(QStringLiteral("\n")), Mail ); } else item->setData( abook.value(field->second, QString()), field->first ); } // attempt to fix the name field if (item->data(Name).toString().isEmpty()) { if (!mails.isEmpty()) item->setData( mails.at(0), Name ); } if (item->data(Name).toString().isEmpty()) { delete item; continue; // junk or format spec entry } item->setData( unknownKeys, UnknownKeys ); if (add) m_contacts->appendRow( item ); } m_contacts->sort(0); // const qint64 elapsed = profile.elapsed(); // qDebug() << "reading too" << elapsed << "ms"; } void AbookAddressbook::saveContacts() { m_filesystemWatcher->blockSignals(true); QSettings abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat); abook.setIniCodec("UTF-8"); abook.clear(); for (int i = 0; i < m_contacts->rowCount(); ++i) { Common::SettingsCategoryGuard guard(&abook, QString::number(i)); QStandardItem *item = m_contacts->item(i); for (QList >::const_iterator it = m_fields.constBegin(), end = m_fields.constEnd(); it != end; ++it) { if (it->first == Mail) abook.setValue(QStringLiteral("email"), item->data(Mail).toString().split(QStringLiteral("\n"))); else { const QVariant v = item->data(it->first); if (!v.toString().isEmpty()) abook.setValue(it->second, v); } } QMap unknownKeys = item->data( UnknownKeys ).toMap(); for (QMap::const_iterator it = unknownKeys.constBegin(), end = unknownKeys.constEnd(); it != end; ++it) { abook.setValue(it.key(), it.value()); } } abook.sync(); m_filesystemWatcher->blockSignals(false); } static inline bool ignore(const QString &string, const QStringList &ignores) { Q_FOREACH (const QString &ignore, ignores) { if (ignore.contains(string, Qt::CaseInsensitive)) return true; } return false; } NameEmailList AbookAddressbook::complete(const QString &string, const QStringList &ignores, int max) const { NameEmailList list; if (string.isEmpty()) return list; // In e-mail addresses, dot, dash, _ and @ shall be treated as delimiters - QRegExp mailMatch = QRegExp(QStringLiteral("[\\.\\-_@]%1").arg(QRegExp::escape(string)), Qt::CaseInsensitive); + QRegularExpression mailMatch(QStringLiteral("[\\.\\-_@]%1").arg(QRegularExpression::escape(string)), + QRegularExpression::CaseInsensitiveOption); // In human readable names, match on word boundaries - QRegExp nameMatch = QRegExp(QStringLiteral("\\b%1").arg(QRegExp::escape(string)), Qt::CaseInsensitive); + QRegularExpression nameMatch(QStringLiteral("\\b%1").arg(QRegularExpression::escape(string)), + QRegularExpression::CaseInsensitiveOption); // These REs are still not perfect, they won't match on e.g. ".net" or "-project", but screw these I say for (int i = 0; i < m_contacts->rowCount(); ++i) { QStandardItem *item = m_contacts->item(i); QString contactName = item->data(Name).toString(); // several mail addresses per contact are stored newline delimited QStringList contactMails(item->data(Mail).toString().split(QLatin1Char('\n'), QString::SkipEmptyParts)); if (contactName.contains(nameMatch)) { Q_FOREACH (const QString &mail, contactMails) { if (ignore(mail, ignores)) continue; list << NameEmail(contactName, mail); if (list.count() == max) return list; } continue; } Q_FOREACH (const QString &mail, contactMails) { if (mail.startsWith(string, Qt::CaseInsensitive) || // don't match on the TLD mail.section(QLatin1Char('.'), 0, -2).contains(mailMatch)) { if (ignore(mail, ignores)) continue; list << NameEmail(contactName, mail); if (list.count() == max) return list; } } } return list; } QStringList AbookAddressbook::prettyNamesForAddress(const QString &mail) const { QStringList res; for (int i = 0; i < m_contacts->rowCount(); ++i) { QStandardItem *item = m_contacts->item(i); if (QString::compare(item->data(Mail).toString(), mail, Qt::CaseInsensitive) == 0) res << item->data(Name).toString(); } return res; } QString trojita_plugin_AbookAddressbookPlugin::name() const { return QStringLiteral("abookaddressbook"); } QString trojita_plugin_AbookAddressbookPlugin::description() const { return tr("Addressbook in ~/.abook/"); } AddressbookPlugin *trojita_plugin_AbookAddressbookPlugin::create(QObject *parent, QSettings *) { return new AbookAddressbook(parent); } diff --git a/src/UiUtils/PlainTextFormatter.cpp b/src/UiUtils/PlainTextFormatter.cpp index 4e1c4aeb..dd657c3a 100644 --- a/src/UiUtils/PlainTextFormatter.cpp +++ b/src/UiUtils/PlainTextFormatter.cpp @@ -1,639 +1,616 @@ /* Copyright (C) 2012 Thomas Lübking Copyright (C) 2006 - 2014 Jan Kundrát + Copyright (C) 2018 Erik Quaeghebeur This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include +#include #include #include +#include +#include +#include #include #include "PlainTextFormatter.h" #include "Common/Paths.h" #include "Imap/Model/ItemRoles.h" #include "UiUtils/Color.h" namespace UiUtils { /** @short Helper for plainTextToHtml for applying the HTML formatting This function recognizes http and https links, e-mail addresses, *bold*, /italic/ and _underline_ text. */ QString helperHtmlifySingleLine(QString line) { // Static regexps for the engine construction. // Warning, these operate on the *escaped* HTML! -#define HTML_RE_INTRO "(^|[\\s\\(\\[\\{])" -#define HTML_RE_EXTRO "($|[\\s\\),;.\\]\\}])" - static const QRegExp patternRe(QLatin1String( - // hyperlinks - "(" // cap(1) - "https?://" // scheme prefix - "(?:[;/?:@=$\\-_.+!',0-9a-zA-Z%#~\\[\\]\\(\\)\\*]|&)+" // allowed characters - "(?:[/@=$\\-_+'0-9a-zA-Z%#~]|&)" // termination - ")" - // end of hyperlink - "|" - // e-mail pattern - "((?:[a-zA-Z0-9_\\.!#$%'\\*\\+\\-/=?^`\\{|\\}~]|&)+@[a-zA-Z0-9\\.\\-_]+)" // cap(2) - // end of e-mail pattern - "|" - // formatting markup - "(" // cap(3) - // bold text - HTML_RE_INTRO /* cap(4) */ "\\*((?!\\*)\\S+)\\*" /* cap(5) */ HTML_RE_EXTRO /* cap(6) */ - "|" - // italics - HTML_RE_INTRO /* cap(7) */ "/((?!/)\\S+)/" /* cap(8) */ HTML_RE_EXTRO /* cap(9) */ - "|" - // underline - HTML_RE_INTRO /* cap(10) */ "_((?!_)\\S+)_" /* cap(11) */ HTML_RE_EXTRO /* cap(12) */ - ")" - // end of the formatting markup - ), Qt::CaseSensitive, QRegExp::RegExp2 - ); - - // RE instances to work on - QRegExp pattern(patternRe); + static const QRegularExpression patternRe(QLatin1String( + "(" // 1: hyperlink + "https?://" // scheme prefix + "(?:[][;/?:@=$_.+!',0-9a-zA-Z%#~()*-]|&)+" // allowed characters + "(?:[/@=$_+'0-9a-zA-Z%#~-]|&)" // termination + ")" // end of hyperlink + "|" + "(" // 2: e-mail + "(?:[a-zA-Z0-9_.!#$%'*+/=?^`{|}~-]|&)+" + "@" + "[a-zA-Z0-9._-]+" + ")" // end of e-mail + "|" + "(?<=^|[[({\\s])" // markup group surroundings + "(" // 3: markup group + "([*/_])(?!\\4)" // 4: markup character, not repeated + "(\\S+?)" // 5: marked-up text + "\\4(?!\\4)" // markup character, not repeated + ")" // end of markup group + "(?=$|[])}\\s,;.])" // markup group surroundings + ), QRegularExpression::CaseInsensitiveOption); // Escape the HTML entities line = line.toHtmlEscaped(); + static const QMap markupletter({{QStringLiteral("*"), QLatin1Char('b')}, + {QStringLiteral("/"), QLatin1Char('i')}, + {QStringLiteral("_"), QLatin1Char('u')}}); + + const uint orig_length = line.length(); + // Now prepare markup *bold*, /italic/ and _underline_ and also turn links into HTML. - // This is a bit more involved because we want to apply the regular expressions in a certain order and also at the same - // time prevent the lower-priority regexps from clobbering the output of the previous stages. - int start = 0; - while (start < line.size()) { - // Find the position of the first thing which matches - int pos = pattern.indexIn(line, start, QRegExp::CaretAtOffset); - if (pos == -1 || pos == line.size()) { - // No further matches for this line -> we're done + // This is a bit more involved because we want to apply the regular expressions in a certain order and + // also at the same time prevent the lower-priority regexps from clobbering the output of the previous stages. + QRegularExpressionMatchIterator i = patternRe.globalMatch(line); + QRegularExpressionMatch match; + uint growth = 0; + while (i.hasNext()) { + match = i.next(); + switch (match.lastCapturedIndex()) { // at most one match 1 xor 2 xor 5 (5 implies 4 and 3) + case 1: + line.replace(match.capturedStart(1) + growth, match.capturedLength(1), + QStringLiteral("%1").arg(match.captured(1))); + break; + case 2: + line.replace(match.capturedStart(2) + growth, match.capturedLength(2), + QStringLiteral("%1").arg(match.captured(2))); + break; + case 5: // Careful here; the inner contents of the current match shall be formatted as well which is why we need recursion + line.replace(match.capturedStart(3) + growth, match.capturedLength(3), + QStringLiteral("<%1>%2%3%2") + .arg(markupletter[match.captured(4)], + QStringLiteral("%1").arg(match.captured(4)), + helperHtmlifySingleLine(match.captured(5)))); break; } - - const QString &linkText = pattern.cap(1); - const QString &mailText = pattern.cap(2); - const QString &boldText = pattern.cap(5); - const QString &italicText = pattern.cap(8); - const QString &underlineText = pattern.cap(11); - bool isSpecialFormat = !boldText.isEmpty() || !italicText.isEmpty() || !underlineText.isEmpty(); - QString replacement; - - if (!linkText.isEmpty()) { - replacement = QStringLiteral("%1").arg(linkText); - } else if (!mailText.isEmpty()) { - replacement = QStringLiteral("%1").arg(mailText); - } else if (isSpecialFormat) { - // Careful here; the inner contents of the current match shall be formatted as well which is why we need recursion - QChar elementName; - QChar markupChar; - int whichOne = 0; - - if (!boldText.isEmpty()) { - elementName = QLatin1Char('b'); - markupChar = QLatin1Char('*'); - whichOne = 3; - } else if (!italicText.isEmpty()) { - elementName = QLatin1Char('i'); - markupChar = QLatin1Char('/'); - whichOne = 6; - } else if (!underlineText.isEmpty()) { - elementName = QLatin1Char('u'); - markupChar = QLatin1Char('_'); - whichOne = 9; - } - Q_ASSERT(whichOne); - replacement = QStringLiteral("%1<%2>%3%4%3%5") - .arg(pattern.cap(whichOne + 1), elementName, markupChar, - helperHtmlifySingleLine(pattern.cap(whichOne + 2)), pattern.cap(whichOne + 3)); - } - Q_ASSERT(!replacement.isEmpty()); - line = line.left(pos) + replacement + line.mid(pos + pattern.matchedLength()); - start = pos + replacement.size(); + growth = line.length() - orig_length; } + return line; } /** @short Return a preview of the quoted text The goal is to produce "roughly N" lines of non-empty text. Blanks are ignored and lines longer than charsPerLine are broken into multiple chunks. */ QString firstNLines(const QString &input, int numLines, const int charsPerLine) { Q_ASSERT(numLines >= 2); QString out = input.section(QLatin1Char('\n'), 0, numLines - 1, QString::SectionSkipEmpty); const int cutoff = numLines * charsPerLine; if (out.size() >= cutoff) { int pos = input.indexOf(QLatin1Char(' '), cutoff); if (pos != -1) return out.left(pos - 1); } return out; } /** @short Helper for closing blockquotes and adding the interactive control elements at the right places */ void closeQuotesUpTo(QStringList &markup, QStack > &controlStack, int "eLevel, const int finalQuoteLevel) { static QString closingLabel(QStringLiteral("")); static QLatin1String closeSingleQuote(""); static QLatin1String closeQuoteBlock(""); Q_ASSERT(quoteLevel >= finalQuoteLevel); while (quoteLevel > finalQuoteLevel) { // Check whether an interactive control element is supposed to be present here bool controlBlock = !controlStack.isEmpty() && (quoteLevel == controlStack.top().first); if (controlBlock) { markup << closingLabel.arg(controlStack.pop().second); } markup << closeSingleQuote; --quoteLevel; if (controlBlock) { markup << closeQuoteBlock; } } } -/** @short Returna a regular expression which matches the signature separators */ -QRegExp signatureSeparator() +/** @short Return a a regular expression which matches the signature separators */ +QRegularExpression signatureSeparator() { // "-- " is the standards-compliant signature separator. // "Line of underscores" is non-standard garbage which Mailman happily generates. Yes, it's nasty and ugly. - return QRegExp(QLatin1String("(-- |_{45,55})(\\r)?")); + return QRegularExpression(QLatin1String("^(-- |_{45,55})(\\r)?$")); } struct TextInfo { int depth; QString text; TextInfo(const int depth, const QString &text): depth(depth), text(text) { } }; static QString lineWithoutTrailingCr(const QString &line) { return line.endsWith(QLatin1Char('\r')) ? line.left(line.size() - 1) : line; } QString plainTextToHtml(const QString &plaintext, const FlowedFormat flowed) { - QRegExp quotemarks; + QRegularExpression quotemarks; switch (flowed) { case FlowedFormat::FLOWED: case FlowedFormat::FLOWED_DELSP: - quotemarks = QRegExp(QLatin1String("^>+")); + quotemarks = QRegularExpression(QLatin1String("^>+")); break; case FlowedFormat::PLAIN: // Also accept > interleaved by spaces. That's what KMail happily produces. // A single leading space is accepted, too. That's what Gerrit produces. - quotemarks = QRegExp(QLatin1String("^( >|>)+")); + quotemarks = QRegularExpression(QLatin1String("^( >|>)+")); break; } const int SIGNATURE_SEPARATOR = -2; auto lines = plaintext.split(QLatin1Char('\n')); std::vector lineBuffer; lineBuffer.reserve(lines.size()); // First pass: determine the quote level for each source line. // The quote level is ignored for the signature. bool signatureSeparatorSeen = false; Q_FOREACH(const QString &line, lines) { // Fast path for empty lines if (line.isEmpty()) { lineBuffer.emplace_back(0, line); continue; } // Special marker for the signature separator - if (signatureSeparator().exactMatch(line)) { + if (signatureSeparator().match(line).hasMatch()) { lineBuffer.emplace_back(SIGNATURE_SEPARATOR, lineWithoutTrailingCr(line)); signatureSeparatorSeen = true; continue; } // Determine the quoting level int quoteLevel = 0; - if (!signatureSeparatorSeen && quotemarks.indexIn(line) == 0) { - quoteLevel = quotemarks.cap(0).count(QLatin1Char('>')); + QRegularExpressionMatch match = quotemarks.match(line); + if (!signatureSeparatorSeen && match.capturedStart() == 0) { + quoteLevel = match.captured().count(QLatin1Char('>')); } lineBuffer.emplace_back(quoteLevel, lineWithoutTrailingCr(line)); } // Second pass: // - Remove the quotemarks for everything prior to the signature separator. // - Collapse the lines with the same quoting level into a single block // (optionally into a single line if format=flowed is active) auto it = lineBuffer.begin(); while (it < lineBuffer.end() && it->depth != SIGNATURE_SEPARATOR) { // Remove the quotemarks it->text.remove(quotemarks); switch (flowed) { case FlowedFormat::FLOWED: case FlowedFormat::FLOWED_DELSP: if (flowed == FlowedFormat::FLOWED || flowed == FlowedFormat::FLOWED_DELSP) { // check for space-stuffing if (it->text.startsWith(QLatin1Char(' '))) { it->text.remove(0, 1); } // quirk: fix a flowed line which actually isn't flowed if (it->text.endsWith(QLatin1Char(' ')) && ( it+1 == lineBuffer.end() || // end-of-document (it+1)->depth == SIGNATURE_SEPARATOR || // right in front of the separator (it+1)->depth != it->depth // end of paragraph )) { it->text.chop(1); } } break; case FlowedFormat::PLAIN: if (it->depth > 0 && it->text.startsWith(QLatin1Char(' '))) { // Because the space is re-added when we prepend the quotes. Adding that space is done // in order to make it look nice, i.e. to prevent lines like ">>something". it->text.remove(0, 1); } break; } if (it == lineBuffer.begin()) { // No "previous line" ++it; continue; } // Check for the line joining auto prev = it - 1; if (prev->depth == it->depth) { QString separator = QStringLiteral("\n"); switch (flowed) { case FlowedFormat::PLAIN: // nothing fancy to do here, we cannot really join lines break; case FlowedFormat::FLOWED: case FlowedFormat::FLOWED_DELSP: // CR LF trailing is stripped already (LFs by the split into lines, CRs by lineWithoutTrailingCr in pass #1), // so we only have to check for the trailing space if (prev->text.endsWith(QLatin1Char(' '))) { // implement the DelSp thingy if (flowed == FlowedFormat::FLOWED_DELSP) { prev->text.chop(1); } if (it->text.isEmpty() || prev->text.isEmpty()) { // This one or the previous line is a blank one, so we cannot really join them } else { separator = QString(); } } break; } prev->text += separator + it->text; it = lineBuffer.erase(it); } else { ++it; } } // Third pass: HTML escaping, formatting and adding fancy markup signatureSeparatorSeen = false; int quoteLevel = 0; QStringList markup; int interactiveControlsId = 0; QStack > controlStack; for (it = lineBuffer.begin(); it != lineBuffer.end(); ++it) { if (it->depth == SIGNATURE_SEPARATOR && !signatureSeparatorSeen) { // The first signature separator signatureSeparatorSeen = true; closeQuotesUpTo(markup, controlStack, quoteLevel, 0); markup << QLatin1String("") + helperHtmlifySingleLine(it->text); markup << QStringLiteral("\n"); continue; } if (signatureSeparatorSeen) { // Just copy the data markup << helperHtmlifySingleLine(it->text); if (it+1 != lineBuffer.end()) markup << QStringLiteral("\n"); continue; } Q_ASSERT(quoteLevel == 0 || quoteLevel != it->depth); if (quoteLevel > it->depth) { // going back in the quote hierarchy closeQuotesUpTo(markup, controlStack, quoteLevel, it->depth); } // Pretty-formatted block of the ">>>" characters QString quotemarks; if (it->depth) { quotemarks += QLatin1String(""); for (int i = 0; i < it->depth; ++i) { quotemarks += QLatin1String(">"); } quotemarks += QLatin1String(" "); } static const int previewLines = 5; static const int charsPerLineEquivalent = 160; static const int forceCollapseAfterLines = 10; if (quoteLevel < it->depth) { // We're going deeper in the quote hierarchy QString line; while (quoteLevel < it->depth) { ++quoteLevel; // Check whether there is anything at the newly entered level of nesting bool anythingOnJustThisLevel = false; // A short summary of the quotation QString preview; auto runner = it; while (runner != lineBuffer.end()) { if (runner->depth == quoteLevel) { anythingOnJustThisLevel = true; ++interactiveControlsId; controlStack.push(qMakePair(quoteLevel, interactiveControlsId)); QString omittedStuff; QString previewPrefix, previewSuffix; QString currentChunk = firstNLines(runner->text, previewLines, charsPerLineEquivalent); QString omittedPrefix, omittedSuffix; QString previewQuotemarks; if (runner != it ) { // we have skipped something, make it obvious to the user // Find the closest level which got collapsed int closestDepth = std::numeric_limits::max(); auto depthRunner(it); while (depthRunner != runner) { closestDepth = std::min(closestDepth, depthRunner->depth); ++depthRunner; } // The [...] marks shall be prefixed by the closestDepth quote markers omittedStuff = QStringLiteral(""); for (int i = 0; i < closestDepth; ++i) { omittedStuff += QLatin1String(">"); } for (int i = runner->depth; i < closestDepth; ++i) { omittedPrefix += QLatin1String("
    "); omittedSuffix += QLatin1String("
    "); } omittedStuff += QStringLiteral("
    ").arg(interactiveControlsId); // Now produce the proper quotation for the preview itself for (int i = quoteLevel; i < runner->depth; ++i) { previewPrefix.append(QLatin1String("
    ")); previewSuffix.append(QLatin1String("
    ")); } } previewQuotemarks = QStringLiteral(""); for (int i = 0; i < runner->depth; ++i) { previewQuotemarks += QLatin1String(">"); } previewQuotemarks += QLatin1String(" "); preview = previewPrefix + omittedPrefix + omittedStuff + omittedSuffix + previewQuotemarks + helperHtmlifySingleLine(currentChunk) .replace(QLatin1String("\n"), QLatin1String("\n") + previewQuotemarks) + previewSuffix; break; } if (runner->depth < quoteLevel) { // This means that we have left the current level of nesting, so there cannot possible be anything else // at the current level of nesting *and* in the current quote block break; } ++runner; } // Is there nothing but quotes until the end of mail or until the signature separator? bool nothingButQuotesAndSpaceTillSignature = true; runner = it; while (++runner != lineBuffer.end()) { if (runner->depth == SIGNATURE_SEPARATOR) break; if (runner->depth > 0) continue; if (runner->depth == 0 && !runner->text.isEmpty()) { nothingButQuotesAndSpaceTillSignature = false; break; } } // Size of the current level, including the nested stuff int currentLevelCharCount = 0; int currentLevelLineCount = 0; runner = it; while (runner != lineBuffer.end() && runner->depth >= quoteLevel) { currentLevelCharCount += runner->text.size(); // one for the actual block currentLevelLineCount += runner->text.count(QLatin1Char('\n')) + 1; ++runner; } if (!anythingOnJustThisLevel) { // no need for fancy UI controls line += QLatin1String("
    "); continue; } if (quoteLevel == it->depth && currentLevelCharCount <= charsPerLineEquivalent * previewLines && currentLevelLineCount <= previewLines) { // special case: the quote is very short, no point in making it collapsible line += QStringLiteral("").arg(interactiveControlsId) + QLatin1String("
    ") + quotemarks + helperHtmlifySingleLine(it->text).replace(QLatin1String("\n"), QLatin1String("\n") + quotemarks); } else { bool collapsed = nothingButQuotesAndSpaceTillSignature || quoteLevel > 1 || currentLevelCharCount >= charsPerLineEquivalent * forceCollapseAfterLines || currentLevelLineCount >= forceCollapseAfterLines; line += QStringLiteral("") .arg(QString::number(interactiveControlsId), collapsed ? QStringLiteral("checked=\"checked\"") : QString()) + QLatin1String("
    ") + preview + QStringLiteral(" ").arg(interactiveControlsId) + QLatin1String("
    ") + QLatin1String("
    "); if (quoteLevel == it->depth) { // We're now finally on the correct level of nesting so we can output the current line line += quotemarks + helperHtmlifySingleLine(it->text) .replace(QLatin1String("\n"), QLatin1String("\n") + quotemarks); } } } markup << line; } else { // Either no quotation or we're continuing an old quote block and there was a nested quotation before markup << quotemarks + helperHtmlifySingleLine(it->text) .replace(QLatin1String("\n"), QLatin1String("\n") + quotemarks); } auto next = it + 1; if (next != lineBuffer.end()) { if (next->depth >= 0 && next->depth < it->depth) { // Decreasing the quotation level -> no starting
    markup << QStringLiteral("\n"); } else if (it->depth == 0) { // Non-quoted block which is not enclosed in a
    markup << QStringLiteral("\n"); } } } if (signatureSeparatorSeen) { // Terminate the signature markup << QStringLiteral(""); } if (quoteLevel) { // Terminate the quotes closeQuotesUpTo(markup, controlStack, quoteLevel, 0); } Q_ASSERT(controlStack.isEmpty()); return markup.join(QString()); } QString htmlizedTextPart(const QModelIndex &partIndex, const QFontInfo &font, const QColor &backgroundColor, const QColor &textColor, const QColor &linkColor, const QColor &visitedLinkColor) { static const QString defaultStyle = QString::fromUtf8( "pre{word-wrap: break-word; white-space: pre-wrap;}" // The following line, sadly, produces a warning "QFont::setPixelSize: Pixel size <= 0 (0)". // However, if it is not in place or if the font size is set higher, even to 0.1px, WebKit reserves space for the // quotation characters and therefore a weird white area appears. Even width: 0px doesn't help, so it looks like // we will have to live with this warning for the time being. ".quotemarks{color:transparent;font-size:0px;}" // Cannot really use the :dir(rtl) selector for putting the quote indicator to the "correct" side. // It's CSS4 and it isn't supported yet. "blockquote{font-size:90%; margin: 4pt 0 4pt 0; padding: 0 0 0 1em; border-left: 2px solid %1; unicode-bidi: -webkit-plaintext}" // Stop the font size from getting smaller after reaching two levels of quotes // (ie. starting on the third level, don't make the size any smaller than what it already is) "blockquote blockquote blockquote {font-size: 100%}" ".signature{opacity: 0.6;}" // Dynamic quote collapsing via pure CSS, yay "input {display: none}" "input ~ span.full {display: block}" "input ~ span.short {display: none}" "input:checked ~ span.full {display: none}" "input:checked ~ span.short {display: block}" "label {border: 1px solid %2; border-radius: 5px; padding: 0px 4px 0px 4px; white-space: nowrap}" // BLACK UP-POINTING SMALL TRIANGLE (U+25B4) // BLACK DOWN-POINTING SMALL TRIANGLE (U+25BE) "span.full > blockquote > label:before {content: \"\u25b4\"}" "span.short > blockquote > label:after {content: \" \u25be\"}" "span.shortquote > blockquote > label {display: none}" ); QString fontSpecification(QStringLiteral("pre{")); if (font.italic()) fontSpecification += QLatin1String("font-style: italic; "); if (font.bold()) fontSpecification += QLatin1String("font-weight: bold; "); fontSpecification += QStringLiteral("font-size: %1px; font-family: \"%2\", monospace }").arg( QString::number(font.pixelSize()), font.family()); QString textColors = QString::fromUtf8("body { background-color: %1; color: %2 }" "a:link { color: %3 } a:visited { color: %4 } a:hover { color: %3 }").arg( backgroundColor.name(), textColor.name(), linkColor.name(), visitedLinkColor.name()); // looks like there's no special color for hovered links in Qt // build stylesheet and html header QColor tintForQuoteIndicator = backgroundColor; tintForQuoteIndicator.setAlpha(0x66); static QString stylesheet = defaultStyle.arg(linkColor.name(), tintColor(textColor, tintForQuoteIndicator).name()); static QFile file(Common::writablePath(Common::LOCATION_DATA) + QLatin1String("message.css")); static QDateTime lastVersion; QDateTime lastTouched(file.exists() ? QFileInfo(file).lastModified() : QDateTime()); if (lastVersion < lastTouched) { stylesheet = defaultStyle; if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { const QString userSheet = QString::fromLocal8Bit(file.readAll().data()); lastVersion = lastTouched; stylesheet += QLatin1Char('\n') + userSheet; file.close(); } } // The dir="auto" is required for WebKit to treat all paragraphs as entities with possibly different text direction. // The individual paragraphs unfortunately share the same text alignment, though, as per // https://bugs.webkit.org/show_bug.cgi?id=71194 (fixed in Blink already). QString htmlHeader(QLatin1String("
    "));
         static QString htmlFooter(QStringLiteral("\n
    ")); // We cannot rely on the QWebFrame's toPlainText because of https://bugs.kde.org/show_bug.cgi?id=321160 QString markup = plainTextToHtml(partIndex.data(Imap::Mailbox::RolePartUnicodeText).toString(), flowedFormatForPart(partIndex)); return htmlHeader + markup + htmlFooter; } FlowedFormat flowedFormatForPart(const QModelIndex &partIndex) { FlowedFormat flowedFormat = FlowedFormat::PLAIN; if (partIndex.data(Imap::Mailbox::RolePartContentFormat).toString().toLower() == QLatin1String("flowed")) { flowedFormat = FlowedFormat::FLOWED; if (partIndex.data(Imap::Mailbox::RolePartContentDelSp).toString().toLower() == QLatin1String("yes")) flowedFormat = FlowedFormat::FLOWED_DELSP; } return flowedFormat; } } diff --git a/src/UiUtils/PlainTextFormatter.h b/src/UiUtils/PlainTextFormatter.h index 24ee7cdd..95949d14 100644 --- a/src/UiUtils/PlainTextFormatter.h +++ b/src/UiUtils/PlainTextFormatter.h @@ -1,53 +1,54 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef UIUTILS_PLAINTEXTFORMATTER_H #define UIUTILS_PLAINTEXTFORMATTER_H #include +#include class QColor; class QFontInfo; class QModelIndex; namespace UiUtils { /** @short Enable decoding of format=flowed, RFC 3676 */ enum class FlowedFormat { PLAIN, /**< @short No value, use default */ FLOWED, /**< @short format=flowed, but DelSp is not active */ FLOWED_DELSP, /**< @short format=flowed; delsp=yes (see RFC 3676 for details */ }; QString plainTextToHtml(const QString &plaintext, const FlowedFormat flowed); QString htmlizedTextPart(const QModelIndex &partIndex, const QFontInfo &font, const QColor &backgroundColor, const QColor &textColor, const QColor &linkColor, const QColor &visitedLinkColor); FlowedFormat flowedFormatForPart(const QModelIndex &partIndex); -QRegExp signatureSeparator(); +QRegularExpression signatureSeparator(); } #endif // UIUTILS_PLAINTEXTFORMATTER_H diff --git a/src/qwwsmtpclient/qwwsmtpclient.cpp b/src/qwwsmtpclient/qwwsmtpclient.cpp index 95f93a93..1c34fb86 100644 --- a/src/qwwsmtpclient/qwwsmtpclient.cpp +++ b/src/qwwsmtpclient/qwwsmtpclient.cpp @@ -1,729 +1,731 @@ // // C++ Implementation: qwwsmtpclient // // Description: // // // Author: Witold Wysota , (C) 2009 // // Copyright: See COPYING file that comes with this distribution // // #include "qwwsmtpclient.h" #include #include +#include #include #include #include /* CONNECTION ESTABLISHMENT S: 220 E: 554 EHLO or HELO S: 250 E: 504, 550 MAIL S: 250 E: 552, 451, 452, 550, 553, 503 RCPT S: 250, 251 (but see section 3.4 for discussion of 251 and 551) E: 550, 551, 552, 553, 450, 451, 452, 503, 550 DATA I: 354 -> data -> S: 250 E: 552, 554, 451, 452 E: 451, 554, 503 RSET S: 250 VRFY S: 250, 251, 252 E: 550, 551, 553, 502, 504 EXPN S: 250, 252 E: 550, 500, 502, 504 HELP S: 211, 214 E: 502, 504 NOOP S: 250 QUIT S: 221 */ struct SMTPCommand { enum Type { Connect, Disconnect, StartTLS, Authenticate, Mail, MailBurl, RawCommand }; int id; Type type; QVariant data; QVariant extra; }; class QwwSmtpClientPrivate { public: QwwSmtpClientPrivate(QwwSmtpClient *qq) { q = qq; } QSslSocket *socket; QwwSmtpClient::State state; void setState(QwwSmtpClient::State s); void parseOption(const QString &buffer); void onConnected(); void onDisconnected(); void onError(QAbstractSocket::SocketError); void _q_readFromSocket(); void _q_encrypted(); void processNextCommand(bool ok = true); void abortDialog(); void sendAuthPlain(const QString &username, const QString &password); void sendAuthLogin(const QString &username, const QString &password, int stage); void sendEhlo(); void sendHelo(); void sendQuit(); void sendRcpt(); int lastId; bool inProgress; QString localName; QString localNameEncrypted; QString errorString; // server caps: QwwSmtpClient::Options options; QwwSmtpClient::AuthModes authModes; QQueue commandqueue; private: QwwSmtpClient *q; QwwSmtpClientPrivate(const QwwSmtpClientPrivate&); // don't implement QwwSmtpClientPrivate& operator=(const QwwSmtpClientPrivate&); // don't implement }; // private slot triggered upon connection to the server // - clears options // - notifies the environment void QwwSmtpClientPrivate::onConnected() { options = QwwSmtpClient::NoOptions; authModes = QwwSmtpClient::AuthNone; emit q->stateChanged(QwwSmtpClient::Connected); emit q->connected(); } // private slot triggered upon disconnection from the server // - checks the cause of disconnection // - aborts or continues processing void QwwSmtpClientPrivate::onDisconnected() { setState(QwwSmtpClient::Disconnected); if (commandqueue.isEmpty()) { inProgress = false; emit q->done(true); return; } if (commandqueue.head().type == SMTPCommand::Disconnect) { inProgress = false; emit q->done(true); return; } emit q->commandFinished(commandqueue.head().id, true); commandqueue.clear(); inProgress = false; emit q->done(false); } void QwwSmtpClientPrivate::onError(QAbstractSocket::SocketError e) { emit q->socketError(e, socket->errorString()); onDisconnected(); } // main logic of the component - a slot triggered upon data entering the socket // comments inline... void QwwSmtpClientPrivate::_q_readFromSocket() { while (socket->canReadLine()) { QString line = socket->readLine(); emit q->logReceived(line.toUtf8()); - QRegExp rx("(\\d+)-(.*)\n"); // multiline response (aka 250-XYZ) - QRegExp rxlast("(\\d+) (.*)\n"); // single or last line response (aka 250 XYZ) - bool mid = rx.exactMatch(line); - bool last = rxlast.exactMatch(line); + QRegularExpression rx("(*ANYCRLF)^(\\d+)-(.*)$", QRegularExpression::MultilineOption); // multiline response (aka 250-XYZ) + QRegularExpression rxlast("(*ANYCRLF)^(\\d+) (.*)$", QRegularExpression::MultilineOption); // single or last line response (aka 250 XYZ) // multiline - if (mid){ - int status = rx.cap(1).toInt(); + QRegularExpressionMatch mid_match = rx.match(line); + if (mid_match.hasMatch()) { + int status = mid_match.captured(1).toInt(); SMTPCommand &cmd = commandqueue.head(); switch (cmd.type) { // trying to connect case SMTPCommand::Connect: { int stage = cmd.extra.toInt(); // stage 0 completed with success - socket is connected and EHLO was sent if(stage==1 && status==250){ - QString arg = rx.cap(2).trimmed(); + QString arg = mid_match.captured(2).trimmed(); parseOption(arg); // we're probably receiving options } } break; // trying to establish deferred SSL handshake case SMTPCommand::StartTLS: { int stage = cmd.extra.toInt(); // stage 0 (negotiation) completed ok if(stage==1 && status==250){ - QString arg = rx.cap(2).trimmed(); + QString arg = mid_match.captured(2).trimmed(); parseOption(arg); // we're probably receiving options } } default: break; } - } else - // single line - if (last) { - int status = rxlast.cap(1).toInt(); - SMTPCommand &cmd = commandqueue.head(); - switch (cmd.type) { - // trying to connect - case SMTPCommand::Connect: { - int stage = cmd.extra.toInt(); - // connection established, server sent its banner - if (stage==0 && status==220) { - sendEhlo(); // connect ok, send ehlo - } - // server responded to EHLO - if (stage==1 && status==250){ - // success (EHLO) - parseOption(rxlast.cap(2).trimmed()); // we're probably receiving the last option - errorString.clear(); - setState(QwwSmtpClient::Connected); - processNextCommand(); - } - // server responded to HELO (EHLO failed) - if (stage==2 && status==250) { - // success (HELO) - errorString.clear(); - setState(QwwSmtpClient::Connected); - processNextCommand(); - } - // EHLO failed, reason given in errorString - if (stage==1 && (status==554 || status==501 || status==502 || status==421)) { - errorString = rxlast.cap(2).trimmed(); - sendHelo(); // ehlo failed, send helo - cmd.extra = 2; - } - //abortDialog(); - } - break; - // trying to establish a delayed SSL handshake - case SMTPCommand::StartTLS: { - int stage = cmd.extra.toInt(); - // received an invitation from the server to enter TLS mode - if (stage==0 && status==220) { - emit q->logSent("*** startClientEncryption"); - socket->startClientEncryption(); - } - // TLS established, connection is encrypted, EHLO was sent - else if (stage==1 && status==250) { - setState(QwwSmtpClient::Connected); - parseOption(rxlast.cap(2).trimmed()); // we're probably receiving options - errorString.clear(); - emit q->tlsStarted(); - processNextCommand(); - } - // starttls failed - else { - emit q->logReceived(QByteArrayLiteral("*** TLS failed at stage ") + QByteArray::number(stage) + ": " + line.toUtf8()); - errorString = "TLS failed"; - emit q->done(false); + } else { + // single line + QRegularExpressionMatch last_match = rxlast.match(line); + if (last_match.hasMatch()) { + int status = last_match.captured(1).toInt(); + SMTPCommand &cmd = commandqueue.head(); + switch (cmd.type) { + // trying to connect + case SMTPCommand::Connect: { + int stage = cmd.extra.toInt(); + // connection established, server sent its banner + if (stage==0 && status==220) { + sendEhlo(); // connect ok, send ehlo + } + // server responded to EHLO + if (stage==1 && status==250){ + // success (EHLO) + parseOption(last_match.captured(2).trimmed()); // we're probably receiving the last option + errorString.clear(); + setState(QwwSmtpClient::Connected); + processNextCommand(); + } + // server responded to HELO (EHLO failed) + if (stage==2 && status==250) { + // success (HELO) + errorString.clear(); + setState(QwwSmtpClient::Connected); + processNextCommand(); + } + // EHLO failed, reason given in errorString + if (stage==1 && (status==554 || status==501 || status==502 || status==421)) { + errorString = last_match.captured(2).trimmed(); + sendHelo(); // ehlo failed, send helo + cmd.extra = 2; + } + //abortDialog(); } - } - break; - // trying to authenticate the client to the server - case SMTPCommand::Authenticate: { - int stage = cmd.extra.toInt(); - if (stage==0 && status==334) { - // AUTH mode was accepted by the server, 1st challenge sent - QwwSmtpClient::AuthMode authmode = (QwwSmtpClient::AuthMode)cmd.data.toList().at(0).toInt(); - errorString.clear(); - switch (authmode) { - case QwwSmtpClient::AuthPlain: - sendAuthPlain(cmd.data.toList().at(1).toString(), cmd.data.toList().at(2).toString()); - break; - case QwwSmtpClient::AuthLogin: - sendAuthLogin(cmd.data.toList().at(1).toString(), cmd.data.toList().at(2).toString(), 1); - break; - default: - qWarning("I shouldn't be here"); + break; + // trying to establish a delayed SSL handshake + case SMTPCommand::StartTLS: { + int stage = cmd.extra.toInt(); + // received an invitation from the server to enter TLS mode + if (stage==0 && status==220) { + emit q->logSent("*** startClientEncryption"); + socket->startClientEncryption(); + } + // TLS established, connection is encrypted, EHLO was sent + else if (stage==1 && status==250) { setState(QwwSmtpClient::Connected); + parseOption(last_match.captured(2).trimmed()); // we're probably receiving options + errorString.clear(); + emit q->tlsStarted(); processNextCommand(); - break; } - cmd.extra = stage+1; - } else if (stage==1 && status==334) { - // AUTH mode and user names were acccepted by the server, 2nd challenge sent - QwwSmtpClient::AuthMode authmode = (QwwSmtpClient::AuthMode)cmd.data.toList().at(0).toInt(); - errorString.clear(); - switch (authmode) { - case QwwSmtpClient::AuthPlain: + // starttls failed + else { + emit q->logReceived(QByteArrayLiteral("*** TLS failed at stage ") + QByteArray::number(stage) + ": " + line.toUtf8()); + errorString = "TLS failed"; + emit q->done(false); + } + } + break; + // trying to authenticate the client to the server + case SMTPCommand::Authenticate: { + int stage = cmd.extra.toInt(); + if (stage==0 && status==334) { + // AUTH mode was accepted by the server, 1st challenge sent + QwwSmtpClient::AuthMode authmode = (QwwSmtpClient::AuthMode)cmd.data.toList().at(0).toInt(); + errorString.clear(); + switch (authmode) { + case QwwSmtpClient::AuthPlain: + sendAuthPlain(cmd.data.toList().at(1).toString(), cmd.data.toList().at(2).toString()); + break; + case QwwSmtpClient::AuthLogin: + sendAuthLogin(cmd.data.toList().at(1).toString(), cmd.data.toList().at(2).toString(), 1); + break; + default: + qWarning("I shouldn't be here"); + setState(QwwSmtpClient::Connected); + processNextCommand(); + break; + } + cmd.extra = stage+1; + } else if (stage==1 && status==334) { + // AUTH mode and user names were acccepted by the server, 2nd challenge sent + QwwSmtpClient::AuthMode authmode = (QwwSmtpClient::AuthMode)cmd.data.toList().at(0).toInt(); + errorString.clear(); + switch (authmode) { + case QwwSmtpClient::AuthPlain: + // auth failed + setState(QwwSmtpClient::Connected); + processNextCommand(); + break; + case QwwSmtpClient::AuthLogin: + sendAuthLogin(cmd.data.toList().at(1).toString(), cmd.data.toList().at(2).toString(), 2); + break; + default: + qWarning("I shouldn't be here"); + setState(QwwSmtpClient::Connected); + processNextCommand(); + break; + } + } else if (stage==2 && status==334) { // auth failed + errorString = last_match.captured(2).trimmed(); setState(QwwSmtpClient::Connected); processNextCommand(); - break; - case QwwSmtpClient::AuthLogin: - sendAuthLogin(cmd.data.toList().at(1).toString(), cmd.data.toList().at(2).toString(), 2); - break; - default: - qWarning("I shouldn't be here"); + } else if (status==235) { + // auth ok + errorString.clear(); + emit q->authenticated(); setState(QwwSmtpClient::Connected); processNextCommand(); - break; + } else { + errorString = last_match.captured(2).trimmed(); + setState(QwwSmtpClient::Connected); + emit q->done(false); } - } else if (stage==2 && status==334) { - // auth failed - errorString = rxlast.cap(2).trimmed(); - setState(QwwSmtpClient::Connected); - processNextCommand(); - } else if (status==235) { - // auth ok - errorString.clear(); - emit q->authenticated(); - setState(QwwSmtpClient::Connected); - processNextCommand(); - } else { - errorString = rxlast.cap(2).trimmed(); - setState(QwwSmtpClient::Connected); - emit q->done(false); - } - } - break; - // trying to send mail - case SMTPCommand::Mail: - case SMTPCommand::MailBurl: - { - int stage = cmd.extra.toInt(); - // temporary failure upon receiving the sender address (greylisting probably) - if (status==421 && stage==0) { - errorString = rxlast.cap(2).trimmed(); - // temporary envelope failure (greylisting) - setState(QwwSmtpClient::Connected); - processNextCommand(false); } - if (status==250 && stage==0) { - // sender accepted - errorString.clear(); - sendRcpt(); - } else if (status==250 && stage==1) { - // all receivers accepted - if (cmd.type == SMTPCommand::MailBurl) { + break; + // trying to send mail + case SMTPCommand::Mail: + case SMTPCommand::MailBurl: + { + int stage = cmd.extra.toInt(); + // temporary failure upon receiving the sender address (greylisting probably) + if (status==421 && stage==0) { + errorString = last_match.captured(2).trimmed(); + // temporary envelope failure (greylisting) + setState(QwwSmtpClient::Connected); + processNextCommand(false); + } + if (status==250 && stage==0) { + // sender accepted errorString.clear(); - QByteArray url = cmd.data.toList().at(2).toByteArray(); - auto data = "BURL " + url + " LAST\r\n"; - emit q->logSent(data); - socket->write(data); - cmd.extra=2; - } else { + sendRcpt(); + } else if (status==250 && stage==1) { + // all receivers accepted + if (cmd.type == SMTPCommand::MailBurl) { + errorString.clear(); + QByteArray url = cmd.data.toList().at(2).toByteArray(); + auto data = "BURL " + url + " LAST\r\n"; + emit q->logSent(data); + socket->write(data); + cmd.extra=2; + } else { + errorString.clear(); + QByteArray data("DATA\r\n"); + emit q->logSent(data); + socket->write(data); + cmd.extra=2; + } + } else if ((cmd.type == SMTPCommand::Mail && status==354 && stage==2)) { + // DATA command accepted + errorString.clear(); + QByteArray toBeWritten = cmd.data.toList().at(2).toByteArray() + "\r\n.\r\n"; // termination token - CRLF.CRLF + emit q->logSent(toBeWritten); + socket->write(toBeWritten); // expecting data to be already escaped (CRLF.CRLF) + cmd.extra=3; + } else if ((cmd.type == SMTPCommand::MailBurl && status==250 && stage==2)) { + // BURL succeeded + setState(QwwSmtpClient::Connected); errorString.clear(); - QByteArray data("DATA\r\n"); - emit q->logSent(data); - socket->write(data); - cmd.extra=2; + processNextCommand(); + } else if ((cmd.type == SMTPCommand::Mail && status==250 && stage==3)) { + // mail queued + setState(QwwSmtpClient::Connected); + errorString.clear(); + processNextCommand(); + } else { + // something went wrong + errorString = last_match.captured(2).trimmed(); + setState(QwwSmtpClient::Connected); + emit q->done(false); + processNextCommand(); } - } else if ((cmd.type == SMTPCommand::Mail && status==354 && stage==2)) { - // DATA command accepted - errorString.clear(); - QByteArray toBeWritten = cmd.data.toList().at(2).toByteArray() + "\r\n.\r\n"; // termination token - CRLF.CRLF - emit q->logSent(toBeWritten); - socket->write(toBeWritten); // expecting data to be already escaped (CRLF.CRLF) - cmd.extra=3; - } else if ((cmd.type == SMTPCommand::MailBurl && status==250 && stage==2)) { - // BURL succeeded - setState(QwwSmtpClient::Connected); - errorString.clear(); - processNextCommand(); - } else if ((cmd.type == SMTPCommand::Mail && status==250 && stage==3)) { - // mail queued - setState(QwwSmtpClient::Connected); - errorString.clear(); - processNextCommand(); - } else { - // something went wrong - errorString = rxlast.cap(2).trimmed(); - setState(QwwSmtpClient::Connected); - emit q->done(false); - processNextCommand(); } + default: break; + } + } else { + qDebug() << "None of two regular expressions matched the input" << line; } - default: break; - } - } else { - qDebug() << "None of two regular expressions matched the input" << line; } } } void QwwSmtpClientPrivate::setState(QwwSmtpClient::State s) { QwwSmtpClient::State old = state; state = s; emit q->stateChanged(s); if (old == QwwSmtpClient::Connecting && s==QwwSmtpClient::Connected) emit q->connected(); if (s==QwwSmtpClient::Disconnected) emit q->disconnected(); } void QwwSmtpClientPrivate::processNextCommand(bool ok) { if (inProgress && !commandqueue.isEmpty()) { emit q->commandFinished(commandqueue.head().id, !ok); commandqueue.dequeue(); } if (commandqueue.isEmpty()) { inProgress = false; emit q->done(false); return; } SMTPCommand &cmd = commandqueue.head(); switch (cmd.type) { case SMTPCommand::Connect: { QString hostName = cmd.data.toList().at(0).toString(); uint port = cmd.data.toList().at(1).toUInt(); bool ssl = cmd.data.toList().at(2).toBool(); if(ssl){ emit q->logSent(QByteArrayLiteral("*** connectToHostEncrypted: ") + hostName.toUtf8() + ':' + QByteArray::number(port)); socket->connectToHostEncrypted(hostName, port); } else { emit q->logSent(QByteArrayLiteral("*** connectToHost: ") + hostName.toUtf8() + ':' + QByteArray::number(port)); socket->connectToHost(hostName, port); } setState(QwwSmtpClient::Connecting); } break; case SMTPCommand::Disconnect: { sendQuit(); } break; case SMTPCommand::StartTLS: { QByteArray data("STARTTLS\r\n"); emit q->logSent(data); socket->write(data); setState(QwwSmtpClient::TLSRequested); } break; case SMTPCommand::Authenticate: { QwwSmtpClient::AuthMode authmode = (QwwSmtpClient::AuthMode)cmd.data.toList().at(0).toInt(); if (authmode == QwwSmtpClient::AuthAny){ bool modified = false; if (authModes.testFlag(QwwSmtpClient::AuthPlain)) { authmode = QwwSmtpClient::AuthPlain; modified = true; } else if (authModes.testFlag(QwwSmtpClient::AuthLogin)) { authmode = QwwSmtpClient::AuthLogin; modified = true; } if (modified) { QVariantList data = cmd.data.toList(); data[0] = (int)authmode; cmd.data = data; } } switch (authmode) { case QwwSmtpClient::AuthPlain: { QByteArray buf("AUTH PLAIN\r\n"); emit q->logSent(buf); socket->write(buf); setState(QwwSmtpClient::Authenticating); break; } case QwwSmtpClient::AuthLogin: { QByteArray buf("AUTH LOGIN\r\n"); emit q->logSent(buf); socket->write(buf); setState(QwwSmtpClient::Authenticating); break; } default: errorString = QwwSmtpClient::tr("Unsupported or unknown authentication scheme"); emit q->done(false); } } break; case SMTPCommand::Mail: case SMTPCommand::MailBurl: { setState(QwwSmtpClient::Sending); QByteArray buf = QByteArray("MAIL FROM:<").append(cmd.data.toList().at(0).toByteArray()).append(">\r\n"); emit q->logSent(buf); socket->write(buf); break; } case SMTPCommand::RawCommand: { QString cont = cmd.data.toString(); if(!cont.endsWith("\r\n")) cont.append("\r\n"); setState(QwwSmtpClient::Sending); auto buf = cont.toUtf8(); emit q->logSent(buf); socket->write(buf); } break; } inProgress = true; emit q->commandStarted(cmd.id); } void QwwSmtpClientPrivate::_q_encrypted() { options = QwwSmtpClient::NoOptions; // forget everything, restart ehlo // SMTPCommand &cmd = commandqueue.head(); sendEhlo(); } void QwwSmtpClientPrivate::sendEhlo() { SMTPCommand &cmd = commandqueue.head(); QString domain = localName; if (socket->isEncrypted() && !localNameEncrypted.isEmpty()) domain = localNameEncrypted; QByteArray buf = QString("EHLO "+domain+"\r\n").toUtf8(); emit q->logSent(buf); socket->write(buf); cmd.extra = 1; } void QwwSmtpClientPrivate::sendHelo() { SMTPCommand &cmd = commandqueue.head(); QString domain = localName; if (socket->isEncrypted() && localNameEncrypted.isEmpty()) domain = localNameEncrypted; QByteArray buf = QString("HELO "+domain+"\r\n").toUtf8(); emit q->logSent(buf); socket->write(buf); cmd.extra = 1; } void QwwSmtpClientPrivate::sendQuit() { QByteArray buf("QUIT\r\n"); emit q->logSent(buf); socket->write(buf); socket->waitForBytesWritten(1000); socket->disconnectFromHost(); setState(QwwSmtpClient::Disconnecting); } void QwwSmtpClientPrivate::sendRcpt() { SMTPCommand &cmd = commandqueue.head(); QVariantList vlist = cmd.data.toList(); QList rcptlist = vlist.at(1).toList(); QByteArray buf = QByteArray("RCPT TO:<").append(rcptlist.first().toByteArray()).append(">\r\n"); emit q->logSent(buf); socket->write(buf); rcptlist.removeFirst(); vlist[1] = rcptlist; cmd.data = vlist; if (rcptlist.isEmpty()) cmd.extra = 1; } void QwwSmtpClientPrivate::sendAuthPlain(const QString & username, const QString & password) { QByteArray ba; ba.append('\0'); ba.append(username.toUtf8()); ba.append('\0'); ba.append(password.toUtf8()); QByteArray encoded = ba.toBase64(); emit q->logSent(QByteArrayLiteral("*** [sending authentication data: username '") + username.toUtf8() + "']"); socket->write(encoded); socket->write("\r\n"); } void QwwSmtpClientPrivate::sendAuthLogin(const QString & username, const QString & password, int stage) { if (stage==1) { auto buf = username.toUtf8().toBase64() + "\r\n"; emit q->logSent(buf); socket->write(buf); } else if (stage==2) { emit q->logSent("*** [AUTH LOGIN password]"); socket->write(password.toUtf8().toBase64()); socket->write("\r\n"); } } void QwwSmtpClientPrivate::parseOption(const QString &buffer){ if(buffer.toLower()=="pipelining"){ options |= QwwSmtpClient::PipeliningOption; } else if(buffer.toLower()=="starttls"){ options |= QwwSmtpClient::StartTlsOption; } else if(buffer.toLower()=="8bitmime"){ options |= QwwSmtpClient::EightBitMimeOption; } else if(buffer.toLower().startsWith("auth ")){ options |= QwwSmtpClient::AuthOption; // parse auth modes QStringList slist = buffer.mid(5).split(" "); foreach(const QString &s, slist){ if(s.toLower()=="plain"){ authModes |= QwwSmtpClient::AuthPlain; } if(s.toLower()=="login"){ authModes |= QwwSmtpClient::AuthLogin; } } } } QwwSmtpClient::QwwSmtpClient(QObject *parent) : QObject(parent), d(new QwwSmtpClientPrivate(this)) { d->state = Disconnected; d->lastId = 0; d->inProgress = false; d->localName = "localhost"; d->socket = new QSslSocket(this); connect(d->socket, SIGNAL(connected()), this, SLOT(onConnected())); connect(d->socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError)) ); connect(d->socket, SIGNAL(disconnected()), this, SLOT(onDisconnected())); connect(d->socket, SIGNAL(readyRead()), this, SLOT(_q_readFromSocket())); connect(d->socket, SIGNAL(sslErrors(const QList &)), this, SIGNAL(sslErrors(const QList&))); } QwwSmtpClient::~QwwSmtpClient() { delete d; } int QwwSmtpClient::connectToHost(const QString & hostName, quint16 port) { SMTPCommand cmd; cmd.type = SMTPCommand::Connect; cmd.data = QVariantList() << hostName << port << false; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } // int QwwSmtpClient::connectToHost(const QHostAddress & address, quint16 port) { // d->socket->connectToHost(address, port); // d->setState(Connecting); // } int QwwSmtpClient::connectToHostEncrypted(const QString & hostName, quint16 port) { SMTPCommand cmd; cmd.type = SMTPCommand::Connect; cmd.data = QVariantList() << hostName << port << true; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if(!d->inProgress) d->processNextCommand(); return cmd.id; } int QwwSmtpClient::disconnectFromHost() { SMTPCommand cmd; cmd.type = SMTPCommand::Disconnect; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } int QwwSmtpClient::startTls() { connect(d->socket, SIGNAL(encrypted()), this, SLOT(_q_encrypted()), Qt::UniqueConnection); SMTPCommand cmd; cmd.type = SMTPCommand::StartTLS; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } void QwwSmtpClient::setLocalName(const QString & ln) { d->localName = ln; } void QwwSmtpClient::setLocalNameEncrypted(const QString & ln) { d->localNameEncrypted = ln; } int QwwSmtpClient::authenticate(const QString &user, const QString &password, AuthMode mode) { SMTPCommand cmd; cmd.type = SMTPCommand::Authenticate; cmd.data = QVariantList() << (int)mode << user << password; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } int QwwSmtpClient::sendMail(const QByteArray &from, const QList &to, const QByteArray &content) { QList rcpts; for(QList::const_iterator it = to.begin(); it != to.end(); it ++) { rcpts.append(QVariant(*it)); } SMTPCommand cmd; cmd.type = SMTPCommand::Mail; cmd.data = QVariantList() << from << QVariant(rcpts) << content; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } int QwwSmtpClient::sendMailBurl(const QByteArray &from, const QList &to, const QByteArray &url) { QList rcpts; for(QList::const_iterator it = to.begin(); it != to.end(); it ++) { rcpts.append(QVariant(*it)); } SMTPCommand cmd; cmd.type = SMTPCommand::MailBurl; cmd.data = QVariantList() << from << QVariant(rcpts) << url; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } int QwwSmtpClient::rawCommand(const QString & raw) { SMTPCommand cmd; cmd.type = SMTPCommand::RawCommand; cmd.data = raw; cmd.id = ++d->lastId; d->commandqueue.enqueue(cmd); if (!d->inProgress) d->processNextCommand(); return cmd.id; } void QwwSmtpClientPrivate::abortDialog() { emit q->commandFinished(commandqueue.head().id, true); commandqueue.clear(); sendQuit(); } void QwwSmtpClient::ignoreSslErrors() {d->socket->ignoreSslErrors(); } QwwSmtpClient::AuthModes QwwSmtpClient::supportedAuthModes() const{ return d->authModes; } QwwSmtpClient::Options QwwSmtpClient::options() const{ return d->options; } QString QwwSmtpClient::errorString() const{ return d->errorString; } #include "moc_qwwsmtpclient.cpp"