diff --git a/resources/ews/test/statemonitor.h b/resources/ews/test/statemonitor.h index 6dfc17373..eea4de86b 100644 --- a/resources/ews/test/statemonitor.h +++ b/resources/ews/test/statemonitor.h @@ -1,130 +1,134 @@ /* Copyright (C) 2017 Krzysztof Nowicki 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 TEST_STATEMONITOR_H #define TEST_STATEMONITOR_H #include #include #include #include #include #include class StateMonitorBase : public QObject { Q_OBJECT public: explicit StateMonitorBase(QObject *parent) : QObject(parent) { } ~StateMonitorBase() override = default; Q_SIGNALS: void stateReached(); void errorOccurred(); }; template class CollectionStateMonitor : public StateMonitorBase { public: typedef std::function StateComparisonFunc; CollectionStateMonitor(QObject *parent, const QHash &stateHash, const QString &inboxId, const StateComparisonFunc &comparisonFunc, int recheckInterval = 0); ~CollectionStateMonitor() override = default; Akonadi::Monitor &monitor() { return mMonitor; } void forceRecheck(); private: void stateChanged(const Akonadi::Collection &col); Akonadi::Monitor mMonitor; QSet mPending; const QHash &mStateHash; StateComparisonFunc mComparisonFunc; const QString &mInboxId; QTimer mRecheckTimer; }; template CollectionStateMonitor::CollectionStateMonitor(QObject *parent, const QHash &stateHash, const QString &inboxId, const StateComparisonFunc &comparisonFunc, int recheckInterval) : StateMonitorBase(parent) , mMonitor(this) +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) , mPending(stateHash.keys().toSet()) +#else + , mPending(stateHash.keyBegin(), stateHash.keyEnd()) +#endif , mStateHash(stateHash) , mComparisonFunc(comparisonFunc) , mInboxId(inboxId) , mRecheckTimer(this) { connect(&mMonitor, &Akonadi::Monitor::collectionAdded, this, [this](const Akonadi::Collection &col, const Akonadi::Collection &) { stateChanged(col); }); connect(&mMonitor, QOverload::of(&Akonadi::Monitor::collectionChanged), this, [this](const Akonadi::Collection &col) { stateChanged(col); }); if (recheckInterval > 0) { mRecheckTimer.setInterval(recheckInterval); connect(&mRecheckTimer, &QTimer::timeout, this, &CollectionStateMonitor::forceRecheck); mRecheckTimer.start(); } } template void CollectionStateMonitor::stateChanged(const Akonadi::Collection &col) { auto remoteId = col.remoteId(); auto state = mStateHash.find(remoteId); if (state == mStateHash.end()) { qDebug() << "Cannot find state for collection" << remoteId; Q_EMIT errorOccurred(); } if (mComparisonFunc(col, *state)) { mPending.remove(remoteId); } else { mPending.insert(remoteId); } if (mPending.empty()) { Q_EMIT stateReached(); } } template void CollectionStateMonitor::forceRecheck() { auto fetchJob = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this); fetchJob->setFetchScope(mMonitor.collectionFetchScope()); if (fetchJob->exec()) { const auto collections = fetchJob->collections(); for (const auto &col : collections) { const auto remoteId = col.remoteId(); const auto state = mStateHash.find(remoteId); if (state != mStateHash.end()) { stateChanged(col); } } } } #endif diff --git a/resources/imap/messagehelper.cpp b/resources/imap/messagehelper.cpp index 2f47a9867..d3ed29a0f 100644 --- a/resources/imap/messagehelper.cpp +++ b/resources/imap/messagehelper.cpp @@ -1,65 +1,65 @@ /* Copyright (c) 2014 Christian Mollekopf 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 "messagehelper.h" #include #include "resourcetask.h" #include "imapresource_debug.h" MessageHelper::~MessageHelper() { } Akonadi::Item MessageHelper::createItemFromMessage(const KMime::Message::Ptr &message, const qint64 uid, const qint64 size, const QMap &attrs, const QList &flags, const KIMAP::FetchJob::FetchScope &scope, bool &ok) const { Q_UNUSED(attrs); Akonadi::Item i; if (scope.mode == KIMAP::FetchJob::FetchScope::Flags) { i.setRemoteId(QString::number(uid)); i.setMimeType(KMime::Message::mimeType()); - i.setFlags(Akonadi::Item::Flags::fromList(ResourceTask::toAkonadiFlags(flags))); + i.setFlags(ResourceTask::toAkonadiFlags(flags)); } else { if (!message) { qCWarning(IMAPRESOURCE_LOG) << "Got empty message: " << uid; ok = false; return Akonadi::Item(); } // Sometimes messages might not have a body at all if (message->body().isEmpty() && (scope.mode == KIMAP::FetchJob::FetchScope::Full || scope.mode == KIMAP::FetchJob::FetchScope::Content)) { // In that case put a space in as body so that it gets cached // otherwise we'll wrongly believe the body part is missing from the cache message->setBody(" "); } i.setRemoteId(QString::number(uid)); i.setMimeType(KMime::Message::mimeType()); i.setPayload(KMime::Message::Ptr(message)); i.setSize(size); Akonadi::MessageFlags::copyMessageFlags(*message, i); foreach (const QByteArray &flag, ResourceTask::toAkonadiFlags(flags)) { i.setFlag(flag); } } ok = true; return i; } diff --git a/resources/imap/resourcetask.cpp b/resources/imap/resourcetask.cpp index 59f744d18..37c04d2af 100644 --- a/resources/imap/resourcetask.cpp +++ b/resources/imap/resourcetask.cpp @@ -1,569 +1,569 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: 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 "resourcetask.h" #include #include #include "imapresource_debug.h" #include "imapresource_trace.h" #include "collectionflagsattribute.h" #include #include "imapflags.h" #include "sessionpool.h" #include "resourcestateinterface.h" ResourceTask::ResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent) : QObject(parent) , m_pool(nullptr) , m_sessionRequestId(0) , m_session(nullptr) , m_actionIfNoSession(action) , m_resource(resource) , mCancelled(false) { } ResourceTask::~ResourceTask() { if (m_pool) { if (m_sessionRequestId) { m_pool->cancelSessionRequest(m_sessionRequestId); } if (m_session) { m_pool->releaseSession(m_session); } } } void ResourceTask::start(SessionPool *pool) { qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); m_pool = pool; connect(m_pool, &SessionPool::sessionRequestDone, this, &ResourceTask::onSessionRequested); m_sessionRequestId = m_pool->requestSession(); if (m_sessionRequestId <= 0) { m_sessionRequestId = 0; abortTask(QString()); // In this case we were likely disconnected, try to get the resource online m_resource->scheduleConnectionAttempt(); } } void ResourceTask::abortTask(const QString &errorString) { if (!mCancelled) { mCancelled = true; switch (m_actionIfNoSession) { case CancelIfNoSession: qCDebug(IMAPRESOURCE_LOG) << "Cancelling this request."; m_resource->cancelTask(errorString.isEmpty() ? i18n("Unable to connect to the IMAP server.") : errorString); break; case DeferIfNoSession: qCDebug(IMAPRESOURCE_LOG) << "Defering this request."; m_resource->deferTask(); break; } } deleteLater(); } void ResourceTask::onSessionRequested(qint64 requestId, KIMAP::Session *session, int errorCode, const QString &errorString) { if (requestId != m_sessionRequestId) { // Not for us, ignore return; } disconnect(m_pool, &SessionPool::sessionRequestDone, this, &ResourceTask::onSessionRequested); m_sessionRequestId = 0; if (errorCode != SessionPool::NoError) { abortTask(errorString); return; } m_session = session; connect(m_pool, &SessionPool::connectionLost, this, &ResourceTask::onConnectionLost); connect(m_pool, &SessionPool::disconnectDone, this, &ResourceTask::onPoolDisconnect); qCDebug(IMAPRESOURCE_TRACE) << "starting: " << metaObject()->className(); doStart(m_session); } void ResourceTask::onConnectionLost(KIMAP::Session *session) { if (session == m_session) { // Our session becomes invalid, so get rid of // the pointer, we don't need to release it once the // task is done m_session = nullptr; qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); abortTask(i18n("Connection lost")); } } void ResourceTask::onPoolDisconnect() { // All the sessions in the pool we used changed, // so get rid of the pointer, we don't need to // release our session anymore m_pool = nullptr; qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); abortTask(i18n("Connection lost")); } QString ResourceTask::userName() const { return m_resource->userName(); } QString ResourceTask::resourceName() const { return m_resource->resourceName(); } QStringList ResourceTask::serverCapabilities() const { return m_resource->serverCapabilities(); } QList ResourceTask::serverNamespaces() const { return m_resource->serverNamespaces(); } bool ResourceTask::isAutomaticExpungeEnabled() const { return m_resource->isAutomaticExpungeEnabled(); } bool ResourceTask::isSubscriptionEnabled() const { return m_resource->isSubscriptionEnabled(); } bool ResourceTask::isDisconnectedModeEnabled() const { return m_resource->isDisconnectedModeEnabled(); } int ResourceTask::intervalCheckTime() const { return m_resource->intervalCheckTime(); } static Akonadi::Collection detachCollection(const Akonadi::Collection &collection) { //HACK: Attributes are accessed via a const function, and the implicitly shared private pointer thus doesn't detach. //We force a detach to avoid surprises. (RetrieveItemsTask used to write back the collection changes, even though the task was canceled) //Once this is fixed this function can go away. Akonadi::Collection col = collection; col.setId(col.id()); return col; } Akonadi::Collection ResourceTask::collection() const { return detachCollection(m_resource->collection()); } Akonadi::Item ResourceTask::item() const { return m_resource->item(); } Akonadi::Item::List ResourceTask::items() const { return m_resource->items(); } Akonadi::Collection ResourceTask::parentCollection() const { return detachCollection(m_resource->parentCollection()); } Akonadi::Collection ResourceTask::sourceCollection() const { return detachCollection(m_resource->sourceCollection()); } Akonadi::Collection ResourceTask::targetCollection() const { return detachCollection(m_resource->targetCollection()); } QSet ResourceTask::parts() const { return m_resource->parts(); } QSet< QByteArray > ResourceTask::addedFlags() const { return m_resource->addedFlags(); } QSet< QByteArray > ResourceTask::removedFlags() const { return m_resource->removedFlags(); } QString ResourceTask::rootRemoteId() const { return m_resource->rootRemoteId(); } QString ResourceTask::mailBoxForCollection(const Akonadi::Collection &collection) const { return m_resource->mailBoxForCollection(collection); } void ResourceTask::setIdleCollection(const Akonadi::Collection &collection) { if (!mCancelled) { m_resource->setIdleCollection(collection); } } void ResourceTask::applyCollectionChanges(const Akonadi::Collection &collection) { if (!mCancelled) { m_resource->applyCollectionChanges(collection); } } void ResourceTask::itemRetrieved(const Akonadi::Item &item) { if (!mCancelled) { m_resource->itemRetrieved(item); emitPercent(100); } deleteLater(); } void ResourceTask::itemsRetrieved(const Akonadi::Item::List &items) { if (!mCancelled) { m_resource->itemsRetrieved(items); } } void ResourceTask::itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed) { if (!mCancelled) { m_resource->itemsRetrievedIncremental(changed, removed); } } void ResourceTask::itemsRetrievalDone() { if (!mCancelled) { m_resource->itemsRetrievalDone(); } deleteLater(); } void ResourceTask::setTotalItems(int totalItems) { if (!mCancelled) { m_resource->setTotalItems(totalItems); } } void ResourceTask::changeCommitted(const Akonadi::Item &item) { if (!mCancelled) { m_resource->itemChangeCommitted(item); } deleteLater(); } void ResourceTask::changesCommitted(const Akonadi::Item::List &items) { if (!mCancelled) { m_resource->itemsChangesCommitted(items); } deleteLater(); } void ResourceTask::searchFinished(const QVector &result, bool isRid) { if (!mCancelled) { m_resource->searchFinished(result, isRid); } deleteLater(); } void ResourceTask::collectionsRetrieved(const Akonadi::Collection::List &collections) { if (!mCancelled) { m_resource->collectionsRetrieved(collections); } deleteLater(); } void ResourceTask::collectionAttributesRetrieved(const Akonadi::Collection &col) { if (!mCancelled) { m_resource->collectionAttributesRetrieved(col); } deleteLater(); } void ResourceTask::changeCommitted(const Akonadi::Collection &collection) { if (!mCancelled) { m_resource->collectionChangeCommitted(collection); } deleteLater(); } void ResourceTask::changeCommitted(const Akonadi::Tag &tag) { if (!mCancelled) { m_resource->tagChangeCommitted(tag); } deleteLater(); } void ResourceTask::changeProcessed() { if (!mCancelled) { m_resource->changeProcessed(); } deleteLater(); } void ResourceTask::cancelTask(const QString &errorString) { qCDebug(IMAPRESOURCE_LOG) << "Cancel task: " << errorString; if (!mCancelled) { mCancelled = true; m_resource->cancelTask(errorString); } deleteLater(); } void ResourceTask::deferTask() { if (!mCancelled) { mCancelled = true; m_resource->deferTask(); } deleteLater(); } void ResourceTask::restartItemRetrieval(Akonadi::Collection::Id col) { if (!mCancelled) { m_resource->restartItemRetrieval(col); } deleteLater(); } void ResourceTask::taskDone() { m_resource->taskDone(); deleteLater(); } void ResourceTask::emitPercent(int percent) { m_resource->emitPercent(percent); } void ResourceTask::emitError(const QString &message) { m_resource->emitError(message); } void ResourceTask::emitWarning(const QString &message) { m_resource->emitWarning(message); } void ResourceTask::synchronizeCollectionTree() { m_resource->synchronizeCollectionTree(); } void ResourceTask::showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName) { m_resource->showInformationDialog(message, title, dontShowAgainName); } QList ResourceTask::fromAkonadiToSupportedImapFlags(const QList &flags, const Akonadi::Collection &collection) { QList imapFlags = fromAkonadiFlags(flags); const Akonadi::CollectionFlagsAttribute *flagAttr = collection.attribute(); // the server does not support arbitrary flags, so filter out those it can't handle if (flagAttr && !flagAttr->flags().isEmpty() && !flagAttr->flags().contains("\\*")) { for (QList< QByteArray >::iterator it = imapFlags.begin(); it != imapFlags.end();) { if (flagAttr->flags().contains(*it)) { ++it; } else { qCDebug(IMAPRESOURCE_LOG) << "Server does not support flag" << *it; it = imapFlags.erase(it); } } } return imapFlags; } QList ResourceTask::fromAkonadiFlags(const QList &flags) { QList newFlags; for (const QByteArray &oldFlag : flags) { if (oldFlag == Akonadi::MessageFlags::Seen) { newFlags.append(ImapFlags::Seen); } else if (oldFlag == Akonadi::MessageFlags::Deleted) { newFlags.append(ImapFlags::Deleted); } else if (oldFlag == Akonadi::MessageFlags::Answered || oldFlag == Akonadi::MessageFlags::Replied) { newFlags.append(ImapFlags::Answered); } else if (oldFlag == Akonadi::MessageFlags::Flagged) { newFlags.append(ImapFlags::Flagged); } else { newFlags.append(oldFlag); } } return newFlags; } -QList ResourceTask::toAkonadiFlags(const QList &flags) +QSet ResourceTask::toAkonadiFlags(const QList &flags) { - QList newFlags; + QSet newFlags; for (const QByteArray &oldFlag : flags) { if (oldFlag == ImapFlags::Seen) { - newFlags.append(Akonadi::MessageFlags::Seen); + newFlags.insert(Akonadi::MessageFlags::Seen); } else if (oldFlag == ImapFlags::Deleted) { - newFlags.append(Akonadi::MessageFlags::Deleted); + newFlags.insert(Akonadi::MessageFlags::Deleted); } else if (oldFlag == ImapFlags::Answered) { - newFlags.append(Akonadi::MessageFlags::Answered); + newFlags.insert(Akonadi::MessageFlags::Answered); } else if (oldFlag == ImapFlags::Flagged) { - newFlags.append(Akonadi::MessageFlags::Flagged); + newFlags.insert(Akonadi::MessageFlags::Flagged); } else if (oldFlag.isEmpty()) { // filter out empty flags, to avoid isNull/isEmpty confusions higher up continue; } else { - newFlags.append(oldFlag); + newFlags.insert(oldFlag); } } return newFlags; } void ResourceTask::kill() { qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); abortTask(i18n("killed")); } const QChar ResourceTask::separatorCharacter() const { const QChar separator = m_resource->separatorCharacter(); if (!separator.isNull()) { return separator; } else { //If we request the separator before first folder listing, then try to guess //the separator: //If we create a toplevel folder, assume the separator to be '/'. This is not perfect, but detecting the right //IMAP separator is not straightforward for toplevel folders, and fixes bug 292418 and maybe other, where //subfolders end up with remote id's starting with "i" (the first letter of imap:// ...) QString remoteId; // We don't always have parent collection set (for example for CollectionChangeTask), // in such cases however we can use current collection's remoteId to get the separator const Akonadi::Collection parent = parentCollection(); if (parent.isValid()) { remoteId = parent.remoteId(); } else { remoteId = collection().remoteId(); } return ((remoteId != rootRemoteId()) && !remoteId.isEmpty()) ? remoteId.at(0) : QLatin1Char('/'); } } void ResourceTask::setSeparatorCharacter(QChar separator) { m_resource->setSeparatorCharacter(separator); } bool ResourceTask::serverSupportsAnnotations() const { return serverCapabilities().contains(QLatin1String("METADATA")) || serverCapabilities().contains(QLatin1String("ANNOTATEMORE")); } bool ResourceTask::serverSupportsCondstore() const { // Don't enable CONDSTORE for GMail (X-GM-EXT-1 is a GMail-specific capability) // because it breaks changes synchronization when using labels. return serverCapabilities().contains(QLatin1String("CONDSTORE")) && !serverCapabilities().contains(QLatin1String("X-GM-EXT-1")); } int ResourceTask::batchSize() const { return m_resource->batchSize(); } ResourceStateInterface::Ptr ResourceTask::resourceState() { return m_resource; } KIMAP::Acl::Rights ResourceTask::myRights(const Akonadi::Collection &col) { const Akonadi::ImapAclAttribute *aclAttribute = col.attribute(); if (aclAttribute) { //HACK, only return myrights if they are available if (aclAttribute->myRights() != KIMAP::Acl::None) { return aclAttribute->myRights(); } else { //This should be removed after 4.14, and myrights should be always used. return aclAttribute->rights().value(userName().toUtf8()); } } return KIMAP::Acl::None; } void ResourceTask::setItemMergingMode(Akonadi::ItemSync::MergeMode mode) { m_resource->setItemMergingMode(mode); } diff --git a/resources/imap/resourcetask.h b/resources/imap/resourcetask.h index 7e20545d0..30e9df6f1 100644 --- a/resources/imap/resourcetask.h +++ b/resources/imap/resourcetask.h @@ -1,167 +1,167 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: 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. */ #ifndef RESOURCETASK_H #define RESOURCETASK_H #include #include #include #include #include #include "resourcestateinterface.h" namespace KIMAP { class Session; } class SessionPool; class ResourceTask : public QObject { Q_OBJECT public: enum ActionIfNoSession { CancelIfNoSession, DeferIfNoSession }; Q_ENUM(ActionIfNoSession) explicit ResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent = nullptr); ~ResourceTask() override; void start(SessionPool *pool); void kill(); static QList fromAkonadiToSupportedImapFlags(const QList &flags, const Akonadi::Collection &collection); - static QList toAkonadiFlags(const QList &flags); + static QSet toAkonadiFlags(const QList &flags); Q_SIGNALS: void status(int status, const QString &message = QString()); protected: virtual void doStart(KIMAP::Session *session) = 0; protected: QString userName() const; QString resourceName() const; QStringList serverCapabilities() const; QList serverNamespaces() const; bool isAutomaticExpungeEnabled() const; bool isSubscriptionEnabled() const; bool isDisconnectedModeEnabled() const; int intervalCheckTime() const; Akonadi::Collection collection() const; Akonadi::Item item() const; Akonadi::Item::List items() const; Akonadi::Collection parentCollection() const; Akonadi::Collection sourceCollection() const; Akonadi::Collection targetCollection() const; QSet parts() const; QSet addedFlags() const; QSet removedFlags() const; QString rootRemoteId() const; QString mailBoxForCollection(const Akonadi::Collection &collection) const; void setIdleCollection(const Akonadi::Collection &collection); void applyCollectionChanges(const Akonadi::Collection &collection); void itemRetrieved(const Akonadi::Item &item); void itemsRetrieved(const Akonadi::Item::List &items); void itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed); void itemsRetrievalDone(); void setTotalItems(int); void changeCommitted(const Akonadi::Item &item); void changesCommitted(const Akonadi::Item::List &items); void collectionsRetrieved(const Akonadi::Collection::List &collections); void collectionAttributesRetrieved(const Akonadi::Collection &col); void changeCommitted(const Akonadi::Collection &collection); void changeCommitted(const Akonadi::Tag &tag); void changeProcessed(); void searchFinished(const QVector &result, bool isRid = true); void cancelTask(const QString &errorString); void deferTask(); void restartItemRetrieval(Akonadi::Collection::Id col); void taskDone(); void emitPercent(int percent); void emitError(const QString &message); void emitWarning(const QString &message); void synchronizeCollectionTree(); void showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName); const QChar separatorCharacter() const; void setSeparatorCharacter(QChar separator); virtual bool serverSupportsAnnotations() const; virtual bool serverSupportsCondstore() const; int batchSize() const; void setItemMergingMode(Akonadi::ItemSync::MergeMode mode); ResourceStateInterface::Ptr resourceState(); KIMAP::Acl::Rights myRights(const Akonadi::Collection &); private: void abortTask(const QString &errorString); static QList fromAkonadiFlags(const QList &flags); private Q_SLOTS: void onSessionRequested(qint64 requestId, KIMAP::Session *session, int errorCode, const QString &errorString); void onConnectionLost(KIMAP::Session *session); void onPoolDisconnect(); private: SessionPool *m_pool = nullptr; qint64 m_sessionRequestId = 0; KIMAP::Session *m_session = nullptr; ActionIfNoSession m_actionIfNoSession; ResourceStateInterface::Ptr m_resource; bool mCancelled = false; }; #endif diff --git a/resources/pop3/pop3resource.cpp b/resources/pop3/pop3resource.cpp index 4042809df..0d0f030fa 100644 --- a/resources/pop3/pop3resource.cpp +++ b/resources/pop3/pop3resource.cpp @@ -1,1046 +1,1050 @@ /* Copyright 2009 Thomas McGuire 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 ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 "pop3resource.h" #include "settings.h" #include "jobs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pop3resource_debug.h" #include using namespace Akonadi; using namespace MailTransport; using namespace KWallet; POP3Resource::POP3Resource(const QString &id) : ResourceBase(id) , mState(Idle) , mIntervalTimer(new QTimer(this)) { new Settings(KSharedConfig::openConfig()); Akonadi::AttributeFactory::registerAttribute(); setNeedsNetwork(true); Settings::self()->setResourceId(identifier()); if (Settings::self()->name().isEmpty()) { if (name() == identifier()) { Settings::self()->setName(i18n("POP3 Account")); } else { Settings::self()->setName(name()); } } setName(Settings::self()->name()); resetState(); connect(this, &POP3Resource::abortRequested, this, &POP3Resource::slotAbortRequested); connect(mIntervalTimer, &QTimer::timeout, this, &POP3Resource::intervalCheckTriggered); connect(this, &POP3Resource::reloadConfiguration, this, &POP3Resource::configurationChanged); } POP3Resource::~POP3Resource() { Settings::self()->save(); delete mWallet; mWallet = nullptr; } void POP3Resource::configurationChanged() { updateIntervalTimer(); mPassword.clear(); } void POP3Resource::updateIntervalTimer() { if (Settings::self()->intervalCheckEnabled() && mState == Idle) { mIntervalTimer->start(Settings::self()->intervalCheckInterval() * 1000 * 60); } else { mIntervalTimer->stop(); } } void POP3Resource::intervalCheckTriggered() { Q_ASSERT(mState == Idle); if (isOnline()) { qCDebug(POP3RESOURCE_LOG) << "Starting interval mail check."; startMailCheck(); mIntervalCheckInProgress = true; } else { mIntervalTimer->start(); } } void POP3Resource::aboutToQuit() { if (mState != Idle) { cancelSync(i18n("Mail check aborted.")); } } void POP3Resource::slotAbortRequested() { if (mState != Idle) { cancelSync(i18n("Mail check was canceled manually."), false /* no error */); } } void POP3Resource::retrieveItems(const Akonadi::Collection &collection) { Q_UNUSED(collection); qCWarning(POP3RESOURCE_LOG) << "This should never be called, we don't have a collection!"; } bool POP3Resource::retrieveItem(const Akonadi::Item &item, const QSet &parts) { Q_UNUSED(item); Q_UNUSED(parts); qCWarning(POP3RESOURCE_LOG) << "This should never be called, we don't have any item!"; return false; } QString POP3Resource::buildLabelForPasswordDialog(const QString &detailedError) const { const QString queryText = i18n("Please enter the username and password for account '%1'.", agentName()) + QLatin1String("
") + detailedError; return queryText; } void POP3Resource::walletOpenedForLoading(bool success) { bool passwordLoaded = success; if (success) { if (mWallet && mWallet->isOpen() && mWallet->hasFolder(QStringLiteral("pop3"))) { mWallet->setFolder(QStringLiteral("pop3")); if (mWallet->hasEntry(identifier())) { mWallet->readPassword(identifier(), mPassword); } else { passwordLoaded = false; } } else { passwordLoaded = false; } } delete mWallet; mWallet = nullptr; if (!passwordLoaded) { const QString queryText = buildLabelForPasswordDialog( i18n("You are asked here because the password could not be loaded from the wallet.")); showPasswordDialog(queryText); } else { advanceState(Connect); } } void POP3Resource::walletOpenedForSaving(bool success) { if (success) { if (mWallet && mWallet->isOpen()) { if (!mWallet->hasFolder(QStringLiteral("pop3"))) { mWallet->createFolder(QStringLiteral("pop3")); } mWallet->setFolder(QStringLiteral("pop3")); mWallet->writePassword(identifier(), mPassword); } } else { qCWarning(POP3RESOURCE_LOG) << "Unable to write the password to the wallet."; } delete mWallet; mWallet = nullptr; finish(); } void POP3Resource::showPasswordDialog(const QString &queryText) { QPointer dlg = new KPasswordDialog( nullptr, KPasswordDialog::ShowUsernameLine); dlg->setModal(true); dlg->setUsername(Settings::self()->login()); dlg->setPassword(mPassword); dlg->setPrompt(queryText); dlg->setWindowTitle(name()); dlg->addCommentLine(i18n("Account:"), name()); bool gotIt = false; if (dlg->exec()) { mPassword = dlg->password(); Settings::self()->setLogin(dlg->username()); Settings::self()->save(); if (!dlg->password().isEmpty()) { mSavePassword = true; } mAskAgain = false; advanceState(Connect); gotIt = true; } delete dlg; if (!gotIt) { cancelSync(i18n("No username and password supplied.")); } } void POP3Resource::advanceState(State nextState) { mState = nextState; doStateStep(); } void POP3Resource::doStateStep() { switch (mState) { case Idle: Q_ASSERT(false); qCWarning(POP3RESOURCE_LOG) << "State machine should not be called in idle state!"; break; case FetchTargetCollection: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state FetchTargetCollection =========="; Q_EMIT status(Running, i18n("Preparing transmission from \"%1\".", name())); Collection targetCollection(Settings::self()->targetCollection()); if (targetCollection.isValid()) { CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base); fetchJob->start(); connect(fetchJob, &CollectionFetchJob::result, this, &POP3Resource::targetCollectionFetchJobFinished); } else { // No target collection set in the config? Try requesting a default inbox SpecialMailCollectionsRequestJob *requestJob = new SpecialMailCollectionsRequestJob(this); requestJob->requestDefaultCollection(SpecialMailCollections::Inbox); requestJob->start(); connect(requestJob, &SpecialMailCollectionsRequestJob::result, this, &POP3Resource::localFolderRequestJobFinished); } break; } case Precommand: qCDebug(POP3RESOURCE_LOG) << "================ Starting state Precommand ====================="; if (!Settings::self()->precommand().isEmpty()) { PrecommandJob *precommandJob = new PrecommandJob(Settings::self()->precommand(), this); connect(precommandJob, &PrecommandJob::result, this, &POP3Resource::precommandResult); precommandJob->start(); Q_EMIT status(Running, i18n("Executing precommand.")); } else { advanceState(RequestPassword); } break; case RequestPassword: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state RequestPassword ================"; // Don't show any wallet or password prompts when we are unit-testing if (!Settings::self()->unitTestPassword().isEmpty()) { mPassword = Settings::self()->unitTestPassword(); advanceState(Connect); break; } const bool passwordNeeded = Settings::self()->authenticationMethod() != MailTransport::Transport::EnumAuthenticationType::GSSAPI; const bool loadPasswordFromWallet = !mAskAgain && passwordNeeded && !Settings::self()->login().isEmpty() && mPassword.isEmpty(); if (loadPasswordFromWallet) { mWallet = Wallet::openWallet(Wallet::NetworkWallet(), winIdForDialogs(), Wallet::Asynchronous); } if (loadPasswordFromWallet && mWallet) { connect(mWallet, &KWallet::Wallet::walletOpened, this, &POP3Resource::walletOpenedForLoading); } else if (passwordNeeded && (mPassword.isEmpty() || mAskAgain)) { QString detail; if (mAskAgain) { detail = i18n("You are asked here because the previous login was not successful."); } else if (Settings::self()->login().isEmpty()) { detail = i18n("You are asked here because the username you supplied is empty."); } else if (!mWallet) { detail = i18n("You are asked here because the wallet password storage is disabled."); } showPasswordDialog(buildLabelForPasswordDialog(detail)); } else { // No password needed or using previous password, go on with Connect advanceState(Connect); } break; } case Connect: qCDebug(POP3RESOURCE_LOG) << "================ Starting state Connect ========================"; Q_ASSERT(!mPopSession); mPopSession = new POPSession(mPassword); connect(mPopSession, &POPSession::slaveError, this, &POP3Resource::slotSessionError); advanceState(Login); break; case Login: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state Login =========================="; LoginJob *loginJob = new LoginJob(mPopSession); connect(loginJob, &LoginJob::result, this, &POP3Resource::loginJobResult); loginJob->start(); break; } case List: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state List ==========================="; Q_EMIT status(Running, i18n("Fetching mail listing.")); ListJob *listJob = new ListJob(mPopSession); connect(listJob, &ListJob::result, this, &POP3Resource::listJobResult); listJob->start(); break; } case UIDList: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state UIDList ========================"; UIDListJob *uidListJob = new UIDListJob(mPopSession); connect(uidListJob, &UIDListJob::result, this, &POP3Resource::uidListJobResult); uidListJob->start(); break; } case Download: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state Download ======================="; // Determine which mails we want to download. Those are all mails which are // currently on ther server, minus the ones we have already downloaded (we // remember which UIDs we have downloaded in the settings) QList idsToDownload = mIdsToSizeMap.keys(); const QStringList alreadyDownloadedUIDs = Settings::self()->seenUidList(); foreach (const QString &uidOnServer, mIdsToUidsMap) { if (alreadyDownloadedUIDs.contains(uidOnServer)) { const int idOfUIDOnServer = mUidsToIdsMap.value(uidOnServer, -1); Q_ASSERT(idOfUIDOnServer != -1); idsToDownload.removeAll(idOfUIDOnServer); } } mIdsToDownload = idsToDownload; qCDebug(POP3RESOURCE_LOG) << "We are going to download" << mIdsToDownload.size() << "messages"; // For proper progress, the job needs to know the sizes of the messages, so // put them into a list here QList sizesOfMessagesToDownload; sizesOfMessagesToDownload.reserve(idsToDownload.count()); foreach (int id, idsToDownload) { sizesOfMessagesToDownload.append(mIdsToSizeMap.value(id)); } if (mIdsToDownload.empty()) { advanceState(CheckRemovingMessage); } else { FetchJob *fetchJob = new FetchJob(mPopSession); fetchJob->setFetchIds(idsToDownload, sizesOfMessagesToDownload); connect(fetchJob, &FetchJob::result, this, &POP3Resource::fetchJobResult); connect(fetchJob, &FetchJob::messageFinished, this, &POP3Resource::messageFinished); //TODO wait kf6. For the moment we can't convert to new connect api. connect(fetchJob, SIGNAL(processedAmount(KJob*,KJob::Unit,qulonglong)), this, SLOT(messageDownloadProgress(KJob*,KJob::Unit,qulonglong))); fetchJob->start(); } break; } case Save: qCDebug(POP3RESOURCE_LOG) << "================ Starting state Save ==========================="; qCDebug(POP3RESOURCE_LOG) << mPendingCreateJobs.size() << "item create jobs are pending"; if (!mPendingCreateJobs.isEmpty()) { Q_EMIT status(Running, i18n("Saving downloaded messages.")); } if (shouldAdvanceToQuitState()) { advanceState(Quit); } break; case Quit: { qCDebug(POP3RESOURCE_LOG) << "================ Starting state Quit ==========================="; QuitJob *quitJob = new QuitJob(mPopSession); connect(quitJob, &QuitJob::result, this, &POP3Resource::quitJobResult); quitJob->start(); break; } case SavePassword: qCDebug(POP3RESOURCE_LOG) << "================ Starting state SavePassword ==================="; if (mSavePassword) { qCDebug(POP3RESOURCE_LOG) << "Writing password back to the wallet."; Q_EMIT status(Running, i18n("Saving password to the wallet.")); mWallet = Wallet::openWallet(Wallet::NetworkWallet(), winIdForDialogs(), Wallet::Asynchronous); if (mWallet) { connect(mWallet, &KWallet::Wallet::walletOpened, this, &POP3Resource::walletOpenedForSaving); } else { finish(); } } else { finish(); } break; case CheckRemovingMessage: qCDebug(POP3RESOURCE_LOG) << "================ Starting state CheckRemovingMessage ==================="; checkRemovingMessageFromServer(); break; } } void POP3Resource::checkRemovingMessageFromServer() { const QList idToDeleteMessage = shouldDeleteId(-1); if (!idToDeleteMessage.isEmpty()) { mIdsWaitingToDelete << idToDeleteMessage; if (!mDeleteJob) { mDeleteJob = new DeleteJob(mPopSession); mDeleteJob->setDeleteIds(mIdsWaitingToDelete); mIdsWaitingToDelete.clear(); connect(mDeleteJob, &DeleteJob::result, this, &POP3Resource::deleteJobResult); mDeleteJob->start(); } } else { advanceState(Save); } } void POP3Resource::localFolderRequestJobFinished(KJob *job) { if (job->error()) { cancelSync(i18n("Error while trying to get the local inbox folder, " "aborting mail check.") + QLatin1Char('\n') + job->errorString()); return; } if (mTestLocalInbox) { KMessageBox::information(nullptr, i18n("The folder you deleted was associated with the account " "%1 which delivered mail into it. The folder the account " "delivers new mail into was reset to the main Inbox folder.", name())); } mTestLocalInbox = false; mTargetCollection = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::Inbox); Q_ASSERT(mTargetCollection.isValid()); advanceState(Precommand); } void POP3Resource::targetCollectionFetchJobFinished(KJob *job) { if (job->error()) { if (!mTestLocalInbox) { mTestLocalInbox = true; Settings::self()->setTargetCollection(-1); advanceState(FetchTargetCollection); return; } else { cancelSync(i18n("Error while trying to get the folder for incoming mail, " "aborting mail check.") + QLatin1Char('\n') + job->errorString()); mTestLocalInbox = false; return; } } mTestLocalInbox = false; Akonadi::CollectionFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); Q_ASSERT(fetchJob->collections().size() <= 1); if (fetchJob->collections().isEmpty()) { cancelSync(i18n("Could not find folder for incoming mail, aborting mail check.")); return; } else { mTargetCollection = fetchJob->collections().at(0); advanceState(Precommand); } } void POP3Resource::precommandResult(KJob *job) { if (job->error()) { cancelSync(i18n("Error while executing precommand.") +QLatin1Char('\n') + job->errorString()); return; } else { advanceState(RequestPassword); } } void POP3Resource::loginJobResult(KJob *job) { if (job->error()) { qCDebug(POP3RESOURCE_LOG) << job->error() << job->errorText(); if (job->error() == KIO::ERR_CANNOT_LOGIN) { mAskAgain = true; } cancelSync(i18n("Unable to login to the server %1.", Settings::self()->host()) +QLatin1Char('\n') + job->errorString()); } else { advanceState(List); } } void POP3Resource::listJobResult(KJob *job) { if (job->error()) { cancelSync(i18n("Error while getting the list of messages on the server.") +QLatin1Char('\n') + job->errorString()); } else { ListJob *listJob = qobject_cast(job); Q_ASSERT(listJob); mIdsToSizeMap = listJob->idList(); mIdsToSaveValid = false; qCDebug(POP3RESOURCE_LOG) << "IdsToSizeMap size" << mIdsToSizeMap.size(); advanceState(UIDList); } } void POP3Resource::uidListJobResult(KJob *job) { if (job->error()) { cancelSync(i18n("Error while getting list of unique mail identifiers from the server.") +QLatin1Char('\n') + job->errorString()); } else { UIDListJob *listJob = qobject_cast(job); Q_ASSERT(listJob); mIdsToUidsMap = listJob->uidList(); mUidsToIdsMap = listJob->idList(); qCDebug(POP3RESOURCE_LOG) << "IdsToUidsMap size" << mIdsToUidsMap.size(); qCDebug(POP3RESOURCE_LOG) << "UidsToIdsMap size" << mUidsToIdsMap.size(); mUidListValid = !mIdsToUidsMap.isEmpty() || mIdsToSizeMap.isEmpty(); if (Settings::self()->leaveOnServer() && !mUidListValid) { // FIXME: this needs a proper parent window KMessageBox::sorry(nullptr, i18n("Your POP3 server (Account: %1) does not support " "the UIDL command: this command is required to determine, in a reliable way, " "which of the mails on the server KMail has already seen before;\n" "the feature to leave the mails on the server will therefore not " "work properly.", name())); } advanceState(Download); } } void POP3Resource::fetchJobResult(KJob *job) { if (job->error()) { cancelSync(i18n("Error while fetching mails from the server.") +QLatin1Char('\n') + job->errorString()); return; } else { qCDebug(POP3RESOURCE_LOG) << "Downloaded" << mDownloadedIDs.size() << "mails"; if (!mIdsToDownload.isEmpty()) { qCWarning(POP3RESOURCE_LOG) << "We did not download all messages, there are still some remaining " "IDs, even though we requested their download:" << mIdsToDownload; } advanceState(Save); } } void POP3Resource::messageFinished(int messageId, KMime::Message::Ptr message) { if (mState != Download) { // This can happen if the slave does not get notified in time about the fact // that the job was killed return; } //qCDebug(POP3RESOURCE_LOG) << "Got message" << messageId // << "with subject" << message->subject()->asUnicodeString(); Akonadi::Item item; item.setMimeType(QStringLiteral("message/rfc822")); item.setPayload(message); Akonadi::Pop3ResourceAttribute *attr = item.attribute(Akonadi::Item::AddIfMissing); attr->setPop3AccountName(identifier()); Akonadi::MessageFlags::copyMessageFlags(*message, item); ItemCreateJob *itemCreateJob = new ItemCreateJob(item, mTargetCollection); mPendingCreateJobs.insert(itemCreateJob, messageId); connect(itemCreateJob, &ItemCreateJob::result, this, &POP3Resource::itemCreateJobResult); mDownloadedIDs.append(messageId); mIdsToDownload.removeAll(messageId); } void POP3Resource::messageDownloadProgress(KJob *job, KJob::Unit unit, qulonglong totalBytes) { Q_UNUSED(totalBytes); Q_UNUSED(unit); Q_ASSERT(unit == KJob::Bytes); QString statusMessage; const int totalMessages = mIdsToDownload.size() + mDownloadedIDs.size(); int bytesRemainingOnServer = 0; foreach (const QString &alreadyDownloadedUID, Settings::self()->seenUidList()) { const int alreadyDownloadedID = mUidsToIdsMap.value(alreadyDownloadedUID, -1); if (alreadyDownloadedID != -1) { bytesRemainingOnServer += mIdsToSizeMap.value(alreadyDownloadedID); } } if (Settings::self()->leaveOnServer() && bytesRemainingOnServer > 0) { statusMessage = i18n("Fetching message %1 of %2 (%3 of %4 KB) for %5 " "(%6 KB remain on the server).", mDownloadedIDs.size() + 1, totalMessages, job->processedAmount(KJob::Bytes) / 1024, job->totalAmount(KJob::Bytes) / 1024, name(), bytesRemainingOnServer / 1024); } else { statusMessage = i18n("Fetching message %1 of %2 (%3 of %4 KB) for %5", mDownloadedIDs.size() + 1, totalMessages, job->processedAmount(KJob::Bytes) / 1024, job->totalAmount(KJob::Bytes) / 1024, name()); } Q_EMIT status(Running, statusMessage); Q_EMIT percent(job->percent()); } void POP3Resource::itemCreateJobResult(KJob *job) { if (mState != Download && mState != Save) { // This can happen if the slave does not get notified in time about the fact // that the job was killed return; } ItemCreateJob *createJob = qobject_cast(job); Q_ASSERT(createJob); if (job->error()) { cancelSync(i18n("Unable to store downloaded mails.") +QLatin1Char('\n') + job->errorString()); return; } const int idOfMessageJustCreated = mPendingCreateJobs.value(createJob, -1); Q_ASSERT(idOfMessageJustCreated != -1); mPendingCreateJobs.remove(createJob); mIDsStored.append(idOfMessageJustCreated); //qCDebug(POP3RESOURCE_LOG) << "Just stored message with ID" << idOfMessageJustCreated // << "on the Akonadi server"; const QList idToDeleteMessage = shouldDeleteId(idOfMessageJustCreated); if (!idToDeleteMessage.isEmpty()) { mIdsWaitingToDelete << idToDeleteMessage; if (!mDeleteJob) { mDeleteJob = new DeleteJob(mPopSession); mDeleteJob->setDeleteIds(mIdsWaitingToDelete); mIdsWaitingToDelete.clear(); connect(mDeleteJob, &DeleteJob::result, this, &POP3Resource::deleteJobResult); mDeleteJob->start(); } } // Have all create jobs finished? Go to the next state, then if (shouldAdvanceToQuitState()) { advanceState(Quit); } } int POP3Resource::idToTime(int id) const { const QString uid = mIdsToUidsMap.value(id); if (!uid.isEmpty()) { const QList seenUIDs = Settings::self()->seenUidList(); const QList timeOfSeenUids = Settings::self()->seenUidTimeList(); Q_ASSERT(seenUIDs.size() == timeOfSeenUids.size()); const int index = seenUIDs.indexOf(uid); if (index != -1 && (index < timeOfSeenUids.size())) { return timeOfSeenUids.at(index); } } // If we don't find any mail, either we have no UID, or it is not in the seen UID // list. In that case, we assume that the mail is new, i.e. from now return time(nullptr); } int POP3Resource::idOfOldestMessage(const QSet &idList) const { int timeOfOldestMessage = time(nullptr) + 999; int idOfOldestMessage = -1; foreach (int id, idList) { const int idTime = idToTime(id); if (idTime < timeOfOldestMessage) { timeOfOldestMessage = idTime; idOfOldestMessage = id; } } Q_ASSERT(idList.isEmpty() || idOfOldestMessage != -1); return idOfOldestMessage; } QList POP3Resource::shouldDeleteId(int downloadedId) const { QList idsToDeleteFromServer; // By default, we delete all messages. But if we have "leave on server" // rules, we can save some messages. if (Settings::self()->leaveOnServer()) { idsToDeleteFromServer = mIdsToSizeMap.keys(); if (!mIdsToSaveValid) { mIdsToSaveValid = true; mIdsToSave.clear(); +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) const QSet idsOnServer = QSet::fromList(idsToDeleteFromServer); +#else + const QSet idsOnServer(idsToDeleteFromServer.constBegin(), idsToDeleteFromServer.constEnd()); +#endif // If the time-limited leave rule is checked, add the newer messages to // the list of messages to keep if (Settings::self()->leaveOnServerDays() > 0) { const int secondsPerDay = 86400; time_t timeLimit = time(nullptr) - (secondsPerDay * Settings::self()->leaveOnServerDays()); foreach (int idToDelete, idsOnServer) { const int msgTime = idToTime(idToDelete); if (msgTime >= timeLimit) { mIdsToSave << idToDelete; } else { qCDebug(POP3RESOURCE_LOG) << "Message" << idToDelete << "is too old and will be deleted."; } } } // Otherwise, add all messages to the list of messages to keep - this may // be reduced in the following number-limited leave rule and size-limited // leave rule checks else { mIdsToSave = idsOnServer; } // // Delete more old messages if there are more than mLeaveOnServerCount // if (Settings::self()->leaveOnServerCount() > 0) { const int numToDelete = mIdsToSave.count() - Settings::self()->leaveOnServerCount(); if (numToDelete > 0 && numToDelete < mIdsToSave.count()) { // Get rid of the first numToDelete messages for (int i = 0; i < numToDelete; i++) { mIdsToSave.remove(idOfOldestMessage(mIdsToSave)); } } else if (numToDelete >= mIdsToSave.count()) { mIdsToSave.clear(); } } // // Delete more old messages until we're under mLeaveOnServerSize MBs // if (Settings::self()->leaveOnServerSize() > 0) { const qint64 limitInBytes = Settings::self()->leaveOnServerSize() * (1024 * 1024); qint64 sizeOnServerAfterDeletion = 0; for (int id : qAsConst(mIdsToSave)) { sizeOnServerAfterDeletion += mIdsToSizeMap.value(id); } while (sizeOnServerAfterDeletion > limitInBytes) { int oldestId = idOfOldestMessage(mIdsToSave); mIdsToSave.remove(oldestId); sizeOnServerAfterDeletion -= mIdsToSizeMap.value(oldestId); } } } // Now save the messages from deletion // foreach (int idToSave, mIdsToSave) { idsToDeleteFromServer.removeAll(idToSave); } if (downloadedId != -1 && !mIdsToSave.contains(downloadedId)) { idsToDeleteFromServer << downloadedId; } } else { if (downloadedId != -1) { idsToDeleteFromServer << downloadedId; } else { idsToDeleteFromServer << mIdsToSizeMap.keys(); } } return idsToDeleteFromServer; } void POP3Resource::deleteJobResult(KJob *job) { if (job->error()) { cancelSync(i18n("Failed to delete the messages from the server.") +QLatin1Char('\n') + job->errorString()); return; } DeleteJob *finishedDeleteJob = qobject_cast(job); Q_ASSERT(finishedDeleteJob); Q_ASSERT(finishedDeleteJob == mDeleteJob); mDeletedIDs = finishedDeleteJob->deletedIDs(); // Remove all deleted messages from the list of already downloaded messages, // as it is no longer necessary to store them (they just waste space) QList seenUIDs = Settings::self()->seenUidList(); QList timeOfSeenUids = Settings::self()->seenUidTimeList(); Q_ASSERT(seenUIDs.size() == timeOfSeenUids.size()); foreach (int deletedId, mDeletedIDs) { const QString deletedUID = mIdsToUidsMap.value(deletedId); if (!deletedUID.isEmpty()) { int index = seenUIDs.indexOf(deletedUID); if (index != -1) { // TEST qCDebug(POP3RESOURCE_LOG) << "Removing UID" << deletedUID << "from the seen UID list, as it was deleted."; seenUIDs.removeAt(index); timeOfSeenUids.removeAt(index); } } mIdsToUidsMap.remove(deletedId); mIdsToSizeMap.remove(deletedId); } Settings::self()->setSeenUidList(seenUIDs); Settings::self()->setSeenUidTimeList(timeOfSeenUids); Settings::self()->save(); mDeleteJob = nullptr; if (!mIdsWaitingToDelete.isEmpty()) { mDeleteJob = new DeleteJob(mPopSession); mDeleteJob->setDeleteIds(mIdsWaitingToDelete); mIdsWaitingToDelete.clear(); connect(mDeleteJob, &DeleteJob::result, this, &POP3Resource::deleteJobResult); mDeleteJob->start(); } if (shouldAdvanceToQuitState()) { advanceState(Quit); } else if (mDeleteJob == nullptr) { advanceState(Save); } } void POP3Resource::finish() { qCDebug(POP3RESOURCE_LOG) << "================= Mail check finished. ============================="; saveSeenUIDList(); if (!mIntervalCheckInProgress) { collectionsRetrieved(Akonadi::Collection::List()); } if (mDownloadedIDs.isEmpty()) { Q_EMIT status(Idle, i18n("Finished mail check, no message downloaded.")); } else { Q_EMIT status(Idle, i18np("Finished mail check, 1 message downloaded.", "Finished mail check, %1 messages downloaded.", mDownloadedIDs.size())); } resetState(); } bool POP3Resource::shouldAdvanceToQuitState() const { return mState == Save && mPendingCreateJobs.isEmpty() && mIdsWaitingToDelete.isEmpty() && !mDeleteJob; } void POP3Resource::quitJobResult(KJob *job) { if (job->error()) { cancelSync(i18n("Unable to complete the mail fetch.") +QLatin1Char('\n') + job->errorString()); return; } advanceState(SavePassword); } void POP3Resource::slotSessionError(int errorCode, const QString &errorMessage) { qCWarning(POP3RESOURCE_LOG) << "Error in our session, unrelated to a currently running job!"; cancelSync(KIO::buildErrorString(errorCode, errorMessage)); } void POP3Resource::saveSeenUIDList() { QList seenUIDs = Settings::self()->seenUidList(); QList timeOfSeenUIDs = Settings::self()->seenUidTimeList(); // // Find the messages that we have successfully stored, but did not actually get // deleted. // Those messages, we have to remember, so we don't download them again. // QList idsOfMessagesDownloadedButNotDeleted = mIDsStored; foreach (int deletedId, mDeletedIDs) { idsOfMessagesDownloadedButNotDeleted.removeAll(deletedId); } QList uidsOfMessagesDownloadedButNotDeleted; foreach (int id, idsOfMessagesDownloadedButNotDeleted) { const QString uid = mIdsToUidsMap.value(id); if (!uid.isEmpty()) { uidsOfMessagesDownloadedButNotDeleted.append(uid); } } Q_ASSERT(seenUIDs.size() == timeOfSeenUIDs.size()); foreach (const QString &uid, uidsOfMessagesDownloadedButNotDeleted) { if (!seenUIDs.contains(uid)) { seenUIDs.append(uid); timeOfSeenUIDs.append(time(nullptr)); } } // // If we have a valid UID list from the server, delete those UIDs that are in // the seenUidList but are not on the server. // This can happen if someone else than this resource deleted the mails from the // server which we kept here. // if (mUidListValid) { QList::iterator uidIt = seenUIDs.begin(); QList::iterator timeIt = timeOfSeenUIDs.begin(); while (uidIt != seenUIDs.end()) { const QString curSeenUID = *uidIt; if (!mUidsToIdsMap.contains(curSeenUID)) { // Ok, we have a UID in the seen UID list that is not anymore on the server. // Therefore remove it from the seen UID list, it is not needed there anymore, // it just wastes space. uidIt = seenUIDs.erase(uidIt); timeIt = timeOfSeenUIDs.erase(timeIt); } else { ++uidIt; ++timeIt; } } } else { qCWarning(POP3RESOURCE_LOG) << "UID list from server is not valid."; } // // Now save it in the settings // qCDebug(POP3RESOURCE_LOG) << "The seen UID list has" << seenUIDs.size() << "entries"; Settings::self()->setSeenUidList(seenUIDs); Settings::self()->setSeenUidTimeList(timeOfSeenUIDs); Settings::self()->save(); } void POP3Resource::cancelSync(const QString &errorMessage, bool error) { if (error) { cancelTask(errorMessage); qCWarning(POP3RESOURCE_LOG) << "============== ERROR DURING POP3 SYNC =========================="; qCWarning(POP3RESOURCE_LOG) << errorMessage; KNotification::event(QStringLiteral("mail-check-error"), name(), errorMessage.toHtmlEscaped(), QString(), nullptr, KNotification::CloseOnTimeout, QStringLiteral("akonadi_pop3_resource")); } else { qCDebug(POP3RESOURCE_LOG) << "Canceled the sync, but no error."; cancelTask(); } saveSeenUIDList(); resetState(); } void POP3Resource::resetState() { mState = Idle; mTargetCollection = Collection(-1); mIdsToSizeMap.clear(); mIdsToUidsMap.clear(); mUidsToIdsMap.clear(); mDownloadedIDs.clear(); mIdsToDownload.clear(); mPendingCreateJobs.clear(); mIDsStored.clear(); mDeletedIDs.clear(); mIdsWaitingToDelete.clear(); if (mDeleteJob) { mDeleteJob->deleteLater(); mDeleteJob = nullptr; } mUidListValid = false; mIntervalCheckInProgress = false; mSavePassword = false; updateIntervalTimer(); delete mWallet; mWallet = nullptr; if (mPopSession) { // Closing the POP session means the KIO slave will get disconnected, which // automatically issues the QUIT command. // Delete the POP session later, otherwise the scheduler makes us crash mPopSession->abortCurrentJob(); mPopSession->deleteLater(); mPopSession = nullptr; } } void POP3Resource::startMailCheck() { resetState(); mIntervalTimer->stop(); Q_EMIT percent(0); // Otherwise the value from the last sync is taken advanceState(FetchTargetCollection); } void POP3Resource::retrieveCollections() { if (mState == Idle) { startMailCheck(); } else { cancelSync( i18n("Mail check already in progress, unable to start a second check.")); } } void POP3Resource::clearCachedPassword() { mPassword.clear(); } void POP3Resource::cleanup() { if (mWallet && mWallet->isOpen() && mWallet->hasFolder(QStringLiteral("pop3"))) { mWallet->setFolder(QStringLiteral("pop3")); if (mWallet->hasEntry(identifier())) { mWallet->removeEntry(identifier()); } } ResourceBase::cleanup(); } void POP3Resource::doSetOnline(bool online) { ResourceBase::doSetOnline(online); if (online) { Q_EMIT status(Idle, i18n("Ready")); } else { if (mState != Idle) { cancelSync(i18n("Mail check aborted after going offline."), false /* no error */); } Q_EMIT status(Idle, i18n("Offline")); delete mWallet; mWallet = nullptr; clearCachedPassword(); } } AKONADI_RESOURCE_MAIN(POP3Resource)