From 1710fab0965d32b883dfcc327c36d3fd38a91357 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 15 Dec 2015 17:05:20 +0100 Subject: A read-only maildir resource. Respectively a first prototype thereof. --- CMakeLists.txt | 1 + common/domain/mail.fbs | 1 + examples/CMakeLists.txt | 5 + examples/maildirresource/CMakeLists.txt | 15 + examples/maildirresource/domainadaptor.cpp | 35 + examples/maildirresource/domainadaptor.h | 38 + examples/maildirresource/facade.cpp | 41 + examples/maildirresource/facade.h | 36 + examples/maildirresource/libmaildir/CMakeLists.txt | 8 + examples/maildirresource/libmaildir/keycache.cpp | 88 +++ examples/maildirresource/libmaildir/keycache.h | 77 ++ examples/maildirresource/libmaildir/maildir.cpp | 865 +++++++++++++++++++++ examples/maildirresource/libmaildir/maildir.h | 266 +++++++ examples/maildirresource/maildirresource.cpp | 284 +++++++ examples/maildirresource/maildirresource.h | 61 ++ tests/CMakeLists.txt | 7 +- tests/maildirresourcetest.cpp | 66 ++ 17 files changed, 1893 insertions(+), 1 deletion(-) create mode 100644 examples/maildirresource/CMakeLists.txt create mode 100644 examples/maildirresource/domainadaptor.cpp create mode 100644 examples/maildirresource/domainadaptor.h create mode 100644 examples/maildirresource/facade.cpp create mode 100644 examples/maildirresource/facade.h create mode 100644 examples/maildirresource/libmaildir/CMakeLists.txt create mode 100644 examples/maildirresource/libmaildir/keycache.cpp create mode 100644 examples/maildirresource/libmaildir/keycache.h create mode 100644 examples/maildirresource/libmaildir/maildir.cpp create mode 100644 examples/maildirresource/libmaildir/maildir.h create mode 100644 examples/maildirresource/maildirresource.cpp create mode 100644 examples/maildirresource/maildirresource.h create mode 100644 tests/maildirresourcetest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d4c305e..c480ddd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 2.8.12) +option(BUILD_MAILDIR "BUILD_MAILDIR" ON) # ECM setup find_package(ECM 0.0.10 REQUIRED NO_MODULE) diff --git a/common/domain/mail.fbs b/common/domain/mail.fbs index 13aa36d..17b29a0 100644 --- a/common/domain/mail.fbs +++ b/common/domain/mail.fbs @@ -9,6 +9,7 @@ table Mail { date:string; unread:bool = false; important:bool = false; + mimeMessage:string; } root_type Mail; diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ea2b0ce..837903a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,3 +3,8 @@ add_subdirectory(client) # a simple dummy resource implementation add_subdirectory(dummyresource) + +if (BUILD_MAILDIR) + # a maildir resource implementation + add_subdirectory(maildirresource) +endif() diff --git a/examples/maildirresource/CMakeLists.txt b/examples/maildirresource/CMakeLists.txt new file mode 100644 index 0000000..2340cf6 --- /dev/null +++ b/examples/maildirresource/CMakeLists.txt @@ -0,0 +1,15 @@ +project(akonadi2_resource_maildir) + +add_definitions(-DQT_PLUGIN) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + +find_package(KF5 COMPONENTS REQUIRED Mime) + +add_library(${PROJECT_NAME} SHARED facade.cpp maildirresource.cpp domainadaptor.cpp) +# generate_flatbuffers(${PROJECT_NAME} dummycalendar) +qt5_use_modules(${PROJECT_NAME} Core Network) +target_link_libraries(${PROJECT_NAME} akonadi2common maildir KF5::Mime) + +install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${AKONADI2_RESOURCE_PLUGINS_PATH}) + +add_subdirectory(libmaildir) diff --git a/examples/maildirresource/domainadaptor.cpp b/examples/maildirresource/domainadaptor.cpp new file mode 100644 index 0000000..71b2354 --- /dev/null +++ b/examples/maildirresource/domainadaptor.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "domainadaptor.h" + +using namespace flatbuffers; + +MaildirMailAdaptorFactory::MaildirMailAdaptorFactory() + : DomainTypeAdaptorFactory() +{ + +} + +MaildirFolderAdaptorFactory::MaildirFolderAdaptorFactory() + : DomainTypeAdaptorFactory() +{ + +} + diff --git a/examples/maildirresource/domainadaptor.h b/examples/maildirresource/domainadaptor.h new file mode 100644 index 0000000..0fc7108 --- /dev/null +++ b/examples/maildirresource/domainadaptor.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once + +#include +#include "mail_generated.h" +#include "folder_generated.h" +#include "dummy_generated.h" + +class MaildirMailAdaptorFactory : public DomainTypeAdaptorFactory +{ +public: + MaildirMailAdaptorFactory(); + virtual ~MaildirMailAdaptorFactory() {}; +}; + +class MaildirFolderAdaptorFactory : public DomainTypeAdaptorFactory +{ +public: + MaildirFolderAdaptorFactory(); + virtual ~MaildirFolderAdaptorFactory() {}; +}; diff --git a/examples/maildirresource/facade.cpp b/examples/maildirresource/facade.cpp new file mode 100644 index 0000000..3cf3fde --- /dev/null +++ b/examples/maildirresource/facade.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "facade.h" + +#include "domainadaptor.h" + +MaildirResourceMailFacade::MaildirResourceMailFacade(const QByteArray &instanceIdentifier) + : Akonadi2::GenericFacade(instanceIdentifier, QSharedPointer::create()) +{ +} + +MaildirResourceMailFacade::~MaildirResourceMailFacade() +{ +} + + +MaildirResourceFolderFacade::MaildirResourceFolderFacade(const QByteArray &instanceIdentifier) + : Akonadi2::GenericFacade(instanceIdentifier, QSharedPointer::create()) +{ +} + +MaildirResourceFolderFacade::~MaildirResourceFolderFacade() +{ +} diff --git a/examples/maildirresource/facade.h b/examples/maildirresource/facade.h new file mode 100644 index 0000000..80f0d06 --- /dev/null +++ b/examples/maildirresource/facade.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "common/facade.h" + +class MaildirResourceMailFacade : public Akonadi2::GenericFacade +{ +public: + MaildirResourceMailFacade(const QByteArray &instanceIdentifier); + virtual ~MaildirResourceMailFacade(); +}; + +class MaildirResourceFolderFacade : public Akonadi2::GenericFacade +{ +public: + MaildirResourceFolderFacade(const QByteArray &instanceIdentifier); + virtual ~MaildirResourceFolderFacade(); +}; diff --git a/examples/maildirresource/libmaildir/CMakeLists.txt b/examples/maildirresource/libmaildir/CMakeLists.txt new file mode 100644 index 0000000..e7803f5 --- /dev/null +++ b/examples/maildirresource/libmaildir/CMakeLists.txt @@ -0,0 +1,8 @@ +# add_subdirectory( tests ) + +set(maildir_LIB_SRCS keycache.cpp maildir.cpp) + +add_library(maildir ${LIBRARY_TYPE} ${maildir_LIB_SRCS}) +qt5_use_modules(maildir Core Network) +# set_target_properties(maildir PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) +install(TARGETS maildir ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/examples/maildirresource/libmaildir/keycache.cpp b/examples/maildirresource/libmaildir/keycache.cpp new file mode 100644 index 0000000..814ce6c --- /dev/null +++ b/examples/maildirresource/libmaildir/keycache.cpp @@ -0,0 +1,88 @@ +/* + Copyright (C) 2012 Andras Mantia + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#include "keycache.h" + +#include + +KeyCache* KeyCache::mSelf = 0; + +void KeyCache::addKeys( const QString& dir ) +{ + if ( !mNewKeys.contains( dir ) ) { + mNewKeys.insert( dir, listNew( dir ) ); + //kDebug() << "Added new keys for: " << dir; + } + + if ( !mCurKeys.contains( dir ) ) { + mCurKeys.insert( dir, listCurrent( dir ) ); + //kDebug() << "Added cur keys for: " << dir; + } +} + +void KeyCache::refreshKeys( const QString& dir ) +{ + mNewKeys.remove( dir ); + mCurKeys.remove( dir ); + addKeys( dir ); +} + +void KeyCache::addNewKey( const QString& dir, const QString& key ) +{ + mNewKeys[dir].insert( key ); + // kDebug() << "Added new key for : " << dir << " key: " << key; +} + +void KeyCache::addCurKey( const QString& dir, const QString& key ) +{ + mCurKeys[dir].insert( key ); + // kDebug() << "Added cur key for : " << dir << " key:" << key; +} + +void KeyCache::removeKey( const QString& dir, const QString& key ) +{ + //kDebug() << "Removed new and cur key for: " << dir << " key:" << key; + mNewKeys[dir].remove( key ); + mCurKeys[dir].remove( key ); +} + +bool KeyCache::isCurKey( const QString& dir, const QString& key ) const +{ + return mCurKeys.value( dir ).contains( key ); +} + +bool KeyCache::isNewKey( const QString& dir, const QString& key ) const +{ + return mNewKeys.value( dir ).contains( key ); +} + +QSet< QString > KeyCache::listNew( const QString& dir ) const +{ + QDir d( dir + QString::fromLatin1( "/new" ) ); + d.setSorting(QDir::NoSort); + return d.entryList( QDir::Files ).toSet(); +} + +QSet< QString > KeyCache::listCurrent( const QString& dir ) const +{ + QDir d( dir + QString::fromLatin1( "/cur" ) ); + d.setSorting(QDir::NoSort); + return d.entryList( QDir::Files ).toSet(); +} + diff --git a/examples/maildirresource/libmaildir/keycache.h b/examples/maildirresource/libmaildir/keycache.h new file mode 100644 index 0000000..3cce6f0 --- /dev/null +++ b/examples/maildirresource/libmaildir/keycache.h @@ -0,0 +1,77 @@ +/* + Copyright (C) 2012 Andras Mantia + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#ifndef KEYCACHE_H +#define KEYCACHE_H + +/** @brief a cache for the maildir keys (file names in cur/new folders). + * It is used to find if a file is in cur or new + */ + +#include +#include + +class KeyCache { + +public: + static KeyCache *self() + { + if ( !mSelf ) + mSelf = new KeyCache(); + return mSelf; + } + + /** Find the new and cur keys on the disk for @param dir and add them to the cache */ + void addKeys( const QString& dir ); + + /** Refresh the new and cur keys for @param dir */ + void refreshKeys( const QString& dir ); + + /** Add a "new" key for @param dir. */ + void addNewKey( const QString& dir, const QString& key ); + + /** Add a "cur" key for @param dir. */ + void addCurKey( const QString& dir, const QString& key ); + + /** Remove all keys associated with @param dir. */ + void removeKey( const QString& dir, const QString& key ); + + /** Check if the @param key is a "cur" key in @param dir */ + bool isCurKey( const QString& dir, const QString& key ) const; + + /** Check if the @param key is a "new" key in @param dir */ + bool isNewKey( const QString& dir, const QString& key ) const; + +private: + KeyCache() { + } + + QSet listNew( const QString& dir ) const; + + QSet listCurrent( const QString& dir ) const; + + QHash< QString, QSet > mNewKeys; + QHash< QString, QSet > mCurKeys; + + static KeyCache* mSelf; + +}; + + +#endif // KEYCACHE_H diff --git a/examples/maildirresource/libmaildir/maildir.cpp b/examples/maildirresource/libmaildir/maildir.cpp new file mode 100644 index 0000000..37bf6ea --- /dev/null +++ b/examples/maildirresource/libmaildir/maildir.cpp @@ -0,0 +1,865 @@ +/* + 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 + +//Define it to get more debug output to expense of operating speed +// #define DEBUG_KEYCACHE_CONSITENCY + + +// static void initRandomSeed() +// { +// static bool init = false; +// if (!init) { +// unsigned int seed; +// init = true; +// int fd = KDE_open("/dev/urandom", O_RDONLY); +// if (fd < 0 || ::read(fd, &seed, sizeof(seed)) != sizeof(seed)) { +// // No /dev/urandom... try something else. +// srand(getpid()); +// seed = rand() + time(0); +// } +// +// if (fd >= 0) +// close(fd); +// +// qsrand(seed); +// } +// } + + +bool removeDirAndContentsRecursively(const QString & path) +{ + bool success = true; + + QDir d; + d.setPath(path); + d.setFilter(QDir::Files | QDir::Dirs | QDir::Hidden | QDir::NoSymLinks); + + QFileInfoList list = d.entryInfoList(); + + Q_FOREACH (const QFileInfo &fi, list) { + if (fi.isDir()) { + if (fi.fileName() != QLatin1String(".") && fi.fileName() != QLatin1String("..")) { + success = success && removeDirAndContentsRecursively(fi.absoluteFilePath()); + } + } else { + success = success && d.remove(fi.absoluteFilePath()); + } + } + + if (success) { + success = success && d.rmdir(path); // nuke ourselves, we should be empty now + } + return success; +} + +using namespace KPIM; + +Q_GLOBAL_STATIC_WITH_ARGS(QRegExp, statusSeparatorRx, (":|!")) + +class Maildir::Private +{ +public: + Private(const QString& p, bool isRoot) + :path(p), isRoot(isRoot) + { + hostName = QHostInfo::localHostName(); + // The default implementation of QUuid::createUuid() doesn't use + // a seed that is random enough. Therefor we use our own initialization + // until this issue will be fixed in Qt 4.7. + // initRandomSeed(); + + //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 + QString::fromLatin1("/cur"); + paths << path + QString::fromLatin1("/new"); + paths << path + QString::fromLatin1("/tmp"); + return paths; + } + + QStringList listNew() const + { + QDir d(path + QString::fromLatin1("/new")); + d.setSorting(QDir::NoSort); + return d.entryList(QDir::Files); + } + + QStringList listCurrent() const + { + QDir d(path + QString::fromLatin1("/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)) { + qDebug() << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/new/") + key; + } +#endif + return path + QString::fromLatin1("/new/") + key; + } + if (keyCache->isCurKey(path, key)) { +#ifdef DEBUG_KEYCACHE_CONSITENCY + if (!QFile::exists(path + QString::fromLatin1("/cur/") + key)) { + qDebug() << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/cur/") + key; + } +#endif + return path + QString::fromLatin1("/cur/") + key; + } + QString realKey = path + QString::fromLatin1("/new/") + key; + + QFile f(realKey); + if (f.exists()) { + keyCache->addNewKey(path, key); + } else { //not in "new", search in "cur" + realKey = path + QString::fromLatin1("/cur/") + key; + QFile f2(realKey); + if (f2.exists()) { + keyCache->addCurKey(path, key); + } else { + realKey.clear(); //not in "cur" either + } + } + + return realKey; + } + + static QString subDirNameForFolderName(const QString &folderName) + { + return QString::fromLatin1(".%1.directory").arg(folderName); + } + + QString subDirPath() const + { + QDir dir(path); + return subDirNameForFolderName(dir.dirName()); + } + + bool moveAndRename(QDir &dest, const QString &newName) + { + if (!dest.exists()) { + qDebug() << "Destination does not exist"; + return false; + } + if (dest.exists(newName) || dest.exists(subDirNameForFolderName(newName))) { + qDebug() << "New name already in use"; + return false; + } + + if (!dest.rename(path, newName)) { + qDebug() << "Failed to rename maildir"; + return false; + } + const QDir subDirs(Maildir::subDirPathForFolderPath(path)); + if (subDirs.exists() && !dest.rename(subDirs.path(), subDirNameForFolderName(newName))) { + qDebug() << "Failed to rename subfolders"; + return false; + } + + path = dest.path() + QDir::separator() + newName; + return true; + } + + QString path; + bool isRoot; + QString hostName; + QString lastError; +}; + +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); + + Q_FOREACH (const QString &p, paths) { + if (!QFile::exists(p)) { + if (!createMissingFolders) { + // lastError = i18n("Error opening %1; this folder is missing.", p); + return false; + } + QDir().mkpath(p); + if (!QFile::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 { + Q_FOREACH (const QString &sf, subFolderList()) { + const Maildir subMd = Maildir(path() + QLatin1Char('/') + sf); + if (!subMd.isValid()) { + 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... + Q_FOREACH (const QString &p, d->subPaths()) { + 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 = removeDirAndContentsRecursively(dir.absolutePath() + QLatin1Char('/') + folderName); + QString subfolderName = subDirNameForFolderName(folderName); + if (dir.exists(subfolderName)) + result &= removeDirAndContentsRecursively(dir.absolutePath() + QLatin1Char('/') + subfolderName); + 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(); + result += d->listCurrent(); + } + // qDebug() <<"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 + QString::fromLatin1("/new"); + } + return QString(); +} + +QString Maildir::pathToCurrent() const +{ + if (isValid()) { + return d->path + QString::fromLatin1("/cur"); + } + return QString(); +} + +QString Maildir::subDirPath() const +{ + QDir dir(d->path); + dir.cdUp(); + return dir.path() + QDir::separator() + 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(QLatin1String("cur")); + entries.removeAll(QLatin1String("new")); + entries.removeAll(QLatin1String("tmp")); + return entries; +} + +QByteArray Maildir::readEntry(const QString& key) const +{ + QByteArray result; + + QString realKey(d->findRealKey(key)); + if (realKey.isEmpty()) { + // FIXME error handling? + qWarning() << "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? + qWarning() << "Maildir::size unable to find: " << key; + // d->lastError = i18n("Cannot locate mail file %1." , key); + return -1; + } + + 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()) { + qWarning() << "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? + qWarning() << "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()) { + qWarning() << "Maildir::readEntryHeaders unable to find: " << key; + // d->lastError = i18n("Cannot locate mail file %1." , key); + return QByteArray(); + } + + return readEntryHeadersFromFile(realKey); +} + + +static QString createUniqueFileName() +{ + qint64 time = QDateTime::currentMSecsSinceEpoch() / 1000; + int r = qrand() % 1000; + 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? + qWarning() << "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)) { + qWarning() << "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()) { + qWarning() << "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()) { +// qWarning() << "Maildir::changeEntryFlags unable to find: " << key; +// d->lastError = i18n("Cannot locate mail file %1." , key); +// return QString(); +// } +// +// const QRegExp rx = *(statusSeparatorRx()); +// QString finalKey = key.left(key.indexOf(rx)); +// +// QStringList mailDirFlags; +// Q_FOREACH (const Akonadi::Item::Flag &flag, flags) { +// if (flag == Akonadi::MessageFlags::Forwarded) +// mailDirFlags << QLatin1String("P"); +// if (flag == Akonadi::MessageFlags::Replied) +// mailDirFlags << QLatin1String("R"); +// if (flag == Akonadi::MessageFlags::Seen) +// mailDirFlags << QLatin1String("S"); +// if (flag == Akonadi::MessageFlags::Deleted) +// mailDirFlags << QLatin1String("T"); +// if (flag == Akonadi::MessageFlags::Flagged) +// mailDirFlags << QLatin1String("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 +// } +// +// QString newUniqueKey = finalKey; //key without path +// finalKey.prepend(d->path + QString::fromLatin1("/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; +// while (QFile::exists(d->path + QString::fromLatin1("/cur/") + newFinalKey)) { +// i++; +// newFinalKey = QString::number(i) + QLatin1Char('-') + newUniqueKey; +// } +// finalKey = d->path + QString::fromLatin1("/cur/") + newFinalKey; +// } else { +// QFile::remove(finalKey); //they are the same +// } +// } +// +// if (!f.rename(finalKey)) { +// qWarning() << "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; +// } +// +Maildir::Flags Maildir::readEntryFlags(const QString& key) const +{ + Flags flags; + const QRegExp rx = *(statusSeparatorRx()); + const int index = key.indexOf(rx); + if (index != -1) { + const QString mailDirFlags = key.mid(index + 3); // after "(:|!)2," + const int flagSize(mailDirFlags.size()); + for (int i = 0; i < flagSize; ++i) { + if (mailDirFlags[i] == QLatin1Char('P')) + flags |= Forwarded; + else if (mailDirFlags[i] == QLatin1Char('R')) + flags |= Replied; + else if (mailDirFlags[i] == QLatin1Char('S')) + flags |= Seen; + else if (mailDirFlags[i] == QLatin1Char('F')) + flags |= 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()) { + qWarning() << "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; + if (!f.rename(targetKey)) { + qDebug() << "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(); + Q_FOREACH (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/examples/maildirresource/libmaildir/maildir.h b/examples/maildirresource/libmaildir/maildir.h new file mode 100644 index 0000000..6853033 --- /dev/null +++ b/examples/maildirresource/libmaildir/maildir.h @@ -0,0 +1,266 @@ +/* + 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. +*/ + +#ifndef MAILDIR_H +#define MAILDIR_H + + +#include "maildir_export.h" + +#include +#include + +class QDateTime; + +namespace KPIM { + +class MAILDIR_EXPORT Maildir +{ +public: + /** + Create a new Maildir object. + @param path The path to the maildir, if @p isRoot is @c false, that's the path + to the folder containing the cur/new/tmp folders, if @p isRoot is @c true this + is the path to a folder containing a number of maildirs. + @param isRoot Indicate whether this is a maildir containing mails and various + sub-folders or a container only containing maildirs. + */ + explicit Maildir( const QString& path = QString(), bool isRoot = false ); + /* Copy constructor */ + Maildir(const Maildir & rhs); + /* Copy operator */ + Maildir& operator=(const Maildir & rhs); + /** Equality comparison */ + bool operator==(const Maildir & rhs) const; + /* Destructor */ + ~Maildir(); + + /** Returns whether the maildir has all the necessary subdirectories, + * that they are readable, etc. + * @param createMissingFolders if true (the default), the cur/new/tmp folders are created if they are missing + */ + bool isValid( bool createMissingFolders = true ) const; + + /** + * Returns whether this is a normal maildir or a container containing maildirs. + */ + bool isRoot() const; + + /** + * Make a valid maildir at the path of this Maildir object. This involves + * creating the necessary subdirs, etc. Note that an empty Maildir is + * not valid, unless it is given valid path, or until create( ) is + * called on it. + */ + bool create(); + + /** + * Returns the path of this maildir. + */ + QString path() const; + + /** + * Returns the name of this maildir. + */ + QString name() const; + + /** + * Returns the list of items (mails) in the maildir. These are keys, which + * map to filenames, internally, but that's an implementation detail, which + * should not be relied on. + */ + QStringList entryList() const; + + /** Returns the list of items (mails) in the maildirs "new" folder. These are keys, which + * map to filenames, internally, but that's an implementation detail, which + * should not be relied on. + */ + QStringList listNew() const; + + /** Returns the list of items (mails) in the maildirs "cur" folder. These are keys, which + * map to filenames, internally, but that's an implementation detail, which + * should not be relied on. + */ + QStringList listCurrent() const; + + /** Return the path to the "new" directory */ + QString pathToNew() const; + + /** Return the path to the "cur" directory */ + QString pathToCurrent() const; + + /** + * Returns the full path to the subdir (the NAME.directory folder ). + **/ + QString subDirPath() const; + + /** + * Return the full path to the file identified by key (it can be either in the "new" or "cur" folder + **/ + QString findRealKey( const QString& key ) const; + + /** + * Returns the list of subfolders, as names (relative paths). Use the + * subFolder method to get Maildir objects representing them. + */ + QStringList subFolderList() const; + + /** + * Adds subfolder with the given @p folderName. + * @return an empty string on failure or the full path of the new subfolder + * on success + */ + QString addSubFolder( const QString& folderName ); + + /** + * Removes subfolder with the given @p folderName. Returns success or failure. + */ + bool removeSubFolder( const QString& folderName ); + + /** + * Returns a Maildir object for the given @p folderName. If such a folder + * exists, the Maildir object will be valid, otherwise you can call create() + * on it, to make a subfolder with that name. + */ + Maildir subFolder( const QString& folderName ) const; + + /** + * Returns the parent Maildir object for this Maildir, if there is one (ie. this is not the root). + */ + Maildir parent() const; + + /** + * Returns the size of the file in the maildir with the given @p key or \c -1 if key is not valid. + * @since 4.2 + */ + qint64 size( const QString& key ) const; + + /** + * Returns the modification time of the file in the maildir with the given @p key. + * @since 4.7 + */ + QDateTime lastModified( const QString &key ) const; + + /** + * Return the contents of the file in the maildir with the given @p key. + */ + QByteArray readEntry( const QString& key ) const; + + enum MailFlags { + Forwarded, + Replied, + Seen, + Flagged + }; + Q_DECLARE_FLAGS(Flags, MailFlags); + + /** + * Return the flags encoded in the maildir file name for an entry + **/ + Flags readEntryFlags( const QString& key ) const; + + /** + * Return the contents of the headers section of the file the maildir with the given @p file, that + * is a full path to the file. You can get it by using findRealKey(key) . + */ + QByteArray readEntryHeadersFromFile( const QString& file ) const; + + /** + * Return the contents of the headers section of the file the maildir with the given @p key. + */ + QByteArray readEntryHeaders( const QString& key ) const; + + /** + * Write the given @p data to a file in the maildir with the given @p key. + * Returns true in case of success, false in case of any error. + */ + bool writeEntry( const QString& key, const QByteArray& data ); + + /** + * Adds the given @p data to the maildir. Returns the key of the entry. + */ + QString addEntry( const QByteArray& data ); + + /** + * Removes the entry with the given @p key. Returns success or failure. + */ + bool removeEntry( const QString& key ); + + /** + * Change the flags for an entry specified by @p key. Returns the new key of the entry (the key might change because + * flags are stored in the unique filename). + */ + // QString changeEntryFlags( const QString& key, const Akonadi::Item::Flags& flags ); + + /** + * Moves this maildir into @p destination. + */ + bool moveTo( const Maildir &destination ); + + /** + * Renames this maildir to @p newName. + */ + bool rename( const QString &newName ); + + /** + * Moves the file with the given @p key into the Maildir @p destination. + * @returns The new file name inside @p destination. + */ + QString moveEntryTo( const QString& key, const KPIM::Maildir& destination ); + + /** + * Creates the maildir tree structure specific directory path that the + * given @p folderPath folder would have for its sub folders + * @param folderPath a maildir folder path + * @return the relative subDirPath for the given @p folderPath + * + * @see subDirNameForFolderName() + */ + static QString subDirPathForFolderPath( const QString &folderPath ); + + /** + * Creates the maildir tree structure specific directory name that the + * given @p folderName folder would have for its sub folders + * @param folderName a maildir folder name + * @return the relative subDirName for the given @p folderMame + * + * @see subDirPathForFolderPath() + */ + static QString subDirNameForFolderName( const QString &folderName ); + + /** Removes the listed keys from the key cache */ + void removeCachedKeys(const QStringList & keys); + + + /** Reloads the keys associated with the maildir in the key cache*/ + void refreshKeyCache(); + + /** Return the last error message string. The error might not come from the last performed operation, + if that was sucessful. The caller should always check the return value of the methods before + querying the last error string. */ + QString lastError() const; + +private: + void swap( const Maildir& ); + class Private; + Private *d; +}; + +} +#endif // __MAILDIR_H__ diff --git a/examples/maildirresource/maildirresource.cpp b/examples/maildirresource/maildirresource.cpp new file mode 100644 index 0000000..f9cc2a4 --- /dev/null +++ b/examples/maildirresource/maildirresource.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2015 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "maildirresource.h" +#include "facade.h" +#include "entitybuffer.h" +#include "pipeline.h" +#include "mail_generated.h" +#include "createentity_generated.h" +#include "domainadaptor.h" +#include "resourceconfig.h" +#include "commands.h" +#include "index.h" +#include "log.h" +#include "domain/mail.h" +#include "definitions.h" +#include "facadefactory.h" +#include "indexupdater.h" +#include "libmaildir/maildir.h" +#include +#include +#include +#include +#include + +//This is the resources entity type, and not the domain type +#define ENTITY_TYPE_MAIL "mail" +#define ENTITY_TYPE_FOLDER "folder" + +MaildirResource::MaildirResource(const QByteArray &instanceIdentifier, const QSharedPointer &pipeline) + : Akonadi2::GenericResource(instanceIdentifier, pipeline) +{ + addType(ENTITY_TYPE_MAIL, QSharedPointer::create(), + QVector() << new DefaultIndexUpdater); + addType(ENTITY_TYPE_MAIL, QSharedPointer::create(), + QVector() << new DefaultIndexUpdater); + auto config = ResourceConfig::getConfiguration(instanceIdentifier); + mMaildirPath = config.value("path").toString(); +} + +QString MaildirResource::resolveRemoteId(const QByteArray &bufferType, const QString &remoteId, Akonadi2::Storage::Transaction &transaction) +{ + //Lookup local id for remote id, or insert a new pair otherwise + auto remoteIdWithType = bufferType + remoteId.toUtf8(); + QByteArray akonadiId = Index("rid.mapping", transaction).lookup(remoteIdWithType); + if (akonadiId.isEmpty()) { + akonadiId = QUuid::createUuid().toString().toUtf8(); + Index("rid.mapping", transaction).add(remoteIdWithType, akonadiId); + } + return akonadiId; +} + +static QStringList listRecursive( const QString &root, const KPIM::Maildir &dir ) +{ + QStringList list; + foreach (const QString &sub, dir.subFolderList()) { + const KPIM::Maildir md = dir.subFolder(sub); + if (!md.isValid()) { + continue; + } + QString path = root + QDir::separator() + sub; + list << path; + list += listRecursive(path, md ); + } + return list; +} + +QStringList MaildirResource::listAvailableFolders() +{ + KPIM::Maildir dir(mMaildirPath, true); + if (!dir.isValid()) { + return QStringList(); + } + QStringList folderList; + folderList << mMaildirPath; + folderList += listRecursive(mMaildirPath, dir); + return folderList; +} + +void MaildirResource::synchronizeFolders(Akonadi2::Storage::Transaction &transaction) +{ + const QString bufferType = ENTITY_TYPE_FOLDER; + QStringList folderList = listAvailableFolders(); + Trace() << "Found folders " << folderList; + + Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite); + auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite); + Index ridMapping("rid.mapping", synchronizationTransaction); + for (const auto folder : folderList) { + const auto remoteId = folder.toUtf8(); + auto akonadiId = resolveRemoteId(bufferType.toUtf8(), remoteId, synchronizationTransaction); + + bool found = false; + transaction.openDatabase(bufferType.toUtf8() + ".main").scan(akonadiId.toUtf8(), [&found](const QByteArray &, const QByteArray &) -> bool { + found = true; + return false; + }, [this](const Akonadi2::Storage::Error &error) { + }, true); + + if (!found) { //A new entity + m_fbb.Clear(); + + KPIM::Maildir md(folder); + + flatbuffers::FlatBufferBuilder entityFbb; + auto name = m_fbb.CreateString(md.name().toStdString()); + auto icon = m_fbb.CreateString("folder"); + flatbuffers::Offset parent; + + if (!md.isRoot()) { + auto akonadiId = resolveRemoteId(ENTITY_TYPE_FOLDER, md.parent().path(), transaction); + parent = m_fbb.CreateString(akonadiId.toStdString()); + } + + auto builder = Akonadi2::ApplicationDomain::Buffer::FolderBuilder(m_fbb); + builder.add_name(name); + if (!md.isRoot()) { + builder.add_parent(parent); + } + builder.add_icon(icon); + auto buffer = builder.Finish(); + Akonadi2::ApplicationDomain::Buffer::FinishFolderBuffer(m_fbb, buffer); + Akonadi2::EntityBuffer::assembleEntityBuffer(entityFbb, 0, 0, 0, 0, m_fbb.GetBufferPointer(), m_fbb.GetSize()); + + flatbuffers::FlatBufferBuilder fbb; + //This is the resource type and not the domain type + auto entityId = fbb.CreateString(akonadiId.toStdString()); + auto type = fbb.CreateString(bufferType.toStdString()); + auto delta = Akonadi2::EntityBuffer::appendAsVector(fbb, entityFbb.GetBufferPointer(), entityFbb.GetSize()); + auto location = Akonadi2::Commands::CreateCreateEntity(fbb, entityId, type, delta); + Akonadi2::Commands::FinishCreateEntityBuffer(fbb, location); + + Trace() << "Found a new entity: " << remoteId; + enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, QByteArray::fromRawData(reinterpret_cast(fbb.GetBufferPointer()), fbb.GetSize())); + } else { //modification + Trace() << "Found a modified entity: " << remoteId; + //TODO diff and create modification if necessary + } + } + //TODO find items to remove +} + +void MaildirResource::synchronizeMails(Akonadi2::Storage::Transaction &transaction, const QString &path) +{ + Trace() << "Synchronizing mails" << path; + const QString bufferType = ENTITY_TYPE_MAIL; + + KPIM::Maildir maildir(path, true); + if (!maildir.isValid()) { + Warning() << "Failed to sync folder " << maildir.lastError(); + return; + } + + auto listingPath = maildir.pathToCurrent(); + auto entryIterator = QSharedPointer::create(listingPath, QDir::Files); + Trace() << "Looking into " << maildir.pathToNew(); + + QFileInfo entryInfo; + + Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite); + auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite); + Index ridMapping("rid.mapping", synchronizationTransaction); + + while (entryIterator->hasNext()) { + QString filePath = entryIterator->next(); + QString fileName = entryIterator->fileName(); + + const auto remoteId = fileName.toUtf8(); + auto akonadiId = resolveRemoteId(bufferType.toUtf8(), remoteId, synchronizationTransaction); + + bool found = false; + transaction.openDatabase(bufferType.toUtf8() + ".main").scan(akonadiId.toUtf8(), [&found](const QByteArray &, const QByteArray &) -> bool { + found = true; + return false; + }, [this](const Akonadi2::Storage::Error &error) { + }, true); + + if (!found) { //A new entity + m_fbb.Clear(); + + KMime::Message *msg = new KMime::Message; + auto filepath = listingPath + QDir::separator() + fileName; + msg->setHead(KMime::CRLFtoLF(maildir.readEntryHeadersFromFile(filepath))); + msg->parse(); + + const auto flags = maildir.readEntryFlags(fileName); + + Trace() << "Found a mail " << filePath << fileName << msg->subject(true)->asUnicodeString(); + flatbuffers::FlatBufferBuilder entityFbb; + auto subject = m_fbb.CreateString(msg->subject(true)->asUnicodeString().toStdString()); + auto sender = m_fbb.CreateString(msg->from(true)->asUnicodeString().toStdString()); + auto senderName = m_fbb.CreateString(msg->from(true)->asUnicodeString().toStdString()); + auto date = m_fbb.CreateString(msg->date(true)->dateTime().toString().toStdString()); + auto folder = m_fbb.CreateString(resolveRemoteId(ENTITY_TYPE_FOLDER, path, transaction).toStdString()); + auto mimeMessage = m_fbb.CreateString(filepath.toStdString()); + + auto builder = Akonadi2::ApplicationDomain::Buffer::MailBuilder(m_fbb); + builder.add_subject(subject); + builder.add_sender(sender); + builder.add_senderName(senderName); + builder.add_unread(!(flags & KPIM::Maildir::Seen)); + builder.add_important(flags & KPIM::Maildir::Flagged); + builder.add_date(date); + builder.add_folder(folder); + builder.add_mimeMessage(mimeMessage); + auto buffer = builder.Finish(); + Akonadi2::ApplicationDomain::Buffer::FinishMailBuffer(m_fbb, buffer); + Akonadi2::EntityBuffer::assembleEntityBuffer(entityFbb, 0, 0, 0, 0, m_fbb.GetBufferPointer(), m_fbb.GetSize()); + + flatbuffers::FlatBufferBuilder fbb; + //This is the resource type and not the domain type + auto entityId = fbb.CreateString(akonadiId.toStdString()); + auto type = fbb.CreateString(bufferType.toStdString()); + auto delta = Akonadi2::EntityBuffer::appendAsVector(fbb, entityFbb.GetBufferPointer(), entityFbb.GetSize()); + auto location = Akonadi2::Commands::CreateCreateEntity(fbb, entityId, type, delta); + Akonadi2::Commands::FinishCreateEntityBuffer(fbb, location); + + Trace() << "Found a new entity: " << remoteId; + enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, QByteArray::fromRawData(reinterpret_cast(fbb.GetBufferPointer()), fbb.GetSize())); + } else { //modification + Trace() << "Found a modified entity: " << remoteId; + //TODO diff and create modification if necessary + } + } + //TODO find items to remove +} + +KAsync::Job MaildirResource::synchronizeWithSource() +{ + Log() << " Synchronizing"; + return KAsync::start([this]() { + auto transaction = Akonadi2::Storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier, Akonadi2::Storage::ReadOnly).createTransaction(Akonadi2::Storage::ReadOnly); + synchronizeFolders(transaction); + for (const auto &folder : listAvailableFolders()) { + synchronizeMails(transaction, folder); + } + }); +} + +KAsync::Job MaildirResource::replay(const QByteArray &type, const QByteArray &key, const QByteArray &value) +{ + Trace() << "Replaying " << key; + return KAsync::null(); +} + +void MaildirResource::removeFromDisk(const QByteArray &instanceIdentifier) +{ + GenericResource::removeFromDisk(instanceIdentifier); + Akonadi2::Storage(Akonadi2::storageLocation(), instanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite).removeFromDisk(); +} + +MaildirResourceFactory::MaildirResourceFactory(QObject *parent) + : Akonadi2::ResourceFactory(parent) +{ + +} + +Akonadi2::Resource *MaildirResourceFactory::createResource(const QByteArray &instanceIdentifier) +{ + return new MaildirResource(instanceIdentifier); +} + +void MaildirResourceFactory::registerFacades(Akonadi2::FacadeFactory &factory) +{ + factory.registerFacade(PLUGIN_NAME); + factory.registerFacade(PLUGIN_NAME); +} + diff --git a/examples/maildirresource/maildirresource.h b/examples/maildirresource/maildirresource.h new file mode 100644 index 0000000..a3e12d3 --- /dev/null +++ b/examples/maildirresource/maildirresource.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "common/genericresource.h" + +#include + +#include + +//TODO: a little ugly to have this in two places, once here and once in Q_PLUGIN_METADATA +#define PLUGIN_NAME "org.kde.maildir" + +class MaildirResource : public Akonadi2::GenericResource +{ +public: + MaildirResource(const QByteArray &instanceIdentifier, const QSharedPointer &pipeline = QSharedPointer()); + KAsync::Job synchronizeWithSource() Q_DECL_OVERRIDE; + static void removeFromDisk(const QByteArray &instanceIdentifier); +private: + KAsync::Job replay(const QByteArray &type, const QByteArray &key, const QByteArray &value) Q_DECL_OVERRIDE; + QString resolveRemoteId(const QByteArray &type, const QString &remoteId, Akonadi2::Storage::Transaction &transaction); + // void createMail(const QByteArray &rid, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb, Akonadi2::Storage::Transaction &); + // void createFolder(const QByteArray &rid, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb, Akonadi2::Storage::Transaction &); + // void synchronize(const QString &bufferType, const QMap > &data, Akonadi2::Storage::Transaction &transaction, std::function &data, flatbuffers::FlatBufferBuilder &entityFbb, Akonadi2::Storage::Transaction &)> createEntity); + void synchronizeFolders(Akonadi2::Storage::Transaction &transaction); + void synchronizeMails(Akonadi2::Storage::Transaction &transaction, const QString &folder); + QStringList listAvailableFolders(); + QString mMaildirPath; +}; + +class MaildirResourceFactory : public Akonadi2::ResourceFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.maildir") + Q_INTERFACES(Akonadi2::ResourceFactory) + +public: + MaildirResourceFactory(QObject *parent = 0); + + Akonadi2::Resource *createResource(const QByteArray &instanceIdentifier) Q_DECL_OVERRIDE; + void registerFacades(Akonadi2::FacadeFactory &factory) Q_DECL_OVERRIDE; +}; + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 11fe415..b909681 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -47,8 +47,13 @@ auto_tests ( pipelinetest querytest ) - target_link_libraries(dummyresourcetest akonadi2_resource_dummy) target_link_libraries(dummyresourcebenchmark akonadi2_resource_dummy) target_link_libraries(querytest akonadi2_resource_dummy) +if (BUILD_MAILDIR) + auto_tests ( + maildirresourcetest + ) + target_link_libraries(maildirresourcetest akonadi2_resource_maildir) +endif() diff --git a/tests/maildirresourcetest.cpp b/tests/maildirresourcetest.cpp new file mode 100644 index 0000000..c65cdf0 --- /dev/null +++ b/tests/maildirresourcetest.cpp @@ -0,0 +1,66 @@ +#include + +#include + +#include "maildirresource/maildirresource.h" +#include "clientapi.h" +#include "commands.h" +#include "entitybuffer.h" +#include "resourceconfig.h" +#include "modelresult.h" +#include "pipeline.h" +#include "log.h" + +/** + * Test of complete system using the maildir resource. + * + * This test requires the maildir resource installed. + */ +class MaildirResourceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + Akonadi2::Log::setDebugOutputLevel(Akonadi2::Log::Trace); + auto factory = Akonadi2::ResourceFactory::load("org.kde.maildir"); + QVERIFY(factory); + MaildirResource::removeFromDisk("org.kde.maildir.instance1"); + Akonadi2::ApplicationDomain::AkonadiResource resource; + resource.setProperty("identifier", "org.kde.maildir.instance1"); + resource.setProperty("type", "org.kde.maildir"); + resource.setProperty("path", "/work/build/local-mail"); + Akonadi2::Store::create(resource).exec().waitForFinished(); + } + + void cleanup() + { + Akonadi2::Store::shutdown(QByteArray("org.kde.maildir.instance1")).exec().waitForFinished(); + MaildirResource::removeFromDisk("org.kde.maildir.instance1"); + } + + void init() + { + qDebug(); + qDebug() << "-----------------------------------------"; + qDebug(); + } + + void testListFolders() + { + Akonadi2::Query query; + query.resources << "org.kde.maildir.instance1"; + query.syncOnDemand = true; + query.processAll = true; + + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + + auto model = Akonadi2::Store::loadModel(query); + QTRY_VERIFY(model->rowCount(QModelIndex()) > 1); + } + +}; + +QTEST_MAIN(MaildirResourceTest) +#include "maildirresourcetest.moc" -- cgit v1.2.3