diff --git a/autotests/imapsettest.cpp b/autotests/imapsettest.cpp index 0d58aa8..c4b368e 100644 --- a/autotests/imapsettest.cpp +++ b/autotests/imapsettest.cpp @@ -1,76 +1,175 @@ /* Copyright (C) 2009 Kevin Ottens 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 "kimap/imapset.h" #include #include using namespace KIMAP; +QByteArray operator""_ba(const char *str, std::size_t len) +{ + return QByteArray{str, static_cast(len)}; +} + class ImapSetTest : public QObject { Q_OBJECT private Q_SLOTS: void shouldConvertToAndFromByteArray_data() { ImapSet set; QTest::addColumn("imapSet"); QTest::addColumn("byteArray"); QTest::newRow("empty set") << ImapSet() << QByteArray(); QTest::newRow("unique value") << ImapSet(7) << QByteArray("7"); QTest::newRow("single interval") << ImapSet(7, 10) << QByteArray("7:10"); QTest::newRow("single interval with no upper bound") << ImapSet(1, 0) << QByteArray("1:*"); set = ImapSet(7, 10); set.add(ImapInterval(12, 14)); QTest::newRow("two intervals") << set << QByteArray("7:10,12:14"); set = ImapSet(7, 10); set.add(ImapInterval(12)); QTest::newRow("two intervals with an infinite one") << set << QByteArray("7:10,12:*"); set = ImapSet(7, 10); set.add(5); QTest::newRow("one interval and a value") << set << QByteArray("7:10,5"); set = ImapSet(7, 10); set.add(QVector() << 5 << 3); QTest::newRow("one interval and two values") << set << QByteArray("7:10,3,5"); } void shouldConvertToAndFromByteArray() { QFETCH(ImapSet, imapSet); QFETCH(QByteArray, byteArray); QCOMPARE(QString::fromUtf8(imapSet.toImapSequenceSet()), QString::fromUtf8(byteArray)); //qDebug() << "Expects" << imapSet << "got" << ImapSet::fromImapSequenceSet( byteArray ); QCOMPARE(ImapSet::fromImapSequenceSet(byteArray), imapSet); } + + void testOptimize_data() + { + QTest::addColumn("imapSet"); + QTest::addColumn("originalString"); + QTest::addColumn("expectedString"); + + { + ImapSet imapSet; + for (int i = 1; i <= 10; ++i) { + imapSet.add(i); + } + QTest::newRow("Neighbouring numbers") << imapSet << "1,2,3,4,5,6,7,8,9,10"_ba << "1:10"_ba; + } + { + ImapSet imapSet; + imapSet.add(ImapInterval{1, 3}); + imapSet.add(ImapInterval{5, 7}); + QTest::newRow("Neighbouring intervals with a gap") << imapSet << "1:3,5:7"_ba << "1:3,5:7"_ba; + } + { + ImapSet imapSet; + for (int i : { 5, 8, 3, 1, 9, 2, 7, 4, 6 }) { + imapSet.add(i); + } + QTest::newRow("Random order") << imapSet << "5,8,3,1,9,2,7,4,6"_ba << "1:9"_ba; + } + { + ImapSet imapSet; + imapSet.add(ImapInterval{1, 3}); + imapSet.add(ImapInterval{2, 4}); + QTest::newRow("Overlapping") << imapSet << "1:3,2:4"_ba << "1:4"_ba; + } + { + ImapSet imapSet; + imapSet.add(ImapInterval{2, 4}); + imapSet.add(ImapInterval{1, 3}); + imapSet.add(4); + imapSet.add(ImapInterval{7, 8}); + imapSet.add(ImapInterval{8, 9}); + QTest::newRow("Multiple overlapping with a gap") << imapSet << "2:4,1:3,4,7:8,8:9"_ba << "1:4,7:9"_ba; + } + { + ImapSet imapSet; + imapSet.add(5); + imapSet.add(8); + imapSet.add(10); + imapSet.add(ImapInterval{0, 20}); + QTest::newRow("Overlapping multiple intervals") << imapSet << "5,8,10,0:20"_ba << "0:20"_ba; + } + { + ImapSet imapSet; + imapSet.add(1); + imapSet.add(ImapInterval{3, 5}); + imapSet.add(ImapInterval{4, 0}); + QTest::newRow("Open end overlap") << imapSet << "1,3:5,4:*"_ba << "1,3:*"_ba; + } + { + ImapSet imapSet; + imapSet.add(ImapInterval{1, 4}); + imapSet.add(3); + QTest::newRow("Value within interval") << imapSet << "1:4,3"_ba << "1:4"_ba; + } + { + ImapSet imapSet; + imapSet.add(ImapInterval{1, 0}); + imapSet.add(ImapInterval{3, 0}); + imapSet.add(5); + QTest::newRow("Multiple open end intervals") << imapSet << "1:*,3:*,5"_ba << "1:*"_ba; + } + { + ImapSet imapSet; + for (ImapSet::Id id : {1, 2, 3, 5, 6, 8, 9, 10, 15, 16, 19, 20, 21, 23}) { + imapSet.add(id); + } + QTest::newRow("Merge single values") << imapSet << "1,2,3,5,6,8,9,10,15,16,19,20,21,23"_ba + << "1:3,5:6,8:10,15:16,19:21,23"_ba; + } + } + + void testOptimize() + { + QFETCH(ImapSet, imapSet); + QFETCH(QByteArray, originalString); + QFETCH(QByteArray, expectedString); + + QCOMPARE(imapSet.intervals().size(), originalString.count(",") + 1); + QCOMPARE(imapSet.toImapSequenceSet(), originalString); + + imapSet.optimize(); + + QCOMPARE(imapSet.intervals().size(), expectedString.count(",") + 1); + QCOMPARE(imapSet.toImapSequenceSet(), expectedString); + } }; QTEST_GUILESS_MAIN(ImapSetTest) #include "imapsettest.moc" diff --git a/src/copyjob.cpp b/src/copyjob.cpp index 3450d7f..f6d5b46 100644 --- a/src/copyjob.cpp +++ b/src/copyjob.cpp @@ -1,131 +1,132 @@ /* 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 "copyjob.h" #include #include "job_p.h" #include "response_p.h" #include "session_p.h" #include "rfccodecs.h" //TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response namespace KIMAP { class CopyJobPrivate : public JobPrivate { public: CopyJobPrivate(Session *session, const QString &name) : JobPrivate(session, name), uidBased(false) { } ~CopyJobPrivate() { } QString mailBox; ImapSet set; bool uidBased; ImapSet resultingUids; }; } using namespace KIMAP; CopyJob::CopyJob(Session *session) : Job(*new CopyJobPrivate(session, i18n("Copy"))) { Q_D(CopyJob); d->uidBased = false; } CopyJob::~CopyJob() { } void CopyJob::setMailBox(const QString &mailBox) { Q_D(CopyJob); d->mailBox = mailBox; } QString CopyJob::mailBox() const { Q_D(const CopyJob); return d->mailBox; } void CopyJob::setSequenceSet(const ImapSet &set) { Q_D(CopyJob); d->set = set; } ImapSet CopyJob::sequenceSet() const { Q_D(const CopyJob); return d->set; } void CopyJob::setUidBased(bool uidBased) { Q_D(CopyJob); d->uidBased = uidBased; } bool CopyJob::isUidBased() const { Q_D(const CopyJob); return d->uidBased; } ImapSet CopyJob::resultingUids() const { Q_D(const CopyJob); return d->resultingUids; } void CopyJob::doStart() { Q_D(CopyJob); + d->set.optimize(); QByteArray parameters = d->set.toImapSequenceSet() + ' '; parameters += '\"' + KIMAP::encodeImapFolderName(d->mailBox.toUtf8()) + '\"'; QByteArray command = "COPY"; if (d->uidBased) { command = "UID " + command; } d->tags << d->sessionInternal()->sendCommand(command, parameters); } void CopyJob::handleResponse(const Response &response) { Q_D(CopyJob); for (auto it = response.responseCode.cbegin(), end = response.responseCode.cend(); it != end; ++it) { if (it->toString() == "COPYUID") { it = it + 3; if (it < end) { d->resultingUids = ImapSet::fromImapSequenceSet(it->toString()); } break; } } handleErrorReplies(response); } diff --git a/src/fetchjob.cpp b/src/fetchjob.cpp index 5f7ebc9..9523286 100644 --- a/src/fetchjob.cpp +++ b/src/fetchjob.cpp @@ -1,608 +1,609 @@ /* Copyright (c) 2009 Kevin Ottens 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 "fetchjob.h" #include #include "kimap_debug.h" #include #include "job_p.h" #include "response_p.h" #include "session_p.h" namespace KIMAP { class FetchJobPrivate : public JobPrivate { public: FetchJobPrivate(FetchJob *job, Session *session, const QString &name) : JobPrivate(session, name) , q(job) , uidBased(false) , gmailEnabled(false) { } ~FetchJobPrivate() { } void parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content); void parsePart(const QByteArray &structure, int &pos, KMime::Content *content); QByteArray parseString(const QByteArray &structure, int &pos); QByteArray parseSentence(const QByteArray &structure, int &pos); void skipLeadingSpaces(const QByteArray &structure, int &pos); void emitPendings() { if (pendingMsgs.isEmpty()) { return; } Q_EMIT q->messagesAvailable(pendingMsgs); if (!pendingParts.isEmpty()) { Q_EMIT q->partsReceived(selectedMailBox, pendingUids, pendingParts); Q_EMIT q->partsReceived(selectedMailBox, pendingUids, pendingAttributes, pendingParts); } if (!pendingSizes.isEmpty() || !pendingFlags.isEmpty() || !pendingMessages.isEmpty()) { Q_EMIT q->headersReceived(selectedMailBox, pendingUids, pendingSizes, pendingFlags, pendingMessages); Q_EMIT q->headersReceived(selectedMailBox, pendingUids, pendingSizes, pendingAttributes, pendingFlags, pendingMessages); } if (!pendingMessages.isEmpty()) { Q_EMIT q->messagesReceived(selectedMailBox, pendingUids, pendingMessages); Q_EMIT q->messagesReceived(selectedMailBox, pendingUids, pendingAttributes, pendingMessages); } pendingUids.clear(); pendingMessages.clear(); pendingParts.clear(); pendingSizes.clear(); pendingFlags.clear(); pendingAttributes.clear(); pendingMsgs.clear(); } FetchJob *const q; ImapSet set; bool uidBased; FetchJob::FetchScope scope; QString selectedMailBox; bool gmailEnabled; QTimer emitPendingsTimer; QMap pendingMessages; QMap pendingParts; QMap pendingFlags; QMap pendingAttributes; QMap pendingSizes; QMap pendingUids; QMap pendingMsgs; }; } using namespace KIMAP; FetchJob::FetchScope::FetchScope(): mode(FetchScope::Content), changedSince(0) { } FetchJob::FetchJob(Session *session) : Job(*new FetchJobPrivate(this, session, i18n("Fetch"))) { Q_D(FetchJob); connect(&d->emitPendingsTimer, SIGNAL(timeout()), this, SLOT(emitPendings())); } FetchJob::~FetchJob() { } void FetchJob::setSequenceSet(const ImapSet &set) { Q_D(FetchJob); - Q_ASSERT(!set.toImapSequenceSet().trimmed().isEmpty()); + Q_ASSERT(!set.isEmpty()); d->set = set; } ImapSet FetchJob::sequenceSet() const { Q_D(const FetchJob); return d->set; } void FetchJob::setUidBased(bool uidBased) { Q_D(FetchJob); d->uidBased = uidBased; } bool FetchJob::isUidBased() const { Q_D(const FetchJob); return d->uidBased; } void FetchJob::setScope(const FetchScope &scope) { Q_D(FetchJob); d->scope = scope; } FetchJob::FetchScope FetchJob::scope() const { Q_D(const FetchJob); return d->scope; } bool FetchJob::setGmailExtensionsEnabled() const { Q_D(const FetchJob); return d->gmailEnabled; } void FetchJob::setGmailExtensionsEnabled(bool enabled) { Q_D(FetchJob); d->gmailEnabled = enabled; } QString FetchJob::mailBox() const { Q_D(const FetchJob); return d->selectedMailBox; } void FetchJob::doStart() { Q_D(FetchJob); + d->set.optimize(); QByteArray parameters = d->set.toImapSequenceSet() + ' '; Q_ASSERT(!parameters.trimmed().isEmpty()); switch (d->scope.mode) { case FetchScope::Headers: if (d->scope.parts.isEmpty()) { parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID"; } else { parameters += '('; for (const QByteArray &part : qAsConst(d->scope.parts)) { parameters += "BODY.PEEK[" + part + ".MIME] "; } parameters += "UID"; } break; case FetchScope::Flags: parameters += "(FLAGS UID"; break; case FetchScope::Structure: parameters += "(BODYSTRUCTURE UID"; break; case FetchScope::Content: if (d->scope.parts.isEmpty()) { parameters += "(BODY.PEEK[] UID"; } else { parameters += '('; for (const QByteArray &part : qAsConst(d->scope.parts)) { parameters += "BODY.PEEK[" + part + "] "; } parameters += "UID"; } break; case FetchScope::Full: parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID"; break; case FetchScope::HeaderAndContent: if (d->scope.parts.isEmpty()) { parameters += "(BODY.PEEK[] FLAGS UID"; } else { parameters += "(BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)]"; for (const QByteArray &part : qAsConst(d->scope.parts)) { parameters += " BODY.PEEK[" + part + ".MIME] BODY.PEEK[" + part + "]"; //krazy:exclude=doublequote_chars } parameters += " FLAGS UID"; } break; case FetchScope::FullHeaders: parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID"; break; } if (d->gmailEnabled) { parameters += " X-GM-LABELS X-GM-MSGID X-GM-THRID"; } parameters += ")"; if (d->scope.changedSince > 0) { parameters += " (CHANGEDSINCE " + QByteArray::number(d->scope.changedSince) + ")"; } QByteArray command = "FETCH"; if (d->uidBased) { command = "UID " + command; } d->emitPendingsTimer.start(100); d->selectedMailBox = d->m_session->selectedMailBox(); d->tags << d->sessionInternal()->sendCommand(command, parameters); } void FetchJob::handleResponse(const Response &response) { Q_D(FetchJob); // We can predict it'll be handled by handleErrorReplies() so stop // the timer now so that result() will really be the last emitted signal. if (!response.content.isEmpty() && d->tags.size() == 1 && d->tags.contains(response.content.first().toString())) { d->emitPendingsTimer.stop(); d->emitPendings(); } if (handleErrorReplies(response) == NotHandled) { if (response.content.size() == 4 && response.content[2].toString() == "FETCH" && response.content[3].type() == Response::Part::List) { qint64 id = response.content[1].toString().toLongLong(); QList content = response.content[3].toList(); Message msg; MessagePtr message(new KMime::Message); bool shouldParseMessage = false; MessageParts parts; for (QList::ConstIterator it = content.constBegin(); it != content.constEnd(); ++it) { QByteArray str = *it; ++it; if (it == content.constEnd()) { // Uh oh, message was truncated? qCWarning(KIMAP_LOG) << "FETCH reply got truncated, skipping."; break; } if (str == "UID") { d->pendingUids[id] = msg.uid = it->toLongLong(); } else if (str == "RFC822.SIZE") { d->pendingSizes[id] = msg.size = it->toLongLong(); } else if (str == "INTERNALDATE") { message->date()->setDateTime(QDateTime::fromString(QLatin1String(*it), Qt::RFC2822Date)); } else if (str == "FLAGS") { if ((*it).startsWith('(') && (*it).endsWith(')')) { QByteArray str = *it; str.chop(1); str.remove(0, 1); const auto flags = str.split(' '); d->pendingFlags[id] = flags; msg.flags = flags; } else { d->pendingFlags[id] << *it; msg.flags << *it; } } else if (str == "X-GM-LABELS") { d->pendingAttributes.insert(id, { "X-GM-LABELS", *it }); msg.attributes.insert("X-GM-LABELS", *it); } else if (str == "X-GM-THRID") { d->pendingAttributes.insert(id, { "X-GM-THRID", *it }); msg.attributes.insert("X-GM-THRID", *it); } else if (str == "X-GM-MSGID") { d->pendingAttributes.insert(id, { "X-GM-MSGID", *it }); msg.attributes.insert("X-GM-MSGID", *it); } else if (str == "BODYSTRUCTURE") { int pos = 0; d->parseBodyStructure(*it, pos, message.data()); message->assemble(); d->pendingMessages[id] = message; msg.message = message; } else if (str.startsWith("BODY[")) { //krazy:exclude=strings if (!str.endsWith(']')) { // BODY[ ... ] might have been split, skip until we find the ] while (!(*it).endsWith(']')) { ++it; } ++it; } int index; if ((index = str.indexOf("HEADER")) > 0 || (index = str.indexOf("MIME")) > 0) { // headers if (str[index - 1] == '.') { QByteArray partId = str.mid(5, index - 6); if (!parts.contains(partId)) { parts[partId] = ContentPtr(new KMime::Content); } parts[partId]->setHead(*it); parts[partId]->parse(); d->pendingParts[id] = parts; msg.parts = parts; } else { message->setHead(*it); shouldParseMessage = true; } } else { // full payload if (str == "BODY[]") { message->setContent(KMime::CRLFtoLF(*it)); shouldParseMessage = true; d->pendingMessages[id] = message; msg.message = message; } else { QByteArray partId = str.mid(5, str.size() - 6); if (!parts.contains(partId)) { parts[partId] = ContentPtr(new KMime::Content); } parts[partId]->setBody(*it); parts[partId]->parse(); d->pendingParts[id] = parts; msg.parts = parts; } } } } if (shouldParseMessage) { message->parse(); } // For the headers mode the message is built in several // steps, hence why we wait it to be done until putting it // in the pending queue. if (d->scope.mode == FetchScope::Headers || d->scope.mode == FetchScope::HeaderAndContent || d->scope.mode == FetchScope::FullHeaders) { d->pendingMessages[id] = message; msg.message = message; } d->pendingMsgs[id] = msg; } } } void FetchJobPrivate::parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content) { skipLeadingSpaces(structure, pos); if (structure[pos] != '(') { return; } pos++; if (structure[pos] != '(') { // simple part pos--; parsePart(structure, pos, content); } else { // multi part content->contentType()->setMimeType("MULTIPART/MIXED"); while (pos < structure.size() && structure[pos] == '(') { KMime::Content *child = new KMime::Content; content->addContent(child); parseBodyStructure(structure, pos, child); child->assemble(); } QByteArray subType = parseString(structure, pos); content->contentType()->setMimeType("MULTIPART/" + subType); QByteArray parameters = parseSentence(structure, pos); // FIXME: Read the charset if (parameters.contains("BOUNDARY")) { content->contentType()->setBoundary(parameters.remove(0, parameters.indexOf("BOUNDARY") + 11).split('\"')[0]); } QByteArray disposition = parseSentence(structure, pos); if (disposition.contains("INLINE")) { content->contentDisposition()->setDisposition(KMime::Headers::CDinline); } else if (disposition.contains("ATTACHMENT")) { content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); } parseSentence(structure, pos); // Ditch the body language } // Consume what's left while (pos < structure.size() && structure[pos] != ')') { skipLeadingSpaces(structure, pos); parseSentence(structure, pos); skipLeadingSpaces(structure, pos); } pos++; } void FetchJobPrivate::parsePart(const QByteArray &structure, int &pos, KMime::Content *content) { if (structure[pos] != '(') { return; } pos++; QByteArray mainType = parseString(structure, pos); QByteArray subType = parseString(structure, pos); content->contentType()->setMimeType(mainType + '/' + subType); parseSentence(structure, pos); // Ditch the parameters... FIXME: Read it to get charset and name parseString(structure, pos); // ... and the id content->contentDescription()->from7BitString(parseString(structure, pos)); parseString(structure, pos); // Ditch the encoding too parseString(structure, pos); // ... and the size parseString(structure, pos); // ... and the line count QByteArray disposition = parseSentence(structure, pos); if (disposition.contains("INLINE")) { content->contentDisposition()->setDisposition(KMime::Headers::CDinline); } else if (disposition.contains("ATTACHMENT")) { content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); } if ((content->contentDisposition()->disposition() == KMime::Headers::CDattachment || content->contentDisposition()->disposition() == KMime::Headers::CDinline) && disposition.contains("FILENAME")) { QByteArray filename = disposition.remove(0, disposition.indexOf("FILENAME") + 11).split('\"')[0]; content->contentDisposition()->setFilename(QLatin1String(filename)); } // Consume what's left while (pos < structure.size() && structure[pos] != ')') { skipLeadingSpaces(structure, pos); parseSentence(structure, pos); skipLeadingSpaces(structure, pos); } } QByteArray FetchJobPrivate::parseSentence(const QByteArray &structure, int &pos) { QByteArray result; int stack = 0; skipLeadingSpaces(structure, pos); if (structure[pos] != '(') { return parseString(structure, pos); } int start = pos; do { switch (structure[pos]) { case '(': pos++; stack++; break; case ')': pos++; stack--; break; case '[': pos++; stack++; break; case ']': pos++; stack--; break; default: skipLeadingSpaces(structure, pos); parseString(structure, pos); skipLeadingSpaces(structure, pos); break; } } while (pos < structure.size() && stack != 0); result = structure.mid(start, pos - start); return result; } QByteArray FetchJobPrivate::parseString(const QByteArray &structure, int &pos) { QByteArray result; skipLeadingSpaces(structure, pos); int start = pos; bool foundSlash = false; // quoted string if (structure[pos] == '"') { pos++; Q_FOREVER { if (structure[pos] == '\\') { pos += 2; foundSlash = true; continue; } if (structure[pos] == '"') { result = structure.mid(start + 1, pos - start - 1); pos++; break; } pos++; } } else { // unquoted string Q_FOREVER { if (structure[pos] == ' ' || structure[pos] == '(' || structure[pos] == ')' || structure[pos] == '[' || structure[pos] == ']' || structure[pos] == '\n' || structure[pos] == '\r' || structure[pos] == '"') { break; } if (structure[pos] == '\\') { foundSlash = true; } pos++; } result = structure.mid(start, pos - start); // transform unquoted NIL if (result == "NIL") { result.clear(); } } // simplify slashes if (foundSlash) { while (result.contains("\\\"")) { result.replace("\\\"", "\""); } while (result.contains("\\\\")) { result.replace("\\\\", "\\"); } } return result; } void FetchJobPrivate::skipLeadingSpaces(const QByteArray &structure, int &pos) { while (pos < structure.size() && structure[pos] == ' ') { pos++; } } #include "moc_fetchjob.cpp" diff --git a/src/imapset.cpp b/src/imapset.cpp index 234516b..4e9028e 100644 --- a/src/imapset.cpp +++ b/src/imapset.cpp @@ -1,329 +1,361 @@ /* Copyright (c) 2007 Volker Krause 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 "imapset.h" #include using namespace KIMAP; class ImapInterval::Private : public QSharedData { public: Private() : QSharedData(), begin(0), end(0) {} Private(const Private &other) : QSharedData(other) { begin = other.begin; end = other.end; } Id begin; Id end; }; class ImapSet::Private : public QSharedData { public: Private() : QSharedData() {} Private(const Private &other) : QSharedData(other) { intervals = other.intervals; } ImapInterval::List intervals; }; ImapInterval::ImapInterval() : d(new Private) { } ImapInterval::ImapInterval(const ImapInterval &other) : d(other.d) { } ImapInterval::ImapInterval(Id begin, Id end) : d(new Private) { d->begin = begin; d->end = end; } ImapInterval::~ ImapInterval() { } ImapInterval &ImapInterval::operator =(const ImapInterval &other) { if (this != &other) { d = other.d; } return *this; } bool ImapInterval::operator ==(const ImapInterval &other) const { return (d->begin == other.d->begin && d->end == other.d->end); } ImapInterval::Id ImapInterval::size() const { if (!d->begin && !d->end) { return 0; } if (d->begin && !d->end) { return Q_INT64_C(0x7FFFFFFFFFFFFFFF) - d->begin + 1; } return d->end - d->begin + 1; } bool ImapInterval::hasDefinedBegin() const { return d->begin != 0; } ImapInterval::Id ImapInterval::begin() const { return d->begin; } bool ImapInterval::hasDefinedEnd() const { return d->end != 0; } ImapInterval::Id ImapInterval::end() const { if (hasDefinedEnd()) { return d->end; } return std::numeric_limits::max(); } void ImapInterval::setBegin(Id value) { Q_ASSERT(value >= 0); Q_ASSERT(value <= d->end || !hasDefinedEnd()); d->begin = value; } void ImapInterval::setEnd(Id value) { Q_ASSERT(value >= 0); Q_ASSERT(value >= d->begin || !hasDefinedBegin()); d->end = value; } QByteArray ImapInterval::toImapSequence() const { if (size() == 0) { return QByteArray(); } if (size() == 1) { return QByteArray::number(d->begin); } QByteArray rv = QByteArray::number(d->begin) + ':'; if (hasDefinedEnd()) { rv += QByteArray::number(d->end); } else { rv += '*'; } return rv; } ImapInterval ImapInterval::fromImapSequence(const QByteArray &sequence) { QList values = sequence.split(':'); if (values.isEmpty() || values.size() > 2) { return ImapInterval(); } bool ok = false; Id begin = values[0].toLongLong(&ok); if (!ok) { return ImapInterval(); } Id end; if (values.size() == 1) { end = begin; } else if (values[1] == QByteArray("*")) { end = 0; } else { ok = false; end = values[1].toLongLong(&ok); if (!ok) { return ImapInterval(); } } return ImapInterval(begin, end); } ImapSet::ImapSet() : d(new Private) { } ImapSet::ImapSet(Id begin, Id end) : d(new Private) { add(ImapInterval(begin, end)); } ImapSet::ImapSet(Id value) : d(new Private) { add(QVector() << value); } ImapSet::ImapSet(const ImapSet &other) : d(other.d) { } ImapSet::~ImapSet() { } ImapSet &ImapSet::operator =(const ImapSet &other) { if (this != &other) { d = other.d; } return *this; } bool ImapSet::operator ==(const ImapSet &other) const { if (d->intervals.size() != other.d->intervals.size()) { return false; } for (const ImapInterval &interval : qAsConst(d->intervals)) { if (!other.d->intervals.contains(interval)) { return false; } } return true; } void ImapSet::add(Id value) { add(QVector() << value); } void ImapSet::add(const QVector &values) { QVector vals = values; std::sort(vals.begin(), vals.end()); for (int i = 0; i < vals.count(); ++i) { const Id begin = vals[i]; Q_ASSERT(begin >= 0); if (i == vals.count() - 1) { d->intervals << ImapInterval(begin, begin); break; } do { ++i; Q_ASSERT(vals[i] >= 0); if (vals[i] != (vals[i - 1] + 1)) { --i; break; } } while (i < vals.count() - 1); d->intervals << ImapInterval(begin, vals[i]); } } void ImapSet::add(const ImapInterval &interval) { d->intervals << interval; } QByteArray ImapSet::toImapSequenceSet() const { QList rv; rv.reserve(d->intervals.count()); for (const ImapInterval &interval : qAsConst(d->intervals)) { rv << interval.toImapSequence(); } QByteArray result; if (!rv.isEmpty()) { result = rv.first(); QList::ConstIterator it = rv.constBegin(); const QList::ConstIterator end = rv.constEnd(); ++it; for (; it != end; ++it) { result += ',' + (*it); } } return result; } ImapSet ImapSet::fromImapSequenceSet(const QByteArray &sequence) { ImapSet result; const QList intervals = sequence.split(','); for (const QByteArray &interval : qAsConst(intervals)) { if (!interval.isEmpty()) { result.add(ImapInterval::fromImapSequence(interval)); } } return result; } ImapInterval::List ImapSet::intervals() const { return d->intervals; } bool ImapSet::isEmpty() const { return d->intervals.isEmpty(); } +void ImapSet::optimize() +{ + // There's nothing to optimize if we have fewer than 2 intervals + if (d->intervals.size() < 2) { + return; + } + + // Sort the intervals in ascending order by their beginning value + std::sort(d->intervals.begin(), d->intervals.end(), + [](const ImapInterval &lhs, const ImapInterval &rhs) { + return lhs.begin() < rhs.begin(); + }); + + auto it = d->intervals.begin(); + while (it != d->intervals.end() && it != std::prev(d->intervals.end())) { + auto next = std::next(it); + // +1 so that we also merge neighbouring intervals, e.g. 1:2,3:4 -> 1:4 + if (it->hasDefinedEnd() && it->end() + 1 >= next->begin()) { + next->setBegin(it->begin()); + if (next->hasDefinedEnd() && it->end() > next->end()) { + next->setEnd(it->end()); + } + it = d->intervals.erase(it); + } else if (!it->hasDefinedEnd()) { + // We can eat up all the remaining intervals + it = d->intervals.erase(next, d->intervals.end()); + } else { + ++it; + } + } +} + QDebug &operator<<(QDebug &d, const ImapInterval &interval) { d << interval.toImapSequence(); return d; } QDebug &operator<<(QDebug &d, const ImapSet &set) { d << set.toImapSequenceSet(); return d; } diff --git a/src/imapset.h b/src/imapset.h index d2b8b46..9c4aa6d 100644 --- a/src/imapset.h +++ b/src/imapset.h @@ -1,241 +1,250 @@ /* Copyright (c) 2007 Volker Krause 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. */ #ifndef KIMAP_IMAPSET_H #define KIMAP_IMAPSET_H #include "kimap_export.h" #include #include #include #include #include namespace KIMAP { /** Represents a single interval in an ImapSet. This class is implicitly shared. */ class KIMAP_EXPORT ImapInterval { public: /** * Describes the ids stored in the interval. */ typedef qint64 Id; /** A list of ImapInterval objects. */ typedef QList List; /** Constructs an interval that covers all positive numbers. */ ImapInterval(); /** Copy constructor. */ ImapInterval(const ImapInterval &other); /** Create a new interval. @param begin The begin of the interval. @param end Keep default (0) to just set the interval begin */ explicit ImapInterval(Id begin, Id end = 0); /** Destructor. */ ~ImapInterval(); /** Assignment operator. */ ImapInterval &operator=(const ImapInterval &other); /** Comparison operator. */ bool operator==(const ImapInterval &other) const; /** Returns the size of this interval. Size is only defined for finite intervals. */ Id size() const; /** Returns true if this interval has a defined begin. */ bool hasDefinedBegin() const; /** Returns the begin of this interval. The value is the smallest value part of the interval. Only valid if begin is defined. */ Id begin() const; /** Returns true if this intercal has been defined. */ bool hasDefinedEnd() const; /** Returns the end of this interval. This value is the largest value part of the interval. Only valid if hasDefinedEnd() returned true. */ Id end() const; /** Sets the begin of the interval. */ void setBegin(Id value); /** Sets the end of this interval. */ void setEnd(Id value); /** Converts this set into an IMAP compatible sequence. */ QByteArray toImapSequence() const; /** Return the interval corresponding to the given IMAP-compatible QByteArray representation */ static ImapInterval fromImapSequence(const QByteArray &sequence); private: class Private; QSharedDataPointer d; }; /** Represents a set of natural numbers (1->\f$\infty\f$) in a as compact as possible form. Used to address Akonadi items via the IMAP protocol or in the database. This class is implicitly shared. */ class KIMAP_EXPORT ImapSet { public: /** * Describes the ids stored in the set. */ typedef qint64 Id; /** Constructs an empty set. */ ImapSet(); /** Constructs a set containing a single interval. */ ImapSet(Id begin, Id end); /** Constructs a set containing a single value. */ explicit ImapSet(Id value); /** Copy constructor. */ ImapSet(const ImapSet &other); /** Destructor. */ ~ImapSet(); /** Assignment operator. */ ImapSet &operator=(const ImapSet &other); /** Comparison operator. */ bool operator==(const ImapSet &other) const; /** Adds a single positive integer numbers to the set. The list is sorted and split into as large as possible intervals. No interval merging is performed. @param value A positive integer number */ void add(Id value); /** Adds the given list of positive integer numbers to the set. The list is sorted and split into as large as possible intervals. No interval merging is performed. @param values List of positive integer numbers in arbitrary order */ void add(const QVector &values); /** Adds the given ImapInterval to this set. No interval merging is performed. @param interval the interval to add */ void add(const ImapInterval &interval); /** Returns a IMAP-compatible QByteArray representation of this set. */ QByteArray toImapSequenceSet() const; /** Return the set corresponding to the given IMAP-compatible QByteArray representation */ static ImapSet fromImapSequenceSet(const QByteArray &sequence); /** Returns the intervals this set consists of. */ ImapInterval::List intervals() const; /** Returns true if this set doesn't contains any values. */ bool isEmpty() const; + /** + * Optimizes the ImapSet by sorting and merging overlapping intervals. + * + * Normally you shouldn't need to call this method. KIMAP will make sure + * to opimize the ImapSet before serializing it to string and sending it + * to the IMAP server. + */ + void optimize(); + private: class Private; QSharedDataPointer d; }; } KIMAP_EXPORT QDebug &operator<<(QDebug &d, const KIMAP::ImapInterval &interval); KIMAP_EXPORT QDebug &operator<<(QDebug &d, const KIMAP::ImapSet &set); Q_DECLARE_METATYPE(KIMAP::ImapInterval) Q_DECLARE_METATYPE(KIMAP::ImapInterval::List) Q_DECLARE_METATYPE(KIMAP::ImapSet) #endif diff --git a/src/movejob.cpp b/src/movejob.cpp index 9bb1203..7aca7bd 100644 --- a/src/movejob.cpp +++ b/src/movejob.cpp @@ -1,137 +1,138 @@ /* Copyright (c) 2016 Daniel Vrátil 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 "movejob.h" #include "job_p.h" #include "response_p.h" #include "session_p.h" #include "rfccodecs.h" #include //TODO: when custom error codes are introduced, handle the NO [TRYCREATE] response namespace KIMAP { class MoveJobPrivate : public JobPrivate { public: MoveJobPrivate(Session *session, const QString &name) : JobPrivate(session, name) , uidBased(false) {} ~MoveJobPrivate() {} QString mailBox; ImapSet set; ImapSet resultingUids; bool uidBased; }; } using namespace KIMAP; MoveJob::MoveJob(Session *session) : Job(*new MoveJobPrivate(session, i18n("Move"))) { Q_D(MoveJob); d->uidBased = false; } MoveJob::~MoveJob() { } void MoveJob::setMailBox(const QString &mailBox) { Q_D(MoveJob); d->mailBox = mailBox; } QString MoveJob::mailBox() const { Q_D(const MoveJob); return d->mailBox; } void MoveJob::setSequenceSet(const ImapSet &set) { Q_D(MoveJob); d->set = set; } ImapSet MoveJob::sequenceSet() const { Q_D(const MoveJob); return d->set; } void MoveJob::setUidBased(bool uidBased) { Q_D(MoveJob); d->uidBased = uidBased; } bool MoveJob::isUidBased() const { Q_D(const MoveJob); return d->uidBased; } ImapSet MoveJob::resultingUids() const { Q_D(const MoveJob); return d->resultingUids; } void MoveJob::doStart() { Q_D(MoveJob); + d->set.optimize(); QByteArray parameters = d->set.toImapSequenceSet() + ' '; parameters += '\"' + KIMAP::encodeImapFolderName(d->mailBox.toUtf8()) + '\"'; QByteArray command = "MOVE"; if (d->uidBased) { command = "UID " + command; } d->tags << d->sessionInternal()->sendCommand(command, parameters); } void MoveJob::handleResponse(const Response &response) { Q_D(MoveJob); for (auto it = response.responseCode.cbegin(), end = response.responseCode.cend(); it != end; ++it) { if (it->toString() == "COPYUID") { it = it + 3; if (it < end) { d->resultingUids = ImapSet::fromImapSequenceSet(it->toString()); } break; } } handleErrorReplies(response); } diff --git a/src/searchjob.cpp b/src/searchjob.cpp index e41e6d6..10999ec 100644 --- a/src/searchjob.cpp +++ b/src/searchjob.cpp @@ -1,585 +1,587 @@ /* 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(); } } else { for (const Term &t : subterms) { d->command += t.serialize() + ' '; } if (!subterms.isEmpty()) { d->command.chop(1); } } 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; } - d->command += " " + set.toImapSequenceSet(); + 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; } diff --git a/src/storejob.cpp b/src/storejob.cpp index 7a2432f..e5a5572 100644 --- a/src/storejob.cpp +++ b/src/storejob.cpp @@ -1,232 +1,233 @@ /* Copyright (c) 2009 Kevin Ottens 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 "storejob.h" #include "kimap_debug.h" #include #include "job_p.h" #include "response_p.h" #include "session_p.h" namespace KIMAP { class StoreJobPrivate : public JobPrivate { public: StoreJobPrivate(Session *session, const QString &name) : JobPrivate(session, name), uidBased(false) { } ~StoreJobPrivate() { } QByteArray addFlags(const QByteArray ¶m, const MessageFlags &flags) { QByteArray parameters; switch (mode) { case StoreJob::SetFlags: parameters += param; break; case StoreJob::AppendFlags: parameters += "+" + param; break; case StoreJob::RemoveFlags: parameters += "-" + param; break; } parameters += " ("; for (const QByteArray &flag : flags) { parameters += flag + ' '; } if (!flags.isEmpty()) { parameters.chop(1); } parameters += ')'; return parameters; } ImapSet set; bool uidBased; StoreJob::StoreMode mode; MessageFlags flags; MessageFlags gmLabels; QMap resultingFlags; }; } using namespace KIMAP; StoreJob::StoreJob(Session *session) : Job(*new StoreJobPrivate(session, i18n("Store"))) { Q_D(StoreJob); d->uidBased = false; d->mode = SetFlags; } StoreJob::~StoreJob() { } void StoreJob::setSequenceSet(const ImapSet &set) { Q_D(StoreJob); d->set = set; } ImapSet StoreJob::sequenceSet() const { Q_D(const StoreJob); return d->set; } void StoreJob::setUidBased(bool uidBased) { Q_D(StoreJob); d->uidBased = uidBased; } bool StoreJob::isUidBased() const { Q_D(const StoreJob); return d->uidBased; } void StoreJob::setFlags(const MessageFlags &flags) { Q_D(StoreJob); d->flags = flags; } MessageFlags StoreJob::flags() const { Q_D(const StoreJob); return d->flags; } void StoreJob::setGMLabels(const MessageFlags &gmLabels) { Q_D(StoreJob); d->gmLabels = gmLabels; } MessageFlags StoreJob::gmLabels() const { Q_D(const StoreJob); return d->gmLabels; } void StoreJob::setMode(StoreMode mode) { Q_D(StoreJob); d->mode = mode; } StoreJob::StoreMode StoreJob::mode() const { Q_D(const StoreJob); return d->mode; } QMap StoreJob::resultingFlags() const { Q_D(const StoreJob); return d->resultingFlags; } void StoreJob::doStart() { Q_D(StoreJob); if (d->set.isEmpty()) { qCWarning(KIMAP_LOG) << "Empty uid set passed to store job"; setError(KJob::UserDefinedError); setErrorText(QStringLiteral("Empty uid set passed to store job")); emitResult(); return; } + d->set.optimize(); QByteArray parameters = d->set.toImapSequenceSet() + ' '; if (!d->flags.isEmpty()) { parameters += d->addFlags("FLAGS", d->flags); } if (!d->gmLabels.isEmpty()) { if (!d->flags.isEmpty()) { parameters += ' '; } parameters += d->addFlags("X-GM-LABELS", d->gmLabels); } qCDebug(KIMAP_LOG) << parameters; QByteArray command = "STORE"; if (d->uidBased) { command = "UID " + command; } d->tags << d->sessionInternal()->sendCommand(command, parameters); } void StoreJob::handleResponse(const Response &response) { Q_D(StoreJob); if (handleErrorReplies(response) == NotHandled) { if (response.content.size() == 4 && response.content[2].toString() == "FETCH" && response.content[3].type() == Response::Part::List) { int id = response.content[1].toString().toInt(); qint64 uid = 0; bool uidFound = false; QList resultingFlags; QList content = response.content[3].toList(); for (QList::ConstIterator it = content.constBegin(); it != content.constEnd(); ++it) { QByteArray str = *it; ++it; if (str == "FLAGS") { if ((*it).startsWith('(') && (*it).endsWith(')')) { QByteArray str = *it; str.chop(1); str.remove(0, 1); resultingFlags = str.split(' '); } else { resultingFlags << *it; } } else if (str == "UID") { uid = it->toLongLong(&uidFound); } } if (!d->uidBased) { d->resultingFlags[id] = resultingFlags; } else if (uidFound) { d->resultingFlags[uid] = resultingFlags; } else { qCWarning(KIMAP_LOG) << "We asked for UID but the server didn't give it back, resultingFlags not stored."; } } } }