File indexing completed on 2024-11-17 04:51:14
0001 /* 0002 SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "searchrulestring.h" 0008 #include "filter/filterlog.h" 0009 using MailCommon::FilterLog; 0010 0011 #include <Akonadi/ContactSearchJob> 0012 0013 #include <Akonadi/SearchQuery> 0014 0015 #include <KMime/KMimeMessage> 0016 0017 #include <KEmailAddress> 0018 0019 #include <KLocalizedString> 0020 0021 #include <QRegularExpression> 0022 0023 #include <algorithm> 0024 0025 using namespace MailCommon; 0026 SearchRuleString::SearchRuleString(const QByteArray &field, Function func, const QString &contents) 0027 : SearchRule(field, func, contents) 0028 { 0029 } 0030 0031 SearchRuleString::SearchRuleString(const SearchRuleString &other) 0032 0033 = default; 0034 0035 const SearchRuleString &SearchRuleString::operator=(const SearchRuleString &other) 0036 { 0037 if (this == &other) { 0038 return *this; 0039 } 0040 0041 setField(other.field()); 0042 setFunction(other.function()); 0043 setContents(other.contents()); 0044 0045 return *this; 0046 } 0047 0048 SearchRuleString::~SearchRuleString() = default; 0049 0050 bool SearchRuleString::isEmpty() const 0051 { 0052 return field().trimmed().isEmpty() || contents().isEmpty(); 0053 } 0054 0055 SearchRule::RequiredPart SearchRuleString::requiredPart() const 0056 { 0057 const QByteArray f = field(); 0058 SearchRule::RequiredPart part = Header; 0059 if (qstricmp(f.constData(), "<recipients>") == 0 || qstricmp(f.constData(), "<status>") == 0 || qstricmp(f.constData(), "<tag>") == 0 0060 || qstricmp(f.constData(), "subject") == 0 || qstricmp(f.constData(), "from") == 0 || qstricmp(f.constData(), "sender") == 0 0061 || qstricmp(f.constData(), "reply-to") == 0 || qstricmp(f.constData(), "to") == 0 || qstricmp(f.constData(), "cc") == 0 0062 || qstricmp(f.constData(), "bcc") == 0 || qstricmp(f.constData(), "in-reply-to") == 0 || qstricmp(f.constData(), "message-id") == 0 0063 || qstricmp(f.constData(), "references") == 0) { 0064 // these fields are directly provided by KMime::Message, no need to fetch the whole Header part 0065 part = Envelope; 0066 } else if (qstricmp(f.constData(), "<message>") == 0 || qstricmp(f.constData(), "<body>") == 0) { 0067 part = CompleteMessage; 0068 } 0069 0070 return part; 0071 } 0072 0073 bool SearchRuleString::matches(const Akonadi::Item &item) const 0074 { 0075 if (isEmpty()) { 0076 return false; 0077 } 0078 if (!item.hasPayload<KMime::Message::Ptr>()) { 0079 return false; 0080 } 0081 0082 const auto msg = item.payload<KMime::Message::Ptr>(); 0083 Q_ASSERT(msg.data()); 0084 0085 if (!msg->hasHeader("From")) { 0086 msg->parse(); // probably not parsed yet: make sure we can access all headers 0087 } 0088 0089 QString msgContents; 0090 // Show the value used to compare the rules against in the log. 0091 // Overwrite the value for complete messages and all headers! 0092 bool logContents = true; 0093 0094 if (qstricmp(field().constData(), "<message>") == 0) { 0095 msgContents = QString::fromUtf8(msg->encodedContent()); 0096 logContents = false; 0097 } else if (qstricmp(field().constData(), "<body>") == 0) { 0098 msgContents = QString::fromUtf8(msg->body()); 0099 logContents = false; 0100 } else if (qstricmp(field().constData(), "<any header>") == 0) { 0101 msgContents = QString::fromUtf8(msg->head()); 0102 logContents = false; 0103 } else if (qstricmp(field().constData(), "<recipients>") == 0) { 0104 // (mmutz 2001-11-05) hack to fix "<recipients> !contains foo" to 0105 // meet user's expectations. See FAQ entry in KDE 2.2.2's KMail 0106 // handbook 0107 if (function() == FuncEquals || function() == FuncNotEqual) { 0108 // do we need to treat this case specially? Ie.: What shall 0109 // "equality" mean for recipients. 0110 return matchesInternal(msg->to()->asUnicodeString()) || matchesInternal(msg->cc()->asUnicodeString()) 0111 || matchesInternal(msg->bcc()->asUnicodeString()); 0112 } 0113 msgContents = msg->to()->asUnicodeString(); 0114 msgContents += QLatin1StringView(", ") + msg->cc()->asUnicodeString(); 0115 msgContents += QLatin1StringView(", ") + msg->bcc()->asUnicodeString(); 0116 } else if (qstricmp(field().constData(), "<tag>") == 0) { 0117 // port? 0118 // const Nepomuk2::Resource res( item.url() ); 0119 // foreach ( const Nepomuk2::Tag &tag, res.tags() ) { 0120 // msgContents += tag.label(); 0121 // } 0122 logContents = false; 0123 } else { 0124 // make sure to treat messages with multiple header lines for 0125 // the same header correctly 0126 msgContents.clear(); 0127 if (auto hrd = msg->headerByType(field().constData())) { 0128 msgContents = hrd->asUnicodeString(); 0129 } 0130 } 0131 0132 if (function() == FuncIsInAddressbook || function() == FuncIsNotInAddressbook) { 0133 // I think only the "from"-field makes sense. 0134 msgContents.clear(); 0135 if (auto hrd = msg->headerByType(field().constData())) { 0136 msgContents = hrd->asUnicodeString(); 0137 } 0138 0139 if (msgContents.isEmpty()) { 0140 return (function() == FuncIsInAddressbook) ? false : true; 0141 } 0142 } 0143 0144 // these two functions need the kmmessage therefore they don't call matchesInternal 0145 if (function() == FuncHasAttachment) { 0146 return KMime::hasAttachment(msg.data()); 0147 } else if (function() == FuncHasNoAttachment) { 0148 return !KMime::hasAttachment(msg.data()); 0149 } 0150 0151 bool rc = matchesInternal(msgContents); 0152 if (!rc) { 0153 // Try to search endwith for emails => remove > 0154 // Bug 455273 0155 if ((qstricmp(field().constData(), "to") == 0) || (qstricmp(field().constData(), "cc") == 0) || (qstricmp(field().constData(), "bcc") == 0) 0156 || (qstricmp(field().constData(), "from") == 0) || (qstricmp(field().constData(), "reply-to") == 0)) { 0157 if (function() == SearchRule::FuncEndWith || function() == SearchRule::FuncNotEndWith) { 0158 QString newContents = msgContents; 0159 if (newContents.endsWith(QLatin1Char('>'))) { 0160 newContents.chop(1); 0161 rc = matchesInternal(newContents); 0162 } 0163 } 0164 } 0165 } 0166 if (FilterLog::instance()->isLogging()) { 0167 QString msgStr = (rc ? QStringLiteral("<font color=#00FF00>1 = </font>") : QStringLiteral("<font color=#FF0000>0 = </font>")); 0168 msgStr += FilterLog::recode(asString()); 0169 // only log headers because messages and bodies can be pretty large 0170 if (logContents) { 0171 msgStr += QLatin1StringView(" (<i>") + FilterLog::recode(msgContents) + QLatin1StringView("</i>)"); 0172 } 0173 FilterLog::instance()->add(msgStr, FilterLog::RuleResult); 0174 } 0175 return rc; 0176 } 0177 0178 void SearchRuleString::addQueryTerms(Akonadi::SearchTerm &groupTerm, bool &emptyIsNotAnError) const 0179 { 0180 using namespace Akonadi; 0181 emptyIsNotAnError = false; 0182 SearchTerm termGroup(SearchTerm::RelOr); 0183 if (qstricmp(field().constData(), "subject") == 0) { 0184 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Subject, contents(), akonadiComparator())); 0185 } else if (qstricmp(field().constData(), "reply-to") == 0) { 0186 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderReplyTo, contents(), akonadiComparator())); 0187 } else if (qstricmp(field().constData(), "<message>") == 0) { 0188 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Message, contents(), akonadiComparator())); 0189 } else if (field() == "<body>") { 0190 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Body, contents(), akonadiComparator())); 0191 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Attachment, contents(), akonadiComparator())); 0192 } else if (qstricmp(field().constData(), "<recipients>") == 0) { 0193 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderTo, contents(), akonadiComparator())); 0194 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderCC, contents(), akonadiComparator())); 0195 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderBCC, contents(), akonadiComparator())); 0196 } else if (qstricmp(field().constData(), "<any header>") == 0) { 0197 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Headers, contents(), akonadiComparator())); 0198 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Subject, contents(), akonadiComparator())); 0199 } else if (qstricmp(field().constData(), "to") == 0) { 0200 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderTo, contents(), akonadiComparator())); 0201 } else if (qstricmp(field().constData(), "cc") == 0) { 0202 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderCC, contents(), akonadiComparator())); 0203 } else if (qstricmp(field().constData(), "bcc") == 0) { 0204 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderBCC, contents(), akonadiComparator())); 0205 } else if (qstricmp(field().constData(), "from") == 0) { 0206 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderFrom, contents(), akonadiComparator())); 0207 } else if (qstricmp(field().constData(), "list-id") == 0) { 0208 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderListId, contents(), akonadiComparator())); 0209 } else if (qstricmp(field().constData(), "resent-from") == 0) { 0210 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderResentFrom, contents(), akonadiComparator())); 0211 } else if (qstricmp(field().constData(), "x-loop") == 0) { 0212 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXLoop, contents(), akonadiComparator())); 0213 } else if (qstricmp(field().constData(), "x-mailing-list") == 0) { 0214 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXMailingList, contents(), akonadiComparator())); 0215 } else if (qstricmp(field().constData(), "x-spam-flag") == 0) { 0216 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXSpamFlag, contents(), akonadiComparator())); 0217 } else if (qstricmp(field().constData(), "organization") == 0) { 0218 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderOrganization, contents(), akonadiComparator())); 0219 } else if (qstricmp(field().constData(), "<tag>") == 0) { 0220 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::MessageTag, contents(), akonadiComparator())); 0221 } else if (!field().isEmpty()) { 0222 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Headers, contents(), akonadiComparator())); 0223 } 0224 0225 // TODO complete for other headers, generic headers 0226 0227 if (!termGroup.subTerms().isEmpty()) { 0228 termGroup.setIsNegated(isNegated()); 0229 groupTerm.addSubTerm(termGroup); 0230 } 0231 } 0232 0233 QString SearchRuleString::informationAboutNotValidRules() const 0234 { 0235 return i18n("String is empty."); 0236 } 0237 0238 // helper, does the actual comparing 0239 bool SearchRuleString::matchesInternal(const QString &msgContents) const 0240 { 0241 if (msgContents.isEmpty()) { 0242 return false; 0243 } 0244 0245 switch (function()) { 0246 case SearchRule::FuncEquals: 0247 return QString::compare(msgContents.toLower(), contents().toLower()) == 0; 0248 0249 case SearchRule::FuncNotEqual: 0250 return QString::compare(msgContents.toLower(), contents().toLower()) != 0; 0251 0252 case SearchRule::FuncContains: 0253 return msgContents.contains(contents(), Qt::CaseInsensitive); 0254 0255 case SearchRule::FuncContainsNot: 0256 return !msgContents.contains(contents(), Qt::CaseInsensitive); 0257 0258 case SearchRule::FuncRegExp: 0259 return msgContents.contains(QRegularExpression(contents(), QRegularExpression::CaseInsensitiveOption)); 0260 0261 case SearchRule::FuncNotRegExp: 0262 return !msgContents.contains(QRegularExpression(contents(), QRegularExpression::CaseInsensitiveOption)); 0263 0264 case SearchRule::FuncStartWith: 0265 return msgContents.startsWith(contents()); 0266 0267 case SearchRule::FuncNotStartWith: 0268 return !msgContents.startsWith(contents()); 0269 0270 case SearchRule::FuncEndWith: 0271 return msgContents.endsWith(contents()); 0272 0273 case SearchRule::FuncNotEndWith: 0274 return !msgContents.endsWith(contents()); 0275 0276 case FuncIsGreater: 0277 return QString::compare(msgContents.toLower(), contents().toLower()) > 0; 0278 0279 case FuncIsLessOrEqual: 0280 return QString::compare(msgContents.toLower(), contents().toLower()) <= 0; 0281 0282 case FuncIsLess: 0283 return QString::compare(msgContents.toLower(), contents().toLower()) < 0; 0284 0285 case FuncIsGreaterOrEqual: 0286 return QString::compare(msgContents.toLower(), contents().toLower()) >= 0; 0287 0288 case FuncIsInAddressbook: { 0289 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower()); 0290 QStringList::ConstIterator end(addressList.constEnd()); 0291 for (QStringList::ConstIterator it = addressList.constBegin(); (it != end); ++it) { 0292 const QString email(KEmailAddress::extractEmailAddress(*it).toLower()); 0293 if (!email.isEmpty()) { 0294 auto job = new Akonadi::ContactSearchJob(); 0295 job->setLimit(1); 0296 job->setQuery(Akonadi::ContactSearchJob::Email, email); 0297 job->exec(); 0298 0299 if (!job->contacts().isEmpty()) { 0300 return true; 0301 } 0302 } 0303 } 0304 return false; 0305 } 0306 0307 case FuncIsNotInAddressbook: { 0308 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower()); 0309 QStringList::ConstIterator end(addressList.constEnd()); 0310 0311 for (QStringList::ConstIterator it = addressList.constBegin(); (it != end); ++it) { 0312 const QString email(KEmailAddress::extractEmailAddress(*it).toLower()); 0313 if (!email.isEmpty()) { 0314 auto job = new Akonadi::ContactSearchJob(); 0315 job->setLimit(1); 0316 job->setQuery(Akonadi::ContactSearchJob::Email, email); 0317 job->exec(); 0318 0319 if (job->contacts().isEmpty()) { 0320 return true; 0321 } 0322 } 0323 } 0324 return false; 0325 } 0326 0327 case FuncIsInCategory: { 0328 QString category = contents(); 0329 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower()); 0330 0331 QStringList::ConstIterator end(addressList.constEnd()); 0332 for (QStringList::ConstIterator it = addressList.constBegin(); it != end; ++it) { 0333 const QString email(KEmailAddress::extractEmailAddress(*it).toLower()); 0334 if (!email.isEmpty()) { 0335 auto job = new Akonadi::ContactSearchJob(); 0336 job->setQuery(Akonadi::ContactSearchJob::Email, email); 0337 job->exec(); 0338 0339 const KContacts::Addressee::List contacts = job->contacts(); 0340 0341 for (const KContacts::Addressee &contact : contacts) { 0342 if (contact.hasCategory(category)) { 0343 return true; 0344 } 0345 } 0346 } 0347 } 0348 return false; 0349 } 0350 0351 case FuncIsNotInCategory: { 0352 QString category = contents(); 0353 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower()); 0354 0355 QStringList::ConstIterator end(addressList.constEnd()); 0356 for (QStringList::ConstIterator it = addressList.constBegin(); it != end; ++it) { 0357 const QString email(KEmailAddress::extractEmailAddress(*it).toLower()); 0358 if (!email.isEmpty()) { 0359 auto job = new Akonadi::ContactSearchJob(); 0360 job->setQuery(Akonadi::ContactSearchJob::Email, email); 0361 job->exec(); 0362 0363 const KContacts::Addressee::List contacts = job->contacts(); 0364 0365 for (const KContacts::Addressee &contact : contacts) { 0366 if (contact.hasCategory(category)) { 0367 return false; 0368 } 0369 } 0370 } 0371 } 0372 return true; 0373 } 0374 default:; 0375 } 0376 0377 return false; 0378 }