File indexing completed on 2024-05-12 05:17:16

0001 /*
0002     SPDX-FileCopyrightText: 2009 Andras Mantia <amantia@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "searchjob.h"
0008 
0009 #include "kimap_debug.h"
0010 #include <KLocalizedString>
0011 
0012 #include <QDate>
0013 
0014 #include "imapset.h"
0015 #include "job_p.h"
0016 #include "response_p.h"
0017 #include "session_p.h"
0018 
0019 namespace KIMAP
0020 {
0021 class Term::Private : public QSharedData
0022 {
0023 public:
0024     Private()
0025         : QSharedData()
0026         , isFuzzy(false)
0027         , isNegated(false)
0028         , isNull(false)
0029     {
0030     }
0031     Private(const Private &other)
0032         : QSharedData(other)
0033         , command(other.command)
0034         , isFuzzy(other.isFuzzy)
0035         , isNegated(other.isNegated)
0036         , isNull(other.isNull)
0037     {
0038     }
0039 
0040     Private &operator=(const Private &other)
0041     {
0042         command = other.command;
0043         isFuzzy = other.isFuzzy;
0044         isNegated = other.isNegated;
0045         isNull = other.isNull;
0046         return *this;
0047     }
0048 
0049     QByteArray command;
0050     bool isFuzzy;
0051     bool isNegated;
0052     bool isNull;
0053 };
0054 
0055 Term::Term()
0056     : d(new Term::Private)
0057 {
0058     d->isNull = true;
0059 }
0060 
0061 Term::Term(Term::Relation relation, const QList<Term> &subterms)
0062     : d(new Term::Private)
0063 {
0064     if (subterms.size() >= 2) {
0065         if (relation == KIMAP::Term::Or) {
0066             for (int i = 0; i < subterms.size() - 1; ++i) {
0067                 d->command += "(OR " + subterms[i].serialize() + " ";
0068             }
0069             d->command += subterms.back().serialize();
0070             for (int i = 0; i < subterms.size() - 1; ++i) {
0071                 d->command += ")";
0072             }
0073         } else {
0074             d->command += "(";
0075             for (const Term &t : subterms) {
0076                 d->command += t.serialize() + ' ';
0077             }
0078             if (!subterms.isEmpty()) {
0079                 d->command.chop(1);
0080             }
0081             d->command += ")";
0082         }
0083     } else if (subterms.size() == 1) {
0084         d->command += subterms.first().serialize();
0085     } else {
0086         d->isNull = true;
0087     }
0088 }
0089 
0090 Term::Term(Term::SearchKey key, const QString &value)
0091     : d(new Term::Private)
0092 {
0093     switch (key) {
0094     case All:
0095         d->command += "ALL";
0096         break;
0097     case Bcc:
0098         d->command += "BCC";
0099         break;
0100     case Cc:
0101         d->command += "CC";
0102         break;
0103     case Body:
0104         d->command += "BODY";
0105         break;
0106     case From:
0107         d->command += "FROM";
0108         break;
0109     case Keyword:
0110         d->command += "KEYWORD";
0111         break;
0112     case Subject:
0113         d->command += "SUBJECT";
0114         break;
0115     case Text:
0116         d->command += "TEXT";
0117         break;
0118     case To:
0119         d->command += "TO";
0120         break;
0121     }
0122     if (key != All) {
0123         d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\"";
0124     }
0125 }
0126 
0127 Term::Term(const QString &header, const QString &value)
0128     : d(new Term::Private)
0129 {
0130     d->command += "HEADER";
0131     d->command += ' ' + QByteArray(header.toUtf8().constData());
0132     d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\"";
0133 }
0134 
0135 Term::Term(Term::BooleanSearchKey key)
0136     : d(new Term::Private)
0137 {
0138     switch (key) {
0139     case Answered:
0140         d->command = "ANSWERED";
0141         break;
0142     case Deleted:
0143         d->command = "DELETED";
0144         break;
0145     case Draft:
0146         d->command = "DRAFT";
0147         break;
0148     case Flagged:
0149         d->command = "FLAGGED";
0150         break;
0151     case New:
0152         d->command = "NEW";
0153         break;
0154     case Old:
0155         d->command = "OLD";
0156         break;
0157     case Recent:
0158         d->command = "RECENT";
0159         break;
0160     case Seen:
0161         d->command = "SEEN";
0162         break;
0163     }
0164 }
0165 
0166 static QByteArray monthName(int month)
0167 {
0168     static const char *names[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
0169     return (month >= 1 && month <= 12) ? QByteArray(names[month - 1]) : QByteArray();
0170 }
0171 
0172 Term::Term(Term::DateSearchKey key, const QDate &date)
0173     : d(new Term::Private)
0174 {
0175     switch (key) {
0176     case Before:
0177         d->command = "BEFORE";
0178         break;
0179     case On:
0180         d->command = "ON";
0181         break;
0182     case SentBefore:
0183         d->command = "SENTBEFORE";
0184         break;
0185     case SentOn:
0186         d->command = "SENTON";
0187         break;
0188     case SentSince:
0189         d->command = "SENTSINCE";
0190         break;
0191     case Since:
0192         d->command = "SINCE";
0193         break;
0194     }
0195     d->command += " \"";
0196     d->command += QByteArray::number(date.day()) + '-';
0197     d->command += monthName(date.month()) + '-';
0198     d->command += QByteArray::number(date.year());
0199     d->command += '\"';
0200 }
0201 
0202 Term::Term(Term::NumberSearchKey key, int value)
0203     : d(new Term::Private)
0204 {
0205     switch (key) {
0206     case Larger:
0207         d->command = "LARGER";
0208         break;
0209     case Smaller:
0210         d->command = "SMALLER";
0211         break;
0212     }
0213     d->command += " " + QByteArray::number(value);
0214 }
0215 
0216 Term::Term(Term::SequenceSearchKey key, const ImapSet &set)
0217     : d(new Term::Private)
0218 {
0219     switch (key) {
0220     case Uid:
0221         d->command = "UID";
0222         break;
0223     case SequenceNumber:
0224         break;
0225     }
0226     auto optimizedSet = set;
0227     optimizedSet.optimize();
0228     d->command += " " + optimizedSet.toImapSequenceSet();
0229 }
0230 
0231 Term::Term(const Term &other)
0232     : d(new Term::Private)
0233 {
0234     *d = *other.d;
0235 }
0236 
0237 Term::~Term()
0238 {
0239 }
0240 
0241 Term &Term::operator=(const Term &other)
0242 {
0243     *d = *other.d;
0244     return *this;
0245 }
0246 
0247 bool Term::operator==(const Term &other) const
0248 {
0249     return d->command == other.d->command && d->isNegated == other.d->isNegated && d->isFuzzy == other.d->isFuzzy;
0250 }
0251 
0252 QByteArray Term::serialize() const
0253 {
0254     QByteArray command;
0255     if (d->isFuzzy) {
0256         command = "FUZZY ";
0257     }
0258     if (d->isNegated) {
0259         command = "NOT ";
0260     }
0261     return command + d->command;
0262 }
0263 
0264 Term &Term::setFuzzy(bool fuzzy)
0265 {
0266     d->isFuzzy = fuzzy;
0267     return *this;
0268 }
0269 
0270 Term &Term::setNegated(bool negated)
0271 {
0272     d->isNegated = negated;
0273     return *this;
0274 }
0275 
0276 bool Term::isNull() const
0277 {
0278     return d->isNull;
0279 }
0280 
0281 // TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response
0282 
0283 class SearchJobPrivate : public JobPrivate
0284 {
0285 public:
0286     SearchJobPrivate(Session *session, const QString &name)
0287         : JobPrivate(session, name)
0288         , logic(SearchJob::And)
0289     {
0290         criteriaMap[SearchJob::All] = "ALL";
0291         criteriaMap[SearchJob::Answered] = "ANSWERED";
0292         criteriaMap[SearchJob::BCC] = "BCC";
0293         criteriaMap[SearchJob::Before] = "BEFORE";
0294         criteriaMap[SearchJob::Body] = "BODY";
0295         criteriaMap[SearchJob::CC] = "CC";
0296         criteriaMap[SearchJob::Deleted] = "DELETED";
0297         criteriaMap[SearchJob::Draft] = "DRAFT";
0298         criteriaMap[SearchJob::Flagged] = "FLAGGED";
0299         criteriaMap[SearchJob::From] = "FROM";
0300         criteriaMap[SearchJob::Header] = "HEADER";
0301         criteriaMap[SearchJob::Keyword] = "KEYWORD";
0302         criteriaMap[SearchJob::Larger] = "LARGER";
0303         criteriaMap[SearchJob::New] = "NEW";
0304         criteriaMap[SearchJob::Old] = "OLD";
0305         criteriaMap[SearchJob::On] = "ON";
0306         criteriaMap[SearchJob::Recent] = "RECENT";
0307         criteriaMap[SearchJob::Seen] = "SEEN";
0308         criteriaMap[SearchJob::SentBefore] = "SENTBEFORE";
0309         criteriaMap[SearchJob::SentOn] = "SENTON";
0310         criteriaMap[SearchJob::SentSince] = "SENTSINCE";
0311         criteriaMap[SearchJob::Since] = "SINCE";
0312         criteriaMap[SearchJob::Smaller] = "SMALLER";
0313         criteriaMap[SearchJob::Subject] = "SUBJECT";
0314         criteriaMap[SearchJob::Text] = "TEXT";
0315         criteriaMap[SearchJob::To] = "TO";
0316         criteriaMap[SearchJob::Uid] = "UID";
0317         criteriaMap[SearchJob::Unanswered] = "UNANSWERED";
0318         criteriaMap[SearchJob::Undeleted] = "UNDELETED";
0319         criteriaMap[SearchJob::Undraft] = "UNDRAFT";
0320         criteriaMap[SearchJob::Unflagged] = "UNFLAGGED";
0321         criteriaMap[SearchJob::Unkeyword] = "UNKEYWORD";
0322         criteriaMap[SearchJob::Unseen] = "UNSEEN";
0323 
0324         // don't use QDate::shortMonthName(), it returns a localized month name
0325         months[1] = "Jan";
0326         months[2] = "Feb";
0327         months[3] = "Mar";
0328         months[4] = "Apr";
0329         months[5] = "May";
0330         months[6] = "Jun";
0331         months[7] = "Jul";
0332         months[8] = "Aug";
0333         months[9] = "Sep";
0334         months[10] = "Oct";
0335         months[11] = "Nov";
0336         months[12] = "Dec";
0337 
0338         nextContent = 0;
0339         uidBased = false;
0340     }
0341     ~SearchJobPrivate()
0342     {
0343     }
0344 
0345     QByteArray charset;
0346     QList<QByteArray> criterias;
0347     QMap<SearchJob::SearchCriteria, QByteArray> criteriaMap;
0348     QMap<int, QByteArray> months;
0349     SearchJob::SearchLogic logic;
0350     QList<QByteArray> contents;
0351     QList<qint64> results;
0352     uint nextContent;
0353     bool uidBased;
0354     Term term;
0355 };
0356 }
0357 
0358 using namespace KIMAP;
0359 
0360 SearchJob::SearchJob(Session *session)
0361     : Job(*new SearchJobPrivate(session, i18nc("Name of the search job", "Search")))
0362 {
0363 }
0364 
0365 SearchJob::~SearchJob()
0366 {
0367 }
0368 
0369 void SearchJob::setTerm(const Term &term)
0370 {
0371     Q_D(SearchJob);
0372     d->term = term;
0373 }
0374 
0375 void SearchJob::doStart()
0376 {
0377     Q_D(SearchJob);
0378 
0379     QByteArray searchKey;
0380 
0381     if (!d->charset.isEmpty()) {
0382         searchKey = "CHARSET " + d->charset;
0383     }
0384 
0385     if (!d->term.isNull()) {
0386         const QByteArray term = d->term.serialize();
0387         if (term.startsWith('(')) {
0388             searchKey += term.mid(1, term.size() - 2);
0389         } else {
0390             searchKey += term;
0391         }
0392     } else {
0393         if (d->logic == SearchJob::Not) {
0394             searchKey += "NOT ";
0395         } else if (d->logic == SearchJob::Or && d->criterias.size() > 1) {
0396             searchKey += "OR ";
0397         }
0398 
0399         if (d->logic == SearchJob::And) {
0400             const int numberCriterias(d->criterias.size());
0401             for (int i = 0; i < numberCriterias; i++) {
0402                 const QByteArray key = d->criterias.at(i);
0403                 if (i > 0) {
0404                     searchKey += ' ';
0405                 }
0406                 searchKey += key;
0407             }
0408         } else {
0409             const int numberCriterias(d->criterias.size());
0410             for (int i = 0; i < numberCriterias; i++) {
0411                 const QByteArray key = d->criterias.at(i);
0412                 if (i > 0) {
0413                     searchKey += ' ';
0414                 }
0415                 searchKey += '(' + key + ')';
0416             }
0417         }
0418     }
0419 
0420     QByteArray command = "SEARCH";
0421     if (d->uidBased) {
0422         command = "UID " + command;
0423     }
0424 
0425     d->tags << d->sessionInternal()->sendCommand(command, searchKey);
0426 }
0427 
0428 void SearchJob::handleResponse(const Response &response)
0429 {
0430     Q_D(SearchJob);
0431 
0432     if (handleErrorReplies(response) == NotHandled) {
0433         if (response.content.size() >= 1 && response.content[0].toString() == "+") {
0434             if (d->term.isNull()) {
0435                 d->sessionInternal()->sendData(d->contents[d->nextContent]);
0436             } else {
0437                 qCWarning(KIMAP_LOG) << "The term API only supports inline strings.";
0438             }
0439             d->nextContent++;
0440         } else if (response.content.size() >= 2 && response.content[1].toString() == "SEARCH") {
0441             for (int i = 2; i < response.content.size(); i++) {
0442                 d->results.append(response.content[i].toString().toInt());
0443             }
0444         }
0445     }
0446 }
0447 
0448 void SearchJob::setCharset(const QByteArray &charset)
0449 {
0450     Q_D(SearchJob);
0451     d->charset = charset;
0452 }
0453 
0454 QByteArray SearchJob::charset() const
0455 {
0456     Q_D(const SearchJob);
0457     return d->charset;
0458 }
0459 
0460 void SearchJob::setSearchLogic(SearchLogic logic)
0461 {
0462     Q_D(SearchJob);
0463     d->logic = logic;
0464 }
0465 
0466 void SearchJob::addSearchCriteria(SearchCriteria criteria)
0467 {
0468     Q_D(SearchJob);
0469 
0470     switch (criteria) {
0471     case All:
0472     case Answered:
0473     case Deleted:
0474     case Draft:
0475     case Flagged:
0476     case New:
0477     case Old:
0478     case Recent:
0479     case Seen:
0480     case Unanswered:
0481     case Undeleted:
0482     case Undraft:
0483     case Unflagged:
0484     case Unseen:
0485         d->criterias.append(d->criteriaMap[criteria]);
0486         break;
0487     default:
0488         // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
0489         qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " needs an argument, but none was specified.";
0490         break;
0491     }
0492 }
0493 
0494 void SearchJob::addSearchCriteria(SearchCriteria criteria, int argument)
0495 {
0496     Q_D(SearchJob);
0497     switch (criteria) {
0498     case Larger:
0499     case Smaller:
0500         d->criterias.append(d->criteriaMap[criteria] + ' ' + QByteArray::number(argument));
0501         break;
0502     default:
0503         // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
0504         qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept an integer as an argument.";
0505         break;
0506     }
0507 }
0508 
0509 void SearchJob::addSearchCriteria(SearchCriteria criteria, const QByteArray &argument)
0510 {
0511     Q_D(SearchJob);
0512     switch (criteria) {
0513     case BCC:
0514     case Body:
0515     case CC:
0516     case From:
0517     case Subject:
0518     case Text:
0519     case To:
0520         d->contents.append(argument);
0521         d->criterias.append(d->criteriaMap[criteria] + " {" + QByteArray::number(argument.size()) + '}');
0522         break;
0523     case Keyword:
0524     case Unkeyword:
0525     case Header:
0526     case Uid:
0527         d->criterias.append(d->criteriaMap[criteria] + ' ' + argument);
0528         break;
0529     default:
0530         // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
0531         qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept any argument.";
0532         break;
0533     }
0534 }
0535 
0536 void SearchJob::addSearchCriteria(SearchCriteria criteria, const QDate &argument)
0537 {
0538     Q_D(SearchJob);
0539     switch (criteria) {
0540     case Before:
0541     case On:
0542     case SentBefore:
0543     case SentSince:
0544     case Since: {
0545         QByteArray date = QByteArray::number(argument.day()) + '-';
0546         date += d->months[argument.month()] + '-';
0547         date += QByteArray::number(argument.year());
0548         d->criterias.append(d->criteriaMap[criteria] + " \"" + date + '\"');
0549         break;
0550     }
0551     default:
0552         // TODO Discuss if we keep error checking here, or accept anything, even if it is wrong
0553         qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept a date as argument.";
0554         break;
0555     }
0556 }
0557 
0558 void SearchJob::addSearchCriteria(const QByteArray &searchCriteria)
0559 {
0560     Q_D(SearchJob);
0561     d->criterias.append(searchCriteria);
0562 }
0563 
0564 void SearchJob::setUidBased(bool uidBased)
0565 {
0566     Q_D(SearchJob);
0567     d->uidBased = uidBased;
0568 }
0569 
0570 bool SearchJob::isUidBased() const
0571 {
0572     Q_D(const SearchJob);
0573     return d->uidBased;
0574 }
0575 
0576 QList<qint64> SearchJob::results() const
0577 {
0578     Q_D(const SearchJob);
0579     return d->results;
0580 }
0581 
0582 #include "moc_searchjob.cpp"