Mailcommon

searchrulestring.cpp
1/*
2 SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "searchrulestring.h"
8#include "filter/filterlog.h"
10
11#include <Akonadi/ContactSearchJob>
12
13#include <Akonadi/SearchQuery>
14
15#include <KMime/Message>
16
17#include <KEmailAddress>
18
19#include <KLocalizedString>
20
21#include <QRegularExpression>
22
23#include <algorithm>
24
25using namespace MailCommon;
26SearchRuleString::SearchRuleString(const QByteArray &field, Function func, const QString &contents)
27 : SearchRule(field, func, contents)
28{
29}
30
31SearchRuleString::SearchRuleString(const SearchRuleString &other) = default;
32
33const SearchRuleString &SearchRuleString::operator=(const SearchRuleString &other)
34{
35 if (this == &other) {
36 return *this;
37 }
38
39 setField(other.field());
40 setFunction(other.function());
41 setContents(other.contents());
42
43 return *this;
44}
45
46SearchRuleString::~SearchRuleString() = default;
47
48bool SearchRuleString::isEmpty() const
49{
50 return field().trimmed().isEmpty() || contents().isEmpty();
51}
52
53SearchRule::RequiredPart SearchRuleString::requiredPart() const
54{
55 const QByteArray f = field();
57 if (qstricmp(f.constData(), "<recipients>") == 0 || qstricmp(f.constData(), "<status>") == 0 || qstricmp(f.constData(), "<tag>") == 0
58 || qstricmp(f.constData(), "subject") == 0 || qstricmp(f.constData(), "from") == 0 || qstricmp(f.constData(), "sender") == 0
59 || qstricmp(f.constData(), "reply-to") == 0 || qstricmp(f.constData(), "to") == 0 || qstricmp(f.constData(), "cc") == 0
60 || qstricmp(f.constData(), "bcc") == 0 || qstricmp(f.constData(), "in-reply-to") == 0 || qstricmp(f.constData(), "message-id") == 0
61 || qstricmp(f.constData(), "references") == 0) {
62 // these fields are directly provided by KMime::Message, no need to fetch the whole Header part
63 part = Envelope;
64 } else if (qstricmp(f.constData(), "<message>") == 0 || qstricmp(f.constData(), "<body>") == 0) {
65 part = CompleteMessage;
66 }
67
68 return part;
69}
70
71bool SearchRuleString::matches(const Akonadi::Item &item) const
72{
73 if (isEmpty()) {
74 return false;
75 }
76 if (!item.hasPayload<KMime::Message::Ptr>()) {
77 return false;
78 }
79
80 const auto msg = item.payload<KMime::Message::Ptr>();
81 Q_ASSERT(msg.data());
82
83 if (!msg->hasHeader("From")) {
84 msg->parse(); // probably not parsed yet: make sure we can access all headers
85 }
86
87 QString msgContents;
88 // Show the value used to compare the rules against in the log.
89 // Overwrite the value for complete messages and all headers!
90 bool logContents = true;
91
92 if (qstricmp(field().constData(), "<message>") == 0) {
93 msgContents = QString::fromUtf8(msg->encodedContent());
94 logContents = false;
95 } else if (qstricmp(field().constData(), "<body>") == 0) {
96 msgContents = QString::fromUtf8(msg->body());
97 logContents = false;
98 } else if (qstricmp(field().constData(), "<any header>") == 0) {
99 msgContents = QString::fromUtf8(msg->head());
100 logContents = false;
101 } else if (qstricmp(field().constData(), "<recipients>") == 0) {
102 // (mmutz 2001-11-05) hack to fix "<recipients> !contains foo" to
103 // meet user's expectations. See FAQ entry in KDE 2.2.2's KMail
104 // handbook
105 if (function() == FuncEquals || function() == FuncNotEqual) {
106 // do we need to treat this case specially? Ie.: What shall
107 // "equality" mean for recipients.
108 return matchesInternal(msg->to()->asUnicodeString()) || matchesInternal(msg->cc()->asUnicodeString())
109 || matchesInternal(msg->bcc()->asUnicodeString());
110 }
111 msgContents = msg->to()->asUnicodeString();
112 msgContents += QLatin1StringView(", ") + msg->cc()->asUnicodeString();
113 msgContents += QLatin1StringView(", ") + msg->bcc()->asUnicodeString();
114 } else if (qstricmp(field().constData(), "<tag>") == 0) {
115 // port?
116 // const Nepomuk2::Resource res( item.url() );
117 // foreach ( const Nepomuk2::Tag &tag, res.tags() ) {
118 // msgContents += tag.label();
119 // }
120 logContents = false;
121 } else {
122 // make sure to treat messages with multiple header lines for
123 // the same header correctly
124 msgContents.clear();
125 if (auto hrd = msg->headerByType(field().constData())) {
126 msgContents = hrd->asUnicodeString();
127 }
128 }
129
130 if (function() == FuncIsInAddressbook || function() == FuncIsNotInAddressbook) {
131 // I think only the "from"-field makes sense.
132 msgContents.clear();
133 if (auto hrd = msg->headerByType(field().constData())) {
134 msgContents = hrd->asUnicodeString();
135 }
136
137 if (msgContents.isEmpty()) {
138 return (function() == FuncIsInAddressbook) ? false : true;
139 }
140 }
141
142 // these two functions need the kmmessage therefore they don't call matchesInternal
143 if (function() == FuncHasAttachment) {
144 return KMime::hasAttachment(msg.data());
145 } else if (function() == FuncHasNoAttachment) {
146 return !KMime::hasAttachment(msg.data());
147 }
148
149 bool rc = matchesInternal(msgContents);
150 if (!rc) {
151 // Try to search endwith for emails => remove >
152 // Bug 455273
153 if ((qstricmp(field().constData(), "to") == 0) || (qstricmp(field().constData(), "cc") == 0) || (qstricmp(field().constData(), "bcc") == 0)
154 || (qstricmp(field().constData(), "from") == 0) || (qstricmp(field().constData(), "reply-to") == 0)) {
155 if (function() == SearchRule::FuncEndWith || function() == SearchRule::FuncNotEndWith) {
156 QString newContents = msgContents;
157 if (newContents.endsWith(QLatin1Char('>'))) {
158 newContents.chop(1);
159 rc = matchesInternal(newContents);
160 }
161 }
162 }
163 }
164 if (FilterLog::instance()->isLogging()) {
165 QString msgStr = (rc ? QStringLiteral("<font color=#00FF00>1 = </font>") : QStringLiteral("<font color=#FF0000>0 = </font>"));
166 msgStr += FilterLog::recode(asString());
167 // only log headers because messages and bodies can be pretty large
168 if (logContents) {
169 msgStr += QLatin1StringView(" (<i>") + FilterLog::recode(msgContents) + QLatin1StringView("</i>)");
170 }
172 }
173 return rc;
174}
175
176void SearchRuleString::addQueryTerms(Akonadi::SearchTerm &groupTerm, bool &emptyIsNotAnError) const
177{
178 using namespace Akonadi;
179 emptyIsNotAnError = false;
180 SearchTerm termGroup(SearchTerm::RelOr);
181 if (qstricmp(field().constData(), "subject") == 0) {
182 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Subject, contents(), akonadiComparator()));
183 } else if (qstricmp(field().constData(), "reply-to") == 0) {
184 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderReplyTo, contents(), akonadiComparator()));
185 } else if (qstricmp(field().constData(), "<message>") == 0) {
186 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Message, contents(), akonadiComparator()));
187 } else if (field() == "<body>") {
188 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Body, contents(), akonadiComparator()));
189 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Attachment, contents(), akonadiComparator()));
190 } else if (qstricmp(field().constData(), "<recipients>") == 0) {
191 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderTo, contents(), akonadiComparator()));
192 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderCC, contents(), akonadiComparator()));
193 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderBCC, contents(), akonadiComparator()));
194 } else if (qstricmp(field().constData(), "<any header>") == 0) {
195 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Headers, contents(), akonadiComparator()));
196 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Subject, contents(), akonadiComparator()));
197 } else if (qstricmp(field().constData(), "to") == 0) {
198 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderTo, contents(), akonadiComparator()));
199 } else if (qstricmp(field().constData(), "cc") == 0) {
200 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderCC, contents(), akonadiComparator()));
201 } else if (qstricmp(field().constData(), "bcc") == 0) {
202 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderBCC, contents(), akonadiComparator()));
203 } else if (qstricmp(field().constData(), "from") == 0) {
204 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderFrom, contents(), akonadiComparator()));
205 } else if (qstricmp(field().constData(), "list-id") == 0) {
206 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderListId, contents(), akonadiComparator()));
207 } else if (qstricmp(field().constData(), "resent-from") == 0) {
208 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderResentFrom, contents(), akonadiComparator()));
209 } else if (qstricmp(field().constData(), "x-loop") == 0) {
210 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXLoop, contents(), akonadiComparator()));
211 } else if (qstricmp(field().constData(), "x-mailing-list") == 0) {
212 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXMailingList, contents(), akonadiComparator()));
213 } else if (qstricmp(field().constData(), "x-spam-flag") == 0) {
214 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXSpamFlag, contents(), akonadiComparator()));
215 } else if (qstricmp(field().constData(), "organization") == 0) {
216 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderOrganization, contents(), akonadiComparator()));
217 } else if (qstricmp(field().constData(), "<tag>") == 0) {
218 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::MessageTag, contents(), akonadiComparator()));
219 } else if (!field().isEmpty()) {
220 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Headers, contents(), akonadiComparator()));
221 }
222
223 // TODO complete for other headers, generic headers
224
225 if (!termGroup.subTerms().isEmpty()) {
226 termGroup.setIsNegated(isNegated());
227 groupTerm.addSubTerm(termGroup);
228 }
229}
230
231QString SearchRuleString::informationAboutNotValidRules() const
232{
233 return i18n("String is empty.");
234}
235
236// helper, does the actual comparing
237bool SearchRuleString::matchesInternal(const QString &msgContents) const
238{
239 if (msgContents.isEmpty()) {
240 return false;
241 }
242
243 switch (function()) {
244 case SearchRule::FuncEquals:
245 return QString::compare(msgContents.toLower(), contents().toLower()) == 0;
246
247 case SearchRule::FuncNotEqual:
248 return QString::compare(msgContents.toLower(), contents().toLower()) != 0;
249
250 case SearchRule::FuncContains:
251 return msgContents.contains(contents(), Qt::CaseInsensitive);
252
253 case SearchRule::FuncContainsNot:
254 return !msgContents.contains(contents(), Qt::CaseInsensitive);
255
256 case SearchRule::FuncRegExp:
258
259 case SearchRule::FuncNotRegExp:
261
262 case SearchRule::FuncStartWith:
263 return msgContents.startsWith(contents());
264
265 case SearchRule::FuncNotStartWith:
266 return !msgContents.startsWith(contents());
267
268 case SearchRule::FuncEndWith:
269 return msgContents.endsWith(contents());
270
271 case SearchRule::FuncNotEndWith:
272 return !msgContents.endsWith(contents());
273
274 case FuncIsGreater:
275 return QString::compare(msgContents.toLower(), contents().toLower()) > 0;
276
277 case FuncIsLessOrEqual:
278 return QString::compare(msgContents.toLower(), contents().toLower()) <= 0;
279
280 case FuncIsLess:
281 return QString::compare(msgContents.toLower(), contents().toLower()) < 0;
282
283 case FuncIsGreaterOrEqual:
284 return QString::compare(msgContents.toLower(), contents().toLower()) >= 0;
285
286 case FuncIsInAddressbook: {
287 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
289 for (QStringList::ConstIterator it = addressList.constBegin(); (it != end); ++it) {
291 if (!email.isEmpty()) {
292 auto job = new Akonadi::ContactSearchJob();
293 job->setLimit(1);
294 job->setQuery(Akonadi::ContactSearchJob::Email, email);
295 job->exec();
296
297 if (!job->contacts().isEmpty()) {
298 return true;
299 }
300 }
301 }
302 return false;
303 }
304
305 case FuncIsNotInAddressbook: {
306 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
308
309 for (QStringList::ConstIterator it = addressList.constBegin(); (it != end); ++it) {
311 if (!email.isEmpty()) {
312 auto job = new Akonadi::ContactSearchJob();
313 job->setLimit(1);
314 job->setQuery(Akonadi::ContactSearchJob::Email, email);
315 job->exec();
316
317 if (job->contacts().isEmpty()) {
318 return true;
319 }
320 }
321 }
322 return false;
323 }
324
325 case FuncIsInCategory: {
327 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
328
330 for (QStringList::ConstIterator it = addressList.constBegin(); it != end; ++it) {
332 if (!email.isEmpty()) {
333 auto job = new Akonadi::ContactSearchJob();
334 job->setQuery(Akonadi::ContactSearchJob::Email, email);
335 job->exec();
336
337 const KContacts::Addressee::List contacts = job->contacts();
338
339 for (const KContacts::Addressee &contact : contacts) {
340 if (contact.hasCategory(category)) {
341 return true;
342 }
343 }
344 }
345 }
346 return false;
347 }
348
349 case FuncIsNotInCategory: {
351 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
352
354 for (QStringList::ConstIterator it = addressList.constBegin(); it != end; ++it) {
356 if (!email.isEmpty()) {
357 auto job = new Akonadi::ContactSearchJob();
358 job->setQuery(Akonadi::ContactSearchJob::Email, email);
359 job->exec();
360
361 const KContacts::Addressee::List contacts = job->contacts();
362
363 for (const KContacts::Addressee &contact : contacts) {
364 if (contact.hasCategory(category)) {
365 return false;
366 }
367 }
368 }
369 }
370 return true;
371 }
372 default:;
373 }
374
375 return false;
376}
T payload() const
bool hasPayload() const
void addSubTerm(const SearchTerm &term)
AddresseeList List
KMail Filter Log Collector.
Definition filterlog.h:33
void add(const QString &entry, ContentType type)
Adds the given log entry under the given content type to the log.
@ RuleResult
Log all rule matching results.
Definition filterlog.h:53
static QString recode(const QString &plain)
Returns an escaped version of the log which can be used in a HTML document.
static FilterLog * instance()
Returns the single global instance of the filter log.
Definition filterlog.cpp:71
This class represents one search pattern rule.
Definition searchrule.h:24
QByteArray field() const
Returns the message header field name (without the trailing ':').
Function function() const
Returns the filter function of the rule.
QString contents() const
Returns the contents of the rule.
void setField(const QByteArray &name)
Sets the message header field name.
Akonadi::SearchTerm::Condition akonadiComparator() const
Converts the rule function into the corresponding Akonadi query operator.
void setFunction(Function function)
Sets the filter function of the rule.
RequiredPart
Possible required parts.
Definition searchrule.h:70
@ CompleteMessage
Whole message.
Definition searchrule.h:73
bool isNegated() const
Helper that returns whether the rule has a negated function.
const QString asString() const
Returns the rule as string for debugging purpose.
void setContents(const QString &contents)
Set the contents of the rule.
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
KCODECS_EXPORT QStringList splitAddressList(const QString &aStr)
QString i18n(const char *text, const TYPE &arg...)
Category category(StandardShortcut id)
const QList< QKeySequence > & end()
The filter dialog.
const char * constData() const const
bool isEmpty() const const
QByteArray toLower() const const
QByteArray trimmed() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
void chop(qsizetype n)
void clear()
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:00:25 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.