KIMAP

searchjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Andras Mantia <[email protected]>
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 
19 namespace KIMAP
20 {
21 class Term::Private : public QSharedData
22 {
23 public:
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 
55 Term::Term()
56  : d(new Term::Private)
57 {
58  d->isNull = true;
59 }
60 
61 Term::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 
90 Term::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 
127 Term::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 
135 Term::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 
166 static 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 
172 Term::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 
202 Term::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 
216 Term::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 
231 Term::Term(const Term &other)
232  : d(new Term::Private)
233 {
234  *d = *other.d;
235 }
236 
237 Term::~Term()
238 {
239 }
240 
241 Term &Term::operator=(const Term &other)
242 {
243  *d = *other.d;
244  return *this;
245 }
246 
247 bool 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 
252 QByteArray 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 
264 Term &Term::setFuzzy(bool fuzzy)
265 {
266  d->isFuzzy = fuzzy;
267  return *this;
268 }
269 
270 Term &Term::setNegated(bool negated)
271 {
272  d->isNegated = negated;
273  return *this;
274 }
275 
276 bool Term::isNull() const
277 {
278  return d->isNull;
279 }
280 
281 // TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response
282 
283 class SearchJobPrivate : public JobPrivate
284 {
285 public:
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;
348  QMap<int, QByteArray> months;
349  SearchJob::SearchLogic logic;
350  QList<QByteArray> contents;
351  QList<qint64> results;
352  uint nextContent;
353  bool uidBased;
354  Term term;
355 };
356 }
357 
358 using namespace KIMAP;
359 
360 SearchJob::SearchJob(Session *session)
361  : Job(*new SearchJobPrivate(session, i18nc("Name of the search job", "Search")))
362 {
363 }
364 
365 SearchJob::~SearchJob()
366 {
367 }
368 
369 void SearchJob::setTerm(const Term &term)
370 {
371  Q_D(SearchJob);
372  d->term = term;
373 }
374 
375 void 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 
428 void 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 
448 void SearchJob::setCharset(const QByteArray &charset)
449 {
450  Q_D(SearchJob);
451  d->charset = charset;
452 }
453 
454 QByteArray SearchJob::charset() const
455 {
456  Q_D(const SearchJob);
457  return d->charset;
458 }
459 
460 void SearchJob::setSearchLogic(SearchLogic logic)
461 {
462  Q_D(SearchJob);
463  d->logic = logic;
464 }
465 
466 void 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 
494 void 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 
509 void 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 
536 void 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 
558 void SearchJob::addSearchCriteria(const QByteArray &searchCriteria)
559 {
560  Q_D(SearchJob);
561  d->criterias.append(searchCriteria);
562 }
563 
564 void SearchJob::setUidBased(bool uidBased)
565 {
566  Q_D(SearchJob);
567  d->uidBased = uidBased;
568 }
569 
570 bool SearchJob::isUidBased() const
571 {
572  Q_D(const SearchJob);
573  return d->uidBased;
574 }
575 
576 QList<qint64> SearchJob::results() const
577 {
578  Q_D(const SearchJob);
579  return d->results;
580 }
581 
582 #include "moc_searchjob.cpp"
int month() const const
const AKONADI_MIME_EXPORT char Body[]
Subject
const AKONADI_MIME_EXPORT char Flagged[]
int year() const const
QByteArray number(int n, int base)
A query term.
Definition: searchjob.h:31
char at(int i) const const
int size() const const
QByteArray mid(int pos, int len) const const
Represents a set of natural numbers (1->∞) in a as compact as possible form.
Definition: imapset.h:126
QByteArray toUtf8() const const
bool startsWith(const QByteArray &ba) const const
const AKONADI_MIME_EXPORT char Header[]
const AKONADI_MIME_EXPORT char Deleted[]
T & back()
const AKONADI_MIME_EXPORT char Answered[]
const char * constData() const const
const char * name(StandardAction id)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
void optimize()
Optimizes the ImapSet by sorting and merging overlapping intervals.
Definition: imapset.cpp:310
int size() const const
const AKONADI_MIME_EXPORT char Seen[]
Q_D(Todo)
int day() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 3 2023 03:51:44 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.