File indexing completed on 2025-01-05 04:58:38

0001 /*
0002  *   Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
0003  *
0004  *   This program is free software; you can redistribute it and/or modify
0005  *   it under the terms of the GNU General Public License as published by
0006  *   the Free Software Foundation; either version 2 of the License, or
0007  *   (at your option) any later version.
0008  *
0009  *   This program is distributed in the hope that it will be useful,
0010  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012  *   GNU General Public License for more details.
0013  *
0014  *   You should have received a copy of the GNU General Public License
0015  *   along with this program; if not, write to the
0016  *   Free Software Foundation, Inc.,
0017  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
0018  */
0019 
0020 #include "imapresource.h"
0021 
0022 #include "facade.h"
0023 #include "resourceconfig.h"
0024 #include "commands.h"
0025 #include "index.h"
0026 #include "log.h"
0027 #include "definitions.h"
0028 #include "inspection.h"
0029 #include "synchronizer.h"
0030 #include "inspector.h"
0031 #include "query.h"
0032 
0033 #include <QDate>
0034 #include <QDateTime>
0035 #include <QUrl>
0036 
0037 #include "facadefactory.h"
0038 #include "adaptorfactoryregistry.h"
0039 
0040 #include "imapserverproxy.h"
0041 #include "mailpreprocessor.h"
0042 #include "specialpurposepreprocessor.h"
0043 
0044 //This is the resources entity type, and not the domain type
0045 #define ENTITY_TYPE_MAIL "mail"
0046 #define ENTITY_TYPE_FOLDER "folder"
0047 
0048 Q_DECLARE_METATYPE(QSharedPointer<Imap::ImapServerProxy>)
0049 
0050 using namespace Imap;
0051 using namespace Sink;
0052 
0053 static qint64 sCommitInterval = 100;
0054 
0055 static qint64 uidFromMailRid(const QByteArray &remoteId)
0056 {
0057     auto ridParts = remoteId.split(':');
0058     Q_ASSERT(ridParts.size() == 2);
0059     return ridParts.last().toLongLong();
0060 }
0061 
0062 static QByteArray folderIdFromMailRid(const QByteArray &remoteId)
0063 {
0064     auto ridParts = remoteId.split(':');
0065     Q_ASSERT(ridParts.size() == 2);
0066     return ridParts.first();
0067 }
0068 
0069 static QByteArray assembleMailRid(const QByteArray &folderLocalId, qint64 imapUid)
0070 {
0071     return folderLocalId + ':' + QByteArray::number(imapUid);
0072 }
0073 
0074 static QByteArray assembleMailRid(const ApplicationDomain::Mail &mail, qint64 imapUid)
0075 {
0076     return assembleMailRid(mail.getFolder(), imapUid);
0077 }
0078 
0079 static QByteArray folderRid(const Imap::Folder &folder)
0080 {
0081     return folder.path().toUtf8();
0082 }
0083 
0084 static QByteArray parentRid(const Imap::Folder &folder)
0085 {
0086     return folder.parentPath().toUtf8();
0087 }
0088 
0089 static QByteArray getSpecialPurposeType(const QByteArrayList &flags)
0090 {
0091     if (Imap::flagsContain(Imap::FolderFlags::Trash, flags)) {
0092         return ApplicationDomain::SpecialPurpose::Mail::trash;
0093     }
0094     if (Imap::flagsContain(Imap::FolderFlags::Drafts, flags)) {
0095         return ApplicationDomain::SpecialPurpose::Mail::drafts;
0096     }
0097     if (Imap::flagsContain(Imap::FolderFlags::Sent, flags)) {
0098         return ApplicationDomain::SpecialPurpose::Mail::sent;
0099     }
0100     return {};
0101 }
0102 
0103 static bool hasSpecialPurposeFlag(const QByteArrayList &flags)
0104 {
0105     return !getSpecialPurposeType(flags).isEmpty();
0106 }
0107 
0108 
0109 class ImapSynchronizer : public Sink::Synchronizer {
0110     Q_OBJECT
0111 public:
0112     ImapSynchronizer(const ResourceContext &resourceContext)
0113         : Sink::Synchronizer(resourceContext)
0114     {
0115 
0116     }
0117 
0118     QByteArray createFolder(const Imap::Folder &f)
0119     {
0120         const auto parentFolderRid = parentRid(f);
0121         bool isToplevel = parentFolderRid.isEmpty();
0122 
0123         SinkTraceCtx(mLogCtx) << "Creating folder: " << f.name() << parentFolderRid << f.flags;
0124 
0125         const auto remoteId = folderRid(f);
0126         Sink::ApplicationDomain::Folder folder;
0127         folder.setName(f.name());
0128         folder.setIcon("folder");
0129         folder.setEnabled(f.subscribed && !f.noselect);
0130         const auto specialPurpose = [&] {
0131             if (hasSpecialPurposeFlag(f.flags)) {
0132                 return getSpecialPurposeType(f.flags);
0133             } else if (SpecialPurpose::isSpecialPurposeFolderName(f.name()) && isToplevel) {
0134                 return SpecialPurpose::getSpecialPurposeType(f.name());
0135             }
0136             return QByteArray{};
0137         }();
0138         if (!specialPurpose.isEmpty()) {
0139             folder.setSpecialPurpose({specialPurpose});
0140         }
0141         //Always show the inbox
0142         if (specialPurpose == ApplicationDomain::SpecialPurpose::Mail::inbox) {
0143             folder.setEnabled(true);
0144         }
0145 
0146         if (!isToplevel) {
0147             folder.setParent(syncStore().resolveRemoteId(ApplicationDomain::Folder::name, parentFolderRid));
0148         }
0149         createOrModify(ApplicationDomain::getTypeName<ApplicationDomain::Folder>(), remoteId, folder);
0150         return remoteId;
0151     }
0152 
0153     static bool contains(const QVector<Folder> &folderList, const QByteArray &remoteId)
0154     {
0155         for (const auto &folder : folderList) {
0156             if (folderRid(folder) == remoteId) {
0157                 return true;
0158             }
0159         }
0160         return false;
0161     }
0162 
0163     void synchronizeFolders(const QVector<Folder> &folderList)
0164     {
0165         SinkTraceCtx(mLogCtx) << "Found folders " << folderList.size();
0166 
0167         scanForRemovals(ENTITY_TYPE_FOLDER,
0168             [&folderList](const QByteArray &remoteId) -> bool {
0169                 return contains(folderList, remoteId);
0170             }
0171         );
0172 
0173         for (const auto &f : folderList) {
0174             createFolder(f);
0175         }
0176     }
0177 
0178     static void setFlags(Sink::ApplicationDomain::Mail &mail, const KIMAP2::MessageFlags &flags)
0179     {
0180         mail.setUnread(!flags.contains(Imap::Flags::Seen));
0181         mail.setImportant(flags.contains(Imap::Flags::Flagged));
0182     }
0183 
0184     static KIMAP2::MessageFlags getFlags(const Sink::ApplicationDomain::Mail &mail)
0185     {
0186         KIMAP2::MessageFlags flags;
0187         if (!mail.getUnread()) {
0188             flags << Imap::Flags::Seen;
0189         }
0190         if (mail.getImportant()) {
0191             flags << Imap::Flags::Flagged;
0192         }
0193         return flags;
0194     }
0195 
0196     void createOrModifyMail(const QByteArray &folderRid, const QByteArray &folderLocalId, const Message &message)
0197     {
0198         SinkTraceCtx(mLogCtx) << "Importing new mail." << folderRid;
0199 
0200         const auto remoteId = assembleMailRid(folderLocalId, message.uid);
0201 
0202         Q_ASSERT(message.msg);
0203         SinkTraceCtx(mLogCtx) << "Found a mail " << remoteId << message.flags;
0204 
0205         auto mail = Sink::ApplicationDomain::Mail::create(mResourceInstanceIdentifier);
0206         mail.setFolder(folderLocalId);
0207         mail.setMimeMessage(message.msg->encodedContent(true));
0208         mail.setExtractedFullPayloadAvailable(message.fullPayload);
0209         setFlags(mail, message.flags);
0210 
0211         createOrModify(ENTITY_TYPE_MAIL, remoteId, mail);
0212     }
0213 
0214     void synchronizeRemovals(const QByteArray &folderRid, const QSet<qint64> &messages)
0215     {
0216         auto time = QSharedPointer<QTime>::create();
0217         time->start();
0218         const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRid);
0219         if (folderLocalId.isEmpty()) {
0220             SinkWarning() << "Failed to lookup local id of: " << folderRid;
0221             return;
0222         }
0223 
0224         SinkTraceCtx(mLogCtx) << "Finding removed mail: " << folderLocalId << " remoteId: " << folderRid;
0225 
0226         int count = scanForRemovals(ENTITY_TYPE_MAIL,
0227             [&](const std::function<void(const QByteArray &)> &callback) {
0228                 store().indexLookup<ApplicationDomain::Mail, ApplicationDomain::Mail::Folder>(folderLocalId, callback);
0229             },
0230             [&](const QByteArray &remoteId) {
0231                 return messages.contains(uidFromMailRid(remoteId));
0232             }
0233         );
0234 
0235         const auto elapsed = time->elapsed();
0236         SinkLog() << "Removed " << count << " mails in " << folderRid << Sink::Log::TraceTime(elapsed) << " " << elapsed/qMax(count, 1) << " [ms/mail]";
0237     }
0238 
0239     KAsync::Job<void> fetchFolderContents(QSharedPointer<ImapServerProxy> imap, const Imap::Folder &folder, const QDate &dateFilter, const SelectResult &selectResult)
0240     {
0241         const auto folderRemoteId = folderRid(folder);
0242         const auto logCtx = mLogCtx.subContext(folder.path().toUtf8());
0243 
0244         bool ok = false;
0245         const auto changedsince = syncStore().readValue(folderRemoteId, "changedsince").toLongLong(&ok);
0246 
0247         //If modseq should change on any change.
0248         if (ok && selectResult.highestModSequence == static_cast<quint64>(changedsince)) {
0249             SinkLogCtx(logCtx) << folder.path() << "highestModSequence didn't change, nothing to do.";
0250             return KAsync::null();
0251         }
0252 
0253         auto time = QSharedPointer<QTime>::create();
0254         time->start();
0255         auto totalCount = QSharedPointer<int>::create(0);
0256 
0257         //First we fetch flag changes for all messages. Since we don't know which messages are locally available we just get everything and only apply to what we have.
0258         return KAsync::start<qint64>([=] {
0259             const auto lastSeenUid = qMax(qint64{0}, syncStore().readValue(folderRemoteId, "uidnext").toLongLong() - 1);
0260             SinkLogCtx(logCtx) << "About to update flags" << folder.path() << "changedsince: " << changedsince << "last seen uid: " << lastSeenUid;
0261             //If we have any mails so far we start off by updating any changed flags using changedsince, unless we don't have any mails at all.
0262             if (ok && lastSeenUid >= 1) {
0263                 return imap->fetchFlags(KIMAP2::ImapSet(1, lastSeenUid), changedsince, [=](const Message &message) {
0264                     const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
0265                     const auto remoteId = assembleMailRid(folderLocalId, message.uid);
0266 
0267                     SinkLogCtx(logCtx) << "Updating mail flags " << remoteId << message.flags;
0268 
0269                     auto mail = Sink::ApplicationDomain::Mail::create(mResourceInstanceIdentifier);
0270                     setFlags(mail, message.flags);
0271 
0272                     modify(ENTITY_TYPE_MAIL, remoteId, mail);
0273                 })
0274                 .then<qint64>([=] {
0275                     SinkLogCtx(logCtx) << "Flags updated. New changedsince value: " << selectResult.highestModSequence;
0276                     syncStore().writeValue(folderRemoteId, "changedsince", QByteArray::number(selectResult.highestModSequence));
0277                     return selectResult.uidNext;
0278                 });
0279             } else {
0280                 //We hit this path on initial sync and simply record the current changedsince value
0281                 return KAsync::start<qint64>([=] {
0282                     SinkLogCtx(logCtx) << "No flags to update. New changedsince value: " << selectResult.highestModSequence;
0283                     syncStore().writeValue(folderRemoteId, "changedsince", QByteArray::number(selectResult.highestModSequence));
0284                     return selectResult.uidNext;
0285                 });
0286             }
0287         })
0288         //Next we synchronize the full set that is given by the date limit.
0289         //We fetch all data for this set.
0290         //This will also pull in any new messages in subsequent runs.
0291         .then([=] (qint64 serverUidNext){
0292             const auto lastSeenUid = syncStore().contains(folderRemoteId, "uidnext") ? qMax(qint64{0}, syncStore().readValue(folderRemoteId, "uidnext").toLongLong() - 1) : -1;
0293             auto job = [=] {
0294                 if (dateFilter.isValid()) {
0295                     SinkLogCtx(logCtx) << "Fetching messages since: " << dateFilter  << " or uid: " << lastSeenUid;
0296                     //Avoid creating a gap if we didn't fetch messages older than dateFilter, but aren't in the initial fetch either
0297                     if (syncStore().contains(folderRemoteId, "uidnext")) {
0298                         return imap->fetchUidsSince(dateFilter, lastSeenUid + 1);
0299                     } else {
0300                         return imap->fetchUidsSince(dateFilter);
0301                     }
0302                 } else {
0303                     SinkLogCtx(logCtx) << "Fetching messages.";
0304                     return imap->fetchUids();
0305                 }
0306             }();
0307             return job.then([=](const QVector<qint64> &uidsToFetch) {
0308                 SinkTraceCtx(logCtx) << "Received result set " << uidsToFetch;
0309                 SinkTraceCtx(logCtx) << "About to fetch mail" << folder.path();
0310 
0311                 //Make sure the uids are sorted in reverse order and drop everything below lastSeenUid (so we don't refetch what we already have)
0312                 QVector<qint64> filteredAndSorted = uidsToFetch;
0313                 std::sort(filteredAndSorted.begin(), filteredAndSorted.end(), std::greater<qint64>());
0314                 //Only filter the set if we have a valid lastSeenUid. Otherwise we would miss uid 1
0315                 if (lastSeenUid > 0) {
0316                     const auto lowerBound = std::lower_bound(filteredAndSorted.begin(), filteredAndSorted.end(), lastSeenUid, std::greater<qint64>());
0317                     if (lowerBound != filteredAndSorted.end()) {
0318                         filteredAndSorted.erase(lowerBound, filteredAndSorted.end());
0319                     }
0320                 }
0321 
0322                 if (filteredAndSorted.isEmpty()) {
0323                     SinkTraceCtx(logCtx) << "Nothing new to fetch for full set.";
0324                     if (serverUidNext) {
0325                         SinkLogCtx(logCtx) << "Storing the server side uidnext: " << serverUidNext << folder.path();
0326                         //If we don't receive a mail we should still record the updated uidnext value.
0327                         syncStore().writeValue(folderRemoteId, "uidnext", QByteArray::number(serverUidNext));
0328                     }
0329                     if (!syncStore().contains(folderRemoteId, "fullsetLowerbound")) {
0330                         syncStore().writeValue(folderRemoteId, "fullsetLowerbound", QByteArray::number(serverUidNext));
0331                     }
0332                     return KAsync::null();
0333                 }
0334 
0335                 const qint64 lowerBoundUid = filteredAndSorted.last();
0336                 *totalCount = filteredAndSorted.size();
0337 
0338                 auto maxUid = QSharedPointer<qint64>::create(filteredAndSorted.first());
0339                 SinkTraceCtx(logCtx) << "Uids to fetch for full set: " << filteredAndSorted;
0340 
0341                 bool headersOnly = false;
0342                 const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
0343                 return imap->fetchMessages(folder, filteredAndSorted, headersOnly, [=](const Message &m) {
0344                     if (*maxUid < m.uid) {
0345                         *maxUid = m.uid;
0346                     }
0347                     createOrModifyMail(folderRemoteId, folderLocalId, m);
0348                 },
0349                 [=](int progress, int total) {
0350                     reportProgress(progress, total, {folderLocalId});
0351                     //commit every 100 messages
0352                     if ((progress % sCommitInterval) == 0) {
0353                         commit();
0354                     }
0355                 })
0356                 .then([=] {
0357                     SinkLogCtx(logCtx) << "Highest found uid: " << *maxUid << folder.path() << " Full set lower bound: " << lowerBoundUid;
0358                     syncStore().writeValue(folderRemoteId, "uidnext", QByteArray::number(*maxUid + 1));
0359                     //Remember the lowest full message we fetched.
0360                     //This is used below to fetch headers for the rest.
0361                     if (!syncStore().contains(folderRemoteId, "fullsetLowerbound")) {
0362                         syncStore().writeValue(folderRemoteId, "fullsetLowerbound", QByteArray::number(lowerBoundUid));
0363                     }
0364                     commit();
0365                 });
0366             });
0367         })
0368         //For all remaining messages we fetch the headers only
0369         //This is supposed to make all existing messages avialable with at least the headers only.
0370         //If we succeed this only needs to happen once (everything new is fetched above as full message).
0371         .then<void>([=] {
0372             bool ok = false;
0373             const auto latestHeaderFetched = syncStore().readValue(folderRemoteId, "latestHeaderFetched").toLongLong();
0374             const auto fullsetLowerbound = syncStore().readValue(folderRemoteId, "fullsetLowerbound").toLongLong(&ok);
0375 
0376             if (ok && latestHeaderFetched < fullsetLowerbound) {
0377                 SinkLogCtx(logCtx) << "Fetching headers for all messages until " << fullsetLowerbound << ". Already available until " <<  latestHeaderFetched;
0378 
0379                 return imap->fetchUids()
0380                 .then([=] (const QVector<qint64> &uids) {
0381                     //sort in reverse order and remove everything greater than fullsetLowerbound.
0382                     //This gives us all emails for which we haven't fetched the full content yet.
0383                     QVector<qint64> toFetch = uids;
0384                     std::sort(toFetch.begin(), toFetch.end(), std::greater<qint64>());
0385                     if (fullsetLowerbound) {
0386                         auto upperBound = std::upper_bound(toFetch.begin(), toFetch.end(), fullsetLowerbound, std::greater<qint64>());
0387                         if (upperBound != toFetch.begin()) {
0388                             toFetch.erase(toFetch.begin(), upperBound);
0389                         }
0390                     }
0391                     SinkTraceCtx(logCtx) << "Uids to fetch for headers only: " << toFetch;
0392 
0393 
0394                     *totalCount = *totalCount += toFetch.size();
0395 
0396                     bool headersOnly = true;
0397                     const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
0398                     return imap->fetchMessages(folder, toFetch, headersOnly, [=](const Message &m) {
0399                         createOrModifyMail(folderRemoteId, folderLocalId, m);
0400                     },
0401                     [=](int progress, int total) {
0402                         reportProgress(progress, total, {folderLocalId});
0403                         //commit every 100 messages
0404                         if ((progress % sCommitInterval) == 0) {
0405                             commit();
0406                         }
0407                     });
0408                 })
0409                 .then([=] {
0410                     SinkLogCtx(logCtx) << "Headers fetched for folder: " << folder.path();
0411                     syncStore().writeValue(folderRemoteId, "latestHeaderFetched", QByteArray::number(fullsetLowerbound));
0412                     commit();
0413                 });
0414 
0415             } else {
0416                 SinkLogCtx(logCtx) << "No additional headers to fetch.";
0417             }
0418             return KAsync::null();
0419         })
0420         //Finally remove messages that are no longer existing on the server.
0421         .then([=] {
0422             const auto elapsed = time->elapsed();
0423             SinkLogCtx(mLogCtx) << "Synchronized " << *totalCount << " mails in " << folderRemoteId << Sink::Log::TraceTime(elapsed) << " " << elapsed/qMax(*totalCount, 1) << " [ms/mail]";
0424 
0425             //TODO do an examine with QRESYNC and remove VANISHED messages if supported instead
0426             return imap->fetchUids().then([=](const QVector<qint64> &uids) {
0427                 SinkTraceCtx(logCtx) << "Syncing removals: " << folder.path();
0428                 synchronizeRemovals(folderRemoteId, uids.toList().toSet());
0429                 commit();
0430             });
0431         });
0432     }
0433 
0434     KAsync::Job<SelectResult> examine(QSharedPointer<ImapServerProxy> imap, const Imap::Folder &folder)
0435     {
0436         const auto logCtx = mLogCtx.subContext(folder.path().toUtf8());
0437         const auto folderRemoteId = folderRid(folder);
0438         Q_ASSERT(!folderRemoteId.isEmpty());
0439         return imap->examine(folder)
0440             .then([=](const SelectResult &selectResult) {
0441                 bool ok = false;
0442                 const auto uidvalidity = syncStore().readValue(folderRemoteId, "uidvalidity").toLongLong(&ok);
0443                 SinkTraceCtx(logCtx) << "Checking UIDVALIDITY. Local" << uidvalidity << "remote " << selectResult.uidValidity;
0444                 if (ok && selectResult.uidValidity != uidvalidity) {
0445                     SinkWarningCtx(logCtx) << "UIDVALIDITY changed " << selectResult.uidValidity << uidvalidity;
0446                     syncStore().removePrefix(folderRemoteId);
0447                 }
0448                 syncStore().writeValue(folderRemoteId, "uidvalidity", QByteArray::number(selectResult.uidValidity));
0449                 return KAsync::value(selectResult);
0450             });
0451     }
0452 
0453     KAsync::Job<void> synchronizeFolder(QSharedPointer<ImapServerProxy> imap, const Imap::Folder &folder, const QDate &dateFilter, bool countOnly)
0454     {
0455         const auto logCtx = mLogCtx.subContext(folder.path().toUtf8());
0456         SinkLogCtx(logCtx) << "Synchronizing mails in folder: " << folderRid(folder);
0457         const auto folderRemoteId = folderRid(folder);
0458         if (folder.path().isEmpty() || folderRemoteId.isEmpty()) {
0459             SinkWarningCtx(logCtx) << "Invalid folder " << folderRemoteId << folder.path();
0460             return KAsync::error<void>("Invalid folder");
0461         }
0462 
0463         //Start by checking if UIDVALIDITY is still correct
0464         return KAsync::start([=] {
0465             return examine(imap, folder)
0466                 .then([=](const SelectResult &selectResult) {
0467                     if (countOnly) {
0468                         const auto uidNext = syncStore().readValue(folderRemoteId, "uidnext").toLongLong();
0469                         SinkTraceCtx(mLogCtx) << "Checking for new messages." << folderRemoteId << " Local uidnext: " << uidNext << " Server uidnext: " << selectResult.uidNext;
0470                         if (selectResult.uidNext > uidNext) {
0471                             const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
0472                             emitNotification(Notification::Info, ApplicationDomain::NewContentAvailable, {}, {}, ENTITY_TYPE_FOLDER, {folderLocalId});
0473                         }
0474                         return KAsync::null();
0475                     }
0476                     return fetchFolderContents(imap, folder, dateFilter, selectResult);
0477                 });
0478         });
0479     }
0480 
0481     Sink::QueryBase applyMailDefaults(const Sink::QueryBase &query)
0482     {
0483         if (mDaysToSync > 0) {
0484             auto defaultDateFilter = QDate::currentDate().addDays(0 - mDaysToSync);
0485             auto queryWithDefaults = query;
0486             if (!queryWithDefaults.hasFilter<ApplicationDomain::Mail::Date>()) {
0487                 queryWithDefaults.filter(ApplicationDomain::Mail::Date::name, QVariant::fromValue(defaultDateFilter));
0488             }
0489             return queryWithDefaults;
0490         }
0491         return query;
0492     }
0493 
0494     QList<Synchronizer::SyncRequest> getSyncRequests(const Sink::QueryBase &query) Q_DECL_OVERRIDE
0495     {
0496         QList<Synchronizer::SyncRequest> list;
0497         if (query.type() == ApplicationDomain::getTypeName<ApplicationDomain::Mail>()) {
0498             auto request = Synchronizer::SyncRequest{applyMailDefaults(query)};
0499             if (query.hasFilter(ApplicationDomain::Mail::Folder::name)) {
0500                 request.applicableEntities << query.getFilter(ApplicationDomain::Mail::Folder::name).value.toByteArray();
0501             }
0502             list << request;
0503         } else if (query.type() == ApplicationDomain::getTypeName<ApplicationDomain::Folder>()) {
0504             list << Synchronizer::SyncRequest{query};
0505             auto mailQuery = Sink::QueryBase(ApplicationDomain::getTypeName<ApplicationDomain::Mail>());
0506             //A pseudo property filter to express that we only need to know if there are new messages at all
0507             mailQuery.filter("countOnly", {true});
0508             list << Synchronizer::SyncRequest{mailQuery, QByteArray{}, Synchronizer::SyncRequest::RequestFlush};
0509         } else {
0510             list << Synchronizer::SyncRequest{Sink::QueryBase(ApplicationDomain::getTypeName<ApplicationDomain::Folder>())};
0511             //This request depends on the previous one so we flush first.
0512             list << Synchronizer::SyncRequest{applyMailDefaults(Sink::QueryBase(ApplicationDomain::getTypeName<ApplicationDomain::Mail>())), QByteArray{}, Synchronizer::SyncRequest::RequestFlush};
0513         }
0514         return list;
0515     }
0516 
0517     QByteArray getFolderFromLocalId(const QByteArray &id)
0518     {
0519         auto mailRemoteId = syncStore().resolveLocalId(ApplicationDomain::getTypeName<ApplicationDomain::Mail>(), id);
0520         if (mailRemoteId.isEmpty()) {
0521             return {};
0522         }
0523         return folderIdFromMailRid(mailRemoteId);
0524     }
0525 
0526     void mergeIntoQueue(const Synchronizer::SyncRequest &request, QList<Synchronizer::SyncRequest> &queue)  Q_DECL_OVERRIDE
0527     {
0528         auto isIndividualMailSync = [](const Synchronizer::SyncRequest &request) {
0529             if (request.requestType == SyncRequest::Synchronization) {
0530                 const auto query = request.query;
0531                 if (query.type() == ApplicationDomain::getTypeName<ApplicationDomain::Mail>()) {
0532                     return !query.ids().isEmpty();
0533                 }
0534             }
0535             return false;
0536 
0537         };
0538 
0539         if (isIndividualMailSync(request)) {
0540             auto newId = request.query.ids().first();
0541             auto requestFolder = getFolderFromLocalId(newId);
0542             if (requestFolder.isEmpty()) {
0543                 SinkWarningCtx(mLogCtx) << "Failed to find folder for local id. Ignoring request: " << request.query;
0544                 return;
0545             }
0546             for (auto &r : queue) {
0547                 if (isIndividualMailSync(r)) {
0548                     auto queueFolder = getFolderFromLocalId(r.query.ids().first());
0549                     if (requestFolder == queueFolder) {
0550                         //Merge
0551                         r.query.filter(newId);
0552                         SinkTrace() << "Merging request " << request.query;
0553                         SinkTrace() << " to " << r.query;
0554                         return;
0555                     }
0556                 }
0557             }
0558         }
0559         queue << request;
0560     }
0561 
0562     KAsync::Job<void> login(const QSharedPointer<ImapServerProxy> &imap)
0563     {
0564         SinkTrace() << "Connecting to:" << mServer << mPort;
0565         SinkTrace() << "as:" << mUser;
0566         return imap->login(mUser, secret())
0567         .addToContext(imap);
0568     }
0569 
0570     KAsync::Job<QVector<Folder>> getFolderList(const QSharedPointer<ImapServerProxy> &imap, const Sink::QueryBase &query)
0571     {
0572         auto localIds = [&] {
0573             if (query.hasFilter<ApplicationDomain::Mail::Folder>()) {
0574                 //If we have a folder filter fetch full payload of date-range & all headers
0575                 return resolveFilter(query.getFilter<ApplicationDomain::Mail::Folder>());
0576             }
0577             Sink::Query folderQuery;
0578             folderQuery.setType<ApplicationDomain::Folder>();
0579             folderQuery.filter<ApplicationDomain::Folder::Enabled>(true);
0580             return resolveQuery(folderQuery);
0581         }();
0582 
0583         QVector<Folder> folders;
0584         auto folderRemoteIds = syncStore().resolveLocalIds(ApplicationDomain::getTypeName<ApplicationDomain::Folder>(), localIds);
0585         for (const auto &r : folderRemoteIds) {
0586             Q_ASSERT(!r.isEmpty());
0587             folders << Folder{r};
0588         }
0589         return KAsync::value(folders);
0590     }
0591 
0592     KAsync::Error getError(const KAsync::Error &error)
0593     {
0594         if (error) {
0595             switch(error.errorCode) {
0596                 case Imap::CouldNotConnectError:
0597                     return {ApplicationDomain::ConnectionError, error.errorMessage};
0598                 case Imap::SslHandshakeError:
0599                 case Imap::LoginFailed:
0600                     return {ApplicationDomain::LoginError, error.errorMessage};
0601                 case Imap::HostNotFoundError:
0602                     return {ApplicationDomain::NoServerError, error.errorMessage};
0603                 case Imap::ConnectionLost:
0604                     return {ApplicationDomain::ConnectionLostError, error.errorMessage};
0605                 case Imap::MissingCredentialsError:
0606                     return {ApplicationDomain::MissingCredentialsError, error.errorMessage};
0607                 default:
0608                     return {ApplicationDomain::UnknownError, error.errorMessage};
0609             }
0610         }
0611         return {};
0612     }
0613 
0614     KAsync::Job<void> synchronizeWithSource(const Sink::QueryBase &query) Q_DECL_OVERRIDE
0615     {
0616         if (!QUrl{mServer}.isValid()) {
0617             return KAsync::error(ApplicationDomain::ConfigurationError, "Invalid server url: " + mServer);
0618         }
0619         auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, mEncryptionMode, mAuthenticationMode, &mSessionCache);
0620         if (query.type() == ApplicationDomain::getTypeName<ApplicationDomain::Folder>()) {
0621             return login(imap)
0622             .then([=] {
0623                 auto folderList = QSharedPointer<QVector<Folder>>::create();
0624                 return imap->fetchFolders([folderList](const Folder &folder) {
0625                     *folderList << folder;
0626                 })
0627                 .then([=]() {
0628                     synchronizeFolders(*folderList);
0629                     return KAsync::null();
0630                 });
0631             })
0632             .then([=] (const KAsync::Error &error) {
0633                 return imap->logout()
0634                     .then(KAsync::error(getError(error)));
0635             });
0636         } else if (query.type() == ApplicationDomain::getTypeName<ApplicationDomain::Mail>()) {
0637             //TODO
0638             //if we have a folder filter:
0639             //* execute the folder query and resolve the results to the remote identifier
0640             //* query only those folders
0641             //if we have a date filter:
0642             //* apply the date filter to the fetch
0643             //if we have no folder filter:
0644             //* fetch list of folders from server directly and sync (because we have no guarantee that the folder sync was already processed by the pipeline).
0645             return login(imap)
0646             .then([=] {
0647                 if (!query.ids().isEmpty()) {
0648                     //If we have mail id's simply fetch the full payload of those mails
0649                     QVector<qint64> toFetch;
0650                     auto mailRemoteIds = syncStore().resolveLocalIds(ApplicationDomain::getTypeName<ApplicationDomain::Mail>(), query.ids());
0651                     QByteArray folderRemoteId;
0652                     for (const auto &r : mailRemoteIds) {
0653                         const auto folderLocalId = folderIdFromMailRid(r);
0654                         auto f = syncStore().resolveLocalId(ApplicationDomain::getTypeName<ApplicationDomain::Folder>(), folderLocalId);
0655                         if (folderRemoteId.isEmpty()) {
0656                             folderRemoteId = f;
0657                         } else {
0658                             if (folderRemoteId != f) {
0659                                 SinkWarningCtx(mLogCtx) << "Not all messages come from the same folder " << r << folderRemoteId << ". Skipping message.";
0660                                 continue;
0661                             }
0662                         }
0663                         toFetch << uidFromMailRid(r);
0664                     }
0665                     SinkLog() << "Fetching messages: " << toFetch << folderRemoteId;
0666                     bool headersOnly = false;
0667                     const auto folderLocalId = syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, folderRemoteId);
0668                     return imap->fetchMessages(Folder{folderRemoteId}, toFetch, headersOnly, [=](const Message &m) {
0669                         createOrModifyMail(folderRemoteId, folderLocalId, m);
0670                     },
0671                     [=](int progress, int total) {
0672                         reportProgress(progress, total, {folderLocalId});
0673                         //commit every 100 messages
0674                         if ((progress % sCommitInterval) == 0) {
0675                             commit();
0676                         }
0677                     });
0678                 } else {
0679                     const QDate dateFilter = [&] {
0680                         auto filter = query.getFilter<ApplicationDomain::Mail::Date>();
0681                         if (filter.value.canConvert<QDate>()) {
0682                             SinkLog() << " with date-range " << filter.value.value<QDate>();
0683                             return filter.value.value<QDate>();
0684                         }
0685                         return QDate{};
0686                     }();
0687 
0688                     return getFolderList(imap, query)
0689                         .then([=](const QVector<Folder> &folders) {
0690                             auto job = KAsync::null<void>();
0691                             for (const auto &folder : folders) {
0692                                 job = job.then([=] {
0693                                     if (aborting()) {
0694                                         return KAsync::null();
0695                                     }
0696                                     return synchronizeFolder(imap, folder, dateFilter, query.hasFilter("countOnly"))
0697                                         .then([=](const KAsync::Error &error) {
0698                                             if (error) {
0699                                                 if (error.errorCode == Imap::CommandFailed) {
0700                                                     SinkWarning() << "Continuing after protocol error: " << folder.path() << "Error: " << error;
0701                                                     //Ignore protocol-level errors and continue
0702                                                     return KAsync::null();
0703                                                 }
0704                                                 SinkWarning() << "Aborting on error: " << folder.path() << "Error: " << error;
0705                                                 //Abort otherwise, e.g. if we disconnected
0706                                                 return KAsync::error(error);
0707                                             }
0708                                             return KAsync::null();
0709                                         });
0710                                 });
0711 
0712                             }
0713                             return job;
0714                         });
0715                 }
0716             })
0717             .then([=] (const KAsync::Error &error) {
0718                 return imap->logout()
0719                     .then(KAsync::error(getError(error)));
0720             });
0721         }
0722         return KAsync::error<void>("Nothing to do");
0723     }
0724     static QByteArray ensureCRLF(const QByteArray &data) {
0725         auto index = data.indexOf('\n');
0726         if (index > 0 && data.at(index - 1) == '\r') { //First line is LF-only terminated
0727             //Convert back and forth in case there's a mix. We don't want to expand CRLF into CRCRLF.
0728             return KMime::LFtoCRLF(KMime::CRLFtoLF(data));
0729         } else {
0730             return data;
0731         }
0732     }
0733 
0734     static bool validateContent(const QByteArray &data) {
0735         if (data.isEmpty()) {
0736             SinkError() << "No data available.";
0737             return false;
0738         }
0739         if (data.contains('\0')) {
0740             SinkError() << "Data contains NUL, this will fail with IMAP.";
0741             return false;
0742         }
0743         return true;
0744     }
0745 
0746     KAsync::Job<QByteArray> replayToImap(std::function<KAsync::Job<QByteArray>(const QSharedPointer<ImapServerProxy> &)> callback)
0747     {
0748         auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, mEncryptionMode, mAuthenticationMode, &mSessionCache);
0749         auto login = imap->login(mUser, secret());
0750         return login.then(callback(imap))
0751             .addToContext(imap)
0752             .then([=] (const KAsync::Error &error, const QByteArray &remoteId) {
0753                 if (error) {
0754                     SinkWarning() << "Error during changereplay: " << error.errorMessage;
0755                     return imap->logout()
0756                         .then(KAsync::error<QByteArray>(getError(error)));
0757                 }
0758                 return imap->logout()
0759                     .then(KAsync::value(remoteId));
0760             });
0761     }
0762 
0763     KAsync::Job<QByteArray> replay(const ApplicationDomain::Mail &mail, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE
0764     {
0765         if (operation != Sink::Operation_Creation) {
0766             if(oldRemoteId.isEmpty()) {
0767                 SinkWarning() << "Tried to replay modification without old remoteId.";
0768                 // Since we can't recover from the situation we just skip over the revision.
0769                 // This can for instance happen if creation failed, and we then process a removal or modification.
0770                 return KAsync::null<QByteArray>();
0771             }
0772         }
0773         if (operation == Sink::Operation_Creation) {
0774             const QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder());
0775             const auto content = ensureCRLF(mail.getMimeMessage());
0776             if (!validateContent(content)) {
0777                 SinkError() << "Validation failed during creation replay " << mail.identifier() << "\n  Content:" << content;
0778                 //We can't recover from this other than deleting the mail, so we skip it.
0779                 return KAsync::null<QByteArray>();
0780             }
0781             const auto flags = getFlags(mail);
0782             const QDateTime internalDate = mail.getDate();
0783             return replayToImap([&] (auto imap) {
0784                     return imap->append(mailbox, content, flags, internalDate)
0785                     .then([mail](qint64 uid) {
0786                         const auto remoteId = assembleMailRid(mail, uid);
0787                         SinkTrace() << "Finished creating a new mail: " << remoteId;
0788                         return remoteId;
0789                     });
0790             });
0791         } else if (operation == Sink::Operation_Removal) {
0792             const auto folderId = folderIdFromMailRid(oldRemoteId);
0793             const QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folderId);
0794             const auto uid = uidFromMailRid(oldRemoteId);
0795             SinkTrace() << "Removing a mail: " << oldRemoteId << "in the mailbox: " << mailbox;
0796             KIMAP2::ImapSet set;
0797             set.add(uid);
0798             return replayToImap([&] (auto imap) {
0799                 return imap->remove(mailbox, set)
0800                     .then([imap, oldRemoteId] {
0801                         SinkTrace() << "Finished removing a mail: " << oldRemoteId;
0802                         return QByteArray();
0803                     });
0804             });
0805         } else if (operation == Sink::Operation_Modification) {
0806             const QString mailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder());
0807             const auto uid = uidFromMailRid(oldRemoteId);
0808 
0809             SinkTrace() << "Modifying a mail: " << oldRemoteId << " in the mailbox: " << mailbox << changedProperties;
0810 
0811             auto flags = getFlags(mail);
0812 
0813             const bool messageMoved = changedProperties.contains(ApplicationDomain::Mail::Folder::name);
0814             const bool messageChanged = changedProperties.contains(ApplicationDomain::Mail::MimeMessage::name);
0815             if (messageChanged || messageMoved) {
0816                 const auto folderId = folderIdFromMailRid(oldRemoteId);
0817                 const QString oldMailbox = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folderId);
0818                 const auto content = ensureCRLF(mail.getMimeMessage());
0819                 if (!validateContent(content)) {
0820                     SinkError() << "Validation failed during modification replay " << mail.identifier() << "\n  Content:" << content;
0821                     //We can't recover from this other than deleting the mail, so we skip it.
0822                     return KAsync::null<QByteArray>();
0823                 }
0824                 const QDateTime internalDate = mail.getDate();
0825                 SinkTrace() << "Replacing message. Old mailbox: " << oldMailbox << "New mailbox: " << mailbox << "Flags: " << flags << "Content: " << content;
0826                 KIMAP2::ImapSet set;
0827                 set.add(uid);
0828                 return replayToImap([&] (auto imap) {
0829                     return imap->append(mailbox, content, flags, internalDate)
0830                         .then([=](qint64 uid) {
0831                             const auto remoteId = assembleMailRid(mail, uid);
0832                             SinkTrace() << "Finished creating a modified mail: " << remoteId;
0833                             return imap->remove(oldMailbox, set).then(KAsync::value(remoteId));
0834                         });
0835                 });
0836             } else {
0837                 SinkTrace() << "Updating flags only.";
0838                 KIMAP2::ImapSet set;
0839                 set.add(uid);
0840 
0841                 return replayToImap([&] (auto imap) {
0842                     return imap->select(mailbox)
0843                         .then(imap->storeFlags(set, flags))
0844                         .then([=] {
0845                             SinkTrace() << "Finished modifying mail";
0846                             return oldRemoteId;
0847                         });
0848                 });
0849             }
0850         }
0851         return KAsync::null<QByteArray>();
0852     }
0853 
0854     KAsync::Job<QByteArray> replay(const ApplicationDomain::Folder &folder, Sink::Operation operation, const QByteArray &oldRemoteId, const QList<QByteArray> &changedProperties) Q_DECL_OVERRIDE
0855     {
0856         if (operation != Sink::Operation_Creation) {
0857             if(oldRemoteId.isEmpty()) {
0858                 Q_ASSERT(false);
0859                 return KAsync::error<QByteArray>("Tried to replay modification without old remoteId.");
0860             }
0861         }
0862         if (operation == Sink::Operation_Creation) {
0863             QString parentFolder;
0864             if (!folder.getParent().isEmpty()) {
0865                 parentFolder = syncStore().resolveLocalId(ENTITY_TYPE_FOLDER, folder.getParent());
0866             }
0867             SinkTraceCtx(mLogCtx) << "Creating a new folder: " << parentFolder << folder.getName();
0868             auto rid = QSharedPointer<QByteArray>::create();
0869             if (folder.getSpecialPurpose().isEmpty()) {
0870                 return replayToImap([&] (auto imap) {
0871                     return imap->createSubfolder(parentFolder, folder.getName())
0872                         .then([this, imap, rid](const QString &createdFolder) {
0873                             SinkTraceCtx(mLogCtx) << "Finished creating a new folder: " << createdFolder;
0874                             *rid = createdFolder.toUtf8();
0875                         })
0876                         .then([rid](){
0877                             return *rid;
0878                         });
0879                 });
0880             } else { //We try to merge special purpose folders first
0881                 auto specialPurposeFolders = QSharedPointer<QHash<QByteArray, QString>>::create();
0882                 return replayToImap([&] (auto imap) {
0883                     return imap->fetchFolders([=](const Imap::Folder &folder) {
0884                             if (SpecialPurpose::isSpecialPurposeFolderName(folder.name())) {
0885                                 specialPurposeFolders->insert(SpecialPurpose::getSpecialPurposeType(folder.name()), folder.path());
0886                             };
0887                         })
0888                         .then([this, specialPurposeFolders, folder, imap, parentFolder, rid]() -> KAsync::Job<void> {
0889                             for (const auto &purpose : folder.getSpecialPurpose()) {
0890                                 if (specialPurposeFolders->contains(purpose)) {
0891                                     auto f = specialPurposeFolders->value(purpose);
0892                                     SinkTraceCtx(mLogCtx) << "Merging specialpurpose folder with: " << f << " with purpose: " << purpose;
0893                                     *rid = f.toUtf8();
0894                                     return KAsync::null<void>();
0895                                 }
0896                             }
0897                             SinkTraceCtx(mLogCtx) << "No match found for merging, creating a new folder";
0898                             return imap->createSubfolder(parentFolder, folder.getName())
0899                                 .then([this, imap, rid](const QString &createdFolder) {
0900                                     SinkTraceCtx(mLogCtx) << "Finished creating a new folder: " << createdFolder;
0901                                     *rid = createdFolder.toUtf8();
0902                                 });
0903 
0904                         })
0905                     .then([rid](){
0906                         return *rid;
0907                     });
0908                 });
0909             }
0910         } else if (operation == Sink::Operation_Removal) {
0911             SinkTraceCtx(mLogCtx) << "Removing a folder: " << oldRemoteId;
0912             return replayToImap([&] (auto imap) {
0913                 return imap->remove(oldRemoteId)
0914                     .then([this, oldRemoteId, imap] {
0915                         SinkTraceCtx(mLogCtx) << "Finished removing a folder: " << oldRemoteId;
0916                         return QByteArray();
0917                     });
0918             });
0919         } else if (operation == Sink::Operation_Modification) {
0920             SinkTraceCtx(mLogCtx) << "Modifying a folder: " << oldRemoteId << folder.getName();
0921             if (changedProperties.contains(ApplicationDomain::Folder::Name::name)) {
0922                 return replayToImap([&] (auto imap) {
0923                     auto rid = QSharedPointer<QByteArray>::create();
0924                     return imap->renameSubfolder(oldRemoteId, folder.getName())
0925                         .then([this, imap, rid](const QString &createdFolder) {
0926                             SinkTraceCtx(mLogCtx) << "Finished renaming a folder: " << createdFolder;
0927                             *rid = createdFolder.toUtf8();
0928                         })
0929                         .then([rid] {
0930                             return *rid;
0931                         });
0932                 });
0933             }
0934         }
0935         return KAsync::null<QByteArray>();
0936     }
0937 
0938 public:
0939     QString mServer;
0940     int mPort;
0941     Imap::EncryptionMode mEncryptionMode = Imap::NoEncryption;
0942     Imap::AuthenticationMode mAuthenticationMode;
0943     QString mUser;
0944     int mDaysToSync = 0;
0945     QByteArray mResourceInstanceIdentifier;
0946     Imap::SessionCache mSessionCache;
0947 };
0948 
0949 class ImapInspector : public Sink::Inspector {
0950 public:
0951     ImapInspector(const Sink::ResourceContext &resourceContext)
0952         : Sink::Inspector(resourceContext)
0953     {
0954 
0955     }
0956 
0957 protected:
0958     KAsync::Job<void> inspect(int inspectionType, const QByteArray &inspectionId, const QByteArray &domainType, const QByteArray &entityId, const QByteArray &property, const QVariant &expectedValue) Q_DECL_OVERRIDE {
0959 
0960 
0961         if (inspectionType == Sink::ResourceControl::Inspection::ConnectionInspectionType) {
0962             SinkLog() << "Checking the connection ";
0963             auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, mEncryptionMode, mAuthenticationMode);
0964             return imap->login(mUser, secret())
0965                 .addToContext(imap)
0966                 .then([] {
0967                     SinkLog() << "Login successful.";
0968                 })
0969                 .then(imap->fetchFolders([=](const Imap::Folder &f) {
0970                     SinkLog() << "Found a folder " << f.path();
0971                 }))
0972                 .then(imap->logout());
0973         }
0974 
0975         auto synchronizationStore = QSharedPointer<Sink::Storage::DataStore>::create(Sink::storageLocation(), mResourceContext.instanceId() + ".synchronization", Sink::Storage::DataStore::ReadOnly);
0976         auto synchronizationTransaction = synchronizationStore->createTransaction(Sink::Storage::DataStore::ReadOnly);
0977 
0978         auto mainStore = QSharedPointer<Sink::Storage::DataStore>::create(Sink::storageLocation(), mResourceContext.instanceId(), Sink::Storage::DataStore::ReadOnly);
0979         auto transaction = mainStore->createTransaction(Sink::Storage::DataStore::ReadOnly);
0980 
0981         Sink::Storage::EntityStore entityStore(mResourceContext, {"imapresource"});
0982         auto syncStore = QSharedPointer<Sink::SynchronizerStore>::create(synchronizationTransaction);
0983 
0984         SinkTrace() << "Inspecting " << inspectionType << domainType << entityId << property << expectedValue;
0985 
0986         if (domainType == ENTITY_TYPE_MAIL) {
0987             const auto mail = entityStore.readLatest<Sink::ApplicationDomain::Mail>(entityId);
0988             const auto folder = entityStore.readLatest<Sink::ApplicationDomain::Folder>(mail.getFolder());
0989             const auto folderRemoteId = syncStore->resolveLocalId(ENTITY_TYPE_FOLDER, mail.getFolder());
0990             const auto mailRemoteId = syncStore->resolveLocalId(ENTITY_TYPE_MAIL, mail.identifier());
0991             if (mailRemoteId.isEmpty() || folderRemoteId.isEmpty()) {
0992                 //There is no remote id to find if we expect the message to not exist
0993                 if (inspectionType == Sink::ResourceControl::Inspection::ExistenceInspectionType && !expectedValue.toBool()) {
0994                     return KAsync::null<void>();
0995                 }
0996                 SinkWarning() << "Missing remote id for folder or mail. " << mailRemoteId << folderRemoteId;
0997                 return KAsync::error<void>();
0998             }
0999             const auto uid = uidFromMailRid(mailRemoteId);
1000             SinkTrace() << "Mail remote id: " << folderRemoteId << mailRemoteId << mail.identifier() << folder.identifier();
1001 
1002             KIMAP2::ImapSet set;
1003             set.add(uid);
1004             if (set.isEmpty()) {
1005                 return KAsync::error<void>(1, "Couldn't determine uid of mail.");
1006             }
1007             KIMAP2::FetchJob::FetchScope scope;
1008             scope.mode = KIMAP2::FetchJob::FetchScope::Full;
1009             auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, mEncryptionMode, mAuthenticationMode);
1010             auto messageByUid = QSharedPointer<QHash<qint64, Imap::Message>>::create();
1011             SinkTrace() << "Connecting to:" << mServer << mPort;
1012             SinkTrace() << "as:" << mUser;
1013             auto inspectionJob = imap->login(mUser, secret())
1014                 .then(imap->select(folderRemoteId))
1015                 .then([](Imap::SelectResult){})
1016                 .then(imap->fetch(set, scope, [imap, messageByUid](const Imap::Message &message) {
1017                     //We avoid parsing normally, so we have to do it explicitly here
1018                     if (message.msg) {
1019                         message.msg->parse();
1020                     }
1021                     messageByUid->insert(message.uid, message);
1022                 }));
1023 
1024             if (inspectionType == Sink::ResourceControl::Inspection::PropertyInspectionType) {
1025                 if (property == "unread") {
1026                     return inspectionJob.then([=] {
1027                         auto msg = messageByUid->value(uid);
1028                         if (expectedValue.toBool() && msg.flags.contains(Imap::Flags::Seen)) {
1029                             return KAsync::error<void>(1, "Expected unread but couldn't find it.");
1030                         }
1031                         if (!expectedValue.toBool() && !msg.flags.contains(Imap::Flags::Seen)) {
1032                             return KAsync::error<void>(1, "Expected read but couldn't find it.");
1033                         }
1034                         return KAsync::null<void>();
1035                     });
1036                 }
1037                 if (property == "subject") {
1038                     return inspectionJob.then([=] {
1039                         auto msg = messageByUid->value(uid);
1040                         if (msg.msg->subject(true)->asUnicodeString() != expectedValue.toString()) {
1041                             return KAsync::error<void>(1, "Subject not as expected: " + msg.msg->subject(true)->asUnicodeString());
1042                         }
1043                         return KAsync::null<void>();
1044                     });
1045                 }
1046             }
1047             if (inspectionType == Sink::ResourceControl::Inspection::ExistenceInspectionType) {
1048                 return inspectionJob.then([=] {
1049                     if (!messageByUid->contains(uid)) {
1050                         SinkWarning() << "Existing messages are: " << messageByUid->keys();
1051                         SinkWarning() << "We're looking for: " << uid;
1052                         return KAsync::error<void>(1, "Couldn't find message: " + mailRemoteId);
1053                     }
1054                     return KAsync::null<void>();
1055                 });
1056             }
1057         }
1058         if (domainType == ENTITY_TYPE_FOLDER) {
1059             const auto remoteId = syncStore->resolveLocalId(ENTITY_TYPE_FOLDER, entityId);
1060             const auto folder = entityStore.readLatest<Sink::ApplicationDomain::Folder>(entityId);
1061 
1062             if (inspectionType == Sink::ResourceControl::Inspection::CacheIntegrityInspectionType) {
1063                 SinkLog() << "Inspecting cache integrity" << remoteId;
1064 
1065                 int expectedCount = 0;
1066                 Index index("mail.index.folder", transaction);
1067                 index.lookup(entityId, [&](const QByteArray &sinkId) {
1068                     expectedCount++;
1069                     return true;
1070                 },
1071                 [&](const Index::Error &error) {
1072                     SinkWarning() << "Error in index: " <<  error.message << property;
1073                 });
1074 
1075                 auto set = KIMAP2::ImapSet::fromImapSequenceSet("1:*");
1076                 KIMAP2::FetchJob::FetchScope scope;
1077                 scope.mode = KIMAP2::FetchJob::FetchScope::Headers;
1078                 auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, mEncryptionMode, mAuthenticationMode);
1079                 auto messageByUid = QSharedPointer<QHash<qint64, Imap::Message>>::create();
1080                 return imap->login(mUser, secret())
1081                     .then(imap->select(remoteId))
1082                     .then(imap->fetch(set, scope, [=](const Imap::Message message) {
1083                         messageByUid->insert(message.uid, message);
1084                     }))
1085                     .then([imap, messageByUid, expectedCount] {
1086                         if (messageByUid->size() != expectedCount) {
1087                             return KAsync::error<void>(1, QString("Wrong number of messages on the server; found %1 instead of %2.").arg(messageByUid->size()).arg(expectedCount));
1088                         }
1089                         return KAsync::null<void>();
1090                     });
1091             }
1092             if (inspectionType == Sink::ResourceControl::Inspection::ExistenceInspectionType) {
1093                 auto  folderByPath = QSharedPointer<QSet<QString>>::create();
1094                 auto  folderByName = QSharedPointer<QSet<QString>>::create();
1095 
1096                 auto imap = QSharedPointer<ImapServerProxy>::create(mServer, mPort, mEncryptionMode, mAuthenticationMode);
1097                 auto inspectionJob = imap->login(mUser, secret())
1098                     .then(imap->fetchFolders([=](const Imap::Folder &f) {
1099                         *folderByPath << f.path();
1100                         *folderByName << f.name();
1101                     }))
1102                     .then([folderByName, folderByPath, folder, remoteId, imap] {
1103                         if (!folderByName->contains(folder.getName())) {
1104                             SinkWarning() << "Existing folders are: " << *folderByPath;
1105                             SinkWarning() << "We're looking for: " << folder.getName();
1106                             return KAsync::error<void>(1, "Wrong folder name: " + remoteId);
1107                         }
1108                         return KAsync::null<void>();
1109                     });
1110 
1111                 return inspectionJob;
1112             }
1113 
1114         }
1115         return KAsync::null<void>();
1116     }
1117 
1118 public:
1119     QString mServer;
1120     int mPort;
1121     Imap::EncryptionMode mEncryptionMode = Imap::NoEncryption;
1122     Imap::AuthenticationMode mAuthenticationMode;
1123     QString mUser;
1124 };
1125 
1126 class FolderCleanupPreprocessor : public Sink::Preprocessor
1127 {
1128 public:
1129     void deletedEntity(const ApplicationDomain::ApplicationDomainType &oldEntity) override
1130     {
1131         //Remove all mails of a folder when removing the folder.
1132         const auto revision = entityStore().maxRevision();
1133         entityStore().indexLookup<ApplicationDomain::Mail, ApplicationDomain::Mail::Folder>(oldEntity.identifier(), [&] (const QByteArray &identifier) {
1134             deleteEntity(ApplicationDomain::ApplicationDomainType{{}, identifier, revision, {}}, ApplicationDomain::getTypeName<ApplicationDomain::Mail>(), false);
1135         });
1136     }
1137 };
1138 
1139 ImapResource::ImapResource(const ResourceContext &resourceContext)
1140     : Sink::GenericResource(resourceContext)
1141 {
1142     auto config = ResourceConfig::getConfiguration(resourceContext.instanceId());
1143     auto server = config.value("server").toString();
1144     auto port = config.value("port").toInt();
1145     auto user = config.value("username").toString();
1146     auto daysToSync = config.value("daysToSync", 14).toInt();
1147     auto starttls = config.value("starttls", false).toBool();
1148     auto auth = config.value("authenticationMode", "PLAIN").toString();
1149 
1150     auto encryption = Imap::NoEncryption;
1151     if (server.startsWith("imaps")) {
1152         encryption = Imap::Tls;
1153     }
1154     if (starttls) {
1155         encryption = Imap::Starttls;
1156     }
1157 
1158     if (server.startsWith("imap")) {
1159         server.remove("imap://");
1160         server.remove("imaps://");
1161     }
1162     if (server.contains(':')) {
1163         auto list = server.split(':');
1164         server = list.at(0);
1165         port = list.at(1).toInt();
1166     }
1167 
1168     //Backwards compatibilty
1169     //For kolabnow we assumed that port 143 means starttls
1170     if (encryption == Imap::Tls && port == 143) {
1171         encryption = Imap::Starttls;
1172     }
1173 
1174     if (!QSslSocket::supportsSsl()) {
1175         SinkWarning() << "Qt doesn't support ssl. This is likely a distribution/packaging problem.";
1176         //On windows this means that the required ssl dll's are missing
1177         SinkWarning() << "Ssl Library Build Version Number: " << QSslSocket::sslLibraryBuildVersionString();
1178         SinkWarning() << "Ssl Library Runtime Version Number: " << QSslSocket::sslLibraryVersionString();
1179     } else {
1180         SinkTrace() << "Ssl support available";
1181         SinkTrace() << "Ssl Library Build Version Number: " << QSslSocket::sslLibraryBuildVersionString();
1182         SinkTrace() << "Ssl Library Runtime Version Number: " << QSslSocket::sslLibraryVersionString();
1183     }
1184 
1185     auto synchronizer = QSharedPointer<ImapSynchronizer>::create(resourceContext);
1186     synchronizer->mServer = server;
1187     synchronizer->mPort = port;
1188     synchronizer->mEncryptionMode = encryption;
1189     synchronizer->mAuthenticationMode = Imap::fromAuthString(auth);
1190     synchronizer->mUser = user;
1191     synchronizer->mDaysToSync = daysToSync;
1192     setupSynchronizer(synchronizer);
1193 
1194     auto inspector = QSharedPointer<ImapInspector>::create(resourceContext);
1195     inspector->mServer = server;
1196     inspector->mPort = port;
1197     inspector->mEncryptionMode = encryption;
1198     inspector->mAuthenticationMode = Imap::fromAuthString(auth);
1199     inspector->mUser = user;
1200     setupInspector(inspector);
1201 
1202     setupPreprocessors(ENTITY_TYPE_MAIL, {new SpecialPurposeProcessor, new MailPropertyExtractor});
1203     setupPreprocessors(ENTITY_TYPE_FOLDER, {new FolderCleanupPreprocessor});
1204 }
1205 
1206 ImapResourceFactory::ImapResourceFactory(QObject *parent)
1207     : Sink::ResourceFactory(parent,
1208             {Sink::ApplicationDomain::ResourceCapabilities::Mail::mail,
1209             Sink::ApplicationDomain::ResourceCapabilities::Mail::folder,
1210             Sink::ApplicationDomain::ResourceCapabilities::Mail::storage,
1211             Sink::ApplicationDomain::ResourceCapabilities::Mail::drafts,
1212             Sink::ApplicationDomain::ResourceCapabilities::Mail::folderhierarchy,
1213             Sink::ApplicationDomain::ResourceCapabilities::Mail::trash,
1214             Sink::ApplicationDomain::ResourceCapabilities::Mail::sent}
1215             )
1216 {
1217 
1218 }
1219 
1220 Sink::Resource *ImapResourceFactory::createResource(const ResourceContext &context)
1221 {
1222     return new ImapResource(context);
1223 }
1224 
1225 void ImapResourceFactory::registerFacades(const QByteArray &name, Sink::FacadeFactory &factory)
1226 {
1227     factory.registerFacade<ApplicationDomain::Mail, DefaultFacade<ApplicationDomain::Mail>>(name);
1228     factory.registerFacade<ApplicationDomain::Folder, DefaultFacade<ApplicationDomain::Folder>>(name);
1229 }
1230 
1231 void ImapResourceFactory::registerAdaptorFactories(const QByteArray &name, Sink::AdaptorFactoryRegistry &registry)
1232 {
1233     registry.registerFactory<ApplicationDomain::Mail, DefaultAdaptorFactory<ApplicationDomain::Mail>>(name);
1234     registry.registerFactory<ApplicationDomain::Folder, DefaultAdaptorFactory<ApplicationDomain::Folder>>(name);
1235 }
1236 
1237 void ImapResourceFactory::removeDataFromDisk(const QByteArray &instanceIdentifier)
1238 {
1239     ImapResource::removeFromDisk(instanceIdentifier);
1240 }
1241 
1242 #include "imapresource.moc"