Mailcommon

searchrulestring.cpp
1 /*
2  SPDX-FileCopyrightText: 2015-2022 Laurent Montel <[email protected]>
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/KMimeMessage>
16 
17 #include <KEmailAddress>
18 
19 #include <KLocalizedString>
20 
21 #include <QRegularExpression>
22 
23 #include <algorithm>
24 
25 using namespace MailCommon;
26 SearchRuleString::SearchRuleString(const QByteArray &field, Function func, const QString &contents)
27  : SearchRule(field, func, contents)
28 {
29 }
30 
31 SearchRuleString::SearchRuleString(const SearchRuleString &other)
32 
33  = default;
34 
35 const 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 
48 SearchRuleString::~SearchRuleString() = default;
49 
50 bool SearchRuleString::isEmpty() const
51 {
52  return field().trimmed().isEmpty() || contents().isEmpty();
53 }
54 
55 SearchRule::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 
73 bool 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 += QLatin1String(", ") + msg->cc()->asUnicodeString();
115  msgContents += QLatin1String(", ") + 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 += QLatin1String(" (<i>") + FilterLog::recode(msgContents) + QLatin1String("</i>)");
172  }
174  }
175  return rc;
176 }
177 
178 void 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 
233 QString SearchRuleString::informationAboutNotValidRules() const
234 {
235  return i18n("String is empty.");
236 }
237 
238 // helper, does the actual comparing
239 bool 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());
290  QStringList::ConstIterator end(addressList.constEnd());
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());
309  QStringList::ConstIterator end(addressList.constEnd());
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: {
328  QString category = contents();
329  const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
330 
331  QStringList::ConstIterator end(addressList.constEnd());
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: {
352  QString category = contents();
353  const QStringList addressList = KEmailAddress::splitAddressList(msgContents.toLower());
354 
355  QStringList::ConstIterator end(addressList.constEnd());
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 }
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray toLower() const const
QString fromUtf8(const char *str, int size)
CaseInsensitive
static QString recode(const QString &plain)
Returns an escaped version of the log which can be used in a HTML document.
Definition: filterlog.cpp:189
void clear()
void chop(int n)
@ RuleResult
Log all rule matching results.
Definition: filterlog.h:53
QList::const_iterator constBegin() const const
KMail Filter Log Collector.
Definition: filterlog.h:32
bool hasPayload() const
KCODECS_EXPORT QStringList splitAddressList(const QString &aStr)
void addSubTerm(const SearchTerm &term)
QString i18n(const char *text, const TYPE &arg...)
const AKONADI_MIME_EXPORT char Envelope[]
RequiredPart
Possible required parts.
Definition: searchrule.h:68
bool isEmpty() const const
static FilterLog * instance()
Returns the single global instance of the filter log.
Definition: filterlog.cpp:72
const AKONADI_MIME_EXPORT char Header[]
This class represents one search pattern rule.
Definition: searchrule.h:23
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
typedef ConstIterator
QString toLower() const const
QList::const_iterator constEnd() const const
const char * constData() const const
void add(const QString &entry, ContentType type)
Adds the given log entry under the given content type to the log.
Definition: filterlog.cpp:129
AddresseeList List
int compare(const QString &other, Qt::CaseSensitivity cs) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
Category category(StandardShortcut id)
const QList< QKeySequence > & end()
The filter dialog.
T payload() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Sep 24 2022 03:58:15 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.