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