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