1 /*
2  * This file is part of the KDE Akonadi Search Project
3  * SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
4  *
5  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6  *
7  */
8 
9 #include "searchplugin.h"
10 
11 #include "query.h"
12 #include "resultiterator.h"
13 #include "term.h"
14 
15 #include <Akonadi/SearchQuery>
16 
17 #include "akonadiplugin_indexer_debug.h"
18 #include <Akonadi/KMime/MessageFlags>
19 #include <KContacts/Addressee>
20 #include <KContacts/ContactGroup>
21 
22 using namespace Akonadi::Search;
23 
mapRelation(Akonadi::SearchTerm::Relation relation)24 static Term::Operation mapRelation(Akonadi::SearchTerm::Relation relation)
25 {
26     if (relation == Akonadi::SearchTerm::RelAnd) {
27         return Term::And;
28     }
29     return Term::Or;
30 }
31 
mapComparator(Akonadi::SearchTerm::Condition comparator)32 static Term::Comparator mapComparator(Akonadi::SearchTerm::Condition comparator)
33 {
34     if (comparator == Akonadi::SearchTerm::CondContains) {
35         return Term::Contains;
36     }
37     if (comparator == Akonadi::SearchTerm::CondGreaterOrEqual) {
38         return Term::GreaterEqual;
39     }
40     if (comparator == Akonadi::SearchTerm::CondGreaterThan) {
41         return Term::Greater;
42     }
43     if (comparator == Akonadi::SearchTerm::CondEqual) {
44         return Term::Equal;
45     }
46     if (comparator == Akonadi::SearchTerm::CondLessOrEqual) {
47         return Term::LessEqual;
48     }
49     if (comparator == Akonadi::SearchTerm::CondLessThan) {
50         return Term::Less;
51     }
52     return Term::Auto;
53 }
54 
getTerm(const Akonadi::SearchTerm & term,const QString & property)55 static Term getTerm(const Akonadi::SearchTerm &term, const QString &property)
56 {
57     Term t(property, term.value().toString(), mapComparator(term.condition()));
58     t.setNegation(term.isNegated());
59     return t;
60 }
61 
recursiveEmailTermMapping(const Akonadi::SearchTerm & term)62 Term recursiveEmailTermMapping(const Akonadi::SearchTerm &term)
63 {
64     if (!term.subTerms().isEmpty()) {
65         Term t(mapRelation(term.relation()));
66         const auto subTermsResult = term.subTerms();
67         for (const Akonadi::SearchTerm &subterm : subTermsResult) {
68             const Term newTerm = recursiveEmailTermMapping(subterm);
69             if (newTerm.isValid()) {
70                 t.addSubTerm(newTerm);
71             }
72         }
73         return t;
74     } else {
75         // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value();
76         const Akonadi::EmailSearchTerm::EmailSearchField field = Akonadi::EmailSearchTerm::fromKey(term.key());
77         switch (field) {
78         case Akonadi::EmailSearchTerm::Message: {
79             Term s(Term::Or);
80             s.setNegation(term.isNegated());
81             s.addSubTerm(Term(QStringLiteral("body"), term.value(), mapComparator(term.condition())));
82             s.addSubTerm(Term(QStringLiteral("headers"), term.value(), mapComparator(term.condition())));
83             return s;
84         }
85         case Akonadi::EmailSearchTerm::Body:
86             return getTerm(term, QStringLiteral("body"));
87         case Akonadi::EmailSearchTerm::Headers:
88             return getTerm(term, QStringLiteral("headers"));
89         case Akonadi::EmailSearchTerm::ByteSize:
90             return getTerm(term, QStringLiteral("size"));
91         case Akonadi::EmailSearchTerm::HeaderDate: {
92             Term s(QStringLiteral("date"), QString::number(term.value().toDateTime().toSecsSinceEpoch()), mapComparator(term.condition()));
93             s.setNegation(term.isNegated());
94             return s;
95         }
96         case Akonadi::EmailSearchTerm::HeaderOnlyDate: {
97             Term s(QStringLiteral("onlydate"), QString::number(term.value().toDate().toJulianDay()), mapComparator(term.condition()));
98             s.setNegation(term.isNegated());
99             return s;
100         }
101         case Akonadi::EmailSearchTerm::Subject:
102             return getTerm(term, QStringLiteral("subject"));
103         case Akonadi::EmailSearchTerm::HeaderFrom:
104             return getTerm(term, QStringLiteral("from"));
105         case Akonadi::EmailSearchTerm::HeaderTo:
106             return getTerm(term, QStringLiteral("to"));
107         case Akonadi::EmailSearchTerm::HeaderCC:
108             return getTerm(term, QStringLiteral("cc"));
109         case Akonadi::EmailSearchTerm::HeaderBCC:
110             return getTerm(term, QStringLiteral("bcc"));
111         case Akonadi::EmailSearchTerm::MessageStatus: {
112             const QString value = term.value().toString();
113             if (value == QString::fromLatin1(Akonadi::MessageFlags::Flagged)) {
114                 return Term(QStringLiteral("isimportant"), !term.isNegated());
115             }
116             if (value == QString::fromLatin1(Akonadi::MessageFlags::ToAct)) {
117                 return Term(QStringLiteral("istoact"), !term.isNegated());
118             }
119             if (value == QString::fromLatin1(Akonadi::MessageFlags::Watched)) {
120                 return Term(QStringLiteral("iswatched"), !term.isNegated());
121             }
122             if (value == QString::fromLatin1(Akonadi::MessageFlags::Deleted)) {
123                 return Term(QStringLiteral("isdeleted"), !term.isNegated());
124             }
125             if (value == QString::fromLatin1(Akonadi::MessageFlags::Spam)) {
126                 return Term(QStringLiteral("isspam"), !term.isNegated());
127             }
128             if (value == QString::fromLatin1(Akonadi::MessageFlags::Replied)) {
129                 return Term(QStringLiteral("isreplied"), !term.isNegated());
130             }
131             if (value == QString::fromLatin1(Akonadi::MessageFlags::Ignored)) {
132                 return Term(QStringLiteral("isignored"), !term.isNegated());
133             }
134             if (value == QString::fromLatin1(Akonadi::MessageFlags::Forwarded)) {
135                 return Term(QStringLiteral("isforwarded"), !term.isNegated());
136             }
137             if (value == QString::fromLatin1(Akonadi::MessageFlags::Sent)) {
138                 return Term(QStringLiteral("issent"), !term.isNegated());
139             }
140             if (value == QString::fromLatin1(Akonadi::MessageFlags::Queued)) {
141                 return Term(QStringLiteral("isqueued"), !term.isNegated());
142             }
143             if (value == QString::fromLatin1(Akonadi::MessageFlags::Ham)) {
144                 return Term(QStringLiteral("isham"), !term.isNegated());
145             }
146             if (value == QString::fromLatin1(Akonadi::MessageFlags::Seen)) {
147                 return Term(QStringLiteral("isread"), !term.isNegated());
148             }
149             if (value == QString::fromLatin1(Akonadi::MessageFlags::HasAttachment)) {
150                 return Term(QStringLiteral("hasattachment"), !term.isNegated());
151             }
152             if (value == QString::fromLatin1(Akonadi::MessageFlags::Encrypted)) {
153                 return Term(QStringLiteral("isencrypted"), !term.isNegated());
154             }
155             if (value == QString::fromLatin1(Akonadi::MessageFlags::HasInvitation)) {
156                 return Term(QStringLiteral("hasinvitation"), !term.isNegated());
157             }
158             break;
159         }
160         case Akonadi::EmailSearchTerm::MessageTag:
161             // search directly in akonadi? or index tags.
162             break;
163         case Akonadi::EmailSearchTerm::HeaderReplyTo:
164             return getTerm(term, QStringLiteral("replyto"));
165         case Akonadi::EmailSearchTerm::HeaderOrganization:
166             return getTerm(term, QStringLiteral("organization"));
167         case Akonadi::EmailSearchTerm::HeaderListId:
168             return getTerm(term, QStringLiteral("listid"));
169         case Akonadi::EmailSearchTerm::HeaderResentFrom:
170             return getTerm(term, QStringLiteral("resentfrom"));
171         case Akonadi::EmailSearchTerm::HeaderXLoop:
172             return getTerm(term, QStringLiteral("xloop"));
173         case Akonadi::EmailSearchTerm::HeaderXMailingList:
174             return getTerm(term, QStringLiteral("xmailinglist"));
175         case Akonadi::EmailSearchTerm::HeaderXSpamFlag:
176             return getTerm(term, QStringLiteral("xspamflag"));
177         case Akonadi::EmailSearchTerm::Attachment:
178             return Term(QStringLiteral("hasattachment"), !term.isNegated());
179         case Akonadi::EmailSearchTerm::Unknown:
180         default:
181             if (!term.key().isEmpty()) {
182                 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key();
183             }
184         }
185     }
186     return Term();
187 }
188 
recursiveCalendarTermMapping(const Akonadi::SearchTerm & term)189 Term recursiveCalendarTermMapping(const Akonadi::SearchTerm &term)
190 {
191     if (!term.subTerms().isEmpty()) {
192         Term t(mapRelation(term.relation()));
193         for (const Akonadi::SearchTerm &subterm : term.subTerms()) {
194             const Term newTerm = recursiveCalendarTermMapping(subterm);
195             if (newTerm.isValid()) {
196                 t.addSubTerm(newTerm);
197             }
198         }
199         return t;
200     } else {
201         // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value();
202         const Akonadi::IncidenceSearchTerm::IncidenceSearchField field = Akonadi::IncidenceSearchTerm::fromKey(term.key());
203         switch (field) {
204         case Akonadi::IncidenceSearchTerm::Organizer:
205             return getTerm(term, QStringLiteral("organizer"));
206         case Akonadi::IncidenceSearchTerm::Summary:
207             return getTerm(term, QStringLiteral("summary"));
208         case Akonadi::IncidenceSearchTerm::Location:
209             return getTerm(term, QStringLiteral("location"));
210         case Akonadi::IncidenceSearchTerm::PartStatus: {
211             Term t(QStringLiteral("partstatus"), term.value().toString(), Term::Equal);
212             t.setNegation(term.isNegated());
213             return t;
214         }
215         default:
216             if (!term.key().isEmpty()) {
217                 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key();
218             }
219         }
220     }
221     return Term();
222 }
223 
recursiveNoteTermMapping(const Akonadi::SearchTerm & term)224 Term recursiveNoteTermMapping(const Akonadi::SearchTerm &term)
225 {
226     if (!term.subTerms().isEmpty()) {
227         Term t(mapRelation(term.relation()));
228         for (const Akonadi::SearchTerm &subterm : term.subTerms()) {
229             const Term newTerm = recursiveNoteTermMapping(subterm);
230             if (newTerm.isValid()) {
231                 t.addSubTerm(newTerm);
232             }
233         }
234         return t;
235     } else {
236         // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value();
237         const Akonadi::EmailSearchTerm::EmailSearchField field = Akonadi::EmailSearchTerm::fromKey(term.key());
238         switch (field) {
239         case Akonadi::EmailSearchTerm::Subject:
240             return getTerm(term, QStringLiteral("subject"));
241         case Akonadi::EmailSearchTerm::Body:
242             return getTerm(term, QStringLiteral("body"));
243         default:
244             if (!term.key().isEmpty()) {
245                 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key();
246             }
247         }
248     }
249     return Term();
250 }
251 
recursiveContactTermMapping(const Akonadi::SearchTerm & term)252 Term recursiveContactTermMapping(const Akonadi::SearchTerm &term)
253 {
254     if (!term.subTerms().isEmpty()) {
255         Term t(mapRelation(term.relation()));
256         for (const Akonadi::SearchTerm &subterm : term.subTerms()) {
257             const Term newTerm = recursiveContactTermMapping(subterm);
258             if (newTerm.isValid()) {
259                 t.addSubTerm(newTerm);
260             }
261         }
262         return t;
263     } else {
264         // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << term.key() << term.value();
265         const Akonadi::ContactSearchTerm::ContactSearchField field = Akonadi::ContactSearchTerm::fromKey(term.key());
266         switch (field) {
267         case Akonadi::ContactSearchTerm::Name:
268             return getTerm(term, QStringLiteral("name"));
269         case Akonadi::ContactSearchTerm::Email:
270             return getTerm(term, QStringLiteral("email"));
271         case Akonadi::ContactSearchTerm::Nickname:
272             return getTerm(term, QStringLiteral("nick"));
273         case Akonadi::ContactSearchTerm::Uid:
274             return getTerm(term, QStringLiteral("uid"));
275         case Akonadi::ContactSearchTerm::Unknown:
276         default:
277             if (!term.key().isEmpty()) {
278                 qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "unknown term " << term.key();
279             }
280         }
281     }
282     return Term();
283 }
284 
search(const QString & akonadiQuery,const QVector<qint64> & collections,const QStringList & mimeTypes)285 QSet<qint64> SearchPlugin::search(const QString &akonadiQuery, const QVector<qint64> &collections, const QStringList &mimeTypes)
286 {
287     if (akonadiQuery.isEmpty() && collections.isEmpty() && mimeTypes.isEmpty()) {
288         qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "empty query";
289         return {};
290     }
291 
292     Akonadi::SearchQuery searchQuery;
293     if (!akonadiQuery.isEmpty()) {
294         searchQuery = Akonadi::SearchQuery::fromJSON(akonadiQuery.toLatin1());
295         if (searchQuery.isNull() && collections.isEmpty() && mimeTypes.isEmpty()) {
296             return {};
297         }
298     }
299 
300     const Akonadi::SearchTerm term = searchQuery.term();
301 
302     Query query;
303     Term t;
304 
305     if (mimeTypes.contains(QLatin1String("message/rfc822"))) {
306         // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << "mail query";
307         query.setType(QStringLiteral("Email"));
308         t = recursiveEmailTermMapping(term);
309     } else if (mimeTypes.contains(KContacts::Addressee::mimeType()) || mimeTypes.contains(KContacts::ContactGroup::mimeType())) {
310         query.setType(QStringLiteral("Contact"));
311         t = recursiveContactTermMapping(term);
312     } else if (mimeTypes.contains(QLatin1String("text/x-vnd.akonadi.note"))) {
313         query.setType(QStringLiteral("Note"));
314         t = recursiveNoteTermMapping(term);
315     } else if (mimeTypes.contains(QLatin1String("application/x-vnd.akonadi.calendar.event"))
316                || mimeTypes.contains(QLatin1String("application/x-vnd.akonadi.calendar.todo"))
317                || mimeTypes.contains(QLatin1String("application/x-vnd.akonadi.calendar.journal"))
318                || mimeTypes.contains(QLatin1String("application/x-vnd.akonadi.calendar.freebusy"))) {
319         query.setType(QStringLiteral("Calendar"));
320         t = recursiveCalendarTermMapping(term);
321     } else {
322         // Unknown type
323         return {};
324     }
325 
326     if (searchQuery.limit() > 0) {
327         query.setLimit(searchQuery.limit());
328     }
329 
330     // Filter by collection if not empty
331     if (!collections.isEmpty()) {
332         Term parentTerm(Term::And);
333         Term collectionTerm(Term::Or);
334         for (const qint64 col : collections) {
335             collectionTerm.addSubTerm(Term(QStringLiteral("collection"), QString::number(col), Term::Equal));
336         }
337         if (t.isEmpty()) {
338             query.setTerm(collectionTerm);
339         } else {
340             parentTerm.addSubTerm(collectionTerm);
341             parentTerm.addSubTerm(t);
342             query.setTerm(parentTerm);
343         }
344     } else {
345         if (t.subTerms().isEmpty()) {
346             qCWarning(AKONADIPLUGIN_INDEXER_LOG) << "no terms added";
347             return QSet<qint64>();
348         }
349 
350         query.setTerm(t);
351     }
352 
353     QSet<qint64> resultSet;
354     // qCDebug(AKONADIPLUGIN_INDEXER_LOG) << query.toJSON();
355     ResultIterator iter = query.exec();
356     while (iter.next()) {
357         const QByteArray id = iter.id();
358         const int fid = deserialize("akonadi", id);
359         resultSet << fid;
360     }
361     qCDebug(AKONADIPLUGIN_INDEXER_LOG) << "Got" << resultSet.count() << "results";
362     return resultSet;
363 }
364