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)
32
33 = default;
34
35const SearchRuleString &SearchRuleString::operator=(const SearchRuleString &other)
36{
37 if (this == &other) {
38 return *this;
39 }
40
41 setField(other.field());
42 setFunction(other.function());
43 setContents(other.contents());
44
45 return *this;
46}
47
48SearchRuleString::~SearchRuleString() = default;
49
50bool SearchRuleString::isEmpty() const
51{
52 return field().trimmed().isEmpty() || contents().isEmpty();
53}
54
55SearchRule::RequiredPart SearchRuleString::requiredPart() const
56{
57 const QByteArray f = field();
59 if (qstricmp(f.constData(), "<recipients>") == 0 || qstricmp(f.constData(), "<status>") == 0 || qstricmp(f.constData(), "<tag>") == 0
60 || qstricmp(f.constData(), "subject") == 0 || qstricmp(f.constData(), "from") == 0 || qstricmp(f.constData(), "sender") == 0
61 || qstricmp(f.constData(), "reply-to") == 0 || qstricmp(f.constData(), "to") == 0 || qstricmp(f.constData(), "cc") == 0
62 || qstricmp(f.constData(), "bcc") == 0 || qstricmp(f.constData(), "in-reply-to") == 0 || qstricmp(f.constData(), "message-id") == 0
63 || qstricmp(f.constData(), "references") == 0) {
64 // these fields are directly provided by KMime::Message, no need to fetch the whole Header part
65 part = Envelope;
66 } else if (qstricmp(f.constData(), "<message>") == 0 || qstricmp(f.constData(), "<body>") == 0) {
67 part = CompleteMessage;
68 }
69
70 return part;
71}
72
73bool SearchRuleString::matches(const Akonadi::Item &item) const
74{
75 if (isEmpty()) {
76 return false;
77 }
78 if (!item.hasPayload<KMime::Message::Ptr>()) {
79 return false;
80 }
81
82 const auto msg = item.payload<KMime::Message::Ptr>();
83 Q_ASSERT(msg.data());
84
85 if (!msg->hasHeader("From")) {
86 msg->parse(); // probably not parsed yet: make sure we can access all headers
87 }
88
89 QString msgContents;
90 // Show the value used to compare the rules against in the log.
91 // Overwrite the value for complete messages and all headers!
92 bool logContents = true;
93
94 if (qstricmp(field().constData(), "<message>") == 0) {
95 msgContents = QString::fromUtf8(msg->encodedContent());
96 logContents = false;
97 } else if (qstricmp(field().constData(), "<body>") == 0) {
98 msgContents = QString::fromUtf8(msg->body());
99 logContents = false;
100 } else if (qstricmp(field().constData(), "<any header>") == 0) {
101 msgContents = QString::fromUtf8(msg->head());
102 logContents = false;
103 } else if (qstricmp(field().constData(), "<recipients>") == 0) {
104 // (mmutz 2001-11-05) hack to fix "<recipients> !contains foo" to
105 // meet user's expectations. See FAQ entry in KDE 2.2.2's KMail
106 // handbook
107 if (function() == FuncEquals || function() == FuncNotEqual) {
108 // do we need to treat this case specially? Ie.: What shall
109 // "equality" mean for recipients.
110 return matchesInternal(msg->to()->asUnicodeString()) || matchesInternal(msg->cc()->asUnicodeString())
111 || matchesInternal(msg->bcc()->asUnicodeString());
112 }
113 msgContents = msg->to()->asUnicodeString();
114 msgContents += QLatin1StringView(", ") + msg->cc()->asUnicodeString();
115 msgContents += QLatin1StringView(", ") + msg->bcc()->asUnicodeString();
116 } else if (qstricmp(field().constData(), "<tag>") == 0) {
117 // port?
118 // const Nepomuk2::Resource res( item.url() );
119 // foreach ( const Nepomuk2::Tag &tag, res.tags() ) {
120 // msgContents += tag.label();
121 // }
122 logContents = false;
123 } else {
124 // make sure to treat messages with multiple header lines for
125 // the same header correctly
126 msgContents.clear();
127 if (auto hrd = msg->headerByType(field().constData())) {
128 msgContents = hrd->asUnicodeString();
129 }
130 }
131
132 if (function() == FuncIsInAddressbook || function() == FuncIsNotInAddressbook) {
133 // I think only the "from"-field makes sense.
134 msgContents.clear();
135 if (auto hrd = msg->headerByType(field().constData())) {
136 msgContents = hrd->asUnicodeString();
137 }
138
139 if (msgContents.isEmpty()) {
140 return (function() == FuncIsInAddressbook) ? false : true;
141 }
142 }
143
144 // these two functions need the kmmessage therefore they don't call matchesInternal
145 if (function() == FuncHasAttachment) {
146 return KMime::hasAttachment(msg.data());
147 } else if (function() == FuncHasNoAttachment) {
148 return !KMime::hasAttachment(msg.data());
149 }
150
151 bool rc = matchesInternal(msgContents);
152 if (!rc) {
153 // Try to search endwith for emails => remove >
154 // Bug 455273
155 if ((qstricmp(field().constData(), "to") == 0) || (qstricmp(field().constData(), "cc") == 0) || (qstricmp(field().constData(), "bcc") == 0)
156 || (qstricmp(field().constData(), "from") == 0) || (qstricmp(field().constData(), "reply-to") == 0)) {
157 if (function() == SearchRule::FuncEndWith || function() == SearchRule::FuncNotEndWith) {
158 QString newContents = msgContents;
159 if (newContents.endsWith(QLatin1Char('>'))) {
160 newContents.chop(1);
161 rc = matchesInternal(newContents);
162 }
163 }
164 }
165 }
166 if (FilterLog::instance()->isLogging()) {
167 QString msgStr = (rc ? QStringLiteral("<font color=#00FF00>1 = </font>") : QStringLiteral("<font color=#FF0000>0 = </font>"));
168 msgStr += FilterLog::recode(asString());
169 // only log headers because messages and bodies can be pretty large
170 if (logContents) {
171 msgStr += QLatin1StringView(" (<i>") + FilterLog::recode(msgContents) + QLatin1StringView("</i>)");
172 }
174 }
175 return rc;
176}
177
178void SearchRuleString::addQueryTerms(Akonadi::SearchTerm &groupTerm, bool &emptyIsNotAnError) const
179{
180 using namespace Akonadi;
181 emptyIsNotAnError = false;
182 SearchTerm termGroup(SearchTerm::RelOr);
183 if (qstricmp(field().constData(), "subject") == 0) {
184 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Subject, contents(), akonadiComparator()));
185 } else if (qstricmp(field().constData(), "reply-to") == 0) {
186 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderReplyTo, contents(), akonadiComparator()));
187 } else if (qstricmp(field().constData(), "<message>") == 0) {
188 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Message, contents(), akonadiComparator()));
189 } else if (field() == "<body>") {
190 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Body, contents(), akonadiComparator()));
191 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Attachment, contents(), akonadiComparator()));
192 } else if (qstricmp(field().constData(), "<recipients>") == 0) {
193 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderTo, contents(), akonadiComparator()));
194 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderCC, contents(), akonadiComparator()));
195 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderBCC, contents(), akonadiComparator()));
196 } else if (qstricmp(field().constData(), "<any header>") == 0) {
197 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Headers, contents(), akonadiComparator()));
198 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Subject, contents(), akonadiComparator()));
199 } else if (qstricmp(field().constData(), "to") == 0) {
200 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderTo, contents(), akonadiComparator()));
201 } else if (qstricmp(field().constData(), "cc") == 0) {
202 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderCC, contents(), akonadiComparator()));
203 } else if (qstricmp(field().constData(), "bcc") == 0) {
204 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderBCC, contents(), akonadiComparator()));
205 } else if (qstricmp(field().constData(), "from") == 0) {
206 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderFrom, contents(), akonadiComparator()));
207 } else if (qstricmp(field().constData(), "list-id") == 0) {
208 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderListId, contents(), akonadiComparator()));
209 } else if (qstricmp(field().constData(), "resent-from") == 0) {
210 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderResentFrom, contents(), akonadiComparator()));
211 } else if (qstricmp(field().constData(), "x-loop") == 0) {
212 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXLoop, contents(), akonadiComparator()));
213 } else if (qstricmp(field().constData(), "x-mailing-list") == 0) {
214 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXMailingList, contents(), akonadiComparator()));
215 } else if (qstricmp(field().constData(), "x-spam-flag") == 0) {
216 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderXSpamFlag, contents(), akonadiComparator()));
217 } else if (qstricmp(field().constData(), "organization") == 0) {
218 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::HeaderOrganization, contents(), akonadiComparator()));
219 } else if (qstricmp(field().constData(), "<tag>") == 0) {
220 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::MessageTag, contents(), akonadiComparator()));
221 } else if (!field().isEmpty()) {
222 termGroup.addSubTerm(EmailSearchTerm(EmailSearchTerm::Headers, contents(), akonadiComparator()));
223 }
224
225 // TODO complete for other headers, generic headers
226
227 if (!termGroup.subTerms().isEmpty()) {
228 termGroup.setIsNegated(isNegated());
229 groupTerm.addSubTerm(termGroup);
230 }
231}
232
233QString SearchRuleString::informationAboutNotValidRules() const
234{
235 return i18n("String is empty.");
236}
237
238// helper, does the actual comparing
239bool SearchRuleString::matchesInternal(const QString &msgContents) const
240{
241 if (msgContents.isEmpty()) {
242 return false;
243 }
244
245 switch (function()) {
246 case SearchRule::FuncEquals:
247 return QString::compare(msgContents.toLower(), contents().toLower()) == 0;
248
249 case SearchRule::FuncNotEqual:
250 return QString::compare(msgContents.toLower(), contents().toLower()) != 0;
251
252 case SearchRule::FuncContains:
253 return msgContents.contains(contents(), Qt::CaseInsensitive);
254
255 case SearchRule::FuncContainsNot:
256 return !msgContents.contains(contents(), Qt::CaseInsensitive);
257
258 case SearchRule::FuncRegExp:
260
261 case SearchRule::FuncNotRegExp:
263
264 case SearchRule::FuncStartWith:
265 return msgContents.startsWith(contents());
266
267 case SearchRule::FuncNotStartWith:
268 return !msgContents.startsWith(contents());
269
270 case SearchRule::FuncEndWith:
271 return msgContents.endsWith(contents());
272
273 case SearchRule::FuncNotEndWith:
274 return !msgContents.endsWith(contents());
275
276 case FuncIsGreater:
277 return QString::compare(msgContents.toLower(), contents().toLower()) > 0;
278
279 case FuncIsLessOrEqual:
280 return QString::compare(msgContents.toLower(), contents().toLower()) <= 0;
281
282 case FuncIsLess:
283 return QString::compare(msgContents.toLower(), contents().toLower()) < 0;
284
285 case FuncIsGreaterOrEqual:
286 return QString::compare(msgContents.toLower(), contents().toLower()) >= 0;
287
288 case FuncIsInAddressbook: {
289 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
291 for (QStringList::ConstIterator it = addressList.constBegin(); (it != end); ++it) {
293 if (!email.isEmpty()) {
294 auto job = new Akonadi::ContactSearchJob();
295 job->setLimit(1);
296 job->setQuery(Akonadi::ContactSearchJob::Email, email);
297 job->exec();
298
299 if (!job->contacts().isEmpty()) {
300 return true;
301 }
302 }
303 }
304 return false;
305 }
306
307 case FuncIsNotInAddressbook: {
308 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
310
311 for (QStringList::ConstIterator it = addressList.constBegin(); (it != end); ++it) {
313 if (!email.isEmpty()) {
314 auto job = new Akonadi::ContactSearchJob();
315 job->setLimit(1);
316 job->setQuery(Akonadi::ContactSearchJob::Email, email);
317 job->exec();
318
319 if (job->contacts().isEmpty()) {
320 return true;
321 }
322 }
323 }
324 return false;
325 }
326
327 case FuncIsInCategory: {
329 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
330
332 for (QStringList::ConstIterator it = addressList.constBegin(); it != end; ++it) {
334 if (!email.isEmpty()) {
335 auto job = new Akonadi::ContactSearchJob();
336 job->setQuery(Akonadi::ContactSearchJob::Email, email);
337 job->exec();
338
339 const KContacts::Addressee::List contacts = job->contacts();
340
341 for (const KContacts::Addressee &contact : contacts) {
342 if (contact.hasCategory(category)) {
343 return true;
344 }
345 }
346 }
347 }
348 return false;
349 }
350
351 case FuncIsNotInCategory: {
353 const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
354
356 for (QStringList::ConstIterator it = addressList.constBegin(); it != end; ++it) {
358 if (!email.isEmpty()) {
359 auto job = new Akonadi::ContactSearchJob();
360 job->setQuery(Akonadi::ContactSearchJob::Email, email);
361 job->exec();
362
363 const KContacts::Addressee::List contacts = job->contacts();
364
365 for (const KContacts::Addressee &contact : contacts) {
366 if (contact.hasCategory(category)) {
367 return false;
368 }
369 }
370 }
371 }
372 return true;
373 }
374 default:;
375 }
376
377 return false;
378}
bool hasPayload() const
T payload() 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:68
@ CompleteMessage
Whole message.
Definition searchrule.h:71
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 Fri Dec 6 2024 12:02:04 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.