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 ®istry) 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"