Messagelib

filter.cpp
1/******************************************************************************
2 *
3 * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 *
7 *******************************************************************************/
8
9#include "core/filter.h"
10#include "config-messagelist.h"
11#include "core/messageitem.h"
12#include <MessageCore/StringUtil>
13#include <TextUtils/ConvertText>
14
15#include <KRandom>
16#if !FORCE_DISABLE_AKONADI_SEARCH
17#include <PIM/emailquery.h>
18#include <PIM/resultiterator.h>
19#endif
20using namespace MessageList::Core;
21
22Filter::Filter(QObject *parent)
23 : QObject(parent)
24{
25 generateRandomIdentifier();
26}
27
28bool Filter::containString(const QString &searchInString) const
29{
30 bool found = false;
31 const QString searchInStringNormalize{TextUtils::ConvertText::normalize(searchInString)};
32 for (const QString &str : std::as_const(mSearchList)) {
33 if (searchInStringNormalize.contains(TextUtils::ConvertText::normalize(str), Qt::CaseInsensitive)) {
34 found = true;
35 } else {
36 found = false;
37 break;
38 }
39 }
40 return found;
41}
42
43const QString &Filter::iconName() const
44{
45 return mIconName;
46}
47
48void Filter::setIconName(const QString &newIconName)
49{
50 mIconName = newIconName;
51}
52
53void Filter::setOptions(SearchMessageByButtons::SearchOptions newOptions)
54{
55 mOptions = newOptions;
56}
57
58const QString &Filter::filterName() const
59{
60 return mFilterName;
61}
62
63void Filter::setFilterName(const QString &newFilterName)
64{
65 mFilterName = newFilterName;
66}
67
68void Filter::setIdentifier(const QString &newIdentifier)
69{
70 mIdentifier = newIdentifier;
71}
72
73bool Filter::match(const MessageItem *item) const
74{
75 if (!mStatus.isEmpty()) {
76 for (Akonadi::MessageStatus status : std::as_const(mStatus)) {
77 if (!(status & item->status())) {
78 return false;
79 }
80 }
81 }
82
83 if (!mSearchString.isEmpty()) {
84 if (mMatchingItemIds.contains(item->itemId())) {
85 return true;
86 }
87
88 bool searchMatches = false;
89 bool searchEveryWhere = (mOptions & SearchMessageByButtons::SearchEveryWhere);
90 if (containString(item->subject()) && ((mOptions & SearchMessageByButtons::SearchAgainstSubject) || searchEveryWhere)) {
91 searchMatches = true;
92 } else if (containString(item->sender()) && ((mOptions & SearchMessageByButtons::SearchAgainstFrom) || searchEveryWhere)) {
93 searchMatches = true;
94 } else if (containString(item->receiver()) && ((mOptions & SearchMessageByButtons::SearchAgainstTo) || searchEveryWhere)) {
95 searchMatches = true;
96 }
97 if (!searchMatches) {
98 return false;
99 }
100 }
101
102 if (!mTagId.isEmpty()) {
103 // mTagId is a Akonadi::Tag::url
104 const bool tagMatches = item->findTag(mTagId) != nullptr;
105 if (!tagMatches) {
106 return false;
107 }
108 }
109
110 return true;
111}
112
114{
115 return mStatus;
116}
117
119{
120 mStatus = lstStatus;
121}
122
123bool Filter::isEmpty() const
124{
125 if (!mStatus.isEmpty()) {
126 return false;
127 }
128
129 if (!mSearchString.isEmpty()) {
130 return false;
131 }
132
133 if (!mTagId.isEmpty()) {
134 return false;
135 }
136
137 return true;
138}
139
141{
142 mStatus.clear();
143 mSearchString.clear();
144 mTagId.clear();
145 mMatchingItemIds.clear();
146 mSearchList.clear();
147}
148
150{
151 mCurrentFolder = folder;
152}
153
154QList<SearchLineCommand::SearchLineInfo> Filter::searchLineCommands() const
155{
156 const QString text = mSearchString;
158 if (mOptions & SearchMessageByButtons::SearchAgainstBody) {
159 SearchLineCommand::SearchLineInfo i;
160 i.type = SearchLineCommand::SearchLineType::Body;
161 i.argument = mSearchString;
162 if (i.isValid()) {
163 infos.append(std::move(i));
164 }
165 }
166 if (mOptions & SearchMessageByButtons::SearchAgainstSubject) {
167 SearchLineCommand::SearchLineInfo i;
168 i.type = SearchLineCommand::SearchLineType::Subject;
169 i.argument = mSearchString;
170 if (i.isValid()) {
171 infos.append(std::move(i));
172 }
173 }
174 if (mOptions & SearchMessageByButtons::SearchAgainstBcc) {
175 SearchLineCommand::SearchLineInfo i;
176 i.type = SearchLineCommand::SearchLineType::Bcc;
177 i.argument = mSearchString;
178 if (i.isValid()) {
179 infos.append(std::move(i));
180 }
181 }
182 if (mOptions & SearchMessageByButtons::SearchAgainstCc) {
183 SearchLineCommand::SearchLineInfo i;
184 i.type = SearchLineCommand::SearchLineType::Cc;
185 i.argument = mSearchString;
186 if (i.isValid()) {
187 infos.append(std::move(i));
188 }
189 }
190
191 for (Akonadi::MessageStatus status : status()) {
192 if (status.hasAttachment()) {
193 SearchLineCommand::SearchLineInfo i;
194 i.type = SearchLineCommand::SearchLineType::HasAttachment;
195 if (i.isValid()) {
196 infos.append(std::move(i));
197 }
198 } else if (status.hasInvitation()) {
199 SearchLineCommand::SearchLineInfo i;
200 i.type = SearchLineCommand::SearchLineType::HasInvitation;
201 if (i.isValid()) {
202 infos.append(std::move(i));
203 }
204 } else if (status.isImportant()) {
205 SearchLineCommand::SearchLineInfo i;
206 i.type = SearchLineCommand::SearchLineType::IsImportant;
207 if (i.isValid()) {
208 infos.append(std::move(i));
209 }
210 } else if (status.isEncrypted()) {
211 SearchLineCommand::SearchLineInfo i;
212 i.type = SearchLineCommand::SearchLineType::IsEncrypted;
213 if (i.isValid()) {
214 infos.append(std::move(i));
215 }
216 } else if (status.isReplied()) {
217 SearchLineCommand::SearchLineInfo i;
218 i.type = SearchLineCommand::SearchLineType::IsReplied;
219 if (i.isValid()) {
220 infos.append(std::move(i));
221 }
222 } else if (status.isForwarded()) {
223 SearchLineCommand::SearchLineInfo i;
224 i.type = SearchLineCommand::SearchLineType::IsForwarded;
225 if (i.isValid()) {
226 infos.append(std::move(i));
227 }
228 } else if (status.isRead()) {
229 SearchLineCommand::SearchLineInfo i;
230 i.type = SearchLineCommand::SearchLineType::IsRead;
231 if (i.isValid()) {
232 infos.append(std::move(i));
233 }
234 } else if (status.isIgnored()) {
235 SearchLineCommand::SearchLineInfo i;
236 i.type = SearchLineCommand::SearchLineType::IsIgnored;
237 if (i.isValid()) {
238 infos.append(std::move(i));
239 }
240 } else if (status.isSpam()) {
241 SearchLineCommand::SearchLineInfo i;
242 i.type = SearchLineCommand::SearchLineType::IsSpam;
243 if (i.isValid()) {
244 infos.append(std::move(i));
245 }
246 } else if (status.isHam()) {
247 SearchLineCommand::SearchLineInfo i;
248 i.type = SearchLineCommand::SearchLineType::IsHam;
249 if (i.isValid()) {
250 infos.append(std::move(i));
251 }
252 } else if (status.isQueued()) {
253 SearchLineCommand::SearchLineInfo i;
254 i.type = SearchLineCommand::SearchLineType::IsQueued;
255 if (i.isValid()) {
256 infos.append(std::move(i));
257 }
258 } else if (status.isSent()) {
259 SearchLineCommand::SearchLineInfo i;
260 i.type = SearchLineCommand::SearchLineType::IsSent;
261 if (i.isValid()) {
262 infos.append(std::move(i));
263 }
264 } else if (status.isDeleted()) {
265 SearchLineCommand::SearchLineInfo i;
266 i.type = SearchLineCommand::SearchLineType::IsDeleted;
267 if (i.isValid()) {
268 infos.append(std::move(i));
269 }
270 } else if (status.isWatched()) {
271 SearchLineCommand::SearchLineInfo i;
272 i.type = SearchLineCommand::SearchLineType::IsWatched;
273 if (i.isValid()) {
274 infos.append(std::move(i));
275 }
276 } else if (status.isToAct()) {
277 SearchLineCommand::SearchLineInfo i;
278 i.type = SearchLineCommand::SearchLineType::IsAction;
279 if (i.isValid()) {
280 infos.append(std::move(i));
281 }
282 }
283 }
284
285#if 0
286
287 if (mContainsOutboundMessages) {
288 mButtonGroup->button(SearchMessageByButtons::SearchAgainstTo)->setChecked(opts & SearchMessageByButtons::SearchAgainstTo);
289 } else {
290 mButtonGroup->button(SearchMessageByButtons::SearchAgainstTo)->setChecked(opts & SearchMessageByButtons::SearchAgainstFrom);
291 }
292#endif
293 SearchLineCommand command;
294 command.parseSearchLineCommand(text);
295 infos += command.searchLineInfo();
296 return infos;
297}
298
300{
301 return mSearchString;
302}
303
304SearchMessageByButtons::SearchOptions Filter::currentOptions() const
305{
306 return mOptions;
307}
308
309void Filter::save(const KSharedConfig::Ptr &config, const QString &filtername, const QString &iconName, int numFilter)
310{
311 KConfigGroup grp(config, QStringLiteral("General"));
312 int numberFilter = (numFilter == -1) ? grp.readEntry("NumberFilter").toInt() : numFilter;
313 KConfigGroup newGroup(config, QStringLiteral("Filter_%1").arg(numberFilter++));
314 newGroup.writeEntry("name", filtername);
315 if (!iconName.isEmpty()) {
316 newGroup.writeEntry("iconName", iconName);
317 }
318 newGroup.writeEntry("searchString", mSearchString);
319 newGroup.writeEntry("searchOptions", static_cast<int>(mOptions));
320 newGroup.writeEntry("tagId", mTagId);
321 newGroup.writeEntry("identifier", mIdentifier);
322 QList<qint32> lst;
323 lst.reserve(mStatus.count());
324 for (const auto s : std::as_const(mStatus)) {
325 lst << s.toQInt32();
326 }
327 newGroup.writeEntry("status", lst);
328 newGroup.sync();
329 grp.writeEntry("NumberFilter", numberFilter);
330 grp.sync();
331 config->reparseConfiguration();
332}
333
334Filter *Filter::load(const KSharedConfig::Ptr &config, int filternumber)
335{
336 KConfigGroup grp(config, QStringLiteral("General"));
337 int numberFilter = grp.readEntry("NumberFilter").toInt();
338 if (filternumber < numberFilter) {
339 KConfigGroup newGroup(config, QStringLiteral("Filter_%1").arg(filternumber));
340 return loadFromConfigGroup(newGroup);
341 }
342 return nullptr;
343}
344
345Filter *Filter::loadFromConfigGroup(const KConfigGroup &newGroup)
346{
347 auto filter = new Filter();
348 filter->setSearchString(newGroup.readEntry("searchString"),
349 static_cast<SearchMessageByButtons::SearchOptions>(newGroup.readEntry("searchOptions").toInt()));
350 filter->setTagId(newGroup.readEntry("tagId"));
351 filter->setIdentifier(newGroup.readEntry("identifier"));
352 filter->setFilterName(newGroup.readEntry("name"));
353 filter->setIconName(newGroup.readEntry("iconName"));
354 const QList<qint32> lst = newGroup.readEntry("status", QList<qint32>());
355 QList<Akonadi::MessageStatus> messageStatusLst;
356 messageStatusLst.reserve(lst.count());
357 for (const auto s : std::as_const(lst)) {
358 Akonadi::MessageStatus status;
359 status.fromQInt32(s);
360 messageStatusLst << status;
361 }
362 filter->setStatus(messageStatusLst);
363 filter->setOptions(static_cast<SearchMessageByButtons::SearchOptions>(newGroup.readEntry("searchOptions").toInt()));
364 return filter;
365}
366
367void Filter::setSearchString(const SearchLineCommand &command)
368{
369#if !FORCE_DISABLE_AKONADI_SEARCH
370 mMatchingItemIds.clear();
371 if (command.isEmpty()) {
372 return;
373 }
374 const QList<SearchLineCommand::SearchLineInfo> infos = command.searchLineInfo();
375 QList<Akonadi::MessageStatus> lstStatus;
376 Akonadi::Search::PIM::EmailQuery query;
377 for (const auto &info : infos) {
378 switch (info.type) {
379 case SearchLineCommand::Literal: {
380 QString newStr;
381 const QStringList searchListTmp = info.argument.split(QLatin1Char(' '), Qt::SkipEmptyParts);
382 bool needToSplitString = false;
383 for (const QString &text : searchListTmp) {
384 if (text.size() >= 3) {
385 if (!newStr.isEmpty()) {
386 newStr += QLatin1Char(' ');
387 }
388 newStr += text;
389 }
390 }
391 needToSplitString = true;
392
393 mSearchString = newStr;
394 query.matches(newStr);
395 query.setSplitSearchMatchString(needToSplitString);
396 break;
397 }
398 case SearchLineCommand::Subject: {
399 mSearchString = info.argument;
400 query.subjectMatches(mSearchString);
401 break;
402 }
403 case SearchLineCommand::Body: {
404 mSearchString = info.argument;
405 query.bodyMatches(mSearchString);
406 break;
407 }
408 case SearchLineCommand::Unknown:
409 case SearchLineCommand::HasStateOrAttachment:
410 // Nothing
411 break;
412 case SearchLineCommand::Larger:
413 case SearchLineCommand::Smaller:
414 case SearchLineCommand::OlderThan:
415 case SearchLineCommand::NewerThan:
416 case SearchLineCommand::Date:
417 case SearchLineCommand::Size:
418 case SearchLineCommand::Category:
419 // TODO implement tag support
420 break;
421 case SearchLineCommand::HasAttachment: {
422 Akonadi::MessageStatus status;
423 status.setHasAttachment(true);
424 lstStatus.append(status);
425 break;
426 }
427 case SearchLineCommand::HasInvitation: {
428 Akonadi::MessageStatus status;
429 status.setHasInvitation(true);
430 lstStatus.append(status);
431 break;
432 }
433 case SearchLineCommand::IsImportant: {
434 Akonadi::MessageStatus status;
435 status.setImportant(true);
436 lstStatus.append(status);
437 break;
438 }
439 case SearchLineCommand::IsRead: {
440 Akonadi::MessageStatus status;
441 status.setRead(true);
442 lstStatus.append(status);
443 break;
444 }
445 case SearchLineCommand::IsUnRead: {
446 // TODO verify
447 Akonadi::MessageStatus status;
448 status.setRead(false);
449 lstStatus.append(status);
450 break;
451 }
452 case SearchLineCommand::IsIgnored: {
453 Akonadi::MessageStatus status;
454 status.setIgnored(true);
455 lstStatus.append(status);
456 break;
457 }
458 case SearchLineCommand::IsHam: {
459 Akonadi::MessageStatus status;
460 status.setHam(true);
461 lstStatus.append(status);
462 break;
463 }
464 case SearchLineCommand::IsSpam: {
465 Akonadi::MessageStatus status;
466 status.setSpam(true);
467 lstStatus.append(status);
468 break;
469 }
470 case SearchLineCommand::IsWatched: {
471 Akonadi::MessageStatus status;
472 status.setWatched(true);
473 lstStatus.append(status);
474 break;
475 }
476 case SearchLineCommand::IsReplied: {
477 Akonadi::MessageStatus status;
478 status.setReplied(true);
479 lstStatus.append(status);
480 break;
481 }
482 case SearchLineCommand::IsEncrypted: {
483 Akonadi::MessageStatus status;
484 status.setEncrypted(true);
485 lstStatus.append(status);
486 break;
487 }
488 case SearchLineCommand::IsQueued: {
489 Akonadi::MessageStatus status;
490 status.setQueued(true);
491 lstStatus.append(status);
492 break;
493 }
494 case SearchLineCommand::IsAction: {
495 Akonadi::MessageStatus status;
496 status.setToAct(true);
497 lstStatus.append(status);
498 break;
499 }
500 case SearchLineCommand::IsDeleted: {
501 Akonadi::MessageStatus status;
502 status.setDeleted(true);
503 lstStatus.append(status);
504 break;
505 }
506 case SearchLineCommand::IsSent: {
507 Akonadi::MessageStatus status;
508 status.setSent(true);
509 lstStatus.append(status);
510 break;
511 }
512 case SearchLineCommand::IsForwarded: {
513 Akonadi::MessageStatus status;
514 status.setForwarded(true);
515 lstStatus.append(status);
516 break;
517 }
518 case SearchLineCommand::To: {
519 mSearchString = info.argument;
520 query.addTo(info.argument);
521 break;
522 }
523 case SearchLineCommand::Bcc: {
524 mSearchString = info.argument;
525 query.addBcc(info.argument);
526 break;
527 }
528 case SearchLineCommand::From: {
529 mSearchString = info.argument;
530 query.addFrom(info.argument);
531 break;
532 }
533 case SearchLineCommand::Cc: {
534 mSearchString = info.argument;
535 query.addCc(info.argument);
536 break;
537 }
538 }
539 }
540
541 setStatus(lstStatus);
542 // If the collection is virtual we're probably trying to filter the search collection, so we just search globally
543 if (mCurrentFolder.isValid() && !mCurrentFolder.isVirtual()) {
544 query.addCollection(mCurrentFolder.id());
545 }
546
547 Akonadi::Search::PIM::ResultIterator it = query.exec();
548 while (it.next()) {
549 mMatchingItemIds << it.id();
550 }
551 Q_EMIT finished();
552#endif
553}
554
555void Filter::setSearchString(const QString &search, SearchMessageByButtons::SearchOptions options)
556{
557#if !FORCE_DISABLE_AKONADI_SEARCH
558 const QString trimStr = search.trimmed();
559 if ((mSearchString == trimStr) && (mOptions == options)) {
560 return;
561 }
562 mOptions = options;
563 mSearchString = trimStr;
564 mMatchingItemIds.clear();
565
566 if (mSearchString.isEmpty()) {
567 return;
568 }
569 bool needToSplitString = false;
570 QString newStr = mSearchString;
571 if (mSearchString.startsWith(QLatin1Char('"')) && mSearchString.startsWith(QLatin1Char('"'))) {
572 newStr.remove(0, 1);
573 newStr.remove(newStr.length() - 1, 1);
574 mSearchList = QStringList() << newStr;
575 } else {
576 const QStringList searchListTmp = mSearchString.split(QLatin1Char(' '), Qt::SkipEmptyParts);
577 mSearchList.clear();
578 newStr.clear();
579 for (const QString &text : searchListTmp) {
580 if (text.size() >= 3) {
581 mSearchList << text;
582 if (!newStr.isEmpty()) {
583 newStr += QLatin1Char(' ');
584 }
585 newStr += text;
586 }
587 }
588 needToSplitString = true;
589 }
590 if (!newStr.trimmed().isEmpty()) {
592 if (options & SearchMessageByButtons::SearchEveryWhere) {
593 query.matches(newStr);
594 query.setSplitSearchMatchString(needToSplitString);
595 } else if (options & SearchMessageByButtons::SearchAgainstSubject) {
596 query.subjectMatches(newStr);
597 } else if (options & SearchMessageByButtons::SearchAgainstBody) {
598 query.bodyMatches(newStr);
599 } else if (options & SearchMessageByButtons::SearchAgainstFrom) {
600 query.setFrom(newStr);
601 } else if (options & SearchMessageByButtons::SearchAgainstBcc) {
602 query.setBcc(QStringList() << newStr);
603 } else if (options & SearchMessageByButtons::SearchAgainstCc) {
604 query.setCc(QStringList() << newStr);
605 } else if (options & SearchMessageByButtons::SearchAgainstTo) {
606 query.setTo(QStringList() << newStr);
607 }
608
609 // If the collection is virtual we're probably trying to filter the search collection, so we just search globally
610 if (mCurrentFolder.isValid() && !mCurrentFolder.isVirtual()) {
611 query.addCollection(mCurrentFolder.id());
612 }
613
614 Akonadi::Search::PIM::ResultIterator it = query.exec();
615 while (it.next()) {
616 mMatchingItemIds << it.id();
617 }
618 }
619 Q_EMIT finished();
620#endif
621}
622
623const QString &Filter::tagId() const
624{
625 return mTagId;
626}
627
629{
630 mTagId = tagId;
631}
632
633void Filter::generateRandomIdentifier()
634{
635 mIdentifier = KRandom::randomString(16);
636}
637
638QString Filter::identifier() const
639{
640 return mIdentifier;
641}
642
643QDebug operator<<(QDebug d, const MessageList::Core::Filter &t)
644{
645 d << "filtername " << t.filterName();
646 d << "identifier " << t.identifier();
647 d << "search string " << t.searchString();
648 d << "search option " << t.currentOptions();
649 d << "status " << t.status();
650 return d;
651}
652
653#include "moc_filter.cpp"
QString readEntry(const char *key, const char *aDefault=nullptr) const
This class is responsible of matching messages that should be displayed in the View.
Definition filter.h:34
const QString & tagId() const
Returns the currently set MessageItem::Tag id.
Definition filter.cpp:623
bool isEmpty() const
Returns true if this filter is empty (0 status mask, empty search string and empty tag) and it's usel...
Definition filter.cpp:123
void setTagId(const QString &tagId)
Sets the id of a MessageItem::Tag that the matching messages must contain.
Definition filter.cpp:628
QList< Akonadi::MessageStatus > status() const
Returns the currently set status mask.
Definition filter.cpp:113
void clear()
Clears this filter (sets status to 0, search string and tag id to empty strings)
Definition filter.cpp:140
const QString & searchString() const
Returns the currently set search string.
Definition filter.cpp:299
bool match(const MessageItem *item) const
Returns true if the specified parameters match this filter and false otherwise.
Definition filter.cpp:73
void setStatus(const QList< Akonadi::MessageStatus > &lstStatus)
Sets the status mask for this filter.
Definition filter.cpp:118
void setCurrentFolder(const Akonadi::Collection &collection)
Sets the current folder of this filter.
Definition filter.cpp:149
void setSearchString(const QString &search, SearchMessageByButtons::SearchOptions options)
Sets the search string for this filter.
Definition filter.cpp:555
const QString & receiver() const
Returns the receiver associated to this item.
Definition item.cpp:504
const Akonadi::MessageStatus & status() const
Returns the status associated to this Item.
Definition item.cpp:449
const QString & sender() const
Returns the sender associated to this item.
Definition item.cpp:489
const QString & subject() const
Returns the subject associated to this Item.
Definition item.cpp:534
The MessageItem class.
Definition messageitem.h:36
const Tag * findTag(const QString &szTagId) const
Returns Tag associated to this message that has the specified id or 0 if no such tag exists.
Q_SCRIPTABLE CaptureState status()
std::optional< QSqlQuery > query(const QString &queryStatement)
KCOREADDONS_EXPORT QString randomString(int length)
The implementation independent part of the MessageList library.
Definition aggregation.h:22
void append(QList< T > &&value)
void clear()
qsizetype count() const const
void reserve(qsizetype size)
Q_EMITQ_EMIT
void clear()
bool isEmpty() const const
qsizetype length() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
int toInt(bool *ok, int base) const const
QString trimmed() const const
CaseInsensitive
SkipEmptyParts
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 31 2025 12:05:41 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.