diff --git a/resources/contacts/contactsresource.cpp b/resources/contacts/contactsresource.cpp index 926f7e807..5bb0d90af 100644 --- a/resources/contacts/contactsresource.cpp +++ b/resources/contacts/contactsresource.cpp @@ -1,511 +1,511 @@ /* Copyright (c) 2009 Tobias Koenig 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 "contactsresource.h" #include "settings.h" #include "contactsresourcesettingsadaptor.h" #include #include #include #include #include #include #include #include "contacts_resources_debug.h" #include using namespace Akonadi; ContactsResource::ContactsResource(const QString &id) : ResourceBase(id) { // setup the resource ContactsResourceSettings::instance(KSharedConfig::openConfig()); new ContactsResourceSettingsAdaptor(ContactsResourceSettings::self()); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), ContactsResourceSettings::self(), QDBusConnection::ExportAdaptors); changeRecorder()->fetchCollection(true); changeRecorder()->itemFetchScope().fetchFullPayload(true); changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All); changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All); setHierarchicalRemoteIdentifiersEnabled(true); mSupportedMimeTypes << KContacts::Addressee::mimeType() << KContacts::ContactGroup::mimeType() << Collection::mimeType(); if (name().startsWith(QLatin1String("akonadi_contacts_resource"))) { setName(i18n("Personal Contacts")); } // Make sure we have a valid directory (XDG dirs want this very much). initializeDirectory(ContactsResourceSettings::self()->path()); if (ContactsResourceSettings::self()->isConfigured()) { synchronize(); } connect(this, &ContactsResource::reloadConfiguration, this, &ContactsResource::slotReloadConfig); } ContactsResource::~ContactsResource() { delete ContactsResourceSettings::self(); } void ContactsResource::aboutToQuit() { } void ContactsResource::slotReloadConfig() { ContactsResourceSettings::self()->setIsConfigured(true); ContactsResourceSettings::self()->save(); clearCache(); initializeDirectory(baseDirectoryPath()); synchronize(); } Collection::List ContactsResource::createCollectionsForDirectory(const QDir &parentDirectory, const Collection &parentCollection) const { Collection::List collections; QDir dir(parentDirectory); dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable); const QFileInfoList entries = dir.entryInfoList(); collections.reserve(entries.count() * 2); for (const QFileInfo &entry : entries) { QDir subdir(entry.absoluteFilePath()); Collection collection; collection.setParentCollection(parentCollection); collection.setRemoteId(entry.fileName()); collection.setName(entry.fileName()); collection.setContentMimeTypes(mSupportedMimeTypes); collection.setRights(supportedRights(false)); collections << collection; collections << createCollectionsForDirectory(subdir, collection); } return collections; } void ContactsResource::retrieveCollections() { // create the resource collection Collection resourceCollection; resourceCollection.setParentCollection(Collection::root()); resourceCollection.setRemoteId(baseDirectoryPath()); resourceCollection.setName(name()); resourceCollection.setContentMimeTypes(mSupportedMimeTypes); resourceCollection.setRights(supportedRights(true)); const QDir baseDir(baseDirectoryPath()); Collection::List collections = createCollectionsForDirectory(baseDir, resourceCollection); collections.append(resourceCollection); collectionsRetrieved(collections); } void ContactsResource::retrieveItems(const Akonadi::Collection &collection) { QDir directory(directoryForCollection(collection)); if (!directory.exists()) { cancelTask(i18n("Directory '%1' does not exists", collection.remoteId())); return; } directory.setFilter(QDir::Files | QDir::Readable); Item::List items; const QFileInfoList entries = directory.entryInfoList(); for (const QFileInfo &entry : entries) { if (entry.fileName() == QLatin1String("WARNING_README.txt")) { continue; } Item item; item.setRemoteId(entry.fileName()); if (entry.fileName().endsWith(QLatin1String(".vcf"))) { item.setMimeType(KContacts::Addressee::mimeType()); } else if (entry.fileName().endsWith(QLatin1String(".ctg"))) { item.setMimeType(KContacts::ContactGroup::mimeType()); } else { cancelTask(i18n("Found file of unknown format: '%1'", entry.absoluteFilePath())); return; } items.append(item); } itemsRetrieved(items); } bool ContactsResource::retrieveItem(const Akonadi::Item &item, const QSet &) { - const QString filePath = directoryForCollection(item.parentCollection()) + QDir::separator() + item.remoteId(); + const QString filePath = directoryForCollection(item.parentCollection()) + QLatin1Char('/') + item.remoteId(); Item newItem(item); QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { cancelTask(i18n("Unable to open file '%1'", filePath)); return false; } if (filePath.endsWith(QLatin1String(".vcf"))) { KContacts::VCardConverter converter; const QByteArray content = file.readAll(); const KContacts::Addressee contact = converter.parseVCard(content); if (contact.isEmpty()) { cancelTask(i18n("Found invalid contact in file '%1'", filePath)); return false; } newItem.setPayload(contact); } else if (filePath.endsWith(QLatin1String(".ctg"))) { KContacts::ContactGroup group; QString errorMessage; if (!KContacts::ContactGroupTool::convertFromXml(&file, group, &errorMessage)) { cancelTask(i18n("Found invalid contact group in file '%1': %2", filePath, errorMessage)); return false; } newItem.setPayload(group); } else { cancelTask(i18n("Found file of unknown format: '%1'", filePath)); return false; } file.close(); itemRetrieved(newItem); return true; } void ContactsResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if (ContactsResourceSettings::self()->readOnly()) { cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId())); return; } const QString directoryPath = directoryForCollection(collection); Item newItem(item); if (item.hasPayload()) { const KContacts::Addressee contact = item.payload(); - const QString fileName = directoryPath + QDir::separator() + contact.uid() + QLatin1String(".vcf"); + const QString fileName = directoryPath + QLatin1Char('/') + contact.uid() + QStringLiteral(".vcf"); KContacts::VCardConverter converter; const QByteArray content = converter.createVCard(contact); QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString())); return; } file.write(content); file.close(); - newItem.setRemoteId(contact.uid() + QLatin1String(".vcf")); + newItem.setRemoteId(contact.uid() + QStringLiteral(".vcf")); } else if (item.hasPayload()) { const KContacts::ContactGroup group = item.payload(); - const QString fileName = directoryPath + QDir::separator() + group.id() + QLatin1String(".ctg"); + const QString fileName = directoryPath + QLatin1Char('/') + group.id() + QStringLiteral(".ctg"); QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString())); return; } KContacts::ContactGroupTool::convertToXml(group, &file); file.close(); newItem.setRemoteId(group.id() + QLatin1String(".ctg")); } else { qCWarning(CONTACTSRESOURCES_LOG) << "got item without (usable) payload, ignoring it"; } changeCommitted(newItem); } void ContactsResource::itemChanged(const Akonadi::Item &item, const QSet &) { if (ContactsResourceSettings::self()->readOnly()) { cancelTask(i18n("Trying to write to a read-only file: '%1'", item.remoteId())); return; } Item newItem(item); - const QString fileName = directoryForCollection(item.parentCollection()) + QDir::separator() + item.remoteId(); + const QString fileName = directoryForCollection(item.parentCollection()) + QLatin1Char('/') + item.remoteId(); if (item.hasPayload()) { const KContacts::Addressee contact = item.payload(); KContacts::VCardConverter converter; const QByteArray content = converter.createVCard(contact); QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString())); return; } file.write(content); file.close(); newItem.setRemoteId(item.remoteId()); } else if (item.hasPayload()) { const KContacts::ContactGroup group = item.payload(); QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString())); return; } KContacts::ContactGroupTool::convertToXml(group, &file); file.close(); newItem.setRemoteId(item.remoteId()); } else { cancelTask(i18n("Received item with unknown payload %1", item.mimeType())); return; } changeCommitted(newItem); } void ContactsResource::itemRemoved(const Akonadi::Item &item) { if (ContactsResourceSettings::self()->readOnly()) { cancelTask(i18n("Trying to write to a read-only file: '%1'", item.remoteId())); return; } // If the parent collection has no valid remote id, the parent // collection will be removed in a second, so stop here and remove // all items in collectionRemoved(). if (item.parentCollection().remoteId().isEmpty()) { changeProcessed(); return; } - const QString fileName = directoryForCollection(item.parentCollection()) + QDir::separator() + item.remoteId(); + const QString fileName = directoryForCollection(item.parentCollection()) + QLatin1Char('/') + item.remoteId(); if (!QFile::remove(fileName)) { cancelTask(i18n("Unable to remove file '%1'", fileName)); return; } changeProcessed(); } void ContactsResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) { if (ContactsResourceSettings::self()->readOnly()) { cancelTask(i18n("Trying to write to a read-only directory: '%1'", parent.remoteId())); return; } - const QString dirName = directoryForCollection(parent) + QDir::separator() + collection.name(); + const QString dirName = directoryForCollection(parent) + QLatin1Char('/') + collection.name(); if (!QDir::root().mkpath(dirName)) { cancelTask(i18n("Unable to create folder '%1'.", dirName)); return; } initializeDirectory(dirName); Collection newCollection(collection); newCollection.setRemoteId(collection.name()); changeCommitted(newCollection); } void ContactsResource::collectionChanged(const Akonadi::Collection &collection) { if (ContactsResourceSettings::self()->readOnly()) { cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId())); return; } if (collection.parentCollection() == Collection::root()) { if (collection.name() != name()) { setName(collection.name()); } changeProcessed(); return; } if (collection.remoteId() == collection.name()) { changeProcessed(); return; } const QString dirName = directoryForCollection(collection); QFileInfo oldDirectory(dirName); - if (!QDir::root().rename(dirName, oldDirectory.absolutePath() + QDir::separator() + collection.name())) { + if (!QDir::root().rename(dirName, oldDirectory.absolutePath() + QLatin1Char('/') + collection.name())) { cancelTask(i18n("Unable to rename folder '%1'.", collection.name())); return; } Collection newCollection(collection); newCollection.setRemoteId(collection.name()); changeCommitted(newCollection); } void ContactsResource::collectionRemoved(const Akonadi::Collection &collection) { if (ContactsResourceSettings::self()->readOnly()) { cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId())); return; } const QString collectionDir = directoryForCollection(collection); if (collectionDir.isEmpty()) { cancelTask(i18n("Unknown folder to delete.")); return; } if (!QDir(collectionDir).removeRecursively()) { cancelTask(i18n("Unable to delete folder '%1'.", collection.name())); return; } changeProcessed(); } void ContactsResource::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) { - const QString sourceFileName = directoryForCollection(collectionSource) + QDir::separator() + item.remoteId(); - const QString targetFileName = directoryForCollection(collectionDestination) + QDir::separator() + item.remoteId(); + const QString sourceFileName = directoryForCollection(collectionSource) + QLatin1Char('/') + item.remoteId(); + const QString targetFileName = directoryForCollection(collectionDestination) + QLatin1Char('/') + item.remoteId(); if (QFile::rename(sourceFileName, targetFileName)) { changeProcessed(); } else { cancelTask(i18n("Unable to move file '%1' to '%2', '%2' already exists.", sourceFileName, targetFileName)); } } void ContactsResource::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) { - const QString sourceDirectoryName = directoryForCollection(collectionSource) + QDir::separator() + collection.remoteId(); - const QString targetDirectoryName = directoryForCollection(collectionDestination) + QDir::separator() + collection.remoteId(); + const QString sourceDirectoryName = directoryForCollection(collectionSource) + QLatin1Char('/') + collection.remoteId(); + const QString targetDirectoryName = directoryForCollection(collectionDestination) + QLatin1Char('/') + collection.remoteId(); if (QFile::rename(sourceDirectoryName, targetDirectoryName)) { changeProcessed(); } else { cancelTask(i18n("Unable to move directory '%1' to '%2', '%2' already exists.", sourceDirectoryName, targetDirectoryName)); } } QString ContactsResource::baseDirectoryPath() const { return ContactsResourceSettings::self()->path(); } void ContactsResource::initializeDirectory(const QString &path) const { QDir dir(path); // if folder does not exists, create it QDir::root().mkpath(dir.absolutePath()); // check whether warning file is in place... - QFile file(dir.absolutePath() + QDir::separator() + QLatin1String("WARNING_README.txt")); + QFile file(dir.absolutePath() + QStringLiteral("/WARNING_README.txt")); if (!file.exists()) { // ... if not, create it file.open(QIODevice::WriteOnly); file.write("Important Warning!!!\n\n" "Don't create or copy vCards inside this folder manually, they are managed by the Akonadi framework!\n"); file.close(); } } Collection::Rights ContactsResource::supportedRights(bool isResourceCollection) const { Collection::Rights rights = Collection::ReadOnly; if (!ContactsResourceSettings::self()->readOnly()) { rights |= Collection::CanChangeItem; rights |= Collection::CanCreateItem; rights |= Collection::CanDeleteItem; rights |= Collection::CanCreateCollection; rights |= Collection::CanChangeCollection; if (!isResourceCollection) { rights |= Collection::CanDeleteCollection; } } return rights; } QString ContactsResource::directoryForCollection(const Collection &collection) const { if (collection.remoteId().isEmpty()) { qCWarning(CONTACTSRESOURCES_LOG) << "Got incomplete ancestor chain:" << collection; return QString(); } if (collection.parentCollection() == Collection::root()) { if (collection.remoteId() != baseDirectoryPath()) { qCWarning(CONTACTSRESOURCES_LOG) << "RID mismatch, is " << collection.remoteId() << " expected " << baseDirectoryPath(); } return collection.remoteId(); } const QString parentDirectory = directoryForCollection(collection.parentCollection()); if (parentDirectory.isNull()) { // invalid, != isEmpty() here! return QString(); } QString directory = parentDirectory; if (!directory.endsWith(QLatin1Char('/'))) { - directory += QDir::separator() + collection.remoteId(); + directory += QLatin1Char('/') + collection.remoteId(); } else { directory += collection.remoteId(); } return directory; } AKONADI_RESOURCE_MAIN(ContactsResource) diff --git a/resources/icaldir/icaldirresource.cpp b/resources/icaldir/icaldirresource.cpp index ba612a6f1..a7511907a 100644 --- a/resources/icaldir/icaldirresource.cpp +++ b/resources/icaldir/icaldirresource.cpp @@ -1,317 +1,317 @@ /* Copyright (c) 2008 Tobias Koenig Copyright (c) 2008 Bertjan Broeksema Copyright (c) 2012 Sérgio Martins 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 "icaldirresource.h" #include "settingsadaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KCalendarCore; static Incidence::Ptr readFromFile(const QString &fileName, const QString &expectedIdentifier) { MemoryCalendar::Ptr calendar = MemoryCalendar::Ptr(new MemoryCalendar(QTimeZone::utc())); FileStorage::Ptr fileStorage = FileStorage::Ptr(new FileStorage(calendar, fileName, new ICalFormat())); Incidence::Ptr incidence; if (fileStorage->load()) { Incidence::List incidences = calendar->incidences(); if (incidences.count() == 1 && incidences.first()->instanceIdentifier() == expectedIdentifier) { incidence = incidences.first(); } } else { qCritical() << "Error loading file " << fileName; } return incidence; } static bool writeToFile(const QString &fileName, Incidence::Ptr &incidence) { if (!incidence) { qCritical() << "incidence is 0!"; return false; } MemoryCalendar::Ptr calendar = MemoryCalendar::Ptr(new MemoryCalendar(QTimeZone::utc())); FileStorage::Ptr fileStorage = FileStorage::Ptr(new FileStorage(calendar, fileName, new ICalFormat())); calendar->addIncidence(incidence); Q_ASSERT(calendar->incidences().count() == 1); const bool success = fileStorage->save(); if (!success) { qCritical() << QStringLiteral("Failed to save calendar to file ") + fileName; } return success; } ICalDirResource::ICalDirResource(const QString &id) : ResourceBase(id) { IcalDirResourceSettings::instance(KSharedConfig::openConfig()); // setup the resource new SettingsAdaptor(IcalDirResourceSettings::self()); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), IcalDirResourceSettings::self(), QDBusConnection::ExportAdaptors); changeRecorder()->itemFetchScope().fetchFullPayload(); connect(this, &ICalDirResource::reloadConfiguration, this, &ICalDirResource::slotReloadConfig); } ICalDirResource::~ICalDirResource() { } void ICalDirResource::slotReloadConfig() { initializeICalDirectory(); loadIncidences(); synchronize(); } void ICalDirResource::aboutToQuit() { IcalDirResourceSettings::self()->save(); } bool ICalDirResource::loadIncidences() { mIncidences.clear(); QDirIterator it(iCalDirectoryName()); while (it.hasNext()) { it.next(); if (it.fileName() != QLatin1String(".") && it.fileName() != QLatin1String("..") && it.fileName() != QLatin1String("WARNING_README.txt")) { const KCalendarCore::Incidence::Ptr incidence = readFromFile(it.filePath(), it.fileName()); if (incidence) { mIncidences.insert(incidence->instanceIdentifier(), incidence); } } } Q_EMIT status(Idle); return true; } bool ICalDirResource::retrieveItem(const Akonadi::Item &item, const QSet &) { const QString remoteId = item.remoteId(); if (!mIncidences.contains(remoteId)) { Q_EMIT error(i18n("Incidence with uid '%1' not found.", remoteId)); return false; } Item newItem(item); newItem.setPayload(mIncidences.value(remoteId)); itemRetrieved(newItem); return true; } void ICalDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &) { if (IcalDirResourceSettings::self()->readOnly()) { Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", iCalDirectoryName())); cancelTask(); return; } KCalendarCore::Incidence::Ptr incidence; if (item.hasPayload()) { incidence = item.payload(); } if (incidence) { // add it to the cache... mIncidences.insert(incidence->instanceIdentifier(), incidence); // ... and write it through to the file system const bool success = writeToFile(iCalDirectoryFileName(incidence->instanceIdentifier()), incidence); if (success) { // report everything ok Item newItem(item); newItem.setRemoteId(incidence->instanceIdentifier()); changeCommitted(newItem); } else { cancelTask(); } } else { changeProcessed(); } } void ICalDirResource::itemChanged(const Akonadi::Item &item, const QSet &) { if (IcalDirResourceSettings::self()->readOnly()) { Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", iCalDirectoryName())); cancelTask(); return; } KCalendarCore::Incidence::Ptr incidence; if (item.hasPayload()) { incidence = item.payload(); } if (incidence) { // change it in the cache... mIncidences.insert(incidence->instanceIdentifier(), incidence); // ... and write it through to the file system const bool success = writeToFile(iCalDirectoryFileName(incidence->instanceIdentifier()), incidence); if (success) { Item newItem(item); newItem.setRemoteId(incidence->instanceIdentifier()); changeCommitted(newItem); } else { cancelTask(); } } else { changeProcessed(); } } void ICalDirResource::itemRemoved(const Akonadi::Item &item) { if (IcalDirResourceSettings::self()->readOnly()) { Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", iCalDirectoryName())); cancelTask(); return; } // remove it from the cache... mIncidences.remove(item.remoteId()); // ... and remove it from the file system QFile::remove(iCalDirectoryFileName(item.remoteId())); changeProcessed(); } void ICalDirResource::collectionChanged(const Collection &collection) { if (collection.hasAttribute()) { auto attr = collection.attribute(); if (attr->displayName() != name()) { setName(attr->displayName()); } } changeProcessed(); } void ICalDirResource::retrieveCollections() { Collection c; c.setParentCollection(Collection::root()); c.setRemoteId(iCalDirectoryName()); c.setName(name()); QStringList mimetypes; mimetypes << KCalendarCore::Event::eventMimeType() << KCalendarCore::Todo::todoMimeType() << KCalendarCore::Journal::journalMimeType() << QStringLiteral("text/calendar"); c.setContentMimeTypes(mimetypes); if (IcalDirResourceSettings::self()->readOnly()) { c.setRights(Collection::CanChangeCollection); } else { Collection::Rights rights = Collection::ReadOnly; rights |= Collection::CanChangeItem; rights |= Collection::CanCreateItem; rights |= Collection::CanDeleteItem; rights |= Collection::CanChangeCollection; c.setRights(rights); } EntityDisplayAttribute *attr = c.attribute(Collection::AddIfMissing); attr->setDisplayName(name() == identifier() ? i18n("Calendar Folder") : name()); attr->setIconName(QStringLiteral("office-calendar")); Collection::List list; list << c; collectionsRetrieved(list); } void ICalDirResource::retrieveItems(const Akonadi::Collection &) { loadIncidences(); Item::List items; items.reserve(mIncidences.count()); for (const KCalendarCore::Incidence::Ptr &incidence : qAsConst(mIncidences)) { Item item; item.setRemoteId(incidence->instanceIdentifier()); item.setMimeType(incidence->mimeType()); items.append(item); } itemsRetrieved(items); } QString ICalDirResource::iCalDirectoryName() const { return IcalDirResourceSettings::self()->path(); } QString ICalDirResource::iCalDirectoryFileName(const QString &file) const { - return IcalDirResourceSettings::self()->path() + QDir::separator() + file; + return IcalDirResourceSettings::self()->path() + QLatin1Char('/') + file; } void ICalDirResource::initializeICalDirectory() const { QDir dir(iCalDirectoryName()); // if folder does not exists, create it if (!dir.exists()) { QDir::root().mkpath(dir.absolutePath()); } // check whether warning file is in place... - QFile file(dir.absolutePath() + QDir::separator() + QStringLiteral("WARNING_README.txt")); + QFile file(dir.absolutePath() + QStringLiteral("/WARNING_README.txt")); if (!file.exists()) { // ... if not, create it file.open(QIODevice::WriteOnly); file.write("Important Warning!!!\n\n" "Don't create or copy files inside this folder manually, they are managed by the Akonadi framework!\n"); file.close(); } } AKONADI_RESOURCE_MAIN(ICalDirResource) diff --git a/resources/kalarm/kalarmdir/kalarmdirresource.cpp b/resources/kalarm/kalarmdir/kalarmdirresource.cpp index 095718b17..67df12ce7 100644 --- a/resources/kalarm/kalarmdir/kalarmdirresource.cpp +++ b/resources/kalarm/kalarmdir/kalarmdirresource.cpp @@ -1,1176 +1,1176 @@ /* * kalarmdirresource.cpp - Akonadi directory resource for KAlarm * Program: kalarm * Copyright © 2011-2019 David Jarvie * Copyright (c) 2008 Tobias Koenig * Copyright (c) 2008 Bertjan Broeksema * * 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 "kalarmdirresource.h" #include "kalarmresourcecommon.h" #include "autoqpointer.h" #include "kalarmdirsettingsadaptor.h" #include "settingsdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarmdirresource_debug.h" using namespace Akonadi; using namespace KCalendarCore; using namespace Akonadi_KAlarm_Dir_Resource; using KAlarmResourceCommon::errorMessage; static const char warningFile[] = "WARNING_README.txt"; #define DEBUG_DATA(func) \ qCDebug(KALARMDIRRESOURCE_LOG)<itemFetchScope().fetchFullPayload(); changeRecorder()->fetchCollection(true); connect(KDirWatch::self(), &KDirWatch::created, this, &KAlarmDirResource::fileCreated); connect(KDirWatch::self(), &KDirWatch::dirty, this, &KAlarmDirResource::fileChanged); connect(KDirWatch::self(), &KDirWatch::deleted, this, &KAlarmDirResource::fileDeleted); // Find the collection which this resource manages CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(identifier()); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::collectionFetchResult); QTimer::singleShot(0, this, [this] { loadFiles(); }); } KAlarmDirResource::~KAlarmDirResource() { delete mSettings; } void KAlarmDirResource::aboutToQuit() { mSettings->save(); } /****************************************************************************** * Called when the collection fetch job completes. * Check the calendar files' compatibility statuses if pending. */ void KAlarmDirResource::collectionFetchResult(KJob *j) { qCDebug(KALARMDIRRESOURCE_LOG); if (j->error()) { qCritical() << "Error: collectionFetchResult: " << j->errorString(); } else { CollectionFetchJob *job = static_cast(j); Collection::List collections = job->collections(); int count = collections.count(); qCDebug(KALARMDIRRESOURCE_LOG) << "collectionFetchResult: count:" << count; if (!count) { qCritical() << "Cannot retrieve this resource's collection"; } else { if (count > 1) { qCritical() << "Multiple collections for this resource:" << count; } Collection &c(collections[0]); qCDebug(KALARMDIRRESOURCE_LOG) << "collectionFetchResult: id:" << c.id() << ", remote id:" << c.remoteId(); if (!mCollectionFetched) { bool recreate = mSettings->path().isEmpty(); if (!recreate) { // Remote ID could be path or URL, depending on which version // of Akonadi created it. const QString rid = c.remoteId(); const QUrl url = QUrl::fromLocalFile(mSettings->path()); if (!url.isLocalFile() || (rid != url.toLocalFile() && rid != url.url() && rid != url.toDisplayString())) { qCritical() << "Collection remote ID does not match settings: changing settings"; recreate = true; } } if (recreate) { // Initialising a resource which seems to have no stored // settings config file. Recreate the settings. static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; qCDebug(KALARMDIRRESOURCE_LOG) << "collectionFetchResult: Recreating config for remote id:" << c.remoteId(); mSettings->setPath(c.remoteId()); mSettings->setDisplayName(c.name()); mSettings->setAlarmTypes(c.contentMimeTypes()); mSettings->setReadOnly((c.rights() & writableRights) != writableRights); mSettings->save(); } mCollectionId = c.id(); if (recreate) { // Load items from the backend files now that their location is known loadFiles(true); } // Set collection's format compatibility flag now that the collection // and its attributes have been fetched. KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion); } } } mCollectionFetched = true; if (mWaitingToRetrieve) { mWaitingToRetrieve = false; retrieveCollections(); } } /****************************************************************************** */ void KAlarmDirResource::configure(WId windowId) { qCDebug(KALARMDIRRESOURCE_LOG) << "configure"; // Keep note of the old configuration settings QString path = mSettings->path(); QString name = mSettings->displayName(); bool readOnly = mSettings->readOnly(); QStringList types = mSettings->alarmTypes(); // Note: mSettings->monitorFiles() can't change here // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer dlg = new SettingsDialog(windowId, mSettings); if (dlg->exec()) { if (path.isEmpty()) { // Creating a new resource clearCache(); // this deletes any existing collection loadFiles(true); synchronizeCollectionTree(); } else if (mSettings->path() != path) { // Directory path change is not allowed for existing resources Q_EMIT configurationDialogRejected(); return; } else { bool modify = false; Collection c(mCollectionId); if (mSettings->alarmTypes() != types) { // Settings have changed which might affect the alarm configuration initializeDirectory(); // should only be needed for new resource, but just in case ... CalEvent::Types newTypes = CalEvent::types(mSettings->alarmTypes()); CalEvent::Types oldTypes = CalEvent::types(types); changeAlarmTypes(~newTypes & oldTypes); c.setContentMimeTypes(mSettings->alarmTypes()); modify = true; } if (mSettings->readOnly() != readOnly || mSettings->displayName() != name) { // Need to change the collection's rights or name c.setRemoteId(directoryName()); setNameRights(c); modify = true; } if (modify) { // Update the Akonadi server with the changes CollectionModifyJob *job = new CollectionModifyJob(c); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); } } Q_EMIT configurationDialogAccepted(); } else { Q_EMIT configurationDialogRejected(); } } /****************************************************************************** * Add/remove events to ensure that they match the changed alarm types for the * resource. */ void KAlarmDirResource::changeAlarmTypes(CalEvent::Types removed) { DEBUG_DATA("changeAlarmTypes:"); const QString dirPath = directoryName(); qCDebug(KALARMDIRRESOURCE_LOG) << "changeAlarmTypes:" << dirPath; const QDir dir(dirPath); // Read and parse each file in turn QDirIterator it(dir); while (it.hasNext()) { it.next(); int removeIfInvalid = 0; QString fileEventId; const QString file = it.fileName(); if (!isFileValid(file)) { continue; } QHash::iterator fit = mFileEventIds.find(file); if (fit != mFileEventIds.end()) { // The file is in the existing file list fileEventId = fit.value(); QHash::ConstIterator it = mEvents.constFind(fileEventId); if (it != mEvents.constEnd()) { // And its event is in the existing events list const EventFile &data = it.value(); if (data.files[0] == file) { // It's the file for a used event if (data.event.category() & removed) { // The event's type is no longer wanted, so remove it deleteItem(data.event); removeEvent(data.event.id(), false); } continue; } else { // The file's event is not currently used - load the // file and use its event if appropriate. removeIfInvalid = 0x03; // remove from mEvents and mFileEventIds } } else { // The file's event isn't in the list of current valid // events - this shouldn't ever happen removeIfInvalid = 0x01; // remove from mFileEventIds } } // Load the file and use its event if appropriate. const QString path = filePath(file); if (QFileInfo(path).isFile()) { if (createItemAndIndex(path, file)) { continue; } } // The event wasn't wanted, so remove from lists if (removeIfInvalid & 0x01) { mFileEventIds.erase(fit); } if (removeIfInvalid & 0x02) { removeEventFile(fileEventId, file); } } DEBUG_DATA("changeAlarmTypes exit:"); setCompatibility(); } /****************************************************************************** * Called when the resource settings have changed. * Update the display name if it has changed. * Stop monitoring the directory if 'monitorFiles' is now false. * Update the storage format if UpdateStorageFormat setting = true. * NOTE: no provision is made for changes to the directory path, since this is * not permitted (would need remote ID changed, plus other complications). */ void KAlarmDirResource::settingsChanged() { qCDebug(KALARMDIRRESOURCE_LOG) << "settingsChanged"; const QString display = mSettings->displayName(); if (display != name()) { setName(display); } const QString dirPath = mSettings->path(); if (!dirPath.isEmpty()) { const bool monitoring = KDirWatch::self()->contains(dirPath); if (monitoring && !mSettings->monitorFiles()) { KDirWatch::self()->removeDir(dirPath); } else if (!monitoring && mSettings->monitorFiles()) { KDirWatch::self()->addDir(dirPath, KDirWatch::WatchFiles); } #if 0 if (mSettings->monitorFiles() && !monitor) { // Settings have changed which might affect the alarm configuration qCDebug(KALARMDIRRESOURCE_LOG) << "Monitored changed"; loadFiles(true); // synchronizeCollectionTree(); } #endif } if (mSettings->updateStorageFormat()) { // This is a flag to request that the backend calendar storage format should // be updated to the current KAlarm format. KACalendar::Compat okCompat(KACalendar::Current | KACalendar::Convertible); if (mCompatibility & ~okCompat) { qCWarning(KALARMDIRRESOURCE_LOG) << "Either incompatible storage format or nothing to update"; } else if (mSettings->readOnly()) { qCWarning(KALARMDIRRESOURCE_LOG) << "Cannot update storage format for a read-only resource"; } else { // Update the backend storage format to the current KAlarm format bool ok = true; for (QHash::iterator it = mEvents.begin(); it != mEvents.end(); ++it) { KAEvent &event = it.value().event; if (event.compatibility() == KACalendar::Convertible) { if (writeToFile(event)) { event.setCompatibility(KACalendar::Current); } else { qCWarning(KALARMDIRRESOURCE_LOG) << "Error updating storage format for event id" << event.id(); ok = false; } } } if (ok) { mCompatibility = KACalendar::Current; mVersion = KACalendar::CurrentFormat; const Collection c(mCollectionId); if (c.isValid()) { KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion); } } } mSettings->setUpdateStorageFormat(false); mSettings->save(); } } /****************************************************************************** * Load and parse data from each file in the directory. * The events are cached in mEvents. */ bool KAlarmDirResource::loadFiles(bool sync) { const QString dirPath = directoryName(); if (dirPath.isEmpty()) { return false; } qCDebug(KALARMDIRRESOURCE_LOG) << "loadFiles:" << dirPath; const QDir dir(dirPath); // Create the directory if it doesn't exist. // This should only be needed for a new resource, but just in case ... initializeDirectory(); mEvents.clear(); mFileEventIds.clear(); // Set the resource display name to the configured name, else the directory // name, if not already set. QString display = mSettings->displayName(); if (display.isEmpty() && (name().isEmpty() || name() == identifier())) { display = dir.dirName(); } if (!display.isEmpty()) { setName(display); } // Read and parse each file in turn QDirIterator it(dir); while (it.hasNext()) { it.next(); const QString file = it.fileName(); if (isFileValid(file)) { const QString path = filePath(file); if (QFileInfo(path).isFile()) { const KAEvent event = loadFile(path, file); if (event.isValid()) { addEventFile(event, file); mFileEventIds.insert(file, event.id()); } } } } DEBUG_DATA("loadFiles:"); setCompatibility(false); // don't write compatibility - no collection exists yet if (mSettings->monitorFiles()) { // Monitor the directory for changes to the files if (!KDirWatch::self()->contains(dirPath)) { KDirWatch::self()->addDir(dirPath, KDirWatch::WatchFiles); } } if (sync) { // Ensure the Akonadi server is updated with the current list of events synchronize(); } Q_EMIT status(Idle); return true; } /****************************************************************************** * Load and parse data a single file in the directory. * 'path' is the full path of 'file'. * 'file' should not contain any directory component. */ KAEvent KAlarmDirResource::loadFile(const QString &path, const QString &file) { qCDebug(KALARMDIRRESOURCE_LOG) << "loadFile:" << path; MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::utc())); FileStorage::Ptr fileStorage(new FileStorage(calendar, path, new ICalFormat())); if (!fileStorage->load()) { // Don't output an error in the case of the creation of a temporary // file which triggered fileChanged() but no longer exists. if (QFileInfo::exists(path)) { qCWarning(KALARMDIRRESOURCE_LOG) << "Error loading" << path; } return KAEvent(); } const Event::List events = calendar->events(); if (events.isEmpty()) { qCDebug(KALARMDIRRESOURCE_LOG) << "Empty calendar in file" << path; return KAEvent(); } if (events.count() > 1) { qCWarning(KALARMDIRRESOURCE_LOG) << "Deleting" << events.count() - 1 << "excess events found in file" << path; for (int i = 1; i < events.count(); ++i) { calendar->deleteEvent(events[i]); } } const Event::Ptr kcalEvent(events[0]); if (kcalEvent->uid() != file) { qCWarning(KALARMDIRRESOURCE_LOG) << "File" << path << ": event id differs from file name"; } if (kcalEvent->alarms().isEmpty()) { qCWarning(KALARMDIRRESOURCE_LOG) << "File" << path << ": event contains no alarms"; return KAEvent(); } // Convert event in memory to current KAlarm format if possible int version; KACalendar::Compat compat = KAlarmResourceCommon::getCompatibility(fileStorage, version); KAEvent event(kcalEvent); const QString mime = CalEvent::mimeType(event.category()); if (mime.isEmpty()) { qCWarning(KALARMDIRRESOURCE_LOG) << "loadFile: KAEvent has no usable alarms:" << event.id(); return KAEvent(); } if (!mSettings->alarmTypes().contains(mime)) { qCWarning(KALARMDIRRESOURCE_LOG) << "loadFile: KAEvent has wrong alarm type for resource:" << mime; return KAEvent(); } event.setCompatibility(compat); return event; } /****************************************************************************** * After a file/event has been removed, load the next file in the list for the * event ID. * Reply = new event, or invalid if none. */ KAEvent KAlarmDirResource::loadNextFile(const QString &eventId, const QString &file) { QString nextFile = file; while (!nextFile.isEmpty()) { // There is another file with the same ID - load it const KAEvent event = loadFile(filePath(nextFile), nextFile); if (event.isValid()) { addEventFile(event, nextFile); mFileEventIds.insert(nextFile, event.id()); return event; } mFileEventIds.remove(nextFile); nextFile = removeEventFile(eventId, nextFile); } return KAEvent(); } /****************************************************************************** * Retrieve an event from the calendar, whose uid and Akonadi id are given by * 'item' (item.remoteId() and item.id() respectively). * Set the event into a new item's payload, and signal its retrieval by calling * itemRetrieved(newitem). */ bool KAlarmDirResource::retrieveItem(const Akonadi::Item &item, const QSet &) { const QString rid = item.remoteId(); QHash::ConstIterator it = mEvents.constFind(rid); if (it == mEvents.constEnd()) { qCWarning(KALARMDIRRESOURCE_LOG) << "retrieveItem: Event not found:" << rid; Q_EMIT error(errorMessage(KAlarmResourceCommon::UidNotFound, rid)); return false; } KAEvent event(it.value().event); const Item newItem = KAlarmResourceCommon::retrieveItem(item, event); itemRetrieved(newItem); return true; } /****************************************************************************** * Called when an item has been added to the collection. * Store the event in a file, and set its Akonadi remote ID to the KAEvent's UID. */ void KAlarmDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &) { qCDebug(KALARMDIRRESOURCE_LOG) << "itemAdded:" << item.id(); if (cancelIfReadOnly()) { return; } KAEvent event; if (item.hasPayload()) { event = item.payload(); } if (!event.isValid()) { changeProcessed(); return; } event.setCompatibility(KACalendar::Current); setCompatibility(); if (!writeToFile(event)) { return; } addEventFile(event, event.id()); Item newItem(item); newItem.setRemoteId(event.id()); // scheduleWrite(); //???? is this needed? changeCommitted(newItem); } /****************************************************************************** * Called when an item has been changed. * Store the changed event in a file. */ void KAlarmDirResource::itemChanged(const Akonadi::Item &item, const QSet &) { qCDebug(KALARMDIRRESOURCE_LOG) << "itemChanged:" << item.id() << ", remote ID:" << item.remoteId(); if (cancelIfReadOnly()) { return; } QHash::iterator it = mEvents.find(item.remoteId()); if (it != mEvents.end()) { if (it.value().event.isReadOnly()) { qCWarning(KALARMDIRRESOURCE_LOG) << "Event is read only:" << item.remoteId(); cancelTask(errorMessage(KAlarmResourceCommon::EventReadOnly, item.remoteId())); return; } if (it.value().event.compatibility() != KACalendar::Current) { qCWarning(KALARMDIRRESOURCE_LOG) << "Event not in current format:" << item.remoteId(); cancelTask(errorMessage(KAlarmResourceCommon::EventNotCurrentFormat, item.remoteId())); return; } } KAEvent event; if (item.hasPayload()) { event = item.payload(); } if (!event.isValid()) { changeProcessed(); return; } #if 0 QString errorMsg; KAEvent event = KAlarmResourceCommon::checkItemChanged(item, errorMsg); if (!event.isValid()) { if (errorMsg.isEmpty()) { changeProcessed(); } else { cancelTask(errorMsg); } return; } #endif event.setCompatibility(KACalendar::Current); if (mCompatibility != KACalendar::Current) { setCompatibility(); } if (!writeToFile(event)) { return; } it.value().event = event; changeCommitted(item); } /****************************************************************************** * Called when an item has been deleted. * Delete the item's file. */ void KAlarmDirResource::itemRemoved(const Akonadi::Item &item) { qCDebug(KALARMDIRRESOURCE_LOG) << "itemRemoved:" << item.id(); if (cancelIfReadOnly()) { return; } removeEvent(item.remoteId(), true); setCompatibility(); changeProcessed(); } /****************************************************************************** * Remove an event from the indexes, and optionally delete its file. */ void KAlarmDirResource::removeEvent(const QString &eventId, bool deleteFile) { QString file = eventId; QString nextFile; QHash::iterator it = mEvents.find(eventId); if (it != mEvents.end()) { file = it.value().files[0]; nextFile = removeEventFile(eventId, file); mFileEventIds.remove(file); DEBUG_DATA("removeEvent:"); } if (deleteFile) { QFile::remove(filePath(file)); } loadNextFile(eventId, nextFile); // load any other file with the same event ID } /****************************************************************************** * If the resource is read-only, cancel the task andQ_EMIT an error. * Reply = true if cancelled. */ bool KAlarmDirResource::cancelIfReadOnly() { if (mSettings->readOnly()) { qCWarning(KALARMDIRRESOURCE_LOG) << "Calendar is read-only:" << directoryName(); Q_EMIT error(i18nc("@info", "Trying to write to a read-only calendar: '%1'", directoryName())); cancelTask(); return true; } return false; } /****************************************************************************** * Write an event to a file. The file name is the event's id. */ bool KAlarmDirResource::writeToFile(const KAEvent &event) { Event::Ptr kcalEvent(new Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_SET); MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::utc())); KACalendar::setKAlarmVersion(calendar); // set the KAlarm custom property if (!calendar->addIncidence(kcalEvent)) { qCritical() << "Error adding event with id" << event.id(); Q_EMIT error(errorMessage(KAlarmResourceCommon::CalendarAdd, event.id())); cancelTask(); return false; } mChangedFiles += event.id(); // suppress KDirWatch processing for this write const QString path = filePath(event.id()); qCDebug(KALARMDIRRESOURCE_LOG) << "writeToFile:" << event.id() << " File:" << path; FileStorage::Ptr fileStorage(new FileStorage(calendar, path, new ICalFormat())); if (!fileStorage->save()) { Q_EMIT error(i18nc("@info", "Failed to save event file: %1", path)); cancelTask(); return false; } return true; } /****************************************************************************** * Create the resource's collection. */ void KAlarmDirResource::retrieveCollections() { QString rid = mSettings->path(); if (!mCollectionFetched && rid.isEmpty()) { // The resource config seems to be missing. Execute this function // once the collection config has been set up. mWaitingToRetrieve = true; return; } qCDebug(KALARMDIRRESOURCE_LOG) << "retrieveCollections"; Collection c; c.setParentCollection(Collection::root()); c.setRemoteId(rid); c.setContentMimeTypes(mSettings->alarmTypes()); setNameRights(c); // Don't update CollectionAttribute here, since it hasn't yet been fetched // from Akonadi database. Collection::List list; list << c; collectionsRetrieved(list); } /****************************************************************************** * Set the collection's name and rights. * It is the caller's responsibility to notify the Akonadi server. */ void KAlarmDirResource::setNameRights(Collection &c) { qCDebug(KALARMDIRRESOURCE_LOG) << "setNameRights"; const QString display = mSettings->displayName(); c.setName(display.isEmpty() ? name() : display); EntityDisplayAttribute *attr = c.attribute(Collection::AddIfMissing); attr->setDisplayName(name()); attr->setIconName(QStringLiteral("kalarm")); if (mSettings->readOnly()) { c.setRights(Collection::CanChangeCollection); } else { Collection::Rights rights = Collection::ReadOnly; rights |= Collection::CanChangeItem; rights |= Collection::CanCreateItem; rights |= Collection::CanDeleteItem; rights |= Collection::CanChangeCollection; c.setRights(rights); } qCDebug(KALARMDIRRESOURCE_LOG) << "setNameRights: end"; } /****************************************************************************** * Retrieve all events from the directory, and set each into a new item's * payload. Items are identified by their remote IDs. The Akonadi ID is not * used. * Signal the retrieval of the items by calling itemsRetrieved(items), which * updates Akonadi with any changes to the items. itemsRetrieved() compares * the new and old items, matching them on the remoteId(). If the flags or * payload have changed, or the Item has any new Attributes, the Akonadi * storage is updated. */ void KAlarmDirResource::retrieveItems(const Akonadi::Collection &collection) { mCollectionId = collection.id(); // note the one and only collection for this resource qCDebug(KALARMDIRRESOURCE_LOG) << "retrieveItems: collection" << mCollectionId; // Set the collection's compatibility status KAlarmResourceCommon::setCollectionCompatibility(collection, mCompatibility, mVersion); // Fetch the list of valid mime types const QStringList mimeTypes = mSettings->alarmTypes(); // Retrieve events Item::List items; foreach (const EventFile &data, mEvents) { const KAEvent &event = data.event; const QString mime = CalEvent::mimeType(event.category()); if (mime.isEmpty()) { qCWarning(KALARMDIRRESOURCE_LOG) << "retrieveItems: KAEvent has no alarms:" << event.id(); continue; // event has no usable alarms } if (!mimeTypes.contains(mime)) { continue; // restrict alarms returned to the defined types } Item item(mime); item.setRemoteId(event.id()); item.setPayload(event); items.append(item); } itemsRetrieved(items); } /****************************************************************************** * Called when the collection has been changed. * Set its display name if that has changed. */ void KAlarmDirResource::collectionChanged(const Akonadi::Collection &collection) { qCDebug(KALARMDIRRESOURCE_LOG) << "collectionChanged"; // If the collection has a new display name, set the resource's display // name the same, and save to the settings. const QString newName = collection.displayName(); if (!newName.isEmpty() && newName != name()) { setName(newName); } if (newName != mSettings->displayName()) { mSettings->setDisplayName(newName); mSettings->save(); } changeCommitted(collection); } /****************************************************************************** * Called when a file has been created in the directory. */ void KAlarmDirResource::fileCreated(const QString &path) { qCDebug(KALARMDIRRESOURCE_LOG) << "fileCreated:" << path; if (path == directoryName()) { // The directory has been created. Load all files in it, and // tell the Akonadi server to create an Item for each event. loadFiles(true); foreach (const EventFile &data, mEvents) { createItem(data.event); } } else { const QString file = fileName(path); int i = mChangedFiles.indexOf(file); if (i >= 0) { mChangedFiles.removeAt(i); // the file was updated by this resource } else if (isFileValid(file)) { if (createItemAndIndex(path, file)) { setCompatibility(); } DEBUG_DATA("fileCreated:"); } } } /****************************************************************************** * Called when a file has changed in the directory. */ void KAlarmDirResource::fileChanged(const QString &path) { if (path != directoryName()) { qCDebug(KALARMDIRRESOURCE_LOG) << "fileChanged:" << path; const QString file = fileName(path); int i = mChangedFiles.indexOf(file); if (i >= 0) { mChangedFiles.removeAt(i); // the file was updated by this resource } else if (isFileValid(file)) { QString nextFile, oldId; KAEvent oldEvent; const KAEvent event = loadFile(path, file); // Get the file's old event ID QHash::iterator fit = mFileEventIds.find(file); if (fit != mFileEventIds.end()) { oldId = fit.value(); if (event.id() != oldId) { // The file's event ID has changed - remove the old event nextFile = removeEventFile(oldId, file, &oldEvent); if (event.isValid()) { fit.value() = event.id(); } else { mFileEventIds.erase(fit); } } } else { // The file didn't contain an event before. if (event.isValid()) { // Save details of the new event. mFileEventIds.insert(file, event.id()); } else { // The file still doesn't contain a recognised event. return; } } addEventFile(event, file); KAEvent e = loadNextFile(oldId, nextFile); // load any other file with the same event ID setCompatibility(); // Tell the Akonadi server to amend the Item for the event if (event.id() != oldId) { if (e.isValid()) { modifyItem(e); } else { deleteItem(oldEvent); } createItem(event); // create a new Item for the new event ID } else { modifyItem(event); } DEBUG_DATA("fileChanged:"); } } } /****************************************************************************** * Called when a file has been deleted in the directory. */ void KAlarmDirResource::fileDeleted(const QString &path) { qCDebug(KALARMDIRRESOURCE_LOG) << "fileDeleted:" << path; if (path == directoryName()) { // The directory has been deleted mEvents.clear(); mFileEventIds.clear(); // Tell the Akonadi server to delete all Items in the collection Collection c(mCollectionId); ItemDeleteJob *job = new ItemDeleteJob(c); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); } else { // A single file has been deleted const QString file = fileName(path); if (isFileValid(file)) { QHash::iterator fit = mFileEventIds.find(file); if (fit != mFileEventIds.end()) { QString eventId = fit.value(); KAEvent event; QString nextFile = removeEventFile(eventId, file, &event); mFileEventIds.erase(fit); KAEvent e = loadNextFile(eventId, nextFile); // load any other file with the same event ID setCompatibility(); if (e.isValid()) { // Tell the Akonadi server to amend the Item for the event modifyItem(e); } else { // Tell the Akonadi server to delete the Item for the event deleteItem(event); } DEBUG_DATA("fileDeleted:"); } } } } /****************************************************************************** * Tell the Akonadi server to create an Item for a given file's event, and add * it to the indexes. */ bool KAlarmDirResource::createItemAndIndex(const QString &path, const QString &file) { const KAEvent event = loadFile(path, file); if (event.isValid()) { // Tell the Akonadi server to create an Item for the event if (createItem(event)) { addEventFile(event, file); mFileEventIds.insert(file, event.id()); return true; } } return false; } /****************************************************************************** * Tell the Akonadi server to create an Item for a given event. */ bool KAlarmDirResource::createItem(const KAEvent &event) { Item item; if (!KAlarmCal::setItemPayload(item, event, mSettings->alarmTypes())) { qCWarning(KALARMDIRRESOURCE_LOG) << "createItem: Invalid mime type for collection"; return false; } Collection c(mCollectionId); item.setParentCollection(c); item.setRemoteId(event.id()); ItemCreateJob *job = new ItemCreateJob(item, c); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); return true; } /****************************************************************************** * Tell the Akonadi server to amend the Item for a given event. */ bool KAlarmDirResource::modifyItem(const KAEvent &event) { Item item; if (!KAlarmCal::setItemPayload(item, event, mSettings->alarmTypes())) { qCWarning(KALARMDIRRESOURCE_LOG) << "modifyItem: Invalid mime type for collection"; return false; } Collection c(mCollectionId); item.setParentCollection(c); item.setRemoteId(event.id()); ItemModifyJob *job = new ItemModifyJob(item); job->disableRevisionCheck(); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); return true; } /****************************************************************************** * Tell the Akonadi server to delete the Item for a given event. */ void KAlarmDirResource::deleteItem(const KAEvent &event) { Item item(CalEvent::mimeType(event.category())); Collection c(mCollectionId); item.setParentCollection(c); item.setRemoteId(event.id()); ItemDeleteJob *job = new ItemDeleteJob(item); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); } /****************************************************************************** * Called when a collection or item job has completed. * Checks for any error. */ void KAlarmDirResource::jobDone(KJob *j) { if (j->error()) { qCritical() << j->metaObject()->className() << "error:" << j->errorString(); } } /****************************************************************************** * Create the directory if it doesn't already exist, and ensure that it * contains a WARNING_README.txt file. */ void KAlarmDirResource::initializeDirectory() const { const QDir dir(directoryName()); const QString dirPath = dir.absolutePath(); qCDebug(KALARMDIRRESOURCE_LOG) << "initializeDirectory" << dirPath; // If folder does not exist, create it if (!dir.exists()) { qCDebug(KALARMDIRRESOURCE_LOG) << "initializeDirectory: creating" << dirPath; QDir::root().mkpath(dirPath); } // Check whether warning file is in place... - QFile file(dirPath + QDir::separator() + QLatin1String(warningFile)); + QFile file(dirPath + QLatin1Char('/') + QStringLiteral(warningFile)); if (!file.exists()) { // ... if not, create it file.open(QIODevice::WriteOnly); file.write("Important Warning!!!\n" "Do not create or copy items inside this folder manually:\n" "they are managed by the Akonadi framework!\n"); file.close(); } } QString KAlarmDirResource::directoryName() const { return mSettings->path(); } /****************************************************************************** * Return the full path of an event file. * 'file' should not contain any directory component. */ QString KAlarmDirResource::filePath(const QString &file) const { - return mSettings->path() + QDir::separator() + file; + return mSettings->path() + QLatin1Char('/') + file; } /****************************************************************************** * Strip the directory path from a file name. */ QString KAlarmDirResource::fileName(const QString &path) const { const QFileInfo fi(path); if (fi.isDir() || fi.isBundle()) { return QString(); } if (fi.path() == mSettings->path()) { return fi.fileName(); } return path; } /****************************************************************************** * Evaluate the version compatibility status of the calendar. This is the OR of * the statuses of the individual events. */ void KAlarmDirResource::setCompatibility(bool writeAttr) { static const KACalendar::Compat AllCompat(KACalendar::Current | KACalendar::Convertible | KACalendar::Incompatible); const KACalendar::Compat oldCompatibility = mCompatibility; const int oldVersion = mVersion; if (mEvents.isEmpty()) { mCompatibility = KACalendar::Current; } else { mCompatibility = KACalendar::Unknown; foreach (const EventFile &data, mEvents) { const KAEvent &event = data.event; mCompatibility |= event.compatibility(); if ((mCompatibility & AllCompat) == AllCompat) { break; } } } mVersion = (mCompatibility == KACalendar::Current) ? KACalendar::CurrentFormat : KACalendar::MixedFormat; if (writeAttr && (mCompatibility != oldCompatibility || mVersion != oldVersion)) { const Collection c(mCollectionId); if (c.isValid()) { KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion); } } } /****************************************************************************** * Add an event/file combination to the mEvents map. */ void KAlarmDirResource::addEventFile(const KAEvent &event, const QString &file) { if (event.isValid()) { QHash::iterator it = mEvents.find(event.id()); if (it != mEvents.end()) { EventFile &data = it.value(); data.event = event; data.files.removeAll(file); // in case it isn't the first file data.files.prepend(file); } else { mEvents.insert(event.id(), EventFile(event, QStringList(file))); } } } /****************************************************************************** * Remove an event ID/file combination from the mEvents map. * Reply = next file with the same event ID. */ QString KAlarmDirResource::removeEventFile(const QString &eventId, const QString &file, KAEvent *event) { QHash::iterator it = mEvents.find(eventId); if (it != mEvents.end()) { if (event) { *event = it.value().event; } it.value().files.removeAll(file); if (!it.value().files.isEmpty()) { return it.value().files[0]; } mEvents.erase(it); } else if (event) { *event = KAEvent(); } return QString(); } /****************************************************************************** * Check whether a file is to be ignored. * Reply = false if file is to be ignored. */ bool KAlarmDirResource::isFileValid(const QString &file) const { return !file.isEmpty() && !file.startsWith(QLatin1Char('.')) && !file.endsWith(QLatin1Char('~')) && file != QLatin1String(warningFile) && QFileInfo::exists(filePath(file)); // a temporary file may no longer exist } AKONADI_RESOURCE_MAIN(KAlarmDirResource) diff --git a/resources/maildir/libmaildir/maildir.cpp b/resources/maildir/libmaildir/maildir.cpp index 8a83c8566..fdbeb31b7 100644 --- a/resources/maildir/libmaildir/maildir.cpp +++ b/resources/maildir/libmaildir/maildir.cpp @@ -1,841 +1,841 @@ /* Copyright (c) 2007 Till Adam 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 "maildir.h" #include "keycache.h" #include #include #include #include #include #include #include #include "libmaildir_debug.h" #include #include #include //Define it to get more debug output to expense of operating speed // #define DEBUG_KEYCACHE_CONSITENCY using namespace KPIM; static QRegularExpression statusSeparatorRx() { static const QRegularExpression expr(QStringLiteral(":|!")); return expr; } class Maildir::Private { public: Private(const QString &p, bool isRoot) : path(p) , isRoot(isRoot) { hostName = QHostInfo::localHostName(); //Cache object is created the first time this runs. //It will live throughout the lifetime of the application KeyCache::self()->addKeys(path); } Private(const Private &rhs) { path = rhs.path; isRoot = rhs.isRoot; hostName = rhs.hostName; } bool operator==(const Private &rhs) const { return path == rhs.path; } bool accessIsPossible(bool createMissingFolders = true); bool canAccess(const QString &path) const; QStringList subPaths() const { QStringList paths; paths << path + QLatin1String("/cur"); paths << path + QLatin1String("/new"); paths << path + QLatin1String("/tmp"); return paths; } QStringList listNew() const { QDir d(path + QLatin1String("/new")); d.setSorting(QDir::NoSort); return d.entryList(QDir::Files); } QStringList listCurrent() const { QDir d(path + QLatin1String("/cur")); d.setSorting(QDir::NoSort); return d.entryList(QDir::Files); } QString findRealKey(const QString &key) const { KeyCache *keyCache = KeyCache::self(); if (keyCache->isNewKey(path, key)) { #ifdef DEBUG_KEYCACHE_CONSITENCY if (!QFile::exists(path + QString::fromLatin1("/new/") + key)) { qCDebug(LIBMAILDIR_LOG) << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/new/") + key; } #endif return path + QLatin1String("/new/") + key; } if (keyCache->isCurKey(path, key)) { #ifdef DEBUG_KEYCACHE_CONSITENCY if (!QFile::exists(path + QString::fromLatin1("/cur/") + key)) { qCDebug(LIBMAILDIR_LOG) << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/cur/") + key; } #endif return path + QLatin1String("/cur/") + key; } QString realKey = path + QLatin1String("/new/") + key; if (QFileInfo::exists(realKey)) { keyCache->addNewKey(path, key); } else { //not in "new", search in "cur" realKey = path + QLatin1String("/cur/") + key; if (QFileInfo::exists(realKey)) { keyCache->addCurKey(path, key); } else { realKey.clear(); //not in "cur" either } } return realKey; } static QString subDirNameForFolderName(const QString &folderName) { return QStringLiteral(".%1.directory").arg(folderName); } QString subDirPath() const { QDir dir(path); return subDirNameForFolderName(dir.dirName()); } bool moveAndRename(QDir &dest, const QString &newName) { if (!dest.exists()) { qCDebug(LIBMAILDIR_LOG) << "Destination does not exist"; return false; } if (dest.exists(newName) || dest.exists(subDirNameForFolderName(newName))) { qCDebug(LIBMAILDIR_LOG) << "New name already in use"; return false; } if (!dest.rename(path, newName)) { qCDebug(LIBMAILDIR_LOG) << "Failed to rename maildir"; return false; } const QDir subDirs(Maildir::subDirPathForFolderPath(path)); if (subDirs.exists() && !dest.rename(subDirs.path(), subDirNameForFolderName(newName))) { qCDebug(LIBMAILDIR_LOG) << "Failed to rename subfolders"; return false; } - path = dest.path() + QDir::separator() + newName; + path = dest.path() + QLatin1Char('/') + newName; return true; } QString path; QString hostName; QString lastError; bool isRoot; }; Maildir::Maildir(const QString &path, bool isRoot) : d(new Private(path, isRoot)) { } void Maildir::swap(const Maildir &rhs) { Private *p = d; d = new Private(*rhs.d); delete p; } Maildir::Maildir(const Maildir &rhs) : d(new Private(*rhs.d)) { } Maildir &Maildir::operator=(const Maildir &rhs) { // copy and swap, exception safe, and handles assignment to self Maildir temp(rhs); swap(temp); return *this; } bool Maildir::operator==(const Maildir &rhs) const { return *d == *rhs.d; } Maildir::~Maildir() { delete d; } bool Maildir::Private::canAccess(const QString &path) const { //return access( QFile::encodeName( path ), R_OK | W_OK | X_OK ) != 0; // FIXME X_OK? QFileInfo d(path); return d.isReadable() && d.isWritable(); } bool Maildir::Private::accessIsPossible(bool createMissingFolders) { QStringList paths = subPaths(); paths.prepend(path); for (const QString &p : qAsConst(paths)) { if (!QFileInfo::exists(p)) { if (!createMissingFolders) { lastError = i18n("Error opening %1; this folder is missing.", p); return false; } QDir().mkpath(p); if (!QFileInfo::exists(p)) { lastError = i18n("Error opening %1; this folder is missing.", p); return false; } } if (!canAccess(p)) { lastError = i18n("Error opening %1; either this is not a valid " "maildir folder, or you do not have sufficient access permissions.", p); return false; } } return true; } bool Maildir::isValid(bool createMissingFolders) const { if (path().isEmpty()) { return false; } if (!d->isRoot) { if (d->accessIsPossible(createMissingFolders)) { return true; } } else { const QStringList lstMaildir = subFolderList(); for (const QString &sf : lstMaildir) { const Maildir subMd = Maildir(path() + QLatin1Char('/') + sf); if (!subMd.isValid(createMissingFolders)) { d->lastError = subMd.lastError(); return false; } } return true; } return false; } bool Maildir::isRoot() const { return d->isRoot; } bool Maildir::create() { // FIXME: in a failure case, this will leave partially created dirs around // we should clean them up, but only if they didn't previously existed... const QStringList lstPath = d->subPaths(); for (const QString &p : lstPath) { QDir dir(p); if (!dir.exists(p)) { if (!dir.mkpath(p)) { return false; } } } return true; } QString Maildir::path() const { return d->path; } QString Maildir::name() const { const QDir dir(d->path); return dir.dirName(); } QString Maildir::addSubFolder(const QString &path) { if (!isValid()) { return QString(); } // make the subdir dir QDir dir(d->path); if (!d->isRoot) { dir.cdUp(); if (!dir.exists(d->subDirPath())) { dir.mkdir(d->subDirPath()); } dir.cd(d->subDirPath()); } const QString fullPath = dir.path() + QLatin1Char('/') + path; Maildir subdir(fullPath); if (subdir.create()) { return fullPath; } return QString(); } bool Maildir::removeSubFolder(const QString &folderName) { if (!isValid()) { return false; } QDir dir(d->path); if (!d->isRoot) { dir.cdUp(); if (!dir.exists(d->subDirPath())) { return false; } dir.cd(d->subDirPath()); } if (!dir.exists(folderName)) { return false; } // remove it recursively bool result = QDir(dir.absolutePath() + QLatin1Char('/') + folderName).removeRecursively(); QString subfolderName = subDirNameForFolderName(folderName); if (dir.exists(subfolderName)) { result &= QDir(dir.absolutePath() + QLatin1Char('/') + subfolderName).removeRecursively(); } return result; } Maildir Maildir::subFolder(const QString &subFolder) const { // make the subdir dir QDir dir(d->path); if (!d->isRoot) { dir.cdUp(); if (dir.exists(d->subDirPath())) { dir.cd(d->subDirPath()); } } return Maildir(dir.path() + QLatin1Char('/') + subFolder); } Maildir Maildir::parent() const { if (!isValid() || d->isRoot) { return Maildir(); } QDir dir(d->path); dir.cdUp(); if (!dir.dirName().startsWith(QLatin1Char('.')) || !dir.dirName().endsWith(QLatin1String(".directory"))) { return Maildir(); } const QString parentName = dir.dirName().mid(1, dir.dirName().size() - 11); dir.cdUp(); dir.cd(parentName); return Maildir(dir.path()); } QStringList Maildir::entryList() const { QStringList result; if (isValid()) { result = d->listNew() + d->listCurrent(); } // qCDebug(LIBMAILDIR_LOG) <<"Maildir::entryList()" << result; return result; } QStringList Maildir::listCurrent() const { QStringList result; if (isValid()) { result = d->listCurrent(); } return result; } QString Maildir::findRealKey(const QString &key) const { return d->findRealKey(key); } QStringList Maildir::listNew() const { QStringList result; if (isValid()) { result = d->listNew(); } return result; } QString Maildir::pathToNew() const { if (isValid()) { return d->path + QLatin1String("/new"); } return QString(); } QString Maildir::pathToCurrent() const { if (isValid()) { return d->path + QLatin1String("/cur"); } return QString(); } QString Maildir::subDirPath() const { QDir dir(d->path); dir.cdUp(); - return dir.path() + QDir::separator() + d->subDirPath(); + return dir.path() + QLatin1Char('/') + d->subDirPath(); } QStringList Maildir::subFolderList() const { QDir dir(d->path); // the root maildir has its subfolders directly beneath it if (!d->isRoot) { dir.cdUp(); if (!dir.exists(d->subDirPath())) { return QStringList(); } dir.cd(d->subDirPath()); } dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QStringList entries = dir.entryList(); entries.removeAll(QStringLiteral("cur")); entries.removeAll(QStringLiteral("new")); entries.removeAll(QStringLiteral("tmp")); return entries; } QByteArray Maildir::readEntry(const QString &key) const { QByteArray result; QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { // FIXME error handling? qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntry unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return result; } QFile f(realKey); if (!f.open(QIODevice::ReadOnly)) { d->lastError = i18n("Cannot open mail file %1.", realKey); return result; } // FIXME be safer than this result = f.readAll(); return result; } qint64 Maildir::size(const QString &key) const { QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { // FIXME error handling? qCWarning(LIBMAILDIR_LOG) << "Maildir::size unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return -1; } const QFileInfo info(realKey); if (!info.exists()) { d->lastError = i18n("Cannot open mail file %1.", realKey); return -1; } return info.size(); } QDateTime Maildir::lastModified(const QString &key) const { const QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { qCWarning(LIBMAILDIR_LOG) << "Maildir::lastModified unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return QDateTime(); } const QFileInfo info(realKey); if (!info.exists()) { return QDateTime(); } return info.lastModified(); } QByteArray Maildir::readEntryHeadersFromFile(const QString &file) const { QByteArray result; QFile f(file); if (!f.open(QIODevice::ReadOnly)) { // FIXME error handling? qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntryHeaders unable to find: " << file; d->lastError = i18n("Cannot locate mail file %1.", file); return result; } f.map(0, qMin((qint64)8000, f.size())); forever { QByteArray line = f.readLine(); if (line.isEmpty() || line.startsWith('\n')) { break; } result.append(line); } return result; } QByteArray Maildir::readEntryHeaders(const QString &key) const { const QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntryHeaders unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return QByteArray(); } return readEntryHeadersFromFile(realKey); } static QString createUniqueFileName() { const qint64 time = QDateTime::currentMSecsSinceEpoch(); const int r = QRandomGenerator::global()->bounded(1000); const QString identifier = QLatin1String("R") + QString::number(r); QString fileName = QString::number(time) + QLatin1Char('.') + identifier + QLatin1Char('.'); return fileName; } bool Maildir::writeEntry(const QString &key, const QByteArray &data) { QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { // FIXME error handling? qCWarning(LIBMAILDIR_LOG) << "Maildir::writeEntry unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return false; } QFile f(realKey); bool result = f.open(QIODevice::WriteOnly); result = result & (f.write(data) != -1); f.close(); if (!result) { d->lastError = i18n("Cannot write to mail file %1.", realKey); return false; } return true; } QString Maildir::addEntry(const QByteArray &data) { QString uniqueKey; QString key; QString finalKey; QString curKey; // QUuid doesn't return globally unique identifiers, therefor we query until we // get one that doesn't exists yet do { uniqueKey = createUniqueFileName() + d->hostName; key = d->path + QLatin1String("/tmp/") + uniqueKey; finalKey = d->path + QLatin1String("/new/") + uniqueKey; curKey = d->path + QLatin1String("/cur/") + uniqueKey; } while (QFile::exists(key) || QFile::exists(finalKey) || QFile::exists(curKey)); QFile f(key); bool result = f.open(QIODevice::WriteOnly); result = result & (f.write(data) != -1); f.close(); if (!result) { d->lastError = i18n("Cannot write to mail file %1.", key); return QString(); } /* * FIXME: * * The whole point of the locking free maildir idea is that the moves between * the internal directories are atomic. Afaik QFile::rename does not guarantee * that, so this will need to be done properly. - ta * * For reference: http://trolltech.com/developer/task-tracker/index_html?method=entry&id=211215 */ if (!f.rename(finalKey)) { qCWarning(LIBMAILDIR_LOG) << "Maildir: Failed to add entry: " << finalKey << "! Error: " << f.errorString(); d->lastError = i18n("Failed to create mail file %1. The error was: %2", finalKey, f.errorString()); return QString(); } KeyCache *keyCache = KeyCache::self(); keyCache->removeKey(d->path, key); //remove all keys, be it "cur" or "new" first keyCache->addNewKey(d->path, key); //and add a key for "new", as the mail was moved there return uniqueKey; } bool Maildir::removeEntry(const QString &key) { QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { qCWarning(LIBMAILDIR_LOG) << "Maildir::removeEntry unable to find: " << key; return false; } KeyCache *keyCache = KeyCache::self(); keyCache->removeKey(d->path, key); return QFile::remove(realKey); } QString Maildir::changeEntryFlags(const QString &key, const Akonadi::Item::Flags &flags) { QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { qCWarning(LIBMAILDIR_LOG) << "Maildir::changeEntryFlags unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return QString(); } const QRegularExpression rx = statusSeparatorRx(); QString finalKey = key.left(key.indexOf(rx)); QStringList mailDirFlags; for (const Akonadi::Item::Flag &flag : flags) { if (flag == Akonadi::MessageFlags::Forwarded) { mailDirFlags << QStringLiteral("P"); } else if (flag == Akonadi::MessageFlags::Replied) { mailDirFlags << QStringLiteral("R"); } else if (flag == Akonadi::MessageFlags::Seen) { mailDirFlags << QStringLiteral("S"); } else if (flag == Akonadi::MessageFlags::Deleted) { mailDirFlags << QStringLiteral("T"); } else if (flag == Akonadi::MessageFlags::Flagged) { mailDirFlags << QStringLiteral("F"); } } mailDirFlags.sort(); if (!mailDirFlags.isEmpty()) { #ifdef Q_OS_WIN finalKey.append(QLatin1String("!2,") + mailDirFlags.join(QString())); #else finalKey.append(QLatin1String(":2,") + mailDirFlags.join(QString())); #endif } const QString newUniqueKey = finalKey; //key without path finalKey.prepend(d->path + QLatin1String("/cur/")); if (realKey == finalKey) { // Somehow it already is named this way (e.g. migration bug -> wrong status in akonadi) return newUniqueKey; } QFile f(realKey); if (QFile::exists(finalKey)) { QFile destFile(finalKey); QByteArray destContent; if (destFile.open(QIODevice::ReadOnly)) { destContent = destFile.readAll(); destFile.close(); } QByteArray sourceContent; if (f.open(QIODevice::ReadOnly)) { sourceContent = f.readAll(); f.close(); } if (destContent != sourceContent) { QString newFinalKey = QLatin1String("1-") + newUniqueKey; int i = 1; const QString currentPath = d->path + QLatin1String("/cur/"); while (QFile::exists(currentPath + newFinalKey)) { i++; newFinalKey = QString::number(i) + QLatin1Char('-') + newUniqueKey; } finalKey = currentPath + newFinalKey; } else { QFile::remove(finalKey); //they are the same } } if (!f.rename(finalKey)) { qCWarning(LIBMAILDIR_LOG) << "Maildir: Failed to rename entry: " << f.fileName() << " to " << finalKey << "! Error: " << f.errorString(); d->lastError = i18n("Failed to update the file name %1 to %2 on the disk. The error was: %3.", f.fileName(), finalKey, f.errorString()); return QString(); } KeyCache *keyCache = KeyCache::self(); keyCache->removeKey(d->path, key); keyCache->addCurKey(d->path, newUniqueKey); return newUniqueKey; } Akonadi::Item::Flags Maildir::readEntryFlags(const QString &key) const { Akonadi::Item::Flags flags; const QRegularExpression rx = statusSeparatorRx(); const int index = key.indexOf(rx); if (index != -1) { const QStringRef mailDirFlags = key.midRef(index + 3); // after "(:|!)2," const int flagSize(mailDirFlags.size()); for (int i = 0; i < flagSize; ++i) { const QChar flag = mailDirFlags.at(i); if (flag == QLatin1Char('P')) { flags << Akonadi::MessageFlags::Forwarded; } else if (flag == QLatin1Char('R')) { flags << Akonadi::MessageFlags::Replied; } else if (flag == QLatin1Char('S')) { flags << Akonadi::MessageFlags::Seen; } else if (flag == QLatin1Char('F')) { flags << Akonadi::MessageFlags::Flagged; } } } return flags; } bool Maildir::moveTo(const Maildir &newParent) { if (d->isRoot) { return false; // not supported } QDir newDir(newParent.path()); if (!newParent.d->isRoot) { newDir.cdUp(); if (!newDir.exists(newParent.d->subDirPath())) { newDir.mkdir(newParent.d->subDirPath()); } newDir.cd(newParent.d->subDirPath()); } QDir currentDir(d->path); currentDir.cdUp(); if (newDir == currentDir) { return true; } return d->moveAndRename(newDir, name()); } bool Maildir::rename(const QString &newName) { if (name() == newName) { return true; } if (d->isRoot) { return false; // not (yet) supported } QDir dir(d->path); dir.cdUp(); return d->moveAndRename(dir, newName); } QString Maildir::moveEntryTo(const QString &key, const Maildir &destination) { const QString realKey(d->findRealKey(key)); if (realKey.isEmpty()) { qCWarning(LIBMAILDIR_LOG) << "Unable to find: " << key; d->lastError = i18n("Cannot locate mail file %1.", key); return QString(); } QFile f(realKey); // ### is this safe regarding the maildir locking scheme? - const QString targetKey = destination.path() + QDir::separator() + QLatin1String("new") + QDir::separator() + key; + const QString targetKey = destination.path() + QStringLiteral("/new/") + key; if (!f.rename(targetKey)) { qCDebug(LIBMAILDIR_LOG) << "Failed to rename" << realKey << "to" << targetKey << "! Error: " << f.errorString(); d->lastError = f.errorString(); return QString(); } KeyCache *keyCache = KeyCache::self(); keyCache->addNewKey(destination.path(), key); keyCache->removeKey(d->path, key); return key; } QString Maildir::subDirPathForFolderPath(const QString &folderPath) { QDir dir(folderPath); const QString dirName = dir.dirName(); dir.cdUp(); return QFileInfo(dir, Private::subDirNameForFolderName(dirName)).filePath(); } QString Maildir::subDirNameForFolderName(const QString &folderName) { return Private::subDirNameForFolderName(folderName); } void Maildir::removeCachedKeys(const QStringList &keys) { KeyCache *keyCache = KeyCache::self(); for (const QString &key : keys) { keyCache->removeKey(d->path, key); } } void Maildir::refreshKeyCache() { KeyCache::self()->refreshKeys(d->path); } QString Maildir::lastError() const { return d->lastError; } diff --git a/resources/maildir/maildirresource.cpp b/resources/maildir/maildirresource.cpp index c8b2ad0bf..2535dd90d 100644 --- a/resources/maildir/maildirresource.cpp +++ b/resources/maildir/maildirresource.cpp @@ -1,886 +1,886 @@ /* Copyright (c) 2007 Till Adam 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 "maildirresource.h" #include "settings.h" #include "maildirsettingsadaptor.h" #include "configdialog.h" #include "retrieveitemsjob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "maildirresource_debug.h" #include #include #include "libmaildir/maildir.h" #include using namespace Akonadi; using KPIM::Maildir; using namespace Akonadi_Maildir_Resource; #define CLEANER_TIMEOUT 2*6000 Maildir MaildirResource::maildirForCollection(const Collection &col) { const QString path = maildirPathForCollection(col); if (mMaildirsForCollection.contains(path)) { return mMaildirsForCollection.value(path); } if (col.remoteId().isEmpty()) { qCWarning(MAILDIRRESOURCE_LOG) << "Got incomplete ancestor chain:" << col; return Maildir(); } if (col.parentCollection() == Collection::root()) { if (col.remoteId() != mSettings->path()) { qCWarning(MAILDIRRESOURCE_LOG) << "RID mismatch, is " << col.remoteId() << " expected " << mSettings->path(); } Maildir maildir(col.remoteId(), mSettings->topLevelIsContainer()); mMaildirsForCollection.insert(path, maildir); return maildir; } Maildir parentMd = maildirForCollection(col.parentCollection()); Maildir maildir = parentMd.subFolder(col.remoteId()); mMaildirsForCollection.insert(path, maildir); return maildir; } Collection MaildirResource::collectionForMaildir(const Maildir &md) const { if (!md.isValid()) { return Collection(); } Collection col; if (md.path() == mSettings->path()) { col.setRemoteId(md.path()); col.setParentCollection(Collection::root()); } else { const Collection parent = collectionForMaildir(md.parent()); col.setRemoteId(md.name()); col.setParentCollection(parent); } return col; } MaildirResource::MaildirResource(const QString &id) : ResourceBase(id) , mSettings(new MaildirSettings(config())) , mFsWatcher(new KDirWatch(this)) { // we cannot be sure that a config file is existing // the MaildirResource will always be build // look for a resource of this name QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, id + QLatin1String("rc")); // if not present, create it if (configFile.isEmpty()) { // check if the resource was used before CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), Akonadi::CollectionFetchJob::FirstLevel, this); job->fetchScope().setResource(id); connect(job, &CollectionFetchJob::result, this, &MaildirResource::attemptConfigRestoring); job->start(); } new MaildirSettingsAdaptor(mSettings); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings, QDBusConnection::ExportAdaptors); connect(this, &MaildirResource::reloadConfiguration, this, &MaildirResource::configurationChanged); // We need to enable this here, otherwise we neither get the remote ID of the // parent collection when a collection changes, nor the full item when an item // is added. changeRecorder()->fetchCollection(true); changeRecorder()->itemFetchScope().fetchFullPayload(true); changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All); changeRecorder()->itemFetchScope().setFetchModificationTime(false); changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All); changeRecorder()->fetchChangedOnly(true); setHierarchicalRemoteIdentifiersEnabled(true); ItemFetchScope scope(changeRecorder()->itemFetchScope()); scope.fetchFullPayload(false); scope.fetchPayloadPart(MessagePart::Header); scope.setAncestorRetrieval(ItemFetchScope::None); setItemSynchronizationFetchScope(scope); connect(mFsWatcher, &KDirWatch::dirty, this, &MaildirResource::slotDirChanged); if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); } else { synchronizeCollectionTree(); } mChangedCleanerTimer = new QTimer(this); connect(mChangedCleanerTimer, &QTimer::timeout, this, &MaildirResource::changedCleaner); } void MaildirResource::attemptConfigRestoring(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << job->errorString(); return; } // we cannot be sure that a config file is existing const QString id = identifier(); const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, id + QLatin1String("rc")); // we test it again, to be sure if (configFile.isEmpty()) { // it is still empty, create it qCWarning(MAILDIRRESOURCE_LOG) << "the resource is not properly configured: there is no config file for the resource. We create a new one."; const Collection::List cols = qobject_cast(job)->collections(); QString path; if (!cols.isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "the collections list is not empty"; Collection col = cols.first(); // get the path of the collection path = col.remoteId(); } // test the path if (path.isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "build a new path"; const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/'); // we use "id" to get an unique path path = dataDir; if (!defaultResourceType().isEmpty()) { path += defaultResourceType() + QLatin1Char('/'); } path += id; qCDebug(MAILDIRRESOURCE_LOG) << "set the path" << path; mSettings->setPath(path); // set the resource into container mode for its top level mSettings->setTopLevelIsContainer(true); } else { // check how the directory looks like the actual check is missing. Maildir root(mSettings->path(), true); mSettings->setTopLevelIsContainer(root.isValid()); } qCDebug(MAILDIRRESOURCE_LOG) << "synchronize"; configurationChanged(); } } MaildirResource::~MaildirResource() { delete mSettings; } bool MaildirResource::retrieveItems(const Akonadi::Item::List &items, const QSet &parts) { Q_UNUSED(parts); const Maildir md = maildirForCollection(items.at(0).parentCollection()); if (!md.isValid()) { cancelTask(i18n("Unable to fetch item: The maildir folder \"%1\" is not valid.", md.path())); return false; } Akonadi::Item::List rv; rv.reserve(items.count()); for (const Akonadi::Item &item : items) { const QByteArray data = md.readEntry(item.remoteId()); KMime::Message *mail = new KMime::Message(); mail->setContent(KMime::CRLFtoLF(data)); mail->parse(); // Some messages may have an empty body if (mail->body().isEmpty()) { if (parts.contains("PLD:BODY") || parts.contains("PLD:RFC822")) { // 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 mail->setBody(" "); } } Item i(item); i.setPayload(KMime::Message::Ptr(mail)); Akonadi::MessageFlags::copyMessageFlags(*mail, i); rv.push_back(i); } itemsRetrieved(rv); return true; } QString MaildirResource::itemMimeType() const { return KMime::Message::mimeType(); } void MaildirResource::configurationChanged() { mSettings->save(); bool configValid = ensureSaneConfiguration(); configValid = configValid && ensureDirExists(); if (configValid) { Q_EMIT status(Idle); setOnline(true); } if (name().isEmpty() || name() == identifier()) { Maildir md(mSettings->path()); setName(md.name()); } synchronizeCollectionTree(); } void MaildirResource::aboutToQuit() { // The settings may not have been saved if e.g. they have been modified via // DBus instead of the config dialog. mSettings->save(); } QString MaildirResource::defaultResourceType() { return QString(); } void MaildirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } Maildir dir = maildirForCollection(collection); if (mSettings->readOnly() || !dir.isValid()) { cancelTask(dir.lastError()); return; } // we can only deal with mail if (!item.hasPayload()) { cancelTask(i18n("Error: Unsupported type.")); return; } const KMime::Message::Ptr mail = item.payload(); stopMaildirScan(dir); const QString rid = dir.addEntry(mail->encodedContent()); if (rid.isEmpty()) { restartMaildirScan(dir); cancelTask(dir.lastError()); return; } mChangedFiles.insert(rid); mChangedCleanerTimer->start(CLEANER_TIMEOUT); restartMaildirScan(dir); Item i(item); i.setRemoteId(rid); changeCommitted(i); } void MaildirResource::itemChanged(const Akonadi::Item &item, const QSet &parts) { if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } bool bodyChanged = false; bool flagsChanged = false; bool headChanged = false; for (const QByteArray &part : parts) { if (part.startsWith("PLD:RFC822")) { bodyChanged = true; } else if (part.startsWith("PLD:HEAD")) { headChanged = true; } if (part.contains("FLAGS")) { flagsChanged = true; } } if (mSettings->readOnly() || (!bodyChanged && !flagsChanged && !headChanged)) { changeProcessed(); return; } Maildir dir = maildirForCollection(item.parentCollection()); if (!dir.isValid()) { cancelTask(dir.lastError()); return; } Item newItem(item); if (flagsChanged || bodyChanged || headChanged) { //something has changed that we can deal with stopMaildirScan(dir); if (flagsChanged) { //flags changed, store in file name and get back the new filename (id) const QString newKey = dir.changeEntryFlags(item.remoteId(), item.flags()); if (newKey.isEmpty()) { restartMaildirScan(dir); cancelTask(i18n("Failed to change the flags for the mail. %1", dir.lastError())); return; } newItem.setRemoteId(newKey); } if (bodyChanged || headChanged) { //head or body changed // we can only deal with mail if (item.hasPayload()) { const KMime::Message::Ptr mail = item.payload(); QByteArray data = mail->encodedContent(); if (headChanged && !bodyChanged) { //only the head has changed, get the current version of the mail //replace the head and store the new mail in the file const QByteArray currentData = dir.readEntry(newItem.remoteId()); if (currentData.isEmpty() && !dir.lastError().isEmpty()) { restartMaildirScan(dir); cancelTask(dir.lastError()); return; } const QByteArray newHead = mail->head(); mail->setContent(currentData); mail->setHead(newHead); mail->parse(); data = mail->encodedContent(); } if (!dir.writeEntry(newItem.remoteId(), data)) { restartMaildirScan(dir); cancelTask(dir.lastError()); return; } mChangedFiles.insert(newItem.remoteId()); mChangedCleanerTimer->start(CLEANER_TIMEOUT); } else { restartMaildirScan(dir); cancelTask(i18n("Maildir resource got a non-mail content.")); return; } } restartMaildirScan(dir); changeCommitted(newItem); } else { changeProcessed(); } } void MaildirResource::itemMoved(const Item &item, const Collection &source, const Collection &destination) { if (source == destination) { // should not happen but would confuse Maildir::moveEntryTo changeProcessed(); return; } if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } Maildir sourceDir = maildirForCollection(source); if (!sourceDir.isValid()) { cancelTask(i18n("Source folder is invalid: '%1'.", sourceDir.lastError())); return; } Maildir destDir = maildirForCollection(destination); if (!destDir.isValid()) { cancelTask(i18n("Destination folder is invalid: '%1'.", destDir.lastError())); return; } stopMaildirScan(sourceDir); stopMaildirScan(destDir); const QString newRid = sourceDir.moveEntryTo(item.remoteId(), destDir); mChangedFiles.insert(newRid); mChangedCleanerTimer->start(CLEANER_TIMEOUT); restartMaildirScan(sourceDir); restartMaildirScan(destDir); if (newRid.isEmpty()) { cancelTask(i18n("Could not move message '%1' from '%2' to '%3'. The error was %4.", item.remoteId(), sourceDir.path(), destDir.path(), sourceDir.lastError())); return; } Item i(item); i.setRemoteId(newRid); changeCommitted(i); } void MaildirResource::itemRemoved(const Akonadi::Item &item) { if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } if (!mSettings->readOnly()) { Maildir dir = maildirForCollection(item.parentCollection()); // !dir.isValid() means that our parent folder has been deleted already, // so we don't care at all as that one will be recursive anyway stopMaildirScan(dir); if (dir.isValid() && !dir.removeEntry(item.remoteId())) { Q_EMIT error(i18n("Failed to delete message: %1", item.remoteId())); } restartMaildirScan(dir); } qCDebug(MAILDIRRESOURCE_LOG) << "Item removed" << item.id() << " in collection :" << item.parentCollection().id(); changeProcessed(); } Collection::List MaildirResource::listRecursive(const Collection &root, const Maildir &dir) { if (mSettings->monitorFilesystem()) { - mFsWatcher->addDir(dir.path() + QDir::separator() + QLatin1String("new")); - mFsWatcher->addDir(dir.path() + QDir::separator() + QLatin1String("cur")); + mFsWatcher->addDir(dir.path() + QStringLiteral("/new")); + mFsWatcher->addDir(dir.path() + QStringLiteral("/cur")); mFsWatcher->addDir(dir.subDirPath()); if (dir.isRoot()) { mFsWatcher->addDir(dir.path()); } } Collection::List list; const QStringList mimeTypes = QStringList() << itemMimeType() << Collection::mimeType(); const QStringList lstDir = dir.subFolderList(); for (const QString &sub : lstDir) { Collection c; c.setName(sub); c.setRemoteId(sub); c.setParentCollection(root); c.setContentMimeTypes(mimeTypes); if (sub.compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { c.attribute(Collection::AddIfMissing)->setCollectionType("inbox"); } const Maildir md = maildirForCollection(c); if (!md.isValid()) { continue; } list << c; list += listRecursive(c, md); } return list; } void MaildirResource::retrieveCollections() { Maildir dir(mSettings->path(), mSettings->topLevelIsContainer()); if (!dir.isValid()) { Q_EMIT error(dir.lastError()); collectionsRetrieved(Collection::List()); return; } Collection root; root.setParentCollection(Collection::root()); root.setRemoteId(mSettings->path()); root.setName(name()); if (mSettings->readOnly()) { root.setRights(Collection::ReadOnly); } else { if (mSettings->topLevelIsContainer()) { root.setRights(Collection::ReadOnly | Collection::CanCreateCollection); } else { root.setRights(Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem | Collection::CanCreateCollection); } } CachePolicy policy; policy.setInheritFromParent(false); policy.setSyncOnDemand(true); policy.setLocalParts(QStringList() << QLatin1String(MessagePart::Envelope)); policy.setCacheTimeout(1); policy.setIntervalCheckTime(-1); root.setCachePolicy(policy); QStringList mimeTypes; mimeTypes << Collection::mimeType(); mimeTypes << itemMimeType(); root.setContentMimeTypes(mimeTypes); Collection::List list; list << root; list += listRecursive(root, dir); collectionsRetrieved(list); } void MaildirResource::retrieveItems(const Akonadi::Collection &col) { const Maildir md = maildirForCollection(col); if (!md.isValid()) { cancelTask(i18n("Maildir '%1' for collection '%2' is invalid.", md.path(), col.remoteId())); return; } RetrieveItemsJob *job = new RetrieveItemsJob(col, md, this); job->setMimeType(itemMimeType()); connect(job, &RetrieveItemsJob::result, this, &MaildirResource::slotItemsRetrievalResult); } void MaildirResource::slotItemsRetrievalResult(KJob *job) { if (job->error()) { cancelTask(job->errorString()); } else { itemsRetrievalDone(); } } void MaildirResource::collectionAdded(const Collection &collection, const Collection &parent) { if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } Maildir md = maildirForCollection(parent); qCDebug(MAILDIRRESOURCE_LOG) << md.subFolderList(); if (mSettings->readOnly() || !md.isValid()) { changeProcessed(); return; } else { - const QString collectionName(collection.name().remove(QDir::separator())); + const QString collectionName(collection.name().remove(QLatin1Char('/'))); const QString newFolderPath = md.addSubFolder(collectionName); if (newFolderPath.isEmpty()) { changeProcessed(); return; } qCDebug(MAILDIRRESOURCE_LOG) << md.subFolderList(); Collection col = collection; col.setRemoteId(collectionName); col.setName(collectionName); changeCommitted(col); } } void MaildirResource::collectionChanged(const Collection &collection) { if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } if (collection.parentCollection() == Collection::root()) { if (collection.name() != name()) { setName(collection.name()); } changeProcessed(); return; } if (collection.remoteId() == collection.name()) { changeProcessed(); return; } Maildir md = maildirForCollection(collection); if (!md.isValid()) { assert(!collection.remoteId().isEmpty()); // caught in resourcebase // we don't have a maildir for this collection yet, probably due to a race // make one, otherwise the rename below will fail md.create(); } - const QString collectionName(collection.name().remove(QDir::separator())); + const QString collectionName(collection.name().remove(QLatin1Char('/'))); if (!md.rename(collectionName)) { Q_EMIT error(i18n("Unable to rename maildir folder '%1'.", collection.name())); changeProcessed(); return; } Collection c(collection); c.setRemoteId(collectionName); c.setName(collectionName); changeCommitted(c); } void MaildirResource::collectionMoved(const Collection &collection, const Collection &source, const Collection &dest) { qCDebug(MAILDIRRESOURCE_LOG) << collection << source << dest; if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } if (collection.parentCollection() == Collection::root()) { Q_EMIT error(i18n("Cannot move root maildir folder '%1'.", collection.remoteId())); changeProcessed(); return; } if (source == dest) { // should not happen, but who knows... changeProcessed(); return; } Collection c(collection); c.setParentCollection(source); Maildir md = maildirForCollection(c); Maildir destMd = maildirForCollection(dest); if (!md.moveTo(destMd)) { Q_EMIT error(i18n("Unable to move maildir folder '%1' from '%2' to '%3'.", collection.remoteId(), source.remoteId(), dest.remoteId())); changeProcessed(); } else { const QString path = maildirPathForCollection(c); mMaildirsForCollection.remove(path); changeCommitted(collection); } } void MaildirResource::collectionRemoved(const Akonadi::Collection &collection) { if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } if (collection.parentCollection() == Collection::root()) { Q_EMIT error(i18n("Cannot delete top-level maildir folder '%1'.", mSettings->path())); changeProcessed(); return; } Maildir md = maildirForCollection(collection.parentCollection()); // !md.isValid() means that our parent folder has been deleted already, // so we don't care at all as that one will be recursive anyway if (md.isValid() && !md.removeSubFolder(collection.remoteId())) { Q_EMIT error(i18n("Failed to delete sub-folder '%1'.", collection.remoteId())); } const QString path = maildirPathForCollection(collection); mMaildirsForCollection.remove(path); changeProcessed(); } bool MaildirResource::ensureDirExists() { Maildir root(mSettings->path()); if (!root.isValid(false) && !mSettings->topLevelIsContainer()) { if (!root.create()) { Q_EMIT status(Broken, i18n("Unable to create maildir '%1'.", mSettings->path())); } return false; } return true; } bool MaildirResource::ensureSaneConfiguration() { if (mSettings->path().isEmpty()) { Q_EMIT status(NotConfigured, i18n("No usable storage location configured.")); setOnline(false); return false; } return true; } void MaildirResource::slotDirChanged(const QString &dir) { QFileInfo fileInfo(dir); if (fileInfo.isFile()) { slotFileChanged(fileInfo); return; } if (dir == mSettings->path()) { synchronizeCollectionTree(); synchronizeCollection(Collection::root().id()); return; } if (dir.endsWith(QLatin1String(".directory"))) { synchronizeCollectionTree(); //might be too much, but this is not a common case anyway return; } QDir d(dir); if (!d.cdUp()) { return; } Maildir md(d.path()); if (!md.isValid()) { return; } md.refreshKeyCache(); const Collection col = collectionForMaildir(md); if (col.remoteId().isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "unable to find collection for path" << dir; return; } CollectionFetchJob *job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base, this); connect(job, &CollectionFetchJob::result, this, &MaildirResource::fsWatchDirFetchResult); } void MaildirResource::fsWatchDirFetchResult(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << job->errorString(); return; } const Collection::List cols = qobject_cast(job)->collections(); if (cols.isEmpty()) { return; } synchronizeCollection(cols.first().id()); } void MaildirResource::slotFileChanged(const QFileInfo &fileInfo) { const QString key = fileInfo.fileName(); if (mChangedFiles.remove(key) > 0) { return; } QString path = fileInfo.path(); if (path.endsWith(QLatin1String("/new"))) { path.remove(path.length() - 4, 4); } else if (path.endsWith(QLatin1String("/cur"))) { path.remove(path.length() - 4, 4); } const Maildir md(path); if (!md.isValid()) { return; } const Collection col = collectionForMaildir(md); if (col.remoteId().isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "unable to find collection for path" << fileInfo.path(); return; } Item item; item.setRemoteId(key); item.setParentCollection(col); ItemFetchJob *job = new ItemFetchJob(item, this); job->setProperty("entry", key); job->setProperty("dir", path); connect(job, &ItemFetchJob::result, this, &MaildirResource::fsWatchFileFetchResult); } void MaildirResource::fsWatchFileFetchResult(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << job->errorString(); return; } Item::List items = qobject_cast(job)->items(); if (items.isEmpty()) { return; } const QString fileName = job->property("entry").toString(); const QString path = job->property("dir").toString(); const Maildir md(path); Item item(items.at(0)); const qint64 entrySize = md.size(fileName); if (entrySize >= 0) { item.setSize(entrySize); } const Item::Flags flags = md.readEntryFlags(fileName); for (const Item::Flag &flag : flags) { item.setFlag(flag); } const QByteArray data = md.readEntry(fileName); KMime::Message *mail = new KMime::Message(); mail->setContent(KMime::CRLFtoLF(data)); mail->parse(); item.setPayload(KMime::Message::Ptr(mail)); Akonadi::MessageFlags::copyMessageFlags(*mail, item); ItemModifyJob *mjob = new ItemModifyJob(item); connect(mjob, &ItemModifyJob::result, this, &MaildirResource::fsWatchFileModifyResult); } void MaildirResource::fsWatchFileModifyResult(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << " MaildirResource::fsWatchFileModifyResult error: " << job->errorString(); return; } } QString MaildirResource::maildirPathForCollection(const Collection &collection) const { QString path = collection.remoteId(); Akonadi::Collection parent = collection.parentCollection(); while (!parent.remoteId().isEmpty()) { path.prepend(parent.remoteId() + QLatin1Char('/')); parent = parent.parentCollection(); } return path; } void MaildirResource::stopMaildirScan(const Maildir &maildir) { const QString path = maildir.path(); mFsWatcher->removeDir(path + QLatin1String("/new")); mFsWatcher->removeDir(path + QLatin1String("/cur")); } void MaildirResource::restartMaildirScan(const Maildir &maildir) { const QString path = maildir.path(); mFsWatcher->addDir(path + QLatin1String("/new")); mFsWatcher->addDir(path + QLatin1String("/cur")); } void MaildirResource::changedCleaner() { mChangedFiles.clear(); } diff --git a/resources/mixedmaildir/mixedmaildirstore.cpp b/resources/mixedmaildir/mixedmaildirstore.cpp index 3f824f8b2..76005596d 100644 --- a/resources/mixedmaildir/mixedmaildirstore.cpp +++ b/resources/mixedmaildir/mixedmaildirstore.cpp @@ -1,2421 +1,2421 @@ /* This file is part of the KDE project Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com 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 "mixedmaildirstore.h" #include "kmindexreader/kmindexreader.h" #include "mixedmaildir_debug.h" #include "filestore/collectioncreatejob.h" #include "filestore/collectiondeletejob.h" #include "filestore/collectionfetchjob.h" #include "filestore/collectionmovejob.h" #include "filestore/collectionmodifyjob.h" #include "filestore/entitycompactchangeattribute.h" #include "filestore/itemcreatejob.h" #include "filestore/itemdeletejob.h" #include "filestore/itemfetchjob.h" #include "filestore/itemmodifyjob.h" #include "filestore/itemmovejob.h" #include "filestore/storecompactjob.h" #include "libmaildir/maildir.h" #include #include #include #include #include #include #include #include #include "mixedmaildirresource_debug.h" #include #include #include using namespace Akonadi; using KPIM::Maildir; using namespace KMBox; static bool fullEntryCompare(const KMBox::MBoxEntry &left, const KMBox::MBoxEntry &right) { return left.messageOffset() == right.messageOffset() && left.separatorSize() == right.separatorSize() && left.messageSize() == right.messageSize(); } static bool indexFileForFolder(const QFileInfo &folderDirInfo, QFileInfo &indexFileInfo) { indexFileInfo = QFileInfo(folderDirInfo.dir(), QStringLiteral(".%1.index").arg(folderDirInfo.fileName())); if (!indexFileInfo.exists() || !indexFileInfo.isReadable()) { qCDebug(MIXEDMAILDIR_LOG) << "No index file" << indexFileInfo.absoluteFilePath() << "or not readable"; return false; } return true; } class MBoxContext { public: MBoxContext() : mRevision(0) , mIndexDataLoaded(false) , mHasIndexData(false) { } QString fileName() const { return mMBox.fileName(); } bool load(const QString &fileName) { mModificationTime = QFileInfo(fileName).lastModified(); // in case of reload, check if anything changed, otherwise keep deleted entries if (!mDeletedOffsets.isEmpty() && fileName == mMBox.fileName()) { const KMBox::MBoxEntry::List currentEntryList = mMBox.entries(); if (mMBox.load(fileName)) { const KMBox::MBoxEntry::List newEntryList = mMBox.entries(); if (!std::equal(currentEntryList.begin(), currentEntryList.end(), newEntryList.begin(), fullEntryCompare)) { mDeletedOffsets.clear(); } return true; } return false; } mDeletedOffsets.clear(); return mMBox.load(fileName); } QDateTime modificationTime() const { return mModificationTime; } KMBox::MBoxEntry::List entryList() const { KMBox::MBoxEntry::List result; result.reserve(mMBox.entries().count()); Q_FOREACH (const KMBox::MBoxEntry &entry, mMBox.entries()) { if (!mDeletedOffsets.contains(entry.messageOffset())) { result << entry; } } return result; } QByteArray readRawEntry(quint64 offset) { return mMBox.readRawMessage(KMBox::MBoxEntry(offset)); } QByteArray readEntryHeaders(quint64 offset) { return mMBox.readMessageHeaders(KMBox::MBoxEntry(offset)); } qint64 appendEntry(const KMime::Message::Ptr &entry) { const KMBox::MBoxEntry result = mMBox.appendMessage(entry); if (result.isValid() && mHasIndexData) { mIndexData.insert(result.messageOffset(), KMIndexDataPtr(new KMIndexData)); Q_ASSERT(mIndexData.value(result.messageOffset())->isEmpty()); } return result.messageOffset(); } void deleteEntry(quint64 offset) { mDeletedOffsets << offset; } bool isValidOffset(quint64 offset) const { if (mDeletedOffsets.contains(offset)) { return false; } const KMBox::MBoxEntry::List lstEntry = mMBox.entries(); for (const KMBox::MBoxEntry &entry : lstEntry) { if (entry.messageOffset() == offset) { return true; } } return false; } bool save() { bool ret = mMBox.save(); mModificationTime = QDateTime::currentDateTime(); return ret; } int purge(QList &movedEntries) { const int deleteCount = mDeletedOffsets.count(); KMBox::MBoxEntry::List deletedEntries; deletedEntries.reserve(deleteCount); Q_FOREACH (quint64 offset, mDeletedOffsets) { deletedEntries << KMBox::MBoxEntry(offset); } const bool result = mMBox.purge(deletedEntries, &movedEntries); if (mHasIndexData) { // keep copy of original for lookup const IndexDataHash indexData = mIndexData; // delete index data for removed entries Q_FOREACH (quint64 offset, mDeletedOffsets) { mIndexData.remove(offset); } // delete index data for changed entries // readded below in an extra loop to handled cases where a new index is equal to an // old index of a different entry Q_FOREACH (const KMBox::MBoxEntry::Pair &entry, movedEntries) { mIndexData.remove(entry.first.messageOffset()); } // readd index data for changed entries at their new position Q_FOREACH (const KMBox::MBoxEntry::Pair &entry, movedEntries) { const KMIndexDataPtr data = indexData.value(entry.first.messageOffset()); mIndexData.insert(entry.second.messageOffset(), data); } } mDeletedOffsets.clear(); mModificationTime = QDateTime::currentDateTime(); return result ? deleteCount : -1; } MBox &mbox() { return mMBox; } const MBox &mbox() const { return mMBox; } bool hasDeletedOffsets() const { return !mDeletedOffsets.isEmpty(); } void readIndexData(); KMIndexDataPtr indexData(quint64 offset) const { if (mHasIndexData) { if (!mDeletedOffsets.contains(offset)) { IndexDataHash::const_iterator it = mIndexData.constFind(offset); if (it != mIndexData.constEnd()) { return it.value(); } } } return KMIndexDataPtr(); } bool hasIndexData() const { return mHasIndexData; } void updatePath(const QString &newPath) { // TODO FIXME there has to be a better of doing that mMBox.load(newPath); } public: Collection mCollection; qint64 mRevision; private: QSet mDeletedOffsets; MBox mMBox; QDateTime mModificationTime; typedef QHash IndexDataHash; IndexDataHash mIndexData; bool mIndexDataLoaded; bool mHasIndexData; }; typedef QSharedPointer MBoxPtr; void MBoxContext::readIndexData() { if (mIndexDataLoaded) { return; } mIndexDataLoaded = true; const QFileInfo mboxFileInfo(mMBox.fileName()); QFileInfo indexFileInfo; if (!indexFileForFolder(mboxFileInfo, indexFileInfo)) { return; } if (mboxFileInfo.lastModified() > indexFileInfo.lastModified()) { qCDebug(MIXEDMAILDIR_LOG) << "MBox file " << mboxFileInfo.absoluteFilePath() << "newer than the index: mbox modified at" << mboxFileInfo.lastModified() << ", index modified at" << indexFileInfo.lastModified(); return; } KMIndexReader indexReader(indexFileInfo.absoluteFilePath()); if (indexReader.error() || !indexReader.readIndex()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Index file" << indexFileInfo.path() << "could not be read"; return; } mHasIndexData = true; const KMBox::MBoxEntry::List entries = mMBox.entries(); for (const KMBox::MBoxEntry &entry : entries) { const quint64 indexOffset = entry.messageOffset() + entry.separatorSize(); const KMIndexDataPtr data = indexReader.dataByOffset(indexOffset); if (data != nullptr) { mIndexData.insert(entry.messageOffset(), data); } } qCDebug(MIXEDMAILDIR_LOG) << "Read" << mIndexData.count() << "index entries from" << indexFileInfo.absoluteFilePath(); } class MaildirContext { public: MaildirContext(const QString &path, bool isTopLevel) : mMaildir(path, isTopLevel) , mIndexDataLoaded(false) , mHasIndexData(false) { } MaildirContext(const Maildir &md) : mMaildir(md) , mIndexDataLoaded(false) , mHasIndexData(false) { } QStringList entryList() const { return mMaildir.entryList(); } QString addEntry(const QByteArray &data) { const QString result = mMaildir.addEntry(data); if (!result.isEmpty() && mHasIndexData) { mIndexData.insert(result, KMIndexDataPtr(new KMIndexData)); Q_ASSERT(mIndexData.value(result)->isEmpty()); } else { //TODO: use the error string? qCWarning(MIXEDMAILDIRRESOURCE_LOG) << mMaildir.lastError(); } return result; } void writeEntry(const QString &key, const QByteArray &data) { mMaildir.writeEntry(key, data); //TODO: error handling if (mHasIndexData) { mIndexData.insert(key, KMIndexDataPtr(new KMIndexData)); } } bool removeEntry(const QString &key) { const bool result = mMaildir.removeEntry(key); if (result && mHasIndexData) { mIndexData.remove(key); } return result; } QString moveEntryTo(const QString &key, MaildirContext &destination) { const QString result = mMaildir.moveEntryTo(key, destination.mMaildir); if (!result.isEmpty()) { if (mHasIndexData) { mIndexData.remove(key); } if (destination.mHasIndexData) { destination.mIndexData.insert(result, KMIndexDataPtr(new KMIndexData)); } } else { //TODO error handling? qCWarning(MIXEDMAILDIRRESOURCE_LOG) << mMaildir.lastError(); } return result; } QByteArray readEntryHeaders(const QString &key) const { return mMaildir.readEntryHeaders(key); } QByteArray readEntry(const QString &key) const { return mMaildir.readEntry(key); } bool isValid(QString &error) const { bool result = mMaildir.isValid(); if (!result) { error = mMaildir.lastError(); } return result; } bool isValidEntry(const QString &entry) const { return !mMaildir.findRealKey(entry).isEmpty(); } void readIndexData(); KMIndexDataPtr indexData(const QString &fileName) const { if (mHasIndexData) { IndexDataHash::const_iterator it = mIndexData.constFind(fileName); if (it != mIndexData.constEnd()) { return it.value(); } } return KMIndexDataPtr(); } bool hasIndexData() const { return mHasIndexData; } void updatePath(const QString &newPath) { mMaildir = Maildir(newPath, mMaildir.isRoot()); } const Maildir &maildir() const { return mMaildir; } private: Maildir mMaildir; typedef QHash IndexDataHash; IndexDataHash mIndexData; bool mIndexDataLoaded; bool mHasIndexData; }; void MaildirContext::readIndexData() { if (mIndexDataLoaded) { return; } mIndexDataLoaded = true; const QFileInfo maildirFileInfo(mMaildir.path()); QFileInfo indexFileInfo; if (!indexFileForFolder(maildirFileInfo, indexFileInfo)) { return; } const QDir maildirBaseDir(maildirFileInfo.absoluteFilePath()); const QFileInfo curDirFileInfo(maildirBaseDir, QStringLiteral("cur")); const QFileInfo newDirFileInfo(maildirBaseDir, QStringLiteral("new")); if (curDirFileInfo.lastModified() > indexFileInfo.lastModified()) { qCDebug(MIXEDMAILDIR_LOG) << "Maildir " << maildirFileInfo.absoluteFilePath() << "\"cur\" directory newer than the index: cur modified at" << curDirFileInfo.lastModified() << ", index modified at" << indexFileInfo.lastModified(); return; } if (newDirFileInfo.lastModified() > indexFileInfo.lastModified()) { qCDebug(MIXEDMAILDIR_LOG) << "Maildir \"new\" directory newer than the index: cur modified at" << newDirFileInfo.lastModified() << ", index modified at" << indexFileInfo.lastModified(); return; } KMIndexReader indexReader(indexFileInfo.absoluteFilePath()); if (indexReader.error() || !indexReader.readIndex()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Index file" << indexFileInfo.path() << "could not be read"; return; } mHasIndexData = true; const QStringList entries = mMaildir.entryList(); for (const QString &entry : entries) { const KMIndexDataPtr data = indexReader.dataByFileName(entry); if (data != nullptr) { mIndexData.insert(entry, data); } } qCDebug(MIXEDMAILDIR_LOG) << "Read" << mIndexData.count() << "index entries from" << indexFileInfo.absoluteFilePath(); } typedef QSharedPointer MaildirPtr; class MixedMaildirStore::Private : public FileStore::Job::Visitor { MixedMaildirStore *const q; public: enum FolderType { InvalidFolder, TopLevelFolder, MaildirFolder, MBoxFolder }; Private(MixedMaildirStore *parent) : q(parent) { } FolderType folderForCollection(const Collection &col, QString &path, QString &errorText) const; MBoxPtr getOrCreateMBoxPtr(const QString &path); MaildirPtr getOrCreateMaildirPtr(const QString &path, bool isTopLevel); void fillMBoxCollectionDetails(const MBoxPtr &mbox, Collection &collection); void fillMaildirCollectionDetails(const Maildir &md, Collection &collection); void fillMaildirTreeDetails(const Maildir &md, const Collection &collection, Collection::List &collections, bool recurse); void listCollection(FileStore::Job *job, MBoxPtr &mbox, const Collection &collection, Item::List &items); void listCollection(FileStore::Job *job, MaildirPtr &md, const Collection &collection, Item::List &items); bool fillItem(MBoxPtr &mbox, bool includeHeaders, bool includeBody, Item &item) const; bool fillItem(const MaildirPtr &md, bool includeHeaders, bool includeBody, Item &item) const; void updateContextHashes(const QString &oldPath, const QString &newPath); public: // visitor interface implementation bool visit(FileStore::Job *job) override; bool visit(FileStore::CollectionCreateJob *job) override; bool visit(FileStore::CollectionDeleteJob *job) override; bool visit(FileStore::CollectionFetchJob *job) override; bool visit(FileStore::CollectionModifyJob *job) override; bool visit(FileStore::CollectionMoveJob *job) override; bool visit(FileStore::ItemCreateJob *job) override; bool visit(FileStore::ItemDeleteJob *job) override; bool visit(FileStore::ItemFetchJob *job) override; bool visit(FileStore::ItemModifyJob *job) override; bool visit(FileStore::ItemMoveJob *job) override; bool visit(FileStore::StoreCompactJob *job) override; public: typedef QHash MBoxHash; MBoxHash mMBoxes; typedef QHash MaildirHash; MaildirHash mMaildirs; }; MixedMaildirStore::Private::FolderType MixedMaildirStore::Private::folderForCollection(const Collection &col, QString &path, QString &errorText) const { path.clear(); errorText.clear(); if (col.remoteId().isEmpty()) { errorText = i18nc("@info:status", "Given folder name is empty"); qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Incomplete ancestor chain for collection."; Q_ASSERT(!col.remoteId().isEmpty()); // abort! Look at backtrace to see where we came from. return InvalidFolder; } if (col.parentCollection() == Collection::root()) { path = q->path(); if (col.remoteId() != path) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "RID mismatch, is" << col.remoteId() << "expected" << path; } return TopLevelFolder; } FolderType type = folderForCollection(col.parentCollection(), path, errorText); switch (type) { case InvalidFolder: return InvalidFolder; case TopLevelFolder: // fall through case MaildirFolder: { const Maildir parentMd(path, type == TopLevelFolder); const Maildir subFolder = parentMd.subFolder(col.remoteId()); if (subFolder.isValid(false)) { path = subFolder.path(); return MaildirFolder; } const QString subDirPath = (type == TopLevelFolder ? path : Maildir::subDirPathForFolderPath(path)); QFileInfo fileInfo(QDir(subDirPath), col.remoteId()); if (fileInfo.isFile()) { path = fileInfo.absoluteFilePath(); return MBoxFolder; } errorText = i18nc("@info:status", "Folder %1 does not seem to be a valid email folder", fileInfo.absoluteFilePath()); return InvalidFolder; } case MBoxFolder: { const QString subDirPath = Maildir::subDirPathForFolderPath(path); QFileInfo fileInfo(QDir(subDirPath), col.remoteId()); if (fileInfo.isFile()) { path = fileInfo.absoluteFilePath(); return MBoxFolder; } const Maildir subFolder(fileInfo.absoluteFilePath(), false); if (subFolder.isValid(false)) { path = subFolder.path(); return MaildirFolder; } errorText = i18nc("@info:status", "Folder %1 does not seem to be a valid email folder", fileInfo.absoluteFilePath()); return InvalidFolder; } } return InvalidFolder; } MBoxPtr MixedMaildirStore::Private::getOrCreateMBoxPtr(const QString &path) { MBoxPtr mbox; const MBoxHash::const_iterator it = mMBoxes.constFind(path); if (it == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); mMBoxes.insert(path, mbox); } else { mbox = it.value(); } return mbox; } MaildirPtr MixedMaildirStore::Private::getOrCreateMaildirPtr(const QString &path, bool isTopLevel) { MaildirPtr md; const MaildirHash::const_iterator it = mMaildirs.constFind(path); if (it == mMaildirs.constEnd()) { md = MaildirPtr(new MaildirContext(path, isTopLevel)); mMaildirs.insert(path, md); } else { md = it.value(); } return md; } void MixedMaildirStore::Private::fillMBoxCollectionDetails(const MBoxPtr &mbox, Collection &collection) { collection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType()); if (collection.name().compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { collection.attribute(Collection::AddIfMissing)->setCollectionType("inbox"); } const QFileInfo fileInfo(mbox->fileName()); if (fileInfo.isWritable()) { collection.setRights(Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } else { collection.setRights(Collection::ReadOnly); } if (mbox->mRevision > 0) { collection.setRemoteRevision(QString::number(mbox->mRevision)); } } void MixedMaildirStore::Private::fillMaildirCollectionDetails(const Maildir &md, Collection &collection) { collection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType()); if (collection.name().compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { collection.attribute(Collection::AddIfMissing)->setCollectionType("inbox"); } const QFileInfo fileInfo(md.path()); if (fileInfo.isWritable()) { collection.setRights(Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } else { collection.setRights(Collection::ReadOnly); } } void MixedMaildirStore::Private::fillMaildirTreeDetails(const Maildir &md, const Collection &collection, Collection::List &collections, bool recurse) { if (md.path().isEmpty()) { return; } const QStringList maildirSubFolders = md.subFolderList(); for (const QString &subFolder : maildirSubFolders) { const Maildir subMd = md.subFolder(subFolder); if (!mMaildirs.contains(subMd.path())) { const MaildirPtr mdPtr = MaildirPtr(new MaildirContext(subMd)); mMaildirs.insert(subMd.path(), mdPtr); } Collection col; col.setRemoteId(subFolder); col.setName(subFolder); col.setParentCollection(collection); fillMaildirCollectionDetails(subMd, col); collections << col; if (recurse) { fillMaildirTreeDetails(subMd, col, collections, true); } } const QDir dir(md.isRoot() ? md.path() : Maildir::subDirPathForFolderPath(md.path())); const QFileInfoList fileInfos = dir.entryInfoList(QDir::Files); for (const QFileInfo &fileInfo : fileInfos) { if (fileInfo.isHidden() || !fileInfo.isReadable()) { continue; } const QString mboxPath = fileInfo.absoluteFilePath(); MBoxPtr mbox = getOrCreateMBoxPtr(mboxPath); if (mbox->load(mboxPath)) { const QString subFolder = fileInfo.fileName(); Collection col; col.setRemoteId(subFolder); col.setName(subFolder); col.setParentCollection(collection); mbox->mCollection = col; fillMBoxCollectionDetails(mbox, col); collections << col; if (recurse) { const QString subDirPath = Maildir::subDirPathForFolderPath(fileInfo.absoluteFilePath()); const Maildir subMd(subDirPath, true); fillMaildirTreeDetails(subMd, col, collections, true); } } else { mMBoxes.remove(fileInfo.absoluteFilePath()); } } } void MixedMaildirStore::Private::listCollection(FileStore::Job *job, MBoxPtr &mbox, const Collection &collection, Item::List &items) { mbox->readIndexData(); QHash uidHash; QHash tagListHash; const KMBox::MBoxEntry::List entryList = mbox->entryList(); for (const KMBox::MBoxEntry &entry : entryList) { Item item; item.setMimeType(KMime::Message::mimeType()); item.setRemoteId(QString::number(entry.messageOffset())); item.setParentCollection(collection); if (mbox->hasIndexData()) { const KMIndexDataPtr indexData = mbox->indexData(entry.messageOffset()); if (indexData != nullptr && !indexData->isEmpty()) { item.setFlags(indexData->status().statusFlags()); quint64 uid = indexData->uid(); if (uid != 0) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has UID" << uid; uidHash.insert(item.remoteId(), QString::number(uid)); } const QStringList tagList = indexData->tagList(); if (!tagList.isEmpty()) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has" << tagList.count() << "tags:" << tagList; tagListHash.insert(item.remoteId(), tagList); } } else if (indexData == nullptr) { Akonadi::MessageStatus status; status.setDeleted(true); item.setFlags(status.statusFlags()); qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "no index for item" << item.remoteId() << "in MBox" << mbox->fileName() << "so it has been deleted but not purged. Marking it as" << item.flags(); } } items << item; } if (mbox->hasIndexData()) { QVariant var; if (!uidHash.isEmpty()) { var = QVariant::fromValue< QHash >(uidHash); job->setProperty("remoteIdToIndexUid", var); } if (!tagListHash.isEmpty()) { var = QVariant::fromValue< QHash >(tagListHash); job->setProperty("remoteIdToTagList", var); } } } void MixedMaildirStore::Private::listCollection(FileStore::Job *job, MaildirPtr &md, const Collection &collection, Item::List &items) { md->readIndexData(); QHash uidHash; QHash tagListHash; const QStringList entryList = md->entryList(); for (const QString &entry : entryList) { Item item; item.setMimeType(KMime::Message::mimeType()); item.setRemoteId(entry); item.setParentCollection(collection); if (md->hasIndexData()) { const KMIndexDataPtr indexData = md->indexData(entry); if (indexData != nullptr && !indexData->isEmpty()) { item.setFlags(indexData->status().statusFlags()); const quint64 uid = indexData->uid(); if (uid != 0) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has UID" << uid; uidHash.insert(item.remoteId(), QString::number(uid)); } const QStringList tagList = indexData->tagList(); if (!tagList.isEmpty()) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has" << tagList.count() << "tags:" << tagList; tagListHash.insert(item.remoteId(), tagList); } } } const Akonadi::Item::Flags flags = md->maildir().readEntryFlags(entry); for (const Akonadi::Item::Flag &flag : flags) { item.setFlag(flag); } items << item; } if (md->hasIndexData()) { QVariant var; if (!uidHash.isEmpty()) { var = QVariant::fromValue< QHash >(uidHash); job->setProperty("remoteIdToIndexUid", var); } if (!tagListHash.isEmpty()) { var = QVariant::fromValue< QHash >(tagListHash); job->setProperty("remoteIdToTagList", var); } } } bool MixedMaildirStore::Private::fillItem(MBoxPtr &mbox, bool includeHeaders, bool includeBody, Item &item) const { // qCDebug(MIXEDMAILDIR_LOG) << "Filling item" << item.remoteId() << "from MBox: includeBody=" << includeBody; bool ok = false; const quint64 offset = item.remoteId().toULongLong(&ok); if (!ok || !mbox->isValidOffset(offset)) { return false; } item.setModificationTime(mbox->modificationTime()); // TODO: size? if (includeHeaders || includeBody) { KMime::Message::Ptr messagePtr(new KMime::Message()); if (includeBody) { const QByteArray data = mbox->readRawEntry(offset); messagePtr->setContent(KMime::CRLFtoLF(data)); } else { const QByteArray data = mbox->readEntryHeaders(offset); messagePtr->setHead(KMime::CRLFtoLF(data)); } messagePtr->parse(); item.setPayload(messagePtr); Akonadi::MessageFlags::copyMessageFlags(*messagePtr, item); } return true; } bool MixedMaildirStore::Private::fillItem(const MaildirPtr &md, bool includeHeaders, bool includeBody, Item &item) const { /* qCDebug(MIXEDMAILDIR_LOG) << "Filling item" << item.remoteId() << "from Maildir: includeBody=" << includeBody;*/ const qint64 entrySize = md->maildir().size(item.remoteId()); if (entrySize < 0) { return false; } item.setSize(entrySize); item.setModificationTime(md->maildir().lastModified(item.remoteId())); if (includeHeaders || includeBody) { KMime::Message::Ptr messagePtr(new KMime::Message()); if (includeBody) { const QByteArray data = md->readEntry(item.remoteId()); if (data.isEmpty()) { return false; } messagePtr->setContent(KMime::CRLFtoLF(data)); } else { const QByteArray data = md->readEntryHeaders(item.remoteId()); if (data.isEmpty()) { return false; } messagePtr->setHead(KMime::CRLFtoLF(data)); } messagePtr->parse(); item.setPayload(messagePtr); Akonadi::MessageFlags::copyMessageFlags(*messagePtr, item); } return true; } void MixedMaildirStore::Private::updateContextHashes(const QString &oldPath, const QString &newPath) { //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "oldPath=" << oldPath << "newPath=" << newPath; const QString oldSubDirPath = Maildir::subDirPathForFolderPath(oldPath); const QString newSubDirPath = Maildir::subDirPathForFolderPath(newPath); MBoxHash mboxes; MBoxHash::const_iterator mboxIt = mMBoxes.constBegin(); MBoxHash::const_iterator mboxEndIt = mMBoxes.constEnd(); for (; mboxIt != mboxEndIt; ++mboxIt) { QString key = mboxIt.key(); MBoxPtr mboxPtr = mboxIt.value(); if (key == oldPath) { key = newPath; } else if (key.startsWith(oldSubDirPath)) { if (mboxPtr->hasIndexData() || mboxPtr->mRevision > 0) { key.replace(oldSubDirPath, newSubDirPath); } else { // if there is no index data yet, just discard this context key.clear(); } } if (!key.isEmpty()) { mboxPtr->updatePath(key); mboxes.insert(key, mboxPtr); } } //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "mbox: old keys=" << mMBoxes.keys() << "new keys" << mboxes.keys(); mMBoxes = mboxes; MaildirHash maildirs; MaildirHash::const_iterator mdIt = mMaildirs.constBegin(); MaildirHash::const_iterator mdEndIt = mMaildirs.constEnd(); for (; mdIt != mdEndIt; ++mdIt) { QString key = mdIt.key(); MaildirPtr mdPtr = mdIt.value(); if (key == oldPath) { key = newPath; } else if (key.startsWith(oldSubDirPath)) { if (mdPtr->hasIndexData()) { key.replace(oldSubDirPath, newSubDirPath); } else { // if there is no index data yet, just discard this context key.clear(); } } if (!key.isEmpty()) { mdPtr->updatePath(key); maildirs.insert(key, mdPtr); } } //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "maildir: old keys=" << mMaildirs.keys() << "new keys" << maildirs.keys(); mMaildirs = maildirs; } bool MixedMaildirStore::Private::visit(FileStore::Job *job) { const QString message = i18nc("@info:status", "Unhandled operation %1", QLatin1String(job->metaObject()->className())); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message; q->notifyError(FileStore::Job::InvalidJobContext, message); return false; } bool MixedMaildirStore::Private::visit(FileStore::CollectionCreateJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->targetParent(), path, errorText); if (folderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } - const QString collectionName = job->collection().name().remove(QDir::separator()); + const QString collectionName = job->collection().name().remove(QLatin1Char('/')); Maildir md; if (folderType == MBoxFolder) { const QString subDirPath = Maildir::subDirPathForFolderPath(path); const QDir dir(subDirPath); const QFileInfo dirInfo(dir, collectionName); if (dirInfo.exists() && !dirInfo.isDir()) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType << ", dirInfo exists and it not a dir"; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (!dir.mkpath(collectionName)) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType << ", mkpath failed"; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } md = Maildir(dirInfo.absoluteFilePath(), false); if (!md.create()) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType << ", maildir create failed"; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } const MaildirPtr mdPtr(new MaildirContext(md)); mMaildirs.insert(md.path(), mdPtr); } else { Maildir parentMd(path, folderType == TopLevelFolder); if (parentMd.addSubFolder(collectionName).isEmpty()) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } md = Maildir(parentMd.subFolder(collectionName)); const MaildirPtr mdPtr(new MaildirContext(md)); mMaildirs.insert(md.path(), mdPtr); } Collection collection = job->collection(); collection.setRemoteId(collectionName); collection.setName(collectionName); collection.setParentCollection(job->targetParent()); fillMaildirCollectionDetails(md, collection); q->notifyCollectionsProcessed(Collection::List() << collection); return true; } bool MixedMaildirStore::Private::visit(FileStore::CollectionDeleteJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->collection(), path, errorText); if (folderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } QString parentPath; const FolderType parentFolderType = folderForCollection(job->collection().parentCollection(), parentPath, errorText); if (parentFolderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "Parent FolderType=" << parentFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { if (!QFile::remove(path)) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } else { if (!QDir(path).removeRecursively()) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } const QString subDirPath = Maildir::subDirPathForFolderPath(path); QDir(subDirPath).removeRecursively(); q->notifyCollectionsProcessed(Collection::List() << job->collection()); return true; } bool MixedMaildirStore::Private::visit(FileStore::CollectionFetchJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->collection(), path, errorText); if (folderType == InvalidFolder) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection:" << job->collection(); q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } Collection::List collections; Collection collection = job->collection(); if (job->type() == FileStore::CollectionFetchJob::Base) { collection.setName(collection.remoteId()); if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Failed to load MBox folder %1", path); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection=" << collection; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code return false; } mbox->mCollection = collection; mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } fillMBoxCollectionDetails(mbox, collection); } else { const Maildir md(path, folderType == TopLevelFolder); fillMaildirCollectionDetails(md, collection); } collections << collection; } else { // if the base is an mbox, use its sub folder dir like a top level maildir if (folderType == MBoxFolder) { path = Maildir::subDirPathForFolderPath(path); } const Maildir md(path, folderType != MaildirFolder); fillMaildirTreeDetails(md, collection, collections, job->type() == FileStore::CollectionFetchJob::Recursive); } if (!collections.isEmpty()) { q->notifyCollectionsProcessed(collections); } return true; } static Collection updateMBoxCollectionTree(const Collection &collection, const Collection &oldParent, const Collection &newParent) { if (collection == oldParent) { return newParent; } if (collection == Collection::root()) { return collection; } Collection updatedCollection = collection; updatedCollection.setParentCollection(updateMBoxCollectionTree(collection.parentCollection(), oldParent, newParent)); return updatedCollection; } bool MixedMaildirStore::Private::visit(FileStore::CollectionModifyJob *job) { const Collection collection = job->collection(); - const QString collectionName = collection.name().remove(QDir::separator()); + const QString collectionName = collection.name().remove(QLatin1Char('/')); // we also only do renames if (collection.remoteId() == collection.name()) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "CollectionModifyJob with name still identical to remoteId. Ignoring"; return true; } QString path; QString errorText; const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } const QFileInfo fileInfo(path); const QFileInfo subDirInfo = Maildir::subDirPathForFolderPath(path); QDir parentDir(path); parentDir.cdUp(); const QFileInfo targetFileInfo(parentDir, collectionName); const QFileInfo targetSubDirInfo = Maildir::subDirPathForFolderPath(targetFileInfo.absoluteFilePath()); if (targetFileInfo.exists() || (subDirInfo.exists() && targetSubDirInfo.exists())) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // if there is an index, make sure it is read before renaming // do not rename index as it could already be out of date bool indexInvalidated = false; if (folderType == MBoxFolder) { // TODO would be nice if getOrCreateMBoxPtr() could be used instead, like below for Maildir MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to load mbox" << path; } mbox->mCollection = collection; mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } mbox->readIndexData(); indexInvalidated = mbox->hasIndexData(); } else if (folderType == MaildirFolder) { MaildirPtr md = getOrCreateMaildirPtr(path, false); md->readIndexData(); indexInvalidated = md->hasIndexData(); } if (!parentDir.rename(fileInfo.absoluteFilePath(), targetFileInfo.fileName())) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (subDirInfo.exists()) { if (!parentDir.rename(subDirInfo.absoluteFilePath(), targetSubDirInfo.fileName())) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // try to recover the previous rename parentDir.rename(targetFileInfo.absoluteFilePath(), fileInfo.fileName()); return false; } } // update context hashes updateContextHashes(fileInfo.absoluteFilePath(), targetFileInfo.absoluteFilePath()); Collection renamedCollection = collection; // when renaming top level folder, change path of store if (folderType == TopLevelFolder) { // backup caches, setTopLevelCollection() clears them const MBoxHash mboxes = mMBoxes; const MaildirHash maildirs = mMaildirs; q->setPath(targetFileInfo.absoluteFilePath()); // restore caches mMBoxes = mboxes; mMaildirs = maildirs; renamedCollection = q->topLevelCollection(); } else { renamedCollection.setRemoteId(collectionName); renamedCollection.setName(collectionName); } // update collections in MBox contexts so they stay usable for purge Q_FOREACH (const MBoxPtr &mbox, mMBoxes) { if (mbox->mCollection.isValid()) { MBoxPtr updatedMBox = mbox; updatedMBox->mCollection = updateMBoxCollectionTree(mbox->mCollection, collection, renamedCollection); } } if (indexInvalidated) { const QVariant var = QVariant::fromValue(Collection::List() << renamedCollection); job->setProperty("onDiskIndexInvalidated", var); } q->notifyCollectionsProcessed(Collection::List() << renamedCollection); return true; } bool MixedMaildirStore::Private::visit(FileStore::CollectionMoveJob *job) { QString errorText; const Collection moveCollection = job->collection(); const Collection targetCollection = job->targetParent(); QString movePath; const FolderType moveFolderType = folderForCollection(moveCollection, movePath, errorText); if (moveFolderType == InvalidFolder || moveFolderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << moveFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "moveCollection" << moveCollection.remoteId() // << "movePath=" << movePath // << "moveType=" << moveFolderType; QString targetPath; const FolderType targetFolderType = folderForCollection(targetCollection, targetPath, errorText); if (targetFolderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "targetCollection" << targetCollection.remoteId() // << "targetPath=" << targetPath // << "targetType=" << targetFolderType; const QFileInfo targetSubDirInfo(Maildir::subDirPathForFolderPath(targetPath)); // if target is not the top level folder, make sure the sub folder directory exists if (targetFolderType != TopLevelFolder) { if (!targetSubDirInfo.exists()) { QDir topDir(q->path()); if (!topDir.mkpath(targetSubDirInfo.absoluteFilePath())) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } } bool indexInvalidated = false; QString movedPath; if (moveFolderType == MBoxFolder) { // TODO would be nice if getOrCreateMBoxPtr() could be used instead, like below for Maildir MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(movePath); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(movePath)) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to load mbox" << movePath; } mbox->mCollection = moveCollection; mMBoxes.insert(movePath, mbox); } else { mbox = findIt.value(); } mbox->readIndexData(); indexInvalidated = mbox->hasIndexData(); const QFileInfo moveFileInfo(movePath); const QFileInfo moveSubDirInfo(Maildir::subDirPathForFolderPath(movePath)); const QFileInfo targetFileInfo(targetPath); QDir targetDir(targetFolderType == TopLevelFolder ? targetPath : Maildir::subDirPathForFolderPath(targetPath)); if (targetDir.exists(moveFileInfo.fileName()) || !targetDir.rename(moveFileInfo.absoluteFilePath(), moveFileInfo.fileName())) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (moveSubDirInfo.exists()) { if (targetDir.exists(moveSubDirInfo.fileName()) || !targetDir.rename(moveSubDirInfo.absoluteFilePath(), moveSubDirInfo.fileName())) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; // try to revert the other rename QDir sourceDir(moveFileInfo.absolutePath()); sourceDir.cdUp(); sourceDir.rename(targetFileInfo.absoluteFilePath(), moveFileInfo.fileName()); q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } movedPath = QFileInfo(targetDir, moveFileInfo.fileName()).absoluteFilePath(); } else { MaildirPtr md = getOrCreateMaildirPtr(movePath, false); md->readIndexData(); indexInvalidated = md->hasIndexData(); Maildir moveMd(movePath, false); // for moving purpose we can treat the MBox target's subDirPath like a top level maildir Maildir targetMd; if (targetFolderType == MBoxFolder) { targetMd = Maildir(targetSubDirInfo.absoluteFilePath(), true); } else { targetMd = Maildir(targetPath, targetFolderType == TopLevelFolder); } if (!moveMd.moveTo(targetMd)) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } movedPath = targetMd.subFolder(moveCollection.remoteId()).path(); } // update context hashes updateContextHashes(movePath, movedPath); Collection movedCollection = moveCollection; movedCollection.setParentCollection(targetCollection); // update collections in MBox contexts so they stay usable for purge Q_FOREACH (const MBoxPtr &mbox, mMBoxes) { if (mbox->mCollection.isValid()) { MBoxPtr updatedMBox = mbox; updatedMBox->mCollection = updateMBoxCollectionTree(mbox->mCollection, moveCollection, movedCollection); } } if (indexInvalidated) { const QVariant var = QVariant::fromValue(Collection::List() << movedCollection); job->setProperty("onDiskIndexInvalidated", var); } q->notifyCollectionsProcessed(Collection::List() << movedCollection); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemCreateJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->collection(), path, errorText); if (folderType == InvalidFolder || folderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } Item item = job->item(); if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = job->collection(); mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << job->collection()); job->setProperty("onDiskIndexInvalidated", var); } qint64 result = mbox->appendEntry(item.payload()); if (result < 0) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->save(); item.setRemoteId(QString::number(result)); } else { MaildirPtr mdPtr; MaildirHash::const_iterator findIt = mMaildirs.constFind(path); if (findIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, false)); mMaildirs.insert(path, mdPtr); } else { mdPtr = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mdPtr->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << job->collection()); job->setProperty("onDiskIndexInvalidated", var); } const QString result = mdPtr->addEntry(item.payload()->encodedContent()); if (result.isEmpty()) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(result); } item.setParentCollection(job->collection()); q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemDeleteJob *job) { const Item item = job->item(); const Collection collection = item.parentCollection(); QString path; QString errorText; const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder || folderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } bool ok = false; qint64 offset = item.remoteId().toLongLong(&ok); if (!ok || offset < 0 || !mbox->isValidOffset(offset)) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = collection; mbox->deleteEntry(offset); job->setProperty("compactStore", true); } else { MaildirPtr mdPtr; MaildirHash::const_iterator findIt = mMaildirs.constFind(path); if (findIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, false)); mMaildirs.insert(path, mdPtr); } else { mdPtr = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mdPtr->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << collection); job->setProperty("onDiskIndexInvalidated", var); } if (!mdPtr->removeEntry(item.remoteId())) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemFetchJob *job) { ItemFetchScope scope = job->fetchScope(); const bool includeBody = scope.fullPayload() || scope.payloadParts().contains(MessagePart::Body); const bool includeHeaders = scope.payloadParts().contains(MessagePart::Header) || scope.payloadParts().contains(MessagePart::Envelope); const bool fetchItemsBatch = !job->requestedItems().isEmpty(); const bool fetchSingleItem = job->collection().remoteId().isEmpty() && !fetchItemsBatch; const Collection collection = fetchItemsBatch ? job->requestedItems().at(0).parentCollection() : fetchSingleItem ? job->item().parentCollection() : job->collection(); QString path; QString errorText; Q_ASSERT(!collection.remoteId().isEmpty()); const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection:" << job->collection(); q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { MBoxHash::iterator findIt = mMBoxes.find(path); if (findIt == mMBoxes.end() || (!fetchSingleItem && !fetchItemsBatch)) { MBoxPtr mbox = findIt != mMBoxes.end() ? findIt.value() : MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Failed to load MBox folder %1", path); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection=" << collection; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code if (findIt != mMBoxes.end()) { mMBoxes.erase(findIt); } return false; } if (findIt == mMBoxes.end()) { findIt = mMBoxes.insert(path, mbox); } } Item::List items; if (fetchSingleItem) { items << job->item(); } else if (fetchItemsBatch) { items = job->requestedItems(); } else { listCollection(job, findIt.value(), collection, items); } Item::List::iterator it = items.begin(); Item::List::iterator endIt = items.end(); for (; it != endIt; ++it) { if (!fillItem(findIt.value(), includeHeaders, includeBody, *it)) { const QString errorText = i18nc("@info:status", "Error while reading mails from folder %1", collection.name()); q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read item" << (*it).remoteId() << "in MBox file" << path; return false; } } if (!items.isEmpty()) { q->notifyItemsProcessed(items); } } else { MaildirPtr mdPtr; MaildirHash::const_iterator mdIt = mMaildirs.constFind(path); if (mdIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, folderType == TopLevelFolder)); mMaildirs.insert(path, mdPtr); } else { mdPtr = mdIt.value(); } if (!mdPtr->isValid(errorText)) { errorText = i18nc("@info:status", "Failed to load Maildirs folder %1", path); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection=" << collection; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code return false; } Item::List items; if (fetchSingleItem) { items << job->item(); } else if (fetchItemsBatch) { items = job->requestedItems(); } else { listCollection(job, mdPtr, collection, items); } Item::List::iterator it = items.begin(); Item::List::iterator endIt = items.end(); for (; it != endIt; ++it) { if (!fillItem(mdPtr, includeHeaders, includeBody, *it)) { const QString errorText = i18nc("@info:status", "Error while reading mails from folder %1", collection.name()); q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read item" << (*it).remoteId() << "in Maildir" << path; return false; } } if (!items.isEmpty()) { q->notifyItemsProcessed(items); } } return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemModifyJob *job) { const QSet parts = job->parts(); bool payloadChanged = false; bool flagsChanged = false; for (const QByteArray &part : parts) { if (part.startsWith("PLD:")) { payloadChanged = true; } if (part.contains("FLAGS")) { flagsChanged = true; } } const bool nothingChanged = (!payloadChanged && !flagsChanged); const bool payloadChangedButIgnored = payloadChanged && job->ignorePayload(); const bool ignoreModifyIfValid = nothingChanged || (payloadChangedButIgnored && !flagsChanged); Item item = job->item(); const Collection collection = item.parentCollection(); QString path; QString errorText; const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder || folderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } bool ok = false; qint64 offset = item.remoteId().toLongLong(&ok); if (!ok || offset < 0 || !mbox->isValidOffset(offset)) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // if we can ignore payload, or we have nothing else to change, then we are finished if (ignoreModifyIfValid) { qCDebug(MIXEDMAILDIR_LOG) << "ItemModifyJob for item" << item.remoteId() << "in collection" << collection.remoteId() << "skipped: nothing of interest changed (" << nothingChanged << ") or only payload changed but should be ignored (" << (payloadChanged && !flagsChanged && job->ignorePayload()) << "). Modified parts:" << parts; q->notifyItemsProcessed(Item::List() << job->item()); return true; } // mbox can only change payload, ignore any other change if (!payloadChanged) { q->notifyItemsProcessed(Item::List() << item); return true; } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << collection); job->setProperty("onDiskIndexInvalidated", var); } qint64 newOffset = mbox->appendEntry(item.payload()); if (newOffset < 0) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (newOffset > 0) { mbox->mCollection = collection; mbox->deleteEntry(offset); job->setProperty("compactStore", true); } mbox->save(); item.setRemoteId(QString::number(newOffset)); } else { MaildirPtr mdPtr; MaildirHash::const_iterator findIt = mMaildirs.constFind(path); if (findIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, false)); mMaildirs.insert(path, mdPtr); } else { mdPtr = findIt.value(); } if (!mdPtr->isValidEntry(item.remoteId())) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // if we can ignore payload, or we have nothing else to change, then we are finished if (ignoreModifyIfValid) { qCDebug(MIXEDMAILDIR_LOG) << "ItemModifyJob for item" << item.remoteId() << "in collection" << collection.remoteId() << "skipped: nothing of interest changed (" << nothingChanged << ") or only payload changed but should be ignored (" << (payloadChanged && !flagsChanged && job->ignorePayload()) << "). Modified parts:" << parts; q->notifyItemsProcessed(Item::List() << job->item()); return true; } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mdPtr->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << collection); job->setProperty("onDiskIndexInvalidated", var); } QString newKey = item.remoteId(); if (flagsChanged) { Maildir md(mdPtr->maildir()); newKey = md.changeEntryFlags(item.remoteId(), item.flags()); if (newKey.isEmpty()) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1. %2", collection.name(), md.lastError()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(newKey); } if (payloadChanged) { mdPtr->writeEntry(newKey, item.payload()->encodedContent()); } } q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemMoveJob *job) { QString errorText; QString sourcePath; const Collection sourceCollection = job->item().parentCollection(); const FolderType sourceFolderType = folderForCollection(sourceCollection, sourcePath, errorText); if (sourceFolderType == InvalidFolder || sourceFolderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot move emails from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "sourceCollection" << sourceCollection.remoteId() // << "sourcePath=" << sourcePath // << "sourceType=" << sourceFolderType; QString targetPath; const Collection targetCollection = job->targetParent(); const FolderType targetFolderType = folderForCollection(targetCollection, targetPath, errorText); if (targetFolderType == InvalidFolder || targetFolderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "targetCollection" << targetCollection.remoteId() // << "targetPath=" << targetPath // << "targetType=" << targetFolderType; Item item = job->item(); if (sourceFolderType == MBoxFolder) { /* qCDebug(MIXEDMAILDIR_LOG) << "source is MBox";*/ bool ok = false; quint64 offset = item.remoteId().toULongLong(&ok); if (!ok) { errorText = i18nc("@info:status", "Cannot move emails from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(sourcePath); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(sourcePath)) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = sourceCollection; mMBoxes.insert(sourcePath, mbox); } else { mbox = findIt.value(); } if (!mbox->isValidOffset(offset)) { errorText = i18nc("@info:status", "Cannot move emails from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (!item.hasPayload() || !item.loadedPayloadParts().contains(MessagePart::Body)) { if (!fillItem(mbox, true, true, item)) { errorText = i18nc("@info:status", "Cannot move email from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } Collection::List collections; // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); if (mbox->hasIndexData()) { collections << sourceCollection; } if (targetFolderType == MBoxFolder) { /* qCDebug(MIXEDMAILDIR_LOG) << "target is MBox";*/ MBoxPtr targetMBox; MBoxHash::const_iterator findIt = mMBoxes.constFind(targetPath); if (findIt == mMBoxes.constEnd()) { targetMBox = MBoxPtr(new MBoxContext); if (!targetMBox->load(targetPath)) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } targetMBox->mCollection = targetCollection; mMBoxes.insert(targetPath, targetMBox); } else { targetMBox = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid targetMBox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (targetMBox->hasIndexData()) { collections << targetCollection; } qint64 remoteId = targetMBox->appendEntry(item.payload()); if (remoteId < 0) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (!targetMBox->save()) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(QString::number(remoteId)); } else { /* qCDebug(MIXEDMAILDIR_LOG) << "target is Maildir";*/ MaildirPtr targetMdPtr = getOrCreateMaildirPtr(targetPath, false); // make sure to read the index (if available) before modifying the data, which would // make the index invalid targetMdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (targetMdPtr->hasIndexData()) { collections << targetCollection; } const QString remoteId = targetMdPtr->addEntry(mbox->readRawEntry(offset)); if (remoteId.isEmpty()) { errorText = i18nc("@info:status", "Cannot move email from folder %1 to folder %2", sourceCollection.name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "SourceFolderType=" << sourceFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(remoteId); } if (!collections.isEmpty()) { const QVariant var = QVariant::fromValue(collections); job->setProperty("onDiskIndexInvalidated", var); } mbox->mCollection = sourceCollection; mbox->deleteEntry(offset); job->setProperty("compactStore", true); } else { /* qCDebug(MIXEDMAILDIR_LOG) << "source is Maildir";*/ MaildirPtr sourceMdPtr = getOrCreateMaildirPtr(sourcePath, false); if (!sourceMdPtr->isValidEntry(item.remoteId())) { errorText = i18nc("@info:status", "Cannot move email from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } Collection::List collections; // make sure to read the index (if available) before modifying the data, which would // make the index invalid sourceMdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (sourceMdPtr->hasIndexData()) { collections << sourceCollection; } if (targetFolderType == MBoxFolder) { /* qCDebug(MIXEDMAILDIR_LOG) << "target is MBox";*/ if (!item.hasPayload() || !item.loadedPayloadParts().contains(MessagePart::Body)) { if (!fillItem(sourceMdPtr, true, true, item)) { errorText = i18nc("@info:status", "Cannot move email from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(targetPath); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(targetPath)) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = targetCollection; mMBoxes.insert(targetPath, mbox); } else { mbox = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { collections << targetCollection; } const qint64 remoteId = mbox->appendEntry(item.payload()); if (remoteId < 0) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } sourceMdPtr->removeEntry(item.remoteId()); mbox->save(); item.setRemoteId(QString::number(remoteId)); } else { /* qCDebug(MIXEDMAILDIR_LOG) << "target is Maildir";*/ MaildirPtr targetMdPtr = getOrCreateMaildirPtr(targetPath, false); // make sure to read the index (if available) before modifying the data, which would // make the index invalid targetMdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (targetMdPtr->hasIndexData()) { collections << targetCollection; } const QString remoteId = sourceMdPtr->moveEntryTo(item.remoteId(), *targetMdPtr); if (remoteId.isEmpty()) { errorText = i18nc("@info:status", "Cannot move email from folder %1 to folder %2", sourceCollection.name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "SourceFolderType=" << sourceFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(remoteId); } if (!collections.isEmpty()) { const QVariant var = QVariant::fromValue(collections); job->setProperty("onDiskIndexInvalidated", var); } } item.setParentCollection(targetCollection); q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::StoreCompactJob *job) { Q_UNUSED(job); Collection::List collections; MBoxHash::const_iterator it = mMBoxes.constBegin(); MBoxHash::const_iterator endIt = mMBoxes.constEnd(); for (; it != endIt; ++it) { MBoxPtr mbox = it.value(); if (!mbox->hasDeletedOffsets()) { continue; } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); QList movedEntries; const int result = mbox->purge(movedEntries); if (result > 0) { if (!movedEntries.isEmpty()) { qint64 revision = mbox->mCollection.remoteRevision().toLongLong(); qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "purge of" << mbox->mCollection.name() << "caused item move: oldRevision=" << revision << "(stored)," << mbox->mRevision << "(local)"; revision = qMax(revision, mbox->mRevision) + 1; const QString remoteRevision = QString::number(revision); Collection collection = mbox->mCollection; collection.attribute(Collection::AddIfMissing)->setRemoteRevision(remoteRevision); q->notifyCollectionsProcessed(Collection::List() << collection); mbox->mCollection.setRemoteRevision(remoteRevision); mbox->mRevision = revision; } Item::List items; items.reserve(movedEntries.count()); Q_FOREACH (const KMBox::MBoxEntry::Pair &offsetPair, movedEntries) { const QString oldRemoteId(QString::number(offsetPair.first.messageOffset())); const QString newRemoteId(QString::number(offsetPair.second.messageOffset())); Item item; item.setRemoteId(oldRemoteId); item.setParentCollection(mbox->mCollection); item.attribute(Item::AddIfMissing)->setRemoteId(newRemoteId); items << item; } // if there is index data, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { collections << mbox->mCollection; } if (!items.isEmpty()) { q->notifyItemsProcessed(items); } } } if (!collections.isEmpty()) { const QVariant var = QVariant::fromValue(collections); job->setProperty("onDiskIndexInvalidated", var); } return true; } MixedMaildirStore::MixedMaildirStore() : FileStore::AbstractLocalStore() , d(new Private(this)) { } MixedMaildirStore::~MixedMaildirStore() { delete d; } void MixedMaildirStore::setTopLevelCollection(const Collection &collection) { QStringList contentMimeTypes; contentMimeTypes << Collection::mimeType(); Collection::Rights rights; // TODO check if read-only? rights = Collection::CanCreateCollection | Collection::CanChangeCollection | Collection::CanDeleteCollection; CachePolicy cachePolicy; cachePolicy.setInheritFromParent(false); cachePolicy.setLocalParts(QStringList() << QLatin1String(MessagePart::Envelope)); cachePolicy.setSyncOnDemand(true); cachePolicy.setCacheTimeout(1); Collection modifiedCollection = collection; modifiedCollection.setContentMimeTypes(contentMimeTypes); modifiedCollection.setRights(rights); modifiedCollection.setParentCollection(Collection::root()); modifiedCollection.setCachePolicy(cachePolicy); // clear caches d->mMBoxes.clear(); d->mMaildirs.clear(); FileStore::AbstractLocalStore::setTopLevelCollection(modifiedCollection); } void MixedMaildirStore::processJob(FileStore::Job *job) { if (!job->accept(d)) { // check that an error has been set if (job->error() == 0 || job->errorString().isEmpty()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "visitor did not set either error code or error string when returning false"; Q_ASSERT(job->error() == 0 || job->errorString().isEmpty()); } } else { // check that no error has been set if (job->error() != 0 || !job->errorString().isEmpty()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "visitor did set either error code or error string when returning true"; Q_ASSERT(job->error() != 0 || !job->errorString().isEmpty()); } } } void MixedMaildirStore::checkCollectionMove(FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const { // check if the target is not the collection itself or one if its children Collection targetCollection = job->targetParent(); while (targetCollection.isValid()) { if (targetCollection == job->collection()) { errorCode = FileStore::Job::InvalidJobContext; errorText = i18nc("@info:status", "Cannot move folder %1 into one of its own subfolder tree", job->collection().name()); return; } targetCollection = targetCollection.parentCollection(); } } void MixedMaildirStore::checkItemCreate(FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const { if (!job->item().hasPayload()) { errorCode = FileStore::Job::InvalidJobContext; errorText = i18nc("@info:status", "Cannot add email to folder %1 because there is no email content", job->collection().name()); } } void MixedMaildirStore::checkItemModify(FileStore::ItemModifyJob *job, int &errorCode, QString &errorText) const { if (!job->ignorePayload() && !job->item().hasPayload()) { errorCode = FileStore::Job::InvalidJobContext; errorText = i18nc("@info:status", "Cannot modify email in folder %1 because there is no email content", job->item().parentCollection().name()); } } void MixedMaildirStore::checkItemFetch(FileStore::ItemFetchJob *job, int &errorCode, QString &errorText) const { Q_UNUSED(errorCode); Q_UNUSED(errorText); if (!job->requestedItems().isEmpty()) { // Requesting items const auto items = job->requestedItems(); for (const Item &item : items) { const Collection coll = item.parentCollection(); Q_ASSERT(!coll.remoteId().isEmpty()); } } else { // Requesting an entire collection Q_ASSERT(!job->collection().remoteId().isEmpty()); } } diff --git a/resources/shared/singlefileresource/singlefileresourcebase.cpp b/resources/shared/singlefileresource/singlefileresourcebase.cpp index 649f9094b..c41795402 100644 --- a/resources/shared/singlefileresource/singlefileresourcebase.cpp +++ b/resources/shared/singlefileresource/singlefileresourcebase.cpp @@ -1,310 +1,310 @@ /* Copyright (c) 2008 Bertjan Broeksema Copyright (c) 2008 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 "singlefileresourcebase.h" #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QEventLoopLocker *) using namespace Akonadi; SingleFileResourceBase::SingleFileResourceBase(const QString &id) : ResourceBase(id) , mDownloadJob(nullptr) , mUploadJob(nullptr) { connect(this, &SingleFileResourceBase::reloadConfiguration, this, [this]() { applyConfigurationChanges(); reloadFile(); synchronizeCollectionTree(); }); QTimer::singleShot(0, this, [this]() { readFile(); }); changeRecorder()->itemFetchScope().fetchFullPayload(); changeRecorder()->fetchCollection(true); connect(changeRecorder(), &ChangeRecorder::changesAdded, this, &SingleFileResourceBase::scheduleWrite); connect(KDirWatch::self(), &KDirWatch::dirty, this, &SingleFileResourceBase::fileChanged); connect(KDirWatch::self(), &KDirWatch::created, this, &SingleFileResourceBase::fileChanged); } void SingleFileResourceBase::applyConfigurationChanges() { } KSharedConfig::Ptr SingleFileResourceBase::runtimeConfig() const { return KSharedConfig::openConfig(name() + QLatin1String("rc"), KConfig::SimpleConfig, QStandardPaths::CacheLocation); } bool SingleFileResourceBase::readLocalFile(const QString &fileName) { const QByteArray newHash = calculateHash(fileName); if (mCurrentHash != newHash) { if (!mCurrentHash.isEmpty()) { // There was a hash stored in the config file or a chached one from // a previous read and it is different from the hash we just read. handleHashChange(); } if (!readFromFile(fileName)) { mCurrentHash.clear(); mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file return false; } if (mCurrentHash.isEmpty()) { // This is the very first time we read the file so make sure to store // the hash as writeFile() might not be called at all (e.g in case of // read only resources). saveHash(newHash); } // Only synchronize when the contents of the file have changed wrt to // the last time this file was read. Before we synchronize first // clearCache is called to make sure that the cached items get the // actual values as present in the file. invalidateCache(rootCollection()); synchronize(); } else { // The hash didn't change, notify implementing resources about the // actual file name that should be used when reading the file is // necessary. setLocalFileName(fileName); } mCurrentHash = newHash; return true; } void SingleFileResourceBase::setLocalFileName(const QString &fileName) { // Default implementation. if (!readFromFile(fileName)) { mCurrentHash.clear(); mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file return; } } QString SingleFileResourceBase::cacheFile() const { const QString currentDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir().mkpath(currentDir); return currentDir + QLatin1Char('/') + identifier(); } QByteArray SingleFileResourceBase::calculateHash(const QString &fileName) const { QFile file(fileName); if (!file.exists()) { return QByteArray(); } if (!file.open(QIODevice::ReadOnly)) { return QByteArray(); } QCryptographicHash hash(QCryptographicHash::Md5); qint64 blockSize = 512 * 1024; // Read blocks of 512K while (!file.atEnd()) { hash.addData(file.read(blockSize)); } file.close(); return hash.result(); } void SingleFileResourceBase::handleHashChange() { // Default implementation does nothing. qDebug() << "The hash has changed."; } QByteArray SingleFileResourceBase::loadHash() const { KConfigGroup generalGroup(runtimeConfig(), "General"); return QByteArray::fromHex(generalGroup.readEntry("hash", QByteArray())); } void SingleFileResourceBase::saveHash(const QByteArray &hash) const { KSharedConfig::Ptr config = runtimeConfig(); KConfigGroup generalGroup(config, "General"); generalGroup.writeEntry("hash", hash.toHex()); config->sync(); } void SingleFileResourceBase::setSupportedMimetypes(const QStringList &mimeTypes, const QString &icon) { mSupportedMimetypes = mimeTypes; mCollectionIcon = icon; } void SingleFileResourceBase::collectionChanged(const Akonadi::Collection &collection) { const QString newName = collection.displayName(); if (collection.hasAttribute()) { const EntityDisplayAttribute *attr = collection.attribute(); if (!attr->iconName().isEmpty()) { mCollectionIcon = attr->iconName(); } } if (newName != name()) { setName(newName); } changeCommitted(collection); } void SingleFileResourceBase::reloadFile() { // Update the network setting. setNeedsNetwork(!mCurrentUrl.isEmpty() && !mCurrentUrl.isLocalFile()); // if we have something loaded already, make sure we write that back in case // the settings changed if (!mCurrentUrl.isEmpty() && !readOnly()) { writeFile(); } readFile(); // name or rights could have changed synchronizeCollectionTree(); } void SingleFileResourceBase::handleProgress(KJob *, unsigned long pct) { Q_EMIT percent(pct); } void SingleFileResourceBase::fileChanged(const QString &fileName) { if (fileName != mCurrentUrl.toLocalFile()) { return; } const QByteArray newHash = calculateHash(fileName); // There is only a need to synchronize when the file was changed by another // process. At this point we're sure that it is the file that the resource // was configured for because of the check at the beginning of this function. if (newHash == mCurrentHash) { return; } if (!mCurrentUrl.isEmpty()) { QString lostFoundFileName; const QUrl prevUrl = mCurrentUrl; int i = 0; do { - lostFoundFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + identifier() + QDir::separator() + prevUrl.fileName() + QLatin1Char('-') + lostFoundFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + identifier() + QLatin1Char('/') + prevUrl.fileName() + QLatin1Char('-') + QString::number(++i); } while (QFileInfo::exists(lostFoundFileName)); // create the directory if it doesn't exist yet QDir dir = QFileInfo(lostFoundFileName).dir(); if (!dir.exists()) { dir.mkpath(dir.path()); } mCurrentUrl = QUrl::fromLocalFile(lostFoundFileName); writeFile(); mCurrentUrl = prevUrl; const QString message = i18n("The file '%1' was changed on disk. " "As a precaution, a backup of its previous contents has been created at '%2'.", prevUrl.toDisplayString(), QUrl::fromLocalFile(lostFoundFileName).toDisplayString()); Q_EMIT warning(message); } readFile(); // Notify resources, so that information bound to the file like indexes etc. // can be updated. handleHashChange(); invalidateCache(rootCollection()); synchronize(); } void SingleFileResourceBase::scheduleWrite() { scheduleCustomTask(this, "writeFile", QVariant(true), ResourceBase::AfterChangeReplay); } void SingleFileResourceBase::slotDownloadJobResult(KJob *job) { if (job->error() && job->error() != KIO::ERR_DOES_NOT_EXIST) { const QString message = i18n("Could not load file '%1'.", mCurrentUrl.toDisplayString()); qWarning() << message; Q_EMIT status(Broken, message); } else { readLocalFile(QUrl::fromLocalFile(cacheFile()).toLocalFile()); } mDownloadJob = nullptr; auto ref = job->property("QEventLoopLocker").value(); if (ref) { delete ref; } Q_EMIT status(Idle, i18nc("@info:status", "Ready")); } void SingleFileResourceBase::slotUploadJobResult(KJob *job) { if (job->error()) { const QString message = i18n("Could not save file '%1'.", mCurrentUrl.toDisplayString()); qWarning() << message; Q_EMIT status(Broken, message); } mUploadJob = nullptr; auto ref = job->property("QEventLoopLocker").value(); if (ref) { delete ref; } Q_EMIT status(Idle, i18nc("@info:status", "Ready")); } diff --git a/resources/vcarddir/vcarddirresource.cpp b/resources/vcarddir/vcarddirresource.cpp index be7b1d10b..fd3b95933 100644 --- a/resources/vcarddir/vcarddirresource.cpp +++ b/resources/vcarddir/vcarddirresource.cpp @@ -1,274 +1,274 @@ /* Copyright (c) 2008 Tobias Koenig Copyright (c) 2008 Bertjan Broeksema 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 "vcarddirresource.h" #include "settingsadaptor.h" #include "vcarddirresource_debug.h" #include #include #include #include #include #include #include using namespace Akonadi; VCardDirResource::VCardDirResource(const QString &id) : ResourceBase(id) { VcardDirResourceSettings::instance(KSharedConfig::openConfig()); // setup the resource new SettingsAdaptor(VcardDirResourceSettings::self()); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), VcardDirResourceSettings::self(), QDBusConnection::ExportAdaptors); changeRecorder()->itemFetchScope().fetchFullPayload(); connect(this, &VCardDirResource::reloadConfiguration, this, &VCardDirResource::slotReloadConfig); } VCardDirResource::~VCardDirResource() { // clear cache mAddressees.clear(); } void VCardDirResource::slotReloadConfig() { initializeVCardDirectory(); loadAddressees(); synchronize(); } void VCardDirResource::aboutToQuit() { VcardDirResourceSettings::self()->save(); } bool VCardDirResource::loadAddressees() { mAddressees.clear(); QDirIterator it(vCardDirectoryName()); while (it.hasNext()) { it.next(); if (it.fileName() != QLatin1String(".") && it.fileName() != QLatin1String("..") && it.fileName() != QLatin1String("WARNING_README.txt")) { QFile file(it.filePath()); if (file.open(QIODevice::ReadOnly)) { const QByteArray data = file.readAll(); file.close(); const KContacts::Addressee addr = mConverter.parseVCard(data); if (!addr.isEmpty()) { mAddressees.insert(addr.uid(), addr); } } else { qCCritical(VCARDDIRRESOURCE_LOG) << " file can't be load " << it.filePath(); } } } Q_EMIT status(Idle); return true; } bool VCardDirResource::retrieveItem(const Akonadi::Item &item, const QSet &) { const QString remoteId = item.remoteId(); if (!mAddressees.contains(remoteId)) { Q_EMIT error(i18n("Contact with uid '%1' not found.", remoteId)); return false; } Item newItem(item); newItem.setPayload(mAddressees.value(remoteId)); itemRetrieved(newItem); return true; } void VCardDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &) { if (VcardDirResourceSettings::self()->readOnly()) { Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", vCardDirectoryName())); cancelTask(); return; } KContacts::Addressee addressee; if (item.hasPayload()) { addressee = item.payload(); } if (!addressee.isEmpty()) { // add it to the cache... mAddressees.insert(addressee.uid(), addressee); // ... and write it through to the file system const QByteArray data = mConverter.createVCard(addressee); QFile file(vCardDirectoryFileName(addressee.uid())); file.open(QIODevice::WriteOnly); file.write(data); file.close(); // report everything ok Item newItem(item); newItem.setRemoteId(addressee.uid()); changeCommitted(newItem); } else { changeProcessed(); } } void VCardDirResource::itemChanged(const Akonadi::Item &item, const QSet &) { if (VcardDirResourceSettings::self()->readOnly()) { Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", vCardDirectoryName())); cancelTask(); return; } KContacts::Addressee addressee; if (item.hasPayload()) { addressee = item.payload(); } if (!addressee.isEmpty()) { // change it in the cache... mAddressees.insert(addressee.uid(), addressee); // ... and write it through to the file system const QByteArray data = mConverter.createVCard(addressee); QFile file(vCardDirectoryFileName(addressee.uid())); if (file.open(QIODevice::WriteOnly)) { file.write(data); file.close(); Item newItem(item); newItem.setRemoteId(addressee.uid()); changeCommitted(newItem); } else { qCritical() << " We can't write in file " << file.fileName(); } } else { changeProcessed(); } } void VCardDirResource::itemRemoved(const Akonadi::Item &item) { if (VcardDirResourceSettings::self()->readOnly()) { Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", vCardDirectoryName())); cancelTask(); return; } // remove it from the cache... mAddressees.remove(item.remoteId()); // ... and remove it from the file system QFile::remove(vCardDirectoryFileName(item.remoteId())); changeProcessed(); } void VCardDirResource::retrieveCollections() { Collection c; c.setParentCollection(Collection::root()); c.setRemoteId(vCardDirectoryName()); c.setName(name()); QStringList mimeTypes; mimeTypes << KContacts::Addressee::mimeType(); c.setContentMimeTypes(mimeTypes); if (VcardDirResourceSettings::self()->readOnly()) { c.setRights(Collection::ReadOnly); } else { Collection::Rights rights; rights |= Collection::CanChangeItem; rights |= Collection::CanCreateItem; rights |= Collection::CanDeleteItem; rights |= Collection::CanChangeCollection; c.setRights(rights); } EntityDisplayAttribute *attr = c.attribute(Collection::AddIfMissing); attr->setDisplayName(i18n("Contacts Folder")); attr->setIconName(QStringLiteral("x-office-address-book")); Collection::List list; list << c; collectionsRetrieved(list); } void VCardDirResource::retrieveItems(const Akonadi::Collection &) { Item::List items; items.reserve(mAddressees.count()); for (const KContacts::Addressee &addressee : qAsConst(mAddressees)) { Item item; item.setRemoteId(addressee.uid()); item.setMimeType(KContacts::Addressee::mimeType()); items.append(item); } itemsRetrieved(items); } QString VCardDirResource::vCardDirectoryName() const { return VcardDirResourceSettings::self()->path(); } QString VCardDirResource::vCardDirectoryFileName(const QString &file) const { - return VcardDirResourceSettings::self()->path() + QDir::separator() + file; + return VcardDirResourceSettings::self()->path() + QLatin1Char('/') + file; } void VCardDirResource::initializeVCardDirectory() const { QDir dir(vCardDirectoryName()); // if folder does not exists, create it if (!dir.exists()) { QDir::root().mkpath(dir.absolutePath()); } // check whether warning file is in place... - QFile file(dir.absolutePath() + QDir::separator() + QLatin1String("WARNING_README.txt")); + QFile file(dir.absolutePath() + QStringLiteral("/WARNING_README.txt")); if (!file.exists()) { // ... if not, create it file.open(QIODevice::WriteOnly); file.write("Important Warning!!!\n\n" "Don't create or copy vCards inside this folder manually, they are managed by the Akonadi framework!\n"); file.close(); } } AKONADI_RESOURCE_MAIN(VCardDirResource)