KIMAP

searchjob.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Andras Mantia <amantia@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "searchjob.h"
8
9#include "kimap_debug.h"
10#include <KLocalizedString>
11
12#include <QDate>
13
14#include "imapset.h"
15#include "job_p.h"
16#include "response_p.h"
17#include "session_p.h"
18
19namespace KIMAP
20{
21class Term::Private : public QSharedData
22{
23public:
24 Private()
25 : QSharedData()
26 , isFuzzy(false)
27 , isNegated(false)
28 , isNull(false)
29 {
30 }
31 Private(const Private &other)
32 : QSharedData(other)
33 , command(other.command)
34 , isFuzzy(other.isFuzzy)
35 , isNegated(other.isNegated)
36 , isNull(other.isNull)
37 {
38 }
39
40 Private &operator=(const Private &other)
41 {
42 command = other.command;
43 isFuzzy = other.isFuzzy;
44 isNegated = other.isNegated;
45 isNull = other.isNull;
46 return *this;
47 }
48
49 QByteArray command;
50 bool isFuzzy;
51 bool isNegated;
52 bool isNull;
53};
54
55Term::Term()
56 : d(new Term::Private)
57{
58 d->isNull = true;
59}
60
61Term::Term(Term::Relation relation, const QList<Term> &subterms)
62 : d(new Term::Private)
63{
64 if (subterms.size() >= 2) {
65 if (relation == KIMAP::Term::Or) {
66 for (int i = 0; i < subterms.size() - 1; ++i) {
67 d->command += "(OR " + subterms[i].serialize() + " ";
68 }
69 d->command += subterms.back().serialize();
70 for (int i = 0; i < subterms.size() - 1; ++i) {
71 d->command += ")";
72 }
73 } else {
74 d->command += "(";
75 for (const Term &t : subterms) {
76 d->command += t.serialize() + ' ';
77 }
78 if (!subterms.isEmpty()) {
79 d->command.chop(1);
80 }
81 d->command += ")";
82 }
83 } else if (subterms.size() == 1) {
84 d->command += subterms.first().serialize();
85 } else {
86 d->isNull = true;
87 }
88}
89
90Term::Term(Term::SearchKey key, const QString &value)
91 : d(new Term::Private)
92{
93 switch (key) {
94 case All:
95 d->command += "ALL";
96 break;
97 case Bcc:
98 d->command += "BCC";
99 break;
100 case Cc:
101 d->command += "CC";
102 break;
103 case Body:
104 d->command += "BODY";
105 break;
106 case From:
107 d->command += "FROM";
108 break;
109 case Keyword:
110 d->command += "KEYWORD";
111 break;
112 case Subject:
113 d->command += "SUBJECT";
114 break;
115 case Text:
116 d->command += "TEXT";
117 break;
118 case To:
119 d->command += "TO";
120 break;
121 }
122 if (key != All) {
123 d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\"";
124 }
125}
126
127Term::Term(const QString &header, const QString &value)
128 : d(new Term::Private)
129{
130 d->command += "HEADER";
131 d->command += ' ' + QByteArray(header.toUtf8().constData());
132 d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\"";
133}
134
135Term::Term(Term::BooleanSearchKey key)
136 : d(new Term::Private)
137{
138 switch (key) {
139 case Answered:
140 d->command = "ANSWERED";
141 break;
142 case Deleted:
143 d->command = "DELETED";
144 break;
145 case Draft:
146 d->command = "DRAFT";
147 break;
148 case Flagged:
149 d->command = "FLAGGED";
150 break;
151 case New:
152 d->command = "NEW";
153 break;
154 case Old:
155 d->command = "OLD";
156 break;
157 case Recent:
158 d->command = "RECENT";
159 break;
160 case Seen:
161 d->command = "SEEN";
162 break;
163 }
164}
165
166static QByteArray monthName(int month)
167{
168 static const char *names[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
169 return (month >= 1 && month <= 12) ? QByteArray(names[month - 1]) : QByteArray();
170}
171
172Term::Term(Term::DateSearchKey key, const QDate &date)
173 : d(new Term::Private)
174{
175 switch (key) {
176 case Before:
177 d->command = "BEFORE";
178 break;
179 case On:
180 d->command = "ON";
181 break;
182 case SentBefore:
183 d->command = "SENTBEFORE";
184 break;
185 case SentOn:
186 d->command = "SENTON";
187 break;
188 case SentSince:
189 d->command = "SENTSINCE";
190 break;
191 case Since:
192 d->command = "SINCE";
193 break;
194 }
195 d->command += " \"";
196 d->command += QByteArray::number(date.day()) + '-';
197 d->command += monthName(date.month()) + '-';
198 d->command += QByteArray::number(date.year());
199 d->command += '\"';
200}
201
202Term::Term(Term::NumberSearchKey key, int value)
203 : d(new Term::Private)
204{
205 switch (key) {
206 case Larger:
207 d->command = "LARGER";
208 break;
209 case Smaller:
210 d->command = "SMALLER";
211 break;
212 }
213 d->command += " " + QByteArray::number(value);
214}
215
216Term::Term(Term::SequenceSearchKey key, const ImapSet &set)
217 : d(new Term::Private)
218{
219 switch (key) {
220 case Uid:
221 d->command = "UID";
222 break;
223 case SequenceNumber:
224 break;
225 }
226 auto optimizedSet = set;
227 optimizedSet.optimize();
228 d->command += " " + optimizedSet.toImapSequenceSet();
229}
230
231Term::Term(const Term &other)
232 : d(new Term::Private)
233{
234 *d = *other.d;
235}
236
237Term::~Term()
238{
239}
240
241Term &Term::operator=(const Term &other)
242{
243 *d = *other.d;
244 return *this;
245}
246
247bool Term::operator==(const Term &other) const
248{
249 return d->command == other.d->command && d->isNegated == other.d->isNegated && d->isFuzzy == other.d->isFuzzy;
250}
251
252QByteArray Term::serialize() const
253{
254 QByteArray command;
255 if (d->isFuzzy) {
256 command = "FUZZY ";
257 }
258 if (d->isNegated) {
259 command = "NOT ";
260 }
261 return command + d->command;
262}
263
264Term &Term::setFuzzy(bool fuzzy)
265{
266 d->isFuzzy = fuzzy;
267 return *this;
268}
269
270Term &Term::setNegated(bool negated)
271{
272 d->isNegated = negated;
273 return *this;
274}
275
276bool Term::isNull() const
277{
278 return d->isNull;
279}
280
281// TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response
282
283class SearchJobPrivate : public JobPrivate
284{
285public:
286 SearchJobPrivate(Session *session, const QString &name)
287 : JobPrivate(session, name)
288 , logic(SearchJob::And)
289 {
290 criteriaMap[SearchJob::All] = "ALL";
291 criteriaMap[SearchJob::Answered] = "ANSWERED";
292 criteriaMap[SearchJob::BCC] = "BCC";
293 criteriaMap[SearchJob::Before] = "BEFORE";
294 criteriaMap[SearchJob::Body] = "BODY";
295 criteriaMap[SearchJob::CC] = "CC";
296 criteriaMap[SearchJob::Deleted] = "DELETED";
297 criteriaMap[SearchJob::Draft] = "DRAFT";
298 criteriaMap[SearchJob::Flagged] = "FLAGGED";
299 criteriaMap[SearchJob::From] = "FROM";
300 criteriaMap[SearchJob::Header] = "HEADER";
301 criteriaMap[SearchJob::Keyword] = "KEYWORD";
302 criteriaMap[SearchJob::Larger] = "LARGER";
303 criteriaMap[SearchJob::New] = "NEW";
304 criteriaMap[SearchJob::Old] = "OLD";
305 criteriaMap[SearchJob::On] = "ON";
306 criteriaMap[SearchJob::Recent] = "RECENT";
307 criteriaMap[SearchJob::Seen] = "SEEN";
308 criteriaMap[SearchJob::SentBefore] = "SENTBEFORE";
309 criteriaMap[SearchJob::SentOn] = "SENTON";
310 criteriaMap[SearchJob::SentSince] = "SENTSINCE";
311 criteriaMap[SearchJob::Since] = "SINCE";
312 criteriaMap[SearchJob::Smaller] = "SMALLER";
313 criteriaMap[SearchJob::Subject] = "SUBJECT";
314 criteriaMap[SearchJob::Text] = "TEXT";
315 criteriaMap[SearchJob::To] = "TO";
316 criteriaMap[SearchJob::Uid] = "UID";
317 criteriaMap[SearchJob::Unanswered] = "UNANSWERED";
318 criteriaMap[SearchJob::Undeleted] = "UNDELETED";
319 criteriaMap[SearchJob::Undraft] = "UNDRAFT";
320 criteriaMap[SearchJob::Unflagged] = "UNFLAGGED";
321 criteriaMap[SearchJob::Unkeyword] = "UNKEYWORD";
322 criteriaMap[SearchJob::Unseen] = "UNSEEN";
323
324 // don't use QDate::shortMonthName(), it returns a localized month name
325 months[1] = "Jan";
326 months[2] = "Feb";
327 months[3] = "Mar";
328 months[4] = "Apr";
329 months[5] = "May";
330 months[6] = "Jun";
331 months[7] = "Jul";
332 months[8] = "Aug";
333 months[9] = "Sep";
334 months[10] = "Oct";
335 months[11] = "Nov";
336 months[12] = "Dec";
337
338 nextContent = 0;
339 uidBased = false;
340 }
341 ~SearchJobPrivate()
342 {
343 }
344
345 QByteArray charset;
346 QList<QByteArray> criterias;
349 SearchJob::SearchLogic logic;
350 QList<QByteArray> contents;
351 QList<qint64> results;
352 uint nextContent;
353 bool uidBased;
354 Term term;
355};
356}
357
358using namespace KIMAP;
359
360SearchJob::SearchJob(Session *session)
361 : Job(*new SearchJobPrivate(session, i18nc("Name of the search job", "Search")))
362{
363}
364
365SearchJob::~SearchJob()
366{
367}
368
369void SearchJob::setTerm(const Term &term)
370{
371 Q_D(SearchJob);
372 d->term = term;
373}
374
375void SearchJob::doStart()
376{
377 Q_D(SearchJob);
378
379 QByteArray searchKey;
380
381 if (!d->charset.isEmpty()) {
382 searchKey = "CHARSET " + d->charset;
383 }
384
385 if (!d->term.isNull()) {
386 const QByteArray term = d->term.serialize();
387 if (term.startsWith('(')) {
388 searchKey += term.mid(1, term.size() - 2);
389 } else {
390 searchKey += term;
391 }
392 } else {
393 if (d->logic == SearchJob::Not) {
394 searchKey += "NOT ";
395 } else if (d->logic == SearchJob::Or && d->criterias.size() > 1) {
396 searchKey += "OR ";
397 }
398
399 if (d->logic == SearchJob::And) {
400 const int numberCriterias(d->criterias.size());
401 for (int i = 0; i < numberCriterias; i++) {
402 const QByteArray key = d->criterias.at(i);
403 if (i > 0) {
404 searchKey += ' ';
405 }
406 searchKey += key;
407 }
408 } else {
409 const int numberCriterias(d->criterias.size());
410 for (int i = 0; i < numberCriterias; i++) {
411 const QByteArray key = d->criterias.at(i);
412 if (i > 0) {
413 searchKey += ' ';
414 }
415 searchKey += '(' + key + ')';
416 }
417 }
418 }
419
420 QByteArray command = "SEARCH";
421 if (d->uidBased) {
422 command = "UID " + command;
423 }
424
425 d->tags << d->sessionInternal()->sendCommand(command, searchKey);
426}
427
428void SearchJob::handleResponse(const Response &response)
429{
430 Q_D(SearchJob);
431
432 if (handleErrorReplies(response) == NotHandled) {
433 if (response.content.size() >= 1 && response.content[0].toString() == "+") {
434 if (d->term.isNull()) {
435 d->sessionInternal()->sendData(d->contents[d->nextContent]);
436 } else {
437 qCWarning(KIMAP_LOG) << "The term API only supports inline strings.";
438 }
439 d->nextContent++;
440 } else if (response.content.size() >= 2 && response.content[1].toString() == "SEARCH") {
441 for (int i = 2; i < response.content.size(); i++) {
442 d->results.append(response.content[i].toString().toInt());
443 }
444 }
445 }
446}
447
448void SearchJob::setCharset(const QByteArray &charset)
449{
450 Q_D(SearchJob);
451 d->charset = charset;
452}
453
454QByteArray SearchJob::charset() const
455{
456 Q_D(const SearchJob);
457 return d->charset;
458}
459
460void SearchJob::setSearchLogic(SearchLogic logic)
461{
462 Q_D(SearchJob);
463 d->logic = logic;
464}
465
466void SearchJob::addSearchCriteria(SearchCriteria criteria)
467{
468 Q_D(SearchJob);
469
470 switch (criteria) {
471 case All:
472 case Answered:
473 case Deleted:
474 case Draft:
475 case Flagged:
476 case New:
477 case Old:
478 case Recent:
479 case Seen:
480 case Unanswered:
481 case Undeleted:
482 case Undraft:
483 case Unflagged:
484 case Unseen:
485 d->criterias.append(d->criteriaMap[criteria]);
486 break;
487 default:
488 // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
489 qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " needs an argument, but none was specified.";
490 break;
491 }
492}
493
494void SearchJob::addSearchCriteria(SearchCriteria criteria, int argument)
495{
496 Q_D(SearchJob);
497 switch (criteria) {
498 case Larger:
499 case Smaller:
500 d->criterias.append(d->criteriaMap[criteria] + ' ' + QByteArray::number(argument));
501 break;
502 default:
503 // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
504 qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept an integer as an argument.";
505 break;
506 }
507}
508
509void SearchJob::addSearchCriteria(SearchCriteria criteria, const QByteArray &argument)
510{
511 Q_D(SearchJob);
512 switch (criteria) {
513 case BCC:
514 case Body:
515 case CC:
516 case From:
517 case Subject:
518 case Text:
519 case To:
520 d->contents.append(argument);
521 d->criterias.append(d->criteriaMap[criteria] + " {" + QByteArray::number(argument.size()) + '}');
522 break;
523 case Keyword:
524 case Unkeyword:
525 case Header:
526 case Uid:
527 d->criterias.append(d->criteriaMap[criteria] + ' ' + argument);
528 break;
529 default:
530 // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
531 qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept any argument.";
532 break;
533 }
534}
535
536void SearchJob::addSearchCriteria(SearchCriteria criteria, const QDate &argument)
537{
538 Q_D(SearchJob);
539 switch (criteria) {
540 case Before:
541 case On:
542 case SentBefore:
543 case SentSince:
544 case Since: {
545 QByteArray date = QByteArray::number(argument.day()) + '-';
546 date += d->months[argument.month()] + '-';
547 date += QByteArray::number(argument.year());
548 d->criterias.append(d->criteriaMap[criteria] + " \"" + date + '\"');
549 break;
550 }
551 default:
552 // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
553 qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept a date as argument.";
554 break;
555 }
556}
557
558void SearchJob::addSearchCriteria(const QByteArray &searchCriteria)
559{
560 Q_D(SearchJob);
561 d->criterias.append(searchCriteria);
562}
563
564void SearchJob::setUidBased(bool uidBased)
565{
566 Q_D(SearchJob);
567 d->uidBased = uidBased;
568}
569
570bool SearchJob::isUidBased() const
571{
572 Q_D(const SearchJob);
573 return d->uidBased;
574}
575
576QList<qint64> SearchJob::results() const
577{
578 Q_D(const SearchJob);
579 return d->results;
580}
581
582#include "moc_searchjob.cpp"
Represents a set of natural numbers (1->∞) in a as compact as possible form.
Definition imapset.h:127
void optimize()
Optimizes the ImapSet by sorting and merging overlapping intervals.
Definition imapset.cpp:310
A query term.
Definition searchjob.h:32
QString i18nc(const char *context, const char *text, const TYPE &arg...)
AKONADI_MIME_EXPORT const char Answered[]
AKONADI_MIME_EXPORT const char Seen[]
AKONADI_MIME_EXPORT const char Deleted[]
AKONADI_MIME_EXPORT const char Flagged[]
AKONADI_MIME_EXPORT const char Body[]
QString name(StandardShortcut id)
char at(qsizetype i) const const
const char * constData() const const
QByteArray mid(qsizetype pos, qsizetype len) const const
QByteArray number(double n, char format, int precision)
qsizetype size() const const
bool startsWith(QByteArrayView bv) const const
int day() const const
int month() const const
int year() const const
reference back()
T & first()
bool isEmpty() const const
qsizetype size() const const
QByteArray toUtf8() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:37 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.