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