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"