File indexing completed on 2024-11-10 04:50:15
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 }