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

0001 /*
0002     Copyright (c) 2009 Andras Mantia <amantia@kde.org>
0003 
0004     This library is free software; you can redistribute it and/or modify it
0005     under the terms of the GNU Library General Public License as published by
0006     the Free Software Foundation; either version 2 of the License, or (at your
0007     option) any later version.
0008 
0009     This library is distributed in the hope that it will be useful, but WITHOUT
0010     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0011     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0012     License for more details.
0013 
0014     You should have received a copy of the GNU Library General Public License
0015     along with this library; see the file COPYING.LIB.  If not, write to the
0016     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0017     02110-1301, USA.
0018 */
0019 
0020 #include "searchjob.h"
0021 
0022 #include "kimap_debug.h"
0023 
0024 #include <QtCore/QDate>
0025 
0026 #include "job_p.h"
0027 #include "message_p.h"
0028 #include "session_p.h"
0029 #include "imapset.h"
0030 
0031 namespace KIMAP2
0032 {
0033 
0034 class Term::Private
0035 {
0036 public:
0037     Private(): isFuzzy(false), isNegated(false), isNull(false) {}
0038     QByteArray command;
0039     bool isFuzzy;
0040     bool isNegated;
0041     bool isNull;
0042 };
0043 
0044 Term::Term()
0045     :  d(new Term::Private)
0046 {
0047     d->isNull = true;
0048 }
0049 
0050 Term::Term(Term::Relation relation, const QVector<Term> &subterms)
0051     :  d(new Term::Private)
0052 {
0053     if (subterms.size() >= 2) {
0054         if (relation == KIMAP2::Term::Or) {
0055             for (int i = 0; i < subterms.size() - 1; ++i) {
0056                 d->command += "(OR " + subterms[i].serialize() + " ";
0057             }
0058             d->command += subterms.back().serialize();
0059             for (int i = 0; i < subterms.size() - 1; ++i) {
0060                 d->command += ")";
0061             }
0062         } else {
0063             d->command += "(";
0064             for (const Term &t : subterms) {
0065                 d->command += t.serialize() + ' ';
0066             }
0067             if (!subterms.isEmpty()) {
0068                 d->command.chop(1);
0069             }
0070             d->command += ")";
0071         }
0072     } else if (subterms.size() == 1) {
0073         d->command += subterms.first().serialize();
0074     } else {
0075         d->isNull = true;
0076     }
0077 }
0078 
0079 Term::Term(Term::SearchKey key, const QString &value)
0080     :  d(new Term::Private)
0081 {
0082     switch (key) {
0083     case All:
0084         d->command += "ALL";
0085         break;
0086     case Bcc:
0087         d->command += "BCC";
0088         break;
0089     case Cc:
0090         d->command += "CC";
0091         break;
0092     case Body:
0093         d->command += "BODY";
0094         break;
0095     case From:
0096         d->command += "FROM";
0097         break;
0098     case Keyword:
0099         d->command += "KEYWORD";
0100         break;
0101     case Subject:
0102         d->command += "SUBJECT";
0103         break;
0104     case Text:
0105         d->command += "TEXT";
0106         break;
0107     case To:
0108         d->command += "TO";
0109         break;
0110     }
0111     if (key != All) {
0112         d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\"";
0113     }
0114 }
0115 
0116 Term::Term(const QString &header, const QString &value)
0117     :  d(new Term::Private)
0118 {
0119     d->command += "HEADER";
0120     d->command += ' ' + QByteArray(header.toUtf8().constData());
0121     d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\"";
0122 }
0123 
0124 Term::Term(Term::BooleanSearchKey key)
0125     :  d(new Term::Private)
0126 {
0127     switch (key) {
0128     case Answered:
0129         d->command = "ANSWERED";
0130         break;
0131     case Deleted:
0132         d->command = "DELETED";
0133         break;
0134     case Draft:
0135         d->command = "DRAFT";
0136         break;
0137     case Flagged:
0138         d->command = "FLAGGED";
0139         break;
0140     case New:
0141         d->command = "NEW";
0142         break;
0143     case Old:
0144         d->command = "OLD";
0145         break;
0146     case Recent:
0147         d->command = "RECENT";
0148         break;
0149     case Seen:
0150         d->command = "SEEN";
0151         break;
0152     }
0153 }
0154 
0155 static QByteArray monthName(int month)
0156 {
0157     static const char* names[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
0158     return (month >= 1 && month <= 12) ? QByteArray(names[month - 1]) : QByteArray();
0159 }
0160 
0161 Term::Term(Term::DateSearchKey key, const QDate &date)
0162     :  d(new Term::Private)
0163 {
0164     switch (key) {
0165     case Before:
0166         d->command = "BEFORE";
0167         break;
0168     case On:
0169         d->command = "ON";
0170         break;
0171     case SentBefore:
0172         d->command = "SENTBEFORE";
0173         break;
0174     case SentOn:
0175         d->command = "SENTON";
0176         break;
0177     case SentSince:
0178         d->command = "SENTSINCE";
0179         break;
0180     case Since:
0181         d->command = "SINCE";
0182         break;
0183     }
0184     d->command += " \"";
0185     d->command += QByteArray::number(date.day()) + '-';
0186     d->command += monthName(date.month()) + '-';
0187     d->command += QByteArray::number(date.year());
0188     d->command += '\"';
0189 }
0190 
0191 Term::Term(Term::NumberSearchKey key, int value)
0192     :  d(new Term::Private)
0193 {
0194     switch (key) {
0195     case Larger:
0196         d->command = "LARGER";
0197         break;
0198     case Smaller:
0199         d->command = "SMALLER";
0200         break;
0201     }
0202     d->command += " " + QByteArray::number(value);
0203 }
0204 
0205 Term::Term(Term::SequenceSearchKey key, const ImapSet &set)
0206     :  d(new Term::Private)
0207 {
0208     switch (key) {
0209     case Uid:
0210         d->command = "UID";
0211         break;
0212     case SequenceNumber:
0213         break;
0214     }
0215     auto optimizedSet = set;
0216     optimizedSet.optimize();
0217     d->command += " " + optimizedSet.toImapSequenceSet();
0218 }
0219 
0220 Term::Term(const Term &other)
0221     :  d(new Term::Private)
0222 {
0223     *d = *other.d;
0224 }
0225 
0226 Term &Term::operator=(const Term &other)
0227 {
0228     *d = *other.d;
0229     return *this;
0230 }
0231 
0232 bool Term::operator==(const Term &other) const
0233 {
0234     return d->command == other.d->command &&
0235            d->isNegated == other.d->isNegated &&
0236            d->isFuzzy == other.d->isFuzzy;
0237 }
0238 
0239 QByteArray Term::serialize() const
0240 {
0241     QByteArray command;
0242     if (d->isFuzzy) {
0243         command = "FUZZY ";
0244     }
0245     if (d->isNegated) {
0246         command = "NOT ";
0247     }
0248     return command + d->command;
0249 }
0250 
0251 Term &Term::setFuzzy(bool fuzzy)
0252 {
0253     d->isFuzzy = fuzzy;
0254     return *this;
0255 }
0256 
0257 Term &Term::setNegated(bool negated)
0258 {
0259     d->isNegated = negated;
0260     return *this;
0261 }
0262 
0263 bool Term::isNull() const
0264 {
0265     return d->isNull;
0266 }
0267 
0268 //TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response
0269 
0270 class SearchJobPrivate : public JobPrivate
0271 {
0272 public:
0273     SearchJobPrivate(Session *session, const QString &name) : JobPrivate(session, name), logic(SearchJob::And)
0274     {
0275         criteriaMap[SearchJob::All]  = "ALL";
0276         criteriaMap[SearchJob::Answered] = "ANSWERED";
0277         criteriaMap[SearchJob::BCC] = "BCC";
0278         criteriaMap[SearchJob::Before] = "BEFORE";
0279         criteriaMap[SearchJob::Body] = "BODY";
0280         criteriaMap[SearchJob::CC] = "CC";
0281         criteriaMap[SearchJob::Deleted] = "DELETED";
0282         criteriaMap[SearchJob::Draft] = "DRAFT";
0283         criteriaMap[SearchJob::Flagged] = "FLAGGED";
0284         criteriaMap[SearchJob::From] = "FROM";
0285         criteriaMap[SearchJob::Header] = "HEADER";
0286         criteriaMap[SearchJob::Keyword] = "KEYWORD";
0287         criteriaMap[SearchJob::Larger] = "LARGER";
0288         criteriaMap[SearchJob::New] = "NEW";
0289         criteriaMap[SearchJob::Old] = "OLD";
0290         criteriaMap[SearchJob::On] = "ON";
0291         criteriaMap[SearchJob::Recent] = "RECENT";
0292         criteriaMap[SearchJob::Seen] = "SEEN";
0293         criteriaMap[SearchJob::SentBefore] = "SENTBEFORE";
0294         criteriaMap[SearchJob::SentOn] = "SENTON";
0295         criteriaMap[SearchJob::SentSince] = "SENTSINCE";
0296         criteriaMap[SearchJob::Since] = "SINCE";
0297         criteriaMap[SearchJob::Smaller] = "SMALLER";
0298         criteriaMap[SearchJob::Subject] = "SUBJECT";
0299         criteriaMap[SearchJob::Text] = "TEXT";
0300         criteriaMap[SearchJob::To] = "TO";
0301         criteriaMap[SearchJob::Uid] = "UID";
0302         criteriaMap[SearchJob::Unanswered] = "UNANSWERED";
0303         criteriaMap[SearchJob::Undeleted] = "UNDELETED";
0304         criteriaMap[SearchJob::Undraft] = "UNDRAFT";
0305         criteriaMap[SearchJob::Unflagged] = "UNFLAGGED";
0306         criteriaMap[SearchJob::Unkeyword] = "UNKEYWORD";
0307         criteriaMap[SearchJob::Unseen] = "UNSEEN";
0308 
0309         //don't use QDate::shortMonthName(), it returns a localized month name
0310         months[1] = "Jan";
0311         months[2] = "Feb";
0312         months[3] = "Mar";
0313         months[4] = "Apr";
0314         months[5] = "May";
0315         months[6] = "Jun";
0316         months[7] = "Jul";
0317         months[8] = "Aug";
0318         months[9] = "Sep";
0319         months[10] = "Oct";
0320         months[11] = "Nov";
0321         months[12] = "Dec";
0322 
0323         nextContent = 0;
0324         uidBased = false;
0325     }
0326     ~SearchJobPrivate() { }
0327 
0328     QByteArray charset;
0329     QList<QByteArray> criterias;
0330     QMap<SearchJob::SearchCriteria, QByteArray > criteriaMap;
0331     QMap<int, QByteArray> months;
0332     SearchJob::SearchLogic logic;
0333     QList<QByteArray> contents;
0334     QVector<qint64> results;
0335     uint nextContent;
0336     bool uidBased;
0337     Term term;
0338 };
0339 }
0340 
0341 using namespace KIMAP2;
0342 
0343 SearchJob::SearchJob(Session *session)
0344     : Job(*new SearchJobPrivate(session, "Search"))
0345 {
0346 }
0347 
0348 SearchJob::~SearchJob()
0349 {
0350 }
0351 
0352 void SearchJob::setTerm(const Term &term)
0353 {
0354     Q_D(SearchJob);
0355     d->term = term;
0356 }
0357 
0358 void SearchJob::doStart()
0359 {
0360     Q_D(SearchJob);
0361 
0362     QByteArray searchKey;
0363 
0364     if (!d->charset.isEmpty()) {
0365         searchKey = "CHARSET " + d->charset;
0366     }
0367 
0368     if (!d->term.isNull()) {
0369         const QByteArray term = d->term.serialize();
0370         if (term.startsWith('(')) {
0371             searchKey += term.mid(1, term.size() - 2);
0372         } else {
0373             searchKey += term;
0374         }
0375     } else {
0376 
0377         if (d->logic == SearchJob::Not) {
0378             searchKey += "NOT ";
0379         } else if (d->logic == SearchJob::Or && d->criterias.size() > 1) {
0380             searchKey += "OR ";
0381         }
0382 
0383         if (d->logic == SearchJob::And) {
0384             for (int i = 0; i < d->criterias.size(); i++) {
0385                 const QByteArray key = d->criterias.at(i);
0386                 if (i > 0) {
0387                     searchKey += ' ';
0388                 }
0389                 searchKey += key;
0390             }
0391         } else {
0392             for (int i = 0; i < d->criterias.size(); i++) {
0393                 const QByteArray key = d->criterias.at(i);
0394                 if (i > 0) {
0395                     searchKey += ' ';
0396                 }
0397                 searchKey += '(' + key + ')';
0398             }
0399         }
0400     }
0401 
0402     QByteArray command = "SEARCH";
0403     if (d->uidBased) {
0404         command = "UID " + command;
0405     }
0406 
0407     d->sendCommand(command, searchKey);
0408 }
0409 
0410 void SearchJob::handleResponse(const Message &response)
0411 {
0412     Q_D(SearchJob);
0413 
0414     if (handleErrorReplies(response) == NotHandled) {
0415         if (response.content.size() >= 1 && response.content[0].toString() == "+") {
0416             if (d->term.isNull()) {
0417                 d->sessionInternal()->sendData(d->contents[d->nextContent]);
0418             } else {
0419                 qCWarning(KIMAP2_LOG) << "The term API only supports inline strings.";
0420             }
0421             d->nextContent++;
0422         } else if (response.content.size() >= 2 && response.content[1].toString() == "SEARCH") {
0423             for (int i = 2; i < response.content.size(); i++) {
0424                 d->results.append(response.content[i].toString().toInt());
0425             }
0426         }
0427     }
0428 }
0429 
0430 void SearchJob::setCharset(const QByteArray &charset)
0431 {
0432     Q_D(SearchJob);
0433     d->charset = charset;
0434 }
0435 
0436 QByteArray SearchJob::charset() const
0437 {
0438     Q_D(const SearchJob);
0439     return d->charset;
0440 }
0441 
0442 void SearchJob::setUidBased(bool uidBased)
0443 {
0444     Q_D(SearchJob);
0445     d->uidBased = uidBased;
0446 }
0447 
0448 bool SearchJob::isUidBased() const
0449 {
0450     Q_D(const SearchJob);
0451     return d->uidBased;
0452 }
0453 
0454 QVector<qint64> SearchJob::results() const
0455 {
0456     Q_D(const SearchJob);
0457     return d->results;
0458 }