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 }