1 /*
2 * This file is part of the KDE Akonadi Search Project
3 * SPDX-FileCopyrightText: 2013 Vishesh Handa <me@vhanda.in>
4 *
5 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 *
7 */
8
9 #include <xapian.h>
10
11 #include "../search/email/agepostingsource.h"
12 #include "akonadi_search_pim_debug.h"
13 #include "emailquery.h"
14 #include "resultiterator_p.h"
15
16 #include <QFile>
17 #include <QRegularExpression>
18 #include <QStandardPaths>
19
20 using namespace Akonadi::Search::PIM;
21
22 class Akonadi::Search::PIM::EmailQueryPrivate
23 {
24 public:
25 EmailQueryPrivate();
26
27 QStringList involves;
28 QStringList to;
29 QStringList cc;
30 QStringList bcc;
31 QString from;
32
33 QList<Akonadi::Collection::Id> collections;
34
35 char important;
36 char read;
37 char attachment;
38
39 QString matchString;
40 QString subjectMatchString;
41 QString bodyMatchString;
42
43 EmailQuery::OpType opType = EmailQuery::OpAnd;
44 int limit = 0;
45 bool splitSearchMatchString = true;
46 };
47
EmailQueryPrivate()48 EmailQueryPrivate::EmailQueryPrivate()
49 : important('0')
50 , read('0')
51 , attachment('0')
52 {
53 }
54
EmailQuery()55 EmailQuery::EmailQuery()
56 : Query()
57 , d(new EmailQueryPrivate)
58 {
59 }
60
61 EmailQuery::~EmailQuery() = default;
62
setSplitSearchMatchString(bool split)63 void EmailQuery::setSplitSearchMatchString(bool split)
64 {
65 d->splitSearchMatchString = split;
66 }
67
setSearchType(EmailQuery::OpType op)68 void EmailQuery::setSearchType(EmailQuery::OpType op)
69 {
70 d->opType = op;
71 }
72
addInvolves(const QString & email)73 void EmailQuery::addInvolves(const QString &email)
74 {
75 d->involves << email;
76 }
77
setInvolves(const QStringList & involves)78 void EmailQuery::setInvolves(const QStringList &involves)
79 {
80 d->involves = involves;
81 }
82
addBcc(const QString & bcc)83 void EmailQuery::addBcc(const QString &bcc)
84 {
85 d->bcc << bcc;
86 }
87
setBcc(const QStringList & bcc)88 void EmailQuery::setBcc(const QStringList &bcc)
89 {
90 d->bcc = bcc;
91 }
92
setCc(const QStringList & cc)93 void EmailQuery::setCc(const QStringList &cc)
94 {
95 d->cc = cc;
96 }
97
setFrom(const QString & from)98 void EmailQuery::setFrom(const QString &from)
99 {
100 d->from = from;
101 }
102
addTo(const QString & to)103 void EmailQuery::addTo(const QString &to)
104 {
105 d->to << to;
106 }
107
setTo(const QStringList & to)108 void EmailQuery::setTo(const QStringList &to)
109 {
110 d->to = to;
111 }
112
addCc(const QString & cc)113 void EmailQuery::addCc(const QString &cc)
114 {
115 d->cc << cc;
116 }
117
addFrom(const QString & from)118 void EmailQuery::addFrom(const QString &from)
119 {
120 d->from = from;
121 }
122
addCollection(Akonadi::Collection::Id id)123 void EmailQuery::addCollection(Akonadi::Collection::Id id)
124 {
125 d->collections << id;
126 }
127
setCollection(const QList<Akonadi::Collection::Id> & collections)128 void EmailQuery::setCollection(const QList<Akonadi::Collection::Id> &collections)
129 {
130 d->collections = collections;
131 }
132
limit() const133 int EmailQuery::limit() const
134 {
135 return d->limit;
136 }
137
setLimit(int limit)138 void EmailQuery::setLimit(int limit)
139 {
140 d->limit = limit;
141 }
142
matches(const QString & match)143 void EmailQuery::matches(const QString &match)
144 {
145 d->matchString = match;
146 }
147
subjectMatches(const QString & subjectMatch)148 void EmailQuery::subjectMatches(const QString &subjectMatch)
149 {
150 d->subjectMatchString = subjectMatch;
151 }
152
bodyMatches(const QString & bodyMatch)153 void EmailQuery::bodyMatches(const QString &bodyMatch)
154 {
155 d->bodyMatchString = bodyMatch;
156 }
157
setAttachment(bool hasAttachment)158 void EmailQuery::setAttachment(bool hasAttachment)
159 {
160 d->attachment = hasAttachment ? 'T' : 'F';
161 }
162
setImportant(bool important)163 void EmailQuery::setImportant(bool important)
164 {
165 d->important = important ? 'T' : 'F';
166 }
167
setRead(bool read)168 void EmailQuery::setRead(bool read)
169 {
170 d->read = read ? 'T' : 'F';
171 }
172
exec()173 ResultIterator EmailQuery::exec()
174 {
175 const QString dir = defaultLocation(QStringLiteral("email"));
176 Xapian::Database db;
177 try {
178 db = Xapian::Database(QFile::encodeName(dir).toStdString());
179 } catch (const Xapian::DatabaseOpeningError &) {
180 qCWarning(AKONADI_SEARCH_PIM_LOG) << "Xapian Database does not exist at " << dir;
181 return ResultIterator();
182 } catch (const Xapian::DatabaseCorruptError &) {
183 qCWarning(AKONADI_SEARCH_PIM_LOG) << "Xapian Database corrupted";
184 return ResultIterator();
185 } catch (const Xapian::DatabaseError &e) {
186 qCWarning(AKONADI_SEARCH_PIM_LOG) << "Failed to open Xapian database:" << QString::fromStdString(e.get_description());
187 return ResultIterator();
188 } catch (...) {
189 qCWarning(AKONADI_SEARCH_PIM_LOG) << "Random exception, but we do not want to crash";
190 return ResultIterator();
191 }
192
193 QList<Xapian::Query> m_queries;
194
195 if (!d->involves.isEmpty()) {
196 Xapian::QueryParser parser;
197 parser.set_database(db);
198 parser.add_prefix("", "F");
199 parser.add_prefix("", "T");
200 parser.add_prefix("", "CC");
201 parser.add_prefix("", "BCC");
202
203 // vHanda: Do we really need the query parser over here?
204 for (const QString &str : std::as_const(d->involves)) {
205 const QByteArray ba = str.toUtf8();
206 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
207 }
208 }
209
210 if (!d->from.isEmpty()) {
211 Xapian::QueryParser parser;
212 parser.set_database(db);
213 parser.add_prefix("", "F");
214 const QByteArray ba = d->from.toUtf8();
215 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
216 }
217
218 if (!d->to.isEmpty()) {
219 Xapian::QueryParser parser;
220 parser.set_database(db);
221 parser.add_prefix("", "T");
222
223 for (const QString &str : std::as_const(d->to)) {
224 const QByteArray ba = str.toUtf8();
225 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
226 }
227 }
228
229 if (!d->cc.isEmpty()) {
230 Xapian::QueryParser parser;
231 parser.set_database(db);
232 parser.add_prefix("", "CC");
233
234 for (const QString &str : std::as_const(d->cc)) {
235 const QByteArray ba = str.toUtf8();
236 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
237 }
238 }
239
240 if (!d->bcc.isEmpty()) {
241 Xapian::QueryParser parser;
242 parser.set_database(db);
243 parser.add_prefix("", "BC");
244
245 for (const QString &str : std::as_const(d->bcc)) {
246 const QByteArray ba = str.toUtf8();
247 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
248 }
249 }
250
251 if (!d->subjectMatchString.isEmpty()) {
252 Xapian::QueryParser parser;
253 parser.set_database(db);
254 parser.add_prefix("", "SU");
255 parser.set_default_op(Xapian::Query::OP_AND);
256 const QByteArray ba = d->subjectMatchString.toUtf8();
257 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
258 }
259
260 if (!d->collections.isEmpty()) {
261 Xapian::Query query;
262 for (Akonadi::Collection::Id id : std::as_const(d->collections)) {
263 const QString c = QString::number(id);
264 const Xapian::Query q = Xapian::Query('C' + c.toStdString());
265
266 query = Xapian::Query(Xapian::Query::OP_OR, query, q);
267 }
268
269 m_queries << query;
270 }
271
272 if (!d->bodyMatchString.isEmpty()) {
273 Xapian::QueryParser parser;
274 parser.set_database(db);
275 parser.add_prefix("", "BO");
276 parser.set_default_op(Xapian::Query::OP_AND);
277 const QByteArray ba = d->bodyMatchString.toUtf8();
278 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
279 }
280
281 if (d->important == 'T') {
282 m_queries << Xapian::Query("BI");
283 } else if (d->important == 'F') {
284 m_queries << Xapian::Query("BNI");
285 }
286
287 if (d->read == 'T') {
288 m_queries << Xapian::Query("BR");
289 } else if (d->read == 'F') {
290 m_queries << Xapian::Query("BNR");
291 }
292
293 if (d->attachment == 'T') {
294 m_queries << Xapian::Query("BA");
295 } else if (d->attachment == 'F') {
296 m_queries << Xapian::Query("BNA");
297 }
298
299 if (!d->matchString.isEmpty()) {
300 Xapian::QueryParser parser;
301 parser.set_database(db);
302 parser.set_default_op(Xapian::Query::OP_AND);
303 if (d->splitSearchMatchString) {
304 const QStringList list = d->matchString.split(QRegularExpression(QStringLiteral("\\s")), Qt::SkipEmptyParts);
305 for (const QString &s : list) {
306 const QByteArray ba = s.toUtf8();
307 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
308 }
309 } else {
310 const QByteArray ba = d->matchString.toUtf8();
311 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
312 }
313 }
314 Xapian::Query query;
315 switch (d->opType) {
316 case OpAnd:
317 query = Xapian::Query(Xapian::Query::OP_AND, m_queries.begin(), m_queries.end());
318 break;
319 case OpOr:
320 query = Xapian::Query(Xapian::Query::OP_OR, m_queries.begin(), m_queries.end());
321 break;
322 }
323
324 AgePostingSource ps(0);
325 query = Xapian::Query(Xapian::Query::OP_AND_MAYBE, query, Xapian::Query(&ps));
326
327 try {
328 Xapian::Enquire enquire(db);
329 enquire.set_query(query);
330
331 if (d->limit == 0) {
332 // d->limit = 1000000;
333 d->limit = 100000;
334 }
335
336 Xapian::MSet mset = enquire.get_mset(0, d->limit);
337
338 ResultIterator iter;
339 iter.d->init(mset);
340 return iter;
341 } catch (const Xapian::Error &e) {
342 qCWarning(AKONADI_SEARCH_PIM_LOG) << QString::fromStdString(e.get_type()) << QString::fromStdString(e.get_description());
343 return ResultIterator();
344 }
345 }
346