diff --git a/CMakeLists.txt b/CMakeLists.txt index 977eaad6b..167cd7dbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,166 +1,166 @@ cmake_minimum_required(VERSION 3.5) set(PIM_VERSION "5.14.40") set(KDEPIM_RUNTIME_VERSION_NUMBER ${PIM_VERSION}) project(kdepim-runtime VERSION ${KDEPIM_RUNTIME_VERSION_NUMBER}) if (POLICY CMP0053) cmake_policy(SET CMP0053 NEW) endif() ############### KDEPIM-Runtime version ################ # KDEPIM_RUNTIME_VERSION # Version scheme: "x.y.z build". # # x is the version number. # y is the major release number. # z is the minor release number. # # "x.y.z" follow the kdelibs version kdepim is released with. # # If "z" is 0, it the version is "x.y" # # KDEPIM_DEV_VERSION # is empty for final versions. For development versions "build" is # something like "pre", "", "alpha", "beta1", "beta2", "rc1", "rc2". # # Examples in chronological order: # # 3.0 # 3.0.1 # 3.1 # 3.1 beta1 # 3.1 beta2 # 3.1 rc1 # 3.1 # 3.1.1 # 3.2 pre # 3.2 set(KDEPIM_DEV_VERSION alpha) set(RELEASE_SERVICE_VERSION "20.07.40") # add an extra space if(DEFINED KDEPIM_DEV_VERSION) set(KDEPIM_DEV_VERSION " ${KDEPIM_DEV_VERSION}") endif() set(KDEPIM_RUNTIME_VERSION "${KDEPIM_RUNTIME_VERSION_NUMBER}${KDEPIM_DEV_VERSION} (${RELEASE_SERVICE_VERSION})") configure_file(kdepim-runtime-version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/kdepim-runtime-version.h @ONLY) set(KF5_MIN_VERSION "5.68.0") find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${kdepim-runtime_SOURCE_DIR}/cmake/ ${ECM_MODULE_PATH}) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMQtDeclareLoggingCategory) include(GenerateExportHeader) set(QT_REQUIRED_VERSION "5.12.0") set(KDEPIMRUNTIME_LIB_VERSION "${KDEPIM_RUNTIME_VERSION_NUMBER}") set(KDEPIMRUNTIME_LIB_SOVERSION "5") set(AKONADI_VERSION "5.14.40") set(IDENTITYMANAGEMENT_LIB_VERSION "5.14.40") set(KMAILTRANSPORT_LIB_VERSION "5.14.40") set(CALENDARUTILS_LIB_VERSION "5.14.40") set(KDAV_LIB_VERSION "5.14.41") set(KIMAP_LIB_VERSION "5.14.40") set(KMBOX_LIB_VERSION "5.14.40") set(AKONADICALENDAR_LIB_VERSION "5.14.40") set(KONTACTINTERFACE_LIB_VERSION "5.14.40") set(AKONADIKALARM_LIB_VERSION "5.14.40") set(KMIME_LIB_VERSION "5.14.40") set(XMLRPCCLIENT_LIB_VERSION "5.14.40") -set(AKONADIMIME_LIB_VERSION "5.14.40") +set(AKONADIMIME_LIB_VERSION "5.14.43") set(AKONADICONTACT_LIB_VERSION "5.14.40") set(AKONADINOTE_LIB_VERSION "5.14.40") set(PIMCOMMON_LIB_VERSION "5.14.40") set(KGAPI_LIB_VERSION "5.14.40") set( SharedMimeInfo_MINIMUM_VERSION "1.3" ) find_package(SharedMimeInfo ${SharedMimeInfo_MINIMUM_VERSION} REQUIRED) find_package(Sasl2) set_package_properties(Sasl2 PROPERTIES TYPE REQUIRED) find_package(Qca-qt5 2.3.0 CONFIG REQUIRED) option(KDEPIM_RUN_AKONADI_TEST "Enable autotest based on Akonadi." TRUE) # QT5 package find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED TextToSpeech Network Widgets Test XmlPatterns DBus WebEngineWidgets NetworkAuth) # KF5 package find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ConfigWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5NotifyConfig ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ItemModels ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Codecs ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WindowSystem ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5TextWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) # for KPluralHandlingSpinBox find_package(KF5Notifications ${KF5_MIN_VERSION} CONFIG REQUIRED) # pop3, ews find_package(KF5DocTools ${KF5_MIN_VERSION} CONFIG REQUIRED) # pop3 find_package(KF5Holidays ${KF5_MIN_VERSION} CONFIG REQUIRED) # KdepimLibs package find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED) find_package(KF5DAV ${KDAV_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiMime ${AKONADIMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADICONTACT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Contacts ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5AlarmCalendar ${AKONADIKALARM_LIB_VERSION} CONFIG) find_package(KF5CalendarCore ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5CalendarUtils ${CALENDARUTILS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Mbox ${KMBOX_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IMAP ${KIMAP_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiNotes ${AKONADINOTE_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiCalendar ${AKONADICALENDAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimCommon ${PIMCOMMON_LIB_VERSION} CONFIG REQUIRED) find_package(KPimGAPI ${KGAPI_LIB_VERSION} CONFIG REQUIRED) option(KDEPIM_RUN_ISOLATED_TESTS "Run the isolated tests." FALSE) option(NO_REGENERATE_MIME "Don't regenerate mime file (developer-only option)" FALSE ) if (EXISTS "${CMAKE_SOURCE_DIR}/.git") #add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050e00) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x600000) endif() find_package(Xsltproc) set_package_properties(Xsltproc PROPERTIES DESCRIPTION "XSLT processor from libxslt" TYPE REQUIRED PURPOSE "Required to generate D-Bus interfaces for all Akonadi resources.") add_subdirectory(resources) add_subdirectory(agents) add_subdirectory(defaultsetup) add_subdirectory(kioslave) add_subdirectory(migration) add_subdirectory(doc) ## install the MIME type spec file for KDEPIM specific MIME types install(FILES kdepim-mime.xml DESTINATION ${KDE_INSTALL_MIMEDIR}) if (NOT NO_REGENERATE_MIME) update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR}) endif() ecm_qt_install_logging_categories( EXPORT KDEPIMRUNTIME FILE kdepim-runtime.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES ) diff --git a/agents/newmailnotifier/newmailnotifieragent.cpp b/agents/newmailnotifier/newmailnotifieragent.cpp index 645a203bb..145b0cc5e 100644 --- a/agents/newmailnotifier/newmailnotifieragent.cpp +++ b/agents/newmailnotifier/newmailnotifieragent.cpp @@ -1,481 +1,481 @@ /* Copyright (c) 2013-2020 Laurent Montel Copyright (c) 2010 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 "newmailnotifieragent.h" -#include #include "specialnotifierjob.h" #include "newmailnotifieradaptor.h" #include "newmailnotifieragentsettings.h" -#include #include #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include +#include #include #include #include #include #include "newmailnotifier_debug.h" #include using namespace Akonadi; NewMailNotifierAgent::NewMailNotifierAgent(const QString &id) : AgentBase(id) { Kdelibs4ConfigMigrator migrate(QStringLiteral("newmailnotifieragent")); migrate.setConfigFiles(QStringList() << QStringLiteral("akonadi_newmailnotifier_agentrc") << QStringLiteral("akonadi_newmailnotifier_agent.notifyrc")); migrate.migrate(); connect(this, &Akonadi::AgentBase::reloadConfiguration, this, &NewMailNotifierAgent::slotReloadConfiguration); KLocalizedString::setApplicationDomain("akonadi_newmailnotifier_agent"); Akonadi::AttributeFactory::registerAttribute(); new NewMailNotifierAdaptor(this); NewMailNotifierAgentSettings::instance(KSharedConfig::openConfig()); mIdentityManager = KIdentityManagement::IdentityManager::self(); connect(mIdentityManager, QOverload<>::of(&KIdentityManagement::IdentityManager::changed), this, &NewMailNotifierAgent::slotIdentitiesChanged); slotIdentitiesChanged(); mDefaultIconName = QStringLiteral("kmail"); QDBusConnection::sessionBus().registerObject(QStringLiteral("/NewMailNotifierAgent"), this, QDBusConnection::ExportAdaptors); QString service = QStringLiteral("org.freedesktop.Akonadi.NewMailNotifierAgent"); if (Akonadi::ServerManager::hasInstanceIdentifier()) { service += QLatin1Char('.') + Akonadi::ServerManager::instanceIdentifier(); } QDBusConnection::sessionBus().registerService(service); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceStatusChanged, this, &NewMailNotifierAgent::slotInstanceStatusChanged); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceRemoved, this, &NewMailNotifierAgent::slotInstanceRemoved); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceAdded, this, &NewMailNotifierAgent::slotInstanceAdded); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceNameChanged, this, &NewMailNotifierAgent::slotInstanceNameChanged); changeRecorder()->setMimeTypeMonitored(KMime::Message::mimeType()); changeRecorder()->itemFetchScope().setCacheOnly(true); changeRecorder()->itemFetchScope().setFetchModificationTime(false); changeRecorder()->fetchCollection(true); changeRecorder()->setChangeRecordingEnabled(false); changeRecorder()->ignoreSession(Akonadi::Session::defaultSession()); changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); changeRecorder()->setCollectionMonitored(Collection::root(), true); mTimer.setInterval(5 * 1000); connect(&mTimer, &QTimer::timeout, this, &NewMailNotifierAgent::slotShowNotifications); if (isActive()) { mTimer.setSingleShot(true); } } void NewMailNotifierAgent::slotReloadConfiguration() { NewMailNotifierAgentSettings::self()->load(); } void NewMailNotifierAgent::slotIdentitiesChanged() { mListEmails = mIdentityManager->allEmails(); } void NewMailNotifierAgent::doSetOnline(bool online) { if (!online) { clearAll(); } } void NewMailNotifierAgent::setEnableAgent(bool enabled) { NewMailNotifierAgentSettings::setEnabled(enabled); NewMailNotifierAgentSettings::self()->save(); if (!enabled) { clearAll(); } } bool NewMailNotifierAgent::enabledAgent() const { return NewMailNotifierAgentSettings::enabled(); } void NewMailNotifierAgent::clearAll() { mNewMails.clear(); mInstanceNameInProgress.clear(); } bool NewMailNotifierAgent::excludeSpecialCollection(const Akonadi::Collection &collection) const { if (collection.hasAttribute()) { return true; } if (collection.hasAttribute()) { if (collection.attribute()->ignoreNewMail()) { return true; } } if (!collection.contentMimeTypes().contains(KMime::Message::mimeType())) { return true; } SpecialMailCollections::Type type = SpecialMailCollections::self()->specialCollectionType(collection); switch (type) { case SpecialMailCollections::Invalid: //Not a special collection case SpecialMailCollections::Inbox: return false; default: return true; } } void NewMailNotifierAgent::itemsRemoved(const Item::List &items) { if (!isActive()) { return; } QHash< Akonadi::Collection, QList >::iterator end(mNewMails.end()); for (QHash< Akonadi::Collection, QList >::iterator it = mNewMails.begin(); it != end; ++it) { QList idList = it.value(); bool itemFound = false; for (const Item &item : items) { const int numberOfItemsRemoved = idList.removeAll(item.id()); if (numberOfItemsRemoved > 0) { itemFound = true; } } if (itemFound) { if (mNewMails[it.key()].isEmpty()) { mNewMails.remove(it.key()); } else { mNewMails[it.key()] = idList; } } } } void NewMailNotifierAgent::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) { Q_UNUSED(removedFlags); if (!isActive()) { return; } for (const Akonadi::Item &item : items) { QHash< Akonadi::Collection, QList >::iterator end(mNewMails.end()); for (QHash< Akonadi::Collection, QList >::iterator it = mNewMails.begin(); it != end; ++it) { QList idList = it.value(); if (idList.contains(item.id()) && addedFlags.contains("\\SEEN")) { idList.removeAll(item.id()); if (idList.isEmpty()) { mNewMails.remove(it.key()); break; } else { (*it) = idList; } } } } } void NewMailNotifierAgent::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) { if (!isActive()) { return; } for (const Akonadi::Item &item : items) { if (ignoreStatusMail(item)) { continue; } if (excludeSpecialCollection(collectionSource)) { continue; // outbox, sent-mail, trash, drafts or templates. } if (mNewMails.contains(collectionSource)) { QList idListFrom = mNewMails[ collectionSource ]; const int removeItems = idListFrom.removeAll(item.id()); if (removeItems > 0) { if (idListFrom.isEmpty()) { mNewMails.remove(collectionSource); } else { mNewMails[ collectionSource ] = idListFrom; } if (!excludeSpecialCollection(collectionDestination)) { QList idListTo = mNewMails[ collectionDestination ]; idListTo.append(item.id()); mNewMails[ collectionDestination ] = idListTo; } } } } } bool NewMailNotifierAgent::ignoreStatusMail(const Akonadi::Item &item) { Akonadi::MessageStatus status; status.setStatusFromFlags(item.flags()); if (status.isRead() || status.isSpam() || status.isIgnored()) { return true; } return false; } void NewMailNotifierAgent::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if (!isActive()) { return; } if (excludeSpecialCollection(collection)) { return; // outbox, sent-mail, trash, drafts or templates. } if (ignoreStatusMail(item)) { return; } if (!mTimer.isActive()) { mTimer.start(); } mNewMails[ collection ].append(item.id()); } void NewMailNotifierAgent::slotShowNotifications() { if (mNewMails.isEmpty()) { return; } if (!isActive()) { return; } if (!mInstanceNameInProgress.isEmpty()) { //Restart timer until all is done. mTimer.start(); return; } QString message; if (NewMailNotifierAgentSettings::verboseNotification()) { bool hasUniqMessage = true; Akonadi::Item::Id item = -1; QString currentPath; QStringList texts; const int numberOfCollection(mNewMails.count()); if (numberOfCollection > 1) { hasUniqMessage = false; } QHash< Akonadi::Collection, QList >::const_iterator end(mNewMails.constEnd()); for (QHash< Akonadi::Collection, QList >::const_iterator it = mNewMails.constBegin(); it != end; ++it) { const Akonadi::EntityDisplayAttribute *attr = it.key().attribute(); QString displayName; if (attr && !attr->displayName().isEmpty()) { displayName = attr->displayName(); } else { displayName = it.key().name(); } if (hasUniqMessage) { const int numberOfValue(it.value().count()); if (numberOfValue == 0) { //You can have an unique folder with 0 message return; } else if (numberOfValue == 1) { item = it.value().at(0); currentPath = displayName; break; } else { hasUniqMessage = false; } } QString resourceName; if (!mCacheResourceName.contains(it.key().resource())) { const Akonadi::AgentInstance::List lst = Akonadi::AgentManager::self()->instances(); for (const Akonadi::AgentInstance &instance : lst) { if (instance.identifier() == it.key().resource()) { mCacheResourceName.insert(instance.identifier(), instance.name()); resourceName = instance.name(); break; } } } else { resourceName = mCacheResourceName.value(it.key().resource()); } const int numberOfEmails(it.value().count()); if (numberOfEmails > 0) { texts.append(i18ncp("%2 = name of mail folder; %3 = name of Akonadi POP3/IMAP/etc resource (as user named it)", "One new email in %2 from \"%3\"", "%1 new emails in %2 from \"%3\"", numberOfEmails, displayName, resourceName)); } } if (hasUniqMessage) { SpecialNotifierJob *job = new SpecialNotifierJob(mListEmails, currentPath, item, this); job->setDefaultIconName(mDefaultIconName); connect(job, &SpecialNotifierJob::displayNotification, this, &NewMailNotifierAgent::slotDisplayNotification); connect(job, &SpecialNotifierJob::say, this, &NewMailNotifierAgent::slotSay); mNewMails.clear(); return; } else { message = texts.join(QLatin1String("
")); } } else { message = i18n("New mail arrived"); } qCDebug(NEWMAILNOTIFIER_LOG) << message; slotDisplayNotification(QPixmap(), message); mNewMails.clear(); } void NewMailNotifierAgent::slotDisplayNotification(const QPixmap &pixmap, const QString &message) { if (pixmap.isNull()) { KNotification::event(QStringLiteral("new-email"), QString(), message, mDefaultIconName, nullptr, NewMailNotifierAgentSettings::keepPersistentNotification() ? KNotification::Persistent | KNotification::SkipGrouping : KNotification::CloseOnTimeout, QStringLiteral("akonadi_newmailnotifier_agent")); } else { KNotification::event(QStringLiteral("new-email"), message, pixmap, nullptr, NewMailNotifierAgentSettings::keepPersistentNotification() ? KNotification::Persistent | KNotification::SkipGrouping : KNotification::CloseOnTimeout, QStringLiteral("akonadi_newmailnotifier_agent")); } } void NewMailNotifierAgent::slotInstanceNameChanged(const Akonadi::AgentInstance &instance) { if (!isActive()) { return; } const QString identifier(instance.identifier()); int resourceNameRemoved = mCacheResourceName.remove(identifier); if (resourceNameRemoved > 0) { mCacheResourceName.insert(identifier, instance.name()); } } void NewMailNotifierAgent::slotInstanceStatusChanged(const Akonadi::AgentInstance &instance) { if (!isActive()) { return; } const QString identifier(instance.identifier()); switch (instance.status()) { case Akonadi::AgentInstance::Broken: case Akonadi::AgentInstance::Idle: mInstanceNameInProgress.removeAll(identifier); break; case Akonadi::AgentInstance::Running: if (!excludeAgentType(instance)) { if (!mInstanceNameInProgress.contains(identifier)) { mInstanceNameInProgress.append(identifier); } } break; case Akonadi::AgentInstance::NotConfigured: //Nothing break; } } bool NewMailNotifierAgent::excludeAgentType(const Akonadi::AgentInstance &instance) { if (instance.type().mimeTypes().contains(KMime::Message::mimeType())) { const QStringList capabilities(instance.type().capabilities()); if (capabilities.contains(QLatin1String("Resource")) && !capabilities.contains(QLatin1String("Virtual")) && !capabilities.contains(QLatin1String("MailTransport"))) { return false; } else { return true; } } return true; } void NewMailNotifierAgent::slotInstanceRemoved(const Akonadi::AgentInstance &instance) { if (!isActive()) { return; } const QString identifier(instance.identifier()); mInstanceNameInProgress.removeAll(identifier); } void NewMailNotifierAgent::slotInstanceAdded(const Akonadi::AgentInstance &instance) { mCacheResourceName.insert(instance.identifier(), instance.name()); } void NewMailNotifierAgent::printDebug() { qCDebug(NEWMAILNOTIFIER_LOG) << "instance in progress: " << mInstanceNameInProgress << "\n notifier enabled : " << NewMailNotifierAgentSettings::enabled() << "\n check in progress : " << !mInstanceNameInProgress.isEmpty(); } bool NewMailNotifierAgent::isActive() const { return isOnline() && NewMailNotifierAgentSettings::enabled(); } void NewMailNotifierAgent::slotSay(const QString &message) { if (!mTextToSpeech) { mTextToSpeech = new QTextToSpeech(this); } if (mTextToSpeech->availableEngines().isEmpty()) { qCWarning(NEWMAILNOTIFIER_LOG) << "No texttospeech engine available"; } else { mTextToSpeech->say(message); } } AKONADI_AGENT_MAIN(NewMailNotifierAgent) diff --git a/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp b/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp index d73276280..b09a1fe9e 100644 --- a/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp +++ b/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp @@ -1,240 +1,240 @@ /* Copyright (c) 2013-2020 Laurent Montel 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 "newmailnotifierselectcollectionwidget.h" -#include #include #include #include #include +#include #include #include #include #include #include #include #include #include "newmailnotifier_debug.h" #include #include #include #include #include NewMailNotifierCollectionProxyModel::NewMailNotifierCollectionProxyModel(QObject *parent) : QIdentityProxyModel(parent) { } QVariant NewMailNotifierCollectionProxyModel::data(const QModelIndex &index, int role) const { if (role == Qt::CheckStateRole) { if (index.isValid()) { const Akonadi::Collection collection = data(index, Akonadi::EntityTreeModel::CollectionRole).value(); if (mNotificationCollection.contains(collection)) { return mNotificationCollection.value(collection) ? Qt::Checked : Qt::Unchecked; } else { const Akonadi::NewMailNotifierAttribute *attr = collection.attribute(); if (!attr || !attr->ignoreNewMail()) { return Qt::Checked; } return Qt::Unchecked; } } } return QIdentityProxyModel::data(index, role); } bool NewMailNotifierCollectionProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role == Qt::CheckStateRole) { if (index.isValid()) { const Akonadi::Collection collection = data(index, Akonadi::EntityTreeModel::CollectionRole).value(); mNotificationCollection[collection] = (value == Qt::Checked); emit dataChanged(index, index); return true; } } return QIdentityProxyModel::setData(index, value, role); } Qt::ItemFlags NewMailNotifierCollectionProxyModel::flags(const QModelIndex &index) const { if (index.isValid()) { return QIdentityProxyModel::flags(index) | Qt::ItemIsUserCheckable; } else { return QIdentityProxyModel::flags(index); } } QHash NewMailNotifierCollectionProxyModel::notificationCollection() const { return mNotificationCollection; } NewMailNotifierSelectCollectionWidget::NewMailNotifierSelectCollectionWidget(QWidget *parent) : QWidget(parent) { Akonadi::AttributeFactory::registerAttribute(); QVBoxLayout *vbox = new QVBoxLayout(this); QLabel *label = new QLabel(i18n("Select which folders to monitor for new message notifications:")); vbox->addWidget(label); // Create a new change recorder. mChangeRecorder = new Akonadi::ChangeRecorder(this); mChangeRecorder->setMimeTypeMonitored(KMime::Message::mimeType()); mChangeRecorder->fetchCollection(true); mChangeRecorder->setAllMonitored(true); mModel = new Akonadi::EntityTreeModel(mChangeRecorder, this); // Set the model to show only collections, not items. mModel->setItemPopulationStrategy(Akonadi::EntityTreeModel::NoItemPopulation); connect(mModel, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &NewMailNotifierSelectCollectionWidget::slotCollectionTreeFetched); Akonadi::CollectionFilterProxyModel *mimeTypeProxy = new Akonadi::CollectionFilterProxyModel(this); mimeTypeProxy->setExcludeVirtualCollections(true); mimeTypeProxy->setDynamicSortFilter(true); mimeTypeProxy->addMimeTypeFilters(QStringList() << KMime::Message::mimeType()); mimeTypeProxy->setSourceModel(mModel); mNewMailNotifierProxyModel = new NewMailNotifierCollectionProxyModel(this); mNewMailNotifierProxyModel->setSourceModel(mimeTypeProxy); mCollectionFilter = new QSortFilterProxyModel(this); mCollectionFilter->setRecursiveFilteringEnabled(true); mCollectionFilter->setSourceModel(mNewMailNotifierProxyModel); mCollectionFilter->setDynamicSortFilter(true); mCollectionFilter->setFilterCaseSensitivity(Qt::CaseInsensitive); mCollectionFilter->setSortRole(Qt::DisplayRole); mCollectionFilter->setSortCaseSensitivity(Qt::CaseSensitive); mCollectionFilter->setSortLocaleAware(true); KLineEdit *searchLine = new KLineEdit(this); searchLine->setPlaceholderText(i18n("Search...")); searchLine->setClearButtonEnabled(true); connect(searchLine, &QLineEdit::textChanged, this, &NewMailNotifierSelectCollectionWidget::slotSetCollectionFilter); vbox->addWidget(searchLine); mFolderView = new QTreeView(this); mFolderView->setEditTriggers(QAbstractItemView::NoEditTriggers); mFolderView->setAlternatingRowColors(true); vbox->addWidget(mFolderView); mFolderView->setModel(mCollectionFilter); QHBoxLayout *hbox = new QHBoxLayout; vbox->addLayout(hbox); QPushButton *button = new QPushButton(i18n("&Select All"), this); connect(button, &QPushButton::clicked, this, &NewMailNotifierSelectCollectionWidget::slotSelectAllCollections); hbox->addWidget(button); button = new QPushButton(i18n("&Unselect All"), this); connect(button, &QPushButton::clicked, this, &NewMailNotifierSelectCollectionWidget::slotUnselectAllCollections); hbox->addWidget(button); hbox->addStretch(1); } NewMailNotifierSelectCollectionWidget::~NewMailNotifierSelectCollectionWidget() { } void NewMailNotifierSelectCollectionWidget::slotCollectionTreeFetched() { mCollectionFilter->sort(0, Qt::AscendingOrder); mFolderView->expandAll(); } void NewMailNotifierSelectCollectionWidget::slotSetCollectionFilter(const QString &filter) { mCollectionFilter->setFilterWildcard(filter); mFolderView->expandAll(); } void NewMailNotifierSelectCollectionWidget::slotSelectAllCollections() { forceStatus(QModelIndex(), true); } void NewMailNotifierSelectCollectionWidget::slotUnselectAllCollections() { forceStatus(QModelIndex(), false); } void NewMailNotifierSelectCollectionWidget::forceStatus(const QModelIndex &parent, bool status) { const int nbCol = mNewMailNotifierProxyModel->rowCount(parent); for (int i = 0; i < nbCol; ++i) { const QModelIndex child = mNewMailNotifierProxyModel->index(i, 0, parent); mNewMailNotifierProxyModel->setData(child, status ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole); forceStatus(child, status); } } void NewMailNotifierSelectCollectionWidget::updateCollectionsRecursive() { QHashIterator i(mNewMailNotifierProxyModel->notificationCollection()); while (i.hasNext()) { i.next(); Akonadi::Collection collection = i.key(); Akonadi::NewMailNotifierAttribute *attr = collection.attribute(); Akonadi::CollectionModifyJob *modifyJob = nullptr; const bool selected = i.value(); if (selected && attr && attr->ignoreNewMail()) { collection.removeAttribute(); modifyJob = new Akonadi::CollectionModifyJob(collection); modifyJob->setProperty("AttributeAdded", true); } else if (!selected && (!attr || !attr->ignoreNewMail())) { attr = collection.attribute(Akonadi::Collection::AddIfMissing); attr->setIgnoreNewMail(true); modifyJob = new Akonadi::CollectionModifyJob(collection); modifyJob->setProperty("AttributeAdded", false); } if (modifyJob) { connect(modifyJob, &Akonadi::CollectionModifyJob::finished, this, &NewMailNotifierSelectCollectionWidget::slotModifyJobDone); } } } void NewMailNotifierSelectCollectionWidget::slotModifyJobDone(KJob *job) { Akonadi::CollectionModifyJob *modifyJob = qobject_cast(job); if (modifyJob && job->error()) { if (job->property("AttributeAdded").toBool()) { qCWarning(NEWMAILNOTIFIER_LOG) << "Failed to append NewMailNotifierAttribute to collection" << modifyJob->collection().id() << ":" << job->errorString(); } else { qCWarning(NEWMAILNOTIFIER_LOG) << "Failed to remove NewMailNotifierAttribute from collection" << modifyJob->collection().id() << ":" << job->errorString(); } } } diff --git a/resources/pop3/pop3resource.cpp b/resources/pop3/pop3resource.cpp index 384bf7fef..8e18976e3 100644 --- a/resources/pop3/pop3resource.cpp +++ b/resources/pop3/pop3resource.cpp @@ -1,1051 +1,1051 @@ /* 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 #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)) , mSettings(KSharedConfig::openConfig()) { Akonadi::AttributeFactory::registerAttribute(); setNeedsNetwork(true); mSettings.setResourceId(identifier()); if (mSettings.name().isEmpty()) { if (name() == identifier()) { mSettings.setName(i18n("POP3 Account")); } else { mSettings.setName(name()); } } setName(mSettings.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() { mSettings.save(); delete mWallet; mWallet = nullptr; } void POP3Resource::configurationChanged() { updateIntervalTimer(); mPassword.clear(); } void POP3Resource::updateIntervalTimer() { if (mSettings.intervalCheckEnabled() && mState == Idle) { mIntervalTimer->start(mSettings.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(mSettings.login()); dlg->setPassword(mPassword); dlg->setPrompt(queryText); dlg->setWindowTitle(name()); dlg->addCommentLine(i18n("Account:"), name()); bool gotIt = false; if (dlg->exec()) { mPassword = dlg->password(); mSettings.setLogin(dlg->username()); mSettings.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(mSettings.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 (!mSettings.precommand().isEmpty()) { PrecommandJob *precommandJob = new PrecommandJob(mSettings.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 (!mSettings.unitTestPassword().isEmpty()) { mPassword = mSettings.unitTestPassword(); advanceState(Connect); break; } const bool passwordNeeded = mSettings.authenticationMethod() != MailTransport::Transport::EnumAuthenticationType::GSSAPI; const bool loadPasswordFromWallet = !mAskAgain && passwordNeeded && !mSettings.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 (mSettings.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(mSettings, 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 = mSettings.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; mSettings.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.", mSettings.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 (mSettings.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, mSettings.seenUidList()) { const int alreadyDownloadedID = mUidsToIdsMap.value(alreadyDownloadedUID, -1); if (alreadyDownloadedID != -1) { bytesRemainingOnServer += mIdsToSizeMap.value(alreadyDownloadedID); } } if (mSettings.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 = mSettings.seenUidList(); const QList timeOfSeenUids = mSettings.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 (mSettings.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 (mSettings.leaveOnServerDays() > 0) { const int secondsPerDay = 86400; time_t timeLimit = time(nullptr) - (secondsPerDay * mSettings.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 (mSettings.leaveOnServerCount() > 0) { const int numToDelete = mIdsToSave.count() - mSettings.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 (mSettings.leaveOnServerSize() > 0) { const qint64 limitInBytes = mSettings.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 = mSettings.seenUidList(); QList timeOfSeenUids = mSettings.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); } mSettings.setSeenUidList(seenUIDs); mSettings.setSeenUidTimeList(timeOfSeenUids); mSettings.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 = mSettings.seenUidList(); QList timeOfSeenUIDs = mSettings.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"; mSettings.setSeenUidList(seenUIDs); mSettings.setSeenUidTimeList(timeOfSeenUIDs); mSettings.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)