File indexing completed on 2024-05-19 05:11:52

0001 /*
0002  * This file is part of the KDE Akonadi Search Project
0003  * SPDX-FileCopyrightText: 2013 Vishesh Handa <me@vhanda.in>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006  *
0007  */
0008 
0009 #include <xapian.h>
0010 
0011 #include "../search/email/agepostingsource.h"
0012 #include "akonadi_search_pim_debug.h"
0013 #include "emailquery.h"
0014 #include "resultiterator_p.h"
0015 
0016 #include <QFile>
0017 #include <QRegularExpression>
0018 #include <QStandardPaths>
0019 
0020 using namespace Akonadi::Search::PIM;
0021 
0022 class Akonadi::Search::PIM::EmailQueryPrivate
0023 {
0024 public:
0025     EmailQueryPrivate();
0026 
0027     QStringList involves;
0028     QStringList to;
0029     QStringList cc;
0030     QStringList bcc;
0031     QString from;
0032 
0033     QList<Akonadi::Collection::Id> collections;
0034 
0035     char important;
0036     char read;
0037     char attachment;
0038 
0039     QString matchString;
0040     QString subjectMatchString;
0041     QString bodyMatchString;
0042 
0043     EmailQuery::OpType opType = EmailQuery::OpAnd;
0044     int limit = 0;
0045     bool splitSearchMatchString = true;
0046 };
0047 
0048 EmailQueryPrivate::EmailQueryPrivate()
0049     : important('0')
0050     , read('0')
0051     , attachment('0')
0052 {
0053 }
0054 
0055 EmailQuery::EmailQuery()
0056     : Query()
0057     , d(new EmailQueryPrivate)
0058 {
0059 }
0060 
0061 EmailQuery::~EmailQuery() = default;
0062 
0063 void EmailQuery::setSplitSearchMatchString(bool split)
0064 {
0065     d->splitSearchMatchString = split;
0066 }
0067 
0068 void EmailQuery::setSearchType(EmailQuery::OpType op)
0069 {
0070     d->opType = op;
0071 }
0072 
0073 void EmailQuery::addInvolves(const QString &email)
0074 {
0075     d->involves << email;
0076 }
0077 
0078 void EmailQuery::setInvolves(const QStringList &involves)
0079 {
0080     d->involves = involves;
0081 }
0082 
0083 void EmailQuery::addBcc(const QString &bcc)
0084 {
0085     d->bcc << bcc;
0086 }
0087 
0088 void EmailQuery::setBcc(const QStringList &bcc)
0089 {
0090     d->bcc = bcc;
0091 }
0092 
0093 void EmailQuery::setCc(const QStringList &cc)
0094 {
0095     d->cc = cc;
0096 }
0097 
0098 void EmailQuery::setFrom(const QString &from)
0099 {
0100     d->from = from;
0101 }
0102 
0103 void EmailQuery::addTo(const QString &to)
0104 {
0105     d->to << to;
0106 }
0107 
0108 void EmailQuery::setTo(const QStringList &to)
0109 {
0110     d->to = to;
0111 }
0112 
0113 void EmailQuery::addCc(const QString &cc)
0114 {
0115     d->cc << cc;
0116 }
0117 
0118 void EmailQuery::addFrom(const QString &from)
0119 {
0120     d->from = from;
0121 }
0122 
0123 void EmailQuery::addCollection(Akonadi::Collection::Id id)
0124 {
0125     d->collections << id;
0126 }
0127 
0128 void EmailQuery::setCollection(const QList<Akonadi::Collection::Id> &collections)
0129 {
0130     d->collections = collections;
0131 }
0132 
0133 int EmailQuery::limit() const
0134 {
0135     return d->limit;
0136 }
0137 
0138 void EmailQuery::setLimit(int limit)
0139 {
0140     d->limit = limit;
0141 }
0142 
0143 void EmailQuery::matches(const QString &match)
0144 {
0145     d->matchString = match;
0146 }
0147 
0148 void EmailQuery::subjectMatches(const QString &subjectMatch)
0149 {
0150     d->subjectMatchString = subjectMatch;
0151 }
0152 
0153 void EmailQuery::bodyMatches(const QString &bodyMatch)
0154 {
0155     d->bodyMatchString = bodyMatch;
0156 }
0157 
0158 void EmailQuery::setAttachment(bool hasAttachment)
0159 {
0160     d->attachment = hasAttachment ? 'T' : 'F';
0161 }
0162 
0163 void EmailQuery::setImportant(bool important)
0164 {
0165     d->important = important ? 'T' : 'F';
0166 }
0167 
0168 void EmailQuery::setRead(bool read)
0169 {
0170     d->read = read ? 'T' : 'F';
0171 }
0172 
0173 ResultIterator EmailQuery::exec()
0174 {
0175     const QString dir = defaultLocation(QStringLiteral("email"));
0176     Xapian::Database db;
0177     try {
0178         db = Xapian::Database(QFile::encodeName(dir).toStdString());
0179     } catch (const Xapian::DatabaseOpeningError &) {
0180         qCWarning(AKONADI_SEARCH_PIM_LOG) << "Xapian Database does not exist at " << dir;
0181         return {};
0182     } catch (const Xapian::DatabaseCorruptError &) {
0183         qCWarning(AKONADI_SEARCH_PIM_LOG) << "Xapian Database corrupted";
0184         return {};
0185     } catch (const Xapian::DatabaseError &e) {
0186         qCWarning(AKONADI_SEARCH_PIM_LOG) << "Failed to open Xapian database:" << QString::fromStdString(e.get_description());
0187         return {};
0188     } catch (...) {
0189         qCWarning(AKONADI_SEARCH_PIM_LOG) << "Random exception, but we do not want to crash";
0190         return {};
0191     }
0192 
0193     QList<Xapian::Query> m_queries;
0194 
0195     if (!d->involves.isEmpty()) {
0196         Xapian::QueryParser parser;
0197         parser.set_database(db);
0198         parser.add_prefix("", "F");
0199         parser.add_prefix("", "T");
0200         parser.add_prefix("", "CC");
0201         parser.add_prefix("", "BCC");
0202 
0203         // vHanda: Do we really need the query parser over here?
0204         for (const QString &str : std::as_const(d->involves)) {
0205             const QByteArray ba = str.toUtf8();
0206             m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0207         }
0208     }
0209 
0210     if (!d->from.isEmpty()) {
0211         Xapian::QueryParser parser;
0212         parser.set_database(db);
0213         parser.add_prefix("", "F");
0214         const QByteArray ba = d->from.toUtf8();
0215         m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0216     }
0217 
0218     if (!d->to.isEmpty()) {
0219         Xapian::QueryParser parser;
0220         parser.set_database(db);
0221         parser.add_prefix("", "T");
0222 
0223         for (const QString &str : std::as_const(d->to)) {
0224             const QByteArray ba = str.toUtf8();
0225             m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0226         }
0227     }
0228 
0229     if (!d->cc.isEmpty()) {
0230         Xapian::QueryParser parser;
0231         parser.set_database(db);
0232         parser.add_prefix("", "CC");
0233 
0234         for (const QString &str : std::as_const(d->cc)) {
0235             const QByteArray ba = str.toUtf8();
0236             m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0237         }
0238     }
0239 
0240     if (!d->bcc.isEmpty()) {
0241         Xapian::QueryParser parser;
0242         parser.set_database(db);
0243         parser.add_prefix("", "BC");
0244 
0245         for (const QString &str : std::as_const(d->bcc)) {
0246             const QByteArray ba = str.toUtf8();
0247             m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0248         }
0249     }
0250 
0251     if (!d->subjectMatchString.isEmpty()) {
0252         Xapian::QueryParser parser;
0253         parser.set_database(db);
0254         parser.add_prefix("", "SU");
0255         parser.set_default_op(Xapian::Query::OP_AND);
0256         const QByteArray ba = d->subjectMatchString.toUtf8();
0257         m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0258     }
0259 
0260     if (!d->collections.isEmpty()) {
0261         Xapian::Query query;
0262         for (Akonadi::Collection::Id id : std::as_const(d->collections)) {
0263             const QString c = QString::number(id);
0264             const Xapian::Query q = Xapian::Query('C' + c.toStdString());
0265 
0266             query = Xapian::Query(Xapian::Query::OP_OR, query, q);
0267         }
0268 
0269         m_queries << query;
0270     }
0271 
0272     if (!d->bodyMatchString.isEmpty()) {
0273         Xapian::QueryParser parser;
0274         parser.set_database(db);
0275         parser.add_prefix("", "BO");
0276         parser.set_default_op(Xapian::Query::OP_AND);
0277         const QByteArray ba = d->bodyMatchString.toUtf8();
0278         m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0279     }
0280 
0281     if (d->important == 'T') {
0282         m_queries << Xapian::Query("BI");
0283     } else if (d->important == 'F') {
0284         m_queries << Xapian::Query("BNI");
0285     }
0286 
0287     if (d->read == 'T') {
0288         m_queries << Xapian::Query("BR");
0289     } else if (d->read == 'F') {
0290         m_queries << Xapian::Query("BNR");
0291     }
0292 
0293     if (d->attachment == 'T') {
0294         m_queries << Xapian::Query("BA");
0295     } else if (d->attachment == 'F') {
0296         m_queries << Xapian::Query("BNA");
0297     }
0298 
0299     if (!d->matchString.isEmpty()) {
0300         Xapian::QueryParser parser;
0301         parser.set_database(db);
0302         parser.set_default_op(Xapian::Query::OP_AND);
0303         if (d->splitSearchMatchString) {
0304             const QStringList list = d->matchString.split(QRegularExpression(QStringLiteral("\\s")), Qt::SkipEmptyParts);
0305             for (const QString &s : list) {
0306                 const QByteArray ba = s.toUtf8();
0307                 m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0308             }
0309         } else {
0310             const QByteArray ba = d->matchString.toUtf8();
0311             m_queries << parser.parse_query(ba.constData(), Xapian::QueryParser::FLAG_PARTIAL);
0312         }
0313     }
0314     Xapian::Query query;
0315     switch (d->opType) {
0316     case OpAnd:
0317         query = Xapian::Query(Xapian::Query::OP_AND, m_queries.begin(), m_queries.end());
0318         break;
0319     case OpOr:
0320         query = Xapian::Query(Xapian::Query::OP_OR, m_queries.begin(), m_queries.end());
0321         break;
0322     }
0323 
0324     AgePostingSource ps(0);
0325     query = Xapian::Query(Xapian::Query::OP_AND_MAYBE, query, Xapian::Query(&ps));
0326 
0327     try {
0328         Xapian::Enquire enquire(db);
0329         enquire.set_query(query);
0330 
0331         if (d->limit == 0) {
0332             // d->limit = 1000000;
0333             d->limit = 100000;
0334         }
0335 
0336         Xapian::MSet mset = enquire.get_mset(0, d->limit);
0337 
0338         ResultIterator iter;
0339         iter.d->init(mset);
0340         return iter;
0341     } catch (const Xapian::Error &e) {
0342         qCWarning(AKONADI_SEARCH_PIM_LOG) << QString::fromStdString(e.get_type()) << QString::fromStdString(e.get_description());
0343         return {};
0344     }
0345 }