diff --git a/autotests/searchjobtest.cpp b/autotests/searchjobtest.cpp index ae18abd..97d18b3 100644 --- a/autotests/searchjobtest.cpp +++ b/autotests/searchjobtest.cpp @@ -1,200 +1,213 @@ /* * Copyright (C) 2013 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ #include #include "kimaptest/fakeserver.h" #include "kimap/loginjob.h" #include "kimap/session.h" #include "kimap/searchjob.h" #include typedef QPair< KIMAP::SearchJob::SearchCriteria, QByteArray > SearchCriteriaValuePair; Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(KIMAP::SearchJob::SearchLogic) Q_DECLARE_METATYPE(KIMAP::Term) #define searchPair(a,b) qMakePair( a, b ) class SearchJobTest: public QObject { Q_OBJECT private Q_SLOTS: void testSearch_data() { QTest::addColumn >("scenario"); QTest::addColumn("uidbased"); QTest::addColumn("expectedResultsCount"); QTest::addColumn("searchLogic"); QTest::addColumn >("searchCriteria"); QList scenario; QList criteria; scenario << FakeServer::preauth() << "C: A000001 UID SEARCH HEADER Message-Id <12345678@mail.box>" << "S: * SEARCH 10 12" << "S: A000001 OK search done"; criteria << searchPair(KIMAP::SearchJob::Header, "Message-Id <12345678@mail.box>"); QTest::newRow("uidbased header search") << scenario << true << 2 << KIMAP::SearchJob::And << criteria; scenario.clear(); criteria.clear(); scenario << FakeServer::preauth() << "C: A000001 SEARCH OR (NEW) (HEADER Message-Id <12345678@mail.box>)" << "S: * SEARCH" << "S: A000001 OK search done"; criteria << searchPair(KIMAP::SearchJob::New, QByteArray()) << searchPair(KIMAP::SearchJob::Header, "Message-Id <12345678@mail.box>"); QTest::newRow("OR search with no results") << scenario << false << 0 << KIMAP::SearchJob::Or << criteria; scenario.clear(); criteria.clear(); scenario << FakeServer::preauth() << "C: A000001 SEARCH TO {25}\r\n" << "S: * SEARCH 1" << "S: A000001 OK search done"; criteria << searchPair(KIMAP::SearchJob::To, ""); QTest::newRow("literal data search") << scenario << false << 1 << KIMAP::SearchJob::And << criteria; scenario.clear(); criteria.clear(); scenario << FakeServer::preauth() << "C: A000001 UID SEARCH NOT (NEW)" << "S: * SEARCH 1 2 3 4 5 6" << "S: A000001 OK search done"; criteria << searchPair(KIMAP::SearchJob::New, QByteArray()); QTest::newRow("uidbased NOT NEW search") << scenario << true << 6 << KIMAP::SearchJob::Not << criteria; } void testSearch() { QFETCH(QList, scenario); QFETCH(bool, uidbased); QFETCH(int, expectedResultsCount); QFETCH(KIMAP::SearchJob::SearchLogic, searchLogic); QFETCH(QList, searchCriteria); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989); KIMAP::SearchJob *job = new KIMAP::SearchJob(&session); job->setUidBased(uidbased); job->setSearchLogic(searchLogic); for (const SearchCriteriaValuePair &pair : qAsConst(searchCriteria)) { if (pair.second.isEmpty()) { job->addSearchCriteria(pair.first); } else { job->addSearchCriteria(pair.first, pair.second); } } bool result = job->exec(); QVERIFY(result); if (result) { QVector foundItems = job->results(); QCOMPARE(foundItems.size(), expectedResultsCount); } fakeServer.quit(); } void testSearchTerm_data() { QTest::addColumn >("scenario"); QTest::addColumn("uidbased"); QTest::addColumn("expectedResultsCount"); QTest::addColumn("searchTerm"); { QList scenario; scenario << FakeServer::preauth() << "C: A000001 UID SEARCH HEADER Message-Id \"<12345678@mail.box>\"" << "S: * SEARCH 10 12" << "S: A000001 OK search done"; QTest::newRow("uidbased header search") << scenario << true << 2 << KIMAP::Term(QStringLiteral("Message-Id"), QStringLiteral("<12345678@mail.box>")); } { QList scenario; scenario << FakeServer::preauth() << "C: A000001 SEARCH OR NEW HEADER Message-Id \"<12345678@mail.box>\"" << "S: * SEARCH" << "S: A000001 OK search done"; QTest::newRow("OR search with no results") << scenario << false << 0 << KIMAP::Term(KIMAP::Term::Or, QVector() << KIMAP::Term(KIMAP::Term::New) << KIMAP::Term(QStringLiteral("Message-Id"), QStringLiteral("<12345678@mail.box>"))); } { QList scenario; scenario << FakeServer::preauth() << "C: A000001 SEARCH TO \"\"" << "S: * SEARCH 1" << "S: A000001 OK search done"; QTest::newRow("literal data search") << scenario << false << 1 << KIMAP::Term(KIMAP::Term::To, QStringLiteral("")); } { QList scenario; scenario << FakeServer::preauth() << "C: A000001 UID SEARCH NOT NEW" << "S: * SEARCH 1 2 3 4 5 6" << "S: A000001 OK search done"; QTest::newRow("uidbased NOT NEW search") << scenario << true << 6 << KIMAP::Term(KIMAP::Term::New).setNegated(true); } + + { + QList scenario; + scenario << FakeServer::preauth() + << "C: A000001 UID SEARCH OR HEADER Message-Id \"<1234567@mail.box>\" (OR HEADER Message-Id \"<7654321@mail.box>\" (OR HEADER Message-Id \"\" HEADER Message-Id \"\"))" + << "S: * SEARCH 1 2 3 4" + << "S: A000001 OK search done"; + KIMAP::Term term{KIMAP::Term::Or, {KIMAP::Term{QStringLiteral("Message-Id"), QStringLiteral("<1234567@mail.box>")}, + KIMAP::Term{QStringLiteral("Message-Id"), QStringLiteral("<7654321@mail.box>")}, + KIMAP::Term{QStringLiteral("Message-Id"), QStringLiteral("")}, + KIMAP::Term{QStringLiteral("Message-Id"), QStringLiteral("")}}}; + QTest::newRow("OR with multiple subterms") << scenario << true << 4 << term; + } } void testSearchTerm() { QFETCH(QList, scenario); QFETCH(bool, uidbased); QFETCH(int, expectedResultsCount); QFETCH(KIMAP::Term, searchTerm); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989); KIMAP::SearchJob *job = new KIMAP::SearchJob(&session); job->setUidBased(uidbased); job->setTerm(searchTerm); bool result = job->exec(); QVERIFY(result); if (result) { QVector foundItems = job->results(); QCOMPARE(foundItems.size(), expectedResultsCount); } fakeServer.quit(); } }; QTEST_GUILESS_MAIN(SearchJobTest) #include "searchjobtest.moc" diff --git a/src/searchjob.cpp b/src/searchjob.cpp index 10999ec..9da584b 100644 --- a/src/searchjob.cpp +++ b/src/searchjob.cpp @@ -1,587 +1,586 @@ /* Copyright (c) 2009 Andras Mantia This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "searchjob.h" #include #include "kimap_debug.h" #include #include "job_p.h" #include "response_p.h" #include "session_p.h" #include "imapset.h" namespace KIMAP { class Term::Private : public QSharedData { public: Private(): QSharedData(), isFuzzy(false), isNegated(false), isNull(false) {} Private(const Private &other) : QSharedData(other) , command(other.command) , isFuzzy(other.isFuzzy) , isNegated(other.isNegated) , isNull(other.isNull) {} Private &operator=(const Private &other) { command = other.command; isFuzzy = other.isFuzzy; isNegated = other.isNegated; isNull = other.isNull; return *this; } QByteArray command; bool isFuzzy; bool isNegated; bool isNull; }; Term::Term() : d(new Term::Private) { d->isNull = true; } Term::Term(Term::Relation relation, const QVector &subterms) : d(new Term::Private) { if (subterms.size() >= 2) { - d->command += "("; if (relation == KIMAP::Term::Or) { - d->command += "OR "; - d->command += subterms.at(0).serialize() + ' '; - if (subterms.size() >= 3) { - Term t(relation, subterms.mid(1)); - d->command += t.serialize(); - } else if (subterms.size() == 2) { - d->command += subterms.at(1).serialize(); + for (int i = 0; i < subterms.size() - 1; ++i) { + d->command += "(OR " + subterms[i].serialize() + " "; + } + d->command += subterms.back().serialize(); + for (int i = 0; i < subterms.size() - 1; ++i) { + d->command += ")"; } } else { + d->command += "("; for (const Term &t : subterms) { d->command += t.serialize() + ' '; } if (!subterms.isEmpty()) { d->command.chop(1); } + d->command += ")"; } - d->command += ")"; } else if (subterms.size() == 1) { d->command += subterms.first().serialize(); } else { d->isNull = true; } } Term::Term(Term::SearchKey key, const QString &value) : d(new Term::Private) { switch (key) { case All: d->command += "ALL"; break; case Bcc: d->command += "BCC"; break; case Cc: d->command += "CC"; break; case Body: d->command += "BODY"; break; case From: d->command += "FROM"; break; case Keyword: d->command += "KEYWORD"; break; case Subject: d->command += "SUBJECT"; break; case Text: d->command += "TEXT"; break; case To: d->command += "TO"; break; } if (key != All) { d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\""; } } Term::Term(const QString &header, const QString &value) : d(new Term::Private) { d->command += "HEADER"; d->command += ' ' + QByteArray(header.toUtf8().constData()); d->command += " \"" + QByteArray(value.toUtf8().constData()) + "\""; } Term::Term(Term::BooleanSearchKey key) : d(new Term::Private) { switch (key) { case Answered: d->command = "ANSWERED"; break; case Deleted: d->command = "DELETED"; break; case Draft: d->command = "DRAFT"; break; case Flagged: d->command = "FLAGGED"; break; case New: d->command = "NEW"; break; case Old: d->command = "OLD"; break; case Recent: d->command = "RECENT"; break; case Seen: d->command = "SEEN"; break; } } static QByteArray monthName(int month) { static const char* names[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; return (month >= 1 && month <= 12) ? QByteArray(names[month - 1]) : QByteArray(); } Term::Term(Term::DateSearchKey key, const QDate &date) : d(new Term::Private) { switch (key) { case Before: d->command = "BEFORE"; break; case On: d->command = "ON"; break; case SentBefore: d->command = "SENTBEFORE"; break; case SentOn: d->command = "SENTON"; break; case SentSince: d->command = "SENTSINCE"; break; case Since: d->command = "SINCE"; break; } d->command += " \""; d->command += QByteArray::number(date.day()) + '-'; d->command += monthName(date.month()) + '-'; d->command += QByteArray::number(date.year()); d->command += '\"'; } Term::Term(Term::NumberSearchKey key, int value) : d(new Term::Private) { switch (key) { case Larger: d->command = "LARGER"; break; case Smaller: d->command = "SMALLER"; break; } d->command += " " + QByteArray::number(value); } Term::Term(Term::SequenceSearchKey key, const ImapSet &set) : d(new Term::Private) { switch (key) { case Uid: d->command = "UID"; break; case SequenceNumber: break; } auto optimizedSet = set; optimizedSet.optimize(); d->command += " " + optimizedSet.toImapSequenceSet(); } Term::Term(const Term &other) : d(new Term::Private) { *d = *other.d; } Term::~Term() { } Term &Term::operator=(const Term &other) { *d = *other.d; return *this; } bool Term::operator==(const Term &other) const { return d->command == other.d->command && d->isNegated == other.d->isNegated && d->isFuzzy == other.d->isFuzzy; } QByteArray Term::serialize() const { QByteArray command; if (d->isFuzzy) { command = "FUZZY "; } if (d->isNegated) { command = "NOT "; } return command + d->command; } Term &Term::setFuzzy(bool fuzzy) { d->isFuzzy = fuzzy; return *this; } Term &Term::setNegated(bool negated) { d->isNegated = negated; return *this; } bool Term::isNull() const { return d->isNull; } //TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response class SearchJobPrivate : public JobPrivate { public: SearchJobPrivate(Session *session, const QString &name) : JobPrivate(session, name), logic(SearchJob::And) { criteriaMap[SearchJob::All] = "ALL"; criteriaMap[SearchJob::Answered] = "ANSWERED"; criteriaMap[SearchJob::BCC] = "BCC"; criteriaMap[SearchJob::Before] = "BEFORE"; criteriaMap[SearchJob::Body] = "BODY"; criteriaMap[SearchJob::CC] = "CC"; criteriaMap[SearchJob::Deleted] = "DELETED"; criteriaMap[SearchJob::Draft] = "DRAFT"; criteriaMap[SearchJob::Flagged] = "FLAGGED"; criteriaMap[SearchJob::From] = "FROM"; criteriaMap[SearchJob::Header] = "HEADER"; criteriaMap[SearchJob::Keyword] = "KEYWORD"; criteriaMap[SearchJob::Larger] = "LARGER"; criteriaMap[SearchJob::New] = "NEW"; criteriaMap[SearchJob::Old] = "OLD"; criteriaMap[SearchJob::On] = "ON"; criteriaMap[SearchJob::Recent] = "RECENT"; criteriaMap[SearchJob::Seen] = "SEEN"; criteriaMap[SearchJob::SentBefore] = "SENTBEFORE"; criteriaMap[SearchJob::SentOn] = "SENTON"; criteriaMap[SearchJob::SentSince] = "SENTSINCE"; criteriaMap[SearchJob::Since] = "SINCE"; criteriaMap[SearchJob::Smaller] = "SMALLER"; criteriaMap[SearchJob::Subject] = "SUBJECT"; criteriaMap[SearchJob::Text] = "TEXT"; criteriaMap[SearchJob::To] = "TO"; criteriaMap[SearchJob::Uid] = "UID"; criteriaMap[SearchJob::Unanswered] = "UNANSWERED"; criteriaMap[SearchJob::Undeleted] = "UNDELETED"; criteriaMap[SearchJob::Undraft] = "UNDRAFT"; criteriaMap[SearchJob::Unflagged] = "UNFLAGGED"; criteriaMap[SearchJob::Unkeyword] = "UNKEYWORD"; criteriaMap[SearchJob::Unseen] = "UNSEEN"; //don't use QDate::shortMonthName(), it returns a localized month name months[1] = "Jan"; months[2] = "Feb"; months[3] = "Mar"; months[4] = "Apr"; months[5] = "May"; months[6] = "Jun"; months[7] = "Jul"; months[8] = "Aug"; months[9] = "Sep"; months[10] = "Oct"; months[11] = "Nov"; months[12] = "Dec"; nextContent = 0; uidBased = false; } ~SearchJobPrivate() { } QByteArray charset; QList criterias; QMap criteriaMap; QMap months; SearchJob::SearchLogic logic; QList contents; QVector results; uint nextContent; bool uidBased; Term term; }; } using namespace KIMAP; SearchJob::SearchJob(Session *session) : Job(*new SearchJobPrivate(session, i18nc("Name of the search job", "Search"))) { } SearchJob::~SearchJob() { } void SearchJob::setTerm(const Term &term) { Q_D(SearchJob); d->term = term; } void SearchJob::doStart() { Q_D(SearchJob); QByteArray searchKey; if (!d->charset.isEmpty()) { searchKey = "CHARSET " + d->charset; } if (!d->term.isNull()) { const QByteArray term = d->term.serialize(); if (term.startsWith('(')) { searchKey += term.mid(1, term.size() - 2); } else { searchKey += term; } } else { if (d->logic == SearchJob::Not) { searchKey += "NOT "; } else if (d->logic == SearchJob::Or && d->criterias.size() > 1) { searchKey += "OR "; } if (d->logic == SearchJob::And) { const int numberCriterias(d->criterias.size()); for (int i = 0; i < numberCriterias; i++) { const QByteArray key = d->criterias.at(i); if (i > 0) { searchKey += ' '; } searchKey += key; } } else { const int numberCriterias(d->criterias.size()); for (int i = 0; i < numberCriterias; i++) { const QByteArray key = d->criterias.at(i); if (i > 0) { searchKey += ' '; } searchKey += '(' + key + ')'; } } } QByteArray command = "SEARCH"; if (d->uidBased) { command = "UID " + command; } d->tags << d->sessionInternal()->sendCommand(command, searchKey); } void SearchJob::handleResponse(const Response &response) { Q_D(SearchJob); if (handleErrorReplies(response) == NotHandled) { if (response.content.size() >= 1 && response.content[0].toString() == "+") { if (d->term.isNull()) { d->sessionInternal()->sendData(d->contents[d->nextContent]); } else { qCWarning(KIMAP_LOG) << "The term API only supports inline strings."; } d->nextContent++; } else if (response.content.size() >= 2 && response.content[1].toString() == "SEARCH") { for (int i = 2; i < response.content.size(); i++) { d->results.append(response.content[i].toString().toInt()); } } } } void SearchJob::setCharset(const QByteArray &charset) { Q_D(SearchJob); d->charset = charset; } QByteArray SearchJob::charset() const { Q_D(const SearchJob); return d->charset; } void SearchJob::setSearchLogic(SearchLogic logic) { Q_D(SearchJob); d->logic = logic; } void SearchJob::addSearchCriteria(SearchCriteria criteria) { Q_D(SearchJob); switch (criteria) { case All: case Answered: case Deleted: case Draft: case Flagged: case New: case Old: case Recent: case Seen: case Unanswered: case Undeleted: case Undraft: case Unflagged: case Unseen: d->criterias.append(d->criteriaMap[criteria]); break; default: //TODO Discuss if we keep error checking here, or accept anything, even if it is wrong qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " needs an argument, but none was specified."; break; } } void SearchJob::addSearchCriteria(SearchCriteria criteria, int argument) { Q_D(SearchJob); switch (criteria) { case Larger: case Smaller: d->criterias.append(d->criteriaMap[criteria] + ' ' + QByteArray::number(argument)); break; default: //TODO Discuss if we keep error checking here, or accept anything, even if it is wrong qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept an integer as an argument."; break; } } void SearchJob::addSearchCriteria(SearchCriteria criteria, const QByteArray &argument) { Q_D(SearchJob); switch (criteria) { case BCC: case Body: case CC: case From: case Subject: case Text: case To: d->contents.append(argument); d->criterias.append(d->criteriaMap[criteria] + " {" + QByteArray::number(argument.size()) + '}'); break; case Keyword: case Unkeyword: case Header: case Uid: d->criterias.append(d->criteriaMap[criteria] + ' ' + argument); break; default: //TODO Discuss if we keep error checking here, or accept anything, even if it is wrong qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept any argument."; break; } } void SearchJob::addSearchCriteria(SearchCriteria criteria, const QDate &argument) { Q_D(SearchJob); switch (criteria) { case Before: case On: case SentBefore: case SentSince: case Since: { QByteArray date = QByteArray::number(argument.day()) + '-'; date += d->months[argument.month()] + '-'; date += QByteArray::number(argument.year()); d->criterias.append(d->criteriaMap[criteria] + " \"" + date + '\"'); break; } default: //TODO Discuss if we keep error checking here, or accept anything, even if it is wrong qCDebug(KIMAP_LOG) << "Criteria " << d->criteriaMap[criteria] << " doesn't accept a date as argument."; break; } } void SearchJob::addSearchCriteria(const QByteArray &searchCriteria) { Q_D(SearchJob); d->criterias.append(searchCriteria); } void SearchJob::setUidBased(bool uidBased) { Q_D(SearchJob); d->uidBased = uidBased; } bool SearchJob::isUidBased() const { Q_D(const SearchJob); return d->uidBased; } QVector SearchJob::results() const { Q_D(const SearchJob); return d->results; }