File indexing completed on 2024-05-26 05:25:18

0001 /*
0002 
0003   SPDX-FileCopyrightText: Marc Mutz <mutz@kde.org>
0004   SPDX-FileCopyrightText: 2012 Andras Mantia <amantia@kde.org>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "searchpattern.h"
0010 #include "filter/filterlog.h"
0011 using MailCommon::FilterLog;
0012 #include "mailcommon_debug.h"
0013 #include <Akonadi/ContactSearchJob>
0014 
0015 #include <KMime/KMimeMessage>
0016 
0017 #include <KConfigGroup>
0018 
0019 #include <QDataStream>
0020 #include <QIODevice>
0021 
0022 #include <algorithm>
0023 
0024 using namespace MailCommon;
0025 
0026 //==================================================
0027 //
0028 // class SearchPattern
0029 //
0030 //==================================================
0031 
0032 SearchPattern::SearchPattern()
0033     : QList<SearchRule::Ptr>()
0034 {
0035     init();
0036 }
0037 
0038 SearchPattern::SearchPattern(const KConfigGroup &config)
0039     : QList<SearchRule::Ptr>()
0040 {
0041     readConfig(config);
0042 }
0043 
0044 SearchPattern::~SearchPattern() = default;
0045 
0046 bool SearchPattern::matches(const Akonadi::Item &item, bool ignoreBody) const
0047 {
0048     if (isEmpty()) {
0049         return true;
0050     }
0051     if (!item.hasPayload<KMime::Message::Ptr>()) {
0052         return false;
0053     }
0054 
0055     QList<SearchRule::Ptr>::const_iterator it;
0056     QList<SearchRule::Ptr>::const_iterator end(constEnd());
0057     switch (mOperator) {
0058     case OpAnd: // all rules must match
0059         for (it = constBegin(); it != end; ++it) {
0060             if (!((*it)->requiredPart() == SearchRule::CompleteMessage && ignoreBody)) {
0061                 if (!(*it)->matches(item)) {
0062                     return false;
0063                 }
0064             }
0065         }
0066         return true;
0067 
0068     case OpOr: // at least one rule must match
0069         for (it = constBegin(); it != end; ++it) {
0070             if (!((*it)->requiredPart() == MailCommon::SearchRule::CompleteMessage && ignoreBody)) {
0071                 if ((*it)->matches(item)) {
0072                     return true;
0073                 }
0074             }
0075         }
0076         return false;
0077 
0078     case OpAll:
0079         return true;
0080 
0081     default:
0082         return false;
0083     }
0084 }
0085 
0086 SearchRule::RequiredPart SearchPattern::requiredPart() const
0087 {
0088     SearchRule::RequiredPart reqPart = SearchRule::Envelope;
0089 
0090     if (!isEmpty()) {
0091         reqPart = (*std::max_element(constBegin(), constEnd(), [](const auto &lhs, const auto &rhs) {
0092                       return lhs->requiredPart() < rhs->requiredPart();
0093                   }))->requiredPart();
0094     }
0095     return reqPart;
0096 }
0097 
0098 QString SearchPattern::purify(bool removeAction)
0099 {
0100     QString informationAboutNotValidPattern;
0101     QList<SearchRule::Ptr>::iterator it = end();
0102     while (it != begin()) {
0103         --it;
0104         if ((*it)->isEmpty()) {
0105             if (removeAction) {
0106                 qCDebug(MAILCOMMON_LOG) << "Removing" << (*it)->asString();
0107                 if (!informationAboutNotValidPattern.isEmpty()) {
0108                     informationAboutNotValidPattern += QLatin1Char('\n');
0109                 }
0110                 informationAboutNotValidPattern += (*it)->informationAboutNotValidRules();
0111 
0112                 erase(it);
0113                 it = end();
0114             }
0115         }
0116     }
0117 
0118     return informationAboutNotValidPattern;
0119 }
0120 
0121 void SearchPattern::readConfig(const KConfigGroup &config)
0122 {
0123     init();
0124 
0125     mName = config.readEntry("name");
0126     if (!config.hasKey("rules")) {
0127         qCDebug(MAILCOMMON_LOG) << "Found legacy config! Converting.";
0128         importLegacyConfig(config);
0129         return;
0130     }
0131 
0132     const QString op = config.readEntry("operator");
0133     if (op == QLatin1StringView("or")) {
0134         mOperator = OpOr;
0135     } else if (op == QLatin1StringView("and")) {
0136         mOperator = OpAnd;
0137     } else if (op == QLatin1StringView("all")) {
0138         mOperator = OpAll;
0139     }
0140 
0141     const int nRules = config.readEntry("rules", 0);
0142 
0143     for (int i = 0; i < nRules; ++i) {
0144         SearchRule::Ptr r = SearchRule::createInstanceFromConfig(config, i);
0145         if (!r->isEmpty()) {
0146             append(r);
0147         }
0148     }
0149 }
0150 
0151 void SearchPattern::importLegacyConfig(const KConfigGroup &config)
0152 {
0153     SearchRule::Ptr rule =
0154         SearchRule::createInstance(config.readEntry("fieldA").toLatin1(), config.readEntry("funcA").toLatin1().constData(), config.readEntry("contentsA"));
0155 
0156     if (rule->isEmpty()) {
0157         // if the first rule is invalid,
0158         // we really can't do much heuristics...
0159         return;
0160     }
0161     append(rule);
0162 
0163     const QString sOperator = config.readEntry("operator");
0164     if (sOperator == QLatin1StringView("ignore")) {
0165         return;
0166     }
0167 
0168     rule = SearchRule::createInstance(config.readEntry("fieldB").toLatin1(), config.readEntry("funcB").toLatin1().constData(), config.readEntry("contentsB"));
0169 
0170     if (rule->isEmpty()) {
0171         return;
0172     }
0173     append(rule);
0174 
0175     if (sOperator == QLatin1StringView("or")) {
0176         mOperator = OpOr;
0177         return;
0178     }
0179     // This is the interesting case...
0180     if (sOperator == QLatin1StringView("unless")) { // meaning "and not", ie we need to...
0181         // ...invert the function (e.g. "equals" <-> "doesn't equal")
0182         // We simply toggle the last bit (xor with 0x1)... This assumes that
0183         // SearchRule::Function's come in adjacent pairs of pros and cons
0184         SearchRule::Function func = last()->function();
0185         auto intFunc = (unsigned int)func;
0186         func = SearchRule::Function(intFunc ^ 0x1);
0187 
0188         last()->setFunction(func);
0189     }
0190 
0191     // treat any other case as "and" (our default).
0192 }
0193 
0194 void SearchPattern::writeConfig(KConfigGroup &config) const
0195 {
0196     config.writeEntry("name", mName);
0197     switch (mOperator) {
0198     case OpOr:
0199         config.writeEntry("operator", "or");
0200         break;
0201     case OpAnd:
0202         config.writeEntry("operator", "and");
0203         break;
0204     case OpAll:
0205         config.writeEntry("operator", "all");
0206         break;
0207     }
0208 
0209     int i = 0;
0210     QList<SearchRule::Ptr>::const_iterator it;
0211     QList<SearchRule::Ptr>::const_iterator endIt(constEnd());
0212 
0213     if (count() >= filterRulesMaximumSize()) {
0214         qCDebug(MAILCOMMON_LOG) << "Number of patterns > to filter max rules";
0215     }
0216     for (it = constBegin(); it != endIt && i < filterRulesMaximumSize(); ++i, ++it) {
0217         // we could do this ourselves, but we want the rules to be extensible,
0218         // so we give the rule it's number and let it do the rest.
0219         (*it)->writeConfig(config, i);
0220     }
0221 
0222     // save the total number of rules.
0223     config.writeEntry("rules", i);
0224 }
0225 
0226 int SearchPattern::filterRulesMaximumSize()
0227 {
0228     return 8;
0229 }
0230 
0231 void SearchPattern::init()
0232 {
0233     clear();
0234     mOperator = OpAnd;
0235     mName = QLatin1Char('<') + i18nc("name used for a virgin filter", "unknown") + QLatin1Char('>');
0236 }
0237 
0238 QString SearchPattern::asString() const
0239 {
0240     QString result;
0241     switch (mOperator) {
0242     case OpOr:
0243         result = i18n("(match any of the following)");
0244         break;
0245     case OpAnd:
0246         result = i18n("(match all of the following)");
0247         break;
0248     case OpAll:
0249         result = i18n("(match all messages)");
0250         break;
0251     }
0252 
0253     QList<SearchRule::Ptr>::const_iterator it;
0254     QList<SearchRule::Ptr>::const_iterator endIt = constEnd();
0255     for (it = constBegin(); it != endIt; ++it) {
0256         result += QLatin1StringView("\n\t") + FilterLog::recode((*it)->asString());
0257     }
0258 
0259     return result;
0260 }
0261 
0262 SearchPattern::SparqlQueryError SearchPattern::asAkonadiQuery(Akonadi::SearchQuery &query) const
0263 {
0264     query = Akonadi::SearchQuery();
0265 
0266     Akonadi::SearchTerm term(Akonadi::SearchTerm::RelAnd);
0267     if (op() == SearchPattern::OpOr) {
0268         term = Akonadi::SearchTerm(Akonadi::SearchTerm::RelOr);
0269     }
0270 
0271     const_iterator end(constEnd());
0272     bool emptyIsNotAnError = false;
0273     bool resultAddQuery = false;
0274     for (const_iterator it = constBegin(); it != end; ++it) {
0275         (*it)->addQueryTerms(term, emptyIsNotAnError);
0276         resultAddQuery &= emptyIsNotAnError;
0277     }
0278 
0279     if (term.subTerms().isEmpty()) {
0280         if (resultAddQuery) {
0281             qCDebug(MAILCOMMON_LOG) << " innergroup is Empty. Need to report bug";
0282             return MissingCheck;
0283         } else {
0284             return EmptyResult;
0285         }
0286     }
0287     query.setTerm(term);
0288 
0289     return NoError;
0290 }
0291 
0292 const SearchPattern &SearchPattern::operator=(const SearchPattern &other)
0293 {
0294     if (this == &other) {
0295         return *this;
0296     }
0297 
0298     setOp(other.op());
0299     setName(other.name());
0300 
0301     clear(); // ###
0302     QList<SearchRule::Ptr>::const_iterator it;
0303     QList<SearchRule::Ptr>::const_iterator end(other.constEnd());
0304     for (it = other.constBegin(); it != end; ++it) {
0305         append(SearchRule::createInstance(**it)); // deep copy
0306     }
0307 
0308     return *this;
0309 }
0310 
0311 QByteArray SearchPattern::serialize() const
0312 {
0313     QByteArray out;
0314     QDataStream stream(&out, QIODevice::WriteOnly);
0315     *this >> stream;
0316     return out;
0317 }
0318 
0319 void SearchPattern::deserialize(const QByteArray &str)
0320 {
0321     QDataStream stream(str);
0322     *this << stream;
0323 }
0324 
0325 QDataStream &SearchPattern::operator>>(QDataStream &s) const
0326 {
0327     switch (op()) {
0328     case SearchPattern::OpAnd:
0329         s << QStringLiteral("and");
0330         break;
0331     case SearchPattern::OpOr:
0332         s << QStringLiteral("or");
0333         break;
0334     case SearchPattern::OpAll:
0335         s << QStringLiteral("all");
0336         break;
0337     }
0338 
0339     for (const SearchRule::Ptr &rule : std::as_const(*this)) {
0340         *rule >> s;
0341     }
0342     return s;
0343 }
0344 
0345 QDataStream &SearchPattern::operator<<(QDataStream &s)
0346 {
0347     QString op;
0348     s >> op;
0349     if (op == QLatin1StringView("and")) {
0350         setOp(OpAnd);
0351     } else if (op == QLatin1StringView("or")) {
0352         setOp(OpOr);
0353     } else if (op == QLatin1StringView("all")) {
0354         setOp(OpAll);
0355     }
0356 
0357     while (!s.atEnd()) {
0358         SearchRule::Ptr rule = SearchRule::createInstance(s);
0359         append(rule);
0360     }
0361     return s;
0362 }
0363 
0364 void SearchPattern::generateSieveScript(QStringList &requiresModules, QString &code)
0365 {
0366     code += QLatin1StringView("\n#") + mName + QLatin1Char('\n');
0367     switch (mOperator) {
0368     case OpOr:
0369         code += QLatin1StringView("if anyof (");
0370         break;
0371     case OpAnd:
0372         code += QLatin1StringView("if allof (");
0373         break;
0374     case OpAll:
0375         code += QLatin1StringView("if (true) {");
0376         return;
0377     }
0378 
0379     QList<SearchRule::Ptr>::const_iterator it;
0380     QList<SearchRule::Ptr>::const_iterator endIt(constEnd());
0381     int i = 0;
0382     for (it = constBegin(); it != endIt && i < filterRulesMaximumSize(); ++i, ++it) {
0383         if (i != 0) {
0384             code += QLatin1StringView("\n, ");
0385         }
0386         (*it)->generateSieveScript(requiresModules, code);
0387     }
0388 }