diff --git a/resources/kolab/kolabretrievecollectionstask.cpp b/resources/kolab/kolabretrievecollectionstask.cpp index 620fb3a5b..c47ac7b73 100644 --- a/resources/kolab/kolabretrievecollectionstask.cpp +++ b/resources/kolab/kolabretrievecollectionstask.cpp @@ -1,546 +1,546 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens 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 "kolabretrievecollectionstask.h" #include "kolabhelpers.h" #include "tracer.h" #include "kolabresource_debug.h" #include "helper_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool isNamespaceFolder(const QString &path, const QList &namespaces, bool matchCompletePath = false) { for (const KIMAP::MailBoxDescriptor &desc : namespaces) { - if (path.startsWith(desc.name.left(desc.name.size() - 1))) { //Namespace ends with path separator and pathPart doesn't + if (path.startsWith(desc.name.left(desc.name.size() - 1))) { //Namespace ends with path separator and path doesn't if (!matchCompletePath || path.size() - desc.name.size() <= 1) { //We want to match only for the complete path return true; } } } return false; } RetrieveMetadataJob::RetrieveMetadataJob(KIMAP::Session *session, const QStringList &mailboxes, const QStringList &serverCapabilities, const QSet &requestedMetadata, const QString &separator, const QList &sharedNamespace, const QList &userNamespace, QObject *parent) : KJob(parent) , mJobs(0) , mRequestedMetadata(requestedMetadata) , mServerCapabilities(serverCapabilities) , mMailboxes(mailboxes) , mSession(session) , mSeparator(separator) , mSharedNamespace(sharedNamespace) , mUserNamespace(userNamespace) { } void RetrieveMetadataJob::start() { Trace(); //Fill the map with empty entires so we set the mimetype to mail if no metadata is retrieved for (const QString &mailbox : qAsConst(mMailboxes)) { mMetadata.insert(mailbox, QMap()); } if (mServerCapabilities.contains(QStringLiteral("METADATA")) || mServerCapabilities.contains(QStringLiteral("ANNOTATEMORE"))) { QSet toplevelMailboxes; for (const QString &mailbox : qAsConst(mMailboxes)) { const QStringList parts = mailbox.split(mSeparator); if (!parts.isEmpty()) { if (isNamespaceFolder(mailbox, mUserNamespace) && parts.length() >= 2) { // Other Users can be too big to request with a single command so we request Other Users//* toplevelMailboxes << parts.at(0) + mSeparator + parts.at(1) + mSeparator; } else if (!isNamespaceFolder(mailbox, mSharedNamespace)) { toplevelMailboxes << parts.first(); } } } for (const KIMAP::MailBoxDescriptor &desc : qAsConst(mSharedNamespace)) { toplevelMailboxes << desc.name; } //TODO perhaps exclude the shared and other users namespaces by listing only toplevel (with %), and then only getting metadata of the toplevel folders. for (const QString &mailbox : qAsConst(toplevelMailboxes)) { KIMAP::GetMetaDataJob *meta = new KIMAP::GetMetaDataJob(mSession); meta->setMailBox(mailbox + QLatin1String("*")); if (mServerCapabilities.contains(QStringLiteral("METADATA"))) { meta->setServerCapability(KIMAP::MetaDataJobBase::Metadata); } else { meta->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore); } meta->setDepth(KIMAP::GetMetaDataJob::AllLevels); for (const QByteArray &requestedEntry : qAsConst(mRequestedMetadata)) { meta->addRequestedEntry(requestedEntry); } connect(meta, &KJob::result, this, &RetrieveMetadataJob::onGetMetaDataDone); mJobs++; meta->start(); } } // Get the ACLs from the mailbox if it's supported if (mServerCapabilities.contains(QStringLiteral("ACL"))) { for (const QString &mailbox : qAsConst(mMailboxes)) { // "Shared Folders" is not a valid mailbox, so we have to skip the ACL request for this folder if (isNamespaceFolder(mailbox, mSharedNamespace, true)) { continue; } KIMAP::MyRightsJob *rights = new KIMAP::MyRightsJob(mSession); rights->setMailBox(mailbox); connect(rights, &KJob::result, this, &RetrieveMetadataJob::onRightsReceived); mJobs++; rights->start(); } } checkDone(); } void RetrieveMetadataJob::onGetMetaDataDone(KJob *job) { mJobs--; KIMAP::GetMetaDataJob *meta = static_cast(job); if (job->error()) { qCDebug(KOLABRESOURCE_LOG) << "No metadata for for mailbox: " << meta->mailBox(); if (!isNamespaceFolder(meta->mailBox(), mSharedNamespace)) { qCWarning(KOLABRESOURCE_LOG) << "Get metadata failed: " << job->errorString(); //We ignore the error to avoid failing the complete sync. We can run into this when trying to retrieve rights for non-existing mailboxes. } checkDone(); return; } const QHash > metadata = meta->allMetaDataForMailboxes(); const QStringList lstKeys = metadata.keys(); for (const QString &folder : lstKeys) { mMetadata.insert(folder, metadata.value(folder)); } checkDone(); } void RetrieveMetadataJob::onRightsReceived(KJob *job) { mJobs--; KIMAP::MyRightsJob *rights = static_cast(job); if (job->error()) { qCDebug(KOLABRESOURCE_LOG) << "No rights for mailbox: " << rights->mailBox(); if (!isNamespaceFolder(rights->mailBox(), mSharedNamespace)) { - qCWarning(KOLABRESOURCE_LOG) << "MyRights failed: " << job->errorString(); + qCWarning(KOLABRESOURCE_LOG) << "MyRights for mailbox" << rights->mailBox() << "failed:" << job->errorString(); //We ignore the error to avoid failing the complete sync. We can run into this when trying to retrieve rights for non-existing mailboxes. } checkDone(); return; } const KIMAP::Acl::Rights imapRights = rights->rights(); mRights.insert(rights->mailBox(), imapRights); checkDone(); } void RetrieveMetadataJob::checkDone() { if (!mJobs) { Trace() << "done"; qCDebug(KOLABRESOURCE_LOG) << "done"; emitResult(); } } KolabRetrieveCollectionsTask::KolabRetrieveCollectionsTask(const ResourceStateInterface::Ptr &resource, QObject *parent) : ResourceTask(CancelIfNoSession, resource, parent) , mJobs(0) , mSession(nullptr) , cContentMimeTypes("CONTENTMIMETYPES") , cAccessRights("AccessRights") , cImapAcl("imapacl") , cCollectionAnnotations("collectionannotations") , cDefaultKeepLocalChanges(QSet() << cContentMimeTypes << cAccessRights << cImapAcl << cCollectionAnnotations) , cDefaultMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("application/x-kolab-objects")) , cCollectionOnlyContentMimeTypes(QStringList() << Akonadi::Collection::mimeType()) { mRequestedMetadata << "/shared/vendor/kolab/folder-type"; mRequestedMetadata << "/private/vendor/kolab/folder-type"; mRequestedMetadata << "/shared" KOLAB_COLOR_ANNOTATION << "/private" KOLAB_COLOR_ANNOTATION; } KolabRetrieveCollectionsTask::~KolabRetrieveCollectionsTask() { } void KolabRetrieveCollectionsTask::doStart(KIMAP::Session *session) { Trace(); qCDebug(KOLABRESOURCE_LOG) << "Starting collection retrieval"; mTime.start(); mSession = session; Akonadi::Collection root; root.setName(resourceName()); root.setRemoteId(rootRemoteId()); root.setContentMimeTypes(QStringList(Akonadi::Collection::mimeType())); root.setParentCollection(Akonadi::Collection::root()); root.addAttribute(new NoSelectAttribute(true)); root.attribute(Akonadi::Collection::AddIfMissing)->setIconName(QStringLiteral("kolab")); Akonadi::CachePolicy policy; policy.setInheritFromParent(false); policy.setSyncOnDemand(true); QStringList localParts; localParts << QLatin1String(Akonadi::MessagePart::Envelope) << QLatin1String(Akonadi::MessagePart::Header); int cacheTimeout = 60; if (isDisconnectedModeEnabled()) { // For disconnected mode we also cache the body // and we keep all data indifinitely localParts << QLatin1String(Akonadi::MessagePart::Body); cacheTimeout = -1; } policy.setLocalParts(localParts); policy.setCacheTimeout(cacheTimeout); policy.setIntervalCheckTime(intervalCheckTime()); root.setCachePolicy(policy); mMailCollections.insert(QString(), root); Trace() << "subscription enabled: " << isSubscriptionEnabled(); //jobs are serialized by the session if (isSubscriptionEnabled()) { KIMAP::ListJob *fullListJob = new KIMAP::ListJob(session); fullListJob->setOption(KIMAP::ListJob::NoOption); fullListJob->setQueriedNamespaces(serverNamespaces()); connect(fullListJob, &KIMAP::ListJob::mailBoxesReceived, this, &KolabRetrieveCollectionsTask::onFullMailBoxesReceived); connect(fullListJob, &KJob::result, this, &KolabRetrieveCollectionsTask::onFullMailBoxesReceiveDone); mJobs++; fullListJob->start(); } KIMAP::ListJob *listJob = new KIMAP::ListJob(session); listJob->setOption(KIMAP::ListJob::IncludeUnsubscribed); listJob->setQueriedNamespaces(serverNamespaces()); connect(listJob, &KIMAP::ListJob::mailBoxesReceived, this, &KolabRetrieveCollectionsTask::onMailBoxesReceived); connect(listJob, &KJob::result, this, &KolabRetrieveCollectionsTask::onMailBoxesReceiveDone); mJobs++; listJob->start(); } void KolabRetrieveCollectionsTask::onMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors, const QList< QList > &flags) { const int nbDescriptors = descriptors.size(); for (int i = 0; i < nbDescriptors; ++i) { const KIMAP::MailBoxDescriptor descriptor = descriptors[i]; createCollection(descriptor.name, flags.at(i), !isSubscriptionEnabled() || mSubscribedMailboxes.contains(descriptor.name)); } checkDone(); } Akonadi::Collection KolabRetrieveCollectionsTask::getOrCreateParent(const QString &path) { if (mMailCollections.contains(path)) { return mMailCollections.value(path); } //create a dummy collection const QString separator = separatorCharacter(); const QStringList pathParts = path.split(separator); const QString pathPart = pathParts.last(); Akonadi::Collection c; c.setName(pathPart); c.setRemoteId(separator + pathPart); const QStringList parentPath = pathParts.mid(0, pathParts.size() - 1); const Akonadi::Collection parentCollection = getOrCreateParent(parentPath.join(separator)); c.setParentCollection(parentCollection); c.addAttribute(new NoSelectAttribute(true)); c.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType()); c.setRights(Akonadi::Collection::ReadOnly); c.setEnabled(false); setAttributes(c, pathParts, path); mMailCollections.insert(path, c); return c; } void KolabRetrieveCollectionsTask::setAttributes(Akonadi::Collection &c, const QStringList &pathParts, const QString &path) { Akonadi::CollectionIdentificationAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); attr->setIdentifier(path.toLatin1()); // If the folder is a other users folder block all alarms from default if (isNamespaceFolder(path, resourceState()->userNamespaces())) { Akonadi::BlockAlarmsAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); attr->blockEverything(true); } // If the folder is a other users top-level folder mark it accordingly if (pathParts.size() == 1 && isNamespaceFolder(path, resourceState()->userNamespaces())) { Akonadi::EntityDisplayAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); attr->setDisplayName(i18n("Other Users")); attr->setIconName(QStringLiteral("x-mail-distribution-list")); } //Mark user folders for searching if (pathParts.size() >= 2 && isNamespaceFolder(path, resourceState()->userNamespaces())) { Akonadi::CollectionIdentificationAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); if (pathParts.size() == 2) { attr->setCollectionNamespace("usertoplevel"); } else { attr->setCollectionNamespace("user"); } } // If the folder is a shared folders top-level folder mark it accordingly if (pathParts.size() == 1 && isNamespaceFolder(path, resourceState()->sharedNamespaces())) { Akonadi::EntityDisplayAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); attr->setDisplayName(i18n("Shared Folders")); attr->setIconName(QStringLiteral("x-mail-distribution-list")); } //Mark shared folders for searching if (pathParts.size() >= 2 && isNamespaceFolder(path, resourceState()->sharedNamespaces())) { Akonadi::CollectionIdentificationAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); attr->setCollectionNamespace("shared"); } } void KolabRetrieveCollectionsTask::createCollection(const QString &mailbox, const QList ¤tFlags, bool isSubscribed) { const QString separator = separatorCharacter(); Q_ASSERT(separator.size() == 1); const QString boxName = mailbox.endsWith(separator) ? mailbox.left(mailbox.size() - 1) : mailbox; const QStringList pathParts = boxName.split(separator); const QString pathPart = pathParts.last(); Akonadi::Collection c; //If we had a dummy collection we need to replace it if (mMailCollections.contains(mailbox)) { c = mMailCollections.value(mailbox); } c.setName(pathPart); c.setRemoteId(separator + pathPart); const QStringList parentPath = pathParts.mid(0, pathParts.size() - 1); const Akonadi::Collection parentCollection = getOrCreateParent(parentPath.join(separator)); c.setParentCollection(parentCollection); //TODO get from ResourceState, and add KMime::Message::mimeType() for the normal imap resource by default //We add a dummy mimetype, otherwise the itemsync doesn't even work (action is disabled and resourcebase aborts the operation) c.setContentMimeTypes(cDefaultMimeTypes); c.setKeepLocalChanges(cDefaultKeepLocalChanges); //assume LRS, until myrights is executed if (serverCapabilities().contains(QStringLiteral("ACL"))) { c.setRights(Akonadi::Collection::ReadOnly); } else { c.setRights(Akonadi::Collection::AllRights); } setAttributes(c, pathParts, mailbox); // If the folder is the Inbox, make some special settings. if (pathParts.size() == 1 && pathPart.compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { Akonadi::EntityDisplayAttribute *attr = c.attribute(Akonadi::Collection::AddIfMissing); attr->setDisplayName(i18n("Inbox")); attr->setIconName(QStringLiteral("mail-folder-inbox")); setIdleCollection(c); } // If this folder is a noselect folder, make some special settings. if (currentFlags.contains("\\noselect")) { c.addAttribute(new NoSelectAttribute(true)); c.setContentMimeTypes(cCollectionOnlyContentMimeTypes); c.setRights(Akonadi::Collection::ReadOnly); } else { // remove the noselect attribute explicitly, in case we had set it before (eg. for non-subscribed non-leaf folders) c.removeAttribute(); } // If this folder is a noinferiors folder, it is not allowed to create subfolders inside. if (currentFlags.contains("\\noinferiors")) { //qCDebug(KOLABRESOURCE_LOG) << "Noinferiors: " << currentPath; c.addAttribute(new NoInferiorsAttribute(true)); c.setRights(c.rights() & ~Akonadi::Collection::CanCreateCollection); } c.setEnabled(isSubscribed); // qCDebug(KOLABRESOURCE_LOG) << "creating collection " << mailbox << " with parent " << parentPath; mMailCollections.insert(mailbox, c); } void KolabRetrieveCollectionsTask::onMailBoxesReceiveDone(KJob *job) { Trace(); qCDebug(KOLABRESOURCE_LOG) << "All mailboxes received: " << mTime.elapsed(); qCDebug(KOLABRESOURCE_LOG) << "in total: " << mMailCollections.size(); mJobs--; if (job->error()) { qCWarning(KOLABRESOURCE_LOG) << QStringLiteral("Failed to retrieve mailboxes: ") + job->errorString(); cancelTask(i18n("Collection retrieval failed")); } else { QSet mailboxes; const QStringList lstKeys = mMailCollections.keys(); for (const QString &mailbox : lstKeys) { if (!mailbox.isEmpty() && !isNamespaceFolder(mailbox, resourceState()->userNamespaces() + resourceState()->sharedNamespaces())) { mailboxes << mailbox; } } //Only request metadata for subscribed Other Users Folders const QStringList metadataMailboxes = mailboxes.unite(mSubscribedMailboxes.toList().toSet()).toList(); RetrieveMetadataJob *metadata = new RetrieveMetadataJob(mSession, metadataMailboxes, serverCapabilities(), mRequestedMetadata, separatorCharacter(), resourceState()->sharedNamespaces(), resourceState()->userNamespaces(), this); connect(metadata, &KJob::result, this, &KolabRetrieveCollectionsTask::onMetadataRetrieved); mJobs++; metadata->start(); } } void KolabRetrieveCollectionsTask::applyRights(const QHash &rights) { // qCDebug(KOLABRESOURCE_LOG) << rights; const QStringList lstKeys = rights.keys(); for (const QString &mailbox : lstKeys) { if (mMailCollections.contains(mailbox)) { const KIMAP::Acl::Rights imapRights = rights.value(mailbox); QStringList parts = mailbox.split(separatorCharacter()); parts.removeLast(); QString parentMailbox = parts.join(separatorCharacter()); KIMAP::Acl::Rights parentImapRights; //If the parent folder is not existing we cant rename if (!parentMailbox.isEmpty() && rights.contains(parentMailbox)) { parentImapRights = rights.value(parentMailbox); } // qCDebug(KOLABRESOURCE_LOG) << mailbox << parentMailbox << imapRights << parentImapRights; Akonadi::Collection &collection = mMailCollections[mailbox]; CollectionMetadataHelper::applyRights(collection, imapRights, parentImapRights); // Store the mailbox ACLs Akonadi::ImapAclAttribute *aclAttribute = collection.attribute(Akonadi::Collection::AddIfMissing); const KIMAP::Acl::Rights oldRights = aclAttribute->myRights(); if (oldRights != imapRights) { aclAttribute->setMyRights(imapRights); } } else { qCWarning(KOLABRESOURCE_LOG) << "Can't find mailbox " << mailbox; } } } void KolabRetrieveCollectionsTask::applyMetadata(const QHash > &metadataMap) { // qCDebug(KOLABRESOURCE_LOG) << metadataMap; Q_FOREACH (const QString &mailbox, metadataMap.keys()) { const QMap metadata = metadataMap.value(mailbox); if (mMailCollections.contains(mailbox)) { Akonadi::Collection &collection = mMailCollections[mailbox]; // qCDebug(KOLABRESOURCE_LOG) << mailbox << metadata << type << folderType << KolabHelpers::getContentMimeTypes(folderType); collection.attribute(Akonadi::Collection::AddIfMissing)->setAnnotations(metadata); const QByteArray type = KolabHelpers::getFolderTypeAnnotation(metadata); const Kolab::FolderType folderType = KolabHelpers::folderTypeFromString(type); collection.setContentMimeTypes(KolabHelpers::getContentMimeTypes(folderType)); const QColor color = KolabHelpers::getFolderColor(metadata); if (color.isValid()) { collection.attribute(Akonadi::Collection::AddIfMissing)->setColor(color); } QSet keepLocalChanges = collection.keepLocalChanges(); keepLocalChanges.remove(cContentMimeTypes); collection.setKeepLocalChanges(keepLocalChanges); } } } void KolabRetrieveCollectionsTask::onMetadataRetrieved(KJob *job) { Trace(); qCDebug(KOLABRESOURCE_LOG) << mTime.elapsed(); mJobs--; if (job->error()) { qCWarning(KOLABRESOURCE_LOG) << "Error while retrieving metadata, aborting collection retrieval: " << job->errorString(); cancelTask(i18n("Collection retrieval failed")); } else { RetrieveMetadataJob *metadata = static_cast(job); applyRights(metadata->mRights); applyMetadata(metadata->mMetadata); checkDone(); } } void KolabRetrieveCollectionsTask::checkDone() { if (!mJobs) { Trace() << "done " << mMailCollections.size(); collectionsRetrieved(Akonadi::valuesToVector(mMailCollections)); qCDebug(KOLABRESOURCE_LOG) << "done " << mTime.elapsed(); } } void KolabRetrieveCollectionsTask::onFullMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors, const QList< QList< QByteArray > > &flags) { Q_UNUSED(flags); for (const KIMAP::MailBoxDescriptor &descriptor : descriptors) { mSubscribedMailboxes.insert(descriptor.name); } } void KolabRetrieveCollectionsTask::onFullMailBoxesReceiveDone(KJob *job) { Trace(); qCDebug(KOLABRESOURCE_LOG) << "received subscribed collections " << mTime.elapsed(); mJobs--; if (job->error()) { qCWarning(KOLABRESOURCE_LOG) << QStringLiteral("Failed to retrieve subscribed collections: ") + job->errorString(); cancelTask(i18n("Collection retrieval failed")); } else { checkDone(); } }