diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index ee5d2a0..046b86f 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,291 +1,289 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-server-static STATIC) target_sources(gpgol-server-static PRIVATE websocketclient.cpp websocketclient.h webserver.h webserver.cpp # Identity identity/addressvalidationjob.cpp identity/addressvalidationjob.h identity/identitymanager.cpp identity/identitymanager.h identity/identitydialog.cpp identity/identitydialog.h identity/identityaddvcarddialog.cpp identity/identityaddvcarddialog.h # HTTP Controller controllers/emailcontroller.cpp controllers/emailcontroller.h # Draft draft/draft.cpp draft/draft.h draft/draftmanager.cpp draft/draftmanager.h # EWS integration ews/ewsattachment.cpp ews/ewsattachment.h ews/ewsattendee.cpp ews/ewsattendee.h ews/ewsclient_debug.cpp ews/ewsclient_debug.h ews/ewsid.cpp ews/ewsid.h ews/ewsitem.cpp ews/ewsitem.h ews/ewsitembase.cpp ews/ewsitembase.h ews/ewsitembase_p.h ews/ewsmailbox.cpp ews/ewsmailbox.h ews/ewsmailfactory.cpp ews/ewsmailfactory.h ews/ewsoccurrence.cpp ews/ewsoccurrence.h ews/ewspropertyfield.cpp ews/ewspropertyfield.h ews/ewsrecurrence.cpp ews/ewsrecurrence.h ews/ewsserverversion.cpp ews/ewsserverversion.h ews/ewstypes.cpp ews/ewstypes.h ews/ewsxml.cpp ews/ewsxml.h # Editor editor/addresseelineedit.cpp editor/addresseelineedit.h editor/addresseelineeditmanager.cpp editor/addresseelineeditmanager.h - editor/certificatelineedit.cpp - editor/certificatelineedit.h editor/composer.cpp editor/composer.h editor/composerviewbase.cpp editor/composerviewbase.h editor/composerwindow.cpp editor/composerwindow.h editor/composerwindowfactory.cpp editor/composerwindowfactory.h editor/cryptostateindicatorwidget.cpp editor/cryptostateindicatorwidget.h editor/encryptionstate.cpp editor/encryptionstate.h editor/kmcomposerglobalaction.cpp editor/kmcomposerglobalaction.h editor/nearexpirywarning.cpp editor/nearexpirywarning.h editor/mailtemplates.cpp editor/mailtemplates.h editor/recipient.cpp editor/recipient.h editor/recipientline.cpp editor/recipientline.h editor/recipientseditor.cpp editor/recipientseditor.h editor/util.h editor/util.cpp editor/kmailcompletion.cpp editor/kmailcompletion.h editor/richtextcomposerng.cpp editor/richtextcomposerng.h editor/richtextcomposersignatures.cpp editor/richtextcomposersignatures.h editor/nodehelper.cpp editor/nodehelper.h editor/signaturecontroller.cpp editor/signaturecontroller.h editor/spellcheckerconfigdialog.cpp editor/spellcheckerconfigdialog.h # Editor job editor/job/abstractencryptjob.h editor/job/autocryptheadersjob.h editor/job/contentjobbase.h editor/job/contentjobbase_p.h editor/job/dndfromarkjob.cpp editor/job/dndfromarkjob.h editor/job/encryptjob.h editor/job/inserttextfilejob.h editor/job/itipjob.h editor/job/jobbase.h editor/job/jobbase_p.h editor/job/maintextjob.h editor/job/multipartjob.h editor/job/protectedheadersjob.h editor/job/signencryptjob.h editor/job/signjob.h editor/job/singlepartjob.h editor/job/skeletonmessagejob.h editor/job/transparentjob.h editor/job/autocryptheadersjob.cpp editor/job/contentjobbase.cpp editor/job/encryptjob.cpp editor/job/inserttextfilejob.cpp editor/job/itipjob.cpp editor/job/jobbase.cpp editor/job/maintextjob.cpp editor/job/multipartjob.cpp editor/job/protectedheadersjob.cpp editor/job/saveasfilejob.cpp editor/job/saveasfilejob.h editor/job/signencryptjob.cpp editor/job/signjob.cpp editor/job/singlepartjob.cpp editor/job/skeletonmessagejob.cpp editor/job/transparentjob.cpp ## Editor Part editor/part/globalpart.h editor/part/infopart.h editor/part/itippart.h editor/part/messagepart.h editor/part/textpart.h editor/part/globalpart.cpp editor/part/infopart.cpp editor/part/itippart.cpp editor/part/messagepart.cpp editor/part/textpart.cpp ## Attachment editor/attachment/attachmentjob.cpp editor/attachment/attachmentjob.h editor/attachment/attachmentclipboardjob.cpp editor/attachment/attachmentclipboardjob.h editor/attachment/attachmentcompressjob.cpp editor/attachment/attachmentcompressjob.h editor/attachment/attachmentcontroller.cpp editor/attachment/attachmentcontroller.h editor/attachment/attachmentcontrollerbase.cpp editor/attachment/attachmentcontrollerbase.h editor/attachment/attachmentfromfolderjob.cpp editor/attachment/attachmentfromfolderjob.h editor/attachment/attachmentfrommimecontentjob.cpp editor/attachment/attachmentfrommimecontentjob.h editor/attachment/attachmentfromurlbasejob.cpp editor/attachment/attachmentfromurlbasejob.h editor/attachment/attachmentfromurljob.cpp editor/attachment/attachmentfromurljob.h editor/attachment/attachmentfromurlutils.cpp editor/attachment/attachmentfromurlutils.h editor/attachment/attachmentfrompublickeyjob.cpp editor/attachment/attachmentfrompublickeyjob.h editor/attachment/attachmentloadjob.cpp editor/attachment/attachmentloadjob.h editor/attachment/attachmentmodel.cpp editor/attachment/attachmentmodel.h editor/attachment/attachmentpart.cpp editor/attachment/attachmentpart.h editor/attachment/attachmentpropertiesdialog.cpp editor/attachment/attachmentpropertiesdialog.h editor/attachment/attachmentupdatejob.cpp editor/attachment/attachmentupdatejob.h editor/attachment/attachmentview.cpp editor/attachment/attachmentview.h editor/attachment/editorwatcher.cpp editor/attachment/editorwatcher.h ) qt_add_resources(gpgol-server-static PREFIX "/" FILES assets/certificate.crt ) ki18n_wrap_ui(gpgol-server-static editor/attachment/ui/attachmentpropertiesdialog.ui editor/attachment/ui/attachmentpropertiesdialog_readonly.ui ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER websocket_debug.h IDENTIFIER WEBSOCKET_LOG CATEGORY_NAME org.gpgol.server.websocket DESCRIPTION "Websocket connection in the server" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewsresource_debug.h IDENTIFIER EWSRES_LOG CATEGORY_NAME org.gpgol.ews DESCRIPTION "Ews mail client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.gpgol.ews.client DESCRIPTION "ews client (gpgol-server)" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER editor_debug.h IDENTIFIER EDITOR_LOG CATEGORY_NAME org.gpgol.editor DESCRIPTION "mail composer" EXPORT GPGOL ) set(WARN_TOOMANY_RECIPIENTS_DEFAULT true) set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true) configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg) kconfig_add_kcfg_files(gpgol-server-static editor/settings/messagecomposersettings.kcfgc) install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-server) target_sources(gpgol-server-static PUBLIC ${gpgol-server-static_SRCS}) target_link_libraries(gpgol-server-static PUBLIC common Qt6::HttpServer Qt6::Widgets Qt6::PrintSupport KF6::CalendarCore KF6::ConfigCore KF6::ConfigGui KF6::Contacts KF6::Completion KF6::CoreAddons KF6::ColorScheme KF6::Codecs KF6::GuiAddons KF6::SonnetUi KF6::WidgetsAddons KF6::XmlGui KF6::KIOFileWidgets KF6::Archive KF6::TextAutoCorrectionCore KPim6::MimeTreeParserWidgets KPim6::Libkleo KPim6::Libkdepim KPim6::LdapWidgets KPim6::PimTextEdit KPim6::IdentityManagementCore KPim6::IdentityManagementWidgets ) add_executable(gpgol-server main.cpp) target_link_libraries(gpgol-server PRIVATE gpgol-server-static) if (BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/server/editor/addresseelineedit.cpp b/server/editor/addresseelineedit.cpp index 67333e3..c0e1cce 100644 --- a/server/editor/addresseelineedit.cpp +++ b/server/editor/addresseelineedit.cpp @@ -1,1163 +1,1170 @@ /* 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(" "); inline bool itemIsHeader(const QListWidgetItem *item) { return item && !item->text().startsWith(QLatin1StringView(" ")); } 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++) { - const auto email = QLatin1String(key.userID(i).email()); - const auto name = QLatin1String(key.userID(i).name()); + 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.isEmpty()) { + 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/server/editor/addresseelineeditmanager.cpp b/server/editor/addresseelineeditmanager.cpp index 6dfaadd..3df9ff2 100644 --- a/server/editor/addresseelineeditmanager.cpp +++ b/server/editor/addresseelineeditmanager.cpp @@ -1,100 +1,99 @@ /* SPDX-FileCopyrightText: 2015-2024 Laurent Montel SPDX-License-Identifier: GPL-2.0-or-later */ #include "addresseelineeditmanager.h" -#include "addresseelineeditgnupg.h" #include "kmailcompletion.h" #include "editor_debug.h" #include #include #include #include #include #include #include Q_GLOBAL_STATIC(AddresseeLineEditManager, sInstance) AddresseeLineEditManager::AddresseeLineEditManager() : mCompletion(std::make_unique()) //, mAddresseeLineEditGnupg(new AddresseeLineEditGnupg) { KConfigGroup group(KSharedConfig::openConfig(), QStringLiteral("AddressLineEdit")); mAutoGroupExpand = group.readEntry("AutoGroupExpand", false); } AddresseeLineEditManager::~AddresseeLineEditManager() = default; AddresseeLineEditManager *AddresseeLineEditManager::self() { return sInstance; } int AddresseeLineEditManager::addCompletionSource(const QString &source, int weight) { QMap::iterator it = completionSourceWeights.find(source); if (it == completionSourceWeights.end()) { completionSourceWeights.insert(source, weight); } else { completionSourceWeights[source] = weight; } const int sourceIndex = completionSources.indexOf(source); if (sourceIndex == -1) { completionSources.append(source); return completionSources.size() - 1; } else { return sourceIndex; } } void AddresseeLineEditManager::removeCompletionSource(const QString &source) { QMap::iterator it = completionSourceWeights.find(source); if (it != completionSourceWeights.end()) { completionSourceWeights.remove(source); mCompletion->clear(); } } KMailCompletion *AddresseeLineEditManager::completion() const { return mCompletion.get(); } bool AddresseeLineEditManager::isOnline() const { if (QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability)) { return QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online; } else { qCWarning(EDITOR_LOG) << "Couldn't find a working backend for QNetworkInformation"; return false; } } bool AddresseeLineEditManager::autoGroupExpand() const { return mAutoGroupExpand; } void AddresseeLineEditManager::setAutoGroupExpand(bool checked) { if (mAutoGroupExpand != checked) { mAutoGroupExpand = checked; KConfigGroup group(KSharedConfig::openConfig(), QStringLiteral("AddressLineEdit")); group.writeEntry("AutoGroupExpand", mAutoGroupExpand); } } QColor AddresseeLineEditManager::alternateColor() const { if (!mAlternateColor.isValid()) { const KColorScheme colorScheme(QPalette::Active, KColorScheme::View); mAlternateColor = colorScheme.background(KColorScheme::AlternateBackground).color(); } return mAlternateColor; } diff --git a/server/editor/kmailcompletion.cpp b/server/editor/kmailcompletion.cpp new file mode 100644 index 0000000..5821333 --- /dev/null +++ b/server/editor/kmailcompletion.cpp @@ -0,0 +1,101 @@ +/* + This file is part of libkdepim. + + SPDX-FileCopyrightText: 2006 Christian Schaarschmidt + SPDX-FileCopyrightText: 2017-2024 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kmailcompletion.h" +#include +#include + +KMailCompletion::KMailCompletion() +{ + setIgnoreCase(true); +} + +void KMailCompletion::clear() +{ + m_keyMap.clear(); + KCompletion::clear(); +} + +QString KMailCompletion::makeCompletion(const QString &string) +{ + QString match = KCompletion::makeCompletion(string); + + static const QRegularExpression emailRegularExpression{QRegularExpression(QStringLiteral("(@)|(<.*>)"))}; + // this should be in postProcessMatch, but postProcessMatch is const and will not allow nextMatch + if (!match.isEmpty()) { + const QString firstMatch(match); + while (match.indexOf(emailRegularExpression) == -1) { + /* local email do not require @domain part, if match is an address we'll + * find last+first in m_keyMap and we'll know that match is + * already a valid email. + * + * Distribution list do not have last+first entry, they will be + * in mailAddr + */ + const QStringList &mailAddr = m_keyMap[match]; // get all mailAddr for this keyword + bool isEmail = false; + for (QStringList::ConstIterator sit(mailAddr.begin()), sEnd(mailAddr.end()); sit != sEnd; ++sit) { + if ((*sit).indexOf(QLatin1Char('<') + match + QLatin1Char('>')) != -1 || (*sit) == match) { + isEmail = true; + break; + } + } + + if (!isEmail) { + // match is a keyword, skip it and try to find match + match = nextMatch(); + if (firstMatch == match) { + match.clear(); + break; + } + } else { + break; + } + } + } + return match; +} + +void KMailCompletion::addItemWithKeys(const QString &email, int weight, const QStringList *keyWords) +{ + Q_ASSERT(keyWords != nullptr); + QStringList::ConstIterator end = keyWords->constEnd(); + for (QStringList::ConstIterator it(keyWords->constBegin()); it != end; ++it) { + QStringList &emailList = m_keyMap[(*it)]; // lookup email-list for given keyword + if (!emailList.contains(email)) { // add email if not there + emailList.append(email); + } + addItem((*it), weight); // inform KCompletion about keyword + } +} + +void KMailCompletion::postProcessMatches(QStringList *pMatches) const +{ + Q_ASSERT(pMatches != nullptr); + if (pMatches->isEmpty()) { + return; + } + + // KCompletion has found the keywords for us, we can now map them to mail-addr + QSet mailAddrDistinct; + for (QStringList::ConstIterator sit2(pMatches->begin()), sEnd2(pMatches->end()); sit2 != sEnd2; ++sit2) { + const QStringList &mailAddr = m_keyMap[(*sit2)]; // get all mailAddr for this keyword + if (mailAddr.isEmpty()) { + mailAddrDistinct.insert(*sit2); + } else { + for (QStringList::ConstIterator sit(mailAddr.begin()), sEnd(mailAddr.end()); sit != sEnd; ++sit) { + mailAddrDistinct.insert(*sit); // store mailAddr, QSet will make them unique + } + } + } + pMatches->clear(); // delete keywords + (*pMatches) += mailAddrDistinct.values(); // add emailAddr +} + +#include "moc_kmailcompletion.cpp" diff --git a/server/editor/kmailcompletion.h b/server/editor/kmailcompletion.h new file mode 100644 index 0000000..dbc2f47 --- /dev/null +++ b/server/editor/kmailcompletion.h @@ -0,0 +1,67 @@ +/* + This file is part of libkdepim. + + SPDX-FileCopyrightText: 2006 Christian Schaarschmidt + SPDX-FileCopyrightText: 2017-2024 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include +#include + +/** + * KMailCompletion allows lookup of email addresses by keyword. + * This is used for lookup by nickname, since we don't want the nickname to appear in the final email. + * E.g. you have a nickname "idiot" for your boss, you want to type "idiot" but you want the completion + * to offer "Full Name ", without the nickname being visible. + */ +class KMailCompletion : public KCompletion +{ + Q_OBJECT + +public: + KMailCompletion(); + + /** + * clears internal keyword map and calls KCompletion::clear. + */ + void clear() override; + + /** + * uses KCompletion::makeCompletion to find email addresses which starts + * with string. ignores keywords. + * + * @returns email address + */ + QString makeCompletion(const QString &string) override; + + /** + * specify keywords for email. + * + * Items may be added with KCompletion::addItem, those will only be + * returned as match if they are in one of these formats: + * \li contains localpart@domain + * \li contains + * or if they have also been added with this function. + */ + void addItemWithKeys(const QString &email, int weight, const QStringList *keyWords); + + /** + * use internal map to replace all keywords in pMatches with corresponding + * email addresses. + */ + void postProcessMatches(QStringList *pMatches) const override; + + // We are not using allWeightedMatches() anywhere, therefore we don't need + // to override the other postProcessMatches() function + using KCompletion::postProcessMatches; + +private: + QMap m_keyMap; +};