diff --git a/CMakeLists.txt b/CMakeLists.txt index 39eacad..721af3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,58 +1,61 @@ # SPDX-FileCopyrightText: 2023 g10 code Gmbh # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause cmake_minimum_required(VERSION 3.20) project(gpgoljs) set(KF_MIN_VERSION "6.0.0") set(QT_MIN_VERSION "6.6.0") set(MIMETREEPARSER_VERSION "6.0.0") set(LIBKLEO_VERSION "6.0.0") set(KLDAP_VERSION "6.0.0") set(LIBKDEPIM_VERSION "6.0.0") set(PIMTEXTEDIT_VERSION "6.0.0") find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(ECMQtDeclareLoggingCategory) include(ECMAddTests) include(KDECompilerSettings NO_POLICY_SCOPE) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core HttpServer Widgets PrintSupport) set_package_properties(Qt6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Contacts Completion CoreAddons WidgetsAddons Config ColorScheme Codecs XmlGui GuiAddons JobWidgets Sonnet CalendarCore Archive) set_package_properties(KF6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) find_package(KPim6Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED) find_package(KPim6Libkdepim ${LIBKDEPIM_LIB_VERSION} CONFIG REQUIRED) find_package(KPim6MimeTreeParserWidgets ${MIMETREEPARSER_VERSION} CONFIG REQUIRED) find_package(KPim6TextEdit ${PIMTEXTEDIT_VERSION} CONFIG REQUIRED) find_package(KF6TextAutoCorrectionCore CONFIG REQUIRED) if (BUILD_TESTING) find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test) endif() add_subdirectory(common) add_subdirectory(server) add_subdirectory(client) diff --git a/client/autotests/itipjobtest.cpp b/client/autotests/itipjobtest.cpp index dad132c..73ba4e4 100644 --- a/client/autotests/itipjobtest.cpp +++ b/client/autotests/itipjobtest.cpp @@ -1,143 +1,143 @@ /* SPDX-FileCopyrightText: 2023 Daniel Vrátil SPDX-License-Identifier: LGPL-2.0-or-later */ #include "itipjobtest.h" #include "qtest_messagecomposer.h" #include #include #include #include using namespace KMime; #include "../editor/composer.h" #include "../editor/part/globalpart.h" #include "../editor/job/itipjob.h" #include "../editor/part/itippart.h" using namespace MessageComposer; QTEST_MAIN(ItipJobTest) -static QString testItip = QStringLiteral(R"( +static constexpr QLatin1StringView testItip(R"( BEGIN:VCALENDAR CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT CREATED:20230508T143456Z ORGANIZER;CN=Konqi:MAILTO:konqi@example.com ATTENDEE;CN=Kate;RSVP=TRUE;ROLE=REQ-PARTICIPANT:MAILTO:kate@example.com CREATED:20230508T143456Z UID:KOrganizer-1673850046.1067 SUMMARY:Krypto Party DTSTART;VALUE=DATE:20230520 DTEND;VALUE=DATE:20230520 END:VEVENT END:VCALENDAR)"); -static QString testItipMessage = QStringLiteral("Hi all, let's do some crypto partying!"); +static constexpr QLatin1StringView testItipMessage("Hi all, let's do some crypto partying!"); void ItipJobTest::testInvitationWithAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(false); part->setInvitation(testItip); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "multipart/mixed"); const auto subparts = content->contents(); QCOMPARE(subparts.size(), 2); const auto msgPart = subparts[0]; QCOMPARE(msgPart->contentType(false)->mimeType(), "text/plain"); QCOMPARE(msgPart->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(msgPart->decodedText(), testItipMessage); const auto itipPart = subparts[1]; QCOMPARE(itipPart->contentType(false)->mimeType(), "text/calendar"); QCOMPARE(itipPart->contentType(false)->name(), QStringLiteral("cal.ics")); QCOMPARE(itipPart->contentType(false)->parameter(QStringLiteral("method")), QStringLiteral("request")); QCOMPARE(itipPart->contentType(false)->charset(), "utf-8"); QCOMPARE(itipPart->contentDisposition(false)->disposition(), KMime::Headers::CDattachment); QCOMPARE(itipPart->decodedText(), testItip); } void ItipJobTest::testInvitationWithoutAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(false); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "text/plain"); QCOMPARE(content->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(content->decodedText(), testItipMessage); } void ItipJobTest::testOutlookInvitationWithAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(true); part->setInvitation(testItip); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "text/calendar"); QCOMPARE(content->contentType(false)->name(), QStringLiteral("cal.ics")); QCOMPARE(content->contentType(false)->parameter(QStringLiteral("method")), QStringLiteral("request")); QCOMPARE(content->contentType(false)->charset(), "utf-8"); QCOMPARE(content->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(content->decodedText(), testItip); } void ItipJobTest::testOutlookInvitationWithoutAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(true); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "text/calendar"); QCOMPARE(content->contentType(false)->name(), QStringLiteral("cal.ics")); QCOMPARE(content->contentType(false)->parameter(QStringLiteral("method")), QStringLiteral("request")); QCOMPARE(content->contentType(false)->charset(), "utf-8"); QVERIFY(content->decodedText().isEmpty()); } #include "moc_itipjobtest.cpp" diff --git a/client/editor/addresseelineedit.cpp b/client/editor/addresseelineedit.cpp index c0e1cce..700c220 100644 --- a/client/editor/addresseelineedit.cpp +++ b/client/editor/addresseelineedit.cpp @@ -1,1170 +1,1169 @@ /* This file is part of libkdepim. SPDX-FileCopyrightText: 2002 Helge Deller SPDX-FileCopyrightText: 2002 Lubos Lunak SPDX-FileCopyrightText: 2001, 2003 Carsten Pfeiffer SPDX-FileCopyrightText: 2001 Waldo Bastian SPDX-FileCopyrightText: 2004 Daniel Molkentin SPDX-FileCopyrightText: 2004 Karl-Heinz Zimmer SPDX-FileCopyrightText: 2017-2024 Laurent Montel SPDX-License-Identifier: LGPL-2.0-or-later */ #include "addresseelineedit.h" #include "addresseelineeditmanager.h" #include "kmailcompletion.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include using namespace std::chrono_literals; using namespace Qt::Literals::StringLiterals; -static const QString s_completionItemIndentString = QStringLiteral(" "); +static constexpr QLatin1StringView s_completionItemIndentString(" "); inline bool itemIsHeader(const QListWidgetItem *item) { - return item && !item->text().startsWith(QLatin1StringView(" ")); + return item && !item->text().startsWith(s_completionItemIndentString); } QString adaptPasteMails(const QString &str) { QString newText = str; // remove newlines in the to-be-pasted string static QRegularExpression reg2(QStringLiteral("\r?\n")); QStringList lines = newText.split(reg2, Qt::SkipEmptyParts); QStringList::iterator end(lines.end()); for (QStringList::iterator it = lines.begin(); it != end; ++it) { // remove trailing commas and whitespace static QRegularExpression reg1(QRegularExpression(QStringLiteral(",?\\s*$"))); it->remove(reg1); } newText = lines.join(QLatin1StringView(", ")); if (newText.startsWith(QLatin1StringView("mailto:"))) { const QUrl url(newText); newText = url.path(); } else if (newText.contains(QLatin1StringView(" at "))) { // Anti-spam stuff newText.replace(QStringLiteral(" at "), QStringLiteral("@")); newText.replace(QStringLiteral(" dot "), QStringLiteral(".")); } else if (newText.contains(QLatin1StringView("(at)"))) { static QRegularExpression reg((QStringLiteral("\\s*\\(at\\)\\s*"))); newText.replace(reg, QStringLiteral("@")); } return newText; } class SourceWithWeight { public: int weight; // the weight of the source int index; // index into AddresseeLineEditStatic::self()->completionSources QString sourceName; // the name of the source, e.g. "LDAP Server" bool operator<(const SourceWithWeight &other) const { if (weight > other.weight) { return true; } if (weight < other.weight) { return false; } return sourceName < other.sourceName; } }; class AddresseeLineEditPrivate : public QObject { Q_OBJECT public: AddresseeLineEditPrivate(AddresseeLineEdit *qq); void setCompletedItems(const QStringList &items, bool autoSuggest); void addCompletionItem(const QString &string, int weight, int completionItemSource, const QStringList *keyWords = nullptr); bool smartPaste() const; void setSmartPaste(bool smartPaste); bool completionInitialized() const; bool useCompletion() const; void setUseCompletion(bool useCompletion); const QStringList adjustedCompletionItems(bool fullSearch); void updateSearchString(); void startSearches(); void slotCompletion(); void doCompletion(bool ctrlT); void searchInGnupg(); void slotTriggerDelayedQueries(); [[nodiscard]] QString searchString() const; void setSearchString(const QString &searchString); [[nodiscard]] bool searchExtended() const; void setSearchExtended(bool searchExtended); [[nodiscard]] bool useSemicolonAsSeparator() const; void setUseSemicolonAsSeparator(bool useSemicolonAsSeparator); void slotReturnPressed(const QString &); void slotPopupCompletion(const QString &completion); void slotUserCancelled(const QString &cancelText); private: AddresseeLineEdit * const q; QString mSearchString; QString mPreviousAddresses; bool mSmartPaste; bool mUseCompletion = true; bool mCompletionInitialized = false; bool mLastSearchMode; bool mSearchExtended = false; bool mUseSemicolonAsSeparator = false; QTimer *mDelayedQueryTimer; }; AddresseeLineEditPrivate::AddresseeLineEditPrivate(AddresseeLineEdit *qq) : QObject(qq) , q(qq) , mDelayedQueryTimer(new QTimer(this)) { mDelayedQueryTimer->setSingleShot(true); connect(mDelayedQueryTimer, &QTimer::timeout, this, &AddresseeLineEditPrivate::slotTriggerDelayedQueries); if (!mCompletionInitialized) { q->setCompletionObject(AddresseeLineEditManager::self()->completion(), false); connect(q, &KLineEdit::completion, this, &AddresseeLineEditPrivate::slotCompletion); connect(q, &AddresseeLineEdit::returnKeyPressed, this, &AddresseeLineEditPrivate::slotReturnPressed); KCompletionBox *box = q->completionBox(); connect(box, &KCompletionBox::textActivated, this, &AddresseeLineEditPrivate::slotPopupCompletion); connect(box, &KCompletionBox::userCancelled, this, &AddresseeLineEditPrivate::slotUserCancelled); mCompletionInitialized = true; } connect(q, &AddresseeLineEdit::textCompleted, q, &AddresseeLineEdit::slotEditingFinished); connect(q, &AddresseeLineEdit::editingFinished, q, &AddresseeLineEdit::slotEditingFinished); AddresseeLineEditManager::self()->addCompletionSource(i18n("GPG keychain"), 100); } void AddresseeLineEditPrivate::slotUserCancelled(const QString &cancelText) { q->callUserCancelled(mPreviousAddresses + cancelText); // in KLineEdit } void AddresseeLineEditPrivate::slotPopupCompletion(const QString &completion) { QString c = completion.trimmed(); if (c.endsWith(QLatin1Char(')'))) { c = completion.mid(0, completion.lastIndexOf(QLatin1StringView(" ("))).trimmed(); } q->setText(mPreviousAddresses + c); q->cursorAtEnd(); updateSearchString(); q->emitTextCompleted(); } void AddresseeLineEditPrivate::slotReturnPressed(const QString &) { if (!q->completionBox()->selectedItems().isEmpty()) { slotPopupCompletion(q->completionBox()->selectedItems().constFirst()->text()); } } bool AddresseeLineEditPrivate::useSemicolonAsSeparator() const { return mUseSemicolonAsSeparator; } void AddresseeLineEditPrivate::setUseSemicolonAsSeparator(bool useSemicolonAsSeparator) { mUseSemicolonAsSeparator = useSemicolonAsSeparator; } const QStringList AddresseeLineEditPrivate::adjustedCompletionItems(bool fullSearch) { QStringList items = fullSearch ? AddresseeLineEditManager::self()->completion()->allMatches(mSearchString) : AddresseeLineEditManager::self()->completion()->substringCompletion(mSearchString); // force items to be sorted by email items.sort(); // For weighted mode, the algorithm is the following: // In the first loop, we add each item to its section (there is one section per completion source) // We also add spaces in front of the items. // The sections are appended to the items list. // In the second loop, we then walk through the sections and add all the items in there to the // sorted item list, which is the final result. // // The algo for non-weighted mode is different. int lastSourceIndex = -1; // Maps indices of the items list, which are section headers/source items, // to a QStringList which are the items of that section/source. QMap sections; QStringList sortedItems; for (QStringList::Iterator it = items.begin(); it != items.end(); ++it) { auto cit = AddresseeLineEditManager::self()->completionItemsMap.constFind(*it); if (cit == AddresseeLineEditManager::self()->completionItemsMap.constEnd()) { continue; } const int index = (*cit).second; if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) { if (lastSourceIndex == -1 || lastSourceIndex != index) { const QString sourceLabel(AddresseeLineEditManager::self()->completionSources.at(index)); if (sections.find(index) == sections.end()) { it = items.insert(it, sourceLabel); ++it; // skip new item } lastSourceIndex = index; } it->prepend(s_completionItemIndentString); // remove preferred email sort added in addContact() it->replace(QLatin1StringView(" <"), QStringLiteral(" <")); } sections[index].append(*it); if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Sorted) { sortedItems.append(*it); } } if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) { // Sort the sections QList sourcesAndWeights; const int numberOfCompletionSources(AddresseeLineEditManager::self()->completionSources.count()); sourcesAndWeights.reserve(numberOfCompletionSources); for (int i = 0; i < numberOfCompletionSources; ++i) { SourceWithWeight sww; sww.sourceName = AddresseeLineEditManager::self()->completionSources.at(i); sww.weight = AddresseeLineEditManager::self()->completionSourceWeights[sww.sourceName]; sww.index = i; sourcesAndWeights.append(sww); } std::sort(sourcesAndWeights.begin(), sourcesAndWeights.end()); // Add the sections and their items to the final sortedItems result list const int numberOfSources(sourcesAndWeights.size()); for (int i = 0; i < numberOfSources; ++i) { const SourceWithWeight source = sourcesAndWeights.at(i); const QStringList sectionItems = sections[source.index]; if (!sectionItems.isEmpty()) { sortedItems.append(source.sourceName); for (const QString &itemInSection : sectionItems) { sortedItems.append(itemInSection); } } } } else { sortedItems.sort(); } return sortedItems; } void AddresseeLineEditPrivate::doCompletion(bool ctrlT) { mLastSearchMode = ctrlT; const KCompletion::CompletionMode mode = q->completionMode(); if (mode == KCompletion::CompletionNone) { return; } AddresseeLineEditManager::self()->completion()->setOrder(KCompletion::Weighted); // cursor at end of string - or Ctrl+T pressed for substring completion? if (ctrlT) { const QStringList completions = adjustedCompletionItems(false); if (completions.count() == 1) { q->setText(mPreviousAddresses + completions.first().trimmed()); } // Make sure the completion popup is closed if no matching items were found setCompletedItems(completions, true); q->cursorAtEnd(); q->setCompletionMode(mode); // set back to previous mode return; } switch (mode) { case KCompletion::CompletionPopupAuto: if (mSearchString.isEmpty()) { break; } // else: fall-through to the CompletionPopup case [[fallthrough]]; case KCompletion::CompletionPopup: { const QStringList items = adjustedCompletionItems(false); setCompletedItems(items, false); break; } case KCompletion::CompletionShell: { const QString match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString); if (!match.isNull() && match != mSearchString) { q->setText(mPreviousAddresses + match); q->setModified(true); q->cursorAtEnd(); } break; } case KCompletion::CompletionMan: // Short-Auto in fact case KCompletion::CompletionAuto: // force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect q->setCompletionMode(q->completionMode()); if (!mSearchString.isEmpty()) { // if only our \" is left, remove it since user has not typed it either if (mSearchExtended && mSearchString == QLatin1StringView("\"")) { mSearchExtended = false; mSearchString.clear(); q->setText(mPreviousAddresses); break; } QString match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString); if (!match.isEmpty()) { if (match != mSearchString) { const QString adds = mPreviousAddresses + match; q->callSetCompletedText(adds); } } else { if (!mSearchString.startsWith(QLatin1Char('\"'))) { // try with quoted text, if user has not type one already match = AddresseeLineEditManager::self()->completion()->makeCompletion(QLatin1StringView("\"") + mSearchString); if (!match.isEmpty() && match != mSearchString) { mSearchString = QLatin1StringView("\"") + mSearchString; mSearchExtended = true; q->setText(mPreviousAddresses + mSearchString); q->callSetCompletedText(mPreviousAddresses + match); } } else if (mSearchExtended) { // our added \" does not work anymore, remove it mSearchString.remove(0, 1); mSearchExtended = false; q->setText(mPreviousAddresses + mSearchString); // now try again match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString); if (!match.isEmpty() && match != mSearchString) { const QString adds = mPreviousAddresses + match; q->setCompletedText(adds); } } } } break; case KCompletion::CompletionNone: break; } } void AddresseeLineEditPrivate::setCompletedItems(const QStringList &items, bool autoSuggest) { KCompletionBox *completionBox = q->completionBox(); if (!items.isEmpty() && !(items.count() == 1 && mSearchString == items.first())) { completionBox->clear(); const int numberOfItems(items.count()); for (int i = 0; i < numberOfItems; ++i) { auto item = new QListWidgetItem(items.at(i), completionBox); if (!items.at(i).startsWith(s_completionItemIndentString)) { item->setFlags(item->flags() & ~Qt::ItemIsSelectable); item->setBackground(AddresseeLineEditManager::self()->alternateColor()); } completionBox->addItem(item); } if (!completionBox->isVisible()) { if (!mSearchString.isEmpty()) { completionBox->setCancelledText(mSearchString); } completionBox->popup(); // we have to install the event filter after popup(), since that // calls show(), and that's where KCompletionBox installs its filter. // We want to be first, though, so do it now. if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) { qApp->installEventFilter(q); } } QListWidgetItem *item = completionBox->item(1); if (item) { completionBox->blockSignals(true); completionBox->setCurrentItem(item); item->setSelected(true); completionBox->blockSignals(false); } if (autoSuggest) { const int index = items.first().indexOf(mSearchString); const QString newText = items.first().mid(index); q->callSetUserSelection(false); q->callSetCompletedText(newText, true); } } else { if (completionBox && completionBox->isVisible()) { completionBox->hide(); completionBox->setItems(QStringList()); } } } void AddresseeLineEditPrivate::addCompletionItem(const QString &string, int weight, int completionItemSource, const QStringList *keyWords) { // Check if there is an exact match for item already, and use the // maximum weight if so. Since there's no way to get the information // from KCompletion, we have to keep our own QMap. // We also update the source since the item should always be shown from the source with the highest weight auto manager = AddresseeLineEditManager::self(); auto it = manager->completionItemsMap.find(string); if (it != manager->completionItemsMap.end()) { weight = qMax((*it).first, weight); (*it).first = weight; (*it).second = completionItemSource; } else { manager->completionItemsMap.insert(string, qMakePair(weight, completionItemSource)); } manager->completion()->addItem(string, weight); if (keyWords && !keyWords->isEmpty()) { manager->completion()->addItemWithKeys(string, weight, keyWords); // see kmailcompletion.cpp } } bool AddresseeLineEditPrivate::smartPaste() const { return mSmartPaste; } void AddresseeLineEditPrivate::setSmartPaste(bool smartPaste) { mSmartPaste = smartPaste; } bool AddresseeLineEditPrivate::completionInitialized() const { return mCompletionInitialized; } bool AddresseeLineEditPrivate::useCompletion() const { return mUseCompletion; } void AddresseeLineEditPrivate::setUseCompletion(bool useCompletion) { mUseCompletion = useCompletion; } AddresseeLineEdit::AddresseeLineEdit(QWidget *parent) : KLineEdit(parent) , d(std::make_unique(this)) {} AddresseeLineEdit::~AddresseeLineEdit() = default; void AddresseeLineEdit::addContactGroup(const KContacts::ContactGroup &group, int weight, int source) { d->addCompletionItem(group.name(), weight, source); } void AddresseeLineEdit::addContact(const QStringList &emails, const KContacts::Addressee &addr, int weight, int source, QString append) { int isPrefEmail = 1; // first in list is preferredEmail for (const QString &email : emails) { // TODO: highlight preferredEmail const QString givenName = addr.givenName(); const QString familyName = addr.familyName(); const QString nickName = addr.nickName(); const QString fullEmail = addr.fullEmail(email); QString appendix; if (!append.isEmpty()) { appendix = QStringLiteral(" (%1)"); append.replace(QLatin1Char('('), QStringLiteral("[")); append.replace(QLatin1Char(')'), QStringLiteral("]")); appendix = appendix.arg(append); } // Prepare "givenName" + ' ' + "familyName" QString fullName = givenName; if (!familyName.isEmpty()) { if (!fullName.isEmpty()) { fullName += QLatin1Char(' '); } fullName += familyName; } // Finally, we can add the completion items if (!fullName.isEmpty()) { const QString address = KEmailAddress::normalizedAddress(fullName, email, QString()); if (fullEmail != address) { // This happens when fullEmail contains a middle name, while our own fullName+email only has "first last". // Let's offer both, the fullEmail with 3 parts, looks a tad formal. d->addCompletionItem(address + appendix, weight + isPrefEmail, source); } } QStringList keyWords; if (!nickName.isEmpty()) { keyWords.append(nickName); } d->addCompletionItem(fullEmail + appendix, weight + isPrefEmail, source, &keyWords); isPrefEmail = 0; } } void AddresseeLineEdit::addContact(const KContacts::Addressee &addr, int weight, int source, const QString &append) { const QStringList emails = addr.emails(); if (emails.isEmpty()) { return; } addContact(emails, addr, weight, source, append); } void AddresseeLineEdit::dropEvent(QDropEvent *event) { const QMimeData *md = event->mimeData(); // Case one: The user dropped a text/directory (i.e. vcard), so decode its // contents if (KContacts::VCardDrag::canDecode(md)) { KContacts::Addressee::List list; KContacts::VCardDrag::fromMimeData(md, list); for (const KContacts::Addressee &addr : std::as_const(list)) { insertEmails(addr.emails()); } } // Case two: The user dropped a list or Urls. // Iterate over that list. For mailto: Urls, just add the addressee to the list, // and for other Urls, download the Url and assume it points to a vCard else if (md->hasUrls()) { const QList urls = md->urls(); KContacts::Addressee::List list; for (const QUrl &url : urls) { // First, let's deal with mailto Urls. The path() part contains the // email-address. if (url.scheme() == QLatin1StringView("mailto")) { KContacts::Addressee addressee; KContacts::Email email(KEmailAddress::decodeMailtoUrl(url)); email.setPreferred(true); addressee.addEmail(email); list += addressee; } } // Now, let the user choose which addressee to add. for (const KContacts::Addressee &addressee : std::as_const(list)) { insertEmails(addressee.emails()); } } // Case three: Let AddresseeLineEdit deal with the rest else { if (!isReadOnly()) { const QList uriList = event->mimeData()->urls(); if (!uriList.isEmpty()) { QString contents = text(); // remove trailing white space and comma int eot = contents.length(); while ((eot > 0) && contents.at(eot - 1).isSpace()) { --eot; } if (eot == 0) { contents.clear(); } else if (contents.at(eot - 1) == QLatin1Char(',')) { --eot; contents.truncate(eot); } bool mailtoURL = false; // append the mailto URLs for (const QUrl &url : uriList) { if (url.scheme() == QLatin1StringView("mailto")) { mailtoURL = true; QString address; address = QUrl::fromPercentEncoding(url.path().toLatin1()); address = KCodecs::decodeRFC2047String(address); if (!contents.isEmpty()) { contents.append(QLatin1StringView(", ")); } contents.append(address); } } if (mailtoURL) { setText(contents); setModified(true); return; } } else { // Let's see if this drop contains a comma separated list of emails if (md->hasText()) { const QString dropData = md->text(); const QStringList addrs = KEmailAddress::splitAddressList(dropData); if (!addrs.isEmpty()) { if (addrs.count() == 1) { QUrl url(dropData); if (url.scheme() == QLatin1StringView("mailto")) { KContacts::Addressee addressee; KContacts::Email email(KEmailAddress::decodeMailtoUrl(url)); email.setPreferred(true); addressee.addEmail(email); insertEmails(addressee.emails()); } else { setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData)); } } else { setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData)); } setModified(true); return; } } } } if (d->useCompletion()) { d->setSmartPaste(true); } QLineEdit::dropEvent(event); d->setSmartPaste(false); } } void AddresseeLineEdit::insertEmails(const QStringList &emails) { if (emails.empty()) { return; } QString contents = text(); if (!contents.isEmpty()) { contents += QLatin1Char(','); } // only one address, don't need kpopup to choose if (emails.size() == 1) { setText(contents + emails.front()); return; } // multiple emails, let the user choose one QMenu menu(this); menu.setTitle(i18n("Select email from contact")); menu.setObjectName(QLatin1StringView("Addresschooser")); for (const QString &email : emails) { menu.addAction(email); } const QAction *result = menu.exec(QCursor::pos()); if (!result) { return; } setText(contents + KLocalizedString::removeAcceleratorMarker(result->text())); } void AddresseeLineEdit::cursorAtEnd() { setCursorPosition(text().length()); } void AddresseeLineEdit::enableCompletion(bool enable) { d->setUseCompletion(enable); } bool AddresseeLineEdit::isCompletionEnabled() const { return d->useCompletion(); } bool AddresseeLineEdit::eventFilter(QObject *object, QEvent *event) { if (d->completionInitialized() && (object == completionBox() || completionBox()->findChild(object->objectName()) == object)) { if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonDblClick) { const QMouseEvent *mouseEvent = static_cast(event); // find list box item at the event position QListWidgetItem *item = completionBox()->itemAt(mouseEvent->pos()); if (!item) { // In the case of a mouse move outside of the box we don't want // the parent to fuzzy select a header by mistake. const bool eat = event->type() == QEvent::MouseMove; return eat; } // avoid selection of headers on button press, or move or release while // a button is pressed const Qt::MouseButtons buttons = mouseEvent->buttons(); if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick || buttons & Qt::LeftButton || buttons & Qt::MiddleButton || buttons & Qt::RightButton) { if (itemIsHeader(item)) { return true; // eat the event, we don't want anything to happen } else { // if we are not on one of the group heading, make sure the item // below or above is selected, not the heading, inadvertedly, due // to fuzzy auto-selection from QListBox completionBox()->setCurrentItem(item); item->setSelected(true); if (event->type() == QEvent::MouseMove) { return true; // avoid fuzzy selection behavior } } } } } if ((object == this) && (event->type() == QEvent::ShortcutOverride)) { auto keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Tab) { keyEvent->accept(); return true; } } if ((object == this) && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) && completionBox()->isVisible()) { const QKeyEvent *keyEvent = static_cast(event); int currentIndex = completionBox()->currentRow(); if (currentIndex < 0) { return true; } if (keyEvent->key() == Qt::Key_Up) { // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex; // figure out if the item we would be moving to is one we want // to ignore. If so, go one further const QListWidgetItem *itemAbove = completionBox()->item(currentIndex); if (itemAbove && itemIsHeader(itemAbove)) { // there is a header above is, check if there is even further up // and if so go one up, so it'll be selected if (currentIndex > 0 && completionBox()->item(currentIndex - 1)) { // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1; completionBox()->setCurrentRow(currentIndex - 1); completionBox()->item(currentIndex - 1)->setSelected(true); } else if (currentIndex == 0) { // nothing to skip to, let's stay where we are, but make sure the // first header becomes visible, if we are the first real entry completionBox()->scrollToItem(completionBox()->item(0)); QListWidgetItem *item = completionBox()->item(currentIndex); if (item) { if (itemIsHeader(item)) { currentIndex++; item = completionBox()->item(currentIndex); } completionBox()->setCurrentItem(item); item->setSelected(true); } } return true; } } else if (keyEvent->key() == Qt::Key_Down) { // same strategy for downwards // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex; const QListWidgetItem *itemBelow = completionBox()->item(currentIndex); if (itemBelow && itemIsHeader(itemBelow)) { if (completionBox()->item(currentIndex + 1)) { // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1; completionBox()->setCurrentRow(currentIndex + 1); completionBox()->item(currentIndex + 1)->setSelected(true); } else { // nothing to skip to, let's stay where we are QListWidgetItem *item = completionBox()->item(currentIndex); if (item) { completionBox()->setCurrentItem(item); item->setSelected(true); } } return true; } // special case of the initial selection, which is unfortunately a header. // Setting it to selected tricks KCompletionBox into not treating is special // and selecting making it current, instead of the one below. QListWidgetItem *item = completionBox()->item(currentIndex); if (item && itemIsHeader(item)) { completionBox()->setCurrentItem(item); item->setSelected(true); } } else if (event->type() == QEvent::KeyRelease && (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)) { /// first, find the header of the current section QListWidgetItem *myHeader = nullptr; int myHeaderIndex = -1; const int iterationStep = keyEvent->key() == Qt::Key_Tab ? 1 : -1; int index = qMin(qMax(currentIndex - iterationStep, 0), completionBox()->count() - 1); while (index >= 0) { if (itemIsHeader(completionBox()->item(index))) { myHeader = completionBox()->item(index); myHeaderIndex = index; break; } index--; } Q_ASSERT(myHeader); // we should always be able to find a header // find the next header (searching backwards, for Qt::Key_Backtab) QListWidgetItem *nextHeader = nullptr; // when iterating forward, start at the currentindex, when backwards, // one up from our header, or at the end int j; if (keyEvent->key() == Qt::Key_Tab) { j = currentIndex; } else { index = myHeaderIndex; if (index == 0) { j = completionBox()->count() - 1; } else { j = (index - 1) % completionBox()->count(); } } while ((nextHeader = completionBox()->item(j)) && nextHeader != myHeader) { if (itemIsHeader(nextHeader)) { break; } j = (j + iterationStep) % completionBox()->count(); } if (nextHeader && nextHeader != myHeader) { QListWidgetItem *item = completionBox()->item(j + 1); if (item && !itemIsHeader(item)) { completionBox()->setCurrentItem(item); item->setSelected(true); } } return true; } } return KLineEdit::eventFilter(object, event); } void AddresseeLineEdit::setText(const QString &text) { const int cursorPos = cursorPosition(); KLineEdit::setText(text.trimmed()); setCursorPosition(cursorPos); } void AddresseeLineEdit::insert(const QString &t) { if (!d->smartPaste()) { KLineEdit::insert(t); return; } QString newText = t.trimmed(); if (newText.isEmpty()) { return; } newText = adaptPasteMails(newText); QString contents = text(); int pos = cursorPosition(); if (hasSelectedText()) { // Cut away the selection. int start_sel = selectionStart(); pos = start_sel; contents = contents.left(start_sel) + contents.mid(start_sel + selectedText().length()); } int eot = contents.length(); while ((eot > 0) && contents.at(eot - 1).isSpace()) { --eot; } if (eot == 0) { contents.clear(); } else if (pos >= eot) { if (contents.at(eot - 1) == QLatin1Char(',')) { --eot; } contents.truncate(eot); contents += QStringLiteral(", "); pos = eot + 2; } contents = contents.left(pos) + newText + contents.mid(pos); setText(contents); setModified(true); setCursorPosition(pos + newText.length()); } void AddresseeLineEdit::paste() { if (d->useCompletion()) { d->setSmartPaste(true); } KLineEdit::paste(); d->setSmartPaste(false); } void AddresseeLineEdit::keyPressEvent(QKeyEvent *event) { bool accept = false; const int key = event->key() | event->modifiers(); if (KStandardShortcut::shortcut(KStandardShortcut::SubstringCompletion).contains(key)) { // TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch d->updateSearchString(); d->startSearches(); d->doCompletion(true); accept = true; } else if (KStandardShortcut::shortcut(KStandardShortcut::TextCompletion).contains(key)) { const int len = text().length(); if (len == cursorPosition()) { // at End? d->updateSearchString(); d->startSearches(); d->doCompletion(true); accept = true; } } const QString oldContent = text(); if (!accept) { KLineEdit::keyPressEvent(event); } // if the text didn't change (eg. because a cursor navigation key was pressed) // we don't need to trigger a new search if (oldContent == text()) { return; } if (event->isAccepted()) { d->updateSearchString(); QString searchString(d->searchString()); // LDAP does not know about our string manipulation, remove it if (d->searchExtended()) { searchString = d->searchString().mid(1); } } } QString AddresseeLineEditPrivate::searchString() const { return mSearchString; } void AddresseeLineEditPrivate::setSearchString(const QString &searchString) { mSearchString = searchString; } bool AddresseeLineEditPrivate::searchExtended() const { return mSearchExtended; } void AddresseeLineEditPrivate::setSearchExtended(bool searchExtended) { mSearchExtended = searchExtended; } void AddresseeLineEdit::callUserCancelled(const QString &str) { userCancelled(str); } void AddresseeLineEdit::callSetCompletedText(const QString &text, bool marked) { setCompletedText(text, marked); } void AddresseeLineEdit::callSetCompletedText(const QString &text) { setCompletedText(text); } void AddresseeLineEdit::callSetUserSelection(bool b) { setUserSelection(b); } void AddresseeLineEdit::mouseReleaseEvent(QMouseEvent *event) { // reimplemented from QLineEdit::mouseReleaseEvent() if (d->useCompletion() && QApplication::clipboard()->supportsSelection() && !isReadOnly() && event->button() == Qt::MiddleButton) { d->setSmartPaste(true); } KLineEdit::mouseReleaseEvent(event); d->setSmartPaste(false); } void AddresseeLineEditPrivate::updateSearchString() { mSearchString = q->text(); int n = -1; bool inQuote = false; const int searchStringLength = mSearchString.length(); for (int i = 0; i < searchStringLength; ++i) { const QChar searchChar = mSearchString.at(i); if (searchChar == QLatin1Char('"')) { inQuote = !inQuote; } if (searchChar == QLatin1Char('\\') && (i + 1) < searchStringLength && mSearchString.at(i + 1) == QLatin1Char('"')) { ++i; } if (inQuote) { continue; } if (i < searchStringLength && (searchChar == QLatin1Char(',') || (mUseSemicolonAsSeparator && searchChar == QLatin1Char(';')))) { n = i; } } if (n >= 0) { ++n; // Go past the "," const int len = mSearchString.length(); // Increment past any whitespace... while (n < len && mSearchString.at(n).isSpace()) { ++n; } mPreviousAddresses = mSearchString.left(n); mSearchString = mSearchString.mid(n).trimmed(); } else { mPreviousAddresses.clear(); } } void AddresseeLineEditPrivate::startSearches() { if (!mDelayedQueryTimer->isActive()) { mDelayedQueryTimer->start(50ms); } } void AddresseeLineEditPrivate::slotCompletion() { // Called by KLineEdit's keyPressEvent for CompletionModes // Auto,Popup -> new text, update search string. // not called for CompletionShell, this is been taken care of // in AddresseeLineEdit::keyPressEvent updateSearchString(); if (q->completionBox()) { q->completionBox()->setCancelledText(mSearchString); } startSearches(); doCompletion(false); } void AddresseeLineEditPrivate::slotTriggerDelayedQueries() { const QString strSearch = mSearchString.trimmed(); if (strSearch.size() <= 2) { return; } searchInGnupg(); } void AddresseeLineEditPrivate::searchInGnupg() { const auto keys = Kleo::KeyCache::instance()->keys(); const QString trimmedString = mSearchString.trimmed(); for (const auto &key : keys) { for (int i = 0, count = key.numUserIDs(); i < count; i++) { auto email = QString::fromLatin1(key.userID(i).email()); if (email.startsWith(u'<')) { email.remove(0, 1); } if (email.endsWith(u'>')) { email.chop(1); } const auto name = QLatin1StringView(key.userID(i).name()); if (email.contains(trimmedString) || name.contains(trimmedString)) { if (name.trimmed().isEmpty()) { addCompletionItem(email, 1, 0); } else { addCompletionItem(name + u" <"_s + email + u'>', 1, 0); } } } } } void AddresseeLineEdit::slotEditingFinished() { //const QList listJob = d->mightBeGroupJobs(); //for (KJob *job : listJob) { // disconnect(job); // job->deleteLater(); //} //d->mightBeGroupJobsClear(); //d->groupsClear(); //if (!text().trimmed().isEmpty() && enableAkonadiSearch()) { // const QStringList addresses = KEmailAddress::splitAddressList(text()); // for (const QString &address : addresses) { // auto job = new Akonadi::ContactGroupSearchJob(); // connect(job, &Akonadi::ContactGroupSearchJob::result, this, &AddresseeLineEdit::slotGroupSearchResult); // d->mightBeGroupJobsAdd(job); // job->setQuery(Akonadi::ContactGroupSearchJob::Name, address); // } //} } void AddresseeLineEdit::emitTextCompleted() { Q_EMIT textCompleted(); } #include "addresseelineedit.moc" diff --git a/client/editor/attachment/attachmentfromurljob.cpp b/client/editor/attachment/attachmentfromurljob.cpp index e049b7e..567497f 100644 --- a/client/editor/attachment/attachmentfromurljob.cpp +++ b/client/editor/attachment/attachmentfromurljob.cpp @@ -1,155 +1,153 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan Parts based on KMail code by various authors. SPDX-License-Identifier: LGPL-2.0-or-later */ #include "attachmentfromurljob.h" #include "editor_debug.h" #include #include #include #include #include #include #include using namespace MessageCore; class MessageCore::AttachmentFromUrlJob::AttachmentLoadJobPrivate { public: explicit AttachmentLoadJobPrivate(AttachmentFromUrlJob *qq); void slotRequestFinished(QNetworkReply *reply); AttachmentFromUrlJob *const q; QByteArray mData; }; AttachmentFromUrlJob::AttachmentLoadJobPrivate::AttachmentLoadJobPrivate(AttachmentFromUrlJob *qq) : q(qq) { } void AttachmentFromUrlJob::AttachmentLoadJobPrivate::slotRequestFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { // TODO this loses useful stuff from KIO, like detailed error descriptions, causes+solutions, // ... use UiDelegate somehow? q->setError(reply->error()); q->setErrorText(reply->errorString()); q->emitResult(); return; } mData = reply->readAll(); // Determine the MIME type and filename of the attachment. const QString mimeTypeName = reply->header(QNetworkRequest::ContentTypeHeader).toString(); qCDebug(EDITOR_LOG) << "Mimetype is" << mimeTypeName; QString fileName = q->url().fileName(); fileName.replace(QLatin1Char('\n'), QLatin1Char('_')); if (fileName.isEmpty()) { QMimeDatabase db; const auto mimeType = db.mimeTypeForName(mimeTypeName); if (mimeType.isValid()) { fileName = i18nc("a file called 'unknown.ext'", "unknown%1", mimeType.preferredSuffix()); } else { fileName = i18nc("a file called 'unknown'", "unknown"); } } // Create the AttachmentPart. Q_ASSERT(q->attachmentPart() == nullptr); // Not created before. AttachmentPart::Ptr part = AttachmentPart::Ptr(new AttachmentPart); - QUrlQuery query(q->url()); part->setMimeType(mimeTypeName.toLatin1()); part->setName(fileName); part->setFileName(fileName); part->setData(mData); part->setUrl(q->url()); q->setAttachmentPart(part); q->emitResult(); // Success. } AttachmentFromUrlJob::AttachmentFromUrlJob(const QUrl &url, QObject *parent) : AttachmentFromUrlBaseJob(url, parent) , d(new AttachmentLoadJobPrivate(this)) { } AttachmentFromUrlJob::~AttachmentFromUrlJob() = default; void AttachmentFromUrlJob::doStart() { if (!url().isValid()) { setError(KJob::UserDefinedError); setErrorText(i18n("\"%1\" not found. Please specify the full path.", url().toDisplayString())); emitResult(); return; } if (maximumAllowedSize() != -1 && url().isLocalFile()) { const qint64 size = QFileInfo(url().toLocalFile()).size(); if (size > maximumAllowedSize()) { setError(KJob::UserDefinedError); KFormat format; setErrorText(i18n("You may not attach files bigger than %1. Share it with storage service.", format.formatByteSize(maximumAllowedSize()))); emitResult(); return; } } Q_ASSERT(d->mData.isEmpty()); // Not started twice. if (url().scheme() == QLatin1StringView("https") || url().scheme() == QLatin1StringView("http")) { auto qnam = new QNetworkAccessManager(this); QNetworkRequest request(url()); auto reply = qnam->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { d->slotRequestFinished(reply); }); } else if (url().scheme() == QLatin1StringView("file")) { QFile file(url().toLocalFile()); if (!file.open(QIODeviceBase::ReadOnly)) { setError(KJob::UserDefinedError); setErrorText(i18n("Could not read file \"%1\".", url().toDisplayString())); emitResult(); return; } QString fileName = url().fileName(); QMimeDatabase db; const auto mimeType = db.mimeTypeForFile(url().toLocalFile()); if (mimeType.isValid()) { fileName = i18nc("a file called 'unknown.ext'", "unknown%1", mimeType.preferredSuffix()); } else { fileName = i18nc("a file called 'unknown'", "unknown"); } // Create the AttachmentPart. Q_ASSERT(attachmentPart() == nullptr); // Not created before. AttachmentPart::Ptr part = AttachmentPart::Ptr(new AttachmentPart); - QUrlQuery query(url()); part->setMimeType(mimeType.name().toUtf8()); part->setName(fileName); part->setFileName(fileName); part->setData(file.readAll()); part->setUrl(url()); setAttachmentPart(part); emitResult(); // Success. return; } setError(KJob::UserDefinedError); setErrorText(i18n("scheme not supported \"%1\".", url().scheme())); emitResult(); } #include "moc_attachmentfromurljob.cpp" diff --git a/client/editor/mailtemplates.cpp b/client/editor/mailtemplates.cpp index 1d99c45..c124fcb 100644 --- a/client/editor/mailtemplates.cpp +++ b/client/editor/mailtemplates.cpp @@ -1,932 +1,935 @@ /* Copyright (c) 2009 Constantin Berzan Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Leo Franchi Copyright (c) 2017 Christian Mollekopf 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. */ #include "mailtemplates.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Qt::Literals::StringLiterals; QDebug operator<<(QDebug dbg, const KMime::Types::Mailbox &mb) { dbg << mb.addrSpec().asString(); return dbg; } namespace KMime { namespace Types { static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right) { return (left.asString() == right.asString()); } static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right) { return (left.addrSpec().asString() == right.addrSpec().asString()); } } Message* contentToMessage(Content* content) { content->assemble(); const auto encoded = content->encodedContent(); auto message = new Message(); message->setContent(encoded); message->parse(); return message; } } static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me) { KMime::Types::Mailbox::List addresses(list); for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) { if (me.contains(it->addrSpec())) { it = addresses.erase(it); } else { ++it; } } return addresses; } static QString toPlainText(const QString &s) { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, const QString &newPrefix) { // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); QRegularExpression rx(bigRegExp, QRegularExpression::CaseInsensitiveOption); if (!rx.isValid()) { qWarning() << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; qWarning() << "Error: " << rx.errorString() << rx; Q_ASSERT(false); return str; } QString tmp = str; //We expect a match at the beginning of the string QRegularExpressionMatch match; if (tmp.indexOf(rx, 0, &match) == 0) { return tmp.replace(0, match.capturedLength(), newPrefix + QLatin1Char(' ')); } //No match, we just prefix the newPrefix return newPrefix + u' ' + str; } const QStringList getForwardPrefixes() { //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; //We want to be able to potentially reply to a variety of languages, so only translating is not enough list << i18nc("FWD abbreviation for forwarded in emails", "fwd"); list << u"fwd"_s; list << u"fw"_s; list << u"wg"_s; list << u"vs"_s; list << u"tr"_s; list << u"rv"_s; list << u"enc"_s; return list; } static QString forwardSubject(const QString &s) { //The standandard prefix const QString localPrefix = QStringLiteral("FW:"); QStringList forwardPrefixes; for (const auto &prefix : getForwardPrefixes()) { forwardPrefixes << prefix + QStringLiteral("\\s*:"); } return replacePrefixes(s, forwardPrefixes, localPrefix); } static QStringList getReplyPrefixes() { //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; //We want to be able to potentially reply to a variety of languages, so only translating is not enough list << i18nc("RE abbreviation for reply in emails", "re"); list << u"re"_s; list << u"aw"_s; list << u"sv"_s; list << u"antw"_s; list << u"ref"_s; return list; } static QString replySubject(const QString &s) { //The standandard prefix (latin for "in re", in matter of) const QString localPrefix = QStringLiteral("RE:"); QStringList replyPrefixes; for (const auto &prefix : getReplyPrefixes()) { replyPrefixes << prefix + u"\\s*:"_s; replyPrefixes << prefix + u"\\[.+\\]:"_s; replyPrefixes << prefix + u"\\d+:"_s; } return replacePrefixes(s, replyPrefixes, localPrefix); } static QByteArray getRefStr(const QByteArray &references, const QByteArray &messageId) { QByteArray firstRef, lastRef, refStr{references.trimmed()}, retRefStr; int i, j; if (refStr.isEmpty()) { return messageId; } i = refStr.indexOf('<'); j = refStr.indexOf('>'); firstRef = refStr.mid(i, j - i + 1); if (!firstRef.isEmpty()) { retRefStr = firstRef + ' '; } i = refStr.lastIndexOf('<'); j = refStr.lastIndexOf('>'); lastRef = refStr.mid(i, j - i + 1); if (!lastRef.isEmpty() && lastRef != firstRef) { retRefStr += lastRef + ' '; } retRefStr += messageId; return retRefStr; } KMime::Content *createPlainPartContent(const QString &plainBody, KMime::Content *parent = nullptr) { KMime::Content *textPart = new KMime::Content(parent); textPart->contentType()->setMimeType("text/plain"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, plainBody); // textPart->contentType()->setCharset(charset->name()); textPart->contentType()->setCharset("utf-8"); textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); textPart->fromUnicodeString(plainBody); return textPart; } KMime::Content *createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody, KMime::Message *parent = nullptr) { KMime::Content *multipartAlternative = new KMime::Content(parent); multipartAlternative->contentType()->setMimeType("multipart/alternative"); multipartAlternative->contentType()->setBoundary(KMime::multiPartBoundary()); KMime::Content *textPart = createPlainPartContent(plainBody, multipartAlternative); multipartAlternative->appendContent(textPart); KMime::Content *htmlPart = new KMime::Content(multipartAlternative); htmlPart->contentType()->setMimeType("text/html"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, htmlBody); // htmlPart->contentType()->setCharset(charset->name()); htmlPart->contentType()->setCharset("utf-8"); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); multipartAlternative->appendContent(htmlPart); return multipartAlternative; } KMime::Content *createMultipartMixedContent(QVector contents) { KMime::Content *multiPartMixed = new KMime::Content(); multiPartMixed->contentType()->setMimeType("multipart/mixed"); multiPartMixed->contentType()->setBoundary(KMime::multiPartBoundary()); for (const auto &content : contents) { multiPartMixed->appendContent(content); } return multiPartMixed; } QString plainToHtml(const QString &body) { QString str = body; str = str.toHtmlEscaped(); str.replace(u'\n', u"
\n"_s); return str; } //TODO implement this function using a DOM tree parser void makeValidHtml(QString &body, const QString &headElement) { QRegularExpression regEx(u""_s, QRegularExpression::InvertedGreedinessOption); if (!body.isEmpty() && !body.contains(regEx)) { regEx.setPattern(u""_s); if (!body.contains(regEx)) { body = u""_s + body + u"
"_s; } regEx.setPattern(u""_s); if (!body.contains(regEx)) { body = u""_s + headElement + u""_s + body; } body = u""_s + body + u""_s; } } //FIXME strip signature works partially for HTML mails static QString stripSignature(const QString &msg) { // Following RFC 3676, only > before -- // I prefer to not delete a SB instead of delete good mail content. // We expect no CRLF from the ObjectTreeParser. The regex won't handle it. if (msg.contains(QStringLiteral("\r\n"))) { qWarning() << "Message contains CRLF, but shouldn't: " << msg; Q_ASSERT(false); } static const QRegularExpression sbDelimiterSearch(u"(^|\n)[> ]*-- \n"_s); // The regular expression to look for prefix change static const QRegularExpression commonReplySearch(u"^[ ]*>"_s); QString res = msg; int posDeletingStart = 1; // to start looking at 0 // While there are SB delimiters (start looking just before the deleted SB) while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { QString prefix; // the current prefix QString line; // the line to check if is part of the SB int posNewLine = -1; // Look for the SB beginning int posSignatureBlock = res.indexOf(u'-', posDeletingStart); // The prefix before "-- "$ if (res.at(posDeletingStart) == u'\n') { ++posDeletingStart; } prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; // now go to the end of the SB while (posNewLine < res.size() && posNewLine > 0) { // handle the undefined case for mid ( x , -n ) where n>1 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); if (nextPosNewLine < 0) { nextPosNewLine = posNewLine - 1; } line = res.mid(posNewLine, nextPosNewLine - posNewLine); // check when the SB ends: // * does not starts with prefix or // * starts with prefix+(any substring of prefix) if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; } else { break; // end of the SB } } // remove the SB or truncate when is the last SB if (posNewLine > 0) { res.remove(posDeletingStart, posNewLine - posDeletingStart); } else { res.truncate(posDeletingStart); } } return res; } static void plainMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { const auto result = plainTextContent.isEmpty() ? toPlainText(htmlContent) : plainTextContent; callback(result); } void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { QString htmlElement = htmlContent; if (htmlElement.isEmpty()) { //plain mails only QString htmlReplace = plainTextContent.toHtmlEscaped(); htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("
")); htmlElement = QStringLiteral("%1\n").arg(htmlReplace); } QDomDocument document; document.setContent(htmlElement); QString body; QTextStream bodyStream(&body); QString head; QTextStream headStream(&head); const auto bodies = document.elementsByTagName(u"body"_s); const auto heads = document.elementsByTagName(u"head"_s); if (bodies.isEmpty()) { body = htmlElement; } else { bodies.item(0).save(bodyStream, 2); } if (!heads.isEmpty()) { heads.item(0).save(headStream, 2); } callback(body, head); } QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) { QString result; if (wildString.isEmpty()) { return wildString; } unsigned int strLength(wildString.length()); for (uint i = 0; i < strLength;) { QChar ch = wildString[i++]; if (ch == QLatin1Char('%') && i < strLength) { ch = wildString[i++]; switch (ch.toLatin1()) { case 'f': { // sender's initals if (fromDisplayString.isEmpty()) { break; } uint j = 0; const unsigned int strLength(fromDisplayString.length()); for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) ; for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) ; result += fromDisplayString[0]; if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) { result += fromDisplayString[j]; } else if (strLength > 1) { if (fromDisplayString[1] > QLatin1Char(' ')) { result += fromDisplayString[1]; } } } break; case '_': result += QLatin1Char(' '); break; case '%': result += QLatin1Char('%'); break; default: result += QLatin1Char('%'); result += ch; break; } } else { result += ch; } } return result; } QString quotedPlainText(const QString &selection, const QString &fromDisplayString) { QString content = selection; // Remove blank lines at the beginning: const int firstNonWS = content.indexOf(QRegularExpression(u"\\S"_s)); const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS); if (lineStart >= 0) { content.remove(0, static_cast(lineStart)); } const auto quoteString = QStringLiteral("> "); const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString); //FIXME // if (TemplateParserSettings::self()->smartQuote() && mWrap) { // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); // } content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); content.prepend(indentStr); content += QLatin1Char('\n'); return content; } QString quotedHtmlText(const QString &selection) { QString content = selection; //TODO 1) look for all the variations of
and remove the blank lines //2) implement vertical bar for quoted HTML mail. //3) After vertical bar is implemented, If a user wants to edit quoted message, // then the
tags below should open and close as when required. //Add blockquote tag, so that quoted message can be differentiated from normal message content = QLatin1String("
") + content + QLatin1String("
"); return content; } enum ReplyStrategy { ReplyList, ReplySmart, ReplyAll, ReplyAuthor, ReplyNone }; static QByteArray as7BitString(const KMime::Headers::Base *h) { if (h) { return h->as7BitString(false); } return {}; } static QString asUnicodeString(const KMime::Headers::Base *h) { if (h) { return h->asUnicodeString(); } return {}; } static KMime::Types::Mailbox::List getMailingListAddresses(const KMime::Headers::Base *listPostHeader) { KMime::Types::Mailbox::List mailingListAddresses; const QString listPost = asUnicodeString(listPostHeader); if (listPost.contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { static QRegularExpression rx(QStringLiteral("]+)@([^>]+)>"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch match; if (listPost.indexOf(rx, 0, &match) != -1) { // matched KMime::Types::Mailbox mailbox; mailbox.fromUnicodeString(match.captured(1) + u'@' + match.captured(2)); mailingListAddresses << mailbox; } } return mailingListAddresses; } struct RecipientMailboxes { KMime::Types::Mailbox::List to; KMime::Types::Mailbox::List cc; }; static RecipientMailboxes getRecipients(const KMime::Types::Mailbox::List &from, const KMime::Types::Mailbox::List &to, const KMime::Types::Mailbox::List &cc, const KMime::Types::Mailbox::List &replyToList, const KMime::Types::Mailbox::List &mailingListAddresses, const KMime::Types::AddrSpecList &me) { KMime::Types::Mailbox::List toList; KMime::Types::Mailbox::List ccList; auto listContainsMe = [&] (const KMime::Types::Mailbox::List &list) { for (const auto &m : me) { KMime::Types::Mailbox mailbox; mailbox.setAddress(m); if (list.contains(mailbox)) { return true; } } return false; }; if (listContainsMe(from)) { // sender seems to be one of our own identities, so we assume that this // is a reply to a "sent" mail where the users wants to add additional // information for the recipient. return {to, cc}; } KMime::Types::Mailbox::List recipients; KMime::Types::Mailbox::List ccRecipients; // add addresses from the Reply-To header to the list of recipients if (!replyToList.isEmpty()) { recipients = replyToList; // strip all possible mailing list addresses from the list of Reply-To addresses for (const KMime::Types::Mailbox &mailbox : std::as_const(mailingListAddresses)) { for (const KMime::Types::Mailbox &recipient : std::as_const(recipients)) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } } if (!mailingListAddresses.isEmpty()) { // this is a mailing list message if (recipients.isEmpty() && !from.isEmpty()) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += from; qDebug() << "Added" << from << "to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend(mailingListAddresses[0]); } else { // this is a normal message if (recipients.isEmpty() && !from.isEmpty()) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += from; qDebug() << "Added" << from << "to the list of recipients"; } } // strip all my addresses from the list of recipients toList = stripMyAddressesFromAddressList(recipients, me); // merge To header and CC header into a list of CC recipients auto appendToCcRecipients = [&](const KMime::Types::Mailbox::List & list) { for (const KMime::Types::Mailbox &mailbox : list) { if (!recipients.contains(mailbox) && !ccRecipients.contains(mailbox)) { ccRecipients += mailbox; qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients"; } } }; appendToCcRecipients(to); appendToCcRecipients(cc); if (!ccRecipients.isEmpty()) { // strip all my addresses from the list of CC recipients ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me); // in case of a reply to self, toList might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if (toList.isEmpty() && !ccRecipients.isEmpty()) { toList << ccRecipients.at(0); ccRecipients.pop_front(); } ccList = ccRecipients; } if (toList.isEmpty() && !recipients.isEmpty()) { // reply to self without other recipients toList << recipients.at(0); } return {toList, ccList}; } void MailTemplates::reply(const KMime::Message::Ptr &origMsg, const std::function &callback, const KMime::Types::AddrSpecList &me) { //FIXME const bool alwaysPlain = true; // Decrypt what we have to MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); auto partList = otp.collectContentParts(); if (partList.isEmpty()) { Q_ASSERT(false); return; } auto part = partList[0]; Q_ASSERT(part); // Prepare the reply message KMime::Message::Ptr msg(new KMime::Message); msg->removeHeader(); msg->removeHeader(); msg->contentType(true)->setMimeType("text/plain"); msg->contentType()->setCharset("utf-8"); auto getMailboxes = [](const KMime::Headers::Base *h) -> KMime::Types::Mailbox::List { if (h) { return static_cast(h)->mailboxes(); } return {}; }; auto fromHeader = static_cast(part->header(KMime::Headers::From::staticType())); const auto recipients = getRecipients( fromHeader ? fromHeader->mailboxes() : KMime::Types::Mailbox::List{}, getMailboxes(part->header(KMime::Headers::To::staticType())), getMailboxes(part->header(KMime::Headers::Cc::staticType())), getMailboxes(part->header(KMime::Headers::ReplyTo::staticType())), getMailingListAddresses(part->header("List-Post")), me ); for (const auto &mailbox : recipients.to) { msg->to()->addAddress(mailbox); } for (const auto &mailbox : recipients.cc) { msg->cc(true)->addAddress(mailbox); } const auto messageId = as7BitString(part->header(KMime::Headers::MessageID::staticType())); const QByteArray refStr = getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), messageId); if (!refStr.isEmpty()) { msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } //In-Reply-To = original msg-id msg->inReplyTo()->from7BitString(messageId); const auto subjectHeader = part->header(KMime::Headers::Subject::staticType()); msg->subject()->fromUnicodeString(replySubject(asUnicodeString(subjectHeader)), "utf-8"); auto definedLocale = QLocale::system(); //Add quoted body QString plainBody; QString htmlBody; //On $datetime you wrote: auto dateHeader = static_cast(part->header(KMime::Headers::Date::staticType())); const QDateTime date = dateHeader ? dateHeader->dateTime() : QDateTime{}; - const auto dateTimeString = QStringLiteral("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat)); + const auto dateTimeString = QStringLiteral("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat), definedLocale.toString(date.time(), QLocale::LongFormat)); const auto onDateYouWroteLine = i18nc("Reply header", "On %1 you wrote:\n", dateTimeString); plainBody.append(onDateYouWroteLine); htmlBody.append(plainToHtml(onDateYouWroteLine)); const auto plainTextContent = otp.plainTextContent(); const auto htmlContent = otp.htmlContent(); plainMessageText(plainTextContent, htmlContent, [=] (const QString &body) { QString result = stripSignature(body); //Quoted body result = quotedPlainText(result, fromHeader ? fromHeader->displayString() : QString{}); if (result.endsWith(u'\n')) { result.chop(1); } //The plain body is complete - auto plainBodyResult = plainBody + result; + QString plainBodyResult = plainBody + result; htmlMessageText(plainTextContent, htmlContent, [=] (const QString &body, const QString &headElement) { QString result = stripSignature(body); //The html body is complete const auto htmlBodyResult = [&]() { if (!alwaysPlain) { QString htmlBodyResult = htmlBody + quotedHtmlText(result); makeValidHtml(htmlBodyResult, headElement); return htmlBodyResult; } return QString{}; }(); //Assemble the message msg->contentType()->clear(); // to get rid of old boundary KMime::Content *const mainTextPart = htmlBodyResult.isEmpty() ? createPlainPartContent(plainBodyResult, msg.data()) : createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data()); mainTextPart->assemble(); msg->setBody(mainTextPart->encodedBody()); msg->setHeader(mainTextPart->contentType()); msg->setHeader(mainTextPart->contentTransferEncoding()); //FIXME this does more harm than good right now. msg->assemble(); callback(msg); }); }); } void MailTemplates::forward(const KMime::Message::Ptr &origMsg, const std::function &callback) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); KMime::Message::Ptr wrapperMsg(new KMime::Message); wrapperMsg->to()->clear(); wrapperMsg->cc()->clear(); // Decrypt the original message, it will be encrypted again in the composer // for the right recipient KMime::Message::Ptr forwardedMessage(new KMime::Message()); if (isEncrypted(origMsg.data())) { qDebug() << "Original message was encrypted, decrypting it"; auto htmlContent = otp.htmlContent(); KMime::Content *recreatedMsg = htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) : createMultipartAlternativeContent(otp.plainTextContent(), htmlContent); KMime::Message::Ptr tmpForwardedMessage; auto attachments = otp.collectAttachmentParts(); if (!attachments.isEmpty()) { QVector contents = {recreatedMsg}; - for (const auto &attachment : attachments) { + for (const auto &attachment : std::as_const(attachments)) { //Copy the node, to avoid deleting the parts node. auto c = new KMime::Content; c->setContent(attachment->node()->encodedContent()); c->parse(); contents.append(c); } auto msg = createMultipartMixedContent(contents); tmpForwardedMessage.reset(KMime::contentToMessage(msg)); } else { tmpForwardedMessage.reset(KMime::contentToMessage(recreatedMsg)); } origMsg->contentType()->fromUnicodeString(tmpForwardedMessage->contentType()->asUnicodeString(), "utf-8"); origMsg->assemble(); forwardedMessage->setHead(origMsg->head()); forwardedMessage->setBody(tmpForwardedMessage->encodedBody()); forwardedMessage->parse(); } else { qDebug() << "Original message was not encrypted, using it as-is"; forwardedMessage = origMsg; } auto partList = otp.collectContentParts(); if (partList.isEmpty()) { Q_ASSERT(false); callback({}); return; } auto part = partList[0]; Q_ASSERT(part); const auto subjectHeader = part->header(KMime::Headers::Subject::staticType()); const auto subject = asUnicodeString(subjectHeader); const QByteArray refStr = getRefStr( as7BitString(part->header(KMime::Headers::References::staticType())), as7BitString(part->header(KMime::Headers::MessageID::staticType())) ); wrapperMsg->subject()->fromUnicodeString(forwardSubject(subject), "utf-8"); if (!refStr.isEmpty()) { wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } KMime::Content *fwdAttachment = new KMime::Content; fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline); fwdAttachment->contentType()->setMimeType("message/rfc822"); fwdAttachment->contentDisposition()->setFilename(subject + u".eml"_s); fwdAttachment->setBody(KMime::CRLFtoLF(forwardedMessage->encodedContent(false))); wrapperMsg->appendContent(fwdAttachment); wrapperMsg->assemble(); callback(wrapperMsg); } QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); otp.decryptAndVerify(); const auto plain = otp.plainTextContent(); if (plain.isEmpty()) { //Maybe not as good as the webengine version, but works at least for simple html content return toPlainText(otp.htmlContent()); } return plain; } QString MailTemplates::body(const KMime::Message::Ptr &msg, bool &isHtml) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); otp.decryptAndVerify(); const auto html = otp.htmlContent(); if (html.isEmpty()) { isHtml = false; return otp.plainTextContent(); } isHtml = true; return html; } static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list) { KMime::Types::Mailbox::List mailboxes; for (const auto &s : list) { KMime::Types::Mailbox mb; mb.fromUnicodeString(s); if (mb.hasAddress()) { mailboxes << mb; } else { qWarning() << "Got an invalid address: " << s << list; Q_ASSERT(false); } } return mailboxes; } static void setRecipients(KMime::Message &message, const Recipients &recipients) { message.to(true)->clear(); - for (const auto &mb : stringListToMailboxes(recipients.to)) { + const auto tos = stringListToMailboxes(recipients.to); + for (const auto &mb : tos) { message.to()->addAddress(mb); } message.cc(true)->clear(); - for (const auto &mb : stringListToMailboxes(recipients.cc)) { + const auto ccs = stringListToMailboxes(recipients.cc); + for (const auto &mb : ccs) { message.cc()->addAddress(mb); } message.bcc(true)->clear(); - for (const auto &mb : stringListToMailboxes(recipients.bcc)) { + const auto bccs = stringListToMailboxes(recipients.bcc); + for (const auto &mb : bccs) { message.bcc()->addAddress(mb); } } KMime::Message::Ptr MailTemplates::createIMipMessage( const QString &from, const Recipients &recipients, const QString &subject, const QString &body, const QString &attachment) { KMime::Message::Ptr message = KMime::Message::Ptr( new KMime::Message ); message->contentTransferEncoding()->clear(); // 7Bit, decoded. // Set the headers message->userAgent()->fromUnicodeString(QStringLiteral("%1/%2(%3)").arg(QString::fromLocal8Bit("GPGOL.js")).arg(u"0.1"_s).arg(QSysInfo::prettyProductName()), "utf-8"); message->from()->fromUnicodeString(from, "utf-8"); setRecipients(*message, recipients); message->date()->setDateTime(QDateTime::currentDateTime()); message->subject()->fromUnicodeString(subject, "utf-8"); message->contentType()->setMimeType("multipart/alternative"); message->contentType()->setBoundary(KMime::multiPartBoundary()); // Set the first multipart, the body message. KMime::Content *bodyMessage = new KMime::Content{message.data()}; bodyMessage->contentType()->setMimeType("text/plain"); bodyMessage->contentType()->setCharset("utf-8"); bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); bodyMessage->setBody(KMime::CRLFtoLF(body.toUtf8())); message->appendContent(bodyMessage); // Set the second multipart, the attachment. KMime::Content *attachMessage = new KMime::Content{message.data()}; attachMessage->contentDisposition()->setDisposition(KMime::Headers::CDattachment); attachMessage->contentType()->setMimeType("text/calendar"); attachMessage->contentType()->setCharset("utf-8"); attachMessage->contentType()->setName(QLatin1String("event.ics"), "utf-8"); attachMessage->contentType()->setParameter(QLatin1String("method"), QLatin1String("REPLY")); attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); attachMessage->setBody(KMime::CRLFtoLF(attachment.toUtf8())); message->appendContent(attachMessage); // Job done, attach the both multiparts and assemble the message. message->assemble(); return message; } diff --git a/client/ews/ewsid.cpp b/client/ews/ewsid.cpp index ffcf9f0..7e413e5 100644 --- a/client/ews/ewsid.cpp +++ b/client/ews/ewsid.cpp @@ -1,166 +1,165 @@ /* SPDX-FileCopyrightText: 2015-2017 Krzysztof Nowicki SPDX-License-Identifier: LGPL-2.0-or-later */ #include "ewsid.h" #include #include #include - -#include "ewsclient_debug.h" - -static const QString distinguishedIdNames[] = { - QStringLiteral("calendar"), - QStringLiteral("contacts"), - QStringLiteral("deleteditems"), - QStringLiteral("drafts"), - QStringLiteral("inbox"), - QStringLiteral("journal"), - QStringLiteral("notes"), - QStringLiteral("outbox"), - QStringLiteral("sentitems"), - QStringLiteral("tasks"), - QStringLiteral("msgfolderroot"), - QStringLiteral("root"), - QStringLiteral("junkemail"), - QStringLiteral("searchfolders"), - QStringLiteral("voicemail"), - QStringLiteral("recoverableitemsroot"), - QStringLiteral("recoverableitemsdeletions"), - QStringLiteral("recoverableitemsversions"), - QStringLiteral("recoverableitemspurges"), - QStringLiteral("archiveroot"), - QStringLiteral("archivemsgfolderroot"), - QStringLiteral("archivedeleteditems"), - QStringLiteral("archiverecoverableitemsroot"), - QStringLiteral("archiverecoverableitemsdeletions"), - QStringLiteral("archiverecoverableitemsversions"), - QStringLiteral("archiverecoverableitemspurges"), -}; +#include + +static constexpr auto distinguishedIdNames = std::to_array({ + QLatin1StringView("calendar"), + QLatin1StringView("contacts"), + QLatin1StringView("deleteditems"), + QLatin1StringView("drafts"), + QLatin1StringView("inbox"), + QLatin1StringView("journal"), + QLatin1StringView("notes"), + QLatin1StringView("outbox"), + QLatin1StringView("sentitems"), + QLatin1StringView("tasks"), + QLatin1StringView("msgfolderroot"), + QLatin1StringView("root"), + QLatin1StringView("junkemail"), + QLatin1StringView("searchfolders"), + QLatin1StringView("voicemail"), + QLatin1StringView("recoverableitemsroot"), + QLatin1StringView("recoverableitemsdeletions"), + QLatin1StringView("recoverableitemsversions"), + QLatin1StringView("recoverableitemspurges"), + QLatin1StringView("archiveroot"), + QLatin1StringView("archivemsgfolderroot"), + QLatin1StringView("archivedeleteditems"), + QLatin1StringView("archiverecoverableitemsroot"), + QLatin1StringView("archiverecoverableitemsdeletions"), + QLatin1StringView("archiverecoverableitemsversions"), + QLatin1StringView("archiverecoverableitemspurges"), +}); EwsId::EwsId(QXmlStreamReader &reader) : mDid(EwsDIdCalendar) { // Don't check for this element's name as a folder id may be contained in several elements // such as "FolderId" or "ParentFolderId". const QXmlStreamAttributes &attrs = reader.attributes(); QStringView idRef = attrs.value(QStringLiteral("Id")); QStringView changeKeyRef = attrs.value(QStringLiteral("ChangeKey")); if (idRef.isNull()) { return; } mId = idRef.toString(); if (!changeKeyRef.isNull()) { mChangeKey = changeKeyRef.toString(); } mType = Real; } EwsId::EwsId(const QString &id, const QString &changeKey) : mType(Real) , mId(id) , mChangeKey(changeKey) , mDid(EwsDIdCalendar) { } EwsId &EwsId::operator=(const EwsId &other) { mType = other.mType; if (mType == Distinguished) { mDid = other.mDid; } else if (mType == Real) { mId = other.mId; mChangeKey = other.mChangeKey; } return *this; } EwsId &EwsId::operator=(EwsId &&other) { mType = other.mType; if (mType == Distinguished) { mDid = other.mDid; } else if (mType == Real) { mId = std::move(other.mId); mChangeKey = std::move(other.mChangeKey); } return *this; } bool EwsId::operator==(const EwsId &other) const { if (mType != other.mType) { return false; } if (mType == Distinguished) { return mDid == other.mDid; } else if (mType == Real) { return mId == other.mId && mChangeKey == other.mChangeKey; } return true; } bool EwsId::operator<(const EwsId &other) const { if (mType != other.mType) { return mType < other.mType; } if (mType == Distinguished) { return mDid < other.mDid; } else if (mType == Real) { return mId < other.mId && mChangeKey < other.mChangeKey; } return false; } void EwsId::writeFolderIds(QXmlStreamWriter &writer) const { if (mType == Distinguished) { writer.writeStartElement(ewsTypeNsUri, QStringLiteral("DistinguishedFolderId")); writer.writeAttribute(QStringLiteral("Id"), distinguishedIdNames[mDid]); writer.writeEndElement(); } else if (mType == Real) { writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FolderId")); writer.writeAttribute(QStringLiteral("Id"), mId); if (!mChangeKey.isEmpty()) { writer.writeAttribute(QStringLiteral("ChangeKey"), mChangeKey); } writer.writeEndElement(); } } void EwsId::writeItemIds(QXmlStreamWriter &writer) const { if (mType == Real) { writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ItemId")); writer.writeAttribute(QStringLiteral("Id"), mId); if (!mChangeKey.isEmpty()) { writer.writeAttribute(QStringLiteral("ChangeKey"), mChangeKey); } writer.writeEndElement(); } } void EwsId::writeAttributes(QXmlStreamWriter &writer) const { if (mType == Real) { writer.writeAttribute(QStringLiteral("Id"), mId); if (!mChangeKey.isEmpty()) { writer.writeAttribute(QStringLiteral("ChangeKey"), mChangeKey); } } } uint qHash(const EwsId &id, uint seed) { return qHash(id.id(), seed) ^ qHash(id.changeKey(), seed) ^ static_cast(id.type()); } diff --git a/client/ews/ewsxml.cpp b/client/ews/ewsxml.cpp index 7bd9d32..7b77e79 100644 --- a/client/ews/ewsxml.cpp +++ b/client/ews/ewsxml.cpp @@ -1,330 +1,335 @@ /* SPDX-FileCopyrightText: 2015-2017 Krzysztof Nowicki SPDX-License-Identifier: LGPL-2.0-or-later */ #include "ewsxml.h" #include #include "ewsid.h" #include "ewsitem.h" -static const QList messageSensitivityNames = { - QStringLiteral("Normal"), - QStringLiteral("Personal"), - QStringLiteral("Private"), - QStringLiteral("Confidential"), -}; - -static const QList messageImportanceNames = {QStringLiteral("Low"), QStringLiteral("Normal"), QStringLiteral("High")}; - -static const QList calendarItemTypeNames = { - QStringLiteral("Single"), - QStringLiteral("Occurrence"), - QStringLiteral("Exception"), - QStringLiteral("RecurringMaster"), -}; - -static const QList legacyFreeBusyStatusNames = { - QStringLiteral("Free"), - QStringLiteral("Tentative"), - QStringLiteral("Busy"), - QStringLiteral("OOF"), - QStringLiteral("NoData"), -}; - -static const QList responseTypeNames = { - QStringLiteral("Unknown"), - QStringLiteral("Organizer"), - QStringLiteral("Tentative"), - QStringLiteral("Accept"), - QStringLiteral("Decline"), - QStringLiteral("NoResponseReceived"), -}; +static constexpr auto messageSensitivityNames = std::to_array({ + QLatin1StringView("Normal"), + QLatin1StringView("Personal"), + QLatin1StringView("Private"), + QLatin1StringView("Confidential"), +}); + +static constexpr auto messageImportanceNames = std::to_array({ + QLatin1StringView("Low"), + QLatin1StringView("Normal"), + QLatin1StringView("High"), +}); + +static constexpr auto calendarItemTypeNames = std::to_array({ + QLatin1StringView("Single"), + QLatin1StringView("Occurrence"), + QLatin1StringView("Exception"), + QLatin1StringView("RecurringMaster"), +}); + +static constexpr auto legacyFreeBusyStatusNames = std::to_array({ + QLatin1StringView("Free"), + QLatin1StringView("Tentative"), + QLatin1StringView("Busy"), + QLatin1StringView("OOF"), + QLatin1StringView("NoData"), +}); + +static constexpr auto responseTypeNames = std::to_array({ + QLatin1StringView("Unknown"), + QLatin1StringView("Organizer"), + QLatin1StringView("Tentative"), + QLatin1StringView("Accept"), + QLatin1StringView("Decline"), + QLatin1StringView("NoResponseReceived"), +}); bool ewsXmlBoolReader(QXmlStreamReader &reader, QVariant &val) { const QString elmText = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading %1 element").arg(reader.name().toString()); reader.skipCurrentElement(); return false; } if (elmText == QLatin1String("true")) { val = true; } else if (elmText == QLatin1String("false")) { val = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected invalid boolean value in %1 element:").arg(reader.name().toString()) << elmText; return false; } return true; } bool ewsXmlBoolWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(val.toBool() ? QStringLiteral("true") : QStringLiteral("false")); return true; } bool ewsXmlBase64Reader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); val = QByteArray::fromBase64(reader.readElementText().toLatin1()); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } return true; } bool ewsXmlBase64Writer(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(QString::fromLatin1(val.toByteArray().toBase64())); return true; } bool ewsXmlIdReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); EwsId id = EwsId(reader); if (id.type() == EwsId::Unspecified) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(id); reader.skipCurrentElement(); return true; } bool ewsXmlIdWriter(QXmlStreamWriter &writer, const QVariant &val) { EwsId id = val.value(); if (id.type() == EwsId::Unspecified) { return false; } id.writeAttributes(writer); return true; } bool ewsXmlTextReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); val = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } return true; } bool ewsXmlTextWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(val.toString()); return true; } bool ewsXmlUIntReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); bool ok; val = reader.readElementText().toUInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); return false; } return true; } bool ewsXmlUIntWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(QString::number(val.toUInt())); return true; } bool ewsXmlDateTimeReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); QDateTime dt = QDateTime::fromString(reader.readElementText(), Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !dt.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); return false; } val = QVariant::fromValue(dt); return true; } bool ewsXmlItemReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); EwsItem item = EwsItem(reader); if (!item.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(item); return true; } -bool ewsXmlEnumReader(QXmlStreamReader &reader, QVariant &val, const QList &items) +template +bool ewsXmlEnumReader(QXmlStreamReader &reader, QVariant &val, const std::array &items) { const QString elmName = reader.name().toString(); QString text = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } int i = 0; - QList::const_iterator it; - for (it = items.cbegin(); it != items.cend(); ++it, i++) { + auto it = items.cbegin();; + for (; it != items.cend(); ++it, i++) { if (text == *it) { val = i; break; } } if (it == items.cend()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown value %2.").arg(elmName, text); return false; } return true; } bool ewsXmlSensitivityReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, messageSensitivityNames); } bool ewsXmlImportanceReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, messageImportanceNames); } bool ewsXmlCalendarItemTypeReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, calendarItemTypeNames); } bool ewsXmlLegacyFreeBusyStatusReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, legacyFreeBusyStatusNames); } bool ewsXmlResponseTypeReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, responseTypeNames); } template<> QString readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { ok = true; const QStringView elmName = reader.name(); QString val = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); reader.skipCurrentElement(); val.clear(); ok = false; } return val; } template<> int readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); int val = 0; if (ok) { val = valStr.toInt(&ok); if (!ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); } } return val; } template<> long readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); long val = 0; if (ok) { val = valStr.toLong(&ok); if (!ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); } } return val; } template<> QDateTime readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); QDateTime val; if (ok) { val = QDateTime::fromString(valStr, Qt::ISODate); if (!val.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); ok = false; } } return val; } template<> bool readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); bool val = false; if (ok) { if (valStr == QLatin1String("true")) { val = true; } else if (valStr == QLatin1String("false")) { val = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); ok = false; } } return val; } template<> QByteArray readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QString valStr = readXmlElementValue(reader, ok, parentElement); QByteArray val; if (ok) { /* QByteArray::fromBase64() does not perform any input validity checks and skips invalid input characters */ val = QByteArray::fromBase64(valStr.toLatin1()); } return val; }