File indexing completed on 2024-11-17 04:45:04

0001 /*
0002    SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003    SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0004 
0005    SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "imaptestbase.h"
0009 
0010 #include "retrievecollectionmetadatatask.h"
0011 
0012 #include "imapaclattribute.h"
0013 #include "imapquotaattribute.h"
0014 #include "noinferiorsattribute.h"
0015 #include "noselectattribute.h"
0016 #include <Akonadi/AttributeFactory>
0017 #include <Akonadi/CollectionAnnotationsAttribute>
0018 #include <Akonadi/CollectionQuotaAttribute>
0019 #include <QTest>
0020 using QBYTEARRAYMAP = QMap<QByteArray, QByteArray>;
0021 using QBYTEARRAYINT64MAP = QMap<QByteArray, qint64>;
0022 
0023 Q_DECLARE_METATYPE(Akonadi::Collection::Rights)
0024 Q_DECLARE_METATYPE(QBYTEARRAYMAP)
0025 
0026 class TestRetrieveCollectionMetadataTask : public ImapTestBase
0027 {
0028     Q_OBJECT
0029 
0030 private Q_SLOTS:
0031 
0032     void initTestCase()
0033     {
0034         Akonadi::AttributeFactory::registerAttribute<Akonadi::ImapAclAttribute>();
0035         Akonadi::AttributeFactory::registerAttribute<NoSelectAttribute>();
0036     }
0037 
0038     void shouldCollectionRetrieveMetadata_data()
0039     {
0040         QTest::addColumn<Akonadi::Collection>("collection");
0041         QTest::addColumn<QStringList>("capabilities");
0042         QTest::addColumn<QList<QByteArray>>("scenario");
0043         QTest::addColumn<QStringList>("callNames");
0044         QTest::addColumn<Akonadi::Collection::Rights>("expectedRights");
0045         QTest::addColumn<QBYTEARRAYMAP>("expectedAnnotations");
0046         QTest::addColumn<QList<QByteArray>>("expectedRoots");
0047         QTest::addColumn<QList<QBYTEARRAYINT64MAP>>("expectedLimits");
0048         QTest::addColumn<QList<QBYTEARRAYINT64MAP>>("expectedUsages");
0049 
0050         Akonadi::Collection collection;
0051         QStringList capabilities;
0052         QList<QByteArray> scenario;
0053         QStringList callNames;
0054         QMap<QByteArray, QByteArray> expectedAnnotations;
0055         QList<QByteArray> expectedRoots;
0056         QList<QMap<QByteArray, qint64>> expectedLimits;
0057         QList<QMap<QByteArray, qint64>> expectedUsages;
0058 
0059         expectedRoots.clear();
0060         expectedLimits.clear();
0061         expectedUsages.clear();
0062 
0063         collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
0064         collection.setRights(Akonadi::Collection::ReadOnly);
0065 
0066         capabilities.clear();
0067         capabilities << QStringLiteral("ANNOTATEMORE") << QStringLiteral("ACL") << QStringLiteral("QUOTA");
0068 
0069         scenario.clear();
0070         scenario << defaultPoolConnectionScenario() << R"(C: A000003 GETANNOTATION "INBOX/Foo" "*" "value.shared")"
0071                  << "S: * ANNOTATION INBOX/Foo /vendor/kolab/folder-test ( value.shared true )"
0072                  << "S: A000003 OK annotations retrieved"
0073                  << "C: A000004 MYRIGHTS \"INBOX/Foo\""
0074                  << "S: * MYRIGHTS \"INBOX/Foo\" lrswipkxtecda"
0075                  << "S: A000004 OK rights retrieved"
0076                  << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
0077                  << "S: * QUOTAROOT INBOX/Foo user/foo"
0078                  << "S: * QUOTA user/foo ( )"
0079                  << "S: A000005 OK quota retrieved"
0080                  << "C: A000006 GETACL \"INBOX/Foo\""
0081                  << "S: * ACL INBOX/Foo foo@kde.org lrswipcda"
0082                  << "S: A000006 OK acl retrieved";
0083 
0084         callNames.clear();
0085         callNames << QStringLiteral("collectionAttributesRetrieved");
0086 
0087         expectedAnnotations.clear();
0088         expectedAnnotations.insert("/shared/vendor/kolab/folder-test", "true");
0089 
0090         Akonadi::Collection::Rights rights = Akonadi::Collection::AllRights;
0091         QTest::newRow("first listing, connected IMAP")
0092             << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots << expectedLimits << expectedUsages;
0093 
0094         //
0095         // Test that if the parent collection doesn't allow renaming in its ACL, the child mailbox
0096         // can't be renamed, i.e. doesn't have the CanChangeCollection flag.
0097         //
0098         Akonadi::Collection parentCollection = createCollectionChain(QStringLiteral("/INBOX"));
0099         QMap<QByteArray, KIMAP::Acl::Rights> rightsMap;
0100         rightsMap.insert("Hans",
0101                          KIMAP::Acl::Lookup | KIMAP::Acl::Read | KIMAP::Acl::KeepSeen | KIMAP::Acl::Write | KIMAP::Acl::Insert | KIMAP::Acl::Post
0102                              | KIMAP::Acl::Delete);
0103         auto aclAttribute = new Akonadi::ImapAclAttribute();
0104         aclAttribute->setRights(rightsMap);
0105         parentCollection.addAttribute(aclAttribute);
0106         collection.setParentCollection(parentCollection);
0107         rights = Akonadi::Collection::AllRights;
0108         rights &= ~Akonadi::Collection::CanChangeCollection;
0109         QTest::newRow("parent without create rights") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots
0110                                                       << expectedLimits << expectedUsages;
0111 
0112         //
0113         // Test that if the parent collection is a noselect folder, the child mailbox will not have
0114         // rename (CanChangeCollection) permission.
0115         //
0116         parentCollection = createCollectionChain(QStringLiteral("/INBOX"));
0117         auto noSelectAttribute = new NoSelectAttribute();
0118         parentCollection.addAttribute(noSelectAttribute);
0119         collection.setParentCollection(parentCollection);
0120         QTest::newRow("parent with noselect") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots
0121                                               << expectedLimits << expectedUsages;
0122         parentCollection.removeAttribute<NoSelectAttribute>();
0123 
0124         //
0125         // Test that the rights are properly set on the resulting collection if the mailbox doesn't
0126         // have full rights.
0127         //
0128         collection.setParentCollection(createCollectionChain(QStringLiteral("/INBOX")));
0129         scenario.clear();
0130         scenario << defaultPoolConnectionScenario() << R"(C: A000003 GETANNOTATION "INBOX/Foo" "*" "value.shared")"
0131                  << "S: * ANNOTATION INBOX/Foo /vendor/kolab/folder-test ( value.shared true )"
0132                  << "S: A000003 OK annotations retrieved"
0133                  << "C: A000004 MYRIGHTS \"INBOX/Foo\""
0134                  << "S: * MYRIGHTS \"INBOX/Foo\" wi"
0135                  << "S: A000004 OK rights retrieved"
0136                  << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
0137                  << "S: * QUOTAROOT INBOX/Foo user/foo"
0138                  << "S: * QUOTA user/foo ( )"
0139                  << "S: A000005 OK quota retrieved";
0140         rights = Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanChangeCollection;
0141         QTest::newRow("only some rights") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots
0142                                           << expectedLimits << expectedUsages;
0143 
0144         //
0145         // Test that a warning is issued if the insert rights of a folder have been revoked on the server.
0146         //
0147         collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
0148         collection.setParentCollection(createCollectionChain(QStringLiteral("/INBOX")));
0149         // We use the aclattribute to determine if a collection already has acl's or not
0150         collection.addAttribute(new Akonadi::ImapAclAttribute());
0151         collection.setRights(Akonadi::Collection::CanCreateItem);
0152 
0153         capabilities.clear();
0154         capabilities << QStringLiteral("ANNOTATEMORE") << QStringLiteral("ACL") << QStringLiteral("QUOTA");
0155 
0156         scenario.clear();
0157         scenario << defaultPoolConnectionScenario() << R"(C: A000003 GETANNOTATION "INBOX/Foo" "*" "value.shared")"
0158                  << "S: * ANNOTATION INBOX/Foo /vendor/kolab/folder-test ( value.shared true )"
0159                  << "S: A000003 OK annotations retrieved"
0160                  << "C: A000004 MYRIGHTS \"INBOX/Foo\""
0161                  << "S: * MYRIGHTS \"INBOX/Foo\" w"
0162                  << "S: A000004 OK rights retrieved"
0163                  << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
0164                  << "S: * QUOTAROOT INBOX/Foo user/foo"
0165                  << "S: * QUOTA user/foo ( )"
0166                  << "S: A000005 OK quota retrieved";
0167 
0168         callNames.clear();
0169         callNames << QStringLiteral("showInformationDialog");
0170         callNames << QStringLiteral("collectionAttributesRetrieved");
0171 
0172         rights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanChangeCollection;
0173         QTest::newRow("revoked rights") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots
0174                                         << expectedLimits << expectedUsages;
0175 
0176         //
0177         // Test that NoInferiors overrides acl rights and disallows creating new mailboxes
0178         //
0179         collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
0180         collection.setParentCollection(createCollectionChain(QString()));
0181         collection.setRemoteId(QStringLiteral("/INBOX"));
0182         collection.setRights(Akonadi::Collection::AllRights);
0183         collection.addAttribute(new NoInferiorsAttribute(true));
0184         scenario.clear();
0185         scenario << defaultPoolConnectionScenario() << R"(C: A000003 GETANNOTATION "INBOX" "*" "value.shared")"
0186                  << "S: * ANNOTATION INBOX /vendor/kolab/folder-test ( value.shared true )"
0187                  << "S: A000003 OK annotations retrieved"
0188                  << "C: A000004 MYRIGHTS \"INBOX\""
0189                  << "S: * MYRIGHTS \"INBOX\" wk"
0190                  << "S: A000004 OK rights retrieved"
0191                  << "C: A000005 GETQUOTAROOT \"INBOX\""
0192                  << "S: * QUOTAROOT INBOX user"
0193                  << "S: * QUOTA user ( )"
0194                  << "S: A000005 OK quota retrieved";
0195 
0196         callNames.clear();
0197         callNames << QStringLiteral("collectionAttributesRetrieved");
0198 
0199         rights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanChangeCollection;
0200 
0201         QTest::newRow("noinferiors") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots << expectedLimits
0202                                      << expectedUsages;
0203 
0204         collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
0205         collection.setRights(Akonadi::Collection::ReadOnly);
0206 
0207         capabilities.clear();
0208         capabilities << QStringLiteral("METADATA") << QStringLiteral("ACL") << QStringLiteral("QUOTA");
0209 
0210         expectedAnnotations.clear();
0211         expectedAnnotations.insert("/shared/vendor/kolab/folder-test", "true");
0212         expectedAnnotations.insert("/shared/vendor/kolab/folder-test2", "");
0213 
0214         scenario.clear();
0215         scenario << defaultPoolConnectionScenario() << "C: A000003 GETMETADATA (DEPTH infinity) \"INBOX/Foo\" (/shared)"
0216                  << R"(S: * METADATA "INBOX/Foo" (/shared/vendor/kolab/folder-test "true"))"
0217                  << R"(S: * METADATA "INBOX/Foo" (/shared/vendor/kolab/folder-test2 "NIL"))"
0218                  << R"(S: * METADATA "INBOX/Foo" (/shared/vendor/cmu/cyrus-imapd/lastupdate "true"))"
0219                  << "S: A000003 OK GETMETADATA complete"
0220                  << "C: A000004 MYRIGHTS \"INBOX/Foo\""
0221                  << "S: * MYRIGHTS \"INBOX/Foo\" lrswipkxtecda"
0222                  << "S: A000004 OK rights retrieved"
0223                  << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
0224                  << "S: * QUOTAROOT INBOX/Foo user/Foo"
0225                  << "S: * QUOTA user/Foo ( )"
0226                  << "S: A000005 OK quota retrieved"
0227                  << "C: A000006 GETACL \"INBOX/Foo\""
0228                  << "S: * ACL INBOX/Foo foo@kde.org lrswipcda"
0229                  << "S: A000006 OK acl retrieved";
0230 
0231         callNames.clear();
0232         callNames << QStringLiteral("collectionAttributesRetrieved");
0233 
0234         rights = Akonadi::Collection::AllRights;
0235         QTest::newRow("METADATA") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots << expectedLimits
0236                                   << expectedUsages;
0237 
0238         collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
0239         collection.setRights(Akonadi::Collection::ReadOnly);
0240 
0241         capabilities.clear();
0242         expectedAnnotations.clear();
0243 
0244         callNames.clear();
0245         callNames << QStringLiteral("collectionAttributesRetrieved");
0246 
0247         rights = Akonadi::Collection::ReadOnly;
0248 
0249         scenario.clear();
0250         scenario << defaultPoolConnectionScenario();
0251 
0252         QTest::newRow("no capabilities") << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots
0253                                          << expectedLimits << expectedUsages;
0254 
0255         //
0256         // Test for GETQUOTAROOT with multiple IMAP quota roots but only one QUOTA response.
0257         //
0258         collection = createCollectionChain(QStringLiteral("/INBOX"));
0259         collection.setRights(Akonadi::Collection::ReadOnly);
0260 
0261         capabilities.clear();
0262         capabilities << QStringLiteral("QUOTA");
0263 
0264         rights = Akonadi::Collection::ReadOnly;
0265 
0266         scenario.clear();
0267         scenario << defaultPoolConnectionScenario() << "C: A000003 GETQUOTAROOT \"INBOX\""
0268                  << "S: * QUOTAROOT INBOX mailbox1 mailbox2 mailbox3"
0269                  << "S: * QUOTA mailbox2 ( STORAGE 21 512 )"
0270                  << "S: A000003 OK quota retrieved";
0271 
0272         callNames.clear();
0273         callNames << QStringLiteral("collectionAttributesRetrieved");
0274 
0275         expectedAnnotations.clear();
0276 
0277         expectedRoots = {"mailbox2"};
0278         expectedUsages = {{{"STORAGE", 21}}};
0279         expectedLimits = {{{"STORAGE", 512}}};
0280 
0281         QTest::newRow("multiple quota roots, one resource only")
0282             << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots << expectedLimits << expectedUsages;
0283 
0284         //
0285         // Test for GETQUOTAROOT with multiple IMAP quota roots, some with no resource,
0286         // but only (not the first on the list) matches the mailbox name.
0287         //
0288         collection = createCollectionChain(QStringLiteral("/INBOX"));
0289         collection.setRights(Akonadi::Collection::ReadOnly);
0290 
0291         capabilities.clear();
0292         capabilities << QStringLiteral("QUOTA");
0293 
0294         rights = Akonadi::Collection::ReadOnly;
0295 
0296         scenario.clear();
0297         scenario << defaultPoolConnectionScenario() << "C: A000003 GETQUOTAROOT \"INBOX\""
0298                  << "S: * QUOTAROOT INBOX mailbox1 INBOX mailbox2 mailbox3"
0299                  << "S: * QUOTA mailbox1 ( STORAGE 21 512 )"
0300                  << "S: * QUOTA INBOX ( STORAGE 11 250 )"
0301                  << "S: * QUOTA mailbox2 (  )"
0302                  << "S: * QUOTA mailbox3 ( STORAGE 31 500 )"
0303                  << "S: A000003 OK quota retrieved";
0304 
0305         callNames.clear();
0306         callNames << QStringLiteral("collectionAttributesRetrieved");
0307 
0308         expectedAnnotations.clear();
0309 
0310         expectedRoots = {"mailbox1", "INBOX", "mailbox3"};
0311         expectedUsages = {{{"STORAGE", 21}}, {{"STORAGE", 11}}, {{"STORAGE", 31}}};
0312         expectedLimits = {{{"STORAGE", 512}}, {{"STORAGE", 250}}, {{"STORAGE", 500}}};
0313 
0314         QTest::newRow("multiple quota roots, some with no resources, one matches the mailbox name")
0315             << collection << capabilities << scenario << callNames << rights << expectedAnnotations << expectedRoots << expectedLimits << expectedUsages;
0316     }
0317 
0318     void shouldCollectionRetrieveMetadata()
0319     {
0320         QFETCH(Akonadi::Collection, collection);
0321         QFETCH(QStringList, capabilities);
0322         QFETCH(QList<QByteArray>, scenario);
0323         QFETCH(QStringList, callNames);
0324         QFETCH(Akonadi::Collection::Rights, expectedRights);
0325         QFETCH(QBYTEARRAYMAP, expectedAnnotations);
0326         QFETCH(QList<QByteArray>, expectedRoots);
0327         QFETCH(QList<QBYTEARRAYINT64MAP>, expectedLimits);
0328         QFETCH(QList<QBYTEARRAYINT64MAP>, expectedUsages);
0329 
0330         FakeServer server;
0331         server.setScenario(scenario);
0332         server.startAndWait();
0333 
0334         SessionPool pool(1);
0335 
0336         pool.setPasswordRequester(createDefaultRequester());
0337         QVERIFY(pool.connect(createDefaultAccount()));
0338         QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int, QString))));
0339 
0340         DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
0341         state->setCollection(collection);
0342         state->setServerCapabilities(capabilities);
0343         state->setUserName(QStringLiteral("Hans"));
0344         auto task = new RetrieveCollectionMetadataTask(state);
0345 
0346         task->start(&pool);
0347 
0348         QTRY_COMPARE(state->calls().count(), callNames.size());
0349         for (int i = 0; i < callNames.size(); i++) {
0350             QString command = QString::fromUtf8(state->calls().at(i).first);
0351             QVariant parameter = state->calls().at(i).second;
0352 
0353             if (command == QLatin1StringView("cancelTask") && callNames[i] != QLatin1StringView("cancelTask")) {
0354                 qDebug() << "Got a cancel:" << parameter.toString();
0355             }
0356 
0357             QCOMPARE(command, callNames[i]);
0358 
0359             if (command == QLatin1StringView("cancelTask")) {
0360                 QVERIFY(!parameter.toString().isEmpty());
0361             }
0362 
0363             if (command == QLatin1StringView("collectionAttributesRetrieved")) {
0364                 auto collection = parameter.value<Akonadi::Collection>();
0365                 QCOMPARE(collection.rights(), expectedRights);
0366 
0367                 if (!expectedAnnotations.isEmpty()) {
0368                     QVERIFY(collection.hasAttribute<Akonadi::CollectionAnnotationsAttribute>());
0369                     QCOMPARE(collection.attribute<Akonadi::CollectionAnnotationsAttribute>()->annotations(), expectedAnnotations);
0370                 }
0371 
0372                 if (!expectedRoots.isEmpty()) {
0373                     int index = expectedRoots.indexOf("INBOX");
0374 
0375                     QVERIFY(collection.hasAttribute<Akonadi::ImapQuotaAttribute>());
0376                     QCOMPARE(collection.attribute<Akonadi::ImapQuotaAttribute>()->roots(), expectedRoots);
0377                     QCOMPARE(collection.attribute<Akonadi::ImapQuotaAttribute>()->limits(), expectedLimits);
0378                     QCOMPARE(collection.attribute<Akonadi::ImapQuotaAttribute>()->usages(), expectedUsages);
0379 
0380                     QVERIFY(collection.hasAttribute<Akonadi::CollectionQuotaAttribute>());
0381                     if (index != -1) {
0382                         QCOMPARE(collection.attribute<Akonadi::CollectionQuotaAttribute>()->currentValue(), expectedUsages.at(index)["STORAGE"] * 1024);
0383                         QCOMPARE(collection.attribute<Akonadi::CollectionQuotaAttribute>()->maximumValue(), expectedLimits.at(index)["STORAGE"] * 1024);
0384                     } else {
0385                         QCOMPARE(collection.attribute<Akonadi::CollectionQuotaAttribute>()->currentValue(), expectedUsages.first()["STORAGE"] * 1024);
0386                         QCOMPARE(collection.attribute<Akonadi::CollectionQuotaAttribute>()->maximumValue(), expectedLimits.first()["STORAGE"] * 1024);
0387                     }
0388                 }
0389             }
0390         }
0391 
0392         QVERIFY(server.isAllScenarioDone());
0393 
0394         server.quit();
0395     }
0396 };
0397 
0398 QTEST_GUILESS_MAIN(TestRetrieveCollectionMetadataTask)
0399 
0400 #include "testretrievecollectionmetadatatask.moc"