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

0001 /*
0002  *   Copyright (C) 2016 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 #include <QTest>
0020 #include <QTcpSocket>
0021 
0022 #include <tests/mailsynctest.h>
0023 #include "../imapresource.h"
0024 #include "../imapserverproxy.h"
0025 
0026 #include "common/test.h"
0027 #include "common/domain/applicationdomaintype.h"
0028 #include "common/secretstore.h"
0029 #include "common/store.h"
0030 #include "common/resourcecontrol.h"
0031 #include "common/notifier.h"
0032 
0033 using namespace Sink;
0034 using namespace Sink::ApplicationDomain;
0035 
0036 /**
0037  * Test of complete system using the imap resource.
0038  *
0039  * This test requires the imap resource installed.
0040  */
0041 class ImapMailSyncTest : public Sink::MailSyncTest
0042 {
0043     Q_OBJECT
0044 
0045 protected:
0046     bool isBackendAvailable() Q_DECL_OVERRIDE
0047     {
0048         QTcpSocket socket;
0049         socket.connectToHost("localhost", 143);
0050         return socket.waitForConnected(200);
0051     }
0052 
0053     void resetTestEnvironment() Q_DECL_OVERRIDE
0054     {
0055         system("resetmailbox.sh");
0056     }
0057 
0058     Sink::ApplicationDomain::SinkResource createResource() Q_DECL_OVERRIDE
0059     {
0060         auto resource = ApplicationDomain::ImapResource::create("account1");
0061         resource.setProperty("server", "localhost");
0062         resource.setProperty("port", 143);
0063         resource.setProperty("username", "doe");
0064         resource.setProperty("daysToSync", 0);
0065         Sink::SecretStore::instance().insert(resource.identifier(), "doe");
0066         return resource;
0067     }
0068 
0069     Sink::ApplicationDomain::SinkResource createFaultyResource() Q_DECL_OVERRIDE
0070     {
0071         auto resource = ApplicationDomain::ImapResource::create("account1");
0072         //We try to connect on localhost on port 0 because:
0073         //* Using a bogus ip instead of a bogus hostname avoids getting stuck in the hostname lookup.
0074         //* Using localhost avoids tcp trying to retransmit packets into nirvana
0075         //* Using port 0 fails immediately because it's not an existing port.
0076         //All we really want is something that immediately rejects our connection attempt, and this seems to work.
0077         resource.setProperty("server", "127.0.0.1");
0078         resource.setProperty("port", 0);
0079         resource.setProperty("username", "doe");
0080         Sink::SecretStore::instance().insert(resource.identifier(), "doe");
0081         return resource;
0082     }
0083 
0084     void removeResourceFromDisk(const QByteArray &identifier) Q_DECL_OVERRIDE
0085     {
0086         ::ImapResource::removeFromDisk(identifier);
0087     }
0088 
0089     void createFolder(const QStringList &folderPath) Q_DECL_OVERRIDE
0090     {
0091         Imap::ImapServerProxy imap("localhost", 143, Imap::NoEncryption);
0092         VERIFYEXEC(imap.login("doe", "doe"));
0093         VERIFYEXEC(imap.create("INBOX." + folderPath.join('.')));
0094         VERIFYEXEC(imap.subscribe("INBOX." + folderPath.join('.')));
0095     }
0096 
0097     void removeFolder(const QStringList &folderPath) Q_DECL_OVERRIDE
0098     {
0099         Imap::ImapServerProxy imap("localhost", 143, Imap::NoEncryption);
0100         VERIFYEXEC(imap.login("doe", "doe"));
0101         VERIFYEXEC(imap.remove("INBOX." + folderPath.join('.')));
0102     }
0103 
0104     QByteArray createMessage(const QStringList &folderPath, const QByteArray &message, const QDateTime &internalDate)
0105     {
0106         Imap::ImapServerProxy imap("localhost", 143, Imap::NoEncryption);
0107         VERIFYEXEC_RET(imap.login("doe", "doe"), {});
0108 
0109         auto appendJob = imap.append("INBOX." + folderPath.join('.'), message, {}, internalDate);
0110         auto future = appendJob.exec();
0111         future.waitForFinished();
0112         auto result = future.value();
0113         return QByteArray::number(result);
0114     }
0115 
0116     QByteArray createMessage(const QStringList &folderPath, const QByteArray &message) Q_DECL_OVERRIDE
0117     {
0118         return createMessage(folderPath, message, {});
0119     }
0120 
0121     void removeMessage(const QStringList &folderPath, const QByteArray &messages) Q_DECL_OVERRIDE
0122     {
0123         Imap::ImapServerProxy imap("localhost", 143, Imap::NoEncryption);
0124         VERIFYEXEC(imap.login("doe", "doe"));
0125         VERIFYEXEC(imap.remove("INBOX." + folderPath.join('.'), messages));
0126     }
0127 
0128     void markAsImportant(const QStringList &folderPath, const QByteArray &messageIdentifier) Q_DECL_OVERRIDE
0129     {
0130         Imap::ImapServerProxy imap("localhost", 143, Imap::NoEncryption);
0131         VERIFYEXEC(imap.login("doe", "doe"));
0132         VERIFYEXEC(imap.select("INBOX." + folderPath.join('.')));
0133         VERIFYEXEC(imap.addFlags(KIMAP2::ImapSet::fromImapSequenceSet(messageIdentifier), QByteArrayList() << Imap::Flags::Flagged));
0134     }
0135 
0136     static QByteArray newMessage(const QString &subject, const QDateTime &dt = QDateTime::currentDateTimeUtc())
0137     {
0138         auto msg = KMime::Message::Ptr::create();
0139         msg->messageID(true)->generate("test.com");
0140         msg->subject(true)->fromUnicodeString(subject, "utf8");
0141         msg->date(true)->setDateTime(dt);
0142         msg->assemble();
0143         return msg->encodedContent(true);
0144     }
0145 
0146 private slots:
0147     void testNewMailNotification()
0148     {
0149         createFolder(QStringList() << "testNewMailNotification");
0150         createMessage(QStringList() << "testNewMailNotification", newMessage("Foobar"));
0151 
0152         const auto syncFolders = Sink::SyncScope{ApplicationDomain::getTypeName<Folder>()}.resourceFilter(mResourceInstanceIdentifier);
0153         //Fetch folders initially
0154         VERIFYEXEC(Store::synchronize(syncFolders));
0155         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0156 
0157         auto folder = Store::readOne<Folder>(Sink::Query{}.resourceFilter(mResourceInstanceIdentifier).filter<Folder::Name>("testNewMailNotification"));
0158         Q_ASSERT(!folder.identifier().isEmpty());
0159 
0160         const auto syncTestMails = Sink::SyncScope{ApplicationDomain::getTypeName<Mail>()}.resourceFilter(mResourceInstanceIdentifier).filter<Mail::Folder>(QVariant::fromValue(folder.identifier()));
0161 
0162         bool notificationReceived = false;
0163         auto notifier = QSharedPointer<Sink::Notifier>::create(mResourceInstanceIdentifier);
0164         notifier->registerHandler([&](const Notification &notification) {
0165             if (notification.type == Sink::Notification::Info && notification.code == ApplicationDomain::NewContentAvailable && notification.entities.contains(folder.identifier())) {
0166                 notificationReceived = true;
0167             }
0168         });
0169 
0170         //Should result in a change notification for test
0171         VERIFYEXEC(Store::synchronize(syncFolders));
0172         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0173 
0174         QTRY_VERIFY(notificationReceived);
0175 
0176         notificationReceived = false;
0177 
0178         //Fetch test mails to skip change notification
0179         VERIFYEXEC(Store::synchronize(syncTestMails));
0180         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0181 
0182         //Should no longer result in change notifications for test
0183         VERIFYEXEC(Store::synchronize(syncFolders));
0184         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0185 
0186         QVERIFY(!notificationReceived);
0187 
0188         //Create message and retry
0189         createMessage(QStringList() << "testNewMailNotification", newMessage("This is a Subject."));
0190 
0191         //Should result in change notification
0192         VERIFYEXEC(Store::synchronize(syncFolders));
0193         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0194 
0195         QTRY_VERIFY(notificationReceived);
0196     }
0197 
0198     void testSyncFolderBeforeFetchingNewMessages()
0199     {
0200         const auto syncScope = Sink::Query{}.resourceFilter(mResourceInstanceIdentifier);
0201 
0202         createFolder(QStringList() << "test3");
0203 
0204         VERIFYEXEC(Store::synchronize(syncScope));
0205         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0206 
0207         createMessage(QStringList() << "test3", newMessage("Foobar"));
0208 
0209         VERIFYEXEC(Store::synchronize(syncScope));
0210         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0211 
0212         auto mailQuery = Sink::Query{}.resourceFilter(mResourceInstanceIdentifier).request<Mail::Subject>().filter<Mail::Folder>(Sink::Query{}.filter<Folder::Name>("test3"));
0213         QCOMPARE(Store::read<Mail>(mailQuery).size(), 1);
0214     }
0215 
0216     void testDateFilterSync()
0217     {
0218         auto dt = QDateTime{{2019, 04, 20}};
0219 
0220         //Create a folder
0221         createFolder({"datefilter"});
0222         {
0223             Sink::Query query;
0224             query.setType<ApplicationDomain::Folder>();
0225             VERIFYEXEC(Store::synchronize(query));
0226             VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0227         }
0228 
0229         auto folder = Store::readOne<Folder>(Query{}.resourceFilter(mResourceInstanceIdentifier).filter<Folder::Name>("datefilter"));
0230 
0231         //Create the two messsages with one matching the filter below the other not.
0232         createMessage({"datefilter"}, newMessage("1", dt.addDays(-4)), dt.addDays(-4));
0233         createMessage({"datefilter"}, newMessage("2", dt.addDays(-2)), dt.addDays(-2));
0234 
0235         {
0236             Sink::Query query;
0237             query.setType<ApplicationDomain::Mail>();
0238             query.resourceFilter(mResourceInstanceIdentifier);
0239             query.filter(ApplicationDomain::Mail::Date::name, QVariant::fromValue(dt.addDays(-3).date()));
0240             query.filter<Mail::Folder>(folder);
0241 
0242             VERIFYEXEC(Store::synchronize(query));
0243             VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0244         }
0245 
0246         //For the second message we should have the full payload, for the first only the headers
0247         {
0248             Sink::Query query;
0249             query.resourceFilter(mResourceInstanceIdentifier);
0250             query.filter<Mail::Folder>(folder);
0251             query.sort<ApplicationDomain::Mail::Date>();
0252             auto mails = Store::read<Mail>(query);
0253             QCOMPARE(mails.size(), 2);
0254             QCOMPARE(mails.at(0).getFullPayloadAvailable(), true);
0255             QCOMPARE(mails.at(1).getFullPayloadAvailable(), false);
0256         }
0257     }
0258 
0259     /*
0260      * Ensure that even though we have a date-filter we don't leave any gaps in the maillist.
0261      */
0262     void testDateFilterForGaps()
0263     {
0264         auto dt = QDateTime{{2019, 04, 20}};
0265 
0266         auto foldername = "datefilter1";
0267         createFolder({foldername});
0268         createMessage({foldername}, newMessage("0", dt.addDays(-6)), dt.addDays(-6));
0269 
0270         VERIFYEXEC(Store::synchronize(Sink::SyncScope{}));
0271         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0272 
0273         auto folder = Store::readOne<Folder>(Query{}.resourceFilter(mResourceInstanceIdentifier).filter<Folder::Name>(foldername));
0274 
0275         // We create two messages with one not matching the date filter below, and then ensure we get it nevertheless
0276         createMessage({foldername}, newMessage("1", dt.addDays(-4)), dt.addDays(-4));
0277         createMessage({foldername}, newMessage("2", dt.addDays(-2)), dt.addDays(-2));
0278 
0279         {
0280             Sink::Query query;
0281             query.setType<ApplicationDomain::Mail>();
0282             query.resourceFilter(mResourceInstanceIdentifier);
0283             query.filter(ApplicationDomain::Mail::Date::name, QVariant::fromValue(dt.addDays(-3).date()));
0284             query.filter<Mail::Folder>(folder);
0285 
0286             VERIFYEXEC(Store::synchronize(query));
0287             VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0288         }
0289 
0290         {
0291             Sink::Query query;
0292             query.resourceFilter(mResourceInstanceIdentifier);
0293             query.sort<ApplicationDomain::Mail::Date>();
0294             query.filter<Mail::Folder>(folder);
0295             auto mails = Store::read<Mail>(query);
0296             QCOMPARE(mails.size(), 3);
0297             QCOMPARE(mails.at(0).getFullPayloadAvailable(), true);
0298             //We don't strictly have to pull the full payload for an item that is just fetched to ensure we have no missing mails,
0299             //but we currently do
0300             QCOMPARE(mails.at(1).getFullPayloadAvailable(), true);
0301             QCOMPARE(mails.at(2).getFullPayloadAvailable(), true);
0302         }
0303     }
0304 
0305     /*
0306      * * First sync the folder
0307      * * Then create a message on the server
0308      * * Then attempt to sync it even though it doens't match the date filter.
0309      * We expect the message to be fetched with the payload even though it doesn't match the date-filter.
0310      */
0311     void testDateFilterAfterInitialSync()
0312     {
0313         auto dt = QDateTime{{2019, 04, 20}};
0314 
0315         auto foldername = "datefilter2";
0316         createFolder({foldername});
0317 
0318         Sink::SyncScope query;
0319         query.setType<ApplicationDomain::Folder>();
0320         VERIFYEXEC(Store::synchronize(query));
0321         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0322         auto folder = Store::readOne<Folder>(Query{}.resourceFilter(mResourceInstanceIdentifier).filter<Folder::Name>(foldername));
0323 
0324         {
0325             Sink::Query query;
0326             query.setType<ApplicationDomain::Mail>();
0327             query.resourceFilter(mResourceInstanceIdentifier);
0328             query.filter<Mail::Folder>(folder);
0329 
0330             VERIFYEXEC(Store::synchronize(query));
0331             VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0332         }
0333 
0334         createMessage({foldername}, newMessage("0", dt.addDays(-6)), dt.addDays(-6));
0335         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0336 
0337         {
0338             Sink::Query query;
0339             query.setType<ApplicationDomain::Mail>();
0340             query.resourceFilter(mResourceInstanceIdentifier);
0341             query.filter(ApplicationDomain::Mail::Date::name, QVariant::fromValue(dt.addDays(-3).date()));
0342             query.filter<Mail::Folder>(folder);
0343 
0344             VERIFYEXEC(Store::synchronize(query));
0345             VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0346         }
0347 
0348         {
0349             Sink::Query query;
0350             query.resourceFilter(mResourceInstanceIdentifier);
0351             query.sort<ApplicationDomain::Mail::Date>();
0352             query.filter<Mail::Folder>(folder);
0353             auto mails = Store::read<Mail>(query);
0354             QCOMPARE(mails.size(), 1);
0355             QCOMPARE(mails.at(0).getFullPayloadAvailable(), true);
0356         }
0357     }
0358 
0359     /**
0360      * The mails are too old so we only fetch headers
0361      */
0362     void testDateFilterFetchHeadersOnly()
0363     {
0364         auto dt = QDateTime{{2019, 04, 20}};
0365 
0366         auto foldername = "datefilter3";
0367         createFolder({foldername});
0368         createMessage({foldername}, newMessage("0", dt.addDays(-30)), dt.addDays(-30));
0369 
0370         Sink::SyncScope query;
0371         query.setType<ApplicationDomain::Folder>();
0372         VERIFYEXEC(Store::synchronize(query));
0373         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0374         auto folder = Store::readOne<Folder>(Query{}.resourceFilter(mResourceInstanceIdentifier).filter<Folder::Name>(foldername));
0375 
0376         VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0377 
0378         {
0379             Sink::Query query;
0380             query.setType<ApplicationDomain::Mail>();
0381             query.resourceFilter(mResourceInstanceIdentifier);
0382             query.filter(ApplicationDomain::Mail::Date::name, QVariant::fromValue(dt.addDays(-3).date()));
0383             query.filter<Mail::Folder>(folder);
0384 
0385             VERIFYEXEC(Store::synchronize(query));
0386             VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier));
0387         }
0388 
0389         {
0390             Sink::Query query;
0391             query.resourceFilter(mResourceInstanceIdentifier);
0392             query.sort<ApplicationDomain::Mail::Date>();
0393             query.filter<Mail::Folder>(folder);
0394             auto mails = Store::read<Mail>(query);
0395             QCOMPARE(mails.size(), 1);
0396             QCOMPARE(mails.at(0).getFullPayloadAvailable(), false);
0397         }
0398     }
0399 };
0400 
0401 QTEST_MAIN(ImapMailSyncTest)
0402 
0403 #include "imapmailsynctest.moc"