From 6c85a913c06661aa3f095cdf4bf278d5d65b6930 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 8 Jun 2016 17:08:36 +0200 Subject: Less hardcoded assumptions and a new RID scheme By using (folder local id:imap uid) for mails, we don't have to change mail rid's on folder renames. --- examples/imapresource/imapresource.cpp | 151 +++++++++++++++--------------- examples/imapresource/imapserverproxy.cpp | 79 +++++++++++++--- examples/imapresource/imapserverproxy.h | 22 ++++- 3 files changed, 163 insertions(+), 89 deletions(-) (limited to 'examples/imapresource') diff --git a/examples/imapresource/imapresource.cpp b/examples/imapresource/imapresource.cpp index 6168fa4..fee479a 100644 --- a/examples/imapresource/imapresource.cpp +++ b/examples/imapresource/imapresource.cpp @@ -103,6 +103,31 @@ public: }; +static qint64 uidFromMailRid(const QByteArray &remoteId) +{ + auto ridParts = remoteId.split(':'); + Q_ASSERT(ridParts.size() == 2); + return ridParts.last().toLongLong(); +} + +static QByteArray folderIdFromMailRid(const QByteArray &remoteId) +{ + auto ridParts = remoteId.split(':'); + Q_ASSERT(ridParts.size() == 2); + return ridParts.first(); +} + +static QByteArray assembleMailRid(const QByteArray &folderLocalId, qint64 imapUid) +{ + return folderLocalId + ':' + QByteArray::number(imapUid); +} + +static QByteArray assembleMailRid(const ApplicationDomain::Mail &mail, qint64 imapUid) +{ + return assembleMailRid(mail.getFolder(), imapUid); +} + + class ImapSynchronizer : public Sink::Synchronizer { public: ImapSynchronizer(const QByteArray &resourceType, const QByteArray &resourceInstanceIdentifier) @@ -111,18 +136,17 @@ public: } - QByteArray createFolder(const QString &folderPath, const QByteArray &icon) + QByteArray createFolder(const QString &folderName, const QString &folderPath, const QString &parentFolderRid, const QByteArray &icon) { - auto remoteId = folderPath.toUtf8(); - auto bufferType = ENTITY_TYPE_FOLDER; + Trace() << "Creating folder: " << folderName << parentFolderRid; + const auto remoteId = folderPath.toUtf8(); + const auto bufferType = ENTITY_TYPE_FOLDER; Sink::ApplicationDomain::Folder folder; - auto folderPathParts = folderPath.split('/'); - const auto name = folderPathParts.takeLast(); - folder.setProperty("name", name); + folder.setProperty("name", folderName); folder.setProperty("icon", icon); - if (!folderPathParts.isEmpty()) { - folder.setProperty("parent", syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderPathParts.join('/').toUtf8())); + if (!parentFolderRid.isEmpty()) { + folder.setProperty("parent", syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, parentFolderRid.toUtf8())); } createOrModify(bufferType, remoteId, folder); return remoteId; @@ -146,9 +170,9 @@ public: }); }, [&folderList](const QByteArray &remoteId) -> bool { - //folderList.contains(remoteId) + // folderList.contains(remoteId) for (const auto folderPath : folderList) { - if (folderPath.pathParts.join('/') == remoteId) { + if (folderPath.path == remoteId) { return true; } } @@ -156,21 +180,11 @@ public: } ); - for (const auto folderPath : folderList) { - createFolder(folderPath.pathParts.join('/'), "folder"); + for (const auto &f : folderList) { + createFolder(f.pathParts.last(), f.path, f.parentPath(), "folder"); } } - static QByteArray remoteIdForMessage(const QString &path, qint64 uid) - { - return path.toUtf8() + "/" + QByteArray::number(uid); - } - - static qint64 uidFromMessageRemoteId(const QByteArray &remoteId) - { - return remoteId.split('/').last().toLongLong(); - } - void synchronizeMails(const QString &path, const QVector &messages) { auto time = QSharedPointer::create(); @@ -184,7 +198,7 @@ public: int count = 0; for (const auto &message : messages) { count++; - const auto remoteId = path.toUtf8() + "/" + QByteArray::number(message.uid); + const auto remoteId = assembleMailRid(folderLocalId, message.uid); Trace() << "Found a mail " << remoteId << message.msg->subject(true)->asUnicodeString() << message.flags; @@ -237,7 +251,7 @@ public: }); }, [messages, path, &count](const QByteArray &remoteId) -> bool { - if (messages.contains(uidFromMessageRemoteId(remoteId))) { + if (messages.contains(uidFromMailRid(remoteId))) { return true; } count++; @@ -330,8 +344,7 @@ public: auto imap = QSharedPointer::create(mServer, mPort); auto login = imap->login(mUser, mPassword); if (operation == Sink::Operation_Creation) { - auto parentRemoteId = syncStore().resolveLocalId("folder", mail.getFolder()); - QString mailbox = parentRemoteId; + QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder()); QByteArray content = KMime::LFtoCRLF(mail.getMimeMessage()); QByteArrayList flags; if (!mail.getUnread()) { @@ -343,19 +356,18 @@ public: QDateTime internalDate = mail.getDate(); auto rid = QSharedPointer::create(); return login.then(imap->append(mailbox, content, flags, internalDate)) - .then([imap, mailbox, rid](qint64 uid) { - const auto remoteId = mailbox.toUtf8() + "/" + QByteArray::number(uid); - //FIXME this get's called after the final error ahndler? WTF? + .then([imap, mailbox, rid, mail](qint64 uid) { + const auto remoteId = assembleMailRid(mail, uid); + //FIXME this get's called after the final error handler? WTF? Trace() << "Finished creating a new mail: " << remoteId; *rid = remoteId; }).then([rid, imap]() { //FIXME fix KJob so we don't need this extra clause - return *rid; + return *rid; }); } else if (operation == Sink::Operation_Removal) { - auto ridParts = oldRemoteId.split('/'); - auto uid = ridParts.takeLast().toLongLong(); - //FIXME don't hardcode the separator - auto mailbox = ridParts.join('.'); + const auto folderId = folderIdFromMailRid(oldRemoteId); + const QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folderId); + const auto uid = uidFromMailRid(oldRemoteId); Trace() << "Removing a mail: " << oldRemoteId << "in the mailbox: " << mailbox; KIMAP::ImapSet set; set.add(uid); @@ -365,10 +377,9 @@ public: return QByteArray(); }); } else if (operation == Sink::Operation_Modification) { - auto ridParts = oldRemoteId.split('/'); - auto uid = ridParts.takeLast().toLongLong(); - //FIXME don't hardcode the separator - auto mailbox = ridParts.join('.'); + const QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder()); + const auto uid = uidFromMailRid(oldRemoteId); + Trace() << "Modifying a mail: " << oldRemoteId << " in the mailbox: " << mailbox << changedProperties; QByteArrayList flags; @@ -387,14 +398,14 @@ public: KIMAP::ImapSet set; set.add(uid); return login.then(imap->append(mailbox, content, flags, internalDate)) - .then([imap, mailbox, rid](qint64 uid) { - const auto remoteId = mailbox + "/" + QByteArray::number(uid); + .then([imap, mailbox, rid, mail](qint64 uid) { + const auto remoteId = assembleMailRid(mail, uid); Trace() << "Finished creating a modified mail: " << remoteId; *rid = remoteId; }) .then(imap->remove(mailbox, set)) .then([rid, imap]() { - return *rid; + return *rid; }); } else { KIMAP::ImapSet set; @@ -402,42 +413,34 @@ public: return login.then(imap->select(mailbox)) .then(imap->storeFlags(set, flags)) .then([imap, mailbox](qint64 uid) { - const auto remoteId = mailbox + "/" + QByteArray::number(uid); - Trace() << "Finished modifying mail: " << remoteId; + Trace() << "Finished modifying mail: " << uid; }) .then([oldRemoteId, imap]() { - return oldRemoteId; + return oldRemoteId; }); } } return KAsync::null(); } - QString buildPath(const ApplicationDomain::Folder &folder) - { - //Don't use entityStore in here, it can produce wrong results we're replaying an older revision than the latest one - QChar separator = '/'; - QString path; - auto parent = folder.getParent(); - if (!parent.isEmpty()) { - auto parentRemoteId = syncStore().resolveLocalId("folder", parent); - return parentRemoteId + separator.toLatin1() + folder.getName().toUtf8(); - } - return separator + folder.getName(); - } - KAsync::Job replay(const ApplicationDomain::Folder &folder, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE { auto imap = QSharedPointer::create(mServer, mPort); auto login = imap->login(mUser, mPassword); if (operation == Sink::Operation_Creation) { - auto folderPath = buildPath(folder); - auto imapPath = "INBOX" + folderPath.replace('/', '.'); - Trace() << "Creating a new folder: " << imapPath; - return login.then(imap->create(imapPath)) - .then([imapPath, imap]() { - Trace() << "Finished creating a new folder: " << imapPath; - return imapPath.toUtf8(); + QString parentFolder; + if (!folder.getParent().isEmpty()) { + parentFolder = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folder.getParent()); + } + Trace() << "Creating a new folder: " << parentFolder << folder.getName(); + auto rid = QSharedPointer::create(); + return login.then(imap->createSubfolder(parentFolder, folder.getName())) + .then([imap, rid](const QString &createdFolder) { + Trace() << "Finished creating a new folder: " << createdFolder; + *rid = createdFolder.toUtf8(); + }) + .then([rid](){ + return *rid; }); } else if (operation == Sink::Operation_Removal) { Trace() << "Removing a folder: " << oldRemoteId; @@ -447,13 +450,15 @@ public: return QByteArray(); }); } else if (operation == Sink::Operation_Modification) { - auto newFolderPath = buildPath(folder); - auto newImapPath = "INBOX" + newFolderPath.replace('/', '.'); - Trace() << "Renaming a folder: " << oldRemoteId << newImapPath; - return login.then(imap->rename(oldRemoteId, newImapPath)) - .then([newImapPath, imap]() { - Trace() << "Finished renaming a folder: " << newImapPath; - return newImapPath.toUtf8(); + Trace() << "Renaming a folder: " << oldRemoteId << folder.getName(); + auto rid = QSharedPointer::create(); + return login.then(imap->renameSubfolder(oldRemoteId, folder.getName())) + .then([imap, rid](const QString &createdFolder) { + Trace() << "Finished renaming a folder: " << createdFolder; + *rid = createdFolder.toUtf8(); + }) + .then([rid](){ + return *rid; }); } return KAsync::null(); @@ -522,7 +527,7 @@ KAsync::Job ImapResource::inspect(int inspectionType, const QByteArray &in Warning() << "Missing remote id for folder or mail. " << mailRemoteId << folderRemoteId; return KAsync::error(); } - auto uid = mailRemoteId.split('/').last().toLongLong(); + const auto uid = uidFromMailRid(mailRemoteId); Trace() << "Mail remote id: " << folderRemoteId << mailRemoteId << mail.identifier() << folder.identifier(); KIMAP::ImapSet set; @@ -606,7 +611,7 @@ KAsync::Job ImapResource::inspect(int inspectionType, const QByteArray &in })) .then>([imap, messageByUid, expectedCount]() { if (messageByUid->size() != expectedCount) { - return KAsync::error(1, QString("Wrong number of files; found %1 instead of %2.").arg(messageByUid->size()).arg(expectedCount)); + return KAsync::error(1, QString("Wrong number of messages on the server; found %1 instead of %2.").arg(messageByUid->size()).arg(expectedCount)); } return KAsync::null(); }); diff --git a/examples/imapresource/imapserverproxy.cpp b/examples/imapresource/imapserverproxy.cpp index 8c12b6c..db68a53 100644 --- a/examples/imapresource/imapserverproxy.cpp +++ b/examples/imapresource/imapserverproxy.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -98,15 +99,33 @@ KAsync::Job ImapServerProxy::login(const QString &username, const QString QObject::connect(capabilitiesJob, &KIMAP::CapabilitiesJob::capabilitiesReceived, [this](const QStringList &capabilities) { mCapabilities = capabilities; }); + auto namespaceJob = new KIMAP::NamespaceJob(mSession); + return runJob(loginJob).then(runJob(capabilitiesJob)).then([this](){ Trace() << "Supported capabilities: " << mCapabilities; - QStringList requiredExtensions = QStringList() << "UIDPLUS"; + QStringList requiredExtensions = QStringList() << "UIDPLUS" << "NAMESPACE"; for (const auto &requiredExtension : requiredExtensions) { if (!mCapabilities.contains(requiredExtension)) { Warning() << "Server doesn't support required capability: " << requiredExtension; //TODO fail the job } } + }).then(runJob(namespaceJob)).then([this, namespaceJob](){ + for (const auto &ns :namespaceJob->personalNamespaces()) { + mPersonalNamespaces << ns.name; + mPersonalNamespaceSeparator = ns.separator; + } + for (const auto &ns :namespaceJob->sharedNamespaces()) { + mSharedNamespaces << ns.name; + mSharedNamespaceSeparator = ns.separator; + } + for (const auto &ns :namespaceJob->userNamespaces()) { + mUserNamespaces << ns.name; + mUserNamespaceSeparator = ns.separator; + } + Trace() << "Found personal namespaces: " << mPersonalNamespaces << mPersonalNamespaceSeparator; + Trace() << "Found shared namespaces: " << mSharedNamespaces << mSharedNamespaceSeparator; + Trace() << "Found user namespaces: " << mUserNamespaces << mUserNamespaceSeparator; }); } @@ -271,16 +290,6 @@ KAsync::Job ImapServerProxy::list(KIMAP::ListJob::Option option, const std // listJob->setQueriedNamespaces(serverNamespaces()); QObject::connect(listJob, &KIMAP::ListJob::mailBoxesReceived, listJob, callback); - //Figure out the separator character on the first list issued. - if (mSeparatorCharacter.isNull()) { - QObject::connect(listJob, &KIMAP::ListJob::mailBoxesReceived, - listJob, [this](const QList &mailboxes,const QList > &flags) { - if (!mailboxes.isEmpty() && mSeparatorCharacter.isNull()) { - mSeparatorCharacter = mailboxes.first().separator; - } - } - ); - } return runJob(listJob); } @@ -295,6 +304,40 @@ KAsync::Job ImapServerProxy::remove(const QString &mailbox, const QByteArr return remove(mailbox, set); } +KAsync::Job ImapServerProxy::createSubfolder(const QString &parentMailbox, const QString &folderName) +{ + auto folder = QSharedPointer::create(); + return KAsync::start>([this, parentMailbox, folderName, folder]() { + Q_ASSERT(!mPersonalNamespaceSeparator.isNull()); + if (parentMailbox.isEmpty()) { + *folder = mPersonalNamespaces.toList().first() + folderName; + } else { + *folder = parentMailbox + mPersonalNamespaceSeparator + folderName; + } + Trace() << "Creating subfolder: " << *folder; + return create(*folder); + }) + .then([=]() { + return *folder; + }); +} + +KAsync::Job ImapServerProxy::renameSubfolder(const QString &oldMailbox, const QString &newName) +{ + auto folder = QSharedPointer::create(); + return KAsync::start>([this, oldMailbox, newName, folder]() { + Q_ASSERT(!mPersonalNamespaceSeparator.isNull()); + auto parts = oldMailbox.split(mPersonalNamespaceSeparator); + parts.removeLast(); + *folder = parts.join(mPersonalNamespaceSeparator) + mPersonalNamespaceSeparator + newName; + Trace() << "Renaming subfolder: " << oldMailbox << *folder; + return rename(oldMailbox, *folder); + }) + .then([=]() { + return *folder; + }); +} + KAsync::Job ImapServerProxy::fetchFolders(std::function &)> callback) { Trace() << "Fetching folders"; @@ -302,17 +345,23 @@ KAsync::Job ImapServerProxy::fetchFolders(std::function list; for (const auto &mailbox : mailboxes) { Trace() << "Found mailbox: " << mailbox.name; - list << Folder{mailbox.name.split(mailbox.separator)}; + list << Folder{mailbox.name.split(mailbox.separator), mailbox.name, mailbox.separator}; } callback(list); }); } +QString ImapServerProxy::mailboxFromFolder(const Folder &folder) const +{ + Q_ASSERT(!mPersonalNamespaceSeparator.isNull()); + return folder.pathParts.join(mPersonalNamespaceSeparator); +} + KAsync::Job ImapServerProxy::fetchMessages(const Folder &folder, std::function &)> callback) { - Q_ASSERT(!mSeparatorCharacter.isNull()); - return select(folder.pathParts.join(mSeparatorCharacter)).then>([this, callback, folder]() -> KAsync::Job { - return fetchHeaders(folder.pathParts.join(mSeparatorCharacter)).then, QList>([this, callback](const QList &uidsToFetch){ + Q_ASSERT(!mPersonalNamespaceSeparator.isNull()); + return select(mailboxFromFolder(folder)).then>([this, callback, folder]() -> KAsync::Job { + return fetchHeaders(mailboxFromFolder(folder)).then, QList>([this, callback](const QList &uidsToFetch){ Trace() << "Uids to fetch: " << uidsToFetch; if (uidsToFetch.isEmpty()) { Trace() << "Nothing to fetch"; diff --git a/examples/imapresource/imapserverproxy.h b/examples/imapresource/imapserverproxy.h index 05409c5..67ff000 100644 --- a/examples/imapresource/imapserverproxy.h +++ b/examples/imapresource/imapserverproxy.h @@ -54,13 +54,29 @@ struct Folder { return pathParts.join('/'); } + QString parentPath() const + { + auto parts = pathParts; + parts.removeLast(); + return parts.join(separator); + } + QList pathParts; + QString path; + QChar separator; }; class ImapServerProxy { KIMAP::Session *mSession; - QChar mSeparatorCharacter; QStringList mCapabilities; + + QSet mPersonalNamespaces; + QChar mPersonalNamespaceSeparator; + QSet mSharedNamespaces; + QChar mSharedNamespaceSeparator; + QSet mUserNamespaces; + QChar mUserNamespaceSeparator; + public: ImapServerProxy(const QString &serverUrl, int port); @@ -93,6 +109,10 @@ public: KAsync::Job> fetchHeaders(const QString &mailbox); KAsync::Job remove(const QString &mailbox, const KIMAP::ImapSet &set); KAsync::Job remove(const QString &mailbox, const QByteArray &imapSet); + KAsync::Job createSubfolder(const QString &parentMailbox, const QString &folderName); + KAsync::Job renameSubfolder(const QString &mailbox, const QString &newName); + + QString mailboxFromFolder(const Folder &) const; KAsync::Job fetchFolders(std::function &)> callback); KAsync::Job fetchMessages(const Folder &folder, std::function &)> callback); -- cgit v1.2.3