File indexing completed on 2024-05-19 05:11:50
0001 /* 0002 * This file is part of the KDE Akonadi Search Project 0003 * SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 * 0007 */ 0008 0009 #include "searchplugin.h" 0010 0011 #include "query.h" 0012 #include "resultiterator.h" 0013 #include "term.h" 0014 0015 #include <Akonadi/SearchQuery> 0016 0017 #include "akonadiplugin_indexer_debug.h" 0018 #include <Akonadi/MessageFlags> 0019 #include <KContacts/Addressee> 0020 #include <KContacts/ContactGroup> 0021 0022 using namespace Akonadi::Search; 0023 0024 static Term::Operation mapRelation(Akonadi::SearchTerm::Relation relation) 0025 { 0026 if (relation == Akonadi::SearchTerm::RelAnd) { 0027 return Term::And; 0028 } 0029 return Term::Or; 0030 } 0031 0032 static Term::Comparator mapComparator(Akonadi::SearchTerm::Condition comparator) 0033 { 0034 if (comparator == Akonadi::SearchTerm::CondContains) { 0035 return Term::Contains; 0036 } 0037 if (comparator == Akonadi::SearchTerm::CondGreaterOrEqual) { 0038 return Term::GreaterEqual; 0039 } 0040 if (comparator == Akonadi::SearchTerm::CondGreaterThan) { 0041 return Term::Greater; 0042 } 0043 if (comparator == Akonadi::SearchTerm::CondEqual) { 0044 return Term::Equal; 0045 } 0046 if (comparator == Akonadi::SearchTerm::CondLessOrEqual) { 0047 return Term::LessEqual; 0048 } 0049 if (comparator == Akonadi::SearchTerm::CondLessThan) { 0050 return Term::Less; 0051 } 0052 return Term::Auto; 0053 } 0054 0055 static Term getTerm(const Akonadi::SearchTerm &term, const QString &property) 0056 { 0057 Term t(property, term.value().toString(), mapComparator(term.condition())); 0058 t.setNegation(term.isNegated()); 0059 return t; 0060 } 0061 0062 Term recursiveEmailTermMapping(const Akonadi::SearchTerm &term) 0063 { 0064 const auto subTermsResult = term.subTerms(); 0065 if (!subTermsResult.isEmpty()) { 0066 Term t(mapRelation(term.relation())); 0067 for (const Akonadi::SearchTerm &subterm : subTermsResult) { 0068 const Term newTerm = recursiveEmailTermMapping(subterm); 0069 if (newTerm.isValid()) { 0070 t.addSubTerm(newTerm); 0071 } 0072 } 0073 return t; 0074 } else { 0075 // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value(); 0076 const Akonadi::EmailSearchTerm::EmailSearchField field = Akonadi::EmailSearchTerm::fromKey(term.key()); 0077 switch (field) { 0078 case Akonadi::EmailSearchTerm::Message: { 0079 Term s(Term::Or); 0080 s.setNegation(term.isNegated()); 0081 s.addSubTerm(Term(QStringLiteral("body"), term.value(), mapComparator(term.condition()))); 0082 s.addSubTerm(Term(QStringLiteral("headers"), term.value(), mapComparator(term.condition()))); 0083 return s; 0084 } 0085 case Akonadi::EmailSearchTerm::Body: 0086 return getTerm(term, QStringLiteral("body")); 0087 case Akonadi::EmailSearchTerm::Headers: 0088 return getTerm(term, QStringLiteral("headers")); 0089 case Akonadi::EmailSearchTerm::ByteSize: 0090 return getTerm(term, QStringLiteral("size")); 0091 case Akonadi::EmailSearchTerm::HeaderDate: { 0092 Term s(QStringLiteral("date"), QString::number(term.value().toDateTime().toSecsSinceEpoch()), mapComparator(term.condition())); 0093 s.setNegation(term.isNegated()); 0094 return s; 0095 } 0096 case Akonadi::EmailSearchTerm::HeaderOnlyDate: { 0097 Term s(QStringLiteral("onlydate"), QString::number(term.value().toDate().toJulianDay()), mapComparator(term.condition())); 0098 s.setNegation(term.isNegated()); 0099 return s; 0100 } 0101 case Akonadi::EmailSearchTerm::Subject: 0102 return getTerm(term, QStringLiteral("subject")); 0103 case Akonadi::EmailSearchTerm::HeaderFrom: 0104 return getTerm(term, QStringLiteral("from")); 0105 case Akonadi::EmailSearchTerm::HeaderTo: 0106 return getTerm(term, QStringLiteral("to")); 0107 case Akonadi::EmailSearchTerm::HeaderCC: 0108 return getTerm(term, QStringLiteral("cc")); 0109 case Akonadi::EmailSearchTerm::HeaderBCC: 0110 return getTerm(term, QStringLiteral("bcc")); 0111 case Akonadi::EmailSearchTerm::MessageStatus: { 0112 const QString value = term.value().toString(); 0113 if (value == QLatin1StringView(Akonadi::MessageFlags::Flagged)) { 0114 return Term(QStringLiteral("isimportant"), !term.isNegated()); 0115 } 0116 if (value == QLatin1StringView(Akonadi::MessageFlags::ToAct)) { 0117 return Term(QStringLiteral("istoact"), !term.isNegated()); 0118 } 0119 if (value == QLatin1StringView(Akonadi::MessageFlags::Watched)) { 0120 return Term(QStringLiteral("iswatched"), !term.isNegated()); 0121 } 0122 if (value == QLatin1StringView(Akonadi::MessageFlags::Deleted)) { 0123 return Term(QStringLiteral("isdeleted"), !term.isNegated()); 0124 } 0125 if (value == QLatin1StringView(Akonadi::MessageFlags::Spam)) { 0126 return Term(QStringLiteral("isspam"), !term.isNegated()); 0127 } 0128 if (value == QLatin1StringView(Akonadi::MessageFlags::Replied)) { 0129 return Term(QStringLiteral("isreplied"), !term.isNegated()); 0130 } 0131 if (value == QLatin1StringView(Akonadi::MessageFlags::Ignored)) { 0132 return Term(QStringLiteral("isignored"), !term.isNegated()); 0133 } 0134 if (value == QLatin1StringView(Akonadi::MessageFlags::Forwarded)) { 0135 return Term(QStringLiteral("isforwarded"), !term.isNegated()); 0136 } 0137 if (value == QLatin1StringView(Akonadi::MessageFlags::Sent)) { 0138 return Term(QStringLiteral("issent"), !term.isNegated()); 0139 } 0140 if (value == QLatin1StringView(Akonadi::MessageFlags::Queued)) { 0141 return Term(QStringLiteral("isqueued"), !term.isNegated()); 0142 } 0143 if (value == QLatin1StringView(Akonadi::MessageFlags::Ham)) { 0144 return Term(QStringLiteral("isham"), !term.isNegated()); 0145 } 0146 if (value == QLatin1StringView(Akonadi::MessageFlags::Seen)) { 0147 return Term(QStringLiteral("isread"), !term.isNegated()); 0148 } 0149 if (value == QLatin1StringView(Akonadi::MessageFlags::HasAttachment)) { 0150 return Term(QStringLiteral("hasattachment"), !term.isNegated()); 0151 } 0152 if (value == QLatin1StringView(Akonadi::MessageFlags::Encrypted)) { 0153 return Term(QStringLiteral("isencrypted"), !term.isNegated()); 0154 } 0155 if (value == QLatin1StringView(Akonadi::MessageFlags::HasInvitation)) { 0156 return Term(QStringLiteral("hasinvitation"), !term.isNegated()); 0157 } 0158 break; 0159 } 0160 case Akonadi::EmailSearchTerm::MessageTag: 0161 // search directly in akonadi? or index tags. 0162 break; 0163 case Akonadi::EmailSearchTerm::HeaderReplyTo: 0164 return getTerm(term, QStringLiteral("replyto")); 0165 case Akonadi::EmailSearchTerm::HeaderOrganization: 0166 return getTerm(term, QStringLiteral("organization")); 0167 case Akonadi::EmailSearchTerm::HeaderListId: 0168 return getTerm(term, QStringLiteral("listid")); 0169 case Akonadi::EmailSearchTerm::HeaderResentFrom: 0170 return getTerm(term, QStringLiteral("resentfrom")); 0171 case Akonadi::EmailSearchTerm::HeaderXLoop: 0172 return getTerm(term, QStringLiteral("xloop")); 0173 case Akonadi::EmailSearchTerm::HeaderXMailingList: 0174 return getTerm(term, QStringLiteral("xmailinglist")); 0175 case Akonadi::EmailSearchTerm::HeaderXSpamFlag: 0176 return getTerm(term, QStringLiteral("xspamflag")); 0177 case Akonadi::EmailSearchTerm::Attachment: 0178 return Term(QStringLiteral("hasattachment"), !term.isNegated()); 0179 case Akonadi::EmailSearchTerm::Unknown: 0180 default: 0181 if (!term.key().isEmpty()) { 0182 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key(); 0183 } 0184 } 0185 } 0186 return {}; 0187 } 0188 0189 Term recursiveCalendarTermMapping(const Akonadi::SearchTerm &term) 0190 { 0191 const auto subTerms{term.subTerms()}; 0192 if (!subTerms.isEmpty()) { 0193 Term t(mapRelation(term.relation())); 0194 for (const Akonadi::SearchTerm &subterm : subTerms) { 0195 const Term newTerm = recursiveCalendarTermMapping(subterm); 0196 if (newTerm.isValid()) { 0197 t.addSubTerm(newTerm); 0198 } 0199 } 0200 return t; 0201 } else { 0202 // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value(); 0203 const Akonadi::IncidenceSearchTerm::IncidenceSearchField field = Akonadi::IncidenceSearchTerm::fromKey(term.key()); 0204 switch (field) { 0205 case Akonadi::IncidenceSearchTerm::Organizer: 0206 return getTerm(term, QStringLiteral("organizer")); 0207 case Akonadi::IncidenceSearchTerm::Summary: 0208 return getTerm(term, QStringLiteral("summary")); 0209 case Akonadi::IncidenceSearchTerm::Location: 0210 return getTerm(term, QStringLiteral("location")); 0211 case Akonadi::IncidenceSearchTerm::PartStatus: { 0212 Term t(QStringLiteral("partstatus"), term.value().toString(), Term::Equal); 0213 t.setNegation(term.isNegated()); 0214 return t; 0215 } 0216 default: 0217 if (!term.key().isEmpty()) { 0218 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key(); 0219 } 0220 } 0221 } 0222 return {}; 0223 } 0224 0225 Term recursiveNoteTermMapping(const Akonadi::SearchTerm &term) 0226 { 0227 const auto subTerms{term.subTerms()}; 0228 if (!subTerms.isEmpty()) { 0229 Term t(mapRelation(term.relation())); 0230 for (const Akonadi::SearchTerm &subterm : subTerms) { 0231 const Term newTerm = recursiveNoteTermMapping(subterm); 0232 if (newTerm.isValid()) { 0233 t.addSubTerm(newTerm); 0234 } 0235 } 0236 return t; 0237 } else { 0238 // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value(); 0239 const Akonadi::EmailSearchTerm::EmailSearchField field = Akonadi::EmailSearchTerm::fromKey(term.key()); 0240 switch (field) { 0241 case Akonadi::EmailSearchTerm::Subject: 0242 return getTerm(term, QStringLiteral("subject")); 0243 case Akonadi::EmailSearchTerm::Body: 0244 return getTerm(term, QStringLiteral("body")); 0245 default: 0246 if (!term.key().isEmpty()) { 0247 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key(); 0248 } 0249 } 0250 } 0251 return {}; 0252 } 0253 0254 Term recursiveContactTermMapping(const Akonadi::SearchTerm &term) 0255 { 0256 const auto subTerms{term.subTerms()}; 0257 if (!subTerms.isEmpty()) { 0258 Term t(mapRelation(term.relation())); 0259 for (const Akonadi::SearchTerm &subterm : subTerms) { 0260 const Term newTerm = recursiveContactTermMapping(subterm); 0261 if (newTerm.isValid()) { 0262 t.addSubTerm(newTerm); 0263 } 0264 } 0265 return t; 0266 } else { 0267 // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value(); 0268 const Akonadi::ContactSearchTerm::ContactSearchField field = Akonadi::ContactSearchTerm::fromKey(term.key()); 0269 switch (field) { 0270 case Akonadi::ContactSearchTerm::Name: 0271 return getTerm(term, QStringLiteral("name")); 0272 case Akonadi::ContactSearchTerm::Email: 0273 return getTerm(term, QStringLiteral("email")); 0274 case Akonadi::ContactSearchTerm::Nickname: 0275 return getTerm(term, QStringLiteral("nick")); 0276 case Akonadi::ContactSearchTerm::Uid: 0277 return getTerm(term, QStringLiteral("uid")); 0278 case Akonadi::ContactSearchTerm::Unknown: 0279 default: 0280 if (!term.key().isEmpty()) { 0281 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key(); 0282 } 0283 } 0284 } 0285 return {}; 0286 } 0287 0288 QSet<qint64> SearchPlugin::search(const QString &akonadiQuery, const QList<qint64> &collections, const QStringList &mimeTypes) 0289 { 0290 if (akonadiQuery.isEmpty() && collections.isEmpty() && mimeTypes.isEmpty()) { 0291 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "empty query"; 0292 return {}; 0293 } 0294 0295 Akonadi::SearchQuery searchQuery; 0296 if (!akonadiQuery.isEmpty()) { 0297 searchQuery = Akonadi::SearchQuery::fromJSON(akonadiQuery.toLatin1()); 0298 if (searchQuery.isNull() && collections.isEmpty() && mimeTypes.isEmpty()) { 0299 return {}; 0300 } 0301 } 0302 0303 const Akonadi::SearchTerm term = searchQuery.term(); 0304 0305 Query query; 0306 Term t; 0307 0308 if (mimeTypes.contains(QLatin1StringView("message/rfc822"))) { 0309 // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << "mail query"; 0310 query.setType(QStringLiteral("Email")); 0311 t = recursiveEmailTermMapping(term); 0312 } else if (mimeTypes.contains(KContacts::Addressee::mimeType()) || mimeTypes.contains(KContacts::ContactGroup::mimeType())) { 0313 query.setType(QStringLiteral("Contact")); 0314 t = recursiveContactTermMapping(term); 0315 } else if (mimeTypes.contains(QLatin1StringView("text/x-vnd.akonadi.note"))) { 0316 query.setType(QStringLiteral("Note")); 0317 t = recursiveNoteTermMapping(term); 0318 } else if (mimeTypes.contains(QLatin1StringView("application/x-vnd.akonadi.calendar.event")) 0319 || mimeTypes.contains(QLatin1StringView("application/x-vnd.akonadi.calendar.todo")) 0320 || mimeTypes.contains(QLatin1StringView("application/x-vnd.akonadi.calendar.journal")) 0321 || mimeTypes.contains(QLatin1StringView("application/x-vnd.akonadi.calendar.freebusy"))) { 0322 query.setType(QStringLiteral("Calendar")); 0323 t = recursiveCalendarTermMapping(term); 0324 } else { 0325 // Unknown type 0326 return {}; 0327 } 0328 0329 if (searchQuery.limit() > 0) { 0330 query.setLimit(searchQuery.limit()); 0331 } 0332 0333 // Filter by collection if not empty 0334 if (!collections.isEmpty()) { 0335 Term parentTerm(Term::And); 0336 Term collectionTerm(Term::Or); 0337 for (const qint64 col : collections) { 0338 collectionTerm.addSubTerm(Term(QStringLiteral("collection"), QString::number(col), Term::Equal)); 0339 } 0340 if (t.isEmpty()) { 0341 query.setTerm(collectionTerm); 0342 } else { 0343 parentTerm.addSubTerm(collectionTerm); 0344 parentTerm.addSubTerm(t); 0345 query.setTerm(parentTerm); 0346 } 0347 } else { 0348 if (t.subTerms().isEmpty()) { 0349 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "no terms added"; 0350 return {}; 0351 } 0352 0353 query.setTerm(t); 0354 } 0355 0356 QSet<qint64> resultSet; 0357 // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << query.toJSON(); 0358 ResultIterator iter = query.exec(); 0359 while (iter.next()) { 0360 const QByteArray id = iter.id(); 0361 const int fid = deserialize("akonadi", id); 0362 resultSet << fid; 0363 } 0364 qCDebug(AKONADIPLUGIN_INDEXER_LOG) << "Got" << resultSet.count() << "results"; 0365 return resultSet; 0366 } 0367 0368 #include "moc_searchplugin.cpp"