File indexing completed on 2025-01-05 04:46:59

0001 /*
0002     SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org>
0003     SPDX-FileCopyrightText: 2010 Milian Wolff <mail@milianw.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "itemretriever.h"
0009 
0010 #include "akonadi.h"
0011 #include "connection.h"
0012 #include "storage/datastore.h"
0013 #include "storage/itemqueryhelper.h"
0014 #include "storage/itemretrievalmanager.h"
0015 #include "storage/itemretrievalrequest.h"
0016 #include "storage/parthelper.h"
0017 #include "storage/parttypehelper.h"
0018 #include "storage/querybuilder.h"
0019 #include "storage/selectquerybuilder.h"
0020 #include "utils.h"
0021 
0022 #include "private/protocol_p.h"
0023 #include "shared/akranges.h"
0024 
0025 #include <QEventLoop>
0026 
0027 #include "akonadiserver_debug.h"
0028 
0029 using namespace Akonadi;
0030 using namespace Akonadi::Server;
0031 using namespace AkRanges;
0032 
0033 Q_DECLARE_METATYPE(ItemRetrievalResult)
0034 
0035 ItemRetriever::ItemRetriever(ItemRetrievalManager &manager, Connection *connection, const CommandContext &context)
0036     : mItemRetrievalManager(manager)
0037     , mConnection(connection)
0038     , mContext(context)
0039     , mFullPayload(false)
0040     , mRecursive(false)
0041     , mCanceled(false)
0042 {
0043     qRegisterMetaType<ItemRetrievalResult>("Akonadi::Server::ItemRetrievalResult");
0044     if (mConnection) {
0045         connect(mConnection, &Connection::disconnected, this, [this]() {
0046             mCanceled = true;
0047         });
0048     }
0049 }
0050 
0051 Connection *ItemRetriever::connection() const
0052 {
0053     return mConnection;
0054 }
0055 
0056 void ItemRetriever::setRetrieveParts(const QList<QByteArray> &parts)
0057 {
0058     mParts = parts;
0059     std::sort(mParts.begin(), mParts.end());
0060     mParts.erase(std::unique(mParts.begin(), mParts.end()), mParts.end());
0061 
0062     // HACK, we need a full payload available flag in PimItem
0063     if (mFullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) {
0064         mParts.append(AKONADI_PARAM_PLD_RFC822);
0065     }
0066 }
0067 
0068 void ItemRetriever::setItemSet(const ImapSet &set, const Collection &collection)
0069 {
0070     mItemSet = set;
0071     mCollection = collection;
0072 }
0073 
0074 void ItemRetriever::setItemSet(const ImapSet &set, bool isUid)
0075 {
0076     if (!isUid && mContext.collectionId() >= 0) {
0077         setItemSet(set, mContext.collection());
0078     } else {
0079         setItemSet(set);
0080     }
0081 }
0082 
0083 void ItemRetriever::setItem(Entity::Id id)
0084 {
0085     ImapSet set;
0086     set.add(ImapInterval(id, id));
0087     mItemSet = set;
0088     mCollection = Collection();
0089 }
0090 
0091 void ItemRetriever::setRetrieveFullPayload(bool fullPayload)
0092 {
0093     mFullPayload = fullPayload;
0094     // HACK, we need a full payload available flag in PimItem
0095     if (fullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) {
0096         mParts.append(AKONADI_PARAM_PLD_RFC822);
0097     }
0098 }
0099 
0100 void ItemRetriever::setCollection(const Collection &collection, bool recursive)
0101 {
0102     mCollection = collection;
0103     mItemSet = ImapSet();
0104     mRecursive = recursive;
0105 }
0106 
0107 void ItemRetriever::setScope(const Scope &scope)
0108 {
0109     mScope = scope;
0110 }
0111 
0112 Scope ItemRetriever::scope() const
0113 {
0114     return mScope;
0115 }
0116 
0117 void ItemRetriever::setChangedSince(const QDateTime &changedSince)
0118 {
0119     mChangedSince = changedSince;
0120 }
0121 
0122 QList<QByteArray> ItemRetriever::retrieveParts() const
0123 {
0124     return mParts;
0125 }
0126 
0127 enum QueryColumns {
0128     PimItemIdColumn,
0129 
0130     CollectionIdColumn,
0131     ResourceIdColumn,
0132 
0133     PartTypeNameColumn,
0134     PartDatasizeColumn
0135 };
0136 
0137 QSqlQuery ItemRetriever::buildQuery() const
0138 {
0139     QueryBuilder qb(PimItem::tableName());
0140 
0141     qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName());
0142 
0143     qb.addJoin(QueryBuilder::LeftJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName());
0144 
0145     Query::Condition partTypeJoinCondition;
0146     partTypeJoinCondition.addColumnCondition(Part::partTypeIdFullColumnName(), Query::Equals, PartType::idFullColumnName());
0147     if (!mFullPayload && !mParts.isEmpty()) {
0148         partTypeJoinCondition.addCondition(PartTypeHelper::conditionFromFqNames(mParts));
0149     }
0150     partTypeJoinCondition.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("PLD"));
0151     qb.addJoin(QueryBuilder::LeftJoin, PartType::tableName(), partTypeJoinCondition);
0152 
0153     qb.addColumn(PimItem::idFullColumnName());
0154     qb.addColumn(PimItem::collectionIdFullColumnName());
0155     qb.addColumn(Collection::resourceIdFullColumnName());
0156     qb.addColumn(PartType::nameFullColumnName());
0157     qb.addColumn(Part::datasizeFullColumnName());
0158 
0159     if (!mItemSet.isEmpty() || mCollection.isValid()) {
0160         ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection);
0161     } else {
0162         ItemQueryHelper::scopeToQuery(mScope, mContext, qb);
0163     }
0164 
0165     // prevent a resource to trigger item retrieval from itself
0166     if (mConnection) {
0167         const Resource res = Resource::retrieveByName(QString::fromUtf8(mConnection->sessionId()));
0168         if (res.isValid()) {
0169             qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::NotEquals, res.id());
0170         }
0171     }
0172 
0173     if (mChangedSince.isValid()) {
0174         qb.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mChangedSince.toUTC());
0175     }
0176 
0177     qb.addSortColumn(PimItem::idFullColumnName(), Query::Ascending);
0178 
0179     if (!qb.exec()) {
0180         mLastError = "Unable to retrieve items";
0181         throw ItemRetrieverException(mLastError);
0182     }
0183 
0184     qb.query().next();
0185 
0186     return qb.query();
0187 }
0188 
0189 namespace
0190 {
0191 bool hasAllParts(const ItemRetrievalRequest &req, const QSet<QByteArray> &availableParts)
0192 {
0193     return std::all_of(req.parts.begin(), req.parts.end(), [&availableParts](const auto &part) {
0194         return availableParts.contains(part);
0195     });
0196 }
0197 }
0198 
0199 bool ItemRetriever::runItemRetrievalRequests(std::list<ItemRetrievalRequest> requests) // clazy:exclude=function-args-by-ref
0200 {
0201     QEventLoop eventLoop;
0202     std::vector<ItemRetrievalRequest::Id> pendingRequests;
0203     connect(&mItemRetrievalManager,
0204             &ItemRetrievalManager::requestFinished,
0205             this,
0206             [this, &eventLoop, &pendingRequests](const ItemRetrievalResult &result) { // clazy:exclude=lambda-in-connect
0207                 const auto requestId = std::find(pendingRequests.begin(), pendingRequests.end(), result.request.id);
0208                 if (requestId != pendingRequests.end()) {
0209                     if (mCanceled) {
0210                         eventLoop.exit(1);
0211                     } else if (result.errorMsg.has_value()) {
0212                         mLastError = result.errorMsg->toUtf8();
0213                         eventLoop.exit(1);
0214                     } else {
0215                         Q_EMIT itemsRetrieved(result.request.ids);
0216                         pendingRequests.erase(requestId);
0217                         if (pendingRequests.empty()) {
0218                             eventLoop.quit();
0219                         }
0220                     }
0221                 }
0222             });
0223 
0224     if (mConnection) {
0225         connect(mConnection, &Connection::connectionClosing, &eventLoop, [&eventLoop]() {
0226             eventLoop.exit(1);
0227         });
0228     }
0229 
0230     for (auto &&request : requests) {
0231         if ((!mFullPayload && request.parts.isEmpty()) || request.ids.isEmpty()) {
0232             continue;
0233         }
0234 
0235         // TODO: how should we handle retrieval errors here? so far they have been ignored,
0236         // which makes sense in some cases, do we need a command parameter for this?
0237         try {
0238             // Request is deleted inside ItemRetrievalManager, so we need to take
0239             // a copy here
0240             // const auto ids = request->ids;
0241             pendingRequests.push_back(request.id);
0242             mItemRetrievalManager.requestItemDelivery(std::move(request));
0243         } catch (const ItemRetrieverException &e) {
0244             qCCritical(AKONADISERVER_LOG) << e.type() << ": " << e.what();
0245             mLastError = e.what();
0246             return false;
0247         }
0248     }
0249 
0250     if (!pendingRequests.empty()) {
0251         if (eventLoop.exec()) {
0252             return false;
0253         }
0254     }
0255 
0256     return true;
0257 }
0258 
0259 std::optional<ItemRetriever::PreparedRequests> ItemRetriever::prepareRequests(QSqlQuery &query, const QByteArrayList &parts)
0260 {
0261     QHash<qint64, QString> resourceIdNameCache;
0262     std::list<ItemRetrievalRequest> requests;
0263     QHash<qint64 /* collection */, decltype(requests)::iterator> colRequests;
0264     QHash<qint64 /* item */, decltype(requests)::iterator> itemRequests;
0265     QList<qint64> readyItems;
0266     qint64 prevPimItemId = -1;
0267     QSet<QByteArray> availableParts;
0268     auto lastRequest = requests.end();
0269     while (query.isValid()) {
0270         const qint64 pimItemId = query.value(PimItemIdColumn).toLongLong();
0271         const qint64 collectionId = query.value(CollectionIdColumn).toLongLong();
0272         const qint64 resourceId = query.value(ResourceIdColumn).toLongLong();
0273         const auto itemIter = itemRequests.constFind(pimItemId);
0274 
0275         if (Q_UNLIKELY(mCanceled)) {
0276             return std::nullopt;
0277         }
0278 
0279         if (pimItemId == prevPimItemId) {
0280             if (query.value(PartTypeNameColumn).isNull()) {
0281                 // This is not the first part of the Item we saw, but LEFT JOIN PartTable
0282                 // returned a null row - that means the row is an ATR part
0283                 // which we don't care about
0284                 query.next();
0285                 continue;
0286             }
0287         } else {
0288             if (lastRequest != requests.end()) {
0289                 if (hasAllParts(*lastRequest, availableParts)) {
0290                     // We went through all parts of a single item, if we have all
0291                     // parts available in the DB and they are not expired, then
0292                     // exclude this item from the retrieval
0293                     lastRequest->ids.removeOne(prevPimItemId);
0294                     itemRequests.remove(prevPimItemId);
0295                     readyItems.push_back(prevPimItemId);
0296                 }
0297             }
0298             availableParts.clear();
0299             prevPimItemId = pimItemId;
0300         }
0301 
0302         if (itemIter != itemRequests.constEnd()) {
0303             lastRequest = itemIter.value();
0304         } else {
0305             const auto colIt = colRequests.find(collectionId);
0306             lastRequest = (colIt == colRequests.end()) ? requests.end() : colIt.value();
0307             if (lastRequest == requests.end() || lastRequest->ids.size() > 100) {
0308                 requests.emplace_front(ItemRetrievalRequest{});
0309                 lastRequest = requests.begin();
0310                 lastRequest->ids.push_back(pimItemId);
0311                 auto resIter = resourceIdNameCache.find(resourceId);
0312                 if (resIter == resourceIdNameCache.end()) {
0313                     resIter = resourceIdNameCache.insert(resourceId, Resource::retrieveById(resourceId).name());
0314                 }
0315                 lastRequest->resourceId = *resIter;
0316                 lastRequest->parts = parts;
0317                 colRequests.insert(collectionId, lastRequest);
0318                 itemRequests.insert(pimItemId, lastRequest);
0319             } else {
0320                 lastRequest->ids.push_back(pimItemId);
0321                 itemRequests.insert(pimItemId, lastRequest);
0322                 colRequests.insert(collectionId, lastRequest);
0323             }
0324         }
0325         Q_ASSERT(lastRequest != requests.end());
0326 
0327         if (query.value(PartTypeNameColumn).isNull()) {
0328             // LEFT JOIN did not find anything, retrieve all parts
0329             query.next();
0330             continue;
0331         }
0332 
0333         qint64 datasize = query.value(PartDatasizeColumn).toLongLong();
0334         const QByteArray partName = Utils::variantToByteArray(query.value(PartTypeNameColumn));
0335         Q_ASSERT(!partName.startsWith(AKONADI_PARAM_PLD));
0336         if (datasize <= 0) {
0337             // request update for this part
0338             if (mFullPayload && !lastRequest->parts.contains(partName)) {
0339                 lastRequest->parts.push_back(partName);
0340             }
0341         } else {
0342             // add the part to list of available parts, we will compare it with
0343             // the list of request parts once we handle all parts of this item
0344             availableParts.insert(partName);
0345         }
0346         query.next();
0347     }
0348     query.finish();
0349 
0350     // Post-check in case we only queried one item thus did not reach the check
0351     // at the beginning of the while() loop above
0352     if (lastRequest != requests.end() && hasAllParts(*lastRequest, availableParts)) {
0353         lastRequest->ids.removeOne(prevPimItemId);
0354         readyItems.push_back(prevPimItemId);
0355         // No need to update the hashtable at this point
0356     }
0357 
0358     return PreparedRequests{std::move(requests), std::move(readyItems)};
0359 }
0360 
0361 bool ItemRetriever::exec()
0362 {
0363     if (mParts.isEmpty() && !mFullPayload) {
0364         return true;
0365     }
0366 
0367     verifyCache();
0368 
0369     QSqlQuery query = buildQuery();
0370     const auto parts = mParts | Views::filter([](const auto &part) {
0371                            return part.startsWith(AKONADI_PARAM_PLD);
0372                        })
0373         | Views::transform([](const auto &part) {
0374                            return part.mid(4);
0375                        })
0376         | Actions::toQList;
0377 
0378     auto requests = prepareRequests(query, parts);
0379     if (!requests.has_value()) {
0380         return false;
0381     }
0382 
0383     if (!requests->readyItems.isEmpty()) {
0384         Q_EMIT itemsRetrieved(requests->readyItems);
0385     }
0386 
0387     if (!runItemRetrievalRequests(std::move(requests->requests))) {
0388         return false;
0389     }
0390 
0391     // retrieve items in child collections if requested
0392     bool result = true;
0393     if (mRecursive && mCollection.isValid()) {
0394         const auto children = mCollection.children();
0395         for (const Collection &col : children) {
0396             ItemRetriever retriever(mItemRetrievalManager, mConnection, mContext);
0397             retriever.setCollection(col, mRecursive);
0398             retriever.setRetrieveParts(mParts);
0399             retriever.setRetrieveFullPayload(mFullPayload);
0400             connect(&retriever, &ItemRetriever::itemsRetrieved, this, &ItemRetriever::itemsRetrieved);
0401             result = retriever.exec();
0402             if (!result) {
0403                 break;
0404             }
0405         }
0406     }
0407 
0408     return result;
0409 }
0410 
0411 void ItemRetriever::verifyCache()
0412 {
0413     if (!connection() || !connection()->verifyCacheOnRetrieval()) {
0414         return;
0415     }
0416 
0417     SelectQueryBuilder<Part> qb;
0418     qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName());
0419     qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External);
0420     qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
0421     if (mScope.scope() != Scope::Invalid) {
0422         ItemQueryHelper::scopeToQuery(mScope, mContext, qb);
0423     } else {
0424         ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection);
0425     }
0426 
0427     if (!qb.exec()) {
0428         mLastError = QByteArrayLiteral("Unable to query parts.");
0429         throw ItemRetrieverException(mLastError);
0430     }
0431 
0432     const Part::List externalParts = qb.result();
0433     for (Part part : externalParts) {
0434         PartHelper::verify(part);
0435     }
0436 }
0437 
0438 QByteArray ItemRetriever::lastError() const
0439 {
0440     return mLastError;
0441 }
0442 
0443 #include "moc_itemretriever.cpp"