File indexing completed on 2024-05-12 05:11:20
0001 /* 0002 * SPDX-FileCopyrightText: 2015 Daniel Vrátil <dvratil@redhat.com> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.1-or-later 0005 * 0006 */ 0007 0008 #include "pimcontactsrunner.h" 0009 #include "akonadi_runner_debug.h" 0010 0011 #include <KConfigGroup> 0012 #include <KLocalizedString> 0013 0014 #include <QDesktopServices> 0015 #include <QIcon> 0016 #include <QSharedPointer> 0017 #include <QThread> 0018 0019 #include <Akonadi/ItemFetchJob> 0020 #include <Akonadi/ItemFetchScope> 0021 #include <Akonadi/Session> 0022 0023 #include <KContacts/Addressee> 0024 0025 #include <KSharedConfig> 0026 0027 #include <KEmailAddress> 0028 0029 #include "lib/contactcompleter.h" 0030 #include "lib/contactquery.h" 0031 #include "lib/resultiterator.h" 0032 0033 #include <array> 0034 0035 Q_DECLARE_METATYPE(KContacts::Addressee *) 0036 0037 PIMContactsRunner::PIMContactsRunner(QObject *parent, const KPluginMetaData &metaData) 0038 : AbstractRunner(parent, metaData) 0039 { 0040 setObjectName(QLatin1StringView("PIMContactsRunner")); 0041 // reloadConfiguration() called by default init() implementation 0042 } 0043 0044 PIMContactsRunner::~PIMContactsRunner() = default; 0045 0046 void PIMContactsRunner::reloadConfiguration() 0047 { 0048 mQueryAutocompleter = config().readEntry(QStringLiteral("queryAutocompleter"), true); 0049 } 0050 0051 void PIMContactsRunner::match(RunnerContext &context) 0052 { 0053 const QString queryString = context.query(); 0054 if (queryString.size() < 3) { 0055 return; 0056 } 0057 mListEmails.clear(); 0058 0059 queryContacts(context, queryString); 0060 0061 qCDebug(AKONADI_KRUNNER_LOG) << this << "MATCH: queryAutocompleter =" << mQueryAutocompleter; 0062 if (mQueryAutocompleter) { 0063 queryAutocompleter(context, queryString); 0064 } 0065 } 0066 0067 void PIMContactsRunner::queryContacts(RunnerContext &context, const QString &queryString) 0068 { 0069 Akonadi::Search::PIM::ContactQuery query; 0070 query.matchName(queryString); 0071 query.matchEmail(queryString); 0072 query.setMatchCriteria(Akonadi::Search::PIM::ContactQuery::StartsWithMatch); 0073 // Does not make sense to list more than 50 contacts on broad search terms 0074 query.setLimit(50); 0075 0076 // Accumulate the results so that we can fetch all in single Akonadi request 0077 Akonadi::Search::PIM::ResultIterator iter = query.exec(); 0078 QList<Akonadi::Item::Id> results; 0079 while (iter.next()) { 0080 results.push_back(iter.id()); 0081 } 0082 0083 qCDebug(AKONADI_KRUNNER_LOG) << "Query:" << queryString << ", results:" << results.count(); 0084 0085 if (results.isEmpty()) { 0086 return; 0087 } 0088 0089 // There can be multiple queries running at the same time, make sure we have 0090 // a separate Session for each, otherwise things might explode 0091 QScopedPointer<Akonadi::Session, QScopedPointerDeleteLater> session( 0092 new Akonadi::Session("PIIMContactRunner-" + QByteArray::number((qlonglong)QThread::currentThread()))); 0093 auto fetch = new Akonadi::ItemFetchJob(results, session.data()); 0094 Akonadi::ItemFetchScope &scope = fetch->fetchScope(); 0095 scope.fetchFullPayload(true); 0096 scope.setFetchRemoteIdentification(false); 0097 scope.setAncestorRetrieval(Akonadi::ItemFetchScope::None); 0098 0099 if (!fetch->exec()) { 0100 qCWarning(AKONADI_KRUNNER_LOG) << "Error while fetching contacts:" << fetch->errorString(); 0101 return; 0102 } 0103 0104 const auto items = fetch->items(); 0105 for (const Akonadi::Item &item : items) { 0106 KContacts::Addressee contact; 0107 try { 0108 contact = item.payload<KContacts::Addressee>(); 0109 } catch (const Akonadi::Exception &e) { 0110 qCDebug(AKONADI_KRUNNER_LOG) << "Corrupted index? Index referrers to an Item without contact"; 0111 // Error? 0112 continue; 0113 } 0114 0115 if (contact.isEmpty()) { 0116 qCDebug(AKONADI_KRUNNER_LOG) << "Corrupted index? Index refers to an Item with an empty contact"; 0117 continue; 0118 } 0119 0120 const QStringList emails = contact.emails(); 0121 if (emails.isEmpty()) { 0122 // No email, don't show the contact 0123 qCDebug(AKONADI_KRUNNER_LOG) << "Skipping" << contact.uid() << ", because it has no emails"; 0124 continue; 0125 } 0126 0127 QueryMatch match(this); 0128 match.setMatchCategory(i18n("Contacts")); 0129 match.setRelevance(0.75); // 0.75 is used by most runners, we don't 0130 0131 const KContacts::Picture photo = contact.photo(); 0132 if (!photo.isEmpty()) { 0133 const QImage img = photo.data().scaled(16, 16, Qt::KeepAspectRatio); 0134 match.setIcon(QIcon(QPixmap::fromImage(img))); 0135 } else { 0136 // The icon should be cached by Qt or FrameworkIntegration 0137 match.setIcon(QIcon::fromTheme(QStringLiteral("user-identity"))); 0138 } 0139 0140 QString matchedEmail; 0141 0142 QString name = contact.formattedName(); 0143 if (name.isEmpty()) { 0144 name = contact.assembledName(); 0145 } 0146 0147 // We got perfect match by name 0148 if (name == queryString) { 0149 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest); 0150 0151 // We got perfect match by one of the email addresses 0152 } else if (emails.contains(queryString)) { 0153 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest); 0154 matchedEmail = queryString; 0155 0156 // We got partial match either by name, or email 0157 } else { 0158 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Low); 0159 0160 // See if the match was by one of the email addresses 0161 for (const QString &email : emails) { 0162 if (email.startsWith(queryString)) { 0163 matchedEmail = email; 0164 break; 0165 } 0166 } 0167 } 0168 // If we had an email match, then use it, otherwise assume name-based 0169 // match and explode the contact to all available email addresses 0170 if (!matchedEmail.isEmpty()) { 0171 if (!mListEmails.contains(matchedEmail)) { 0172 mListEmails.append(matchedEmail); 0173 match.setText(i18nc("Name (email)", "%1 (%2)", name, matchedEmail)); 0174 match.setData(QStringLiteral("mailto:%1<%2>").arg(name, matchedEmail)); 0175 context.addMatch(match); 0176 } 0177 } else { 0178 for (const QString &email : emails) { 0179 if (!mListEmails.contains(email)) { 0180 mListEmails.append(email); 0181 QueryMatch alternativeMatch = match; 0182 alternativeMatch.setText(i18nc("Name (email)", "%1 (%2)", name, email)); 0183 alternativeMatch.setData(QStringLiteral("mailto:%1<%2>").arg(name, email)); 0184 context.addMatch(alternativeMatch); 0185 } 0186 } 0187 } 0188 } 0189 } 0190 0191 void PIMContactsRunner::queryAutocompleter(RunnerContext &context, const QString &queryString) 0192 { 0193 Akonadi::Search::PIM::ContactCompleter completer(queryString); 0194 const QStringList completerResults = completer.complete(); 0195 qCDebug(AKONADI_KRUNNER_LOG) << "Autocompleter returned" << completerResults.count() << "results"; 0196 for (const QString &result : completerResults) { 0197 // Filter out results where writing a mail wouldn't make sense, 0198 // e.g. anything with noreply or various automatic emails from git forges 0199 static const std::array filters = { 0200 QRegularExpression(QStringLiteral("no[-]?reply@")), 0201 QRegularExpression(QStringLiteral("incoming\\+.+@invent\\.kde\\.org")), 0202 QRegularExpression(QStringLiteral("reply\\+.+@reply\\.github\\.com")), 0203 QRegularExpression(QStringLiteral("@noreply\\.github\\.com")), 0204 QRegularExpression(QStringLiteral("notifications@github\\.com")), 0205 QRegularExpression(QStringLiteral("incoming\\+.+@gitlab\\.com")), 0206 QRegularExpression(QStringLiteral("gitlab@gitlab\\.freedesktop\\.org")), 0207 }; 0208 0209 const bool skip = std::any_of(filters.cbegin(), filters.cend(), [result](const QRegularExpression &filter) { 0210 return result.contains(filter); 0211 }); 0212 0213 if (skip) { 0214 continue; 0215 } 0216 0217 QueryMatch match(this); 0218 match.setRelevance(0.7); // slightly lower relevance than real addressbook contacts 0219 match.setMatchCategory(i18n("Contacts")); 0220 match.setSubtext(i18n("Autocompleted from received and sent emails")); 0221 match.setIcon(QIcon::fromTheme(QStringLiteral("user-identity"))); 0222 if (result == queryString) { 0223 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest); 0224 } else { 0225 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Low); 0226 } 0227 0228 QString name; 0229 QString email; 0230 if (KEmailAddress::extractEmailAddressAndName(result, email, name)) { 0231 if (mListEmails.contains(email)) { 0232 continue; 0233 } 0234 mListEmails.append(email); 0235 if (name.isEmpty()) { 0236 match.setText(email); 0237 match.setData(QStringLiteral("mailto:%1").arg(email)); 0238 } else { 0239 match.setText(i18nc("Name (email)", "%1 (%2)", name, email)); 0240 match.setData(QStringLiteral("mailto:%1<%2>").arg(name, email)); 0241 } 0242 } else { 0243 if (mListEmails.contains(result)) { 0244 continue; 0245 } 0246 mListEmails.append(result); 0247 match.setText(result); 0248 match.setData(QStringLiteral("mailto:%1").arg(result)); 0249 } 0250 context.addMatch(match); 0251 } 0252 } 0253 0254 void PIMContactsRunner::run(const RunnerContext &context, const QueryMatch &match) 0255 { 0256 Q_UNUSED(context) 0257 0258 const QString mailto = match.data().toString(); 0259 if (!mailto.isEmpty()) { 0260 QDesktopServices::openUrl(QUrl::fromUserInput(mailto)); 0261 } 0262 } 0263 0264 K_PLUGIN_CLASS_WITH_JSON(PIMContactsRunner, "plasma-krunner-pimcontacts.json") 0265 0266 #include "pimcontactsrunner.moc" 0267 0268 #include "moc_pimcontactsrunner.cpp"