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
32namespace KIMAP
33{
34
35
36class Term::Private {
37public:
38 Private(): isFuzzy(false), isNegated(false), isNull(false) {};
39 QByteArray command;
40 bool isFuzzy;
41 bool isNegated;
42 bool isNull;
43};
44
45Term::Term()
46: d(new Term::Private)
47{
48 d->isNull = true;
49}
50
51Term::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
81Term::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
118Term::Term( const QString &header, 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
126Term::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
157QMap<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
176static QMap<int, QByteArray> months = initializeMonths();
177
178Term::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
208Term::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
222Term::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
235Term::Term(const Term& other)
236: d(new Term::Private)
237{
238 *d = *other.d;
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 &&
250 d->isNegated == other.d->isNegated &&
251 d->isFuzzy == other.d->isFuzzy;
252}
253
254QByteArray 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
266Term &Term::setFuzzy(bool fuzzy)
267{
268 d->isFuzzy = fuzzy;
269 return *this;
270}
271
272Term &Term::setNegated(bool negated)
273{
274 d->isNegated = negated;
275 return *this;
276}
277
278bool 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
355using namespace KIMAP;
356
357SearchJob::SearchJob( Session *session )
358 : Job( *new SearchJobPrivate( session, i18nc( "Name of the search job", "Search" ) ) )
359{
360}
361
362SearchJob::~SearchJob()
363{
364}
365
366void SearchJob::setTerm(const Term &term)
367{
368 Q_D( SearchJob );
369 d->term = term;
370}
371
372void 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
424void 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
444void SearchJob::setCharset( const QByteArray &charset )
445{
446 Q_D( SearchJob );
447 d->charset = charset;
448}
449
450QByteArray SearchJob::charset() const
451{
452 Q_D( const SearchJob );
453 return d->charset;
454}
455
456void SearchJob::setSearchLogic( SearchLogic logic )
457{
458 Q_D( SearchJob );
459 d->logic = logic;
460}
461
462void 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
490void 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
505void 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
532void 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
554void SearchJob::addSearchCriteria( const QByteArray &searchCriteria )
555{
556 Q_D( SearchJob );
557 d->criterias.append( searchCriteria );
558}
559
560void SearchJob::setUidBased(bool uidBased)
561{
562 Q_D( SearchJob );
563 d->uidBased = uidBased;
564}
565
566bool SearchJob::isUidBased() const
567{
568 Q_D( const SearchJob );
569 return d->uidBased;
570}
571
572QList<qint64> SearchJob::results() const
573{
574 Q_D( const SearchJob );
575 return d->results;
576}
577
578QList<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