File indexing completed on 2024-04-28 05:18:34

0001 /*
0002   SPDX-FileCopyrightText: 2009 Bertjan Broeksema <broeksema@kde.org>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-only
0005 */
0006 
0007 #include "mboxtest.h"
0008 
0009 #include <QDir>
0010 #include <QFile>
0011 
0012 #include <QStandardPaths>
0013 #include <QTemporaryDir>
0014 #include <QTest>
0015 
0016 QTEST_MAIN(MboxTest)
0017 
0018 #include "test-entries.h"
0019 
0020 using namespace KMBox;
0021 
0022 static const char *testDir = "libmbox-unit-test";
0023 static const char *testFile = "test-mbox-file";
0024 static const char *testLockFile = "test-mbox-lock-file";
0025 
0026 QString MboxTest::fileName()
0027 {
0028     return mTempDir->path() + QLatin1Char('/') + QLatin1StringView(testFile);
0029 }
0030 
0031 QString MboxTest::lockFileName()
0032 {
0033     return mTempDir->path() + QLatin1Char('/') + QLatin1StringView(testLockFile);
0034 }
0035 
0036 void MboxTest::removeTestFile()
0037 {
0038     QFile file(fileName());
0039     file.remove();
0040     QVERIFY(!file.exists());
0041 }
0042 
0043 void MboxTest::initTestCase()
0044 {
0045     mTempDir = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1StringView(testDir));
0046 
0047     QDir temp(mTempDir->path());
0048     QVERIFY(temp.exists());
0049 
0050     QFile mboxfile(fileName());
0051     mboxfile.open(QIODevice::WriteOnly);
0052     mboxfile.close();
0053     QVERIFY(mboxfile.exists());
0054 
0055     mMail1 = KMime::Message::Ptr(new KMime::Message);
0056     mMail1->setContent(KMime::CRLFtoLF(sEntry1));
0057     mMail1->parse();
0058 
0059     mMail2 = KMime::Message::Ptr(new KMime::Message);
0060     mMail2->setContent(KMime::CRLFtoLF(sEntry2));
0061     mMail2->parse();
0062 }
0063 
0064 void MboxTest::testSetLockMethod()
0065 {
0066     MBox mbox1;
0067 
0068     if (!QStandardPaths::findExecutable(QStringLiteral("lockfile")).isEmpty()) {
0069         QVERIFY(mbox1.setLockType(MBox::ProcmailLockfile));
0070     } else {
0071         QVERIFY(!mbox1.setLockType(MBox::ProcmailLockfile));
0072     }
0073 
0074     if (!QStandardPaths::findExecutable(QStringLiteral("mutt_dotlock")).isEmpty()) {
0075         QVERIFY(mbox1.setLockType(MBox::MuttDotlock));
0076         QVERIFY(mbox1.setLockType(MBox::MuttDotlockPrivileged));
0077     } else {
0078         QVERIFY(!mbox1.setLockType(MBox::MuttDotlock));
0079         QVERIFY(!mbox1.setLockType(MBox::MuttDotlockPrivileged));
0080     }
0081 
0082     QVERIFY(mbox1.setLockType(MBox::None));
0083 }
0084 
0085 void MboxTest::testLockBeforeLoad()
0086 {
0087     // Should fail because it's not known which file to lock.
0088     MBox mbox;
0089 
0090     if (!QStandardPaths::findExecutable(QStringLiteral("lockfile")).isEmpty()) {
0091         QVERIFY(mbox.setLockType(MBox::ProcmailLockfile));
0092         QVERIFY(!mbox.lock());
0093     }
0094 
0095     if (!QStandardPaths::findExecutable(QStringLiteral("mutt_dotlock")).isEmpty()) {
0096         QVERIFY(mbox.setLockType(MBox::MuttDotlock));
0097         QVERIFY(!mbox.lock());
0098         QVERIFY(mbox.setLockType(MBox::MuttDotlockPrivileged));
0099         QVERIFY(!mbox.lock());
0100     }
0101 
0102     QVERIFY(mbox.setLockType(MBox::None));
0103     QVERIFY(!mbox.lock());
0104 }
0105 
0106 void MboxTest::testProcMailLock()
0107 {
0108     // It really only makes sense to test this if the lockfile executable can be
0109     // found.
0110 
0111     MBox mbox;
0112     if (!mbox.setLockType(MBox::ProcmailLockfile)) {
0113         QEXPECT_FAIL("", "This test only works when procmail is installed.", Abort);
0114         QVERIFY(false);
0115     }
0116 
0117     QVERIFY(mbox.load(fileName()));
0118 
0119     // By default the filename is used as part of the lockfile filename.
0120     QVERIFY(!QFile(fileName() + QLatin1StringView(".lock")).exists());
0121     QVERIFY(mbox.lock());
0122     QVERIFY(QFile(fileName() + QLatin1StringView(".lock")).exists());
0123     QVERIFY(mbox.unlock());
0124     QVERIFY(!QFile(fileName() + QLatin1StringView(".lock")).exists());
0125 
0126     mbox.setLockFile(lockFileName());
0127     QVERIFY(!QFile(lockFileName()).exists());
0128     QVERIFY(mbox.lock());
0129     QVERIFY(QFile(lockFileName()).exists());
0130     QVERIFY(mbox.unlock());
0131     QVERIFY(!QFile(lockFileName()).exists());
0132 }
0133 
0134 void MboxTest::testConcurrentAccess()
0135 {
0136     // tests if mbox works correctly when another program locks the file
0137     // and appends new messages
0138 
0139     MBox mbox;
0140 
0141     if (!mbox.setLockType(MBox::ProcmailLockfile)) {
0142         QEXPECT_FAIL("", "This test only works when procmail is installed.", Abort);
0143         QVERIFY(false);
0144     }
0145 
0146     ThreadFillsMBox thread(fileName()); // locks the mbox file and adds a new message
0147     thread.start();
0148 
0149     QVERIFY(mbox.load(fileName()));
0150 
0151     MBoxEntry entry = mbox.appendMessage(mMail1);
0152 
0153     // as the thread appended sEntry1, the offset for the now appended message
0154     // must be greater
0155     QVERIFY(static_cast<int>(entry.messageOffset()) > sEntry1.length());
0156 
0157     thread.wait();
0158 }
0159 
0160 void MboxTest::testAppend()
0161 {
0162     removeTestFile();
0163 
0164     QFileInfo info(fileName());
0165     QCOMPARE(info.size(), static_cast<qint64>(0));
0166 
0167     MBox mbox;
0168     mbox.setLockType(MBox::None);
0169 
0170     QVERIFY(mbox.load(fileName()));
0171 
0172     // First message added to an empty file should be at offset 0
0173     QCOMPARE(mbox.entries().size(), 0);
0174     QCOMPARE(mbox.appendMessage(mMail1).messageOffset(), static_cast<quint64>(0));
0175     QCOMPARE(mbox.entries().size(), 1);
0176     QVERIFY(mbox.entries().constFirst().separatorSize() > 0);
0177     QCOMPARE(mbox.entries().constFirst().messageSize(), static_cast<quint64>(sEntry1.size()));
0178 
0179     const MBoxEntry offsetMail2 = mbox.appendMessage(mMail2);
0180     QVERIFY(offsetMail2.messageOffset() > static_cast<quint64>(sEntry1.size()));
0181     QCOMPARE(mbox.entries().size(), 2);
0182     QVERIFY(mbox.entries().constLast().separatorSize() > 0);
0183     QCOMPARE(mbox.entries().constLast().messageSize(), static_cast<quint64>(sEntry2.size()));
0184 
0185     // check if appended entries can be read
0186     const MBoxEntry::List list = mbox.entries();
0187     for (const MBoxEntry &msgInfo : list) {
0188         const QByteArray header = mbox.readMessageHeaders(msgInfo);
0189         QVERIFY(!header.isEmpty());
0190 
0191         KMime::Message *message = mbox.readMessage(msgInfo);
0192         QVERIFY(message != nullptr);
0193 
0194         auto headers = new KMime::Message();
0195         headers->setHead(KMime::CRLFtoLF(header));
0196         headers->parse();
0197 
0198         QCOMPARE(message->messageID()->identifier(), headers->messageID()->identifier());
0199         QCOMPARE(message->subject()->as7BitString(), headers->subject()->as7BitString());
0200         QCOMPARE(message->to()->as7BitString(), headers->to()->as7BitString());
0201         QCOMPARE(message->from()->as7BitString(), headers->from()->as7BitString());
0202 
0203         if (msgInfo.messageOffset() == 0) {
0204             QCOMPARE(message->messageID()->identifier(), mMail1->messageID()->identifier());
0205             QCOMPARE(message->subject()->as7BitString(), mMail1->subject()->as7BitString());
0206             QCOMPARE(message->to()->as7BitString(), mMail1->to()->as7BitString());
0207             QCOMPARE(message->from()->as7BitString(), mMail1->from()->as7BitString());
0208         } else if (msgInfo.messageOffset() == offsetMail2.messageOffset()) {
0209             QCOMPARE(message->messageID()->identifier(), mMail2->messageID()->identifier());
0210             QCOMPARE(message->subject()->as7BitString(), mMail2->subject()->as7BitString());
0211             QCOMPARE(message->to()->as7BitString(), mMail2->to()->as7BitString());
0212             QCOMPARE(message->from()->as7BitString(), mMail2->from()->as7BitString());
0213         }
0214 
0215         delete message;
0216         delete headers;
0217     }
0218 }
0219 
0220 void MboxTest::testSaveAndLoad()
0221 {
0222     removeTestFile();
0223 
0224     MBox mbox;
0225     QVERIFY(mbox.setLockType(MBox::None));
0226     QVERIFY(mbox.load(fileName()));
0227     QVERIFY(mbox.entries().isEmpty());
0228     mbox.appendMessage(mMail1);
0229     mbox.appendMessage(mMail2);
0230 
0231     MBoxEntry::List infos1 = mbox.entries();
0232     QCOMPARE(infos1.size(), 2);
0233 
0234     QVERIFY(mbox.save());
0235     QVERIFY(QFileInfo::exists(fileName()));
0236 
0237     MBoxEntry::List infos2 = mbox.entries();
0238     QCOMPARE(infos2.size(), 2);
0239 
0240     for (int i = 0; i < 2; ++i) {
0241         QCOMPARE(infos1.at(i).messageOffset(), infos2.at(i).messageOffset());
0242         QCOMPARE(infos1.at(i).separatorSize(), infos2.at(i).separatorSize());
0243         QCOMPARE(infos1.at(i).messageSize(), infos2.at(i).messageSize());
0244     }
0245 
0246     MBox mbox2;
0247     QVERIFY(mbox2.setLockType(MBox::None));
0248     QVERIFY(mbox2.load(fileName()));
0249 
0250     MBoxEntry::List infos3 = mbox2.entries();
0251     QCOMPARE(infos3.size(), 2);
0252 
0253     for (int i = 0; i < 2; ++i) {
0254         QCOMPARE(infos3.at(i), infos2.at(i));
0255 
0256         QCOMPARE(infos3.at(i).messageOffset(), infos1.at(i).messageOffset());
0257         QCOMPARE(infos3.at(i).separatorSize(), infos1.at(i).separatorSize());
0258         QCOMPARE(infos3.at(i).messageSize(), infos1.at(i).messageSize());
0259 
0260         quint64 minSize = infos2.at(i).messageSize();
0261         quint64 maxSize = infos2.at(i).messageSize() + 1;
0262         QVERIFY(infos3.at(i).messageSize() >= minSize);
0263         QVERIFY(infos3.at(i).messageSize() <= maxSize);
0264     }
0265 }
0266 
0267 void MboxTest::testBlankLines()
0268 {
0269     for (int i = 0; i < 5; ++i) {
0270         removeTestFile();
0271 
0272         KMime::Message::Ptr mail = KMime::Message::Ptr(new KMime::Message);
0273         mail->setContent(KMime::CRLFtoLF(sEntry1 + QByteArray(i, '\n')));
0274         mail->parse();
0275 
0276         MBox mbox1;
0277         QVERIFY(mbox1.setLockType(MBox::None));
0278         QVERIFY(mbox1.load(fileName()));
0279         mbox1.appendMessage(mail);
0280         mbox1.appendMessage(mail);
0281         mbox1.appendMessage(mail);
0282         mbox1.save();
0283 
0284         QVERIFY(mbox1.setLockType(MBox::None));
0285         QVERIFY(mbox1.load(fileName()));
0286         QCOMPARE(mbox1.entries().size(), 3);
0287 
0288         quint64 minSize = sEntry1.size() + i - 1; // Possibly on '\n' falls off.
0289         quint64 maxSize = sEntry1.size() + i;
0290         for (int j = 0; j < 3; ++j) {
0291             QVERIFY(mbox1.entries().at(j).messageSize() >= minSize);
0292             QVERIFY(mbox1.entries().at(j).messageSize() <= maxSize);
0293         }
0294     }
0295 }
0296 
0297 void MboxTest::testEntries()
0298 {
0299     removeTestFile();
0300 
0301     MBox mbox1;
0302     QVERIFY(mbox1.setLockType(MBox::None));
0303     QVERIFY(mbox1.load(fileName()));
0304     mbox1.appendMessage(mMail1);
0305     mbox1.appendMessage(mMail2);
0306     mbox1.appendMessage(mMail1);
0307 
0308     MBoxEntry::List infos = mbox1.entries();
0309     QCOMPARE(infos.size(), 3);
0310 
0311     MBoxEntry::List deletedEntries;
0312     deletedEntries << infos.at(0);
0313 
0314     MBoxEntry::List infos2 = mbox1.entries(deletedEntries);
0315     QCOMPARE(infos2.size(), 2);
0316     QVERIFY(infos2.first() != infos.first());
0317     QVERIFY(infos2.last() != infos.first());
0318 
0319     deletedEntries << infos.at(1);
0320     infos2 = mbox1.entries(deletedEntries);
0321 
0322     QCOMPARE(infos2.size(), 1);
0323     QVERIFY(infos2.first() != infos.at(0));
0324     QVERIFY(infos2.first() != infos.at(1));
0325 
0326     deletedEntries << infos.at(2);
0327     infos2 = mbox1.entries(deletedEntries);
0328     QCOMPARE(infos2.size(), 0);
0329 
0330     QVERIFY(!deletedEntries.contains(MBoxEntry(10))); // some random offset
0331     infos2 = mbox1.entries(MBoxEntry::List() << MBoxEntry(10));
0332     QCOMPARE(infos2.size(), 3);
0333     QCOMPARE(infos2.at(0), infos.at(0));
0334     QCOMPARE(infos2.at(1), infos.at(1));
0335     QCOMPARE(infos2.at(2), infos.at(2));
0336 }
0337 
0338 void MboxTest::testPurge()
0339 {
0340     MBox mbox1;
0341     QVERIFY(mbox1.setLockType(MBox::None));
0342     QVERIFY(mbox1.load(fileName()));
0343     mbox1.appendMessage(mMail1);
0344     mbox1.appendMessage(mMail1);
0345     mbox1.appendMessage(mMail1);
0346     QVERIFY(mbox1.save());
0347 
0348     MBoxEntry::List list = mbox1.entries();
0349 
0350     // First test: Delete only the first (all messages afterwards have to be moved).
0351     mbox1.purge(MBoxEntry::List() << list.first());
0352 
0353     MBox mbox2;
0354     QVERIFY(mbox2.load(fileName()));
0355     MBoxEntry::List list2 = mbox2.entries();
0356     QCOMPARE(list2.size(), 2); // Is a message actually gone?
0357 
0358     quint64 newOffsetSecondMessage = list.last().messageOffset() - list.at(1).messageOffset();
0359 
0360     QCOMPARE(list2.first().messageOffset(), static_cast<quint64>(0));
0361     QCOMPARE(list2.last().messageOffset(), newOffsetSecondMessage);
0362 
0363     // Second test: Delete the first two (the last message have to be moved).
0364     removeTestFile();
0365 
0366     QVERIFY(mbox1.load(fileName()));
0367     mbox1.appendMessage(mMail1);
0368     mbox1.appendMessage(mMail1);
0369     mbox1.appendMessage(mMail1);
0370     QVERIFY(mbox1.save());
0371 
0372     list = mbox1.entries();
0373 
0374     mbox1.purge(MBoxEntry::List() << list.at(0) << list.at(1));
0375     QVERIFY(mbox2.load(fileName()));
0376     list2 = mbox2.entries();
0377     QCOMPARE(list2.size(), 1); // Are the messages actually gone?
0378     QCOMPARE(list2.first().messageOffset(), static_cast<quint64>(0));
0379 
0380     // Third test: Delete all messages.
0381     removeTestFile();
0382 
0383     QVERIFY(mbox1.load(fileName()));
0384     mbox1.appendMessage(mMail1);
0385     mbox1.appendMessage(mMail1);
0386     mbox1.appendMessage(mMail1);
0387     QVERIFY(mbox1.save());
0388 
0389     list = mbox1.entries();
0390 
0391     mbox1.purge(MBoxEntry::List() << list.at(0) << list.at(1) << list.at(2));
0392     QVERIFY(mbox2.load(fileName()));
0393     list2 = mbox2.entries();
0394     QCOMPARE(list2.size(), 0); // Are the messages actually gone?
0395 }
0396 
0397 void MboxTest::testLockTimeout()
0398 {
0399     MBox mbox;
0400     mbox.load(fileName());
0401     mbox.setLockType(MBox::None);
0402     mbox.setUnlockTimeout(1000);
0403 
0404     QVERIFY(!mbox.locked());
0405     mbox.lock();
0406     QVERIFY(mbox.locked());
0407 
0408     QTest::qWait(1500);
0409     QVERIFY(!mbox.locked());
0410 }
0411 
0412 void MboxTest::testHeaders()
0413 {
0414     MBox mbox;
0415     QVERIFY(mbox.setLockType(MBox::None));
0416     QVERIFY(mbox.load(fileName()));
0417     mbox.appendMessage(mMail1);
0418     mbox.appendMessage(mMail2);
0419     QVERIFY(mbox.save());
0420 
0421     const MBoxEntry::List list = mbox.entries();
0422 
0423     for (const MBoxEntry &msgInfo : list) {
0424         const QByteArray header = mbox.readMessageHeaders(msgInfo);
0425         QVERIFY(!header.isEmpty());
0426 
0427         KMime::Message *message = mbox.readMessage(msgInfo);
0428         QVERIFY(message != nullptr);
0429 
0430         auto headers = new KMime::Message();
0431         headers->setHead(KMime::CRLFtoLF(header));
0432         headers->parse();
0433 
0434         QCOMPARE(message->messageID()->identifier(), headers->messageID()->identifier());
0435         QCOMPARE(message->subject()->as7BitString(), headers->subject()->as7BitString());
0436         QCOMPARE(message->to()->as7BitString(), headers->to()->as7BitString());
0437         QCOMPARE(message->from()->as7BitString(), headers->from()->as7BitString());
0438 
0439         delete message;
0440         delete headers;
0441     }
0442 }
0443 
0444 void MboxTest::testReadOnlyMbox()
0445 {
0446     { // create a non-empty mbox file
0447         MBox mbox;
0448         QVERIFY(mbox.load(fileName()));
0449         mbox.appendMessage(mMail1);
0450         mbox.appendMessage(mMail2);
0451         QVERIFY(mbox.save());
0452     }
0453 
0454     QFile::Permissions perm = QFile::permissions(fileName());
0455     QFile::setPermissions(fileName(), QFile::ReadOwner);
0456 
0457     MBox mbox;
0458     QVERIFY(mbox.load(fileName()));
0459     QVERIFY(mbox.isReadOnly());
0460 
0461     // this still works since we could save it to a different file
0462     MBoxEntry entry = mbox.appendMessage(mMail1);
0463 
0464     QVERIFY(!mbox.save()); // original mbox is read-only
0465 
0466     // reading back the appended message (from memory)
0467     QByteArray msg = mbox.readRawMessage(entry);
0468     QVERIFY(!msg.isEmpty());
0469 
0470     // read first message from disk
0471     MBoxEntry::List list = mbox.entries();
0472     msg = mbox.readRawMessage(list.at(0));
0473     QVERIFY(!msg.isEmpty());
0474 
0475     QVERIFY(!mbox.purge(list)); // original mbox is read-only
0476 
0477     QString tmpSaved = mTempDir->path() + QLatin1StringView("tempSaved.mbox");
0478     QVERIFY(mbox.save(tmpSaved)); // other mbox file can be written
0479 
0480     MBox savedMbox;
0481     savedMbox.setReadOnly();
0482     QVERIFY(savedMbox.isReadOnly());
0483     QVERIFY(savedMbox.load(tmpSaved));
0484     QVERIFY(!savedMbox.save());
0485 
0486     // set back to initial permissions
0487     QFile::setPermissions(fileName(), perm);
0488 }
0489 
0490 void MboxTest::cleanupTestCase()
0491 {
0492     mTempDir->remove();
0493     delete mTempDir;
0494 }
0495 
0496 //---------------------------------------------------------------------
0497 
0498 ThreadFillsMBox::ThreadFillsMBox(const QString &fileName)
0499 {
0500     mbox = new MBox;
0501     QVERIFY(mbox->load(fileName));
0502     mbox->setLockType(MBox::ProcmailLockfile);
0503     mbox->lock();
0504 }
0505 
0506 void ThreadFillsMBox::run()
0507 {
0508     QTest::qSleep(2000);
0509 
0510     QFile file(mbox->fileName());
0511     file.open(QIODevice::WriteOnly | QIODevice::Append);
0512 
0513     QByteArray message = KMime::CRLFtoLF(sEntry1);
0514     file.write(QByteArray("From test@local.local ") + QDateTime::currentDateTime().toString(Qt::ISODate).toUtf8() + "\n");
0515     file.write(message);
0516     file.write("\n\n");
0517     file.close();
0518 
0519     mbox->unlock();
0520     delete mbox;
0521 }
0522 
0523 #include "moc_mboxtest.cpp"